|
|
@@ -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;
|
|
|
}
|