operator_template.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  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 VALUE_SOURCE_MODE = "default"; // default | server_only | router_only
  10. const NODE_NAME_REGEX = /(argo|cf|vm|优选)/i;
  11. const CACHE_KEY = "vmess-domain-rotator:current";
  12. const CACHE_TTL_MS = 5 * 60 * 1000;
  13. const JSON_VALUE_KEYS = ["domain", "ip"];
  14. async function fetchRuntimeJson(url, sourceName) {
  15. const $ = $substore;
  16. const { body, statusCode } = await $.http.get({
  17. url,
  18. headers: {
  19. Accept: "application/json",
  20. "Cache-Control": "no-cache"
  21. },
  22. timeout: 5000
  23. });
  24. if (statusCode < 200 || statusCode >= 300) {
  25. throw new Error(`${sourceName} http status ${statusCode}`);
  26. }
  27. let obj;
  28. try {
  29. obj = JSON.parse(body || "{}");
  30. } catch (e) {
  31. throw new Error(`${sourceName} invalid json: ${e.message}`);
  32. }
  33. return obj;
  34. }
  35. function extractValue(obj, sourceName) {
  36. let value = "";
  37. for (const key of JSON_VALUE_KEYS) {
  38. value = String(obj[key] || "").trim().toLowerCase();
  39. if (value) break;
  40. }
  41. if (!value) {
  42. throw new Error(`${sourceName} empty value field, expected one of: ${JSON_VALUE_KEYS.join(", ")}`);
  43. }
  44. return value;
  45. }
  46. function parseRouterSpeedMBps(routerObj) {
  47. const raw = String(routerObj?.top_candidates?.[0]?.download_speed || "").trim();
  48. if (!raw) {
  49. return { mbPerSec: null, raw: "", status: "missing", unit: "" };
  50. }
  51. const compact = raw.replace(/\s+/g, "");
  52. const normalized = compact.toLowerCase();
  53. const match = compact.match(/([0-9]+(?:\.[0-9]+)?)/);
  54. if (!match) {
  55. return { mbPerSec: null, raw, status: "invalid", unit: "" };
  56. }
  57. const numeric = Number(match[1]);
  58. if (!Number.isFinite(numeric) || numeric < 0) {
  59. return { mbPerSec: null, raw, status: "invalid", unit: "" };
  60. }
  61. let mbPerSec = numeric;
  62. let unit = "MB/s";
  63. if (compact.includes("MB/s")) {
  64. mbPerSec = numeric;
  65. unit = "MB/s";
  66. } else if (compact.includes("KB/s")) {
  67. mbPerSec = numeric / 1024;
  68. unit = "KB/s";
  69. } else if (compact.includes("GB/s")) {
  70. mbPerSec = numeric * 1024;
  71. unit = "GB/s";
  72. } else if (compact.includes("B/s")) {
  73. mbPerSec = numeric / (1024 * 1024);
  74. unit = "B/s";
  75. } else if (normalized.includes("mbps") || normalized.includes("mb/s")) {
  76. mbPerSec = numeric / 8;
  77. unit = "Mbps";
  78. } else if (normalized.includes("gbps") || normalized.includes("gb/s")) {
  79. mbPerSec = (numeric * 1024) / 8;
  80. unit = "Gbps";
  81. } else if (normalized.includes("kbps") || normalized.includes("kb/s")) {
  82. mbPerSec = (numeric / 1024) / 8;
  83. unit = "Kbps";
  84. } else if (normalized.includes("bps") || normalized.includes("b/s")) {
  85. mbPerSec = (numeric / (1024 * 1024)) / 8;
  86. unit = "b/s";
  87. }
  88. return { mbPerSec, raw, status: "ok", unit };
  89. }
  90. async function selectValueRouterFirst() {
  91. const telemetry = {
  92. router: { ok: false, value: "", status: "", sourceType: "", error: "", speedRaw: "", speedMBps: null, speedParse: "missing", speedUnit: "" },
  93. server: { ok: false, value: "", status: "", sourceType: "", error: "" }
  94. };
  95. try {
  96. const routerObj = await fetchRuntimeJson(ROUTER_VALUE_JSON_URL, "router");
  97. const routerValue = extractValue(routerObj, "router");
  98. const speed = parseRouterSpeedMBps(routerObj);
  99. telemetry.router.ok = true;
  100. telemetry.router.value = routerValue;
  101. telemetry.router.status = String(routerObj?.status || "");
  102. telemetry.router.sourceType = String(routerObj?.source_type || "");
  103. telemetry.router.speedRaw = speed.raw;
  104. telemetry.router.speedMBps = speed.mbPerSec;
  105. telemetry.router.speedParse = speed.status;
  106. telemetry.router.speedUnit = speed.unit;
  107. } catch (e) {
  108. telemetry.router.error = String(e.message || e);
  109. }
  110. try {
  111. const serverObj = await fetchRuntimeJson(SERVER_VALUE_JSON_URL, "server");
  112. const serverValue = extractValue(serverObj, "server");
  113. telemetry.server.ok = true;
  114. telemetry.server.value = serverValue;
  115. telemetry.server.status = String(serverObj?.status || "");
  116. telemetry.server.sourceType = String(serverObj?.source_type || "");
  117. } catch (e) {
  118. telemetry.server.error = String(e.message || e);
  119. }
  120. if (!telemetry.router.ok && !telemetry.server.ok) {
  121. throw new Error(`router failed: ${telemetry.router.error}; server failed: ${telemetry.server.error}`);
  122. }
  123. if (!telemetry.router.ok && telemetry.server.ok) {
  124. return { value: telemetry.server.value, chosenSource: "server", reason: "router_fail", telemetry };
  125. }
  126. if (telemetry.router.ok && !telemetry.server.ok) {
  127. return { value: telemetry.router.value, chosenSource: "router", reason: "server_fail", telemetry };
  128. }
  129. if (telemetry.router.speedParse === "ok" && telemetry.router.speedMBps < ROUTER_MIN_SPEED_MB_PER_S) {
  130. return { value: telemetry.server.value, chosenSource: "server", reason: "router_low_speed", telemetry };
  131. }
  132. return { value: telemetry.router.value, chosenSource: "router", reason: "router_preferred", telemetry };
  133. }
  134. async function selectValueByMode() {
  135. if (VALUE_SOURCE_MODE === "default") {
  136. return selectValueRouterFirst();
  137. }
  138. if (VALUE_SOURCE_MODE === "server_only") {
  139. const serverObj = await fetchRuntimeJson(SERVER_VALUE_JSON_URL, "server");
  140. const serverValue = extractValue(serverObj, "server");
  141. return {
  142. value: serverValue,
  143. chosenSource: "server",
  144. reason: "server_only_mode",
  145. telemetry: {
  146. router: { ok: false, value: "", status: "", sourceType: "", error: "skipped_by_mode", speedRaw: "", speedMBps: null, speedParse: "missing", speedUnit: "" },
  147. server: { ok: true, value: serverValue, status: String(serverObj?.status || ""), sourceType: String(serverObj?.source_type || ""), error: "" }
  148. }
  149. };
  150. }
  151. if (VALUE_SOURCE_MODE === "router_only") {
  152. const routerObj = await fetchRuntimeJson(ROUTER_VALUE_JSON_URL, "router");
  153. const routerValue = extractValue(routerObj, "router");
  154. const speed = parseRouterSpeedMBps(routerObj);
  155. return {
  156. value: routerValue,
  157. chosenSource: "router",
  158. reason: "router_only_mode",
  159. telemetry: {
  160. router: {
  161. ok: true,
  162. value: routerValue,
  163. status: String(routerObj?.status || ""),
  164. sourceType: String(routerObj?.source_type || ""),
  165. error: "",
  166. speedRaw: speed.raw,
  167. speedMBps: speed.mbPerSec,
  168. speedParse: speed.status,
  169. speedUnit: speed.unit
  170. },
  171. server: { ok: false, value: "", status: "", sourceType: "", error: "skipped_by_mode" }
  172. }
  173. };
  174. }
  175. throw new Error(`invalid VALUE_SOURCE_MODE=${VALUE_SOURCE_MODE}, expected: default | server_only | router_only`);
  176. }
  177. async function operator(proxies = [], targetPlatform, context) {
  178. const cache = scriptResourceCache;
  179. let value = cache.get(CACHE_KEY);
  180. let decision = null;
  181. if (!value) {
  182. try {
  183. decision = await selectValueByMode();
  184. value = decision.value;
  185. cache.set(CACHE_KEY, value, CACHE_TTL_MS);
  186. } catch (e) {
  187. console.log(`[vmess-domain-rotator] fetch failed: ${e.message}`);
  188. return proxies;
  189. }
  190. }
  191. let updated = 0;
  192. for (const p of proxies) {
  193. if (!p || p.type !== "vmess") continue;
  194. if (!NODE_NAME_REGEX.test(p.name || "")) continue;
  195. if (p.server !== value) {
  196. p.server = value;
  197. updated += 1;
  198. }
  199. }
  200. if (decision) {
  201. const t = decision.telemetry;
  202. console.log(
  203. `[vmess-domain-rotator] mode=${VALUE_SOURCE_MODE}, 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}`
  204. );
  205. } else {
  206. console.log(`[vmess-domain-rotator] mode=${VALUE_SOURCE_MODE}, chosen=cache, value=${value}, updated=${updated}, total=${proxies.length}, target=${targetPlatform}`);
  207. }
  208. return proxies;
  209. }