cicy-desktop 2.1.93 → 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.
- package/package.json +6 -6
- package/src/backends/homepage-preload.js +12 -0
- package/src/backends/homepage-react/assets/index-C7gQsfPP.js +365 -0
- package/src/backends/homepage-react/index.html +1 -1
- package/src/backends/sidecar-ipc.js +60 -0
- package/src/main.js +19 -1
- package/src/sidecar/docker.js +33 -21
- package/src/utils/context-menu-options.js +8 -6
- package/workers/render/src/App.jsx +222 -1
- package/src/backends/homepage-react/assets/index-5vDVCGqD.js +0 -365
|
@@ -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-
|
|
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 () => {
|
package/src/main.js
CHANGED
|
@@ -20,7 +20,25 @@ const appUpdater = require("./app-updater");
|
|
|
20
20
|
// and guests fell back to the OS-native menu; this unifies them and adds 重新加载
|
|
21
21
|
// + 切换开发者工具 + 检查元素 everywhere (see utils/context-menu-options.js).
|
|
22
22
|
const { attachContextMenu } = require("./utils/context-menu-options");
|
|
23
|
-
electronApp.on("web-contents-created", (_e, wc) =>
|
|
23
|
+
electronApp.on("web-contents-created", (_e, wc) => {
|
|
24
|
+
attachContextMenu(wc);
|
|
25
|
+
// Ctrl/Cmd+C inside a <webview> guest: on Windows the application menu's
|
|
26
|
+
// role:"copy" accelerator doesn't reach the focused guest, so a selection
|
|
27
|
+
// couldn't be copied with the keyboard (right-click 复制 is fixed separately via
|
|
28
|
+
// attachContextMenu). Wire copy directly on the guest. COPY-ONLY and WITHOUT
|
|
29
|
+
// preventDefault, so the gotty terminal's Ctrl+C=SIGINT and any web app's own
|
|
30
|
+
// copy handler still fire; wc.copy() is a no-op when there's no selection.
|
|
31
|
+
try {
|
|
32
|
+
if (wc.getType && wc.getType() === "webview") {
|
|
33
|
+
wc.on("before-input-event", (_ev, input) => {
|
|
34
|
+
if (input.type === "keyDown" && (input.control || input.meta) &&
|
|
35
|
+
!input.alt && !input.shift && (input.key === "c" || input.key === "C")) {
|
|
36
|
+
try { wc.copy(); } catch (_) {}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
} catch (_) {}
|
|
41
|
+
});
|
|
24
42
|
|
|
25
43
|
// Setup Electron flags IMMEDIATELY after require
|
|
26
44
|
electronApp.commandLine.appendSwitch("ignore-certificate-errors");
|
package/src/sidecar/docker.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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",
|
|
284
|
+
try { await run(["rm", "-f", container]); } catch {}
|
|
276
285
|
|
|
277
286
|
const args = [
|
|
278
|
-
"run", "-d", "--name",
|
|
287
|
+
"run", "-d", "--name", container, "--restart", "unless-stopped",
|
|
279
288
|
"-p", `${port}:8008`,
|
|
280
|
-
"-v", `${
|
|
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 ${
|
|
290
|
-
return { docker: true, container
|
|
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",
|
|
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
|
-
|
|
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
|
|
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,
|
|
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(
|
|
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 ? "
|
|
384
|
-
return { ok: healthy, 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
|
};
|
|
@@ -58,17 +58,19 @@ const OPTIONS = {
|
|
|
58
58
|
},
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
-
// Attach the menu to the host window
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
61
|
+
// Attach the menu to the host window, BrowserView tabs AND <webview> guests, so
|
|
62
|
+
// right-click → 复制/粘贴 works on EVERY surface. Webviews used to be excluded on
|
|
63
|
+
// the assumption they carry their own copy menu, but on Windows that left
|
|
64
|
+
// selections uncopyable: the app menu's Ctrl+C / role:"copy" doesn't reach the
|
|
65
|
+
// focused <webview> guest, and no own-menu filled the gap. Giving guests the ecm
|
|
66
|
+
// 复制/粘贴 menu fixes copy cross-platform. Idempotent via __cicyCtxMenu so the
|
|
67
|
+
// web-contents-created hook + the tab-browser's per-tab call never double-pop.
|
|
66
68
|
function attachContextMenu(wc) {
|
|
67
69
|
try {
|
|
68
70
|
if (!wc || (wc.isDestroyed && wc.isDestroyed())) return;
|
|
69
71
|
if (wc.__cicyCtxMenu) return;
|
|
70
72
|
const t = wc.getType && wc.getType();
|
|
71
|
-
if (t !== "window" && t !== "browserView") return; //
|
|
73
|
+
if (t !== "window" && t !== "browserView" && t !== "webview") return; // host + tabs + <webview>
|
|
72
74
|
wc.__cicyCtxMenu = true;
|
|
73
75
|
contextMenu({ ...OPTIONS, window: wc });
|
|
74
76
|
} catch (e) {}
|
|
@@ -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(".");
|