useXraySetting.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. // Drives the xray page's fetch / dirty / save lifecycle. The Go side
  2. // returns the live xraySetting (the full JSON config), the inboundTags
  3. // list, and a few sidecar values (clientReverseTags, outboundTestUrl)
  4. // the structured tabs need. We keep the JSON as a string here — pretty-
  5. // printed for the textarea; tabs that want a parsed view can JSON.parse
  6. // it themselves.
  7. import { onMounted, onUnmounted, ref, watch } from 'vue';
  8. import { HttpUtil, PromiseUtil } from '@/utils';
  9. const DIRTY_POLL_MS = 1000;
  10. // Hoists the parsed `templateSettings` alongside the JSON string so
  11. // structured tabs (Basics/Routing/Outbounds/etc.) can mutate fields
  12. // directly while the Advanced (JSON) tab edits the same data as text.
  13. // We keep both in sync with two cooperating watches:
  14. // • mutating templateSettings re-stringifies into xraySetting;
  15. // • editing the JSON text re-parses into templateSettings (only on
  16. // valid JSON — invalid edits leave templateSettings untouched
  17. // so the structured tabs don't blow up while the user types).
  18. let syncing = false;
  19. export function useXraySetting() {
  20. const fetched = ref(false);
  21. const spinning = ref(false);
  22. const saveDisabled = ref(true);
  23. // Holds a user-facing message when fetchAll fails; lets the page
  24. // render an error UI instead of an endless spinner.
  25. const fetchError = ref('');
  26. const xraySetting = ref('');
  27. const oldXraySetting = ref('');
  28. // Parsed mirror — null until first successful fetch / parse.
  29. const templateSettings = ref(null);
  30. const outboundTestUrl = ref('https://www.google.com/generate_204');
  31. const oldOutboundTestUrl = ref('');
  32. const inboundTags = ref([]);
  33. const clientReverseTags = ref([]);
  34. const restartResult = ref('');
  35. // Outbounds tab data — traffic stats + per-row test state. Test
  36. // states are keyed by outbound index (sparse object), each entry
  37. // is `{ testing, result }` where result is the wire response from
  38. // /panel/xray/testOutbound or null while the test is in flight.
  39. const outboundsTraffic = ref([]);
  40. const outboundTestStates = ref({});
  41. async function fetchAll() {
  42. fetchError.value = '';
  43. const msg = await HttpUtil.post('/panel/xray/');
  44. if (!msg?.success) {
  45. fetchError.value = msg?.msg || 'Failed to load xray config';
  46. // Mark as fetched so the spinner clears and the error UI renders.
  47. fetched.value = true;
  48. return;
  49. }
  50. let obj;
  51. try {
  52. obj = JSON.parse(msg.obj);
  53. } catch (e) {
  54. fetchError.value = `Malformed xray config response: ${e?.message || e}`;
  55. fetched.value = true;
  56. return;
  57. }
  58. const pretty = JSON.stringify(obj.xraySetting, null, 2);
  59. syncing = true;
  60. xraySetting.value = pretty;
  61. oldXraySetting.value = pretty;
  62. templateSettings.value = obj.xraySetting;
  63. syncing = false;
  64. inboundTags.value = obj.inboundTags || [];
  65. clientReverseTags.value = obj.clientReverseTags || [];
  66. outboundTestUrl.value = obj.outboundTestUrl || 'https://www.google.com/generate_204';
  67. oldOutboundTestUrl.value = outboundTestUrl.value;
  68. fetched.value = true;
  69. saveDisabled.value = true;
  70. }
  71. // Structured tabs mutate templateSettings deeply. Re-stringify on
  72. // change so the Advanced JSON view + the dirty-poll see the edits.
  73. watch(
  74. templateSettings,
  75. (next) => {
  76. if (syncing || !next) return;
  77. syncing = true;
  78. try {
  79. xraySetting.value = JSON.stringify(next, null, 2);
  80. } finally {
  81. syncing = false;
  82. }
  83. },
  84. { deep: true },
  85. );
  86. // Advanced JSON edits — only refresh templateSettings when the text
  87. // parses, so structured tabs stay readable mid-edit.
  88. watch(xraySetting, (next) => {
  89. if (syncing) return;
  90. try {
  91. const parsed = JSON.parse(next);
  92. syncing = true;
  93. try {
  94. templateSettings.value = parsed;
  95. } finally {
  96. syncing = false;
  97. }
  98. } catch (_e) { /* ignore — wait for user to finish */ }
  99. });
  100. async function saveAll() {
  101. spinning.value = true;
  102. try {
  103. const msg = await HttpUtil.post('/panel/xray/update', {
  104. xraySetting: xraySetting.value,
  105. outboundTestUrl: outboundTestUrl.value || 'https://www.google.com/generate_204',
  106. });
  107. if (msg?.success) await fetchAll();
  108. } finally {
  109. spinning.value = false;
  110. }
  111. }
  112. async function fetchOutboundsTraffic() {
  113. const msg = await HttpUtil.get('/panel/xray/getOutboundsTraffic');
  114. if (msg?.success) outboundsTraffic.value = msg.obj || [];
  115. }
  116. async function resetOutboundsTraffic(tag) {
  117. const msg = await HttpUtil.post('/panel/xray/resetOutboundsTraffic', { tag });
  118. if (msg?.success) await fetchOutboundsTraffic();
  119. }
  120. // Merges a WebSocket `outbounds` event into outboundsTraffic in place.
  121. // The xray traffic job pushes the full snapshot every ~10s so the user
  122. // doesn't have to click the (now-removed) refresh button.
  123. function applyOutboundsEvent(payload) {
  124. if (Array.isArray(payload)) outboundsTraffic.value = payload;
  125. }
  126. async function testOutbound(index, outbound) {
  127. if (!outbound) return null;
  128. if (!outboundTestStates.value[index]) outboundTestStates.value[index] = {};
  129. outboundTestStates.value[index] = { testing: true, result: null };
  130. try {
  131. const msg = await HttpUtil.post('/panel/xray/testOutbound', {
  132. outbound: JSON.stringify(outbound),
  133. allOutbounds: JSON.stringify(templateSettings.value?.outbounds || []),
  134. });
  135. if (msg?.success) {
  136. outboundTestStates.value[index] = { testing: false, result: msg.obj };
  137. return msg.obj;
  138. }
  139. outboundTestStates.value[index] = {
  140. testing: false,
  141. result: { success: false, error: msg?.msg || 'Unknown error' },
  142. };
  143. } catch (e) {
  144. outboundTestStates.value[index] = {
  145. testing: false,
  146. result: { success: false, error: String(e) },
  147. };
  148. }
  149. return null;
  150. }
  151. async function resetToDefault() {
  152. spinning.value = true;
  153. try {
  154. const msg = await HttpUtil.get('/panel/setting/getDefaultJsonConfig');
  155. if (msg?.success) {
  156. // Mutate templateSettings — the watch above re-stringifies into
  157. // xraySetting so the Advanced JSON tab and dirty-poll see it.
  158. templateSettings.value = JSON.parse(JSON.stringify(msg.obj));
  159. }
  160. } finally {
  161. spinning.value = false;
  162. }
  163. }
  164. async function restartXray() {
  165. spinning.value = true;
  166. try {
  167. const msg = await HttpUtil.post('/panel/api/server/restartXrayService');
  168. if (msg?.success) {
  169. // Match legacy: short pause, then poll for the result blob so
  170. // the popover surfaces any startup error from the new process.
  171. await PromiseUtil.sleep(500);
  172. const r = await HttpUtil.get('/panel/xray/getXrayResult');
  173. if (r?.success) restartResult.value = r.obj || '';
  174. }
  175. } finally {
  176. spinning.value = false;
  177. }
  178. }
  179. // Same 1s busy-loop pattern the settings page uses — keep it cheap
  180. // and consistent. Real work (the JSON diff) is just a string compare.
  181. let timer = null;
  182. function startDirtyPoll() {
  183. if (timer != null) return;
  184. timer = setInterval(() => {
  185. saveDisabled.value =
  186. oldXraySetting.value === xraySetting.value
  187. && oldOutboundTestUrl.value === outboundTestUrl.value;
  188. }, DIRTY_POLL_MS);
  189. }
  190. function stopDirtyPoll() {
  191. if (timer != null) {
  192. clearInterval(timer);
  193. timer = null;
  194. }
  195. }
  196. onMounted(() => {
  197. fetchAll();
  198. fetchOutboundsTraffic();
  199. startDirtyPoll();
  200. });
  201. onUnmounted(stopDirtyPoll);
  202. return {
  203. fetched,
  204. spinning,
  205. saveDisabled,
  206. fetchError,
  207. xraySetting,
  208. templateSettings,
  209. outboundTestUrl,
  210. inboundTags,
  211. clientReverseTags,
  212. restartResult,
  213. outboundsTraffic,
  214. outboundTestStates,
  215. fetchAll,
  216. fetchOutboundsTraffic,
  217. resetOutboundsTraffic,
  218. applyOutboundsEvent,
  219. testOutbound,
  220. saveAll,
  221. resetToDefault,
  222. restartXray,
  223. };
  224. }