cicy-desktop 2.1.299 → 2.1.301
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/backends/sidecar-ipc.js +20 -0
- package/src/sidecar/wsl-docker.js +11 -0
- package/src/tabbrowser/newtab-protocol.js +1 -3
- package/src/tabbrowser/panel-menu.js +4 -0
- package/src/tabbrowser/panel-page-router.js +9 -4
- package/src/tabbrowser/panel-preload.js +27 -0
- package/src/tabbrowser/panel-presets.js +1 -0
- package/src/tabbrowser/redroid-matrix-core.js +177 -0
- package/src/tabbrowser/redroid-matrix.html +668 -0
- package/src/tabbrowser/redroid-matrix.js +341 -0
- package/src/tabbrowser/redroid-shell.js +117 -0
- package/src/tools/tab-browser-tools.js +5 -2
- package/test/bootstrap-failure-reporting.test.js +7 -0
- package/test/panel-launcher.test.js +3 -2
- package/test/redroid-matrix.test.js +100 -0
package/package.json
CHANGED
|
@@ -169,6 +169,7 @@ function register({ sidecarLogPath } = {}) {
|
|
|
169
169
|
// WSL 功能启用后必须重启 Windows 才能继续。不等人:自动安排 90 秒后重启(桌面上会有
|
|
170
170
|
// 系统倒计时提示),UI 可「取消」或「立即重启」。每次开机只安排一次。
|
|
171
171
|
let _rebootScheduled = false;
|
|
172
|
+
let _relayMisses = 0, _relayResetDone = false; // WSL localhost 转发自愈(见 daemon 循环)
|
|
172
173
|
// 防重启循环:WSL 始终起不来时不能每次开机都重启。计数存 db/wsl-reboots.json,最多自动重启 2 次
|
|
173
174
|
// (成功后 :8008 起来时清零)。
|
|
174
175
|
const REBOOT_COUNT_FILE = path.join(os.homedir(), "cicy-ai", "db", "wsl-reboots.json");
|
|
@@ -227,6 +228,25 @@ function register({ sidecarLogPath } = {}) {
|
|
|
227
228
|
// bootstrap 幂等且可续跑;首次启用 WSL 若要求重启 Windows,下次登录 Desktop
|
|
228
229
|
// 会再次进入这里并从已完成的步骤继续。unknown 表示 WSL 正卡住/未响应,
|
|
229
230
|
// 此时不并发安装,留给下一轮检测或显式「修复 WSL」。
|
|
231
|
+
// 容器在里面健康、Windows 侧却连不通 = WSL localhost 转发坏了(实测 wsl --shutdown 后恢复)。
|
|
232
|
+
// 连续 2 轮(≈2 分钟)如此 → 每次开机最多自动重置一次 WSL。数据在 volume 里,容器由
|
|
233
|
+
// unless-stopped 策略自动拉起;不算「销毁」——对用户来说它本来就已经不可用了。
|
|
234
|
+
if (!s.running && !s.unknown && s.installed && appDocker.insideHealthy && appDocker.wslShutdown) {
|
|
235
|
+
let inside = false;
|
|
236
|
+
try { inside = await appDocker.insideHealthy(APP_CONTAINER, APP_PORT); } catch {}
|
|
237
|
+
if (inside) {
|
|
238
|
+
_relayMisses += 1;
|
|
239
|
+
if (_relayMisses >= 2 && !_relayResetDone) {
|
|
240
|
+
_relayResetDone = true;
|
|
241
|
+
auditDestructiveIpc(log, "docker:self-heal-localhost-relay", null, { container: APP_CONTAINER, misses: _relayMisses });
|
|
242
|
+
log.warn("[docker-daemon] container healthy inside WSL but 127.0.0.1:8008 unreachable from Windows → wsl --shutdown once to rebuild the localhost relay");
|
|
243
|
+
try { await appDocker.wslShutdown(); } catch (e) { log.warn(`[docker-daemon] wsl --shutdown failed: ${e.message}`); }
|
|
244
|
+
await refreshDockerStatus();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (_relayMisses < 2) { log.warn(`[docker-daemon] :${APP_PORT} unreachable from Windows but healthy inside (${_relayMisses}/2)`); return; }
|
|
248
|
+
} else { _relayMisses = 0; }
|
|
249
|
+
} else if (s.running) { _relayMisses = 0; }
|
|
230
250
|
if (!s.running && !s.unknown && _autoBootstrapPaused && Date.now() < _autoBootstrapRetryAt) {
|
|
231
251
|
// 硬失败后退避(见 recordBootstrapResult);卡片显示 lastError,到点自动再试,用户点「重试」也行。
|
|
232
252
|
} else if (!s.running && !s.unknown) {
|
|
@@ -620,6 +620,16 @@ async function ensureFreshImage({ emit } = {}) {
|
|
|
620
620
|
return true;
|
|
621
621
|
}
|
|
622
622
|
|
|
623
|
+
// 容器内部视角的健康:docker exec 进容器用 node 探 127.0.0.1:8008。用来区分「容器真挂了」和
|
|
624
|
+
// 「容器好好的、只是 WSL 的 localhost 转发坏了」(后者 Windows 侧 :8008 打不通,实测
|
|
625
|
+
// `wsl --shutdown` 后 75 秒恢复)。
|
|
626
|
+
async function insideHealthy(container = "cicy-code-docker-8008", port = 8008) {
|
|
627
|
+
try {
|
|
628
|
+
await wslRun(`docker exec ${container} node -e "fetch('http://127.0.0.1:${port}/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"`, { timeout: 20000 });
|
|
629
|
+
return true;
|
|
630
|
+
} catch { return false; }
|
|
631
|
+
}
|
|
632
|
+
|
|
623
633
|
// HTTP /health probe on 127.0.0.1:port from Windows — WSL2 forwards localhost,
|
|
624
634
|
// so a container published on :port is reachable here. Reuse docker.js's probe.
|
|
625
635
|
const probeHealth = docker.probeHealth;
|
|
@@ -1602,6 +1612,7 @@ async function readMihomoSelections(container = "cicy-code-docker-8008") {
|
|
|
1602
1612
|
}
|
|
1603
1613
|
|
|
1604
1614
|
module.exports = {
|
|
1615
|
+
insideHealthy, wslShutdown,
|
|
1605
1616
|
bootstrap, status, restart, stop, dockerRestart, recreate, update, upgrade, runContainer, readContainerToken,
|
|
1606
1617
|
distroInstalled, dockerInstalled, dockerEngineUp, imagePresent, probeHealth, wslRun, hasGatewayKey,
|
|
1607
1618
|
readMihomoConfig, readMihomoSelections, parseMihomoSelections,
|
|
@@ -20,8 +20,6 @@ const _handled = new WeakSet(); // sessions that already have the cicyui handler
|
|
|
20
20
|
// top-right "+"). <id> keeps each panel tab's URL unique so addTab's
|
|
21
21
|
// origin+pathname reuse never collapses two panels into one; the page keys its
|
|
22
22
|
// persisted layout off the same path.
|
|
23
|
-
const PANEL_HTML = path.join(__dirname, "split-panel.html");
|
|
24
|
-
const TELEGRAM_MATRIX_HTML = path.join(__dirname, "telegram-matrix.html");
|
|
25
23
|
const { panelPageForUrl } = require("./panel-page-router");
|
|
26
24
|
|
|
27
25
|
// NOTE: scheme is "cicyui", NOT "cicy" — "cicy" is already an OS deep-link
|
|
@@ -73,7 +71,7 @@ function handlerFor(ses, partition) {
|
|
|
73
71
|
// read per request (not cached) so dev edits to split-panel.html land on
|
|
74
72
|
// a simple tab reload, no Electron restart.
|
|
75
73
|
try {
|
|
76
|
-
const file = panelPageForUrl(request.url)
|
|
74
|
+
const file = path.join(__dirname, panelPageForUrl(request.url));
|
|
77
75
|
const html = await fs.promises.readFile(file, "utf8");
|
|
78
76
|
return new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } });
|
|
79
77
|
} catch (e) { return new Response("panel page missing", { status: 500 }); }
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
// preset=<name> on a cicyui://panel URL picks the page file; anything else is
|
|
2
|
+
// the generic split panel.
|
|
3
|
+
const PAGES = {
|
|
4
|
+
"telegram-matrix": "telegram-matrix.html",
|
|
5
|
+
"redroid-matrix": "redroid-matrix.html",
|
|
6
|
+
};
|
|
7
|
+
|
|
1
8
|
function panelPageForUrl(value) {
|
|
2
9
|
try {
|
|
3
|
-
return new URL(value).searchParams.get("preset")
|
|
4
|
-
? "telegram-matrix.html"
|
|
5
|
-
: "split-panel.html";
|
|
10
|
+
return PAGES[new URL(value).searchParams.get("preset")] || "split-panel.html";
|
|
6
11
|
} catch (e) {
|
|
7
12
|
return "split-panel.html";
|
|
8
13
|
}
|
|
9
14
|
}
|
|
10
15
|
|
|
11
|
-
module.exports = { panelPageForUrl };
|
|
16
|
+
module.exports = { panelPageForUrl, PAGES };
|
|
@@ -28,3 +28,30 @@ contextBridge.exposeInMainWorld("panelAPI", {
|
|
|
28
28
|
return () => ipcRenderer.removeListener("panelcells:state", h);
|
|
29
29
|
},
|
|
30
30
|
});
|
|
31
|
+
|
|
32
|
+
// Redroid 矩阵 page: docker+adb driven Android devices (redroid-matrix.js).
|
|
33
|
+
const rd = (ch) => (args) => ipcRenderer.invoke(`redroid:${ch}`, args || {});
|
|
34
|
+
contextBridge.exposeInMainWorld("redroidAPI", {
|
|
35
|
+
defaults: rd("defaults"),
|
|
36
|
+
list: rd("list"),
|
|
37
|
+
create: rd("create"),
|
|
38
|
+
start: (name) => rd("start")({ name }),
|
|
39
|
+
stop: (name) => rd("stop")({ name }),
|
|
40
|
+
restart: (name) => rd("restart")({ name }),
|
|
41
|
+
remove: (name, purge) => rd("remove")({ name, purge }),
|
|
42
|
+
screenshot: (name) => rd("screenshot")({ name }),
|
|
43
|
+
input: (name, event) => rd("input")({ name, event }),
|
|
44
|
+
setProxy: (name, proxy) => rd("set-proxy")({ name, proxy }),
|
|
45
|
+
probeIp: (name) => rd("probe-ip")({ name }),
|
|
46
|
+
frida: (name, on) => rd("frida")({ name, on }),
|
|
47
|
+
apps: (name) => rd("apps")({ name }),
|
|
48
|
+
launch: (name, pkg) => rd("launch")({ name, pkg }),
|
|
49
|
+
uninstall: (name, pkg) => rd("uninstall")({ name, pkg }),
|
|
50
|
+
install: (name) => rd("install")({ name }),
|
|
51
|
+
shell: (name, cmd) => rd("shell")({ name, cmd }),
|
|
52
|
+
onProgress: (cb) => {
|
|
53
|
+
const h = (_e, m) => { try { cb(m); } catch (e) {} };
|
|
54
|
+
ipcRenderer.on("redroid:progress", h);
|
|
55
|
+
return () => ipcRenderer.removeListener("redroid:progress", h);
|
|
56
|
+
},
|
|
57
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const PRESETS = {
|
|
2
2
|
"telegram-matrix": { preset: "telegram-matrix", title: "Telegram 矩阵", query: "preset=telegram-matrix" },
|
|
3
|
+
"redroid-matrix": { preset: "redroid-matrix", title: "Redroid 矩阵", query: "preset=redroid-matrix" },
|
|
3
4
|
};
|
|
4
5
|
|
|
5
6
|
function resolvePanelPreset(value) {
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Copyright 2026 CiCy AI
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// Pure helpers for the Redroid matrix panel (no Electron / no shell here so
|
|
5
|
+
// they are unit-testable): container naming, host adb port allocation, the
|
|
6
|
+
// `docker run` command line, `docker ps` parsing and device proxy validation.
|
|
7
|
+
//
|
|
8
|
+
// Conventions (must match what the panel + watchdog expect):
|
|
9
|
+
// container name redroid-<slug> (legacy: bare "redroid"/"redroid11" still listed)
|
|
10
|
+
// label cicy.redroid=1 (how we find "ours")
|
|
11
|
+
// host adb port 15555+ (NOT 5555-5585: that range is reserved by adb for
|
|
12
|
+
// emulators and the connection stays "offline" forever)
|
|
13
|
+
// data volume /root/redroid-<slug>-data:/data (survives recreate, dropped on purge)
|
|
14
|
+
|
|
15
|
+
const LABEL = "cicy.redroid";
|
|
16
|
+
const PORT_BASE = 15555;
|
|
17
|
+
const IMAGES = {
|
|
18
|
+
"11": "redroid/redroid:11.0.0-latest",
|
|
19
|
+
"13": "redroid/redroid:13.0.0-latest",
|
|
20
|
+
"14": "redroid/redroid:14.0.0-latest",
|
|
21
|
+
};
|
|
22
|
+
const DEFAULT_SPEC = { version: "13", width: 720, height: 1280, dpi: 320 };
|
|
23
|
+
|
|
24
|
+
function slugify(name) {
|
|
25
|
+
const s = String(name || "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32);
|
|
26
|
+
if (!s) throw new Error("设备名不能为空(只保留字母、数字和 -)");
|
|
27
|
+
return s;
|
|
28
|
+
}
|
|
29
|
+
function containerName(name) { return `redroid-${slugify(name)}`; }
|
|
30
|
+
function dataDir(container) { return `/root/${container}-data`; }
|
|
31
|
+
|
|
32
|
+
// Next free host port ≥ PORT_BASE given the ports already published by
|
|
33
|
+
// existing redroid containers (any order, holes are reused).
|
|
34
|
+
function allocatePort(usedPorts) {
|
|
35
|
+
const used = new Set((usedPorts || []).map(Number).filter(Number.isFinite));
|
|
36
|
+
let p = PORT_BASE;
|
|
37
|
+
while (used.has(p)) p += 1;
|
|
38
|
+
return p;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeSpec(input = {}) {
|
|
42
|
+
const version = String(input.version || DEFAULT_SPEC.version);
|
|
43
|
+
if (!IMAGES[version]) throw new Error(`不支持的 Android 版本 ${version}(可选 ${Object.keys(IMAGES).join("/")})`);
|
|
44
|
+
const num = (v, d, lo, hi) => { const n = Number(v); if (!Number.isFinite(n) || n < lo || n > hi) return d; return Math.round(n); };
|
|
45
|
+
return {
|
|
46
|
+
version,
|
|
47
|
+
image: IMAGES[version],
|
|
48
|
+
width: num(input.width, DEFAULT_SPEC.width, 240, 4096),
|
|
49
|
+
height: num(input.height, DEFAULT_SPEC.height, 240, 4096),
|
|
50
|
+
dpi: num(input.dpi, DEFAULT_SPEC.dpi, 120, 640),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The docker command line for a fresh device. `gpu_mode=guest` = software
|
|
55
|
+
// rendering: WSL2 has no /dev/dri and the host mode hangs surfaceflinger.
|
|
56
|
+
function buildRunCommand({ container, port, spec }) {
|
|
57
|
+
const s = normalizeSpec(spec);
|
|
58
|
+
return [
|
|
59
|
+
"docker run -d",
|
|
60
|
+
`--name ${container}`,
|
|
61
|
+
`--label ${LABEL}=1`,
|
|
62
|
+
`--label ${LABEL}.version=${s.version}`,
|
|
63
|
+
"--privileged",
|
|
64
|
+
"--restart unless-stopped",
|
|
65
|
+
`-v ${dataDir(container)}:/data`,
|
|
66
|
+
`-p ${port}:5555`,
|
|
67
|
+
s.image,
|
|
68
|
+
"androidboot.redroid_gpu_mode=guest",
|
|
69
|
+
`androidboot.redroid_width=${s.width}`,
|
|
70
|
+
`androidboot.redroid_height=${s.height}`,
|
|
71
|
+
`androidboot.redroid_dpi=${s.dpi}`,
|
|
72
|
+
].join(" ");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Parse `docker ps -a --format '{{.Names}}\t{{.Image}}\t{{.State}}\t{{.Status}}\t{{.Ports}}'`.
|
|
76
|
+
// Keeps only redroid images; extracts the published host port for 5555/tcp.
|
|
77
|
+
function parsePs(stdout) {
|
|
78
|
+
const out = [];
|
|
79
|
+
for (const line of String(stdout || "").split(/\r?\n/)) {
|
|
80
|
+
if (!line.trim()) continue;
|
|
81
|
+
const [name, image, state, status, ports = ""] = line.split("\t");
|
|
82
|
+
if (!/^redroid\/redroid:/.test(image || "")) continue;
|
|
83
|
+
const m = /(?:^|[\s,])(?:0\.0\.0\.0|127\.0\.0\.1|\[::\]|):?(\d+)->5555\/tcp/.exec(ports) || /:(\d+)->5555\/tcp/.exec(ports);
|
|
84
|
+
const vm = /redroid:(\d+)\./.exec(image);
|
|
85
|
+
out.push({
|
|
86
|
+
name, image, state, status,
|
|
87
|
+
version: vm ? vm[1] : "",
|
|
88
|
+
port: m ? Number(m[1]) : null,
|
|
89
|
+
running: state === "running",
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return out.sort((a, b) => (a.port || 0) - (b.port || 0) || a.name.localeCompare(b.name));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Android `settings put global http_proxy` wants host:port (or ":0" = off).
|
|
96
|
+
// Accept "host:port" or "http://host:port"; reject anything else so a typo
|
|
97
|
+
// never silently leaves the device on direct egress.
|
|
98
|
+
function normalizeDeviceProxy(value) {
|
|
99
|
+
const v = String(value || "").trim();
|
|
100
|
+
if (!v) return "";
|
|
101
|
+
let host = v, port = "";
|
|
102
|
+
const u = /^(?:https?:\/\/)?([^/:\s]+):(\d{1,5})\/?$/.exec(v);
|
|
103
|
+
if (!u) throw new Error("代理格式应为 host:port(例如 172.18.0.2:20011),留空 = 直连");
|
|
104
|
+
host = u[1]; port = u[2];
|
|
105
|
+
if (Number(port) < 1 || Number(port) > 65535) throw new Error("代理端口无效");
|
|
106
|
+
return `${host}:${port}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const AOSP_KEYS = { home: 3, back: 4, recents: 187, power: 26, volup: 24, voldown: 25, enter: 66, del: 67, menu: 82 };
|
|
110
|
+
|
|
111
|
+
// adb `input` argument list for one gesture/keypress. Text goes through
|
|
112
|
+
// `input text` with the characters adb chokes on escaped; anything non-ASCII
|
|
113
|
+
// is sent per key event fallback (adb's input text is ASCII-only).
|
|
114
|
+
function inputArgs(ev) {
|
|
115
|
+
const n = (x) => Math.max(0, Math.round(Number(x) || 0));
|
|
116
|
+
switch (ev && ev.type) {
|
|
117
|
+
case "tap": return ["input", "tap", n(ev.x), n(ev.y)];
|
|
118
|
+
case "swipe": return ["input", "swipe", n(ev.x1), n(ev.y1), n(ev.x2), n(ev.y2), n(ev.ms || 200)];
|
|
119
|
+
case "key": {
|
|
120
|
+
const code = AOSP_KEYS[String(ev.key)] || (Number.isInteger(Number(ev.key)) ? Number(ev.key) : null);
|
|
121
|
+
if (code == null) throw new Error(`unknown key ${ev.key}`);
|
|
122
|
+
return ["input", "keyevent", code];
|
|
123
|
+
}
|
|
124
|
+
case "text": {
|
|
125
|
+
const t = String(ev.text || "");
|
|
126
|
+
if (!t) throw new Error("empty text");
|
|
127
|
+
if (/[^\x20-\x7e]/.test(t)) throw new Error("adb input text 只支持 ASCII");
|
|
128
|
+
// spaces → %s, and shell-escape the rest via single quotes
|
|
129
|
+
return ["input", "text", `'${t.replace(/'/g, "'\\''").replace(/ /g, "%s")}'`];
|
|
130
|
+
}
|
|
131
|
+
default: throw new Error(`unknown input ${ev && ev.type}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
// ── egress IP classification ─────────────────────────────────────────────────
|
|
137
|
+
// Two free sources are merged: ip-api.com (geo, isp/as, mobile/proxy/hosting)
|
|
138
|
+
// and ipapi.is (is_datacenter/is_proxy/is_vpn/is_tor/is_abuser, asn). The
|
|
139
|
+
// grade answers the user's real question — "will sign-up risk engines
|
|
140
|
+
// (Facebook, Google, Telegram…) accept this IP?":
|
|
141
|
+
// A residential / mobile line, no flags → best
|
|
142
|
+
// B no datacenter / proxy flags but a backbone or unknown-type ISP
|
|
143
|
+
// C datacenter / hosting → sign-ups usually blocked
|
|
144
|
+
// D flagged proxy / VPN / Tor / abuser → practically dead
|
|
145
|
+
const RESIDENTIAL_ISP = /comcast|xfinity|verizon|at&t|att\b|spectrum|charter|cox\b|frontier|centurylink|lumen|t-mobile|sprint|optimum|altice|windstream|mediacom|deutsche telekom|telekom|vodafone|orange|\bbt\b|british telecom|sky broadband|virgin media|talktalk|kddi|ntt|softbank|docomo|telstra|optus|bell canada|rogers|shaw|telus|sfr|bouygues|free sas|telefonica|movistar|kpn|swisscom|telia|telenor|chunghwa|sk broadband|kt corp|lg uplink|pldt|globe telecom|singtel|starhub|m1 limited|china telecom|china unicom|china mobile/i;
|
|
146
|
+
|
|
147
|
+
function classifyIp({ ipapi = null, ipapis = null } = {}) {
|
|
148
|
+
const a = ipapi || {}, b = ipapis || {};
|
|
149
|
+
const flags = {
|
|
150
|
+
proxy: !!(a.proxy || b.is_proxy),
|
|
151
|
+
vpn: !!b.is_vpn,
|
|
152
|
+
tor: !!b.is_tor,
|
|
153
|
+
abuser: !!b.is_abuser,
|
|
154
|
+
datacenter: !!(a.hosting || b.is_datacenter),
|
|
155
|
+
mobile: !!a.mobile,
|
|
156
|
+
};
|
|
157
|
+
const isp = String(a.isp || a.org || b.asn_org || b.company_name || "");
|
|
158
|
+
const residential = flags.mobile || RESIDENTIAL_ISP.test(isp);
|
|
159
|
+
let grade, kind, verdict;
|
|
160
|
+
if (flags.proxy || flags.vpn || flags.tor || flags.abuser) {
|
|
161
|
+
grade = "D"; kind = "已标记"; verdict = "情报库已标记为代理 / VPN / Tor / 滥用来源,注册类风控基本必拦,建议换节点。";
|
|
162
|
+
} else if (flags.datacenter) {
|
|
163
|
+
grade = "C"; kind = "机房"; verdict = "机房 / 云主机 IP。Facebook、Google 等注册风控大概率拦截或要求验证;日常登录一般可用。";
|
|
164
|
+
} else if (residential) {
|
|
165
|
+
grade = "A"; kind = flags.mobile ? "移动网络" : "住宅"; verdict = "住宅 / 移动线路且无任何标记,风控最友好。";
|
|
166
|
+
} else {
|
|
167
|
+
grade = "B"; kind = "骨干/未知"; verdict = "未被标记为机房或代理,但也不是明确的住宅线路(骨干 / 商业网络)。注册可试,成功率一般。";
|
|
168
|
+
}
|
|
169
|
+
return { grade, kind, verdict, flags, residential };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
classifyIp, RESIDENTIAL_ISP,
|
|
174
|
+
LABEL, PORT_BASE, IMAGES, DEFAULT_SPEC, AOSP_KEYS,
|
|
175
|
+
slugify, containerName, dataDir, allocatePort, normalizeSpec, buildRunCommand, parsePs,
|
|
176
|
+
normalizeDeviceProxy, inputArgs,
|
|
177
|
+
};
|