cicy-desktop 2.1.113 → 2.1.115
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 +3 -0
- package/src/backends/homepage-react/assets/{index-CgqXmbpX.js → index-C6Wa2I-i.js} +13 -13
- package/src/backends/homepage-react/index.html +1 -1
- package/src/backends/local-teams.js +11 -4
- package/src/backends/sidecar-ipc.js +56 -15
- package/src/sidecar/wsl-docker.js +80 -16
- package/workers/render/src/App.jsx +45 -15
|
@@ -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-C6Wa2I-i.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="./assets/index-BcVFakIC.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
@@ -219,12 +219,14 @@ async function list({ refresh = false } = {}) {
|
|
|
219
219
|
// dom-ready electronRPC injection — bare `new BrowserWindow` strips
|
|
220
220
|
// the SPA of every desktop tool, which was the regression in the
|
|
221
221
|
// previous implementation.
|
|
222
|
-
async function openTeam(id) {
|
|
222
|
+
async function openTeam(id, opts = {}) {
|
|
223
223
|
const node = readNodes()[id];
|
|
224
224
|
if (!node) return { ok: false, error: "team not found" };
|
|
225
225
|
const baseUrl = (node.base_url || "").replace(/\/$/, "");
|
|
226
226
|
if (!baseUrl) return { ok: false, error: "no base_url" };
|
|
227
|
-
|
|
227
|
+
// opts.token (a LIVE-read token, e.g. the :8009 container's own) takes
|
|
228
|
+
// precedence over any stored token — the Docker team stores none.
|
|
229
|
+
const token = (opts && opts.token) || node.api_token || "";
|
|
228
230
|
const url = token ? `${baseUrl}/?token=${encodeURIComponent(token)}` : baseUrl;
|
|
229
231
|
|
|
230
232
|
// Compare by origin+pathname only — token + hash both vary per
|
|
@@ -512,7 +514,11 @@ async function addTeam(spec) {
|
|
|
512
514
|
// pass it, leaving the swap URL with no `?token=` and stranding the user
|
|
513
515
|
// at a login screen. Auto-fill from local global.json (top-level api_token)
|
|
514
516
|
// so the common case "Just Works", even when spec.api_token is empty.
|
|
515
|
-
|
|
517
|
+
// skipTokenAutofill: the :8009 Docker team must NEVER store a token — its token
|
|
518
|
+
// is read LIVE from the container on every open (主人: teams.json 不存 8009 的
|
|
519
|
+
// token / docker 的 token 是实时拿的). Without this guard the auto-fill below
|
|
520
|
+
// back-fills the HOST 8008 token, which 8009 rejects → endless login screen.
|
|
521
|
+
if (!spec.api_token && !spec.skipTokenAutofill) {
|
|
516
522
|
try {
|
|
517
523
|
const host = new URL(baseUrl).hostname;
|
|
518
524
|
if (host === "127.0.0.1" || host === "localhost" || host === "::1") {
|
|
@@ -550,7 +556,8 @@ async function addTeam(spec) {
|
|
|
550
556
|
const patch = {
|
|
551
557
|
name: spec.name !== undefined ? String(spec.name || unnamedName()) : undefined,
|
|
552
558
|
base_url: baseUrl,
|
|
553
|
-
|
|
559
|
+
// skipTokenAutofill → force-clear any stored token (Docker :8009 reads live).
|
|
560
|
+
api_token: spec.skipTokenAutofill ? "" : (spec.api_token !== undefined ? String(spec.api_token || "") : undefined),
|
|
554
561
|
install_source: spec.install_source ?? undefined,
|
|
555
562
|
install_os: spec.install_os ?? undefined,
|
|
556
563
|
install_arch: spec.install_arch ?? undefined,
|
|
@@ -32,15 +32,22 @@ const APP_PORT = Number(process.env.CICY_DOCKER_APP_PORT || 8009);
|
|
|
32
32
|
const APP_CONTAINER = process.env.CICY_DOCKER_APP_CONTAINER || "cicy-code-docker";
|
|
33
33
|
const APP_VOLUME = process.env.CICY_DOCKER_APP_VOLUME || "cicy-team";
|
|
34
34
|
const APP_MOUNT = process.env.CICY_DOCKER_APP_MOUNT || "/home/cicy";
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
35
|
+
// 8008 and 8009 are ONE team (主人), so :8009 reaches the LLM through the cicy
|
|
36
|
+
// gateway using 8008's TEAM key — the `sk-cicy-…` apiKey already minted in 8008's
|
|
37
|
+
// global.json providers (NOT the api_token, which is only the local access
|
|
38
|
+
// credential). 8008 is up by the time the Docker card is used, so the key is
|
|
39
|
+
// ready — we just read it and pass it to the container. Same key ⇒ same billing.
|
|
39
40
|
const GATEWAY_ENDPOINT = process.env.CICY_AI_GATEWAY_LLM_ENDPOINT || "https://gateway.cicy-ai.com";
|
|
40
|
-
function
|
|
41
|
+
function readLocalGatewayKey() {
|
|
41
42
|
try {
|
|
42
43
|
const p = path.join(os.homedir(), "cicy-ai", "global.json");
|
|
43
|
-
|
|
44
|
+
const g = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
45
|
+
const items = (g.providers && g.providers.items) || [];
|
|
46
|
+
const pick =
|
|
47
|
+
items.find((it) => it && it.apiKey && String(it.url || "").includes("gateway.cicy-ai.com")) ||
|
|
48
|
+
items.find((it) => it && it.key === "defaultAnthropic" && it.apiKey) ||
|
|
49
|
+
items.find((it) => it && it.apiKey);
|
|
50
|
+
return pick ? String(pick.apiKey || "") : "";
|
|
44
51
|
} catch { return ""; }
|
|
45
52
|
}
|
|
46
53
|
|
|
@@ -123,20 +130,42 @@ function register({ sidecarLogPath } = {}) {
|
|
|
123
130
|
// LLM gateway env keyed by the 8008 team's token. (WSL: whole-home mount via
|
|
124
131
|
// -v <volume>:/home/cicy inside wsl-docker.)
|
|
125
132
|
const appOpts = () => {
|
|
126
|
-
const
|
|
133
|
+
const gwKey = readLocalGatewayKey(); // 8008's team gateway key (sk-cicy-…)
|
|
127
134
|
const env = { CICY_AI_GATEWAY_LLM_ENDPOINT: GATEWAY_ENDPOINT };
|
|
128
|
-
if (
|
|
135
|
+
if (gwKey) env.CICY_AI_GATEWAY_LLM_API_KEY = gwKey;
|
|
129
136
|
return { port: APP_PORT, container: APP_CONTAINER, volume: APP_VOLUME, env };
|
|
130
137
|
};
|
|
131
138
|
// Register the running :8009 instance as a (custom) team so the card's "打开"
|
|
132
139
|
// reuses the token-injected open/reload flow. addTeam dedups by host:port.
|
|
140
|
+
// Upsert the :8009 team with the CONTAINER's OWN live token. Critical: never
|
|
141
|
+
// fall back to the host 8008 token (addTeam auto-fills global.json on an empty
|
|
142
|
+
// api_token — that's the host credential, which 8009 rejects → login screen).
|
|
143
|
+
// Returns the team id, or {ok:false} when the container token can't be read.
|
|
144
|
+
// Register the :8009 team WITHOUT a token. 主人: teams.json 不存 8009 的 token;
|
|
145
|
+
// docker 的 token 是实时拿的. skipTokenAutofill stops addTeam from back-filling
|
|
146
|
+
// the HOST 8008 token (the bug that made 8009 verify with 8008's token → login).
|
|
133
147
|
const registerAppTeam = async () => {
|
|
148
|
+
const lt = require("./local-teams");
|
|
149
|
+
const r = await lt.addTeam({ base_url: `http://127.0.0.1:${APP_PORT}`, name: "Docker cicy-code", skipTokenAutofill: true });
|
|
150
|
+
return { ok: true, id: r && r.id };
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Card「打开」→ read the container's OWN token LIVE from its volume right now,
|
|
154
|
+
// then open the tab with THAT token. Never a stored/host token (主人: 打开前去
|
|
155
|
+
// docker 里实时拿 token 再 open tab). Refuse to open if it can't be read —
|
|
156
|
+
// opening tokenless / with the host token just strands the user at login.
|
|
157
|
+
ipcMain.handle("docker:app-open", async () => {
|
|
158
|
+
if (process.platform !== "win32") return { ok: false, error: "windows_only" };
|
|
134
159
|
try {
|
|
160
|
+
const tok = await wslDocker.readContainerToken(APP_PORT, APP_CONTAINER, APP_VOLUME);
|
|
161
|
+
if (!tok) return { ok: false, error: "no_token" };
|
|
162
|
+
const reg = await registerAppTeam();
|
|
163
|
+
if (!reg.id) return { ok: false, error: "register_failed" };
|
|
135
164
|
const lt = require("./local-teams");
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
} catch {
|
|
139
|
-
};
|
|
165
|
+
const r = await lt.openTeam(reg.id, { token: tok }); // open with the LIVE token
|
|
166
|
+
return r && r.ok ? { ok: true } : { ok: false, error: (r && r.error) || "open_failed" };
|
|
167
|
+
} catch (e) { return { ok: false, error: e.message }; }
|
|
168
|
+
});
|
|
140
169
|
|
|
141
170
|
// One-click bootstrap (方案 A): ensure WSL2 → Ubuntu → Docker Engine → load
|
|
142
171
|
// image → start :8009 container → health. Streams phase/progress on
|
|
@@ -155,13 +184,25 @@ function register({ sidecarLogPath } = {}) {
|
|
|
155
184
|
}
|
|
156
185
|
});
|
|
157
186
|
|
|
158
|
-
// ⋯ menu → 重启
|
|
187
|
+
// ⋯ menu → 重启 cicy-code (supervisorctl restart cicy-code; daemons stay up).
|
|
159
188
|
ipcMain.handle("docker:app-restart", async () => {
|
|
160
|
-
try { const ok = await wslDocker.restart({ container: APP_CONTAINER, port: APP_PORT }); return { ok: !!ok }; }
|
|
189
|
+
try { const ok = await wslDocker.restart({ container: APP_CONTAINER, port: APP_PORT, volume: APP_VOLUME }); return { ok: !!ok }; }
|
|
161
190
|
catch (e) { return { ok: false, error: e.message }; }
|
|
162
191
|
});
|
|
163
192
|
|
|
164
|
-
// ⋯ menu →
|
|
193
|
+
// ⋯ menu → 更新 cicy-code: pull the latest cicy-code into the container +
|
|
194
|
+
// restart it (no container recreate). Streams progress to the drawer.
|
|
195
|
+
ipcMain.handle("docker:app-update", async (e) => {
|
|
196
|
+
if (process.platform !== "win32") return { ok: false, error: "Docker cicy-code is Windows-only" };
|
|
197
|
+
try {
|
|
198
|
+
return await wslDocker.update({
|
|
199
|
+
container: APP_CONTAINER, port: APP_PORT,
|
|
200
|
+
onProgress: (ev) => { try { e.sender.send("docker:app-progress", ev); } catch {} },
|
|
201
|
+
});
|
|
202
|
+
} catch (err) { return { ok: false, error: err.message }; }
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// ⋯ menu → 停止 cicy-code.
|
|
165
206
|
ipcMain.handle("docker:app-stop", async () => {
|
|
166
207
|
try { await wslDocker.stop({ container: APP_CONTAINER }); return { ok: true }; }
|
|
167
208
|
catch (e) { return { ok: false, error: e.message }; }
|
|
@@ -262,17 +262,31 @@ async function runContainer({ port = 8009, container = "cicy-code-docker", volum
|
|
|
262
262
|
return { started: true };
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
// Read the container's
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
265
|
+
// Read the container's OWN api_token (its volume-persisted global.json). This is
|
|
266
|
+
// the ONLY correct credential for :8009 — the host's 8008 token is different and
|
|
267
|
+
// 8009 rejects it. Retries because right after start the entrypoint may not have
|
|
268
|
+
// written global.json yet; returns "" only if it truly can't be read (callers
|
|
269
|
+
// must then NOT open with a wrong/host token — that strands the user at login).
|
|
270
|
+
async function readContainerToken(port = 8009, container = "cicy-code-docker", volume = "cicy-team") {
|
|
271
|
+
for (let attempt = 1; attempt <= 5; attempt++) {
|
|
272
|
+
// 1) Fast + reliable: read the volume-backed global.json straight from the
|
|
273
|
+
// distro fs. `docker exec` into a just-loaded/busy container is slow and
|
|
274
|
+
// frequently times out — and a timeout here was returning "" → callers
|
|
275
|
+
// fell back to the stale host token. The bind volume read never does that.
|
|
276
|
+
try {
|
|
277
|
+
const { stdout } = await wslRun(`cat /var/lib/docker/volumes/${volume}/_data/cicy-ai/global.json 2>/dev/null`, { timeout: 8000 });
|
|
278
|
+
const m = String(stdout).match(/"api_token"\s*:\s*"(cicy_[A-Za-z0-9]+)"/);
|
|
279
|
+
if (m) return m[1];
|
|
280
|
+
} catch { /* not ready yet — retry */ }
|
|
281
|
+
// 2) Fallback: exec into the container.
|
|
282
|
+
try {
|
|
283
|
+
const { stdout } = await wslRun(`docker exec ${container} cat /home/cicy/cicy-ai/global.json`, { timeout: 10000 });
|
|
284
|
+
const tok = JSON.parse(stdout).api_token || "";
|
|
285
|
+
if (tok) return tok;
|
|
286
|
+
} catch { /* retry */ }
|
|
287
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
288
|
+
}
|
|
289
|
+
return "";
|
|
276
290
|
}
|
|
277
291
|
|
|
278
292
|
// Register a Windows logon task that starts dockerd in our distro on every
|
|
@@ -289,6 +303,27 @@ function ensureAutostart() {
|
|
|
289
303
|
});
|
|
290
304
|
}
|
|
291
305
|
|
|
306
|
+
// Drop a desktop shortcut (folder icon) to the container's /home/cicy — i.e. the
|
|
307
|
+
// cicy-team volume on the distro — so the user can browse :8009's files from
|
|
308
|
+
// Windows Explorer. \\wsl$\<distro>\… is the UNC view of the WSL filesystem.
|
|
309
|
+
// Idempotent: CreateShortcut overwrites. Best-effort (errors swallowed).
|
|
310
|
+
function ensureDesktopShortcut(volume = "cicy-team") {
|
|
311
|
+
if (process.platform !== "win32") return Promise.resolve();
|
|
312
|
+
return new Promise((res) => {
|
|
313
|
+
const lnk = path.join(os.homedir(), "Desktop", "cicy-8009 文件.lnk");
|
|
314
|
+
const target = `\\\\wsl$\\${DISTRO}\\var\\lib\\docker\\volumes\\${volume}\\_data`;
|
|
315
|
+
const ps =
|
|
316
|
+
`$w=New-Object -ComObject WScript.Shell;` +
|
|
317
|
+
`$s=$w.CreateShortcut(${JSON.stringify(lnk)});` +
|
|
318
|
+
`$s.TargetPath='explorer.exe';` +
|
|
319
|
+
`$s.Arguments=${JSON.stringify(target)};` +
|
|
320
|
+
`$s.IconLocation='shell32.dll,3';` + // 3 = standard folder icon
|
|
321
|
+
`$s.Description='cicy-code :8009 /home/cicy';` +
|
|
322
|
+
`$s.Save()`;
|
|
323
|
+
execFile("powershell", ["-NoProfile", "-Command", ps], { windowsHide: true, timeout: 15000 }, () => res());
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
292
327
|
// Composite status for the card.
|
|
293
328
|
async function status(port = 8009) {
|
|
294
329
|
const wsl = !docker.wslMissing();
|
|
@@ -373,16 +408,45 @@ async function _bootstrap({ onProgress, port = 8009, container = "cicy-code-dock
|
|
|
373
408
|
// 7) Health — the ONLY path to ok:true.
|
|
374
409
|
emit({ phase: "container", status: "running", message: "等待 cicy-code 就绪…" });
|
|
375
410
|
const healthy = await docker.waitUntil(() => probeHealth(port), { totalMs: 120000, everyMs: 3000 });
|
|
376
|
-
if (healthy) await ensureAutostart(); // survive
|
|
411
|
+
if (healthy) { await ensureAutostart(); await ensureDesktopShortcut(volume); } // survive reboot + desktop shortcut
|
|
377
412
|
emit({ phase: healthy ? "done" : "container", status: healthy ? "done" : "error", message: healthy ? "Docker cicy-code 已就绪 🎉" : `服务起来了但 :${port} 还没响应——稍等或点「重试」` });
|
|
378
413
|
return { ok: healthy, container };
|
|
379
414
|
}
|
|
380
415
|
|
|
381
416
|
// Lifecycle (card ⋯ menu).
|
|
382
|
-
|
|
417
|
+
// Restart ONLY cicy-code via supervisor — cron / sshd / user daemons keep
|
|
418
|
+
// running (that's the whole point of the supervisor layout). Falls back to a
|
|
419
|
+
// full container restart on the pre-supervisor image.
|
|
420
|
+
async function restart({ container = "cicy-code-docker", port = 8009, volume = "cicy-team" } = {}) {
|
|
383
421
|
await startEngine();
|
|
384
|
-
|
|
385
|
-
|
|
422
|
+
try {
|
|
423
|
+
await wslRun(`docker exec ${container} supervisorctl -c /etc/supervisor/supervisord.conf restart cicy-code`, { timeout: 30000 });
|
|
424
|
+
} catch {
|
|
425
|
+
try { await wslRun(`docker restart ${container}`, { timeout: 60000 }); } catch {}
|
|
426
|
+
}
|
|
427
|
+
const ok = await docker.waitUntil(() => probeHealth(port), { totalMs: 60000, everyMs: 2000 });
|
|
428
|
+
if (ok) await ensureDesktopShortcut(volume);
|
|
429
|
+
return ok;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Update cicy-code IN PLACE: the supervisor image ships cicy-code-update.sh,
|
|
433
|
+
// which installs the latest version side-by-side, repoints the symlink, and
|
|
434
|
+
// `supervisorctl restart cicy-code` — no container recreate, daemons untouched.
|
|
435
|
+
// Streamed to the drawer so the user sees the npm pull + restart.
|
|
436
|
+
async function update({ onProgress, container = "cicy-code-docker", port = 8009 } = {}) {
|
|
437
|
+
const emit = (ev) => { try { onProgress && onProgress(ev); } catch {} };
|
|
438
|
+
await startEngine();
|
|
439
|
+
emit({ phase: "image", status: "running", message: "更新 cicy-code(拉取最新版)…" });
|
|
440
|
+
try {
|
|
441
|
+
await wslRunStream(`docker exec ${container} bash -lc "command -v cicy-code-update.sh >/dev/null && cicy-code-update.sh || /usr/local/bin/cicy-code-update.sh"`,
|
|
442
|
+
{ emit, phase: "image", timeout: 300000 });
|
|
443
|
+
} catch (e) {
|
|
444
|
+
emit({ phase: "done", status: "error", message: `更新失败:${e.message}(此镜像可能不支持,试试「升级」重装)` });
|
|
445
|
+
return { ok: false, reason: "update_failed" };
|
|
446
|
+
}
|
|
447
|
+
const healthy = await docker.waitUntil(() => probeHealth(port), { totalMs: 120000, everyMs: 3000 });
|
|
448
|
+
emit({ phase: "done", status: healthy ? "done" : "error", message: healthy ? "cicy-code 已更新到最新 🎉" : "更新了但 :8009 还没响应——稍等或点重试" });
|
|
449
|
+
return { ok: healthy };
|
|
386
450
|
}
|
|
387
451
|
async function stop({ container = "cicy-code-docker" } = {}) {
|
|
388
452
|
try { await wslRun(`docker stop ${container}`, { timeout: 30000 }); } catch {}
|
|
@@ -411,6 +475,6 @@ async function upgrade({ onProgress, port = 8009, container = "cicy-code-docker"
|
|
|
411
475
|
}
|
|
412
476
|
|
|
413
477
|
module.exports = {
|
|
414
|
-
bootstrap, status, restart, stop, upgrade, runContainer, readContainerToken,
|
|
478
|
+
bootstrap, status, restart, stop, update, upgrade, runContainer, readContainerToken,
|
|
415
479
|
distroInstalled, dockerInstalled, dockerEngineUp, imagePresent, probeHealth, wslRun,
|
|
416
480
|
};
|
|
@@ -740,7 +740,15 @@ export default function App() {
|
|
|
740
740
|
{showLocal && (
|
|
741
741
|
<DockerCard
|
|
742
742
|
dockerTeam={dockerTeam}
|
|
743
|
-
onOpen={(
|
|
743
|
+
onOpen={async () => {
|
|
744
|
+
// Always open via the live-token path: re-reads the container's
|
|
745
|
+
// own api_token and refuses to open a tokenless/host-token page
|
|
746
|
+
// (主人: 必须拿到 token 才能打开,否则被卡在登录页).
|
|
747
|
+
try {
|
|
748
|
+
const r = await window.cicy?.docker?.appOpen?.();
|
|
749
|
+
if (!r?.ok) window.alert("拿不到容器 token,无法打开 :8009。请确认服务已就绪(或用卡片菜单「重启」)后再试。");
|
|
750
|
+
} catch (e) { console.warn("[DockerCard] open", e); }
|
|
751
|
+
}}
|
|
744
752
|
onRefresh={fetchLocalTeams}
|
|
745
753
|
/>
|
|
746
754
|
)}
|
|
@@ -1735,6 +1743,24 @@ function DockerCard({ dockerTeam, onOpen, onRefresh }) {
|
|
|
1735
1743
|
}
|
|
1736
1744
|
}, [checkStatus, onRefresh]);
|
|
1737
1745
|
|
|
1746
|
+
// Update cicy-code (in-place: pull latest + supervisorctl restart). Drawer so
|
|
1747
|
+
// the user sees the npm pull + restart log.
|
|
1748
|
+
const runUpdate = useCallback(async () => {
|
|
1749
|
+
setMenuOpen(false); setBusy("update");
|
|
1750
|
+
dockerDrawer.open({ onRetry: runUpdate });
|
|
1751
|
+
const unsub = window.cicy?.docker?.onAppProgress?.((ev) => dockerDrawer.push(ev));
|
|
1752
|
+
try {
|
|
1753
|
+
const r = await window.cicy?.docker?.appUpdate?.();
|
|
1754
|
+
dockerDrawer.finish({ ok: !!r?.ok, message: r?.ok ? tr("docker.updated", "cicy-code 已更新到最新") : (r?.error || tr("docker.updateFailed", "更新失败")) });
|
|
1755
|
+
if (r?.ok) onRefresh?.();
|
|
1756
|
+
} catch (e) {
|
|
1757
|
+
dockerDrawer.finish({ ok: false, message: e.message });
|
|
1758
|
+
} finally {
|
|
1759
|
+
try { unsub && unsub(); } catch {}
|
|
1760
|
+
setBusy(""); checkStatus();
|
|
1761
|
+
}
|
|
1762
|
+
}, [checkStatus, onRefresh]);
|
|
1763
|
+
|
|
1738
1764
|
// Restart / stop: quick lifecycle ops with a toast (no full drawer needed).
|
|
1739
1765
|
const runOp = useCallback(async (op, fn, okMsg) => {
|
|
1740
1766
|
setMenuOpen(false); setBusy(op);
|
|
@@ -1818,21 +1844,25 @@ function DockerCard({ dockerTeam, onOpen, onRefresh }) {
|
|
|
1818
1844
|
ref={menuRef}
|
|
1819
1845
|
style={{ position: "fixed", top: menuPos.top, left: menuPos.left, width: MENU_W }}
|
|
1820
1846
|
onClick={(e) => e.stopPropagation()}>
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1847
|
+
<button type="button" data-id="DockerCard-restart" className="bcard__menu-item"
|
|
1848
|
+
onClick={() => runOp("restart", () => window.cicy.docker.appRestart(), tr("docker.restarted", "已重启 cicy-code"))}>
|
|
1849
|
+
{tr("docker.restart", "重启")}
|
|
1850
|
+
</button>
|
|
1851
|
+
<button type="button" data-id="DockerCard-update" className="bcard__menu-item is-accent" onClick={runUpdate}>
|
|
1852
|
+
{tr("docker.update", "更新")}
|
|
1853
|
+
</button>
|
|
1854
|
+
<button type="button" data-id="DockerCard-reload" className="bcard__menu-item"
|
|
1855
|
+
onClick={() => { setMenuOpen(false); onOpen?.(dockerTeam?.id); }}>
|
|
1856
|
+
{tr("docker.reloadWindow", "刷新窗口")}
|
|
1857
|
+
</button>
|
|
1858
|
+
<button type="button" data-id="DockerCard-stop" className="bcard__menu-item is-danger"
|
|
1859
|
+
onClick={() => runOp("stop", () => window.cicy.docker.appStop(), tr("docker.stopped", "已停止 cicy-code"))}>
|
|
1860
|
+
{tr("docker.stop", "停止")}
|
|
1861
|
+
</button>
|
|
1862
|
+
<button type="button" data-id="DockerCard-billing" className="bcard__menu-item"
|
|
1863
|
+
onClick={() => { setMenuOpen(false); openCloudPage("?view=usage"); }}>
|
|
1864
|
+
{tr("docker.billing", "帐单")}
|
|
1829
1865
|
</button>
|
|
1830
|
-
{running && (
|
|
1831
|
-
<button type="button" data-id="DockerCard-stop" className="bcard__menu-item is-danger"
|
|
1832
|
-
onClick={() => runOp("stop", () => window.cicy.docker.appStop(), tr("docker.stopped", "已停止"))}>
|
|
1833
|
-
{tr("docker.stop", "停止")}
|
|
1834
|
-
</button>
|
|
1835
|
-
)}
|
|
1836
1866
|
</div>,
|
|
1837
1867
|
document.body
|
|
1838
1868
|
)}
|