cicy-desktop 2.1.300 → 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/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/panel-launcher.test.js +3 -2
- package/test/redroid-matrix.test.js +100 -0
package/package.json
CHANGED
|
@@ -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
|
+
};
|