cicy-desktop 2.1.179 → 2.1.181
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 +6 -3
- package/src/main.js +22 -9
- package/src/sidecar/colima-docker.js +70 -43
- package/src/sidecar/docker.js +34 -0
- package/src/sidecar/wsl-docker.js +63 -35
package/package.json
CHANGED
|
@@ -340,13 +340,16 @@ function register({ sidecarLogPath } = {}) {
|
|
|
340
340
|
|
|
341
341
|
// ⋯ menu → 重建 Docker:删容器 + 用新 env(docker team 网关 key)重新 docker run(保留
|
|
342
342
|
// volume 数据)。换 key 的唯一途径。渲染端已 confirm。
|
|
343
|
-
ipcMain.handle("docker:app-recreate", async () => {
|
|
343
|
+
ipcMain.handle("docker:app-recreate", async (e) => {
|
|
344
344
|
try {
|
|
345
345
|
await ensureDockerTeam();
|
|
346
|
-
await appDocker.recreate({
|
|
346
|
+
await appDocker.recreate({
|
|
347
|
+
...(await appOpts()),
|
|
348
|
+
onProgress: (ev) => { try { e.sender.send("docker:app-progress", ev); } catch {} },
|
|
349
|
+
});
|
|
347
350
|
await registerAppTeam();
|
|
348
351
|
return { ok: true };
|
|
349
|
-
} catch (
|
|
352
|
+
} catch (err) { return { ok: false, error: err.message }; }
|
|
350
353
|
});
|
|
351
354
|
|
|
352
355
|
// ⋯ menu(仅 macOS)→ 「授权容器访问 Mac」:不挂 docker,改走 SSH。colima 自带
|
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
|
-
//
|
|
1381
|
-
//
|
|
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
|
|
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
|
-
|
|
1396
|
-
|
|
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
|
-
|
|
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}`);
|
|
@@ -37,6 +37,12 @@ const IMAGE = process.env.CICY_DOCKER_IMAGE || "cicybot/cicy-code:latest";
|
|
|
37
37
|
const IS_ARM = process.arch === "arm64";
|
|
38
38
|
const ARCH_TAG = IS_ARM ? "arm64" : "amd64";
|
|
39
39
|
const PLATFORM_FLAG = IS_ARM ? "--platform linux/amd64" : "";
|
|
40
|
+
// 主人: 容器额外暴露的端口段(给容器内 agent 跑服务用),宿主 127.0.0.1 直达。
|
|
41
|
+
const EXTRA_PORTS = process.env.CICY_EXTRA_PORTS || "18000-19999";
|
|
42
|
+
// 主人: Colima VM 资源,4C8G 起步(可用 env 覆盖,不写死)。
|
|
43
|
+
const VM_CPUS = process.env.CICY_COLIMA_CPUS || "4";
|
|
44
|
+
const VM_MEMORY = process.env.CICY_COLIMA_MEMORY || "8";
|
|
45
|
+
const VM_DISK = process.env.CICY_COLIMA_DISK || "30";
|
|
40
46
|
|
|
41
47
|
// Colima 基础 VM 镜像:colima `start` 默认从 github.com/abiosoft/colima-core 下,
|
|
42
48
|
// CN 直接 EOF 拉不下来(真机实测)。colima 0.10.3 没有 --disk-image-mirror(那是
|
|
@@ -222,7 +228,7 @@ async function startVM({ emit } = {}) {
|
|
|
222
228
|
const vzArgs = IS_ARM
|
|
223
229
|
? `--vm-type vz --vz-rosetta --mount-type virtiofs`
|
|
224
230
|
: `--vm-type vz --mount-type virtiofs`;
|
|
225
|
-
const common = `--cpus
|
|
231
|
+
const common = `--cpus ${VM_CPUS} --memory ${VM_MEMORY} --disk ${VM_DISK} ${diskArgs}`;
|
|
226
232
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
227
233
|
emit && emit({ phase: "container", status: "running", message: `启动 Colima 运行环境(首次较慢,请耐心)…${attempt > 1 ? `(重试 ${attempt})` : ""}` });
|
|
228
234
|
try {
|
|
@@ -263,6 +269,28 @@ async function loadImage(tarball, { emit } = {}) {
|
|
|
263
269
|
if (m && m[1] !== IMAGE) { try { await dk(`tag ${m[1]} ${IMAGE}`, { timeout: 15000 }); } catch {} }
|
|
264
270
|
}
|
|
265
271
|
|
|
272
|
+
// 主人修复「为什么没用最新的 docker」: imagePresent() 只看本地有没有 `:latest` 标签,
|
|
273
|
+
// 旧镜像在就永远跳过下载 → 卡在过期镜像。这里改成校验 OSS tarball 的 ETag:本地缺镜像、
|
|
274
|
+
// 或 OSS 的 ETag 跟上次 load 的不一致 → 重下重载(删旧缓存包,避免同尺寸被误跳过)。
|
|
275
|
+
// 非破坏性:不删 VM、不删 volume(数据/token 不丢)。返回是否真的刷新了。
|
|
276
|
+
async function ensureFreshImage({ emit } = {}) {
|
|
277
|
+
const e = (ev) => { try { emit && emit(ev); } catch {} };
|
|
278
|
+
const present = await imagePresent();
|
|
279
|
+
const remote = await docker.remoteImageEtag().catch(() => "");
|
|
280
|
+
const loaded = docker.readLoadedImageEtag();
|
|
281
|
+
// 本地有镜像、且能证明是最新(ETag 一致)→ 不动。HEAD 失败(remote 空)时不强刷,
|
|
282
|
+
// 避免离线/被墙时把能用的旧镜像也拖垮。
|
|
283
|
+
if (present && remote && loaded && remote === loaded) return false;
|
|
284
|
+
if (present && !remote) return false; // 拿不到远端指纹,保守不刷
|
|
285
|
+
if (present && remote !== loaded) { e({ phase: "image", status: "running", message: "检测到更新的镜像,正在拉取最新版…" }); docker.clearImageTarball(); }
|
|
286
|
+
let tarball;
|
|
287
|
+
try { tarball = await docker.downloadImageTarball({ emit }); }
|
|
288
|
+
catch (err) { if (present) { e({ phase: "image", status: "running", message: `最新镜像下载失败(${err.message}),沿用现有镜像` }); return false; } throw err; }
|
|
289
|
+
await loadImage(tarball, { emit });
|
|
290
|
+
if (remote) docker.writeLoadedImageEtag(remote);
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
|
|
266
294
|
// 宿主 127.0.0.1:port 健康探针 —— Lima 自动把 VM 内 127.0.0.1 端口转发到宿主。
|
|
267
295
|
const probeHealth = docker.probeHealth;
|
|
268
296
|
|
|
@@ -277,44 +305,41 @@ const probeHealth = docker.probeHealth;
|
|
|
277
305
|
// bind-mount 会用空的宿主目录**遮住镜像里预装的 /home/cicy**(cicy-code 装在那),
|
|
278
306
|
// entrypoint 找不到就试图全局 npm 重装 → EACCES 崩溃,:8009 起不来。named volume
|
|
279
307
|
// 首次挂载会**从镜像内容预填充**,容器才看得到预装的 cicy-code(和 WSL 一致)。
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
-
const SHARE_README = `# CiCy 共享目录 / Shared Folder
|
|
308
|
+
// 主人: 把宿主 ~/projects 挂进容器 /home/cicy/projects(~ 用 os.homedir() 展开,绝不写死)。
|
|
309
|
+
// Colima 把宿主 $HOME 挂进 VM,所以直接路径就能用(不像 WSL 要 /mnt 转换)。
|
|
310
|
+
// 源目录不存在先建出来,否则 docker 建挂载点会报错挡住容器启动。
|
|
311
|
+
const PROJECTS_README = `# CiCy 持久工作区 / Persistent Workspace
|
|
285
312
|
|
|
286
|
-
这个文件夹与 CiCy
|
|
313
|
+
这个文件夹与 CiCy 容器双向共享,而且是**持久的** —— 它在你电脑的真实磁盘上,
|
|
314
|
+
不在容器里。容器删了、重建了、升级了,这里的文件都还在,不会丢。
|
|
287
315
|
|
|
288
|
-
- 你的电脑上: ~/
|
|
289
|
-
- 容器里: /home/cicy/
|
|
316
|
+
- 你的电脑上: ~/projects (就是这个文件夹)
|
|
317
|
+
- 容器里: /home/cicy/projects
|
|
290
318
|
|
|
291
|
-
|
|
319
|
+
把代码仓库 / 项目放在这里,CiCy 里的 agent 就能直接读写;它们改的东西也实时
|
|
320
|
+
出现在你电脑上。容器是临时的,这个目录才是你工作的家。
|
|
292
321
|
|
|
293
322
|
---
|
|
294
323
|
|
|
295
|
-
This folder is shared both ways
|
|
324
|
+
This folder is shared both ways with the CiCy container and is **persistent** —
|
|
325
|
+
it lives on your computer's real disk, not inside the container. Delete, rebuild
|
|
326
|
+
or upgrade the container and everything here survives.
|
|
296
327
|
|
|
297
|
-
- On your computer: ~/
|
|
298
|
-
- Inside the container: /home/cicy/
|
|
328
|
+
- On your computer: ~/projects (this folder)
|
|
329
|
+
- Inside the container: /home/cicy/projects
|
|
299
330
|
|
|
300
|
-
|
|
331
|
+
Put your repos / projects here; CiCy agents read and write them directly, and
|
|
332
|
+
their changes show up live on your machine. The container is disposable — this
|
|
333
|
+
directory is where your work actually lives.
|
|
301
334
|
`;
|
|
302
335
|
|
|
303
|
-
function
|
|
336
|
+
function projectsMountArg() {
|
|
304
337
|
try {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
fs.mkdirSync(share, { recursive: true });
|
|
311
|
-
const readme = path.join(share, "readme.md");
|
|
312
|
-
if (!fs.existsSync(readme)) { try { fs.writeFileSync(readme, SHARE_README); } catch {} }
|
|
313
|
-
try {
|
|
314
|
-
const link = path.join(os.homedir(), "Desktop", "Share");
|
|
315
|
-
if (!fs.existsSync(link)) fs.symlinkSync(share, link);
|
|
316
|
-
} catch {}
|
|
317
|
-
return `-v '${share}':/home/cicy/cicy-ai/Share`;
|
|
338
|
+
const dir = path.join(os.homedir(), "projects");
|
|
339
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
340
|
+
const readme = path.join(dir, "README.md");
|
|
341
|
+
if (!fs.existsSync(readme)) { try { fs.writeFileSync(readme, PROJECTS_README); } catch {} }
|
|
342
|
+
return `-v '${dir}':/home/cicy/projects`;
|
|
318
343
|
} catch { return ""; }
|
|
319
344
|
}
|
|
320
345
|
|
|
@@ -329,15 +354,16 @@ async function runContainer({ port = 8009, container = "cicy-code-docker-8009",
|
|
|
329
354
|
// 服务,不再从容器 publish 20001-32 —— colima/Lima 转发那个端口段始终到不了容器里只绑
|
|
330
355
|
// 127.0.0.1 的监听(Chrome 一直 ERR_EMPTY_RESPONSE)。容器只暴露 cicy-code 的 :8009。
|
|
331
356
|
const mk = (s) => `run -d --name ${container} --restart unless-stopped ${PLATFORM_FLAG} ` +
|
|
332
|
-
`-p 127.0.0.1:${port}:8008 -
|
|
333
|
-
|
|
357
|
+
`-p 127.0.0.1:${port}:8008 -p 127.0.0.1:${EXTRA_PORTS}:${EXTRA_PORTS} ` +
|
|
358
|
+
`-e CICY_PUBLIC=1 -v ${volume}:/home/cicy ${s} ${envArgs} ${IMAGE}`;
|
|
359
|
+
const mounts = projectsMountArg();
|
|
334
360
|
try {
|
|
335
|
-
await dk(mk(
|
|
361
|
+
await dk(mk(mounts), { timeout: 90000 });
|
|
336
362
|
} catch (e) {
|
|
337
|
-
if (!
|
|
338
|
-
// 兜底:
|
|
339
|
-
// 一定能起(
|
|
340
|
-
console.warn(`[colima]
|
|
363
|
+
if (!mounts) throw e;
|
|
364
|
+
// 兜底: 带挂载启动失败(如 colima 访问挂载源出错)→ 去掉附加挂载重试,保证容器
|
|
365
|
+
// 一定能起(projects 只是附加共享目录,不该挡住整个服务). 主人: '容器起不来' 的修复.
|
|
366
|
+
console.warn(`[colima] 带挂载启动失败,去掉附加挂载重试: ${e.message}`);
|
|
341
367
|
try { await dk(`rm -f ${container}`, { timeout: 20000 }); } catch {}
|
|
342
368
|
await dk(mk(""), { timeout: 90000 });
|
|
343
369
|
}
|
|
@@ -445,13 +471,11 @@ async function _bootstrap({ onProgress, port = 8009, container = "cicy-code-dock
|
|
|
445
471
|
}
|
|
446
472
|
}
|
|
447
473
|
|
|
448
|
-
// 4) 镜像(docker load R2 包)
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
try { await loadImage(tarball, { emit }); }
|
|
454
|
-
catch (e) { emit({ phase: "image", status: "error", message: `镜像导入失败:${e.message}(点重试)` }); return { ok: false, reason: "image_load_failed" }; }
|
|
474
|
+
// 4) 镜像(docker load R2 包)—— 缺镜像就下,镜像在但 OSS 有更新版也刷新(主人:别卡旧镜像)
|
|
475
|
+
try { await ensureFreshImage({ emit }); }
|
|
476
|
+
catch (e) {
|
|
477
|
+
if (!(await imagePresent())) { emit({ phase: "image", status: "error", message: `镜像下载失败:${e.message}(点重试续传)` }); return { ok: false, reason: "image_download_failed" }; }
|
|
478
|
+
emit({ phase: "image", status: "running", message: `镜像刷新失败(${e.message}),沿用现有镜像` });
|
|
455
479
|
}
|
|
456
480
|
|
|
457
481
|
// 5) 容器
|
|
@@ -507,7 +531,10 @@ async function dockerRestart({ container = "cicy-code-docker-8009" } = {}) {
|
|
|
507
531
|
|
|
508
532
|
// 重建:强删占该端口的任何容器 + 目标容器,再 docker run(用新 env,如新 docker team
|
|
509
533
|
// 网关 key)。保留 bind-mount 宿主目录(数据/api_token 不丢)。破坏性 → 调用方要 confirm。
|
|
510
|
-
async function recreate({ port = 8009, container = "cicy-code-docker-8009", volume = "cicy-team-8009", env = {} } = {}) {
|
|
534
|
+
async function recreate({ onProgress, port = 8009, container = "cicy-code-docker-8009", volume = "cicy-team-8009", env = {} } = {}) {
|
|
535
|
+
const emit = (ev) => { try { onProgress && onProgress(ev); } catch {} };
|
|
536
|
+
// 重建 = 用最新镜像重建。OSS 有更新版先刷新(非破坏性,不删 VM/volume),再 rm + run。
|
|
537
|
+
try { await ensureFreshImage({ emit }); } catch (e) { emit({ phase: "image", status: "running", message: `镜像刷新跳过(${e.message}),用现有镜像重建` }); }
|
|
511
538
|
try { await dk(`ps -aq --filter publish=${port} | xargs -r docker --context ${CTX} rm -f 2>/dev/null; docker --context ${CTX} rm -f ${container} 2>/dev/null; true`, { timeout: 30000 }); } catch {}
|
|
512
539
|
const r = await runContainer({ port, container, volume, env });
|
|
513
540
|
try { await ensureDesktopShortcut(volume, port); } catch {}
|
package/src/sidecar/docker.js
CHANGED
|
@@ -181,6 +181,38 @@ function headSize(url, hops = 5) {
|
|
|
181
181
|
});
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
// HEAD the URL and return a freshness fingerprint (ETag, else Last-Modified).
|
|
185
|
+
// Mirrors headSize (follows redirects, node http so it works without curl).
|
|
186
|
+
// "" on any failure → callers treat unknown as "can't prove fresh".
|
|
187
|
+
function headETag(url, hops = 5) {
|
|
188
|
+
return new Promise((resolve) => {
|
|
189
|
+
if (hops <= 0) return resolve("");
|
|
190
|
+
const lib = url.startsWith("https:") ? https : http;
|
|
191
|
+
const req = lib.request(url, { method: "HEAD", timeout: 15000, headers: { "User-Agent": DL_UA } }, (res) => {
|
|
192
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
193
|
+
res.resume();
|
|
194
|
+
return headETag(res.headers.location, hops - 1).then(resolve);
|
|
195
|
+
}
|
|
196
|
+
resolve(String(res.headers["etag"] || res.headers["last-modified"] || "").trim());
|
|
197
|
+
});
|
|
198
|
+
req.on("error", () => resolve(""));
|
|
199
|
+
req.on("timeout", () => { req.destroy(); resolve(""); });
|
|
200
|
+
req.end();
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Marker recording the OSS fingerprint of the image tarball we last `docker load`ed.
|
|
205
|
+
// Lets recreate/bootstrap detect a refreshed OSS image (same `:latest` tag, new
|
|
206
|
+
// content) and re-pull — without it, `imagePresent()` stays true forever and the
|
|
207
|
+
// machine is pinned to a stale image (主人 bug: "为什么没用最新的docker").
|
|
208
|
+
function imageEtagPath() { return path.join(downloadsDir(), "cicy-code-latest.etag"); }
|
|
209
|
+
function readLoadedImageEtag() { try { return fs.readFileSync(imageEtagPath(), "utf8").trim(); } catch { return ""; } }
|
|
210
|
+
function writeLoadedImageEtag(v) { try { fs.writeFileSync(imageEtagPath(), String(v || "")); } catch {} }
|
|
211
|
+
async function remoteImageEtag() { return headETag(R2_TARBALL); }
|
|
212
|
+
// Drop the cached tarball so a stale copy on disk can't be reused (downloadImageTarball
|
|
213
|
+
// skips a complete file by size, which a same-size new image would wrongly satisfy).
|
|
214
|
+
function clearImageTarball() { try { fs.unlinkSync(imageTarballPath()); } catch {} }
|
|
215
|
+
|
|
184
216
|
// Download `url`→`dest` but: SKIP if the file is already complete, RESUME if it's
|
|
185
217
|
// a partial, retry with progress, fall back to `mirror`. This is the core of the
|
|
186
218
|
// user's "下载了就不重复下载 / 步骤走过的不要再走".
|
|
@@ -735,4 +767,6 @@ module.exports = {
|
|
|
735
767
|
launchElevated, wslMissing, ensureWsl,
|
|
736
768
|
// platform-agnostic download/retry primitives, reused by native.js
|
|
737
769
|
ensureDownloaded, curlDownload, withRetry, waitUntil, run, headSize,
|
|
770
|
+
// image freshness (主人: 修「重建仍用旧镜像」—— 校验 OSS ETag 变了才重下重载)
|
|
771
|
+
headETag, remoteImageEtag, readLoadedImageEtag, writeLoadedImageEtag, clearImageTarball,
|
|
738
772
|
};
|
|
@@ -21,6 +21,8 @@ const log = require("electron-log"); // persisted main.log — bootstrap timing/
|
|
|
21
21
|
// Dedicated distro name — NEVER reuse/clobber a user's own "Ubuntu" distro.
|
|
22
22
|
const DISTRO = process.env.CICY_WSL_DISTRO || "cicy-code-wsl";
|
|
23
23
|
const IMAGE = process.env.CICY_DOCKER_IMAGE || "cicybot/cicy-code:latest";
|
|
24
|
+
// 主人: 容器额外暴露的端口段(给容器内 agent 跑服务用),宿主 127.0.0.1 直达。
|
|
25
|
+
const EXTRA_PORTS = process.env.CICY_EXTRA_PORTS || "18000-19999";
|
|
24
26
|
// PRE-BAKED WSL rootfs (built in CI, .github/workflows/build-wsl-package.yml):
|
|
25
27
|
// Ubuntu 22.04 + Docker Engine + the cicy-code image already loaded into
|
|
26
28
|
// /var/lib/docker, with dockerd auto-start via /etc/wsl.conf. We just download
|
|
@@ -360,6 +362,25 @@ async function loadImage(winTarballPath, { emit } = {}) {
|
|
|
360
362
|
if (m && m[1] !== IMAGE) { try { await wslRun(`docker tag ${m[1]} ${IMAGE}`, { timeout: 15000 }); } catch {} }
|
|
361
363
|
}
|
|
362
364
|
|
|
365
|
+
// 主人修复「卡在旧镜像」: imagePresent() 只看本地有没有 `:latest`,旧镜像在就永远跳过下载。
|
|
366
|
+
// 改成校验 OSS tarball ETag:缺镜像、或 ETag 跟上次 load 不一致 → 重下重载(删旧缓存包)。
|
|
367
|
+
// 非破坏性:不删发行版/不删 volume。返回是否真刷新了。与 colima 端逻辑一致。
|
|
368
|
+
async function ensureFreshImage({ emit } = {}) {
|
|
369
|
+
const e = (ev) => { try { emit && emit(ev); } catch {} };
|
|
370
|
+
const present = await imagePresent();
|
|
371
|
+
const remote = await docker.remoteImageEtag().catch(() => "");
|
|
372
|
+
const loaded = docker.readLoadedImageEtag();
|
|
373
|
+
if (present && remote && loaded && remote === loaded) return false;
|
|
374
|
+
if (present && !remote) return false; // 拿不到远端指纹,保守不刷
|
|
375
|
+
if (present && remote !== loaded) { e({ phase: "image", status: "running", message: "检测到更新的镜像,正在拉取最新版…" }); docker.clearImageTarball(); }
|
|
376
|
+
let tarball;
|
|
377
|
+
try { tarball = await docker.downloadImageTarball({ emit }); }
|
|
378
|
+
catch (err) { if (present) { e({ phase: "image", status: "running", message: `最新镜像下载失败(${err.message}),沿用现有镜像` }); return false; } throw err; }
|
|
379
|
+
await loadImage(tarball, { emit });
|
|
380
|
+
if (remote) docker.writeLoadedImageEtag(remote);
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
|
|
363
384
|
// HTTP /health probe on 127.0.0.1:port from Windows — WSL2 forwards localhost,
|
|
364
385
|
// so a container published on :port is reachable here. Reuse docker.js's probe.
|
|
365
386
|
const probeHealth = docker.probeHealth;
|
|
@@ -375,40 +396,47 @@ const probeHealth = docker.probeHealth;
|
|
|
375
396
|
// network-exposed; the api_token gates access. WSL2's localhost relay then
|
|
376
397
|
// forwards the distro's 127.0.0.1:<port> to Windows 127.0.0.1:<port>.
|
|
377
398
|
// Only :<port> is published — sshd/cron stay inside the container's own netns.
|
|
378
|
-
//
|
|
379
|
-
// /home/cicy/
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
// Returns the
|
|
383
|
-
const
|
|
399
|
+
// Persistent workspace bind: the CURRENT Windows user's ~/projects ↔
|
|
400
|
+
// /home/cicy/projects in the container — both the user and the agent read/write
|
|
401
|
+
// it, and it lives on the host disk so it survives container delete/rebuild.
|
|
402
|
+
// Auto-created (with a README) on every container start if missing; os.homedir()
|
|
403
|
+
// makes it per-user (never hard-coded). Returns the `-v` arg, or "" on failure.
|
|
404
|
+
const PROJECTS_README = `# CiCy 持久工作区 / Persistent Workspace
|
|
384
405
|
|
|
385
|
-
这个文件夹与 CiCy
|
|
406
|
+
这个文件夹与 CiCy 容器双向共享,而且是**持久的** —— 它在你电脑的真实磁盘上,
|
|
407
|
+
不在容器里。容器删了、重建了、升级了,这里的文件都还在,不会丢。
|
|
386
408
|
|
|
387
|
-
- 你的电脑上: ~/
|
|
388
|
-
- 容器里: /home/cicy/
|
|
409
|
+
- 你的电脑上: ~/projects (就是这个文件夹)
|
|
410
|
+
- 容器里: /home/cicy/projects
|
|
389
411
|
|
|
390
|
-
|
|
412
|
+
把代码仓库 / 项目放在这里,CiCy 里的 agent 就能直接读写;它们改的东西也实时
|
|
413
|
+
出现在你电脑上。容器是临时的,这个目录才是你工作的家。
|
|
391
414
|
|
|
392
415
|
---
|
|
393
416
|
|
|
394
|
-
This folder is shared both ways
|
|
417
|
+
This folder is shared both ways with the CiCy container and is **persistent** —
|
|
418
|
+
it lives on your computer's real disk, not inside the container. Delete, rebuild
|
|
419
|
+
or upgrade the container and everything here survives.
|
|
395
420
|
|
|
396
|
-
- On your computer: ~/
|
|
397
|
-
- Inside the container: /home/cicy/
|
|
421
|
+
- On your computer: ~/projects (this folder)
|
|
422
|
+
- Inside the container: /home/cicy/projects
|
|
398
423
|
|
|
399
|
-
|
|
424
|
+
Put your repos / projects here; CiCy agents read and write them directly, and
|
|
425
|
+
their changes show up live on your machine. The container is disposable — this
|
|
426
|
+
directory is where your work actually lives.
|
|
400
427
|
`;
|
|
401
428
|
|
|
402
|
-
|
|
429
|
+
// 主人: 把宿主 ~/projects 挂进容器 /home/cicy/projects(~ 用 os.homedir() 展开,绝不写死)。
|
|
430
|
+
// Windows 路径 C:\Users\<user>\projects → WSL 视图 /mnt/c/Users/<user>/projects。
|
|
431
|
+
function projectsMountArg() {
|
|
403
432
|
try {
|
|
404
|
-
const
|
|
405
|
-
fs.mkdirSync(
|
|
406
|
-
const readme = path.join(
|
|
407
|
-
if (!fs.existsSync(readme)) { try { fs.writeFileSync(readme,
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
} catch (e) { log.warn(`[wsl-docker] Share mount setup failed: ${e.message}`); return ""; }
|
|
433
|
+
const winProjects = path.join(os.homedir(), "projects");
|
|
434
|
+
fs.mkdirSync(winProjects, { recursive: true });
|
|
435
|
+
const readme = path.join(winProjects, "README.md");
|
|
436
|
+
if (!fs.existsSync(readme)) { try { fs.writeFileSync(readme, PROJECTS_README); } catch {} }
|
|
437
|
+
const wslProjects = winProjects.replace(/^([A-Za-z]):/, (_, d) => `/mnt/${d.toLowerCase()}`).replace(/\\/g, "/");
|
|
438
|
+
return `-v '${wslProjects}':/home/cicy/projects`;
|
|
439
|
+
} catch (e) { log.warn(`[wsl-docker] projects mount setup failed: ${e.message}`); return ""; }
|
|
412
440
|
}
|
|
413
441
|
|
|
414
442
|
async function runContainer({ port = 8009, container = "cicy-code-docker", volume = "cicy-team-8009", env = {} } = {}) {
|
|
@@ -427,7 +455,7 @@ async function runContainer({ port = 8009, container = "cicy-code-docker", volum
|
|
|
427
455
|
// 223.5.5.5 (CN-fast) first, Google 8.8.8.8 as the overseas fallback.
|
|
428
456
|
// 主人方案: Chrome 的 per-profile 代理改由「宿主 mihomo」(host-mihomo.js)服务,不再从容器
|
|
429
457
|
// 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 ${
|
|
458
|
+
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:${EXTRA_PORTS}:${EXTRA_PORTS} -e CICY_PUBLIC=1 -v ${volume}:/home/cicy ${projectsMountArg()} ${envArgs} ${IMAGE}`;
|
|
431
459
|
await wslRun(cmd, { timeout: 60000 });
|
|
432
460
|
ensureDesktopShortcut(volume, port).catch(() => {});
|
|
433
461
|
return { started: true };
|
|
@@ -616,17 +644,14 @@ async function _bootstrap({ onProgress, port = 8009, container = "cicy-code-dock
|
|
|
616
644
|
done();
|
|
617
645
|
} else done(true);
|
|
618
646
|
|
|
619
|
-
// 5) Base image — pre-baked into the package,
|
|
620
|
-
//
|
|
647
|
+
// 5) Base image — pre-baked into the package, normally just confirms; but if the
|
|
648
|
+
// OSS tarball has a newer build (ETag changed) it refreshes (主人:别卡旧镜像)。
|
|
621
649
|
begin("ensure-image");
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
catch (e) { fail("image_load_failed", e.message); emit({ phase: "image", status: "error", message: `镜像导入失败:${e.message}(点重试)` }); finish(false, "image_load_failed"); return { ok: false, reason: "image_load_failed" }; }
|
|
628
|
-
done();
|
|
629
|
-
} else done(true);
|
|
650
|
+
try { await ensureFreshImage({ emit }); done(); }
|
|
651
|
+
catch (e) {
|
|
652
|
+
if (!(await imagePresent())) { fail("image_download_failed", e.message); emit({ phase: "image", status: "error", message: `镜像下载失败:${e.message}(点重试续传)` }); finish(false, "image_download_failed"); return { ok: false, reason: "image_download_failed" }; }
|
|
653
|
+
emit({ phase: "image", status: "running", message: `镜像刷新失败(${e.message}),沿用现有镜像` }); done(true);
|
|
654
|
+
}
|
|
630
655
|
|
|
631
656
|
// 6) Container (phase "container" = 启动服务)
|
|
632
657
|
begin("run-container");
|
|
@@ -698,7 +723,10 @@ async function dockerRestart({ container = "cicy-code-docker-8009" } = {}) {
|
|
|
698
723
|
// 重建容器:docker rm -f 旧容器 + docker run 新容器(用新 env,如新的 docker team 网关
|
|
699
724
|
// key)。**保留 volume**(数据/api_token/deviceId 不丢),只是换掉容器本身 + env。
|
|
700
725
|
// 破坏性(短暂中断 + 换 key)→ 调用方要 confirm。
|
|
701
|
-
async function recreate({ port = 8009, container = "cicy-code-docker-8009", volume = "cicy-team-8009", env = {} } = {}) {
|
|
726
|
+
async function recreate({ onProgress, port = 8009, container = "cicy-code-docker-8009", volume = "cicy-team-8009", env = {} } = {}) {
|
|
727
|
+
const emit = (ev) => { try { onProgress && onProgress(ev); } catch {} };
|
|
728
|
+
// 重建 = 用最新镜像重建。OSS 有更新版先刷新(非破坏性,不删发行版/volume),再 rm + run。
|
|
729
|
+
try { await ensureFreshImage({ emit }); } catch (e) { emit({ phase: "image", status: "running", message: `镜像刷新跳过(${e.message}),用现有镜像重建` }); }
|
|
702
730
|
// 强删占用该端口的**任何**容器(含老名字 cicy-code-docker)+ 目标容器 —— 否则
|
|
703
731
|
// runContainer 开头的 probeHealth 看到旧容器还健康会 adopt 它、不重建,key 就换不了。
|
|
704
732
|
try { await wslRun(`docker ps -aq --filter publish=${port} | xargs -r docker rm -f 2>/dev/null; docker rm -f ${container} 2>/dev/null; true`, { timeout: 30000 }); } catch {}
|