Explorar el Código

feature: add substore dual-source selection

Add router-first runtime selection with server fallback in the Sub-Store operator, including a speed threshold switch to server when router top candidate throughput is below 5 MB/s.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dew-OF-Aurora hace 6 días
padre
commit
d13db3acc0
Se han modificado 1 ficheros con 125 adiciones y 9 borrados
  1. 125 9
      substore/operator_template.js

+ 125 - 9
substore/operator_template.js

@@ -1,19 +1,22 @@
 /*
   Sub-Store operator (production-friendly)
-  - Pull dynamic value from current_domain.json / current_ip.json
+  - Pull dynamic value from router/runtime and server/runtime JSON
   - Replace vmess server field for matched nodes
 */
 
-const VALUE_JSON_URL = "https://git.dewofaurora.de/aurora/vmess-domain-rotator/raw/runtime-state/runtime/current_domain.json";
+const ROUTER_VALUE_JSON_URL = "http://<router-lan-ip>: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 fetchValueViaSubStore() {
+async function fetchRuntimeJson(url, sourceName) {
   const $ = $substore;
   const { body, statusCode } = await $.http.get({
-    url: VALUE_JSON_URL,
+    url,
     headers: {
       Accept: "application/json",
       "Cache-Control": "no-cache"
@@ -22,10 +25,20 @@ async function fetchValueViaSubStore() {
   });
 
   if (statusCode < 200 || statusCode >= 300) {
-    throw new Error(`http status ${statusCode}`);
+    throw new Error(`${sourceName} http status ${statusCode}`);
+  }
+
+  let obj;
+  try {
+    obj = JSON.parse(body || "{}");
+  } catch (e) {
+    throw new Error(`${sourceName} invalid json: ${e.message}`);
   }
 
-  const obj = JSON.parse(body || "{}");
+  return obj;
+}
+
+function extractValue(obj, sourceName) {
   let value = "";
   for (const key of JSON_VALUE_KEYS) {
     value = String(obj[key] || "").trim().toLowerCase();
@@ -33,18 +46,113 @@ async function fetchValueViaSubStore() {
   }
 
   if (!value) {
-    throw new Error(`empty value field, expected one of: ${JSON_VALUE_KEYS.join(", ")}`);
+    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 {
-      value = await fetchValueViaSubStore();
+      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}`);
@@ -63,6 +171,14 @@ async function operator(proxies = [], targetPlatform, context) {
     }
   }
 
-  console.log(`[vmess-domain-rotator] value=${value}, updated=${updated}, total=${proxies.length}, target=${targetPlatform}`);
+  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;
 }