cicy-desktop 2.1.178 → 2.1.180

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.
@@ -6,8 +6,8 @@
6
6
  <link rel="icon" type="image/svg+xml" href="./favicon.svg" />
7
7
  <link rel="icon" type="image/png" sizes="256x256" href="./favicon-256.png" />
8
8
  <title>CiCy Desktop</title>
9
- <script type="module" crossorigin src="./assets/index-CH3x7Ecg.js"></script>
10
- <link rel="stylesheet" crossorigin href="./assets/index-CGcN1St7.css">
9
+ <script type="module" crossorigin src="./assets/index-DhqT69Bv.js"></script>
10
+ <link rel="stylesheet" crossorigin href="./assets/index-CyUwy5Um.css">
11
11
  </head>
12
12
  <body>
13
13
  <div id="root"></div>
@@ -24,6 +24,7 @@ const sidecar = require("../sidecar/cicy-code");
24
24
  const docker = require("../sidecar/docker");
25
25
  const wslDocker = require("../sidecar/wsl-docker"); // Docker-版 via WSL2+Ubuntu (方案 A, win32)
26
26
  const colimaDocker = require("../sidecar/colima-docker"); // Docker-版 via Colima (Lima VM, darwin)
27
+ const hostMihomo = require("../sidecar/host-mihomo"); // 宿主侧 mihomo —— 给系统 Chrome 的 per-profile 代理
27
28
  // 按平台分发 Docker-版 cicy-code 的运行层:darwin → Colima,win32 → WSL2。两者同接口
28
29
  // (bootstrap/status/restart/stop/dockerRestart/recreate/update/upgrade/runContainer/
29
30
  // readContainerToken),所以下面的 handler 共用一份逻辑,只换底层模块。
@@ -61,6 +62,9 @@ const cc = (() => { try { return require("../cloud/cloud-client"); } catch { ret
61
62
  const DOCKER_TEAMS_FILE = path.join(os.homedir(), "cicy-ai", "db", "docker-teams.json");
62
63
  function readDockerTeams() { try { return JSON.parse(fs.readFileSync(DOCKER_TEAMS_FILE, "utf8")) || {}; } catch { return {}; } }
63
64
  function writeDockerTeams(obj) { try { fs.mkdirSync(path.dirname(DOCKER_TEAMS_FILE), { recursive: true }); fs.writeFileSync(DOCKER_TEAMS_FILE, JSON.stringify(obj, null, 2)); } catch {} }
65
+ // 「Chrome 代理」开关:在宿主起一个 mihomo(host-mihomo)服务系统 Chrome 的 per-profile 代理,
66
+ // 配置从容器里 cp 出来。按 volume 存 docker-teams.json[volume].chromeProxy。
67
+ function chromeProxyEnabled() { try { return !!(readDockerTeams()[APP_VOLUME] || {}).chromeProxy; } catch { return false; } }
64
68
  let dockerTeamReg = null; // { teamId, title, apiKey } — 缓存,appOpts 读它
65
69
 
66
70
  // docker 团队的权威来源 = cloud(w-10122 #197):POST /api/team/docker/register 按
@@ -105,7 +109,7 @@ function register({ sidecarLogPath } = {}) {
105
109
  // 容器里 cicy-code 的版本(DockerCard 底部显示):running 时读 :APP_PORT/api/health。
106
110
  let ver = null;
107
111
  if (s.running) { try { ver = await require("../sidecar/version").running(APP_PORT); } catch {} }
108
- _dockerStatusCache = { installed: !!s.distro, dockerRunning: !!s.engineUp, running: !!s.running, unknown: !!s.unknown, version: ver, port: APP_PORT, platform: process.platform, ts: Date.now() };
112
+ _dockerStatusCache = { installed: !!s.distro, dockerRunning: !!s.engineUp, running: !!s.running, unknown: !!s.unknown, version: ver, port: APP_PORT, platform: process.platform, chromeProxy: chromeProxyEnabled(), chromeProxyRunning: hostMihomo.running(), ts: Date.now() };
109
113
  } catch (e) {
110
114
  _dockerStatusCache = { installed: false, dockerRunning: false, running: false, unknown: true, port: APP_PORT, platform: process.platform, error: e.message, ts: Date.now() };
111
115
  }
@@ -140,6 +144,8 @@ function register({ sidecarLogPath } = {}) {
140
144
  await refreshDockerStatus();
141
145
  }
142
146
  } catch (e) { log.warn(`[docker-daemon] key self-heal failed: ${e.message}`); }
147
+ // Chrome 代理:开关开着但宿主 mihomo 没跑 → 拉起(从容器同步配置,幂等)。
148
+ try { await maybeStartChromeProxy(); } catch (e) { log.warn(`[chrome-proxy] auto-start failed: ${e.message}`); }
143
149
  }
144
150
  } finally { _dockerDaemonBusy = false; }
