cicy-desktop 2.1.367 → 2.1.369
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/.github/workflows/build-qemu-package.yml +89 -0
- package/package.json +8 -6
- package/src/backends/homepage-react/assets/index-BMeWzitz.js +490 -0
- package/src/backends/homepage-react/index.html +1 -1
- package/src/backends/hub-client.js +1 -1
- package/src/backends/local-teams.js +1 -1
- package/src/backends/sidecar-ipc.js +9 -3
- package/src/i18n/locales/en.json +2 -2
- package/src/i18n/locales/fr.json +2 -2
- package/src/i18n/locales/ja.json +2 -2
- package/src/i18n/locales/zh-CN.json +3 -3
- package/src/main.js +3 -0
- package/src/sidecar/app-docker.js +9 -0
- package/src/sidecar/docker.js +14 -4
- package/src/sidecar/linux-backend.js +55 -0
- package/src/sidecar/qemu-docker.js +521 -0
- package/src/sidecar/qemu-vm.cjs +397 -0
- package/test/linux-backend.test.js +64 -0
- package/workers/render/src/App.jsx +155 -48
- package/src/backends/homepage-react/assets/index-BWZkvmvO.js +0 -679
|
@@ -551,7 +551,7 @@ export default function App() {
|
|
|
551
551
|
// early return(termsOk / !token 等),把 hook 放到它们后面会让登录前后 hook 数量不一致,
|
|
552
552
|
// React 直接抛 #310 "Rendered more hooks than during the previous render",整页白屏。
|
|
553
553
|
// 2026-09-13 我就是这么把全车队首页打崩的。
|
|
554
|
-
|
|
554
|
+
// (2026-10-22 dsh 全车队下线:useDshFleet / DSH tab / DshCard 都已拿掉,别再加回来。)
|
|
555
555
|
// CiCy Hub: email sign-in → every cicy-code instance of that account (no local
|
|
556
556
|
// cicy-code needed). State + cards live in useHub / HubInstanceCard below.
|
|
557
557
|
const hub = useHub();
|
|
@@ -1176,9 +1176,6 @@ export default function App() {
|
|
|
1176
1176
|
const showCloud = false; // 首页只用 CiCy Hub 账号,云端团队不再展示
|
|
1177
1177
|
const showHub = tab === "all" || tab === "hub";
|
|
1178
1178
|
const hubCount = hub.instances ? hub.instances.length : 0;
|
|
1179
|
-
// 「全部」只放真能打开的 dsh;DSH tab 里才把未就绪的一并列出来看状态。
|
|
1180
|
-
const dshReady = (dshFleet.list || []).filter((m) => m.ready);
|
|
1181
|
-
const showDshAll = tab === "all" || tab === "dsh";
|
|
1182
1179
|
// 首次打开:本地或云端团队任一还没拉到(为 null)→ grid 显示 skeleton 占位卡,直到两边都
|
|
1183
1180
|
// resolve(出错也 resolve 成 []),再显示真实内容 —— 避免一个先回来另一个还空的露馅。
|
|
1184
1181
|
const firstLoading = localTeams === null || teams === null;
|
|
@@ -1196,11 +1193,10 @@ export default function App() {
|
|
|
1196
1193
|
<div className="app__tabsrow">
|
|
1197
1194
|
<div className="app__tabs">
|
|
1198
1195
|
{[
|
|
1199
|
-
{ k: "all", label: tr("teamFilter.all", "全部"), n: localCount + customCount + hubCount
|
|
1196
|
+
{ k: "all", label: tr("teamFilter.all", "全部"), n: localCount + customCount + hubCount },
|
|
1200
1197
|
{ k: "local", label: tr("teamFilter.local", "本地"), n: localCount },
|
|
1201
1198
|
{ k: "hub", label: tr("teamFilter.hub", "CiCy Hub"), n: hubCount },
|
|
1202
1199
|
{ k: "custom", label: tr("teamFilter.custom", "自定义"), n: customCount },
|
|
1203
|
-
{ k: "dsh", label: tr("teamFilter.dsh", "DSH"), n: dshReady.length },
|
|
1204
1200
|
].map(({ k, label, n }) => (
|
|
1205
1201
|
<button
|
|
1206
1202
|
key={k}
|
|
@@ -1213,15 +1209,6 @@ export default function App() {
|
|
|
1213
1209
|
</button>
|
|
1214
1210
|
))}
|
|
1215
1211
|
</div>
|
|
1216
|
-
{/* DSH tab 的手动刷新:token 会随 dsh 重启而变,但不值得轮询 —— 要新的自己点。 */}
|
|
1217
|
-
{tab === "dsh" && (
|
|
1218
|
-
<button type="button" className="btn-ghost" data-id="DshFleetRefresh" disabled={dshFleet.busy}
|
|
1219
|
-
title={tr("dshFleet.refresh", "刷新 dsh 中继状态")}
|
|
1220
|
-
style={{ marginLeft: 8, width: 28, height: 28, padding: 0, display: "inline-flex", alignItems: "center", justifyContent: "center" }}
|
|
1221
|
-
onClick={() => dshFleet.refresh()}>
|
|
1222
|
-
{dshFleet.busy ? <Spinner /> : <RefreshIcon />}
|
|
1223
|
-
</button>
|
|
1224
|
-
)}
|
|
1225
1212
|
{/* DesktopIdBadge 已移入用户头像下拉菜单(受信任站点上面)。此处仅留占位撑开右侧布局。 */}
|
|
1226
1213
|
<span style={{ marginLeft: "auto" }} aria-hidden />
|
|
1227
1214
|
{/* 行尾:新加团队 → 直接去云端团队中心添加(私有云)。自定义入口已删。 */}
|
|
@@ -1304,12 +1291,6 @@ export default function App() {
|
|
|
1304
1291
|
{!firstLoading && showHub && hub.loggedIn && (hub.instances || []).filter((it) => it.reachable || it.online).map((it) => (
|
|
1305
1292
|
<HubInstanceCard key={"hub:" + it.id} inst={it} onOpen={(next, title) => hub.open(it, next, title)} />
|
|
1306
1293
|
))}
|
|
1307
|
-
{showDshAll && (tab === "dsh" ? (dshFleet.list || []) : dshReady).map((m) => <DshFleetCard key={"dsh:" + m.name} m={m} />)}
|
|
1308
|
-
{tab === "dsh" && dshFleet.list !== null && dshFleet.list.length === 0 && !dshFleet.busy && (
|
|
1309
|
-
<div className="empty" data-id="DshFleetEmpty" style={{ gridColumn: "1 / -1" }}>
|
|
1310
|
-
{tr("dshFleet.empty", "还没有机器的 dsh 中继连上来。")}
|
|
1311
|
-
</div>
|
|
1312
|
-
)}
|
|
1313
1294
|
{!firstLoading && showLocal && localList.map((t) => (
|
|
1314
1295
|
<LocalTeamCard key={"local:" + t.id} team={t} cloudCode={cloudCodeFor(t.cloud_team_id)} onOpen={() => openLocalTeam(t.id)} onRename={renameLocalTeam} onRefresh={fetchLocalTeams} />
|
|
1315
1296
|
))}
|
|
@@ -1334,7 +1315,6 @@ export default function App() {
|
|
|
1334
1315
|
/>
|
|
1335
1316
|
)}
|
|
1336
1317
|
{/* 首页第二块:DeepSeek Harness(开源 Agent 框架),homepage 自装自启,和 cicy-code 卡并列 */}
|
|
1337
|
-
{!firstLoading && showLocal && <DshCard />}
|
|
1338
1318
|
{!firstLoading && showCustom && customList.map((t) => (
|
|
1339
1319
|
<LocalTeamCard key={"custom:" + t.id} team={t} cloudCode={cloudCodeFor(t.cloud_team_id)} onOpen={() => openLocalTeam(t.id)} onRename={renameLocalTeam} onRefresh={fetchLocalTeams} />
|
|
1340
1320
|
))}
|
|
@@ -1511,7 +1491,11 @@ function useFleetSocket() {
|
|
|
1511
1491
|
try { out.auto = await capped(window.cicy?.app?.getAutoUpdate?.(), null); } catch {}
|
|
1512
1492
|
// Only cache a complete identity; a partial one (some step timed out) is
|
|
1513
1493
|
// re-read on the next connect so the hello self-corrects once main is idle.
|
|
1514
|
-
|
|
1494
|
+
// A token-less identity is never cached either: a fresh install hellos before the
|
|
1495
|
+
// user logs in to the hub, and caching that would keep every later hello token-less
|
|
1496
|
+
// → the hub never marks the machine authed → no dsh config / self-rpc for it
|
|
1497
|
+
// (seen on a new machine 2026-09-25). Re-read until the token shows up.
|
|
1498
|
+
if (out.host && out.v && out.tok) ident = out;
|
|
1515
1499
|
return out;
|
|
1516
1500
|
};
|
|
1517
1501
|
|
|
@@ -3143,8 +3127,8 @@ function DockerInstallDrawerHost() {
|
|
|
3143
3127
|
const isOpen = st.kind === "open"; // 打开失败报告:无安装 stepper、标题/失败标签用「打开」
|
|
3144
3128
|
const phaseIdx = DOCKER_PHASES.findIndex(([k]) => k === st.phase);
|
|
3145
3129
|
const dlBars = isOpen ? [] : ["install-docker", "image"].filter((k) => st.bars?.[k]);
|
|
3146
|
-
const drawerTitle = st.title || (isOpen ?
|
|
3147
|
-
const drawerSub = st.sub ||
|
|
3130
|
+
const drawerTitle = st.title || (isOpen ? `打开${LOCAL_TEAM_NAME}` : `安装${LOCAL_TEAM_NAME}`);
|
|
3131
|
+
const drawerSub = st.sub || `127.0.0.1:${dockerAppPort}`;
|
|
3148
3132
|
// Minimized → a floating restore chip (op keeps running in the background).
|
|
3149
3133
|
if (st.minimized) {
|
|
3150
3134
|
const pcts = dlBars.map((k) => st.bars[k]?.progress).filter(Number.isFinite);
|
|
@@ -3263,7 +3247,13 @@ function useDshFleet() {
|
|
|
3263
3247
|
const refresh = useCallback(async () => {
|
|
3264
3248
|
setBusy(true);
|
|
3265
3249
|
try {
|
|
3266
|
-
|
|
3250
|
+
// 车队清单是登录用户名下的资产:必须带本机 hubAuth token,hub 侧按 token 校验。
|
|
3251
|
+
// 没登录(全新安装)就没有 token → 什么都不列,别把车队里别的机器摆到陌生人首页上。
|
|
3252
|
+
let tok = "";
|
|
3253
|
+
try { tok = String(await mainEval(READ_TOKEN)).trim(); } catch {}
|
|
3254
|
+
if (!tok) { setList([]); return; }
|
|
3255
|
+
const r = await fetch(DSH_FLEET_API, { cache: "no-store", headers: { Authorization: "Bearer " + tok } });
|
|
3256
|
+
if (!r.ok) { setList([]); return; }
|
|
3267
3257
|
const j = await r.json();
|
|
3268
3258
|
setList(Array.isArray(j && j.machines) ? j.machines : []);
|
|
3269
3259
|
} catch { setList([]); }
|
|
@@ -3446,6 +3436,42 @@ const dbDir = path.join(home, "cicy-ai", "db"), logDir = path.join(home, "cicy-a
|
|
|
3446
3436
|
for (const d of [dbDir, logDir]) { try { fs.mkdirSync(d, { recursive: true }); } catch {} }
|
|
3447
3437
|
const logFile = path.join(logDir, "dsh.log"), errFile = path.join(logDir, "dsh.err.log"), pidFile = path.join(dbDir, "dsh.pid"), rootCache = path.join(dbDir, "dsh-npm-root.txt");
|
|
3448
3438
|
const lockFile = path.join(dbDir, "dsh-install.lock"), okMarker = path.join(dbDir, "dsh-ok-" + VER + ".txt");
|
|
3439
|
+
// dsh@0.1.2-rc.1 是 developer preview:安装器只钉 dsh 版本,传递依赖(cordis 栈)浮动一升级就崩
|
|
3440
|
+
// ("user patch-layer watching requires the Cordis HMR service")。运行时 cordis 栈从 dsh 包自己的
|
|
3441
|
+
// nested node_modules 解析(~/.dsh/profiles 自举目录是空的),所以装完把这 8 个钉回已知良好版本。
|
|
3442
|
+
const KNOWN_GOOD = { "@deepseek-ai/cordis": "4.0.2", "@deepseek-ai/cordis-plugin-hmr": "1.0.17", "@deepseek-ai/cordis-plugin-loader": "1.0.3", "@deepseek-ai/cordis-plugin-include": "1.0.7", "@deepseek-ai/cordis-plugin-timer": "1.1.4", "@deepseek-ai/cordis-plugin-group": "1.0.2", "@deepseek-ai/cosmokit": "1.8.3", "@deepseek-ai/schemastery": "3.18.2" };
|
|
3443
|
+
const KG_HASH = require("crypto").createHash("sha1").update(JSON.stringify(Object.keys(KNOWN_GOOD).sort().map((k) => [k, KNOWN_GOOD[k]]))).digest("hex").slice(0, 8);
|
|
3444
|
+
const pinMarker = path.join(dbDir, "dsh-pins-" + VER + "-" + KG_HASH + ".txt");
|
|
3445
|
+
// 读版本要去 BOM:JSON.parse 遇 BOM 会抛
|
|
3446
|
+
function nestedVer(dir, name) { try { return JSON.parse(fs.readFileSync(path.join(dir, "node_modules", ...name.split("/"), "package.json"), "utf8").replace(/^\uFEFF/, "")).version; } catch { return null; } }
|
|
3447
|
+
function depsOk(dir) { for (const n of Object.keys(KNOWN_GOOD)) if (nestedVer(dir, n) !== KNOWN_GOOD[n]) return false; return true; }
|
|
3448
|
+
// 钉法:不能在 dsh 包目录直接 npm i —— npm 会解析整棵 dsh 依赖树,里面的
|
|
3449
|
+
// @deepseek-ai/dsh-experimental-agent-team@^0.1.2-rc.1 在 registry 上不存在,直接 ETARGET 退出。
|
|
3450
|
+
// 改在临时 scratch 目录 npm i 这 8 个钉死版本,再把 8 个包目录整目录覆盖进 dsh/node_modules/@deepseek-ai
|
|
3451
|
+
// (旧目录改名 .bak,失败则还原)。必须 8 个一起(只钉 7 个、hmr 留新版会 hmr.registerConfig is not a function)。
|
|
3452
|
+
function pinDeps(dir, reg) {
|
|
3453
|
+
if (fs.existsSync(pinMarker) && depsOk(dir)) return { ok: true, skipped: true };
|
|
3454
|
+
if (depsOk(dir)) { try { fs.writeFileSync(pinMarker, String(Date.now())); } catch {} return { ok: true, skipped: true }; }
|
|
3455
|
+
const scratch = path.join(os.tmpdir(), "dsh-pin-scratch");
|
|
3456
|
+
try { fs.rmSync(scratch, { recursive: true, force: true }); fs.mkdirSync(scratch, { recursive: true }); fs.writeFileSync(path.join(scratch, "package.json"), JSON.stringify({ name: "dsh-pin-scratch", version: "0.0.0", private: true })); } catch (e) { return { ok: false, error: "pin_scratch_failed", tail: String(e.message).slice(-200) }; }
|
|
3457
|
+
const pins = Object.keys(KNOWN_GOOD).map((n) => n + "@" + KNOWN_GOOD[n]).join(" ");
|
|
3458
|
+
try { run(q(NPM) + " i --no-audit --no-fund --no-progress --loglevel=error --registry=" + reg + " " + pins, { cwd: scratch, timeout: 8 * 60 * 1000 }); }
|
|
3459
|
+
catch (e) { try { fs.rmSync(scratch, { recursive: true, force: true }); } catch {} return { ok: false, error: "pin_install_failed", tail: String((e.stderr || "") + (e.stdout || "") + (e.message || "")).trim().slice(-300) }; }
|
|
3460
|
+
const dstScope = path.join(dir, "node_modules", "@deepseek-ai"), srcScope = path.join(scratch, "node_modules", "@deepseek-ai");
|
|
3461
|
+
const failed = [];
|
|
3462
|
+
for (const n of Object.keys(KNOWN_GOOD)) {
|
|
3463
|
+
const short = n.slice("@deepseek-ai/".length), src = path.join(srcScope, short), dst = path.join(dstScope, short);
|
|
3464
|
+
if (!fs.existsSync(src)) { failed.push(short + ":no-src"); continue; }
|
|
3465
|
+
try { if (fs.existsSync(dst)) { try { fs.rmSync(dst + ".bak", { recursive: true, force: true }); } catch {} fs.renameSync(dst, dst + ".bak"); } fs.cpSync(src, dst, { recursive: true }); }
|
|
3466
|
+
catch (e) { failed.push(short + ":" + String(e.message).slice(0, 40)); try { if (!fs.existsSync(dst) && fs.existsSync(dst + ".bak")) fs.renameSync(dst + ".bak", dst); } catch {} }
|
|
3467
|
+
}
|
|
3468
|
+
try { fs.rmSync(scratch, { recursive: true, force: true }); } catch {}
|
|
3469
|
+
if (failed.length) return { ok: false, error: "pin_copy_failed", tail: failed.join("; ") };
|
|
3470
|
+
if (!depsOk(dir)) return { ok: false, error: "pin_mismatch", tail: JSON.stringify(Object.fromEntries(Object.keys(KNOWN_GOOD).map((n) => [n, nestedVer(dir, n)]))).slice(0, 300) };
|
|
3471
|
+
for (const n of Object.keys(KNOWN_GOOD)) { try { fs.rmSync(path.join(dstScope, n.slice("@deepseek-ai/".length) + ".bak"), { recursive: true, force: true }); } catch {} }
|
|
3472
|
+
try { fs.writeFileSync(pinMarker, String(Date.now())); } catch {}
|
|
3473
|
+
return { ok: true };
|
|
3474
|
+
}
|
|
3449
3475
|
// 车队级启动环境(hub 下发,DshCard 写的 dsh-env.json):并进 dsh 子进程 env。启动环境在 dsh 里优先级最高且只读。
|
|
3450
3476
|
const envFile = path.join(dbDir, "dsh-env.json"), envApplied = path.join(dbDir, "dsh-env.applied");
|
|
3451
3477
|
function fleetEnv() { try { const j = JSON.parse(fs.readFileSync(envFile, "utf8")); const src = (j && j.env) || {}; const out = {}; for (const [k, v] of Object.entries(src)) { if (/^[A-Z][A-Z0-9_]*$/.test(k) && typeof v === "string" && v) out[k] = v; } return out; } catch { return {}; } }
|
|
@@ -3536,7 +3562,9 @@ function installing() { return pidAlive(readPidFile(lockFile)); }
|
|
|
3536
3562
|
async function status() {
|
|
3537
3563
|
const p = pkgInfo(); const pid = readPidFile(pidFile); const pr = await probe();
|
|
3538
3564
|
const eh = envHash(), ea = appliedHash(), wm = wsMissing();
|
|
3539
|
-
|
|
3565
|
+
// 装好但传递依赖没钉/浮动了 → 报出来让卡片显示,别无声"启动中"(2026-09-25:rc.1 cordis 栈浮动崩)
|
|
3566
|
+
const depBad = (p && !p.broken && !fs.existsSync(pinMarker) && !depsOk(p.dir)) ? "cordis-stack-mismatch" : null;
|
|
3567
|
+
return { ok: true, installed: !!p && !p.broken, broken: !!(p && p.broken), depError: depBad, version: p ? p.version : null, installing: installing(), running: pr.dsh, occupied: pr.up && !pr.dsh, pid, pidAlive: pidAlive(pid), hasToken: !!lastToken(), node: process.version, logFile, envKeys: Object.keys(fleetEnv()), envHash: eh, envStale: pr.dsh && ea !== eh, wsMissing: wm, wsStale: pr.dsh && wm.length > 0 };
|
|
3540
3568
|
}
|
|
3541
3569
|
async function install() {
|
|
3542
3570
|
// 安装锁:首页刷新/多开会再次触发安装,两个 npm -g 同时写全局目录会把包写坏。有锁就等它完。
|
|
@@ -3563,7 +3591,9 @@ async function install() {
|
|
|
3563
3591
|
try { fs.writeFileSync(rootCache, root); } catch {}
|
|
3564
3592
|
const p = pkgAt(root);
|
|
3565
3593
|
if (!p || p.broken) return { ok: false, error: "install_incomplete", tail: (outp || "").slice(-300) };
|
|
3566
|
-
|
|
3594
|
+
const pin = pinDeps(p.dir, savedRegistry() || used || "https://registry.npmjs.org");
|
|
3595
|
+
if (!pin.ok) return { ok: false, error: pin.error, tail: pin.tail };
|
|
3596
|
+
return { ok: true, version: p.version, registry: used, speed: regs.speed, secs: Math.round((Date.now() - t0) / 1000), pinned: !pin.skipped };
|
|
3567
3597
|
} finally { try { fs.unlinkSync(lockFile); } catch {} }
|
|
3568
3598
|
}
|
|
3569
3599
|
async function start() {
|
|
@@ -3585,11 +3615,17 @@ async function start() {
|
|
|
3585
3615
|
const cmd = "$p = Start-Process -FilePath " + q(process.execPath)
|
|
3586
3616
|
+ " -ArgumentList @(" + [p.bin, "web", "--no-open", "--port", String(PORT)].map(q).join(",") + ")"
|
|
3587
3617
|
+ " -WindowStyle Hidden -RedirectStandardOutput " + q(logFile) + " -RedirectStandardError " + q(errFile)
|
|
3588
|
-
+ " -WorkingDirectory " + q(home) + " -PassThru
|
|
3618
|
+
+ " -WorkingDirectory " + q(home) + " -PassThru";
|
|
3619
|
+
// PID / 报错走文件,powershell 的 stdio 全给 ignore:Start-Process 带重定向时用继承句柄的
|
|
3620
|
+
// CreateProcess,dsh 会顺手继承 powershell 的 stdout —— 若那是管道,spawnSync 要等 dsh
|
|
3621
|
+
// 退出才 EOF,于是每次「启动」都卡满 60s 超时才返回(实测假进程:管道 25.3s vs 文件 0.25s)。
|
|
3622
|
+
const spawnPid = path.join(dbDir, "dsh-spawn.pid"), spawnErr = path.join(dbDir, "dsh-spawn.err");
|
|
3623
|
+
for (const f of [spawnPid, spawnErr]) { try { fs.unlinkSync(f); } catch {} }
|
|
3624
|
+
const ps = "try { " + cmd + "; Set-Content -Path " + q(spawnPid) + " -Value $p.Id } catch { Set-Content -Path " + q(spawnErr) + " -Value ($_ | Out-String) }";
|
|
3589
3625
|
try { fs.writeFileSync(logFile, ""); fs.writeFileSync(errFile, ""); } catch {}
|
|
3590
|
-
|
|
3591
|
-
pid =
|
|
3592
|
-
if (!pid) return { ok: false, error: "start_failed", tail:
|
|
3626
|
+
cp.spawnSync("powershell", ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", ps], { stdio: "ignore", timeout: 60000, windowsHide: true, env });
|
|
3627
|
+
pid = readPidFile(spawnPid);
|
|
3628
|
+
if (!pid) { let err = ""; try { err = fs.readFileSync(spawnErr, "utf8"); } catch {} return { ok: false, error: "start_failed", tail: err.slice(-800) }; }
|
|
3593
3629
|
} else {
|
|
3594
3630
|
const fd = fs.openSync(logFile, "w");
|
|
3595
3631
|
const child = cp.spawn(process.execPath, [p.bin, "web", "--no-open", "--port", String(PORT)], { detached: true, windowsHide: true, stdio: ["ignore", fd, fd], cwd: home, env });
|
|
@@ -3907,6 +3943,7 @@ function DshCard() {
|
|
|
3907
3943
|
const stateText = !st ? ""
|
|
3908
3944
|
: st.error ? tr("dsh.stateNoNode", "未就绪:{{why}}", { why: st.error === "no_node" ? tr("dsh.noNodeShort", "没有 Node.js(点「安装」自动装便携版)") : st.error })
|
|
3909
3945
|
: st.installing ? tr("dsh.stateInstalling", "另一个安装进程进行中…")
|
|
3946
|
+
: st.depError ? tr("dsh.stateDepBad", "依赖损坏(cordis 栈),点「安装」修复")
|
|
3910
3947
|
: running ? tr("dsh.stateRunning", "运行中")
|
|
3911
3948
|
: st.broken ? tr("dsh.stateBroken", "安装不完整(点「安装」修复)")
|
|
3912
3949
|
: installed ? tr("dsh.stateStopped", "已安装,未运行")
|
|
@@ -3979,6 +4016,21 @@ function DshCard() {
|
|
|
3979
4016
|
// in Docker on :8008, alongside the native local daemon (:8008). If Docker
|
|
3980
4017
|
// Desktop is missing, the install flow downloads its installer to the user's
|
|
3981
4018
|
// Desktop and runs it, streaming progress through the drawer above.
|
|
4019
|
+
// 本机 cicy-code(WSL 或 QEMU 里的 Docker 容器)的团队名:2026-10-21 起统一叫「本机团队」。
|
|
4020
|
+
const LOCAL_TEAM_NAME = "本机团队";
|
|
4021
|
+
const isDefaultLocalTeamTitle = (t) => !t || /^\s*docker\s*(团队|team)?\s*(:\s*\d+)?\s*$/i.test(String(t));
|
|
4022
|
+
|
|
4023
|
+
// QEMU 机器(本机团队在 :8009,跑在 Debian 虚拟机里)的虚拟机操作:跑 desktop(2.1.368+)写好的
|
|
4024
|
+
// ~/cicy-ai/db/qemu-vm-ctl.js(源码 src/sidecar/qemu-vm.cjs),node 用 dsh 同款找法。只打一行 JSON。
|
|
4025
|
+
async function vmCtl(op) {
|
|
4026
|
+
const env = await dshEnv();
|
|
4027
|
+
if (!env.node || !env.home) return { ok: false, error: "no_node" };
|
|
4028
|
+
const r = await dshShell(`"${env.node}" "${env.home}\\cicy-ai\\db\\qemu-vm-ctl.js" ${op}`);
|
|
4029
|
+
const m = String(r.stdout || "").trim().match(/\{[\s\S]*\}$/);
|
|
4030
|
+
if (m) { try { return JSON.parse(m[0]); } catch {} }
|
|
4031
|
+
return { ok: false, error: "ctl_no_output", detail: (r.stderr || r.stdout || "").trim().slice(-300) };
|
|
4032
|
+
}
|
|
4033
|
+
|
|
3982
4034
|
function DockerCard({ dockerTeam, cloudTitle, cloudCode, onOpen, onRename, onRefresh }) {
|
|
3983
4035
|
const [status, setStatus] = useState(null);
|
|
3984
4036
|
const [busy, setBusy] = useState(""); // "" | bootstrap | restart | stop | upgrade | probe
|
|
@@ -3993,8 +4045,21 @@ function DockerCard({ dockerTeam, cloudTitle, cloudCode, onOpen, onRename, onRef
|
|
|
3993
4045
|
const [editing, setEditing] = useState(false);
|
|
3994
4046
|
const [draft, setDraft] = useState("");
|
|
3995
4047
|
// 标题 = 云端 team 的 title(和其它团队同一套;refreshCloudTeams 周期刷新 → 自动跟随
|
|
3996
|
-
|
|
3997
|
-
|
|
4048
|
+
// 云端改名)。没建好云端 team、或标题还是系统默认名(老的「Docker 团队」/ 云端缺省「Docker :<port>」)
|
|
4049
|
+
// 时显示「本机团队」;用户自己改过的名字原样显示。
|
|
4050
|
+
const displayName = isDefaultLocalTeamTitle(cloudTitle) ? LOCAL_TEAM_NAME : cloudTitle;
|
|
4051
|
+
// 本地团队节点(teams.json)里的名字还是默认名 → 一次性改成「本机团队」。tab 标题取的是这个名字;
|
|
4052
|
+
// localTeams.update 会同时刷新已打开的 tab 标题并把名字同步到云端。addTeam 再次注册时保留已有
|
|
4053
|
+
// 名字,所以改一次就稳定。用户自己起的名字不碰。
|
|
4054
|
+
const renamedRef = useRef(false);
|
|
4055
|
+
useEffect(() => {
|
|
4056
|
+
if (renamedRef.current || !dockerTeam?.id || !isDefaultLocalTeamTitle(dockerTeam.name)) return;
|
|
4057
|
+
if (!isDefaultLocalTeamTitle(cloudTitle)) return; // 云端已有用户起的名字 → 以它为准,不覆盖
|
|
4058
|
+
renamedRef.current = true;
|
|
4059
|
+
Promise.resolve(window.cicy?.localTeams?.update?.(dockerTeam.id, { name: LOCAL_TEAM_NAME }))
|
|
4060
|
+
.then((r) => { if (r?.ok) onRefresh?.(); })
|
|
4061
|
+
.catch((e) => console.warn("[DockerCard] rename to 本机团队", e));
|
|
4062
|
+
}, [dockerTeam?.id, dockerTeam?.name, cloudTitle, onRefresh]);
|
|
3998
4063
|
const startEdit = (e) => { e?.stopPropagation?.(); setDraft(displayName); setEditing(true); };
|
|
3999
4064
|
const commitName = async () => {
|
|
4000
4065
|
setEditing(false);
|
|
@@ -4011,6 +4076,7 @@ function DockerCard({ dockerTeam, cloudTitle, cloudCode, onOpen, onRename, onRef
|
|
|
4011
4076
|
const checkStatus = useCallback(async () => {
|
|
4012
4077
|
try {
|
|
4013
4078
|
const s = await window.cicy?.docker?.appStatus?.();
|
|
4079
|
+
if (s && Number(s.port)) dockerAppPort = Number(s.port);
|
|
4014
4080
|
setStatus(s);
|
|
4015
4081
|
// 自愈卡死的设置抽屉:容器已健康(running)说明 setup 实际已完成,但抽屉可能还停在
|
|
4016
4082
|
// 「进行中」—— 网络失败重试后,follower 跟随的 bootstrap promise 迟迟不 resolve、又收
|
|
@@ -4052,7 +4118,7 @@ function DockerCard({ dockerTeam, cloudTitle, cloudCode, onOpen, onRename, onRef
|
|
|
4052
4118
|
for (const raw of portList) {
|
|
4053
4119
|
const s = String(raw).trim(); if (!s) continue;
|
|
4054
4120
|
const p = Number(s);
|
|
4055
|
-
if (!Number.isInteger(p) || p < 1 || p > 65535 || p === 8008 || seen.has(p)) { setPortsErr(tr("docker.ports.invalid", "有端口无效(1-65535,不能是
|
|
4121
|
+
if (!Number.isInteger(p) || p < 1 || p > 65535 || p === 8008 || p === dockerAppPort || seen.has(p)) { setPortsErr(tr("docker.ports.invalid", "有端口无效(1-65535,不能是 {{p}},不能重复)", { p: dockerAppPort })); return; }
|
|
4056
4122
|
seen.add(p); out.push(p);
|
|
4057
4123
|
}
|
|
4058
4124
|
setPortsErr(""); setPortsBusy(true); setPortsOpen(false);
|
|
@@ -4176,7 +4242,7 @@ function DockerCard({ dockerTeam, cloudTitle, cloudCode, onOpen, onRename, onRef
|
|
|
4176
4242
|
// update() 返回时 :8008 已健康(waitUntil probeHealth)。立刻:① reload 已打开的
|
|
4177
4243
|
// docker tab(reloadIgnoringCache,拿新版 SPA)② 强制重探,卡片版本马上更新(不等
|
|
4178
4244
|
// 60s reconcile / 缓存)。
|
|
4179
|
-
try { window.cicy?.tabs?.reloadIfOpen?.(
|
|
4245
|
+
try { window.cicy?.tabs?.reloadIfOpen?.(dockerAppUrl(), LOCAL_TEAM_NAME); } catch {}
|
|
4180
4246
|
try { const s = await window.cicy?.docker?.appRedetect?.(); if (s) setStatus(s); } catch {}
|
|
4181
4247
|
}
|
|
4182
4248
|
} catch (e) {
|
|
@@ -4188,6 +4254,27 @@ function DockerCard({ dockerTeam, cloudTitle, cloudCode, onOpen, onRename, onRef
|
|
|
4188
4254
|
}, [checkStatus, onRefresh]);
|
|
4189
4255
|
|
|
4190
4256
|
// Restart / stop: quick lifecycle ops with a toast (no full drawer needed).
|
|
4257
|
+
// QEMU 机器:菜单打开时顺手查一下虚拟机(加速方式),决定要不要显示「开启硬件加速」。
|
|
4258
|
+
const isQemu = Number(status?.port) === 8009;
|
|
4259
|
+
const [vmSt, setVmSt] = useState(null);
|
|
4260
|
+
useEffect(() => { if (menuOpen && isQemu) vmCtl("status").then(setVmSt).catch(() => {}); }, [menuOpen, isQemu]);
|
|
4261
|
+
const openVmTerminal = async () => {
|
|
4262
|
+
setMenuOpen(false);
|
|
4263
|
+
const r = await vmCtl("open");
|
|
4264
|
+
if (!r.ok) { toast.show({ id: "docker-op", status: "error", ttl: 6000, message: tr("docker.vmTermNotReady", "虚拟机终端还没就绪,稍等再试") }); return; }
|
|
4265
|
+
await window.cicy?.tabs?.open?.(r.url, tr("docker.vmTermTab", "Debian 终端"), "", true);
|
|
4266
|
+
};
|
|
4267
|
+
const enableAccel = async () => {
|
|
4268
|
+
setMenuOpen(false);
|
|
4269
|
+
if (!window.confirm(tr("docker.accelConfirm", "开启「Windows 虚拟机监控程序平台」(WHPX)需要管理员权限,开启后要重启电脑才生效。继续?"))) return;
|
|
4270
|
+
setBusy("accel");
|
|
4271
|
+
try {
|
|
4272
|
+
const r = await vmCtl("accel");
|
|
4273
|
+
toast.show({ id: "docker-op", status: r.ok ? "done" : "error", ttl: r.ok ? 10000 : 6000,
|
|
4274
|
+
message: r.ok ? tr("docker.accelDone", "已开启,重启电脑后虚拟机自动用硬件加速") : (r.error || tr("docker.opFailed", "操作失败")) });
|
|
4275
|
+
} finally { setBusy(""); }
|
|
4276
|
+
};
|
|
4277
|
+
|
|
4191
4278
|
const runOp = useCallback(async (op, fn, okMsg) => {
|
|
4192
4279
|
setMenuOpen(false); setBusy(op);
|
|
4193
4280
|
toast.show({ id: "docker-op", message: tr(`docker.${op}ing`, op === "restart" ? "重启中…" : op === "reload" ? "刷新中…" : "停止中…"), status: "running" });
|
|
@@ -4286,7 +4373,7 @@ function DockerCard({ dockerTeam, cloudTitle, cloudCode, onOpen, onRename, onRef
|
|
|
4286
4373
|
// 打开很慢 → 先探这个 :8008 tab 开过没。开过(openedWc 里有它的
|
|
4287
4374
|
// webContentsId)就**直接 active 秒切**,不再拿 token / 注册 team(那是慢的根)。
|
|
4288
4375
|
try {
|
|
4289
|
-
const r = await window.cicy?.tabs?.activateIfOpen?.(
|
|
4376
|
+
const r = await window.cicy?.tabs?.activateIfOpen?.(dockerAppUrl());
|
|
4290
4377
|
if (r?.active) return;
|
|
4291
4378
|
} catch {}
|
|
4292
4379
|
// 没开过 → 走慢路径(读容器 token 最多 ~50s)。进行中只显示 CTA 的「打开中…」spinner
|
|
@@ -4365,17 +4452,33 @@ function DockerCard({ dockerTeam, cloudTitle, cloudCode, onOpen, onRename, onRef
|
|
|
4365
4452
|
</button>
|
|
4366
4453
|
{/* 常用 */}
|
|
4367
4454
|
<button type="button" data-id="DockerCard-reload" className="bcard__menu-item"
|
|
4368
|
-
onClick={(e) => { e.stopPropagation(); setMenuOpen(false); window.cicy?.tabs?.reloadIfOpen?.(
|
|
4455
|
+
onClick={(e) => { e.stopPropagation(); setMenuOpen(false); window.cicy?.tabs?.reloadIfOpen?.(dockerAppUrl(), LOCAL_TEAM_NAME); }}>
|
|
4369
4456
|
{tr("docker.reloadWindow", "刷新窗口")}
|
|
4370
4457
|
</button>
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4458
|
+
{/* QEMU 机器:没有宿主共享目录(WSL/项目目录打不开),换成虚拟机的终端 + 硬件加速 */}
|
|
4459
|
+
{isQemu ? (<>
|
|
4460
|
+
<button type="button" data-id="DockerCard-vm-terminal" className="bcard__menu-item"
|
|
4461
|
+
title={tr("docker.vmTermHint", "本机团队所在的 Debian 虚拟机的命令行(用户 cicy,sudo 免密;sudo docker ps 可看到容器)")}
|
|
4462
|
+
onClick={(e) => { e.stopPropagation(); openVmTerminal(); }}>
|
|
4463
|
+
{tr("docker.vmTerm", "打开终端")}
|
|
4464
|
+
</button>
|
|
4465
|
+
{!(vmSt && vmSt.accel === "whpx") && (
|
|
4466
|
+
<button type="button" data-id="DockerCard-vm-accel" className="bcard__menu-item"
|
|
4467
|
+
title={tr("docker.accelHint", "开启 Windows 虚拟机监控程序平台(要管理员 + 重启)")}
|
|
4468
|
+
onClick={(e) => { e.stopPropagation(); enableAccel(); }}>
|
|
4469
|
+
{tr("docker.accel", "开启硬件加速")}
|
|
4470
|
+
</button>
|
|
4471
|
+
)}
|
|
4472
|
+
</>) : (<>
|
|
4473
|
+
<button type="button" data-id="DockerCard-open-dir" className="bcard__menu-item"
|
|
4474
|
+
onClick={(e) => { e.stopPropagation(); setMenuOpen(false); window.cicy?.docker?.openDir?.(); }}>
|
|
4475
|
+
{tr("docker.openWslDir", "打开 WSL 目录")}
|
|
4476
|
+
</button>
|
|
4477
|
+
<button type="button" data-id="DockerCard-open-projects" className="bcard__menu-item"
|
|
4478
|
+
onClick={(e) => { e.stopPropagation(); setMenuOpen(false); window.cicy?.docker?.openDir?.("projects"); }}>
|
|
4479
|
+
{tr("docker.openProjectsDir", "打开项目目录")}
|
|
4480
|
+
</button>
|
|
4481
|
+
</>)}
|
|
4379
4482
|
<button type="button" data-id="DockerCard-dood" className="bcard__menu-item"
|
|
4380
4483
|
title={tr("dood.hint", "把宿主 Docker + docker 客户端挂进容器,容器内 agent 能直接跑 docker(切换会重建容器,秒生效、无需下载)")}
|
|
4381
4484
|
onClick={(e) => { e.stopPropagation(); setMenuOpen(false); setDoodOpen(true); }}>
|
|
@@ -4488,7 +4591,7 @@ function DockerCard({ dockerTeam, cloudTitle, cloudCode, onOpen, onRename, onRef
|
|
|
4488
4591
|
<h3 style={{ margin: "0 0 6px", fontSize: 16, color: "#e6edf3" }}>{tr("docker.ports.title", "端口设置")}</h3>
|
|
4489
4592
|
<p style={{ margin: "0 0 14px", fontSize: 12.5, lineHeight: 1.6, color: "#9aa4b2" }}>{tr("docker.ports.sub", "除 :8008 外,额外发布、可从 Windows 直达容器内服务的端口")}</p>
|
|
4490
4593
|
<div style={{ display: "flex", alignItems: "center", gap: 8, padding: "8px 10px", marginBottom: 10, borderRadius: 8, background: "#0d1117", border: "1px solid #21262d", color: "#7d8590", fontSize: 13 }}>
|
|
4491
|
-
🔒 {tr("docker.ports.mainFixed", ":
|
|
4594
|
+
🔒 {tr("docker.ports.mainFixed", ":{{p}} · cicy-code(固定)", { p: dockerAppPort })}
|
|
4492
4595
|
</div>
|
|
4493
4596
|
<div style={{ display: "flex", flexDirection: "column", gap: 8, maxHeight: 220, overflowY: "auto" }}>
|
|
4494
4597
|
{portList.length === 0 && (
|
|
@@ -5036,12 +5139,16 @@ function isLocalSidecar(baseUrl) {
|
|
|
5036
5139
|
|
|
5037
5140
|
// The Docker-版 cicy-code instance — localhost:8008, **只在 Windows**。Owned by
|
|
5038
5141
|
// <DockerCard>, so it's filtered out of the generic node lists.
|
|
5142
|
+
// Docker-版 cicy-code 的本机端口:老机器(WSL)8008,新机器(QEMU,不靠 WSL)8009。一台机器只会有其一;
|
|
5143
|
+
// 真实值以 desktop 报上来的 docker:app-status.port 为准(DockerCard 拿到后写进 dockerAppPort)。
|
|
5144
|
+
let dockerAppPort = 8008;
|
|
5145
|
+
const dockerAppUrl = () => `http://127.0.0.1:${dockerAppPort}`;
|
|
5039
5146
|
function isDockerApp(baseUrl) {
|
|
5040
5147
|
try {
|
|
5041
5148
|
if (window.cicy?.platform !== "win32") return false; // docker-版只在 Windows
|
|
5042
5149
|
const p = new URL(baseUrl);
|
|
5043
5150
|
const local = p.hostname === "127.0.0.1" || p.hostname === "localhost" || p.hostname === "::1";
|
|
5044
|
-
return local && p.port === "8008";
|
|
5151
|
+
return local && (p.port === "8008" || p.port === "8009");
|
|
5045
5152
|
} catch { return false; }
|
|
5046
5153
|
}
|
|
5047
5154
|
|