cicy-desktop 2.1.94 → 2.1.95

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,7 +6,7 @@
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-5vDVCGqD.js"></script>
9
+ <script type="module" crossorigin src="./assets/index-C7gQsfPP.js"></script>
10
10
  <link rel="stylesheet" crossorigin href="./assets/index-CKpaMBKz.css">
11
11
  </head>
12
12
  <body>
@@ -13,10 +13,21 @@
13
13
  // along with src/sidecar/installer.js and src/sidecar/wsl.js.)
14
14
 
15
15
  const { ipcMain } = require("electron");
16
+ const path = require("path");
16
17
  const sidecar = require("../sidecar/cicy-code");
17
18
  const docker = require("../sidecar/docker");
18
19
 
19
20
  const PORT = Number(process.env.CICY_CODE_PORT || 8008);
21
+
22
+ // Docker-版 cicy-code: a SECOND, optional instance that runs inside Docker on
23
+ // :8009 (its own container + volume), alongside the native local daemon on
24
+ // :8008. The homepage "Docker cicy-code" card owns its lifecycle; if Docker
25
+ // Desktop is missing the card installs it first (installer downloads to the
26
+ // user's Desktop).
27
+ const APP_PORT = Number(process.env.CICY_DOCKER_APP_PORT || 8009);
28
+ const APP_CONTAINER = process.env.CICY_DOCKER_APP_CONTAINER || "cicy-code-docker";
29
+ const APP_VOLUME = process.env.CICY_DOCKER_APP_VOLUME || "cicy-ai-docker-data";
30
+
20
31
  let registered = false;
21
32
 
22
33
  function register({ sidecarLogPath } = {}) {
@@ -80,6 +91,55 @@ function register({ sidecarLogPath } = {}) {
80
91
  }
81
92
  });
82
93
 
94
+ // ---- Docker-版 cicy-code on :8009 (homepage "Docker cicy-code" card) ----
95
+ // Status: is Docker Desktop installed, and is the :8009 container healthy?
96
+ // platform tells the card to render only on Windows.
97
+ ipcMain.handle("docker:app-status", async () => {
98
+ try {
99
+ const installed = await docker.dockerOk();
100
+ const running = await docker.probeHealth(APP_PORT);
101
+ return { installed, running, port: APP_PORT, platform: process.platform };
102
+ } catch (e) {
103
+ return { installed: false, running: false, port: APP_PORT, platform: process.platform, error: e.message };
104
+ }
105
+ });
106
+
107
+ // One-click bootstrap of the Docker-版 instance: install Docker Desktop if
108
+ // missing (installer → user's Desktop), load the image, start the :8009
109
+ // container (its own name/volume), wait for health. Streams phase/progress on
110
+ // 'docker:app-progress' so the card's modal mirrors the cicy-code 升级 modal.
111
+ ipcMain.handle("docker:app-bootstrap", async (e) => {
112
+ if (process.platform !== "win32") return { ok: false, error: "Docker cicy-code is Windows-only" };
113
+ try {
114
+ const installDest = path.join(docker.desktopDir(), "Docker Desktop Installer.exe");
115
+ const result = await docker.bootstrap({
116
+ port: APP_PORT, container: APP_CONTAINER, volume: APP_VOLUME, installDest,
117
+ onProgress: (ev) => { try { e.sender.send("docker:app-progress", ev); } catch {} },
118
+ });
119
+ // Healthy → register :8009 as a (custom) team so the card's "打开" reuses
120
+ // the token-injected open/reload flow. addTeam dedups by host:port.
121
+ if (result && result.ok) {
122
+ try {
123
+ const lt = require("./local-teams");
124
+ const tok = await docker.readContainerToken(APP_PORT);
125
+ await lt.addTeam({
126
+ base_url: `http://127.0.0.1:${APP_PORT}`, name: "Docker cicy-code",
127
+ ...(tok ? { api_token: tok } : {}),
128
+ });
129
+ } catch { /* best-effort — the container itself is up */ }
130
+ }
131
+ return result;
132
+ } catch (err) {
133
+ return { ok: false, error: err.message };
134
+ }
135
+ });
136
+
137
+ // Stop + remove the :8009 Docker container (card's "停止").
138
+ ipcMain.handle("docker:app-stop", async () => {
139
+ try { await docker.stop({ container: APP_CONTAINER }); return { ok: true }; }
140
+ catch (e) { return { ok: false, error: e.message }; }
141
+ });
142
+
83
143
  // Start (or reuse) the cicy-code daemon. probeExisting inside start() reuses
