cicy-desktop 2.1.121 → 2.1.123
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 +6 -6
- package/src/backends/local-teams.js +18 -0
- package/src/backends/sidecar-ipc.js +14 -3
- package/src/sidecar/wsl-docker.js +67 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cicy-desktop",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.123",
|
|
4
4
|
"description": "CiCy - AI-powered operating system browser",
|
|
5
5
|
"main": "src/main.js",
|
|
6
6
|
"bin": {
|
|
@@ -135,11 +135,11 @@
|
|
|
135
135
|
"//optionalDependencies": "Runtime Bundle v1 (主人指令): platform binaries delivered by `npm i -g cicy-desktop` itself — npm installs only the current-platform subpackage (os/cpu pinned in each), so first start seeds the runtime store with ZERO network, ZERO npx. Windows packages are named *-windows-* (npm spam filter 403s new names containing win32). cicy-msys2 added once published.",
|
|
136
136
|
"optionalDependencies": {
|
|
137
137
|
"electron": "41.0.3",
|
|
138
|
-
"cicy-code-darwin-x64": "2.3.
|
|
139
|
-
"cicy-code-darwin-arm64": "2.3.
|
|
140
|
-
"cicy-code-linux-x64": "2.3.
|
|
141
|
-
"cicy-code-linux-arm64": "2.3.
|
|
142
|
-
"cicy-code-windows-x64": "2.3.
|
|
138
|
+
"cicy-code-darwin-x64": "2.3.20",
|
|
139
|
+
"cicy-code-darwin-arm64": "2.3.20",
|
|
140
|
+
"cicy-code-linux-x64": "2.3.20",
|
|
141
|
+
"cicy-code-linux-arm64": "2.3.20",
|
|
142
|
+
"cicy-code-windows-x64": "2.3.20",
|
|
143
143
|
"cicy-mihomo-darwin-x64": "1.10.4",
|
|
144
144
|
"cicy-mihomo-darwin-arm64": "1.10.4",
|
|
145
145
|
"cicy-mihomo-linux-x64": "1.10.4",
|
|
@@ -562,6 +562,19 @@ async function addTeam(spec) {
|
|
|
562
562
|
: slugifyId(spec.id || baseUrlKey || (port ? `local-${port}` : "local"));
|
|
563
563
|
if (!id) return { ok: false, error: "could not derive id" };
|
|
564
564
|
|
|
565
|
+
// The Docker app node MUST self-identify as is_docker AT CREATION — atomically,
|
|
566
|
+
// in the SAME writeNodes as the node itself — not in a later updateTeam. Reason:
|
|
567
|
+
// addTeam fires syncNameToCloud(id) below (fire-and-forget). If is_docker isn't
|
|
568
|
+
// already on the node, that first sync sees a plain local team, device-registers
|
|
569
|
+
// it, and the cloud hands back THIS DEVICE's shared team (= the 8008 team) — so
|
|
570
|
+
// :8009 and :8008 end up on one cloud_team_id and renaming one renames both
|
|
571
|
+
// ("串名"). Marking is_docker here (explicit spec OR by the docker app port, which
|
|
572
|
+
// doesn't depend on the caller setting a flag) closes that window for ALL creation
|
|
573
|
+
// paths (sidecar registerAppTeam, a cloud deeplink, a manual add). cloud_team_id
|
|
574
|
+
// (the node's OWN independent team) is written the same atomic way when known.
|
|
575
|
+
const DOCKER_PORT = String(process.env.CICY_DOCKER_APP_PORT || 8009);
|
|
576
|
+
const isDockerNode = !!spec.is_docker || (port != null && String(port) === DOCKER_PORT);
|
|
577
|
+
|
|
565
578
|
const now = new Date().toISOString();
|
|
566
579
|
const patch = {
|
|
567
580
|
name: spec.name !== undefined ? String(spec.name || unnamedName()) : undefined,
|
|
@@ -574,6 +587,11 @@ async function addTeam(spec) {
|
|
|
574
587
|
install_path: spec.install_path ?? undefined,
|
|
575
588
|
container_name: spec.container_name ?? undefined,
|
|
576
589
|
image: spec.image ?? undefined,
|
|
590
|
+
// is_docker: only ever set TRUE (never flip an existing node to false).
|
|
591
|
+
is_docker: isDockerNode ? true : undefined,
|
|
592
|
+
// cloud_team_id: set only when the caller passes a real id — a falsy value is
|
|
593
|
+
// dropped so we never clobber an already-correct independent team with null.
|
|
594
|
+
cloud_team_id: spec.cloud_team_id ? spec.cloud_team_id : undefined,
|
|
577
595
|
};
|
|
578
596
|
// Drop undefined keys so we never overwrite existing fields with null.
|
|
579
597
|
Object.keys(patch).forEach(k => patch[k] === undefined && delete patch[k]);
|
|
@@ -203,9 +203,20 @@ function register({ sidecarLogPath } = {}) {
|
|
|
203
203
|
const lt = require("./local-teams");
|
|
204
204
|
await ensureDockerTeam(); // 确保独立云端 team 存在 + 拿到 teamId/title
|
|
205
205
|
const title = (dockerTeamReg && dockerTeamReg.title) || "Docker 团队";
|
|
206
|
-
|
|
207
|
-
//
|
|
208
|
-
//
|
|
206
|
+
// is_docker + cloud_team_id are passed INTO addTeam so they land in the SAME
|
|
207
|
+
// writeNodes as the node — before addTeam's fire-and-forget syncNameToCloud
|
|
208
|
+
// runs. That's what stops the freshly-created :8009 node from device-registering
|
|
209
|
+
// into THIS device's shared (8008) team and 串名. (addTeam also self-detects
|
|
210
|
+
// is_docker by the :8009 port, so this is belt-and-suspenders.)
|
|
211
|
+
const r = await lt.addTeam({
|
|
212
|
+
base_url: `http://127.0.0.1:${APP_PORT}`,
|
|
213
|
+
name: title,
|
|
214
|
+
skipTokenAutofill: true,
|
|
215
|
+
is_docker: true,
|
|
216
|
+
cloud_team_id: (dockerTeamReg && dockerTeamReg.teamId) || undefined,
|
|
217
|
+
});
|
|
218
|
+
// Belt: re-affirm on the persisted node (no-op when addTeam already set them;
|
|
219
|
+
// also late-binds cloud_team_id if ensureDockerTeam minted the team just now).
|
|
209
220
|
if (r && r.id) {
|
|
210
221
|
try { await lt.updateTeam(r.id, { is_docker: true, ...(dockerTeamReg && dockerTeamReg.teamId ? { cloud_team_id: dockerTeamReg.teamId } : {}) }); } catch {}
|
|
211
222
|
}
|
|
@@ -16,6 +16,7 @@ const path = require("path");
|
|
|
16
16
|
const os = require("os");
|
|
17
17
|
const fs = require("fs");
|
|
18
18
|
const docker = require("./docker"); // shared: downloads, waitUntil, probeHealth, launchElevated, ensureWsl…
|
|
19
|
+
const log = require("electron-log"); // persisted main.log — bootstrap timing/failures land here
|
|
19
20
|
|
|
20
21
|
// Dedicated distro name — NEVER reuse/clobber a user's own "Ubuntu" distro.
|
|
21
22
|
const DISTRO = process.env.CICY_WSL_DISTRO || "cicy-code-wsl";
|
|
@@ -182,6 +183,8 @@ async function startEngine() {
|
|
|
182
183
|
// runtime files, (re)launches dockerd, and waits for the socket; between
|
|
183
184
|
// attempts we hard-kill any half-dead daemon so the next launch is clean.
|
|
184
185
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
186
|
+
const at0 = Date.now();
|
|
187
|
+
log.info(`[startEngine] attempt ${attempt}/3 — (re)launch dockerd + wait for socket`);
|
|
185
188
|
try {
|
|
186
189
|
await wslRun(
|
|
187
190
|
"update-alternatives --set iptables /usr/sbin/iptables-legacy >/dev/null 2>&1; " +
|
|
@@ -199,8 +202,9 @@ async function startEngine() {
|
|
|
199
202
|
// cold again (a restart loop → 「引擎没起来」even though dockerd was fine).
|
|
200
203
|
"for i in $(seq 1 120); do [ -S /var/run/docker.sock ] && docker version >/dev/null 2>&1 && break; sleep 1; done",
|
|
201
204
|
{ timeout: 150000 });
|
|
202
|
-
} catch {}
|
|
203
|
-
if (await dockerEngineUp()) return true;
|
|
205
|
+
} catch (e) { log.warn(`[startEngine] attempt ${attempt} launch/wait errored: ${e.message}`); }
|
|
206
|
+
if (await dockerEngineUp()) { log.info(`[startEngine] ✓ dockerd up on attempt ${attempt} (${((Date.now() - at0) / 1000).toFixed(1)}s)`); return true; }
|
|
207
|
+
log.warn(`[startEngine] attempt ${attempt} dockerd still not up after ${((Date.now() - at0) / 1000).toFixed(1)}s`);
|
|
204
208
|
// Not up after the wait. Only hard-reset when dockerd actually DIED (crash) —
|
|
205
209
|
// if it's still alive it's just slow/mid-init, so leave it and let the next
|
|
206
210
|
// attempt's wait loop keep polling instead of killing a healthy daemon.
|
|
@@ -210,6 +214,7 @@ async function startEngine() {
|
|
|
210
214
|
{ timeout: 15000 });
|
|
211
215
|
} catch {}
|
|
212
216
|
}
|
|
217
|
+
log.error("[startEngine] ✗ dockerd NOT up after 3 attempts");
|
|
213
218
|
return false;
|
|
214
219
|
}
|
|
215
220
|
|
|
@@ -310,7 +315,11 @@ function ensureAutostart() {
|
|
|
310
315
|
return new Promise((res) => {
|
|
311
316
|
const tr = `wsl.exe -d ${DISTRO} -u root -e /usr/local/sbin/start-dockerd.sh`;
|
|
312
317
|
execFile("schtasks", ["/create", "/tn", "cicy-docker-autostart", "/tr", tr, "/sc", "onlogon", "/rl", "HIGHEST", "/f"],
|
|
313
|
-
{ windowsHide: true }, () =>
|
|
318
|
+
{ windowsHide: true }, (err, _stdout, stderr) => {
|
|
319
|
+
if (err) log.warn(`[ensureAutostart] schtasks create FAILED (dockerd won't auto-start after reboot): ${err.message}${stderr ? ` / ${String(stderr).trim()}` : ""}`);
|
|
320
|
+
else log.info("[ensureAutostart] logon task 'cicy-docker-autostart' registered → dockerd starts on next logon");
|
|
321
|
+
res();
|
|
322
|
+
});
|
|
314
323
|
});
|
|
315
324
|
}
|
|
316
325
|
|
|
@@ -375,65 +384,104 @@ async function bootstrap(opts = {}) {
|
|
|
375
384
|
async function _bootstrap({ onProgress, port = 8009, container = "cicy-code-docker", volume = "cicy-team", env = {} } = {}) {
|
|
376
385
|
const emit = (ev) => { try { onProgress && onProgress(ev); } catch {} };
|
|
377
386
|
|
|
387
|
+
// Structured, PERSISTED trace of the whole run (electron-log → main.log) so a
|
|
388
|
+
// failed "重启后起不来" is diagnosable AFTER the fact instead of staring at the
|
|
389
|
+
// ephemeral progress modal. Each step logs ▶start / ✓done(+duration, or "skip"
|
|
390
|
+
// when already satisfied) / ✗fail(+reason+duration); the final line gives the
|
|
391
|
+
// total time and the SLOWEST step, plus a [name,ms] breakdown of every step.
|
|
392
|
+
const BT0 = Date.now();
|
|
393
|
+
const secs = (t) => `${((Date.now() - t) / 1000).toFixed(1)}s`;
|
|
394
|
+
const steps = [];
|
|
395
|
+
let _pt = BT0, _pname = "";
|
|
396
|
+
const begin = (name) => { _pname = name; _pt = Date.now(); log.info(`[bootstrap] ▶ ${name}`); };
|
|
397
|
+
const done = (skip) => { steps.push([_pname, Date.now() - _pt]); log.info(`[bootstrap] ✓ ${_pname}${skip ? " (skip)" : ""} ${secs(_pt)}`); };
|
|
398
|
+
const fail = (reason, extra) => { steps.push([`${_pname}:FAIL`, Date.now() - _pt]); log.error(`[bootstrap] ✗ ${_pname} reason=${reason} ${secs(_pt)}${extra ? `\n${extra}` : ""}`); };
|
|
399
|
+
const finish = (ok, reason) => {
|
|
400
|
+
const slow = steps.slice().sort((a, b) => b[1] - a[1])[0];
|
|
401
|
+
log.info(`[bootstrap] ${ok ? "DONE ✓" : `ABORT ✗ reason=${reason}`} total=${secs(BT0)} slowest=${slow ? `${slow[0]} ${(slow[1] / 1000).toFixed(1)}s` : "n/a"} steps=${JSON.stringify(steps.map(([n, d]) => [n, Math.round(d)]))}`);
|
|
402
|
+
};
|
|
403
|
+
log.info(`[bootstrap] START port=${port} container=${container} volume=${volume}`);
|
|
404
|
+
|
|
378
405
|
// 0) Fast path: already healthy → instant no-op (idempotent one-shot).
|
|
406
|
+
begin("probe-healthy");
|
|
379
407
|
if (await probeHealth(port)) {
|
|
408
|
+
done();
|
|
380
409
|
emit({ phase: "done", status: "done", message: "Docker cicy-code 已就绪 🎉" });
|
|
410
|
+
finish(true);
|
|
381
411
|
return { ok: true, container };
|
|
382
412
|
}
|
|
413
|
+
done();
|
|
383
414
|
|
|
384
415
|
// 1) WSL2 platform
|
|
416
|
+
begin("ensure-wsl");
|
|
385
417
|
if (docker.wslMissing()) {
|
|
386
418
|
const w = await docker.ensureWsl({ emit });
|
|
387
|
-
if (w.needsReboot) { emit({ phase: "done", status: "reboot", message: "WSL2 正在安装——请【重启 Windows】后回来点「重试」继续。" }); return { ok: false, reason: "wsl_reboot_required" }; }
|
|
388
|
-
|
|
419
|
+
if (w.needsReboot) { fail("wsl_reboot_required"); emit({ phase: "done", status: "reboot", message: "WSL2 正在安装——请【重启 Windows】后回来点「重试」继续。" }); finish(false, "wsl_reboot_required"); return { ok: false, reason: "wsl_reboot_required" }; }
|
|
420
|
+
done();
|
|
421
|
+
} else done(true);
|
|
389
422
|
|
|
390
423
|
// 2) Ubuntu distro
|
|
424
|
+
begin("ensure-distro");
|
|
391
425
|
if (!distroInstalled()) {
|
|
392
|
-
try { await installDistro({ emit }); } catch (e) { emit({ phase: "install-docker", status: "error", message: `Ubuntu 安装失败:${e.message}(点重试)` }); return { ok: false, reason: "distro_install_failed" }; }
|
|
426
|
+
try { await installDistro({ emit }); } catch (e) { fail("distro_install_failed", e.message); emit({ phase: "install-docker", status: "error", message: `Ubuntu 安装失败:${e.message}(点重试)` }); finish(false, "distro_install_failed"); return { ok: false, reason: "distro_install_failed" }; }
|
|
393
427
|
const t0 = Date.now();
|
|
394
428
|
const ok = await docker.waitUntil(() => distroInstalled(), { totalMs: 600000, everyMs: 5000, onTick: () => emit({ phase: "install-docker", status: "running", message: `正在下载/注册 Ubuntu…(已 ${Math.round((Date.now() - t0) / 1000)}s,首次较慢请耐心)` }) });
|
|
395
|
-
if (!ok) { emit({ phase: "install-docker", status: "error", message: "Ubuntu 还没装好——稍等或点「重试」" }); return { ok: false, reason: "distro_not_ready" }; }
|
|
396
|
-
|
|
429
|
+
if (!ok) { fail("distro_not_ready"); emit({ phase: "install-docker", status: "error", message: "Ubuntu 还没装好——稍等或点「重试」" }); finish(false, "distro_not_ready"); return { ok: false, reason: "distro_not_ready" }; }
|
|
430
|
+
done();
|
|
431
|
+
} else done(true);
|
|
397
432
|
|
|
398
433
|
// 3) Docker Engine inside Ubuntu
|
|
434
|
+
begin("install-docker-engine");
|
|
399
435
|
if (!(await dockerInstalled())) {
|
|
400
|
-
try { await installDockerEngine({ emit }); } catch (e) { emit({ phase: "install-docker", status: "error", message: `Docker 安装失败:${e.message}(点重试)` }); return { ok: false, reason: "docker_install_failed" }; }
|
|
401
|
-
|
|
436
|
+
try { await installDockerEngine({ emit }); } catch (e) { fail("docker_install_failed", e.message); emit({ phase: "install-docker", status: "error", message: `Docker 安装失败:${e.message}(点重试)` }); finish(false, "docker_install_failed"); return { ok: false, reason: "docker_install_failed" }; }
|
|
437
|
+
done();
|
|
438
|
+
} else done(true);
|
|
402
439
|
|
|
403
440
|
// 4) dockerd up (phase "container" = 启动服务)
|
|
441
|
+
begin("start-dockerd");
|
|
404
442
|
if (!(await dockerEngineUp())) {
|
|
405
443
|
emit({ phase: "container", status: "running", message: "启动 Docker 引擎(首次较慢,请耐心)…" });
|
|
406
444
|
const started = await startEngine(); // 3 clean attempts internally
|
|
407
445
|
const up = started || await docker.waitUntil(dockerEngineUp, { totalMs: 120000, everyMs: 3000 });
|
|
408
446
|
if (!up) {
|
|
409
|
-
const
|
|
410
|
-
|
|
447
|
+
const dlog = await dockerdLogTail();
|
|
448
|
+
fail("dockerd_not_up", dlog ? `dockerd log:\n${dlog}` : "");
|
|
449
|
+
emit({ phase: "container", status: "error", message: "Docker 引擎没起来——点「重试」" + (dlog ? `\n\ndockerd 日志(最后几行):\n${dlog}` : "") });
|
|
450
|
+
finish(false, "dockerd_not_up");
|
|
411
451
|
return { ok: false, reason: "dockerd_not_up" };
|
|
412
452
|
}
|
|
413
|
-
|
|
453
|
+
done();
|
|
454
|
+
} else done(true);
|
|
414
455
|
|
|
415
456
|
// 5) Base image — pre-baked into the package, so this normally just confirms.
|
|
416
457
|
// The download-tarball path is a fallback for a non-pre-baked rootfs.
|
|
458
|
+
begin("ensure-image");
|
|
417
459
|
if (!(await imagePresent())) {
|
|
418
460
|
let tarball;
|
|
419
461
|
try { tarball = await docker.downloadImageTarball({ emit }); }
|
|
420
|
-
catch (e) { emit({ phase: "image", status: "error", message: `镜像下载失败:${e.message}(点重试续传)` }); return { ok: false, reason: "image_download_failed" }; }
|
|
462
|
+
catch (e) { 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" }; }
|
|
421
463
|
try { await loadImage(tarball, { emit }); }
|
|
422
|
-
catch (e) { emit({ phase: "image", status: "error", message: `镜像导入失败:${e.message}(点重试)` }); return { ok: false, reason: "image_load_failed" }; }
|
|
423
|
-
|
|
464
|
+
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" }; }
|
|
465
|
+
done();
|
|
466
|
+
} else done(true);
|
|
424
467
|
|
|
425
468
|
// 6) Container (phase "container" = 启动服务)
|
|
469
|
+
begin("run-container");
|
|
426
470
|
if (!(await probeHealth(port))) {
|
|
427
471
|
emit({ phase: "container", status: "running", message: "启动 cicy-code 服务…" });
|
|
428
472
|
try { await runContainer({ port, container, volume, env }); }
|
|
429
|
-
catch (e) { emit({ phase: "container", status: "error", message: `服务启动失败:${e.message}(点重试)` }); return { ok: false, reason: "container_start_failed" }; }
|
|
430
|
-
|
|
473
|
+
catch (e) { fail("container_start_failed", e.message); emit({ phase: "container", status: "error", message: `服务启动失败:${e.message}(点重试)` }); finish(false, "container_start_failed"); return { ok: false, reason: "container_start_failed" }; }
|
|
474
|
+
done();
|
|
475
|
+
} else done(true);
|
|
431
476
|
|
|
432
477
|
// 7) Health — the ONLY path to ok:true.
|
|
478
|
+
begin("wait-health");
|
|
433
479
|
emit({ phase: "container", status: "running", message: "等待 cicy-code 就绪…" });
|
|
434
480
|
const healthy = await docker.waitUntil(() => probeHealth(port), { totalMs: 120000, everyMs: 3000 });
|
|
435
|
-
if (healthy) { await ensureAutostart(); await ensureDesktopShortcut(volume, port); } // survive reboot + desktop shortcut
|
|
481
|
+
if (healthy) { done(); await ensureAutostart(); await ensureDesktopShortcut(volume, port); } // survive reboot + desktop shortcut
|
|
482
|
+
else fail("health_timeout");
|
|
436
483
|
emit({ phase: healthy ? "done" : "container", status: healthy ? "done" : "error", message: healthy ? "Docker cicy-code 已就绪 🎉" : `服务起来了但 :${port} 还没响应——稍等或点「重试」` });
|
|
484
|
+
finish(healthy, healthy ? null : "health_timeout");
|
|
437
485
|
return { ok: healthy, container };
|
|
438
486
|
}
|
|
439
487
|
|