operator_template.js 9.7 KB

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