84
144
  // a healthy :8008; otherwise it spawns `npx cicy-code` / the Docker container.
85
145
  ipcMain.handle("sidecar:start", async () => {
@@ -252,16 +252,25 @@ async function checkStatus() {
252
252
  return { installed, imagePresent: installed ? await imagePresent() : false };
253
253
  }
254
254
 
255
+ // Resolve the user's Desktop folder (主人指令: docker-desktop.exe 下到 Desktop).
256
+ // %USERPROFILE%\Desktop is the canonical location; OneDrive redirection is rare
257
+ // on the target machines and the file is only a transient installer anyway.
258
+ function desktopDir() {
259
+ return path.join(process.env["USERPROFILE"] || os.homedir(), "Desktop");
260
+ }
261
+
255
262
  // Start the container. Returns a sidecar child token { docker:true, container,
256
263
  // id } or null when Docker isn't ready (homepage guides the user to install
257
- // Docker Desktop).
258
- async function start({ port = 8008 } = {}) {
264
+ // Docker Desktop). `container`/`volume` are parameterized so a SECOND instance
265
+ // (the Docker-版 cicy-code on :8009) can run alongside the native local one
266
+ // without a name/volume collision.
267
+ async function start({ port = 8008, container = CONTAINER, volume = VOLUME } = {}) {
259
268
  // Something already serves a healthy cicy-code on :port (a legacy-named
260
269
  // container auto-revived by `--restart unless-stopped`, a manual run…).
261
270
  // Adopt it — `docker run` would just lose the port-bind fight.
262
271
  if (await probeHealth(port)) {
263
272
  console.log(`[docker-sidecar] :${port} already healthy — adopting existing instance`);
264
- return { docker: true, container: CONTAINER, adopted: true };
273
+ return { docker: true, container, adopted: true };
265
274
  }
266
275
  if (!(await dockerOk())) {
267
276
  console.warn("[docker-sidecar] Docker not available — homepage will guide install");
@@ -272,12 +281,12 @@ async function start({ port = 8008 } = {}) {
272
281
  catch (e) { console.warn(`[docker-sidecar] image load failed: ${e.message}`); return null; }
273
282
  }
274
283
  // Replace any stale container of the same name.
275
- try { await run(["rm", "-f", CONTAINER]); } catch {}
284
+ try { await run(["rm", "-f", container]); } catch {}
276
285
 
277
286
  const args = [
278
- "run", "-d", "--name", CONTAINER, "--restart", "unless-stopped",
287
+ "run", "-d", "--name", container, "--restart", "unless-stopped",
279
288
  "-p", `${port}:8008`,
280
- "-v", `${VOLUME}:/home/cicy/cicy-ai`,
289
+ "-v", `${volume}:/home/cicy/cicy-ai`,
281
290
  ];
282
291
  for (const k of PASS_ENV) {
283
292
  if (process.env[k]) args.push("-e", `${k}=${process.env[k]}`);
@@ -286,28 +295,31 @@ async function start({ port = 8008 } = {}) {
286
295
 
287
296
  const { stdout } = await run(args, { timeout: 60000 });
288
297
  const id = stdout.trim().slice(0, 12);
289
- console.log(`[docker-sidecar] started container ${CONTAINER} (${id}) on :${port}`);
290
- return { docker: true, container: CONTAINER, id };
298
+ console.log(`[docker-sidecar] started container ${container} (${id}) on :${port}`);
299
+ return { docker: true, container, id };
291
300
  }
292
301
 
293
- async function stop() {
294
- try { await run(["rm", "-f", CONTAINER]); } catch {}
302
+ async function stop({ container = CONTAINER } = {}) {
303
+ try { await run(["rm", "-f", container]); } catch {}
295
304
  }
296
305
 
297
306
  // Download + run the Docker Desktop installer (Windows). The installer needs
298
307
  // admin → Windows shows a UAC prompt the user accepts; first run may want a
299
308
  // reboot. We download (skip/resume aware) then launch silent and return.
300
- async function installDocker({ emit } = {}) {
309
+ // `dest` defaults to tmp (the legacy local-team path); the Docker-版 card passes
310
+ // the Desktop folder so the user can see/keep the installer (主人指令).
311
+ async function installDocker({ emit, dest } = {}) {
301
312
  const e = emit || (() => {});
302
- const dest = path.join(os.tmpdir(), "DockerDesktopInstaller.exe");
313
+ const target = dest || path.join(os.tmpdir(), "DockerDesktopInstaller.exe");
314
+ try { fs.mkdirSync(path.dirname(target), { recursive: true }); } catch {}
303
315
  e({ phase: "install-docker", status: "running", message: "下载 Docker Desktop 安装包…", progress: 0 });
304
- await ensureDownloaded(DOCKER_DESKTOP_URL, dest, DOCKER_DESKTOP_MIRROR, {
316
+ await ensureDownloaded(DOCKER_DESKTOP_URL, target, DOCKER_DESKTOP_MIRROR, {
305
317
  emit, phase: "install-docker", label: "下载 Docker Desktop",
306
318
  });
307
319
  e({ phase: "install-docker", status: "running", message: "安装 Docker Desktop(请在弹出的授权框点「是」,装完可能需重启)…" });
308
320
  await new Promise((resolve) => {
309
321
  try {
310
- const child = spawn(dest, ["install", "--quiet", "--accept-license"], {
322
+ const child = spawn(target, ["install", "--quiet", "--accept-license"], {
311
323
  windowsHide: false, detached: true, stdio: "ignore",
312
324
  });
313
325
  child.on("error", () => resolve());
@@ -321,7 +333,7 @@ async function installDocker({ emit } = {}) {
321
333
  // start the container → wait for :8008. Every step CHECKS first and SKIPS if
322
334
  // already done, emits coarse phase events + byte progress, and the downloads
323
335
  // resume. Safe to call again after a failure — it picks up where it left off.
324
- async function bootstrap({ onProgress, port = 8008 } = {}) {
336
+ async function bootstrap({ onProgress, port = 8008, container = CONTAINER, volume = VOLUME, installDest } = {}) {
325
337
  const emit = (ev) => { try { onProgress && onProgress(ev); } catch {} };
326
338
 
327
339
  // 1) Docker present?
@@ -339,7 +351,7 @@ async function bootstrap({ onProgress, port = 8008 } = {}) {
339
351
  }
340
352
  emit({ phase: "install-docker", status: "done", message: "Docker 就绪" });
341
353
  } else {
342
- await installDocker({ emit });
354
+ await installDocker({ emit, dest: installDest });
343
355
  emit({ phase: "install-docker", status: "running", message: "等待 Docker 启动(如需授权/重启,完成后会自动继续)…" });
344
356
  const up = await waitUntil(dockerOk, { totalMs: 900000, everyMs: 6000 });
345
357
  if (!up) {
@@ -369,7 +381,7 @@ async function bootstrap({ onProgress, port = 8008 } = {}) {
369
381
  } else {
370
382
  emit({ phase: "container", status: "running", message: "启动 cicy-code 容器…" });
371
383
  let child = null;
372
- try { child = await start({ port }); }
384
+ try { child = await start({ port, container, volume }); }
373
385
  catch (e) { emit({ phase: "container", status: "error", message: `容器启动失败:${e.message}` }); return { ok: false, reason: "container_start_failed" }; }
374
386
  if (!child) {
375
387
  emit({ phase: "container", status: "error", message: "容器启动失败" });
@@ -378,15 +390,15 @@ async function bootstrap({ onProgress, port = 8008 } = {}) {
378
390
  }
379
391
 
380
392
  // 4) Health
381
- emit({ phase: "health", status: "running", message: "等待本地团队就绪…" });
393
+ emit({ phase: "health", status: "running", message: "等待容器就绪…" });
382
394
  const healthy = await waitUntil(() => probeHealth(port), { totalMs: 120000, everyMs: 3000 });
383
- emit({ phase: "done", status: healthy ? "done" : "error", message: healthy ? "本地团队已就绪 🎉" : "容器起来了但 :8008 还没响应,稍等或点重试" });
384
- return { ok: healthy, container: CONTAINER };
395
+ emit({ phase: "done", status: healthy ? "done" : "error", message: healthy ? "Docker cicy-code 已就绪 🎉" : `容器起来了但 :${port} 还没响应,稍等或点重试` });
396
+ return { ok: healthy, container };
385
397
  }
386
398
 
387
399
  module.exports = {
388
400
  start, stop, checkStatus, loadImage, imagePresent, dockerOk, installDocker,
389
- bootstrap, probeHealth, readContainerToken,
401
+ bootstrap, probeHealth, readContainerToken, dockerDesktopExe, desktopDir,
390
402
  // platform-agnostic download/retry primitives, reused by native.js
391
403
  ensureDownloaded, withRetry, waitUntil, run,
392
404
  };
@@ -625,11 +625,16 @@ export default function App() {
625
625
 
626
626
  // Logged in: unified tabs + cards grid on the left, full-height webview
627
627
  // drawer on the right.
628
+ // The Docker-版 cicy-code on :8009 has its own dedicated <DockerCard> (right of
629
+ // the local card), so pull it out of the generic node list — else it'd ALSO
630
+ // render as a 自定义 card (the bootstrap registers it as a team for the
631
+ // token-injected 打开/刷新 flow).
632
+ const dockerTeam = (localTeams || []).find((t) => isDockerApp(t.base_url)) || null;
628
633
  // Split the cicyDesktopNodes list into 本地 (the localhost:8008 sidecar the
629
634
  // desktop owns — full lifecycle) vs 自定义 (deeplink-added nodes, usually
630
635
  // remote — probe-only, no restart/stop/update, just 打开).
631
636
  const localList = (localTeams || []).filter((t) => isLocalSidecar(t.base_url));
632
- const customList = (localTeams || []).filter((t) => !isLocalSidecar(t.base_url));
637
+ const customList = (localTeams || []).filter((t) => !isLocalSidecar(t.base_url) && !isDockerApp(t.base_url));
633
638
  const localCount = localList.length;
634
639
  const customCount = customList.length;
635
640
  // /api/teams returns ALL of this owner's teams — including kind=local ones
@@ -721,6 +726,13 @@ export default function App() {
721
726
  </button>
722
727
  </div>
723
728
  )}
729
+ {showLocal && (
730
+ <DockerCard
731
+ dockerTeam={dockerTeam}
732
+ onOpen={(id) => { if (id) openLocalTeam(id); else window.cicy?.tabs?.open?.("http://127.0.0.1:8009", "Docker cicy-code"); }}
733
+ onRefresh={fetchLocalTeams}
734
+ />
735
+ )}
724
736
  {showCustom && customList.map((t) => (
725
737
  <LocalTeamCard key={"custom:" + t.id} team={t} onOpen={() => openLocalTeam(t.id)} onRename={renameLocalTeam} onRefresh={fetchLocalTeams} />
726
738
  ))}
@@ -748,6 +760,7 @@ export default function App() {
748
760
  </div>{/* /.shell__left */}
749
761
  <ToastHost />
750
762
  <UpdateDrawerHost />
763
+ <DockerInstallDrawerHost />
751
764
  </div>
752
765
  );
753
766
  }
@@ -1412,6 +1425,204 @@ function DockerSetup({ onReady }) {
1412
1425
  );
1413
1426
  }
1414
1427
 
1428
+ // ── Docker install drawer: streams the Docker-版 cicy-code setup (装 Docker→
1429
+ // 加载镜像→启动容器→就绪), mirroring the cicy-code 升级 drawer. The bootstrap
1430
+ // emits {phase,status,message,progress} on 'docker:app-progress'; the card tees
1431
+ // those here via dockerDrawer.push. Single global instance at shell root.
1432
+ const dockerDrawerListeners = new Set();
1433
+ let dockerDrawerLogSeq = 0;
1434
+ let dockerDrawerState = null; // null = closed
1435
+ function emitDockerDrawer() { dockerDrawerListeners.forEach((l) => l(dockerDrawerState)); }
1436
+ const dockerDrawer = {
1437
+ open({ onRetry } = {}) {
1438
+ dockerDrawerState = { status: "running", phase: "install-docker", logs: [], onRetry: onRetry || null, lastAt: Date.now() };
1439
+ emitDockerDrawer();
1440
+ },
1441
+ push(ev = {}) {
1442
+ if (!dockerDrawerState) return;
1443
+ const phase = ev.phase === "health" ? "container" : (ev.phase || dockerDrawerState.phase);
1444
+ const line = { id: ++dockerDrawerLogSeq, t: clockHHMMSS(), phase, status: ev.status || "running", message: ev.message || "", progress: ev.progress };
1445
+ dockerDrawerState = { ...dockerDrawerState, phase, logs: [...dockerDrawerState.logs, line], lastAt: Date.now() };
1446
+ emitDockerDrawer();
1447
+ },
1448
+ finish({ ok, message } = {}) {
1449
+ if (!dockerDrawerState) return;
1450
+ const status = ok ? "done" : "error";
1451
+ const line = { id: ++dockerDrawerLogSeq, t: clockHHMMSS(), phase: "done", status, message: message || (ok ? "完成" : "失败") };
1452
+ dockerDrawerState = { ...dockerDrawerState, status, phase: "done", logs: [...dockerDrawerState.logs, line], lastAt: Date.now() };
1453
+ emitDockerDrawer();
1454
+ },
1455
+ close() { dockerDrawerState = null; emitDockerDrawer(); },
1456
+ };
1457
+ const DOCKER_PHASES = [["install-docker", "装 Docker"], ["image", "加载镜像"], ["container", "启动容器"], ["done", "完成"]];
1458
+ const DOCKER_BADGE = { "install-docker": "Docker", image: "镜像", container: "容器", health: "容器", done: "完成" };
1459
+ function DockerInstallDrawerHost() {
1460
+ const [st, setSt] = useState(dockerDrawerState);
1461
+ useEffect(() => { dockerDrawerListeners.add(setSt); return () => { dockerDrawerListeners.delete(setSt); }; }, []);
1462
+ const logRef = useRef(null);
1463
+ useEffect(() => { const el = logRef.current; if (el) el.scrollTop = el.scrollHeight; }, [st?.logs?.length]);
1464
+ if (!st) return null;
1465
+ const running = st.status === "running";
1466
+ const phaseIdx = DOCKER_PHASES.findIndex(([k]) => k === st.phase);
1467
+ return (
1468
+ <div className="drawer-scrim" data-id="DockerDrawer-scrim" onClick={() => { if (!running) dockerDrawer.close(); }}>
1469
+ <div className="drawer" data-id="DockerDrawer" data-status={st.status} onClick={(e) => e.stopPropagation()}>
1470
+ <div className="drawer__head">
1471
+ <div className="drawer__title">
1472
+ <span className={`drawer__spark drawer__spark--${st.status}`}>
1473
+ {running ? <Spinner /> : st.status === "done" ? "✓" : "!"}
1474
+ </span>
1475
+ <div>
1476
+ <div className="drawer__h">{tr("docker.setupTitle", "安装 Docker cicy-code")}</div>
1477
+ <div className="drawer__sub">127.0.0.1:8009</div>
1478
+ </div>
1479
+ </div>
1480
+ <button type="button" className="drawer__x" data-id="DockerDrawer-close" disabled={running} title={running ? tr("docker.busy", "进行中") : tr("common.close", "关闭")} onClick={() => dockerDrawer.close()} aria-label="close">×</button>
1481
+ </div>
1482
+
1483
+ <div className="drawer__steps" data-id="DockerDrawer-steps">
1484
+ {DOCKER_PHASES.map(([k, label], i) => {
1485
+ const done = st.status === "done" || (phaseIdx >= 0 && i < phaseIdx);
1486
+ const active = i === phaseIdx && running;
1487
+ const err = st.status === "error" && i === phaseIdx;
1488
+ return (
1489
+ <div key={k} className={`drawer__step${active ? " is-active" : ""}${done ? " is-done" : ""}${err ? " is-error" : ""}`}>
1490
+ <span className="drawer__step-dot">{done ? "✓" : err ? "!" : i + 1}</span>
1491
+ <span className="drawer__step-label">{label}</span>
1492
+ {i < DOCKER_PHASES.length - 1 && <span className="drawer__step-bar" />}
1493
+ </div>
1494
+ );
1495
+ })}
1496
+ </div>
1497
+
1498
+ <div className="drawer__log" data-id="DockerDrawer-log" ref={logRef}>
1499
+ {st.logs.length === 0
1500
+ ? <div className="drawer__log-empty">{tr("docker.preparing", "准备中…")}</div>
1501
+ : st.logs.map((l) => (
1502
+ <div key={l.id} className="drawer__line" data-status={l.status}>
1503
+ <span className="drawer__t">{l.t}</span>
1504
+ <span className={`drawer__badge drawer__badge--${l.phase}`}>{DOCKER_BADGE[l.phase] || l.phase}</span>
1505
+ <span className="drawer__linemsg">{l.message}{Number.isFinite(l.progress) ? ` ${l.progress}%` : ""}</span>
1506
+ </div>
1507
+ ))}
1508
+ </div>
1509
+
1510
+ <div className="drawer__foot">
1511
+ {running ? (
1512
+ <>
1513
+ <span className="drawer__foot-status">{tr("docker.installing2", "安装进行中…")}</span>
1514
+ <button type="button" className="drawer__btn" data-id="DockerDrawer-background" onClick={() => dockerDrawer.close()}>{tr("docker.background", "在后台继续")}</button>
1515
+ </>
1516
+ ) : st.status === "error" ? (
1517
+ <>
1518
+ <span className="drawer__foot-status is-error">{tr("docker.failed", "安装失败")}</span>
1519
+ {st.onRetry && <button type="button" className="drawer__btn is-accent" data-id="DockerDrawer-retry" onClick={() => st.onRetry()}>{tr("common.retry", "重试")}</button>}
1520
+ <button type="button" className="drawer__btn" data-id="DockerDrawer-dismiss" onClick={() => dockerDrawer.close()}>{tr("common.close", "关闭")}</button>
1521
+ </>
1522
+ ) : (
1523
+ <>
1524
+ <span className="drawer__foot-status is-done">{tr("docker.ready", "已就绪")}</span>
1525
+ <button type="button" className="drawer__btn is-accent" data-id="DockerDrawer-finish" onClick={() => dockerDrawer.close()}>{tr("common.done", "完成")}</button>
1526
+ </>
1527
+ )}
1528
+ </div>
1529
+ </div>
1530
+ </div>
1531
+ );
1532
+ }
1533
+
1534
+ // Docker-版 cicy-code card (Windows only): a SECOND cicy-code instance running
1535
+ // in Docker on :8009, alongside the native local daemon (:8008). If Docker
1536
+ // Desktop is missing, the install flow downloads its installer to the user's
1537
+ // Desktop and runs it (主人指令), streaming progress through the drawer above.
1538
+ function DockerCard({ dockerTeam, onOpen, onRefresh }) {
1539
+ const [status, setStatus] = useState(null);
1540
+ const [busy, setBusy] = useState(false);
1541
+ const DOCKER_BLUE = "#2496ed";
1542
+
1543
+ const checkStatus = useCallback(async () => {
1544
+ try { setStatus(await window.cicy?.docker?.appStatus?.()); }
1545
+ catch (e) { console.warn("[DockerCard]", e); }
1546
+ }, []);
1547
+
1548
+ useEffect(() => { checkStatus(); }, [checkStatus]);
1549
+
1550
+ const runBootstrap = useCallback(async () => {
1551
+ setBusy(true);
1552
+ dockerDrawer.open({ onRetry: runBootstrap });
1553
+ const unsub = window.cicy?.docker?.onAppProgress?.((ev) => dockerDrawer.push(ev));
1554
+ try {
1555
+ const r = await window.cicy?.docker?.appBootstrap?.();
1556
+ dockerDrawer.finish({ ok: !!r?.ok, message: r?.ok ? tr("docker.ready", "Docker cicy-code 已就绪") : (r?.error || tr("docker.failed", "安装失败")) });
1557
+ if (r?.ok) onRefresh?.();
1558
+ } catch (e) {
1559
+ dockerDrawer.finish({ ok: false, message: e.message });
1560
+ } finally {
1561
+ try { unsub && unsub(); } catch {}
1562
+ setBusy(false);
1563
+ checkStatus();
1564
+ }
1565
+ }, [checkStatus, onRefresh]);
1566
+
1567
+ // Render only on Windows. window.cicy.platform is sync, so we can decide
1568
+ // immediately without waiting on the async appStatus probe.
1569
+ const platform = window.cicy?.platform || status?.platform;
1570
+ if (platform !== "win32") return null;
1571
+
1572
+ const running = !!status?.running || dockerTeam?.status === "running";
1573
+ const installed = !!status?.installed;
1574
+ const tone = running ? "ok" : installed ? "warn" : "off";
1575
+ const stateText = running
1576
+ ? tr("docker.running", "运行中 · :8009")
1577
+ : installed
1578
+ ? tr("docker.notRunning", "未启动 · 点「启动」")
1579
+ : tr("docker.notInstalled", "Docker Desktop 未安装");
1580
+
1581
+ const ctaLabel = busy
1582
+ ? tr("docker.working", "处理中…")
1583
+ : running
1584
+ ? tr("localTeams.open", "打开")
1585
+ : installed
1586
+ ? tr("docker.start", "启动")
1587
+ : tr("docker.install", "下载安装");
1588
+
1589
+ const onCta = () => {
1590
+ if (busy) return;
1591
+ if (running) { onOpen?.(dockerTeam?.id); return; }
1592
+ runBootstrap();
1593
+ };
1594
+
1595
+ return (
1596
+ <div data-id="DockerCard" className={`bcard bcard--docker${running ? " bcard--online" : ""}`}>
1597
+ <div className="bcard__accent" style={{ background: DOCKER_BLUE }} />
1598
+ <div className="bcard__top">
1599
+ <div className="bcard__pill" style={{ color: DOCKER_BLUE }}>
1600
+ <span className="bcard__dot" data-tone={tone} />
1601
+ <svg style={{ width: 18, height: 18 }} viewBox="0 0 24 24" fill="currentColor" aria-hidden>
1602
+ <path d="M21.81 10.25c-.06-.05-.67-.51-1.95-.51-.34 0-.68.03-1.01.09-.25-1.69-1.64-2.51-1.7-2.55l-.34-.2-.22.32a4.5 4.5 0 0 0-.59 1.4c-.23.94-.09 1.83.39 2.59-.58.32-1.51.4-1.7.41H2.62a.61.61 0 0 0-.61.61 9.32 9.32 0 0 0 .57 3.35 4.9 4.9 0 0 0 1.95 2.53c.92.52 2.42.82 4.12.82.77 0 1.54-.07 2.3-.21a9.6 9.6 0 0 0 3-1.09 8.3 8.3 0 0 0 2.05-1.68c.98-1.11 1.56-2.35 1.99-3.45h.17c1.36 0 2.2-.55 2.66-1l.13-.16zM4.7 11.33h1.78a.16.16 0 0 0 .16-.16V9.58a.16.16 0 0 0-.16-.16H4.7a.16.16 0 0 0-.16.16v1.59c0 .09.07.16.16.16m2.46 0h1.78a.16.16 0 0 0 .16-.16V9.58a.16.16 0 0 0-.16-.16H7.16a.16.16 0 0 0-.16.16v1.59c0 .09.07.16.16.16m2.5 0h1.78a.16.16 0 0 0 .16-.16V9.58a.16.16 0 0 0-.16-.16H9.66a.16.16 0 0 0-.16.16v1.59c0 .09.07.16.16.16m2.47 0h1.78a.16.16 0 0 0 .16-.16V9.58a.16.16 0 0 0-.16-.16h-1.78a.16.16 0 0 0-.16.16v1.59c0 .09.07.16.16.16M7.16 9.06h1.78a.16.16 0 0 0 .16-.16V7.31a.16.16 0 0 0-.16-.16H7.16a.16.16 0 0 0-.16.16v1.59c0 .09.07.16.16.16m2.5 0h1.78a.16.16 0 0 0 .16-.16V7.31a.16.16 0 0 0-.16-.16H9.66a.16.16 0 0 0-.16.16v1.59c0 .09.07.16.16.16m2.47 0h1.78a.16.16 0 0 0 .16-.16V7.31a.16.16 0 0 0-.16-.16h-1.78a.16.16 0 0 0-.16.16v1.59c0 .09.07.16.16.16" />
1603
+ </svg>
1604
+ </div>
1605
+ </div>
1606
+ <div className="bcard__body">
1607
+ <h3 className="bcard__name">{tr("docker.title", "Docker cicy-code")}</h3>
1608
+ <div className="bcard__host">http://127.0.0.1:8009</div>
1609
+ <div className="bcard__meta" style={{ fontSize: 12, color: "#8b949e" }}>{stateText}</div>
1610
+ </div>
1611
+ <button
1612
+ type="button"
1613
+ className="bcard__cta"
1614
+ data-id="DockerCard-cta"
1615
+ disabled={busy}
1616
+ onClick={onCta}
1617
+ style={!running ? { background: DOCKER_BLUE, color: "white" } : undefined}
1618
+ >
1619
+ {busy ? <Spinner /> : <ArrowIcon />}
1620
+ <span>{ctaLabel}</span>
1621
+ </button>
1622
+ </div>
1623
+ );
1624
+ }
1625
+
1415
1626
  function LocalTeamCard({ team, onOpen, onRename, onRefresh }) {
1416
1627
  const statusInfo = LOCAL_STATUS[team.status] || LOCAL_STATUS.error;
1417
1628
  const tone = statusInfo.tone;
@@ -1809,6 +2020,16 @@ function isLocalSidecar(baseUrl) {
1809
2020
  } catch { return false; }
1810
2021
  }
1811
2022
 
2023
+ // The Docker-版 cicy-code instance — localhost:8009. Owned by <DockerCard>, so
2024
+ // it's filtered out of the generic node lists.
2025
+ function isDockerApp(baseUrl) {
2026
+ try {
2027
+ const p = new URL(baseUrl);
2028
+ const local = p.hostname === "127.0.0.1" || p.hostname === "localhost" || p.hostname === "::1";
2029
+ return local && p.port === "8009";
2030
+ } catch { return false; }
2031
+ }
2032
+
1812
2033
  // Compare dotted versions: >0 if a newer than b, <0 older, 0 equal.
1813
2034
  function cmpVer(a, b) {
1814
2035
  const pa = String(a).split("."), pb = String(b).split(".");