145
151
  return _dockerStatusCache;
@@ -149,6 +155,18 @@ function register({ sidecarLogPath } = {}) {
149
155
  setInterval(() => { reconcileDocker().catch(() => {}); }, 60000); // keep fresh + self-heal
150
156
  }
151
157
 
158
+ // 宿主 Chrome 代理:开关开着才做。从容器 cp 出 mihomo 配置 → host-mihomo 在宿主起/续。
159
+ // 已在跑也同步一次配置(容器侧节点可能被云端更新),变了才重启。幂等。
160
+ async function maybeStartChromeProxy() {
161
+ if (!chromeProxyEnabled() || !APP_DOCKER_SUPPORTED) return;
162
+ const yaml = await appDocker.readMihomoConfig(APP_CONTAINER);
163
+ if (hostMihomo.binPresent() && hostMihomo.running()) {
164
+ if (hostMihomo.writeConfig(yaml)) hostMihomo.start({ force: true });
165
+ return;
166
+ }
167
+ await hostMihomo.enable({ containerYaml: yaml });
168
+ }
169
+
152
170
  ipcMain.handle("sidecar:status", async () => {
153
171
  const running = await sidecar.probeExisting(PORT);
154
172
  return { running };
@@ -342,6 +360,26 @@ function register({ sidecarLogPath } = {}) {
342
360
  } catch (err) { return { ok: false, error: err.message }; }
343
361
  });
344
362
 
363
+ // ⋯ menu 开关 → 「Chrome 代理」:在宿主起一个 mihomo 服务系统 Chrome 的 per-profile 代理
364
+ // (127.0.0.1:2000N)。配置从容器里 cp 出来(含云端下发的真实节点),DNS 关掉、控制口错开。
365
+ // 容器不再 publish 20001-32(那条路在 colima/WSL 下根本不通)。开关存 docker-teams.json[volume]。
366
+ ipcMain.handle("docker:app-chrome-proxy", async (e, on) => {
367
+ if (!APP_DOCKER_SUPPORTED) return { ok: false, error: "Chrome 代理仅支持 Windows / macOS" };
368
+ try {
369
+ const store = readDockerTeams();
370
+ store[APP_VOLUME] = { ...(store[APP_VOLUME] || {}), chromeProxy: !!on };
371
+ writeDockerTeams(store);
372
+ if (on) {
373
+ const containerYaml = await appDocker.readMihomoConfig(APP_CONTAINER);
374
+ await hostMihomo.enable({ containerYaml, emit: (ev) => { try { e.sender.send("docker:app-progress", ev); } catch {} } });
375
+ } else {
376
+ hostMihomo.stop();
377
+ }
378
+ await refreshDockerStatus().catch(() => {});
379
+ return { ok: true, chromeProxy: !!on, running: hostMihomo.running() };
380
+ } catch (err) { return { ok: false, error: err.message }; }
381
+ });
382
+
345
383
  // ⋯ menu → 更新 cicy-code: pull the latest cicy-code into the container +
346
384
  // restart it (no container recreate). Streams progress to the drawer.
