/* Sub-Store operator (production-friendly) - Pull dynamic value from router/runtime and server/runtime JSON - Replace vmess server field for matched nodes */ const ROUTER_VALUE_JSON_URL = "http://:8080/current_ip.json"; const SERVER_VALUE_JSON_URL = "https://git.dewofaurora.de/aurora/vmess-domain-rotator/raw/runtime-state/runtime/current_domain.json"; const ROUTER_MIN_SPEED_MBPS = 5; const NODE_NAME_REGEX = /(argo|cf|vm|优选)/i; const CACHE_KEY = "vmess-domain-rotator:current"; const CACHE_TTL_MS = 5 * 60 * 1000; const JSON_VALUE_KEYS = ["domain", "ip"]; async function fetchRuntimeJson(url, sourceName) { const $ = $substore; const { body, statusCode } = await $.http.get({ url, headers: { Accept: "application/json", "Cache-Control": "no-cache" }, timeout: 5000 }); if (statusCode < 200 || statusCode >= 300) { throw new Error(`${sourceName} http status ${statusCode}`); } let obj; try { obj = JSON.parse(body || "{}"); } catch (e) { throw new Error(`${sourceName} invalid json: ${e.message}`); } return obj; } function extractValue(obj, sourceName) { let value = ""; for (const key of JSON_VALUE_KEYS) { value = String(obj[key] || "").trim().toLowerCase(); if (value) break; } if (!value) { throw new Error(`${sourceName} empty value field, expected one of: ${JSON_VALUE_KEYS.join(", ")}`); } return value; } function parseRouterSpeedMbps(routerObj) { const raw = String(routerObj?.top_candidates?.[0]?.download_speed || "").trim(); if (!raw) { return { mbps: null, raw: "", status: "missing", unit: "" }; } const normalized = raw.toLowerCase().replace(/\s+/g, ""); const match = normalized.match(/([0-9]+(?:\.[0-9]+)?)/); if (!match) { return { mbps: null, raw, status: "invalid", unit: "" }; } const numeric = Number(match[1]); if (!Number.isFinite(numeric) || numeric < 0) { return { mbps: null, raw, status: "invalid", unit: "" }; } let mbps = numeric; let unit = "unknown"; if (normalized.includes("gb/s") || normalized.includes("g/s") || normalized.endsWith("gb") || normalized.endsWith("g")) { mbps = numeric * 1024; unit = "gb/s"; } else if (normalized.includes("mb/s") || normalized.includes("m/s") || normalized.endsWith("mb") || normalized.endsWith("m")) { mbps = numeric; unit = "mb/s"; } else if (normalized.includes("kb/s") || normalized.includes("k/s") || normalized.endsWith("kb") || normalized.endsWith("k")) { mbps = numeric / 1024; unit = "kb/s"; } else if (normalized.includes("b/s")) { mbps = numeric / (1024 * 1024); unit = "b/s"; } const status = unit === "unknown" ? "unknown_unit" : "ok"; return { mbps, raw, status, unit }; } async function selectValueRouterFirst() { const telemetry = { router: { ok: false, value: "", status: "", sourceType: "", error: "", speedRaw: "", speedMbps: null, speedParse: "missing", speedUnit: "" }, server: { ok: false, value: "", status: "", sourceType: "", error: "" } }; try { const routerObj = await fetchRuntimeJson(ROUTER_VALUE_JSON_URL, "router"); const routerValue = extractValue(routerObj, "router"); const speed = parseRouterSpeedMbps(routerObj); telemetry.router.ok = true; telemetry.router.value = routerValue; telemetry.router.status = String(routerObj?.status || ""); telemetry.router.sourceType = String(routerObj?.source_type || ""); telemetry.router.speedRaw = speed.raw; telemetry.router.speedMbps = speed.mbps; telemetry.router.speedParse = speed.status; telemetry.router.speedUnit = speed.unit; } catch (e) { telemetry.router.error = String(e.message || e); } try { const serverObj = await fetchRuntimeJson(SERVER_VALUE_JSON_URL, "server"); const serverValue = extractValue(serverObj, "server"); telemetry.server.ok = true; telemetry.server.value = serverValue; telemetry.server.status = String(serverObj?.status || ""); telemetry.server.sourceType = String(serverObj?.source_type || ""); } catch (e) { telemetry.server.error = String(e.message || e); } if (!telemetry.router.ok && !telemetry.server.ok) { throw new Error(`router failed: ${telemetry.router.error}; server failed: ${telemetry.server.error}`); } if (!telemetry.router.ok && telemetry.server.ok) { return { value: telemetry.server.value, chosenSource: "server", reason: "router_fail", telemetry }; } if (telemetry.router.ok && !telemetry.server.ok) { return { value: telemetry.router.value, chosenSource: "router", reason: "server_fail", telemetry }; } if (telemetry.router.speedParse === "ok" && telemetry.router.speedMbps < ROUTER_MIN_SPEED_MBPS) { return { value: telemetry.server.value, chosenSource: "server", reason: "router_low_speed", telemetry }; } return { value: telemetry.router.value, chosenSource: "router", reason: "router_preferred", telemetry }; } async function operator(proxies = [], targetPlatform, context) { const cache = scriptResourceCache; let value = cache.get(CACHE_KEY); let decision = null; if (!value) { try { decision = await selectValueRouterFirst(); value = decision.value; cache.set(CACHE_KEY, value, CACHE_TTL_MS); } catch (e) { console.log(`[vmess-domain-rotator] fetch failed: ${e.message}`); return proxies; } } let updated = 0; for (const p of proxies) { if (!p || p.type !== "vmess") continue; if (!NODE_NAME_REGEX.test(p.name || "")) continue; if (p.server !== value) { p.server = value; updated += 1; } } if (decision) { const t = decision.telemetry; console.log( `[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_mbps=${t.router.speedMbps == null ? "n/a" : t.router.speedMbps.toFixed(3)}, speed_parse=${t.router.speedParse}, threshold_mbps=${ROUTER_MIN_SPEED_MBPS}, value=${value}, updated=${updated}, total=${proxies.length}, target=${targetPlatform}` ); } else { console.log(`[vmess-domain-rotator] chosen=cache, value=${value}, updated=${updated}, total=${proxies.length}, target=${targetPlatform}`); } return proxies; }