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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cicy-desktop",
3
- "version": "2.1.121",
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.19",
139
- "cicy-code-darwin-arm64": "2.3.19",
140
- "cicy-code-linux-x64": "2.3.19",
141
- "cicy-code-linux-arm64": "2.3.19",
142
- "cicy-code-windows-x64": "2.3.19",
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
- const r = await lt.addTeam({ base_url: `http://127.0.0.1:${APP_PORT}`, name: title, skipTokenAutofill: true });
207
- // docker 独立 team cloud_team_id 写到这个本地节点(DockerCard 据此账单 + 改名),
208
- // 并标记 is_docker(syncNameToCloud 跳过它,不被本机 deviceId 复用回 8008)。
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 }, () => res());
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 log = await dockerdLogTail();
410
- emit({ phase: "container", status: "error", message: "Docker 引擎没起来——点「重试」" + (log ? `\n\ndockerd 日志(最后几行):\n${log}` : "") });
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