operator_template.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  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 normalized = raw.toLowerCase().replace(/\s+/g, "");
  51. const match = normalized.match(/([0-9]+(?:\.[0-9]+)?)/);
  52. if (!match) {
  53. return { mbPerSec: null, raw, status: "invalid", unit: "" };
  54. }
  55. const numeric = Number(match[1]);
  56. if (!Number.isFinite(numeric) || numeric < 0) {
  57. return { mbPerSec: null, raw, status: "invalid", unit: "" };
  58. }
  59. let mbPerSec = numeric;
  60. let unit = "unknown";
  61. if (normalized.includes("gb/s") || normalized.includes("g/s") || normalized.endsWith("gb") || normalized.endsWith("g")) {
  62. mbPerSec = numeric * 1024;
  63. unit = "gb/s";
  64. } else if (normalized.includes("mb/s") || normalized.includes("m/s") || normalized.endsWith("mb") || normalized.endsWith("m")) {
  65. mbPerSec = numeric;
  66. unit = "mb/s";
  67. } else if (normalized.includes("kb/s") || normalized.includes("k/s") || normalized.endsWith("kb") || normalized.endsWith("k")) {
  68. mbPerSec = numeric / 1024;
  69. unit = "kb/s";
  70. } else if (normalized.includes("b/s")) {
  71. mbPerSec = numeric / (1024 * 1024);
  72. unit = "b/s";
  73. }
  74. const status = unit === "unknown" ? "unknown_unit" : "ok";
  75. return { mbPerSec, raw, status, unit };
  76. }
  77. async function selectValueRouterFirst() {
  78. const telemetry = {
  79. router: { ok: false, value: "", status: "", sourceType: "", error: "", speedRaw: "", speedMBps: null, speedParse: "missing", speedUnit: "" },
  80. server: { ok: false, value: "", status: "", sourceType: "", error: "" }
  81. };
  82. try {
  83. const routerObj = await fetchRuntimeJson(ROUTER_VALUE_JSON_URL, "router");
  84. const routerValue = extractValue(routerObj, "router");
  85. const speed = parseRouterSpeedMBps(routerObj);
  86. telemetry.router.ok = true;
  87. telemetry.router.value = routerValue;
  88. telemetry.router.status = String(routerObj?.status || "");
  89. telemetry.router.sourceType = String(routerObj?.source_type || "");
  90. telemetry.router.speedRaw = speed.raw;
  91. telemetry.router.speedMBps = speed.mbPerSec;
  92. telemetry.router.speedParse = speed.status;
  93. telemetry.router.speedUnit = speed.unit;
  94. } catch (e) {
  95. telemetry.router.error = String(e.message || e);
  96. }
  97. try {
  98. const serverObj = await fetchRuntimeJson(SERVER_VALUE_JSON_URL, "server");
  99. const serverValue = extractValue(serverObj, "server");
  100. telemetry.server.ok = true;
  101. telemetry.server.value = serverValue;
  102. telemetry.server.status = String(serverObj?.status || "");
  103. telemetry.server.sourceType = String(serverObj?.source_type || "");
  104. } catch (e) {
  105. telemetry.server.error = String(e.message || e);
  106. }
  107. if (!telemetry.router.ok && !telemetry.server.ok) {
  108. throw new Error(`router failed: ${telemetry.router.error}; server failed: ${telemetry.server.error}`);
  109. }
  110. if (!telemetry.router.ok && telemetry.server.ok) {
  111. return { value: telemetry.server.value, chosenSource: "server", reason: "router_fail", telemetry };
  112. }
  113. if (telemetry.router.ok && !telemetry.server.ok) {
  114. return { value: telemetry.router.value, chosenSource: "router", reason: "server_fail", telemetry };
  115. }
  116. if (telemetry.router.speedParse === "ok" && telemetry.router.speedMBps < ROUTER_MIN_SPEED_MB_PER_S) {
  117. return { value: telemetry.server.value, chosenSource: "server", reason: "router_low_speed", telemetry };
  118. }
  119. return { value: telemetry.router.value, chosenSource: "router", reason: "router_preferred", telemetry };
  120. }
  121. async function operator(proxies = [], targetPlatform, context) {
  122. const cache = scriptResourceCache;
  123. let value = cache.get(CACHE_KEY);
  124. let decision = null;
  125. if (!value) {
  126. try {
  127. decision = await selectValueRouterFirst();
  128. value = decision.value;
  129. cache.set(CACHE_KEY, value, CACHE_TTL_MS);
  130. } catch (e) {
  131. console.log(`[vmess-domain-rotator] fetch failed: ${e.message}`);
  132. return proxies;
  133. }
  134. }
  135. let updated = 0;
  136. for (const p of proxies) {
  137. if (!p || p.type !== "vmess") continue;
  138. if (!NODE_NAME_REGEX.test(p.name || "")) continue;
  139. if (p.server !== value) {
  140. p.server = value;
  141. updated += 1;
  142. }
  143. }
  144. if (decision) {
  145. const t = decision.telemetry;
  146. console.log(
  147. `[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}`
  148. );
  149. } else {
  150. console.log(`[vmess-domain-rotator] chosen=cache, value=${value}, updated=${updated}, total=${proxies.length}, target=${targetPlatform}`);
  151. }
  152. return proxies;
  153. }