operator_template.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. /*
  2. Sub-Store operator (production-friendly)
  3. - Pull dynamic value from router/runtime and server/runtime JSON
  4. - Replace vmess server field for matched nodes
  5. */
  6. const ROUTER_VALUE_JSON_URL = "http://192.168.50.1:8080/current_ip.json";
  7. const SERVER_VALUE_JSON_URL = "https://git.dewofaurora.de/aurora/vmess-domain-rotator/raw/runtime-state/runtime/current_domain.json";
  8. const ROUTER_MIN_SPEED_MB_PER_S = 5;
  9. const NODE_NAME_REGEX = /(argo|cf|vm|优选)/i;
  10. const CACHE_KEY = "vmess-domain-rotator:current";
  11. const CACHE_TTL_MS = 5 * 60 * 1000;
  12. const JSON_VALUE_KEYS = ["domain", "ip"];
  13. async function fetchRuntimeJson(url, sourceName) {
  14. const $ = $substore;
  15. const { body, statusCode } = await $.http.get({
  16. url,
  17. headers: {
  18. Accept: "application/json",
  19. "Cache-Control": "no-cache"
  20. },
  21. timeout: 5000
  22. });
  23. if (statusCode < 200 || statusCode >= 300) {
  24. throw new Error(`${sourceName} http status ${statusCode}`);
  25. }
  26. let obj;
  27. try {
  28. obj = JSON.parse(body || "{}");
  29. } catch (e) {
  30. throw new Error(`${sourceName} invalid json: ${e.message}`);
  31. }
  32. return obj;
  33. }
  34. function extractValue(obj, sourceName) {
  35. let value = "";
  36. for (const key of JSON_VALUE_KEYS) {
  37. value = String(obj[key] || "").trim().toLowerCase();
  38. if (value) break;
  39. }
  40. if (!value) {
  41. throw new Error(`${sourceName} empty value field, expected one of: ${JSON_VALUE_KEYS.join(", ")}`);
  42. }
  43. return value;
  44. }
  45. function parseRouterSpeedMBps(routerObj) {
  46. const raw = String(routerObj?.top_candidates?.[0]?.download_speed || "").trim();
  47. if (!raw) {
  48. return { mbPerSec: null, raw: "", status: "missing", unit: "" };
  49. }
  50. const compact = raw.replace(/\s+/g, "");
  51. const normalized = compact.toLowerCase();
  52. const match = compact.match(/([0-9]+(?:\.[0-9]+)?)/);
  53. if (!match) {
  54. return { mbPerSec: null, raw, status: "invalid", unit: "" };
  55. }
  56. const numeric = Number(match[1]);
  57. if (!Number.isFinite(numeric) || numeric < 0) {
  58. return { mbPerSec: null, raw, status: "invalid", unit: "" };
  59. }
  60. let mbPerSec = numeric;
  61. let unit = "MB/s";
  62. if (compact.includes("MB/s")) {
  63. mbPerSec = numeric;
  64. unit = "MB/s";
  65. } else if (compact.includes("KB/s")) {
  66. mbPerSec = numeric / 1024;
  67. unit = "KB/s";
  68. } else if (compact.includes("GB/s")) {
  69. mbPerSec = numeric * 1024;
  70. unit = "GB/s";
  71. } else if (compact.includes("B/s")) {
  72. mbPerSec = numeric / (1024 * 1024);
  73. unit = "B/s";
  74. } else if (normalized.includes("mbps") || normalized.includes("mb/s")) {
  75. mbPerSec = numeric / 8;
  76. unit = "Mbps";
  77. } else if (normalized.includes("gbps") || normalized.includes("gb/s")) {
  78. mbPerSec = (numeric * 1024) / 8;
  79. unit = "Gbps";
  80. } else if (normalized.includes("kbps") || normalized.includes("kb/s")) {
  81. mbPerSec = (numeric / 1024) / 8;
  82. unit = "Kbps";
  83. } else if (normalized.includes("bps") || normalized.includes("b/s")) {
  84. mbPerSec = (numeric / (1024 * 1024)) / 8;
  85. unit = "b/s";
  86. }
  87. return { mbPerSec, raw, status: "ok", unit };
  88. }
  89. async function selectValueRouterFirst() {
  90. const telemetry = {
  91. router: { ok: false, value: "", status: "", sourceType: "", error: "", speedRaw: "", speedMBps: null, speedParse: "missing", speedUnit: "" },
  92. server: { ok: false, value: "", status: "", sourceType: "", error: "" }
  93. };
  94. try {
  95. const routerObj = await fetchRuntimeJson(ROUTER_VALUE_JSON_URL, "router");
  96. const routerValue = extractValue(routerObj, "router");
  97. const speed = parseRouterSpeedMBps(routerObj);
  98. telemetry.router.ok = true;
  99. telemetry.router.value = routerValue;
  100. telemetry.router.status = String(routerObj?.status || "");
  101. telemetry.router.sourceType = String(routerObj?.source_type || "");
  102. telemetry.router.speedRaw = speed.raw;
  103. telemetry.router.speedMBps = speed.mbPerSec;
  104. telemetry.router.speedParse = speed.status;
  105. telemetry.router.speedUnit = speed.unit;
  106. } catch (e) {
  107. telemetry.router.error = String(e.message || e);
  108. }
  109. try {
  110. const serverObj = await fetchRuntimeJson(SERVER_VALUE_JSON_URL, "server");
  111. const serverValue = extractValue(serverObj, "server");
  112. telemetry.server.ok = true;
  113. telemetry.server.value = serverValue;
  114. telemetry.server.status = String(serverObj?.status || "");
  115. telemetry.server.sourceType = String(serverObj?.source_type || "");
  116. } catch (e) {
  117. telemetry.server.error = String(e.message || e);
  118. }
  119. if (!telemetry.router.ok && !telemetry.server.ok) {
  120. throw new Error(`router failed: ${telemetry.router.error}; server failed: ${telemetry.server.error}`);
  121. }
  122. if (!telemetry.router.ok && telemetry.server.ok) {
  123. return { value: telemetry.server.value, chosenSource: "server", reason: "router_fail", telemetry };
  124. }
  125. if (telemetry.router.ok && !telemetry.server.ok) {
  126. return { value: telemetry.router.value, chosenSource: "router", reason: "server_fail", telemetry };
  127. }
  128. if (telemetry.router.speedParse === "ok" && telemetry.router.speedMBps < ROUTER_MIN_SPEED_MB_PER_S) {
  129. return { value: telemetry.server.value, chosenSource: "server", reason: "router_low_speed", telemetry };
  130. }
  131. return { value: telemetry.router.value, chosenSource: "router", reason: "router_preferred", telemetry };
  132. }
  133. async function operator(proxies = [], targetPlatform, context) {
  134. const cache = scriptResourceCache;
  135. let value = cache.get(CACHE_KEY);
  136. let decision = null;
  137. if (!value) {
  138. try {
  139. decision = await selectValueRouterFirst();
  140. value = decision.value;
  141. cache.set(CACHE_KEY, value, CACHE_TTL_MS);
  142. } catch (e) {
  143. console.log(`[vmess-domain-rotator] fetch failed: ${e.message}`);
  144. return proxies;
  145. }
  146. }
  147. let updated = 0;
  148. for (const p of proxies) {
  149. if (!p || p.type !== "vmess") continue;
  150. if (!NODE_NAME_REGEX.test(p.name || "")) continue;
  151. if (p.server !== value) {
  152. p.server = value;
  153. updated += 1;
  154. }
  155. }
  156. if (decision) {
  157. const t = decision.telemetry;
  158. console.log(
  159. `[vmess-domain-rotator] chosen=${decision.chosenSource}, reason=${decision.reason}, router_status=${t.router.status || "n/a"}, server_status=${t.server.status || "n/a"}, router_speed_raw=${t.router.speedRaw || "n/a"}, router_speed_mb_per_s=${t.router.speedMBps == null ? "n/a" : t.router.speedMBps.toFixed(3)}, speed_parse=${t.router.speedParse}, threshold_mb_per_s=${ROUTER_MIN_SPEED_MB_PER_S}, value=${value}, updated=${updated}, total=${proxies.length}, target=${targetPlatform}`
  160. );
  161. } else {
  162. console.log(`[vmess-domain-rotator] chosen=cache, value=${value}, updated=${updated}, total=${proxies.length}, target=${targetPlatform}`);
  163. }
  164. return proxies;
  165. }