cicy-desktop 2.1.96 → 2.1.98
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 +4 -4
- package/src/backends/homepage-react/assets/index-Ka9HcyRP.css +1 -0
- package/src/backends/homepage-react/assets/index-Moep5uf-.js +365 -0
- package/src/backends/homepage-react/index.html +2 -2
- package/src/backends/sidecar-ipc.js +1 -1
- package/src/chrome/chrome-launcher.js +67 -7
- package/src/i18n/locales/en.json +2 -1
- package/src/i18n/locales/fr.json +2 -1
- package/src/i18n/locales/ja.json +2 -1
- package/src/i18n/locales/zh-CN.json +2 -1
- package/src/sidecar/docker.js +35 -10
- package/workers/render/src/App.css +60 -0
- package/workers/render/src/App.jsx +104 -13
- package/src/backends/homepage-react/assets/index-BtVG2Py6.js +0 -365
- package/src/backends/homepage-react/assets/index-CKpaMBKz.css +0 -1
|
@@ -6,8 +6,8 @@
|
|
|
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-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
9
|
+
<script type="module" crossorigin src="./assets/index-Moep5uf-.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="./assets/index-Ka9HcyRP.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
|
@@ -145,7 +145,7 @@ function register({ sidecarLogPath } = {}) {
|
|
|
145
145
|
ipcMain.handle("docker:app-bootstrap", async (e) => {
|
|
146
146
|
if (process.platform !== "win32") return { ok: false, error: "Docker cicy-code is Windows-only" };
|
|
147
147
|
try {
|
|
148
|
-
const installDest = path.join(docker.
|
|
148
|
+
const installDest = path.join(docker.downloadsDir(), "Docker Desktop Installer.exe");
|
|
149
149
|
const result = await docker.bootstrap({
|
|
150
150
|
...appOpts(), installDest,
|
|
151
151
|
onProgress: (ev) => { try { e.sender.send("docker:app-progress", ev); } catch {} },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const os = require("os");
|
|
3
3
|
const path = require("path");
|
|
4
|
-
const { spawn } = require("child_process");
|
|
4
|
+
const { spawn, execFileSync } = require("child_process");
|
|
5
5
|
const { isPortOpen } = require("../utils/process-utils");
|
|
6
6
|
const { waitForDebugger, getVersion } = require("./chrome-cdp-client");
|
|
7
7
|
|
|
@@ -73,19 +73,79 @@ function isDirectPath(binaryPath) {
|
|
|
73
73
|
return binaryPath.includes(path.sep) || (process.platform === "win32" && /^[a-zA-Z]:\\/.test(binaryPath));
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// Windows: Chrome registers its exact install path under "App Paths" on install,
|
|
77
|
+
// independent of where it landed (per-user vs per-machine, custom drive). This is
|
|
78
|
+
// far more reliable than the %ProgramFiles% guesses — those miss per-user installs
|
|
79
|
+
// and break when the launching process runs with a stripped env (e.g. the
|
|
80
|
+
// StartElectron scheduled task), which is why an installed Chrome can still read
|
|
81
|
+
// as "not found".
|
|
82
|
+
function queryWindowsChromeFromRegistry() {
|
|
83
|
+
if (process.platform !== "win32") return null;
|
|
84
|
+
const keys = [
|
|
85
|
+
"HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe",
|
|
86
|
+
"HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe",
|
|
87
|
+
"HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe",
|
|
88
|
+
];
|
|
89
|
+
for (const key of keys) {
|
|
90
|
+
try {
|
|
91
|
+
const out = execFileSync("reg", ["query", key, "/ve"], {
|
|
92
|
+
encoding: "utf8",
|
|
93
|
+
windowsHide: true,
|
|
94
|
+
timeout: 5000,
|
|
95
|
+
});
|
|
96
|
+
const m = out.match(/REG_SZ\s+(.+?\.exe)\s*$/im);
|
|
97
|
+
if (m && fs.existsSync(m[1].trim())) return m[1].trim();
|
|
98
|
+
} catch (_) {}
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// PATH lookup for a bare command — `where` on Windows, `which` on posix. Returns
|
|
104
|
+
// the first existing match, else null.
|
|
105
|
+
function whichBinary(cmd) {
|
|
106
|
+
try {
|
|
107
|
+
const tool = process.platform === "win32" ? "where" : "which";
|
|
108
|
+
const out = execFileSync(tool, [cmd], { encoding: "utf8", windowsHide: true, timeout: 5000 });
|
|
109
|
+
const first = out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0];
|
|
110
|
+
if (first && fs.existsSync(first)) return first;
|
|
111
|
+
} catch (_) {}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
76
115
|
function resolveChromeBinary(binaryPath) {
|
|
77
|
-
|
|
116
|
+
// 1) Explicit override (config.chromeBinary / --chrome-binary) that exists.
|
|
117
|
+
if (binaryPath && isDirectPath(binaryPath) && fs.existsSync(binaryPath)) return binaryPath;
|
|
118
|
+
|
|
119
|
+
// 2) Windows registry App Paths — authoritative, install-location independent.
|
|
120
|
+
const regPath = queryWindowsChromeFromRegistry();
|
|
121
|
+
if (regPath) return regPath;
|
|
78
122
|
|
|
123
|
+
// 3) Platform candidates: concrete paths checked for existence; bare commands
|
|
124
|
+
// resolved via PATH (where/which) so we only accept a Chrome that's actually
|
|
125
|
+
// present.
|
|
126
|
+
const candidates = [binaryPath, ...getBinaryCandidates()].filter(Boolean);
|
|
127
|
+
const bareCommands = [];
|
|
79
128
|
for (const candidate of candidates) {
|
|
80
129
|
if (isDirectPath(candidate)) {
|
|
81
|
-
if (fs.existsSync(candidate))
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
130
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
131
|
+
} else {
|
|
132
|
+
bareCommands.push(candidate);
|
|
133
|
+
const resolved = whichBinary(candidate);
|
|
134
|
+
if (resolved) return resolved;
|
|
85
135
|
}
|
|
86
|
-
return candidate;
|
|
87
136
|
}
|
|
88
137
|
|
|
138
|
+
// 4) Name-based last resort.
|
|
139
|
+
const byName =
|
|
140
|
+
whichBinary(process.platform === "win32" ? "chrome" : "google-chrome") ||
|
|
141
|
+
whichBinary(process.platform === "win32" ? "chrome.exe" : "chromium");
|
|
142
|
+
if (byName) return byName;
|
|
143
|
+
|
|
144
|
+
// 5) Nothing concrete found. On posix, let spawn try the first bare command
|
|
145
|
+
// (covers exotic PATH setups where `which` itself isn't available); on
|
|
146
|
+
// Windows everything is a concrete path, so fail with a clear message.
|
|
147
|
+
if (bareCommands.length) return bareCommands[0];
|
|
148
|
+
|
|
89
149
|
throw new Error(
|
|
90
150
|
"Chrome/Chromium binary not found. Please configure chromeBinary or --chrome-binary."
|
|
91
151
|
);
|
package/src/i18n/locales/en.json
CHANGED
package/src/i18n/locales/fr.json
CHANGED
package/src/i18n/locales/ja.json
CHANGED
package/src/sidecar/docker.js
CHANGED
|
@@ -160,9 +160,10 @@ function headSize(url, hops = 5) {
|
|
|
160
160
|
async function ensureDownloaded(url, dest, mirror, { emit, phase, label, freshOnIncomplete = false } = {}) {
|
|
161
161
|
const expected = (await headSize(url)) || (mirror ? await headSize(mirror) : 0);
|
|
162
162
|
let have = 0; try { have = fs.statSync(dest).size; } catch {}
|
|
163
|
-
// Complete file already on disk → skip (主人: 完整的 exe
|
|
163
|
+
// Complete file already on disk → skip (主人: 完整的 exe/镜像包就别重下了;用户
|
|
164
|
+
// 自己下到 ~/Downloads 同名文件也走这条直接复用).
|
|
164
165
|
if (expected > 0 && have === expected) {
|
|
165
|
-
emit && emit({ phase, status: "skip", message: `${label}:已下载,跳过`, progress: 100 });
|
|
166
|
+
emit && emit({ phase, status: "skip", message: `${label}:已下载,跳过`, progress: 100, received: have, total: expected, url, dest });
|
|
166
167
|
return dest;
|
|
167
168
|
}
|
|
168
169
|
// A partial left by a PREVIOUS, interrupted/restarted session can be corrupt;
|
|
@@ -178,13 +179,18 @@ async function ensureDownloaded(url, dest, mirror, { emit, phase, label, freshOn
|
|
|
178
179
|
let lastPct = -1; // throttle: chunks arrive dozens/s — only emit on whole-percent change
|
|
179
180
|
const attempted = withRetry(async (attempt) => {
|
|
180
181
|
const src = sources[Math.min(attempt - 1, sources.length - 1)];
|
|
182
|
+
// 断点续传 (主人): resume the partial via a Range request instead of
|
|
183
|
+
// restarting from 0 — efficient on a flaky network. The post-download size
|
|
184
|
+
// check below + loadImage's load-failure cleanup guard against a bad partial.
|
|
181
185
|
await download(src, dest, {
|
|
182
186
|
resume: true,
|
|
183
187
|
onProgress: ({ received, total }) => {
|
|
184
188
|
const pct = total ? Math.round((received / total) * 100) : 0;
|
|
185
189
|
if (pct === lastPct) return;
|
|
186
190
|
lastPct = pct;
|
|
187
|
-
|
|
191
|
+
// `url` = source, `dest` = local target path (主人: UI 显示下载目录; lets the
|
|
192
|
+
// user drop a manual download at the same path and have it reused).
|
|
193
|
+
emit && emit({ phase, status: "running", message: label, progress: pct, received, total, url: src, dest });
|
|
188
194
|
},
|
|
189
195
|
});
|
|
190
196
|
if (expected > 0) {
|
|
@@ -240,10 +246,16 @@ function probeHealth(port = 8008, timeoutMs = 2500) {
|
|
|
240
246
|
// ~/Downloads — visible, like the Docker installer on the Desktop). STABLE name
|
|
241
247
|
// (no pid) so a re-run reuses an existing partial/complete file (resume-friendly
|
|
242
248
|
// on a flaky network).
|
|
243
|
-
|
|
249
|
+
// Both the Docker installer AND the image tarball download here (主人: 都下到
|
|
250
|
+
// ~/Downloads). If the user manually downloads either file to this folder with
|
|
251
|
+
// the SAME name, ensureDownloaded sees a complete file and skips the download.
|
|
252
|
+
function downloadsDir() {
|
|
244
253
|
const dir = path.join(process.env["USERPROFILE"] || os.homedir(), "Downloads");
|
|
245
254
|
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
246
|
-
return
|
|
255
|
+
return dir;
|
|
256
|
+
}
|
|
257
|
+
function imageTarballPath() {
|
|
258
|
+
return path.join(downloadsDir(), "cicy-code-latest.tar.gz");
|
|
247
259
|
}
|
|
248
260
|
|
|
249
261
|
// Download the R2 base-env image tarball (no docker needed yet). Split out of
|
|
@@ -251,7 +263,7 @@ function imageTarballPath() {
|
|
|
251
263
|
// install (主人: 装 Docker 的同时下载 R2 镜像). Returns the tarball path.
|
|
252
264
|
async function downloadImageTarball({ emit } = {}) {
|
|
253
265
|
const dest = imageTarballPath();
|
|
254
|
-
await ensureDownloaded(R2_TARBALL, dest, null, { emit, phase: "image", label: "下载镜像"
|
|
266
|
+
await ensureDownloaded(R2_TARBALL, dest, null, { emit, phase: "image", label: "下载镜像" });
|
|
255
267
|
return dest;
|
|
256
268
|
}
|
|
257
269
|
|
|
@@ -260,7 +272,16 @@ async function downloadImageTarball({ emit } = {}) {
|
|
|
260
272
|
async function loadImageFromTarball(tmp, { emit } = {}) {
|
|
261
273
|
emit && emit({ phase: "image", status: "running", message: "docker load…", progress: 100 });
|
|
262
274
|
console.log(`[docker-sidecar] docker load…`);
|
|
263
|
-
|
|
275
|
+
let stdout;
|
|
276
|
+
try {
|
|
277
|
+
({ stdout } = await run(["load", "-i", tmp], { timeout: 300000 }));
|
|
278
|
+
} catch (e) {
|
|
279
|
+
// A resumed download can leave a byte-correct-size but corrupt tarball that
|
|
280
|
+
// `docker load` rejects. Delete it so the next attempt re-downloads fresh
|
|
281
|
+
// (断点续传 normally, fresh only when proven bad).
|
|
282
|
+
try { fs.unlinkSync(tmp); } catch {}
|
|
283
|
+
throw e;
|
|
284
|
+
}
|
|
264
285
|
// The tarball's embedded tag may be a pinned version (e.g. :2.1.6) while we
|
|
265
286
|
// run IMAGE (:latest). Re-tag whatever was loaded so imagePresent()/start()
|
|
266
287
|
// match — otherwise every start() re-downloads the tarball forever.
|
|
@@ -364,7 +385,7 @@ async function installDocker({ emit, dest } = {}) {
|
|
|
364
385
|
try { fs.mkdirSync(path.dirname(target), { recursive: true }); } catch {}
|
|
365
386
|
e({ phase: "install-docker", status: "running", message: "下载 Docker Desktop 安装包…", progress: 0 });
|
|
366
387
|
await ensureDownloaded(DOCKER_DESKTOP_URL, target, DOCKER_DESKTOP_MIRROR, {
|
|
367
|
-
emit, phase: "install-docker", label: "下载 Docker Desktop",
|
|
388
|
+
emit, phase: "install-docker", label: "下载 Docker Desktop",
|
|
368
389
|
});
|
|
369
390
|
e({ phase: "install-docker", status: "running", message: "安装 Docker Desktop(请在弹出的授权框点「是」,装完可能需重启)…" });
|
|
370
391
|
await new Promise((resolve) => {
|
|
@@ -411,7 +432,11 @@ async function bootstrap({ onProgress, port = 8008, container = CONTAINER, volum
|
|
|
411
432
|
// running + the daemon coming up (主人: 装 Docker 的同时下载 R2 镜像).
|
|
412
433
|
if (needImage) imgDl = downloadImageTarball({ emit }).catch((e) => { emit({ phase: "image", status: "error", message: `镜像下载失败:${e.message}` }); return null; });
|
|
413
434
|
await installDocker({ emit, dest: installDest });
|
|
414
|
-
|
|
435
|
+
// A silent install doesn't auto-launch the daemon — explicitly start Docker
|
|
436
|
+
// Desktop once its exe lands so the user doesn't have to (主人: 安装启动有问题).
|
|
437
|
+
emit({ phase: "install-docker", status: "running", message: "启动 Docker Desktop…" });
|
|
438
|
+
const launched = await waitUntil(() => { if (dockerDesktopExe()) { startDockerDesktop(); return true; } return false; }, { totalMs: 120000, everyMs: 5000 });
|
|
439
|
+
emit({ phase: "install-docker", status: "running", message: launched ? "等待 Docker 引擎就绪(首次启动较慢,如弹授权/重启请确认)…" : "等待 Docker 安装完成…" });
|
|
415
440
|
const up = await waitUntil(dockerOk, { totalMs: 900000, everyMs: 6000 });
|
|
416
441
|
if (!up) {
|
|
417
442
|
emit({ phase: "install-docker", status: "error", message: "Docker 还没就绪——装好后启动 Docker Desktop,再点「重试」即可(已完成的步骤不会重来)" });
|
|
@@ -462,7 +487,7 @@ async function bootstrap({ onProgress, port = 8008, container = CONTAINER, volum
|
|
|
462
487
|
module.exports = {
|
|
463
488
|
start, stop, stopContainer, restart, checkStatus, loadImage, loadImageFromTarball,
|
|
464
489
|
downloadImageTarball, imagePresent, dockerOk, installDocker,
|
|
465
|
-
bootstrap, probeHealth, readContainerToken, dockerDesktopExe, desktopDir,
|
|
490
|
+
bootstrap, probeHealth, readContainerToken, dockerDesktopExe, desktopDir, downloadsDir, imageTarballPath,
|
|
466
491
|
// platform-agnostic download/retry primitives, reused by native.js
|
|
467
492
|
ensureDownloaded, withRetry, waitUntil, run,
|
|
468
493
|
};
|
|
@@ -213,6 +213,13 @@ body {
|
|
|
213
213
|
.user-chip__menu-item.is-danger { color: #f7a3a3; }
|
|
214
214
|
.user-chip__menu-item.is-danger:hover { background: rgba(239,68,68,.16); color: #fff; }
|
|
215
215
|
.user-chip__menu-sep { height: 1px; margin: 4px 2px; background: rgba(255,255,255,.08); }
|
|
216
|
+
/* cicy-desktop version — bottom of the avatar dropdown (主人) */
|
|
217
|
+
.user-chip__menu-version {
|
|
218
|
+
margin-top: 4px; padding: 8px 11px 4px;
|
|
219
|
+
font-size: 11px; color: #6b7280; text-align: center;
|
|
220
|
+
font-variant-numeric: tabular-nums; user-select: text;
|
|
221
|
+
border-top: 1px solid rgba(255,255,255,.06);
|
|
222
|
+
}
|
|
216
223
|
/* HTTPS audit tip, rendered as a flat menu row with an on/off switch */
|
|
217
224
|
.user-chip__mitm-row {
|
|
218
225
|
display: flex; align-items: center; justify-content: space-between; gap: 10px;
|
|
@@ -969,6 +976,59 @@ body {
|
|
|
969
976
|
background: rgba(10, 12, 16, 0.7); border: 1px solid rgba(125, 135, 150, 0.12); border-radius: 10px;
|
|
970
977
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11.5px; line-height: 1.7;
|
|
971
978
|
}
|
|
979
|
+
/* Custom thin scrollbar for the install log (主人: scroll 用自定义 style) */
|
|
980
|
+
.drawer__log--scroll { max-height: 168px; scrollbar-width: thin; scrollbar-color: rgba(125,135,150,.4) transparent; }
|
|
981
|
+
.drawer__log--scroll::-webkit-scrollbar { width: 7px; }
|
|
982
|
+
.drawer__log--scroll::-webkit-scrollbar-track { background: transparent; margin: 4px 0; }
|
|
983
|
+
.drawer__log--scroll::-webkit-scrollbar-thumb { background: rgba(125,135,150,.32); border-radius: 6px; border: 1.5px solid transparent; background-clip: padding-box; }
|
|
984
|
+
.drawer__log--scroll::-webkit-scrollbar-thumb:hover { background: rgba(125,135,150,.55); background-clip: padding-box; }
|
|
985
|
+
|
|
986
|
+
/* Fixed download progress bars (Docker Desktop / 基础镜像) — no scroll spam */
|
|
987
|
+
.drawer__dlbars { display: flex; flex-direction: column; gap: 10px; margin: 12px 14px 2px; }
|
|
988
|
+
.dlbar {
|
|
989
|
+
padding: 10px 12px; border-radius: 10px;
|
|
990
|
+
background: rgba(10, 12, 16, 0.55); border: 1px solid rgba(125, 135, 150, 0.14);
|
|
991
|
+
}
|
|
992
|
+
.dlbar__head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 7px; }
|
|
993
|
+
.dlbar__name { font-size: 12.5px; font-weight: 600; color: var(--text, #e6eaf0); }
|
|
994
|
+
.dlbar__pct { font-size: 11px; color: var(--text-dim, #9da7b3); font-variant-numeric: tabular-nums; }
|
|
995
|
+
.dlbar__track { height: 6px; border-radius: 4px; background: rgba(125, 135, 150, 0.16); overflow: hidden; }
|
|
996
|
+
.dlbar__fill {
|
|
997
|
+
height: 100%; border-radius: 4px;
|
|
998
|
+
background: linear-gradient(90deg, #5b8df7, #2496ed);
|
|
999
|
+
transition: width .2s ease;
|
|
1000
|
+
}
|
|
1001
|
+
.dlbar__fill.is-done { background: #4ade80; }
|
|
1002
|
+
.dlbar__url {
|
|
1003
|
+
margin-top: 6px; font-size: 10.5px; color: #6b7686;
|
|
1004
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1005
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
1006
|
+
}
|
|
1007
|
+
.dlbar__urlk {
|
|
1008
|
+
display: inline-block; min-width: 16px; text-align: center;
|
|
1009
|
+
margin-right: 5px; padding: 0 4px; border-radius: 4px;
|
|
1010
|
+
font-family: inherit; font-size: 9.5px; color: #8fb0f5; background: rgba(91,141,247,.16);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/* Drawer header buttons (minimize ‒ / close ×) */
|
|
1014
|
+
.drawer__headbtns { display: flex; align-items: center; gap: 2px; }
|
|
1015
|
+
|
|
1016
|
+
/* Minimized drawer → floating restore chip (op keeps running) */
|
|
1017
|
+
.drawer-min {
|
|
1018
|
+
position: fixed; right: 20px; bottom: 20px; z-index: 2001;
|
|
1019
|
+
display: inline-flex; align-items: center; gap: 9px;
|
|
1020
|
+
padding: 10px 15px; border-radius: 999px; cursor: pointer;
|
|
1021
|
+
background: #16181d; border: 1px solid rgba(125,135,150,.22);
|
|
1022
|
+
box-shadow: 0 10px 30px rgba(0,0,0,.5);
|
|
1023
|
+
color: var(--text, #e6eaf0); font-size: 12.5px; font-weight: 550;
|
|
1024
|
+
animation: drawer-min-in .18s cubic-bezier(.2,.8,.2,1);
|
|
1025
|
+
}
|
|
1026
|
+
.drawer-min:hover { border-color: rgba(125,135,150,.4); background: #1b1e24; }
|
|
1027
|
+
@keyframes drawer-min-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
|
|
1028
|
+
.drawer-min__spark { display: inline-flex; width: 16px; height: 16px; align-items: center; justify-content: center; font-weight: 700; }
|
|
1029
|
+
.drawer-min--done .drawer-min__spark { color: #4ade80; }
|
|
1030
|
+
.drawer-min--error .drawer-min__spark { color: #f87171; }
|
|
1031
|
+
.drawer-min__label { font-variant-numeric: tabular-nums; }
|
|
972
1032
|
.drawer__log-empty { color: var(--text-dim, #9da7b3); }
|
|
973
1033
|
.drawer__line { display: flex; align-items: baseline; gap: 8px; padding: 1px 0; }
|
|
974
1034
|
.drawer__t { color: #6b7686; flex: none; font-variant-numeric: tabular-nums; }
|
|
@@ -109,11 +109,13 @@ const updateDrawer = {
|
|
|
109
109
|
};
|
|
110
110
|
emitDrawer();
|
|
111
111
|
},
|
|
112
|
+
minimize() { if (drawerState) { drawerState = { ...drawerState, minimized: true }; emitDrawer(); } },
|
|
113
|
+
restore() { if (drawerState) { drawerState = { ...drawerState, minimized: false }; emitDrawer(); } },
|
|
112
114
|
finish({ ok, message } = {}) {
|
|
113
115
|
if (!drawerState) return;
|
|
114
116
|
const status = ok ? "done" : "error";
|
|
115
117
|
const line = { id: ++drawerLogSeq, t: clockHHMMSS(), phase: "done", status, message: message || (ok ? "更新完成" : "更新失败") };
|
|
116
|
-
drawerState = { ...drawerState, status, phase: "done", logs: [...drawerState.logs, line], lastAt: Date.now() };
|
|
118
|
+
drawerState = { ...drawerState, status, phase: "done", minimized: false, logs: [...drawerState.logs, line], lastAt: Date.now() };
|
|
117
119
|
emitDrawer();
|
|
118
120
|
},
|
|
119
121
|
close() { drawerState = null; emitDrawer(); },
|
|
@@ -136,8 +138,16 @@ function UpdateDrawerHost() {
|
|
|
136
138
|
if (!st) return null;
|
|
137
139
|
const running = st.status === "running";
|
|
138
140
|
const phaseIdx = DRAWER_PHASES.findIndex(([k]) => k === st.phase);
|
|
141
|
+
if (st.minimized) {
|
|
142
|
+
return (
|
|
143
|
+
<button type="button" className={`drawer-min drawer-min--${st.status}`} data-id="UpdateDrawer-restore" onClick={() => updateDrawer.restore()}>
|
|
144
|
+
<span className="drawer-min__spark">{running ? <Spinner /> : st.status === "done" ? "✓" : "!"}</span>
|
|
145
|
+
<span className="drawer-min__label">更新 cicy-code{st.toVer ? ` · v${st.toVer}` : ""}</span>
|
|
146
|
+
</button>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
139
149
|
return (
|
|
140
|
-
<div className="drawer-scrim" data-id="UpdateDrawer-scrim" onClick={() =>
|
|
150
|
+
<div className="drawer-scrim" data-id="UpdateDrawer-scrim" onClick={() => running ? updateDrawer.minimize() : updateDrawer.close()}>
|
|
141
151
|
<div className="drawer" data-id="UpdateDrawer" data-status={st.status} onClick={(e) => e.stopPropagation()}>
|
|
142
152
|
<div className="drawer__head">
|
|
143
153
|
<div className="drawer__title">
|
|
@@ -149,7 +159,10 @@ function UpdateDrawerHost() {
|
|
|
149
159
|
<div className="drawer__sub">{st.fromVer ? `v${st.fromVer}` : "当前"} → {st.toVer ? `v${st.toVer}` : "最新版"}</div>
|
|
150
160
|
</div>
|
|
151
161
|
</div>
|
|
152
|
-
<
|
|
162
|
+
<div className="drawer__headbtns">
|
|
163
|
+
<button type="button" className="drawer__x" data-id="UpdateDrawer-min" title="最小化" onClick={() => updateDrawer.minimize()} aria-label="minimize">‒</button>
|
|
164
|
+
<button type="button" className="drawer__x" data-id="UpdateDrawer-close" title="关闭" onClick={() => running ? updateDrawer.minimize() : updateDrawer.close()} aria-label="close">×</button>
|
|
165
|
+
</div>
|
|
153
166
|
</div>
|
|
154
167
|
|
|
155
168
|
<div className="drawer__steps" data-id="UpdateDrawer-steps">
|
|
@@ -189,7 +202,6 @@ function UpdateDrawerHost() {
|
|
|
189
202
|
{running ? (
|
|
190
203
|
<>
|
|
191
204
|
<span className="drawer__foot-status">更新进行中…</span>
|
|
192
|
-
<button type="button" className="drawer__btn" data-id="UpdateDrawer-background" onClick={() => updateDrawer.close()}>在后台继续</button>
|
|
193
205
|
</>
|
|
194
206
|
) : st.status === "error" ? (
|
|
195
207
|
<>
|
|
@@ -1022,7 +1034,19 @@ function Header({ me, welcome, onLogout, mitmTeam }) {
|
|
|
1022
1034
|
const [trustOpen, setTrustOpen] = useState(false);
|
|
1023
1035
|
const [auditOpen, setAuditOpen] = useState(false);
|
|
1024
1036
|
const [termsOpen, setTermsOpen] = useState(false);
|
|
1037
|
+
const [appVer, setAppVer] = useState("");
|
|
1025
1038
|
const wrap = useRef(null);
|
|
1039
|
+
// cicy-desktop's own version, shown at the very bottom of this menu (主人).
|
|
1040
|
+
// app.getVersion() returns { desktop, cicyCodeRef, electron, node } — pick the
|
|
1041
|
+
// desktop version string (was rendering as [object Object]).
|
|
1042
|
+
useEffect(() => {
|
|
1043
|
+
let alive = true;
|
|
1044
|
+
window.cicy?.app?.getVersion?.().then((v) => {
|
|
1045
|
+
if (!alive) return;
|
|
1046
|
+
setAppVer(typeof v === "string" ? v : String(v?.desktop || ""));
|
|
1047
|
+
}).catch(() => {});
|
|
1048
|
+
return () => { alive = false; };
|
|
1049
|
+
}, []);
|
|
1026
1050
|
// Click-outside closes the dropdown (mirrors LocalTeamCard's ⋯ menu).
|
|
1027
1051
|
useEffect(() => {
|
|
1028
1052
|
if (!open) return;
|
|
@@ -1077,6 +1101,9 @@ function Header({ me, welcome, onLogout, mitmTeam }) {
|
|
|
1077
1101
|
<button type="button" data-id="UserChip-logout" className="user-chip__menu-item is-danger" onClick={() => { setOpen(false); onLogout(); }}>
|
|
1078
1102
|
退出
|
|
1079
1103
|
</button>
|
|
1104
|
+
<div className="user-chip__menu-version" data-id="UserChip-version">
|
|
1105
|
+
CiCy Desktop {appVer ? `v${appVer}` : "…"}
|
|
1106
|
+
</div>
|
|
1080
1107
|
</div>
|
|
1081
1108
|
)}
|
|
1082
1109
|
</div>
|
|
@@ -1435,27 +1462,71 @@ let dockerDrawerState = null; // null = closed
|
|
|
1435
1462
|
function emitDockerDrawer() { dockerDrawerListeners.forEach((l) => l(dockerDrawerState)); }
|
|
1436
1463
|
const dockerDrawer = {
|
|
1437
1464
|
open({ onRetry } = {}) {
|
|
1438
|
-
dockerDrawerState = { status: "running", phase: "install-docker", logs: [], onRetry: onRetry || null, lastAt: Date.now() };
|
|
1465
|
+
dockerDrawerState = { status: "running", phase: "install-docker", logs: [], bars: {}, minimized: false, onRetry: onRetry || null, lastAt: Date.now() };
|
|
1439
1466
|
emitDockerDrawer();
|
|
1440
1467
|
},
|
|
1468
|
+
minimize() { if (dockerDrawerState) { dockerDrawerState = { ...dockerDrawerState, minimized: true }; emitDockerDrawer(); } },
|
|
1469
|
+
restore() { if (dockerDrawerState) { dockerDrawerState = { ...dockerDrawerState, minimized: false }; emitDockerDrawer(); } },
|
|
1441
1470
|
push(ev = {}) {
|
|
1442
1471
|
if (!dockerDrawerState) return;
|
|
1443
1472
|
const phase = ev.phase === "health" ? "container" : (ev.phase || dockerDrawerState.phase);
|
|
1444
|
-
const
|
|
1445
|
-
|
|
1473
|
+
const next = { ...dockerDrawerState, phase, lastAt: Date.now() };
|
|
1474
|
+
const hasPct = Number.isFinite(ev.progress);
|
|
1475
|
+
const isDl = phase === "install-docker" || phase === "image";
|
|
1476
|
+
// Any download-related event (running %, skip, done — they carry url/dest)
|
|
1477
|
+
// drives a per-phase PROGRESS BAR, not a log line, so the log doesn't
|
|
1478
|
+
// scroll-spam (主人: 下载不要输出滚动/日志太多).
|
|
1479
|
+
if (isDl && (hasPct || ev.dest || ev.url)) {
|
|
1480
|
+
const prev = dockerDrawerState.bars?.[phase] || {};
|
|
1481
|
+
const progress = hasPct ? ev.progress : (ev.status === "skip" || ev.status === "done") ? 100 : prev.progress;
|
|
1482
|
+
next.bars = { ...dockerDrawerState.bars, [phase]: { progress, received: ev.received ?? prev.received, total: ev.total ?? prev.total, url: ev.url || prev.url, dest: ev.dest || prev.dest } };
|
|
1483
|
+
}
|
|
1484
|
+
// Log only milestone events — never the per-% running download ticks.
|
|
1485
|
+
const isRunningTick = ev.status === "running" && hasPct && isDl;
|
|
1486
|
+
if (!isRunningTick) {
|
|
1487
|
+
const line = { id: ++dockerDrawerLogSeq, t: clockHHMMSS(), phase, status: ev.status || "running", message: ev.message || "" };
|
|
1488
|
+
next.logs = [...dockerDrawerState.logs, line];
|
|
1489
|
+
}
|
|
1490
|
+
dockerDrawerState = next;
|
|
1446
1491
|
emitDockerDrawer();
|
|
1447
1492
|
},
|
|
1448
1493
|
finish({ ok, message } = {}) {
|
|
1449
1494
|
if (!dockerDrawerState) return;
|
|
1450
1495
|
const status = ok ? "done" : "error";
|
|
1451
1496
|
const line = { id: ++dockerDrawerLogSeq, t: clockHHMMSS(), phase: "done", status, message: message || (ok ? "完成" : "失败") };
|
|
1452
|
-
|
|
1497
|
+
// Pop back open on finish so the user sees the result even if minimized.
|
|
1498
|
+
dockerDrawerState = { ...dockerDrawerState, status, phase: "done", minimized: false, logs: [...dockerDrawerState.logs, line], lastAt: Date.now() };
|
|
1453
1499
|
emitDockerDrawer();
|
|
1454
1500
|
},
|
|
1455
1501
|
close() { dockerDrawerState = null; emitDockerDrawer(); },
|
|
1456
1502
|
};
|
|
1457
1503
|
const DOCKER_PHASES = [["install-docker", "装 Docker"], ["image", "加载镜像"], ["container", "启动容器"], ["done", "完成"]];
|
|
1458
1504
|
const DOCKER_BADGE = { "install-docker": "Docker", image: "镜像", container: "容器", health: "容器", done: "完成" };
|
|
1505
|
+
const DOCKER_DL_LABEL = { "install-docker": "Docker Desktop", image: "基础镜像" };
|
|
1506
|
+
function fmtBytes(n) {
|
|
1507
|
+
if (!Number.isFinite(n)) return "?";
|
|
1508
|
+
if (n < 1024) return n + " B";
|
|
1509
|
+
if (n < 1048576) return (n / 1024).toFixed(0) + " KB";
|
|
1510
|
+
if (n < 1073741824) return (n / 1048576).toFixed(1) + " MB";
|
|
1511
|
+
return (n / 1073741824).toFixed(2) + " GB";
|
|
1512
|
+
}
|
|
1513
|
+
// One fixed (non-scrolling) progress bar per download (Docker Desktop / image),
|
|
1514
|
+
// showing the source URL + % + bytes (主人: 下载做进度条、显示地址、不要滚动).
|
|
1515
|
+
function DownloadBar({ phaseKey, bar }) {
|
|
1516
|
+
const pct = Number.isFinite(bar?.progress) ? Math.max(0, Math.min(100, bar.progress)) : 0;
|
|
1517
|
+
const done = pct >= 100;
|
|
1518
|
+
return (
|
|
1519
|
+
<div className="dlbar" data-id={`DockerDrawer-dlbar-${phaseKey}`}>
|
|
1520
|
+
<div className="dlbar__head">
|
|
1521
|
+
<span className="dlbar__name">{DOCKER_DL_LABEL[phaseKey] || phaseKey}</span>
|
|
1522
|
+
<span className="dlbar__pct">{pct}%{bar?.total ? ` · ${fmtBytes(bar.received)} / ${fmtBytes(bar.total)}` : ""}</span>
|
|
1523
|
+
</div>
|
|
1524
|
+
<div className="dlbar__track"><div className={`dlbar__fill${done ? " is-done" : ""}`} style={{ width: `${pct}%` }} /></div>
|
|
1525
|
+
{bar?.url && <div className="dlbar__url" title={bar.url}><span className="dlbar__urlk">源</span> {bar.url}</div>}
|
|
1526
|
+
{bar?.dest && <div className="dlbar__url" title={bar.dest}><span className="dlbar__urlk">存</span> {bar.dest}</div>}
|
|
1527
|
+
</div>
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1459
1530
|
function DockerInstallDrawerHost() {
|
|
1460
1531
|
const [st, setSt] = useState(dockerDrawerState);
|
|
1461
1532
|
useEffect(() => { dockerDrawerListeners.add(setSt); return () => { dockerDrawerListeners.delete(setSt); }; }, []);
|
|
@@ -1464,8 +1535,20 @@ function DockerInstallDrawerHost() {
|
|
|
1464
1535
|
if (!st) return null;
|
|
1465
1536
|
const running = st.status === "running";
|
|
1466
1537
|
const phaseIdx = DOCKER_PHASES.findIndex(([k]) => k === st.phase);
|
|
1538
|
+
const dlBars = ["install-docker", "image"].filter((k) => st.bars?.[k]);
|
|
1539
|
+
// Minimized → a floating restore chip (op keeps running in the background).
|
|
1540
|
+
if (st.minimized) {
|
|
1541
|
+
const pcts = dlBars.map((k) => st.bars[k]?.progress).filter(Number.isFinite);
|
|
1542
|
+
const overall = pcts.length ? Math.round(pcts.reduce((a, b) => a + b, 0) / pcts.length) : null;
|
|
1543
|
+
return (
|
|
1544
|
+
<button type="button" className={`drawer-min drawer-min--${st.status}`} data-id="DockerDrawer-restore" onClick={() => dockerDrawer.restore()}>
|
|
1545
|
+
<span className="drawer-min__spark">{running ? <Spinner /> : st.status === "done" ? "✓" : "!"}</span>
|
|
1546
|
+
<span className="drawer-min__label">{tr("docker.setupTitle", "安装 Docker cicy-code")}{overall != null ? ` · ${overall}%` : ""}</span>
|
|
1547
|
+
</button>
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1467
1550
|
return (
|
|
1468
|
-
<div className="drawer-scrim" data-id="DockerDrawer-scrim" onClick={() =>
|
|
1551
|
+
<div className="drawer-scrim" data-id="DockerDrawer-scrim" onClick={() => running ? dockerDrawer.minimize() : dockerDrawer.close()}>
|
|
1469
1552
|
<div className="drawer" data-id="DockerDrawer" data-status={st.status} onClick={(e) => e.stopPropagation()}>
|
|
1470
1553
|
<div className="drawer__head">
|
|
1471
1554
|
<div className="drawer__title">
|
|
@@ -1477,7 +1560,10 @@ function DockerInstallDrawerHost() {
|
|
|
1477
1560
|
<div className="drawer__sub">127.0.0.1:8009</div>
|
|
1478
1561
|
</div>
|
|
1479
1562
|
</div>
|
|
1480
|
-
<
|
|
1563
|
+
<div className="drawer__headbtns">
|
|
1564
|
+
<button type="button" className="drawer__x" data-id="DockerDrawer-min" title={tr("common.minimize", "最小化")} onClick={() => dockerDrawer.minimize()} aria-label="minimize">‒</button>
|
|
1565
|
+
<button type="button" className="drawer__x" data-id="DockerDrawer-close" title={tr("common.close", "关闭")} onClick={() => running ? dockerDrawer.minimize() : dockerDrawer.close()} aria-label="close">×</button>
|
|
1566
|
+
</div>
|
|
1481
1567
|
</div>
|
|
1482
1568
|
|
|
1483
1569
|
<div className="drawer__steps" data-id="DockerDrawer-steps">
|
|
@@ -1495,14 +1581,20 @@ function DockerInstallDrawerHost() {
|
|
|
1495
1581
|
})}
|
|
1496
1582
|
</div>
|
|
1497
1583
|
|
|
1498
|
-
|
|
1584
|
+
{dlBars.length > 0 && (
|
|
1585
|
+
<div className="drawer__dlbars" data-id="DockerDrawer-dlbars">
|
|
1586
|
+
{dlBars.map((k) => <DownloadBar key={k} phaseKey={k} bar={st.bars[k]} />)}
|
|
1587
|
+
</div>
|
|
1588
|
+
)}
|
|
1589
|
+
|
|
1590
|
+
<div className="drawer__log drawer__log--scroll" data-id="DockerDrawer-log" ref={logRef}>
|
|
1499
1591
|
{st.logs.length === 0
|
|
1500
1592
|
? <div className="drawer__log-empty">{tr("docker.preparing", "准备中…")}</div>
|
|
1501
1593
|
: st.logs.map((l) => (
|
|
1502
1594
|
<div key={l.id} className="drawer__line" data-status={l.status}>
|
|
1503
1595
|
<span className="drawer__t">{l.t}</span>
|
|
1504
1596
|
<span className={`drawer__badge drawer__badge--${l.phase}`}>{DOCKER_BADGE[l.phase] || l.phase}</span>
|
|
1505
|
-
<span className="drawer__linemsg">{l.message}
|
|
1597
|
+
<span className="drawer__linemsg">{l.message}</span>
|
|
1506
1598
|
</div>
|
|
1507
1599
|
))}
|
|
1508
1600
|
</div>
|
|
@@ -1511,7 +1603,6 @@ function DockerInstallDrawerHost() {
|
|
|
1511
1603
|
{running ? (
|
|
1512
1604
|
<>
|
|
1513
1605
|
<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
1606
|
</>
|
|
1516
1607
|
) : st.status === "error" ? (
|
|
1517
1608
|
<>
|