347
385
  ipcMain.handle("docker:app-update", async (e) => {
package/src/main.js CHANGED
@@ -1377,25 +1377,38 @@ function cleanup() {
1377
1377
  log.info("[Cleanup] shutting down child services");
1378
1378
  try { cicyCodeSidecar.stop(); } catch (e) { /* best-effort */ }
1379
1379
 
1380
- // Kill cicy-code, all tmux servers, gotty/ttyd, and code-server processes
1381
- // that the cicy-code sidecar may have spawned. best-effort, sync, cross-platform.
1380
+ // Reap leftover NATIVE host helpers from the pre-docker-only era (a stray
1381
+ // cicy-code.exe / ttyd / gotty / code-server). 主人: quitting CiCy Desktop must
1382
+ // NOT touch Docker — in docker-only these all run INSIDE the container, and the
1383
+ // Docker engine (colima VM on mac / WSL distro+dockerd on win) is a separate
1384
+ // background service that survives the app + the container is --restart
1385
+ // unless-stopped. So we ONLY kill exact native host targets, never the engine.
1382
1386
  try {
1383
1387
  const { execSync } = require("child_process");
1384
1388
  const targets = ["cicy-code", "ttyd", "gotty", "code-server"];
1385
1389
 
1386
1390
  if (process.platform === "win32") {
1387
- // Windows: taskkill /F /IM <name>.exe (and base name in case extension differs)
1391
+ // Windows: taskkill /F /IM <name>.exe EXACT image name, no wildcards, so it
1392
+ // can only hit a native cicy-code.exe (retired), never wsl.exe / dockerd /
1393
+ // vmmem / the WSL distro. We never `wsl --terminate/--shutdown` on quit.
1388
1394
  for (const t of targets) {
1389
1395
  try { execSync(`taskkill /F /IM ${t}.exe`, { stdio: "ignore", windowsHide: true }); } catch {}
1390
- try { execSync(`taskkill /F /IM ${t}`, { stdio: "ignore", windowsHide: true }); } catch {}
1391
1396
  }
1392
- // Windows has no tmux by default; nothing to do.
1393
1397
  } else {
1394
- // macOS / Linux: pkill matches by command line.
1395
- for (const t of targets) {
1396
- try { execSync(`pkill -f ${t}`, { stdio: "ignore" }); } catch {}
1398
+ // macOS / Linux: pkill matches by FULL command line. ⚠️ docker-only: cicy-code
1399
+ // / ttyd / gotty / code-server all run INSIDE the container, NOT on the host —
1400
+ // there is normally nothing here to kill. CRITICAL: never `pkill -f cicy-code`
1401
+ // plainly — the colima profile is named `cicy-code`, so the Lima VM hostagent
1402
+ // runs as `limactl ... colima-cicy-code` and a bare match KILLS THE VM on every
1403
+ // quit → next launch sees Docker "down" and re-bootstraps ("重新 install" bug).
1404
+ // Anchor cicy-code to the native --desktop sidecar form (retired) only.
1405
+ const unixPats = ["cicy-code --desktop", "ttyd", "gotty", "code-server"];
1406
+ for (const p of unixPats) {
1407
+ try { execSync(`pkill -f '${p}'`, { stdio: "ignore" }); } catch {}
1397
1408
  }
1398
- try { execSync(`tmux kill-server`, { stdio: "ignore" }); } catch {}
1409
+ // Host tmux only (native path, retired); the container's tmux is in its own
1410
+ // namespace and unaffected. (No -f cicy-code match here either.)
1411
+ try { execSync(`pkill -f 'tmux.*cicy'`, { stdio: "ignore" }); } catch {}
1399
1412
  }
1400
1413
  } catch (e) {
1401
1414
  log.warn(`[Cleanup] kill children failed: ${e.message}`);
@@ -325,12 +325,11 @@ async function runContainer({ port = 8009, container = "cicy-code-docker-8009",
325
325
  .filter(([, v]) => v != null && v !== "")
326
326
  .map(([k, v]) => `-e ${k}='${String(v).replace(/'/g, "'\\''")}'`)
327
327
  .join(" ");
328
- // 主人(A 方案): 映射 mihomo 的 per-Chrome-profile 监听口段(约定 20000+N)回主机回环。
329
- // docker-only mihomo 只在容器里,这些口不暴露则主机系统 Chrome 的代理 127.0.0.1:(20000+N)
330
- // 连不上。默认 20001-20032(32 profile),CICY_CHROME_PROXY_PORTS 可调。
331
- const CHROME_PROXY_PORTS = process.env.CICY_CHROME_PROXY_PORTS || "20001-20032";
328
+ // 主人方案: Chrome 的 per-profile 代理(127.0.0.1:2000N)改由「宿主 mihomo」(host-mihomo.js)
329
+ // 服务,不再从容器 publish 20001-32 —— colima/Lima 转发那个端口段始终到不了容器里只绑
330
+ // 127.0.0.1 的监听(Chrome 一直 ERR_EMPTY_RESPONSE)。容器只暴露 cicy-code 的 :8009。
332
331
  const mk = (s) => `run -d --name ${container} --restart unless-stopped ${PLATFORM_FLAG} ` +
333
- `-p 127.0.0.1:${port}:8008 -p 127.0.0.1:${CHROME_PROXY_PORTS}:${CHROME_PROXY_PORTS} -e CICY_PUBLIC=1 -v ${volume}:/home/cicy ${s} ${envArgs} ${IMAGE}`;
332
+ `-p 127.0.0.1:${port}:8008 -e CICY_PUBLIC=1 -v ${volume}:/home/cicy ${s} ${envArgs} ${IMAGE}`;
334
333
  const share = shareMountArg();
335
334
  try {
336
335
  await dk(mk(share), { timeout: 90000 });
@@ -565,7 +564,15 @@ async function authorizeHostSsh({ container = "cicy-code-docker-8009" } = {}) {
565
564
  return { ok: true, user, added: add.length, verified, detail };
566
565
  }
567
566
 
567
+ // 读容器里 cicy-code 生成的 mihomo.yaml —— host-mihomo 用它在宿主重建 Chrome 代理配置
568
+ // (含云端下发的真实上游节点)。
569
+ async function readMihomoConfig(container = "cicy-code-docker-8009") {
570
+ const { stdout } = await dk(`exec ${container} cat /home/cicy/cicy-ai/db/mihomo.yaml`, { timeout: 15000 });
571
+ return String(stdout || "");
572
+ }
573
+
568
574
  module.exports = {
569
575
  bootstrap, status, restart, stop, dockerRestart, recreate, update, upgrade, runContainer, readContainerToken,
570
576
  vmExists, colimaInstalled, dockerCliInstalled, engineUp, imagePresent, probeHealth, hasGatewayKey, authorizeHostSsh,
577
+ readMihomoConfig,
571
578
  };
@@ -0,0 +1,150 @@
1
+ // Host-side mihomo for the Chrome per-profile proxy.
2
+ //
3
+ // docker-only put mihomo INSIDE the cicy-code container. But the system Chrome
4
+ // runs on the HOST (mac/win/linux), and its per-profile proxy points at
5
+ // 127.0.0.1:2000N — which a container CANNOT reliably serve through colima/WSL
6
+ // port-forwarding (the per-profile listeners bind 127.0.0.1 inside the container;
7
+ // publishing the 20001-32 range through colima/Lima never reached them → Chrome
8
+ // got ERR_EMPTY_RESPONSE / connection-closed). 主人方案: run a SECOND mihomo on
9
+ // the HOST, reusing the SAME proxy nodes/rules — we copy the container's
10
+ // mihomo.yaml out (the cloud provisions it with the real upstream nodes), strip
11
+ // it down to just what Chrome needs (listeners + proxies + groups + rules), and
12
+ // run it natively so Chrome reaches 127.0.0.1:2000N directly. No container port
13
+ // publish involved (runContainer drops the -p 20001-32 range).
14
+ //
15
+ // Binary is pulled from OSS (CN-fast) — github/gh-proxy was too slow on real
16
+ // Macs. Same version the container uses (cicy-ai/cicy-mihomo v1.10.4).
17
+
18
+ const { spawn, execFile } = require("child_process");
19
+ const fs = require("fs");
20
+ const os = require("os");
21
+ const path = require("path");
22
+ const https = require("https");
23
+ const yaml = require("js-yaml");
24
+
25
+ const VER = (process.env.CICY_MIHOMO_VERSION || "v1.10.4").replace(/^v?/, "v");
26
+ const OSS_BASE = process.env.CICY_OSS_BASE || "https://cicy-1372193042-cn.oss-cn-shanghai.aliyuncs.com";
27
+ const IS_WIN = process.platform === "win32";
28
+ const EXT = IS_WIN ? ".exe" : "";
29
+
30
+ function osStr() { return process.platform === "darwin" ? "darwin" : IS_WIN ? "windows" : "linux"; }
31
+ function archStr() { return process.arch === "arm64" ? "arm64" : "amd64"; }
32
+ // OSS mirror of cicy-ai/cicy-mihomo releases (see mihomo/<ver>/mihomo-<os>-<arch>).
33
+ function assetUrl() { return process.env.CICY_MIHOMO_RELEASE_URL || `${OSS_BASE}/mihomo/${VER}/mihomo-${osStr()}-${archStr()}${EXT}`; }
34
+
35
+ const RT_DIR = path.join(os.homedir(), "cicy-ai", "runtime", "mihomo", VER.replace(/^v/, ""));
36
+ function binPath() { return path.join(RT_DIR, "mihomo" + EXT); }
37
+ const HOST_CONFIG = path.join(os.homedir(), "cicy-ai", "db", "mihomo-host.yaml");
38
+ const HOST_LOG = path.join(os.homedir(), "cicy-ai", "db", "mihomo-host.log");
39
+ const PID_FILE = path.join(os.homedir(), "cicy-ai", "db", "mihomo-host.pid");
40
+
41
+ // Host instance's own control/mixed ports — kept OFF the container's (9001/19001)
42
+ // so the two mihomos never clash even if both somehow share a loopback view.
43
+ const HOST_MIXED = Number(process.env.CICY_HOST_MIHOMO_MIXED || 9011);
44
+ const HOST_CTRL = Number(process.env.CICY_HOST_MIHOMO_CTRL || 19011);
45
+
46
+ function binPresent() { try { return fs.statSync(binPath()).size > 1_000_000; } catch { return false; } }
47
+
48
+ function download(url, dest, redirects = 0) {
49
+ return new Promise((resolve, reject) => {
50
+ if (redirects > 5) return reject(new Error("too many redirects"));
51
+ const f = fs.createWriteStream(dest);
52
+ const req = https.get(url, (res) => {
53
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
54
+ f.close(); try { fs.unlinkSync(dest); } catch {}
55
+ return download(res.headers.location, dest, redirects + 1).then(resolve, reject);
56
+ }
57
+ if (res.statusCode !== 200) { f.close(); try { fs.unlinkSync(dest); } catch {} return reject(new Error(`HTTP ${res.statusCode} for ${url}`)); }
58
+ res.pipe(f);
59
+ f.on("finish", () => f.close(() => resolve()));
60
+ f.on("error", (e) => { try { fs.unlinkSync(dest); } catch {} reject(e); });
61
+ });
62
+ req.on("error", (e) => { try { fs.unlinkSync(dest); } catch {} reject(e); });
63
+ });
64
+ }
65
+
66
+ // Pull the mihomo binary from OSS into the runtime store (idempotent).
67
+ async function ensureBinary({ emit } = {}) {
68
+ if (binPresent()) return binPath();
69
+ fs.mkdirSync(RT_DIR, { recursive: true });
70
+ emit && emit({ phase: "chrome-proxy", status: "running", message: "下载 mihomo(Chrome 代理,从 OSS)…" });
71
+ await download(assetUrl(), binPath());
72
+ if (!IS_WIN) { try { fs.chmodSync(binPath(), 0o755); } catch {} }
73
+ if (!binPresent()) throw new Error("mihomo 下载后校验失败(文件过小)");
74
+ return binPath();
75
+ }
76
+
77
+ // Build the HOST config from the container's mihomo.yaml: keep only what Chrome
78
+ // needs (listeners + proxies + proxy-groups + rules), disable DNS (no :53 bind on
79
+ // the host — needs root) and pin our own control/mixed ports. The per-profile
80
+ // listeners already bind 127.0.0.1, exactly what host Chrome connects to.
81
+ function buildHostConfig(containerYaml) {
82
+ let c = {};
83
+ try { c = yaml.load(containerYaml) || {}; } catch (e) { throw new Error(`容器 mihomo 配置解析失败: ${e.message}`); }
84
+ const host = {
85
+ "mixed-port": HOST_MIXED,
86
+ "allow-lan": false,
87
+ "log-level": "warning",
88
+ "external-controller": `127.0.0.1:${HOST_CTRL}`,
89
+ secret: "",
90
+ dns: { enable: false },
91
+ listeners: Array.isArray(c.listeners) ? c.listeners : [],
92
+ proxies: Array.isArray(c.proxies) ? c.proxies : [],
93
+ "proxy-groups": Array.isArray(c["proxy-groups"]) ? c["proxy-groups"] : [],
94
+ rules: Array.isArray(c.rules) ? c.rules : ["MATCH,DIRECT"],
95
+ };
96
+ return yaml.dump(host, { lineWidth: -1 });
97
+ }
98
+
99
+ // Write the adapted config; returns true if it changed (caller restarts on change).
100
+ function writeConfig(containerYaml) {
101
+ const next = buildHostConfig(containerYaml);
102
+ let prev = ""; try { prev = fs.readFileSync(HOST_CONFIG, "utf8"); } catch {}
103
+ fs.mkdirSync(path.dirname(HOST_CONFIG), { recursive: true });
104
+ if (prev === next) return false;
105
+ fs.writeFileSync(HOST_CONFIG, next);
106
+ return true;
107
+ }
108
+
109
+ function readPid() { try { return Number(fs.readFileSync(PID_FILE, "utf8").trim()) || 0; } catch { return 0; } }
110
+ function alive(pid) { if (!pid) return false; try { process.kill(pid, 0); return true; } catch { return false; } }
111
+ function running() { return alive(readPid()); }
112
+
113
+ function stop() {
114
+ const pid = readPid();
115
+ if (alive(pid)) { try { process.kill(pid, "SIGKILL"); } catch {} }
116
+ try { fs.unlinkSync(PID_FILE); } catch {}
117
+ }
118
+
119
+ // Spawn the host mihomo detached so it survives this process. Idempotent: a live
120
+ // instance with the current config is left alone unless force.
121
+ function start({ force = false } = {}) {
122
+ if (!force && running()) return { started: false, adopted: true };
123
+ stop();
124
+ if (!binPresent()) throw new Error("mihomo 二进制不存在(先 ensureBinary)");
125
+ if (!fs.existsSync(HOST_CONFIG)) throw new Error("host 配置不存在(先 writeConfig)");
126
+ const out = fs.openSync(HOST_LOG, "a");
127
+ const child = spawn(binPath(), ["-f", HOST_CONFIG], {
128
+ detached: true, stdio: ["ignore", out, out], windowsHide: true,
129
+ });
130
+ child.unref();
131
+ try { fs.writeFileSync(PID_FILE, String(child.pid)); } catch {}
132
+ return { started: true, pid: child.pid };
133
+ }
134
+
135
+ // Full enable: ensure binary → copy+adapt container config → (re)start.
136
+ // containerYaml is fetched by the caller (sidecar-ipc) via appDocker.
137
+ async function enable({ containerYaml, emit } = {}) {
138
+ await ensureBinary({ emit });
139
+ if (!containerYaml) throw new Error("没有拿到容器 mihomo 配置");
140
+ const changed = writeConfig(containerYaml);
141
+ const res = start({ force: changed });
142
+ emit && emit({ phase: "chrome-proxy", status: "running", message: "Chrome 代理(host mihomo)已就绪" });
143
+ return { ok: true, ...res };
144
+ }
145
+
146
+ module.exports = {
147
+ VER, assetUrl, binPath, binPresent, ensureBinary,
148
+ buildHostConfig, writeConfig, start, stop, running, enable,
149
+ HOST_CONFIG, HOST_LOG,
150
+ };
@@ -425,10 +425,9 @@ async function runContainer({ port = 8009, container = "cicy-code-docker", volum
425
425
  // bridge container → every lookup is EAI_AGAIN and cicy-code's startup `npm i`
426
426
  // crash-loops the container (:8009 never comes up). Pin public resolvers: Aliyun
427
427
  // 223.5.5.5 (CN-fast) first, Google 8.8.8.8 as the overseas fallback.
428
- // 主人(A 方案): 映射 mihomo 的 per-Chrome-profile 监听口段(约定 20000+N)回主机回环 —— docker-only
429
- // 后这些口只在容器里,不暴露则主机系统 Chrome 的代理 127.0.0.1:(20000+N) 连不上. 默认 20001-20032.
430
- const CHROME_PROXY_PORTS = process.env.CICY_CHROME_PROXY_PORTS || "20001-20032";
431
- const cmd = `docker run -d --name ${container} --restart unless-stopped --dns 223.5.5.5 --dns 8.8.8.8 -p 127.0.0.1:${port}:8008 -p 127.0.0.1:${CHROME_PROXY_PORTS}:${CHROME_PROXY_PORTS} -e CICY_PUBLIC=1 -v ${volume}:/home/cicy ${shareMountArg()} ${envArgs} ${IMAGE}`;
428
+ // 主人方案: Chrome 的 per-profile 代理改由「宿主 mihomo」(host-mihomo.js)服务,不再从容器
429
+ // publish 20001-32(WSL/colima 转发那段口到不了容器里只绑 127.0.0.1 的监听)。容器只暴露 :8009。
430
+ const cmd = `docker run -d --name ${container} --restart unless-stopped --dns 223.5.5.5 --dns 8.8.8.8 -p 127.0.0.1:${port}:8008 -e CICY_PUBLIC=1 -v ${volume}:/home/cicy ${shareMountArg()} ${envArgs} ${IMAGE}`;
432
431
  await wslRun(cmd, { timeout: 60000 });
433
432
  ensureDesktopShortcut(volume, port).catch(() => {});
434
433
  return { started: true };
@@ -736,7 +735,14 @@ async function hasGatewayKey(container = "cicy-code-docker") {
736
735
  catch { return false; }
737
736
  }
738
737
 
738
+ // 读容器里 cicy-code 生成的 mihomo.yaml(host-mihomo 用它在 Windows 宿主重建 Chrome 代理配置)。
739
+ async function readMihomoConfig(container = "cicy-code-docker-8009") {
740
+ const { stdout } = await wslRun(`docker exec ${container} cat /home/cicy/cicy-ai/db/mihomo.yaml`, { timeout: 15000 });
741
+ return String(stdout || "");
742
+ }
743
+
739
744
  module.exports = {
740
745
  bootstrap, status, restart, stop, dockerRestart, recreate, update, upgrade, runContainer, readContainerToken,
741
746
  distroInstalled, dockerInstalled, dockerEngineUp, imagePresent, probeHealth, wslRun, hasGatewayKey,
747
+ readMihomoConfig,
742
748
  };
@@ -841,6 +841,12 @@ body {
841
841
  .bcard__menu-item.is-danger:hover { background: rgba(239,68,68,.16); color: #fff; }
842
842
  /* Plain divider line in the DockerCard ⋯ menu (cicy-code ops 上 / Docker 容器 ops 下). */
843
843
  .bcard__menu-sep { height: 1px; margin: 5px 10px; background: rgba(255,255,255,.08); }
844
+ /* Switch-style menu item (Chrome 代理): label 左 + 小开关右. */
845
+ .bcard__menu-item--switch { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
846
+ .bcard__switch { flex: none; width: 30px; height: 17px; border-radius: 999px; background: rgba(255,255,255,.16); position: relative; transition: background .15s; }
847
+ .bcard__switch::after { content: ""; position: absolute; top: 2px; left: 2px; width: 13px; height: 13px; border-radius: 50%; background: #fff; transition: transform .15s; }
848
+ .bcard__switch.is-on { background: var(--accent-text, #4f8cff); }
849
+ .bcard__switch.is-on::after { transform: translateX(13px); }
844
850
  /* Portaled to document.body to escape .bcard's overflow:hidden (which clipped the
845
851
  dropdown). Inline top/left position it under the kebab; here just a high
846
852
  stacking order (below toast/drawer) + word-wrapping items so nothing overflows. */
@@ -1910,6 +1910,19 @@ function DockerCard({ dockerTeam, cloudTitle, onOpen, onRename, onRefresh }) {
1910
1910
  } finally { setBusy(""); checkStatus(); }
1911
1911
  }, [checkStatus]);
1912
1912
 
1913
+ // 「Chrome 代理」开关:在宿主起一个 mihomo 给系统 Chrome 的 per-profile 代理(从容器 cp 配置)。
1914
+ const toggleChromeProxy = useCallback(async (on) => {
1915
+ setMenuOpen(false); setBusy("chromeproxy");
1916
+ toast.show({ id: "docker-op", message: on ? tr("docker.chromeProxyOn", "启用 Chrome 代理(宿主 mihomo)…") : tr("docker.chromeProxyOff", "关闭 Chrome 代理…"), status: "running" });
1917
+ try {
1918
+ const r = await window.cicy.docker.appChromeProxy(on);
1919
+ if (r?.ok) toast.show({ id: "docker-op", message: on ? tr("docker.chromeProxyDone", "Chrome 代理已启用 ✅") : tr("docker.chromeProxyUndone", "Chrome 代理已关闭"), status: "done", ttl: 3000 });
1920
+ else toast.show({ id: "docker-op", message: (r?.error || tr("docker.opFailed", "操作失败")), status: "error", ttl: 6000 });
1921
+ } catch (e) {
1922
+ toast.show({ id: "docker-op", message: e.message, status: "error", ttl: 6000 });
1923
+ } finally { setBusy(""); checkStatus(); }
1924
+ }, [checkStatus]);
1925
+
1913
1926
  // Render on Windows (WSL2) and macOS (Colima) — the two platforms the
1914
1927
  // Docker-版 backend supports. window.cicy.platform is sync, so we can decide
1915
1928
  // immediately without waiting on the async appStatus probe.
@@ -2048,6 +2061,15 @@ function DockerCard({ dockerTeam, cloudTitle, onOpen, onRename, onRefresh }) {
2048
2061
  {tr("docker.authorizeHost", "授权容器访问 Mac")}
2049
2062
  </button>
2050
2063
  )}
2064
+ {/* Chrome 代理:宿主 mihomo 服务系统 Chrome 的 per-profile 代理(mac/win 都有 host Chrome)*/}
2065
+ <button type="button" data-id="DockerCard-chrome-proxy"
2066
+ className="bcard__menu-item bcard__menu-item--switch"
2067
+ role="menuitemcheckbox" aria-checked={!!status?.chromeProxy}
2068
+ title={tr("docker.chromeProxyHint", "在本机起一个 mihomo,给系统 Chrome 的分身代理(127.0.0.1:2000N)。配置从容器同步")}
2069
+ onClick={() => toggleChromeProxy(!status?.chromeProxy)}>
2070
+ <span>{tr("docker.chromeProxy", "Chrome 代理")}</span>
2071
+ <span className={`bcard__switch${status?.chromeProxy ? " is-on" : ""}`} data-id="DockerCard-chrome-proxy-switch" aria-hidden />
2072
+ </button>
2051
2073
  <button type="button" data-id="DockerCard-docker-restart" className="bcard__menu-item"
2052
2074
  onClick={() => runOp("restart", () => window.cicy.docker.appDockerRestart(), tr("docker.dockerRestarted", "已重启 Docker 容器"))}>
2053
2075
  {tr("docker.dockerRestart", "重启 Docker")}