cicy-desktop 2.1.239 → 2.1.241
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 +5 -5
- package/src/backends/homepage-preload.js +2 -0
- package/src/backends/homepage-react/assets/index-CCx0HFAk.js +377 -0
- package/src/backends/homepage-react/index.html +1 -1
- package/src/backends/local-teams.js +27 -3
- package/src/backends/sidecar-ipc.js +24 -1
- package/src/i18n/locales/en.json +12 -0
- package/src/i18n/locales/fr.json +12 -0
- package/src/i18n/locales/ja.json +12 -0
- package/src/i18n/locales/zh-CN.json +12 -0
- package/src/main.js +13 -0
- package/src/sidecar/cicy-code.js +11 -1
- package/src/sidecar/native.js +9 -1
- package/src/sidecar/wsl-docker.js +38 -7
- package/workers/render/src/App.jsx +71 -0
- package/src/backends/homepage-react/assets/index-DEVBS0FI.js +0 -377
|
@@ -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-CCx0HFAk.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="./assets/index-C6Zrdgc0.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
@@ -572,8 +572,10 @@ async function pullCustomTeams() {
|
|
|
572
572
|
for (const [k, v] of Object.entries(nodes)) { if (v?.cloud_team_id != null) byCloudId[String(v.cloud_team_id)] = k; }
|
|
573
573
|
let added = 0, updated = 0;
|
|
574
574
|
for (const t of list.teams) {
|
|
575
|
-
|
|
576
|
-
|
|
575
|
+
// custom teams (remote URL) OR any team with a zero-trust gateway address:
|
|
576
|
+
// gateway teams are reached at <slug>.gw.cicy-ai.com (preferred over host_url).
|
|
577
|
+
if (t.kind !== "custom" && !t.gateway_url) continue;
|
|
578
|
+
const host = String(t.gateway_url || t.host_url || t.hostUrl || "").trim();
|
|
577
579
|
if (!host) continue;
|
|
578
580
|
const tid = t.teamId || t.id || null;
|
|
579
581
|
const localId = tid != null ? byCloudId[String(tid)] : null;
|
|
@@ -1099,4 +1101,26 @@ function avatarForUrl(url) {
|
|
|
1099
1101
|
return "";
|
|
1100
1102
|
}
|
|
1101
1103
|
|
|
1102
|
-
|
|
1104
|
+
// Remove teams tied to a cloud ACCOUNT (remote base_url — pulled from the cloud
|
|
1105
|
+
// via pullCustomTeams, or user-added remote nodes) so switching/leaving an account
|
|
1106
|
+
// doesn't leak them onto the next user. LOCAL machine teams on THIS device
|
|
1107
|
+
// (localhost base_url) are kept — they belong to the box, not the account. The
|
|
1108
|
+
// next login re-pulls the new account's own teams fresh.
|
|
1109
|
+
async function purgeAccountTeams() {
|
|
1110
|
+
let removed = 0;
|
|
1111
|
+
try {
|
|
1112
|
+
await writeNodes((nodes) => {
|
|
1113
|
+
for (const id of Object.keys(nodes)) {
|
|
1114
|
+
let host = "";
|
|
1115
|
+
try { host = new URL(String(nodes[id]?.base_url || "")).hostname.toLowerCase(); } catch {}
|
|
1116
|
+
const isLocal = host === "" || host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
1117
|
+
if (!isLocal) { delete nodes[id]; removed++; }
|
|
1118
|
+
}
|
|
1119
|
+
return nodes;
|
|
1120
|
+
});
|
|
1121
|
+
} catch (e) { log.warn(`[local-teams] purgeAccountTeams failed: ${e.message}`); }
|
|
1122
|
+
if (removed) { _cache = null; _cacheUntil = 0; log.info(`[local-teams] purged ${removed} account team(s) (login/logout)`); }
|
|
1123
|
+
return removed;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
module.exports = { list, openTeam, reloadTeam, closeLocalWindows, addTeam, removeTeam, updateTeam, upgradeTeam, syncAllLocalTeams, purgeAccountTeams, setAvatar, getAvatars, avatarForUrl };
|
|
@@ -293,7 +293,7 @@ function register({ sidecarLogPath } = {}) {
|
|
|
293
293
|
await ensureDockerTeam().catch(() => {});
|
|
294
294
|
const env = { CICY_AI_GATEWAY_LLM_ENDPOINT: GATEWAY_ENDPOINT };
|
|
295
295
|
if (dockerTeamReg && dockerTeamReg.apiKey) env.CICY_AI_GATEWAY_LLM_API_KEY = dockerTeamReg.apiKey;
|
|
296
|
-
return { port: APP_PORT, container: APP_CONTAINER, volume: APP_VOLUME, env, extraPorts: readExtraPorts() };
|
|
296
|
+
return { port: APP_PORT, container: APP_CONTAINER, volume: APP_VOLUME, env, extraPorts: readExtraPorts(), dockerSock: sidecar.isDood() };
|
|
297
297
|
};
|
|
298
298
|
// Register the running :8008 instance as a (custom) team so the card's "打开"
|
|
299
299
|
// reuses the token-injected open/reload flow. addTeam dedups by host:port.
|
|
@@ -675,6 +675,29 @@ function register({ sidecarLogPath } = {}) {
|
|
|
675
675
|
}
|
|
676
676
|
});
|
|
677
677
|
|
|
678
|
+
// 「容器内使用 Docker」(Docker-outside-of-Docker)开关: 读/写 dood flag。set 后重建容器
|
|
679
|
+
// 让 docker.sock + WSL 的 docker 客户端挂载生效(runContainer 内部读 dood flag)。不再下载
|
|
680
|
+
// docker CLI(改为直接挂 WSL 现成的二进制,秒生效,避免卡在"保存中")。进度推抽屉(op:restart)。
|
|
681
|
+
ipcMain.handle("sidecar:get-dood", () => {
|
|
682
|
+
try { return { ok: true, dood: sidecar.isDood() }; } catch (e) { return { ok: false, dood: false, error: e.message }; }
|
|
683
|
+
});
|
|
684
|
+
ipcMain.handle("sidecar:set-dood", async (e, on) => {
|
|
685
|
+
const emit = (ev) => { try { e.sender.send("sidecar:op-progress", { op: "restart", ...ev }); } catch {} };
|
|
686
|
+
try {
|
|
687
|
+
sidecar.setDood(!!on);
|
|
688
|
+
emit({ phase: "swap", status: "running", message: `容器 Docker 访问已${on ? "开启" : "关闭"},正在重建容器…` });
|
|
689
|
+
try { await appDocker.recreate({ ...(await appOpts()), onProgress: emit }); }
|
|
690
|
+
catch (err) { try { await appDocker.dockerRestart?.({ onProgress: emit }); } catch {} }
|
|
691
|
+
let up = false;
|
|
692
|
+
for (let i = 0; i < 240; i++) { if ((await appDocker.probeHealth?.(PORT)) || (await sidecar.probeExisting(PORT))) { up = true; break; } await new Promise((r) => setTimeout(r, 500)); }
|
|
693
|
+
emit({ phase: "done", status: up ? "done" : "error", message: up ? `容器 Docker 访问已${on ? "开启" : "关闭"}` : "重建后 :8008 未就绪——稍等或重试" });
|
|
694
|
+
return { ok: up, dood: !!on };
|
|
695
|
+
} catch (err) {
|
|
696
|
+
emit({ phase: "done", status: "error", message: `设置失败:${err.message}` });
|
|
697
|
+
return { ok: false, error: err.message };
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
|
|
678
701
|
// Update: stop + spawn cicy-code@latest (or reload the Docker image on
|
|
679
702
|
// win32). The npx re-resolve / image pull can take a while on a cold cache,
|
|
680
703
|
// so allow a longer window for :8008 to come back.
|
package/src/i18n/locales/en.json
CHANGED
|
@@ -570,5 +570,17 @@
|
|
|
570
570
|
"tabShell": {
|
|
571
571
|
"myTeam": "My Team",
|
|
572
572
|
"newTabBtn": "New tab"
|
|
573
|
+
},
|
|
574
|
+
"dood": {
|
|
575
|
+
"menu": "Use Docker in container",
|
|
576
|
+
"hint": "Mount the host Docker + docker client into the container so its agent can run docker (toggling rebuilds the container; takes effect instantly, no download)",
|
|
577
|
+
"title": "Use Docker in container",
|
|
578
|
+
"subtitle": "Mount the host Docker + docker client into the container so its agent can run docker (instant, no download)",
|
|
579
|
+
"enable": "Allow Docker inside the container",
|
|
580
|
+
"applying": "Enabling container Docker access, rebuilding…",
|
|
581
|
+
"disabling": "Disabling…",
|
|
582
|
+
"onDone": "Container Docker access enabled",
|
|
583
|
+
"offDone": "Container Docker access disabled",
|
|
584
|
+
"failed": "Failed to apply"
|
|
573
585
|
}
|
|
574
586
|
}
|
package/src/i18n/locales/fr.json
CHANGED
|
@@ -570,5 +570,17 @@
|
|
|
570
570
|
"tabShell": {
|
|
571
571
|
"myTeam": "Mon équipe",
|
|
572
572
|
"newTabBtn": "Nouvel onglet"
|
|
573
|
+
},
|
|
574
|
+
"dood": {
|
|
575
|
+
"menu": "Utiliser Docker dans le conteneur",
|
|
576
|
+
"hint": "Monte le Docker et le client docker de l'hôte dans le conteneur pour que son agent puisse exécuter docker (le basculement recrée le conteneur ; effet immédiat, sans téléchargement)",
|
|
577
|
+
"title": "Utiliser Docker dans le conteneur",
|
|
578
|
+
"subtitle": "Monte le Docker et le client docker de l'hôte dans le conteneur pour que son agent puisse exécuter docker (immédiat, sans téléchargement)",
|
|
579
|
+
"enable": "Autoriser Docker dans le conteneur",
|
|
580
|
+
"applying": "Activation de l'accès Docker du conteneur, reconstruction…",
|
|
581
|
+
"disabling": "Désactivation…",
|
|
582
|
+
"onDone": "Accès Docker du conteneur activé",
|
|
583
|
+
"offDone": "Accès Docker du conteneur désactivé",
|
|
584
|
+
"failed": "Échec de l'application"
|
|
573
585
|
}
|
|
574
586
|
}
|
package/src/i18n/locales/ja.json
CHANGED
|
@@ -570,5 +570,17 @@
|
|
|
570
570
|
"tabShell": {
|
|
571
571
|
"myTeam": "マイチーム",
|
|
572
572
|
"newTabBtn": "新しいタブ"
|
|
573
|
+
},
|
|
574
|
+
"dood": {
|
|
575
|
+
"menu": "コンテナ内で Docker を使う",
|
|
576
|
+
"hint": "ホストの Docker と docker クライアントをコンテナにマウントし、agent が docker を実行できるようにします(切り替えでコンテナを再作成、即時反映・ダウンロード不要)",
|
|
577
|
+
"title": "コンテナ内で Docker を使う",
|
|
578
|
+
"subtitle": "ホストの Docker と docker クライアントをコンテナにマウントして、agent が docker を直接実行できます(即時反映、ダウンロード不要)",
|
|
579
|
+
"enable": "コンテナ内での Docker を許可",
|
|
580
|
+
"applying": "コンテナ Docker アクセスを有効化、再作成中…",
|
|
581
|
+
"disabling": "無効化中…",
|
|
582
|
+
"onDone": "コンテナ Docker アクセスを有効化しました",
|
|
583
|
+
"offDone": "コンテナ Docker アクセスを無効化しました",
|
|
584
|
+
"failed": "設定に失敗しました"
|
|
573
585
|
}
|
|
574
586
|
}
|
|
@@ -570,5 +570,17 @@
|
|
|
570
570
|
"tabShell": {
|
|
571
571
|
"myTeam": "我的团队",
|
|
572
572
|
"newTabBtn": "新建标签"
|
|
573
|
+
},
|
|
574
|
+
"dood": {
|
|
575
|
+
"menu": "容器内使用 Docker",
|
|
576
|
+
"hint": "把宿主 Docker + docker 客户端挂进容器,容器内 agent 能直接跑 docker(切换会重建容器,秒生效、无需下载)",
|
|
577
|
+
"title": "容器内使用 Docker",
|
|
578
|
+
"subtitle": "把宿主 Docker + docker 客户端挂进容器,容器内 agent 就能直接跑 docker(秒生效,无需下载)",
|
|
579
|
+
"enable": "允许容器内使用 Docker",
|
|
580
|
+
"applying": "开启容器 Docker 访问,重建容器中…",
|
|
581
|
+
"disabling": "关闭中…",
|
|
582
|
+
"onDone": "容器 Docker 访问已开启",
|
|
583
|
+
"offDone": "容器 Docker 访问已关闭",
|
|
584
|
+
"failed": "设置失败"
|
|
573
585
|
}
|
|
574
586
|
}
|
package/src/main.js
CHANGED
|
@@ -998,6 +998,17 @@ electronApp.whenReady().then(async () => {
|
|
|
998
998
|
// renderer. A completed email login is thus indistinguishable downstream.
|
|
999
999
|
const onAuthResult = (payload) => {
|
|
1000
1000
|
if (payload && payload.token) {
|
|
1001
|
+
// Account isolation: if a DIFFERENT user is signing in, drop the previous
|
|
1002
|
+
// account's cloud/remote teams so they don't leak onto this account. The
|
|
1003
|
+
// login below re-pulls this account's own teams fresh. Local machine
|
|
1004
|
+
// teams (localhost) are kept — they belong to the device.
|
|
1005
|
+
try {
|
|
1006
|
+
const prevUid = String((readGlobalConfig(GLOBAL_JSON)?.desktopAuth?.userId) || "");
|
|
1007
|
+
const nextUid = payload.userId != null ? String(payload.userId) : "";
|
|
1008
|
+
if (prevUid && nextUid && prevUid !== nextUid) {
|
|
1009
|
+
require("./backends/local-teams").purgeAccountTeams().catch(() => {});
|
|
1010
|
+
}
|
|
1011
|
+
} catch {}
|
|
1001
1012
|
saveDesktopAuth(payload);
|
|
1002
1013
|
// Now that we have a real owner-bound login token, report this
|
|
1003
1014
|
// machine to the cloud (best-effort; safe to call repeatedly —
|
|
@@ -1067,6 +1078,8 @@ electronApp.whenReady().then(async () => {
|
|
|
1067
1078
|
__ipcMainAuth.handle("auth:logout", () => {
|
|
1068
1079
|
try {
|
|
1069
1080
|
updateGlobalConfig(GLOBAL_JSON, (c) => { delete c.desktopAuth; return c; });
|
|
1081
|
+
// Drop this account's cloud/remote teams on logout (local machines stay).
|
|
1082
|
+
require("./backends/local-teams").purgeAccountTeams().catch(() => {});
|
|
1070
1083
|
log.info("[auth] desktop login cleared (explicit logout)");
|
|
1071
1084
|
} catch (e) { log.warn(`[auth] logout clear failed: ${e.message}`); }
|
|
1072
1085
|
return { ok: true };
|
package/src/sidecar/cicy-code.js
CHANGED
|
@@ -269,6 +269,16 @@ function setPublicFlag(on) {
|
|
|
269
269
|
return f.public;
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
// 「容器内使用 Docker」开关(desktop-flags.json 的 dood 字段 = Docker-outside-of-Docker)。
|
|
273
|
+
// 开启后 runContainer 把宿主 dockerd 的 /var/run/docker.sock 挂进容器,并把 docker CLI 装进
|
|
274
|
+
// 容器持久卷,容器内 agent 就能跑 docker。只影响 Windows/WSL(+mac colima)容器路径。
|
|
275
|
+
function isDood() { return !!readFlags().dood; }
|
|
276
|
+
function setDood(on) {
|
|
277
|
+
const f = readFlags(); f.dood = !!on;
|
|
278
|
+
try { fs.mkdirSync(path.dirname(FLAGS_FILE), { recursive: true }); fs.writeFileSync(FLAGS_FILE, JSON.stringify(f, null, 2)); } catch (e) { console.warn(`[cicy-code-sidecar] setDood write failed: ${e.message}`); }
|
|
279
|
+
return f.dood;
|
|
280
|
+
}
|
|
281
|
+
|
|
272
282
|
// 「Cloudflare Tunnel」配置持久化(同 desktop-flags.json 的 cft 字段):
|
|
273
283
|
// { enabled, token, host }。enabled 且 token 非空时,start() 注入 CICY_CFT_TOKEN/
|
|
274
284
|
// CICY_CFT_HOST 环境变量 —— cicy-code 据此跑一条命名隧道(固定域名)。host 是对外
|
|
@@ -565,4 +575,4 @@ async function update({ logPath, port = DEFAULT_PORT, emit } = {}) {
|
|
|
565
575
|
}
|
|
566
576
|
}
|
|
567
577
|
|
|
568
|
-
module.exports = { start, stop, restart, update, probeExisting, clearNpxCache, isUpdating, isBusy, ensureEnv, ensureNode, isPublic, setPublicFlag, getCft, setCft, cftEnv };
|
|
578
|
+
module.exports = { start, stop, restart, update, probeExisting, clearNpxCache, isUpdating, isBusy, ensureEnv, ensureNode, isPublic, setPublicFlag, isDood, setDood, getCft, setCft, cftEnv };
|
package/src/sidecar/native.js
CHANGED
|
@@ -143,7 +143,15 @@ async function start({ port = 8008, logPath = null, emit, version = null } = {})
|
|
|
143
143
|
};
|
|
144
144
|
// --helper removed : boot cicy-code in normal mode (full tmux-based
|
|
145
145
|
// multi-agent), not the single headless 团队助手.
|
|
146
|
-
const
|
|
146
|
+
const args = ["--desktop"];
|
|
147
|
+
// Host role: if a zero-trust gateway is provisioned (~/cicy-ai/db/gateway.json
|
|
148
|
+
// with url+token), boot with --gateway so this machine dials OUT and becomes
|
|
149
|
+
// reachable through the gateway (no inbound port). Absent → plain local boot.
|
|
150
|
+
try {
|
|
151
|
+
const gwPath = require("path").join(require("os").homedir(), "cicy-ai", "db", "gateway.json");
|
|
152
|
+
if (require("fs").existsSync(gwPath)) args.push("--gateway");
|
|
153
|
+
} catch { /* best-effort */ }
|
|
154
|
+
const child = spawn(exe, args, { stdio, detached: true, windowsHide: true, env });
|
|
147
155
|
child.unref();
|
|
148
156
|
try { fs.writeFileSync(PID_FILE, String(child.pid)); } catch {}
|
|
149
157
|
console.log(`[native-sidecar] spawned ${exe} --desktop pid=${child.pid} port=${port} log=${logPath || "(none)"}`);
|
|
@@ -560,6 +560,32 @@ async function allDrivesMountArg() {
|
|
|
560
560
|
} catch (e) { log.warn(`[wsl-docker] enumerate drives failed: ${e.message}`); return ""; }
|
|
561
561
|
}
|
|
562
562
|
|
|
563
|
+
// Docker-outside-of-Docker: 把 WSL 里 dockerd 的 socket 挂进容器,容器内 agent 就能直接跑
|
|
564
|
+
// docker —— 复用 WSL 那个 dockerd,起的是**兄弟容器**(非嵌套),所以兄弟容器里 `-v` 的路径
|
|
565
|
+
// 是 WSL 宿主视角、不是 cicy-code 容器内路径。socket 通常 root:docker(或 root:root)660,
|
|
566
|
+
// 给容器进程 `--group-add <socket 的 gid>` 让非 root 的 cicy 用户也能访问。
|
|
567
|
+
// 同时把 WSL 自带的 docker 客户端(静态 Go 二进制,/usr/bin/docker)只读挂进容器 PATH —— 这样
|
|
568
|
+
// 容器里立刻有 `docker` 命令可用,**不用下载**(之前下 70MB static tarball 会让 UI 卡在"保存中")。
|
|
569
|
+
// socket 不在(dockerd 没起)→ 返回 ""(best-effort,不挡容器启动)。挂载在 `docker run` 时定死,
|
|
570
|
+
// 已在跑的容器要 recreate 才带上。
|
|
571
|
+
async function dockerSockMountArg() {
|
|
572
|
+
const SOCK = "/var/run/docker.sock";
|
|
573
|
+
try { await wslRun(`test -S ${SOCK}`, { timeout: 8000 }); } catch { return ""; } // socket 不存在
|
|
574
|
+
let groupAdd = "";
|
|
575
|
+
try {
|
|
576
|
+
const { stdout } = await wslRun(`stat -c '%g' ${SOCK} 2>/dev/null`, { timeout: 8000 });
|
|
577
|
+
const gid = String(stdout || "").trim();
|
|
578
|
+
if (/^\d+$/.test(gid)) groupAdd = ` --group-add ${gid}`;
|
|
579
|
+
} catch {}
|
|
580
|
+
let cliMount = "";
|
|
581
|
+
try {
|
|
582
|
+
const { stdout } = await wslRun(`command -v docker 2>/dev/null`, { timeout: 8000 });
|
|
583
|
+
const p = String(stdout || "").trim();
|
|
584
|
+
if (p) cliMount = ` -v ${p}:/usr/local/bin/docker:ro`;
|
|
585
|
+
} catch {}
|
|
586
|
+
return `-v ${SOCK}:${SOCK}${cliMount}${groupAdd}`;
|
|
587
|
+
}
|
|
588
|
+
|
|
563
589
|
// extraPorts → additional `-p 127.0.0.1:<p>:<p>` mappings (host=container, loopback
|
|
564
590
|
// only). For container-internal agent services the user wants reachable from
|
|
565
591
|
// Windows. Each adds one docker-proxy, so keep the list small. 8008/主端口自动排除。
|
|
@@ -575,7 +601,7 @@ function publishArgs(port, extraPorts = []) {
|
|
|
575
601
|
return args;
|
|
576
602
|
}
|
|
577
603
|
|
|
578
|
-
async function runContainer({ port = 8008, container = "cicy-code-docker", volume = "cicy-team-8008", env = {}, extraPorts = [], emit } = {}) {
|
|
604
|
+
async function runContainer({ port = 8008, container = "cicy-code-docker", volume = "cicy-team-8008", env = {}, extraPorts = [], dockerSock = false, emit } = {}) {
|
|
579
605
|
// 每次容器"启动"(含已在跑被 adopt)都确保桌面快捷方式存在 —— 不存在就建,坏了就修。
|
|
580
606
|
if (await probeHealth(port)) { ensureDesktopShortcut(volume, port).catch(() => {}); return { adopted: true }; }
|
|
581
607
|
// Replace any stale same-named container.
|
|
@@ -599,7 +625,12 @@ async function runContainer({ port = 8008, container = "cicy-code-docker", volum
|
|
|
599
625
|
// 已删——docker 默认每端口起一个 userland-proxy 进程 → 2000 进程,docker run 卡死/吃内存/
|
|
600
626
|
// 偶发失败(实测)。容器内 agent 服务需要从 Windows 直达时再按需单独暴露。
|
|
601
627
|
const drivesArg = await allDrivesMountArg(); // 动态:有几个盘挂几个
|
|
602
|
-
|
|
628
|
+
// DooD:开关开时挂 WSL docker.sock。跟 cftEnv 一样内部读 flag(与显式 opt 取或),
|
|
629
|
+
// 这样任何 recreate 调用方式(带不带 appOpts)都能生效。
|
|
630
|
+
let doodOn = dockerSock;
|
|
631
|
+
if (!doodOn) { try { doodOn = require("./cicy-code").isDood(); } catch {} }
|
|
632
|
+
const sockArg = doodOn ? await dockerSockMountArg() : "";
|
|
633
|
+
const cmd = `docker run -d --name ${container} --restart unless-stopped --dns 223.5.5.5 --dns 8.8.8.8 ${publishArgs(port, extraPorts)} -e CICY_PUBLIC=1 -v ${volume}:/home/cicy ${projectsMountArg()} ${drivesArg} ${sockArg} ${envArgs} ${IMAGE}`;
|
|
603
634
|
emit && emit({ phase: "container", status: "running", message: `$ ${cmd.length > 220 ? cmd.slice(0, 220) + " …" : cmd}` });
|
|
604
635
|
await wslRun(cmd, { timeout: 60000 }); // 失败时 err.stderr 带 docker 真错误 → _bootstrap 的 errTail 显示
|
|
605
636
|
ensureDesktopShortcut(volume, port).catch(() => {});
|
|
@@ -764,7 +795,7 @@ async function bootstrap(opts = {}) {
|
|
|
764
795
|
// 注意: 默认容器/卷名保持 cicy-team / cicy-code-docker(回退实测"现在不行了"的改动)。
|
|
765
796
|
// live 路径(docker:app-bootstrap)始终传显式 APP_*(cicy-team-8008)名,不靠这里的默认;
|
|
766
797
|
// 改默认会让既有 cicy-team 卷的装机对不上 → 退回原值。
|
|
767
|
-
async function _bootstrap({ onProgress, port = 8008, container = "cicy-code-docker", volume = "cicy-team", env = {}, extraPorts = [] } = {}) {
|
|
798
|
+
async function _bootstrap({ onProgress, port = 8008, container = "cicy-code-docker", volume = "cicy-team", env = {}, extraPorts = [], dockerSock = false } = {}) {
|
|
768
799
|
const emit = (ev) => { try { onProgress && onProgress(ev); } catch {} };
|
|
769
800
|
|
|
770
801
|
// Structured, PERSISTED trace of the whole run (electron-log → main.log) so a
|
|
@@ -875,7 +906,7 @@ async function _bootstrap({ onProgress, port = 8008, container = "cicy-code-dock
|
|
|
875
906
|
begin("run-container");
|
|
876
907
|
if (!(await probeHealth(port))) {
|
|
877
908
|
emit({ phase: "container", status: "running", message: "启动 cicy-code 服务…" });
|
|
878
|
-
try { await runContainer({ port, container, volume, env, extraPorts, emit }); }
|
|
909
|
+
try { await runContainer({ port, container, volume, env, extraPorts, dockerSock, emit }); }
|
|
879
910
|
catch (e) { fail("container_start_failed", e.message); emit({ phase: "container", status: "error", message: `服务启动失败:${e.message}(点重试)${errTail(e)}` }); finish(false, "container_start_failed"); return { ok: false, reason: "container_start_failed" }; }
|
|
880
911
|
done();
|
|
881
912
|
} else done(true);
|
|
@@ -1028,14 +1059,14 @@ async function dockerRestart({ container = "cicy-code-docker-8008" } = {}) {
|
|
|
1028
1059
|
// 重建容器:docker rm -f 旧容器 + docker run 新容器(用新 env,如新的 docker team 网关
|
|
1029
1060
|
// key)。**保留 volume**(数据/api_token/deviceId 不丢),只是换掉容器本身 + env。
|
|
1030
1061
|
// 破坏性(短暂中断 + 换 key)→ 调用方要 confirm。
|
|
1031
|
-
async function recreate({ onProgress, port = 8008, container = "cicy-code-docker-8008", volume = "cicy-team-8008", env = {}, extraPorts = [] } = {}) {
|
|
1062
|
+
async function recreate({ onProgress, port = 8008, container = "cicy-code-docker-8008", volume = "cicy-team-8008", env = {}, extraPorts = [], dockerSock = false } = {}) {
|
|
1032
1063
|
const emit = (ev) => { try { onProgress && onProgress(ev); } catch {} };
|
|
1033
1064
|
// 重建 = 用最新镜像重建。OSS 有更新版先刷新(非破坏性,不删发行版/volume),再 rm + run。
|
|
1034
1065
|
try { await ensureFreshImage({ emit }); } catch (e) { emit({ phase: "image", status: "running", message: `镜像刷新跳过(${e.message}),用现有镜像重建` }); }
|
|
1035
1066
|
// 强删占用该端口的**任何**容器(含老名字 cicy-code-docker)+ 目标容器 —— 否则
|
|
1036
1067
|
// runContainer 开头的 probeHealth 看到旧容器还健康会 adopt 它、不重建,key 就换不了。
|
|
1037
1068
|
try { await wslRun(`docker ps -aq --filter publish=${port} | xargs -r docker rm -f 2>/dev/null; docker rm -f ${container} 2>/dev/null; true`, { timeout: 30000 }); } catch {}
|
|
1038
|
-
const r = await runContainer({ port, container, volume, env, extraPorts, emit });
|
|
1069
|
+
const r = await runContainer({ port, container, volume, env, extraPorts, dockerSock, emit });
|
|
1039
1070
|
try { await ensureDesktopShortcut(volume, port); } catch {}
|
|
1040
1071
|
// 等 :8008 真正起来再返回——否则 docker run 一返回卡片就探测「没运行」,显示「启动」让
|
|
1041
1072
|
// 用户再点一下(实测:端口保存后重建,drawer 关了还要手动点启动)。
|
|
@@ -1064,7 +1095,7 @@ async function upgrade({ onProgress, port = 8008, container = "cicy-code-docker"
|
|
|
1064
1095
|
try { await stop({ container }); } catch {}
|
|
1065
1096
|
try { await unregisterDistro(); } catch {}
|
|
1066
1097
|
// Reuse the robust one-shot install flow (download → import → dockerd → run).
|
|
1067
|
-
return await _bootstrap({ onProgress, port, container, volume, env });
|
|
1098
|
+
return await _bootstrap({ onProgress, port, container, volume, env, dockerSock });
|
|
1068
1099
|
}
|
|
1069
1100
|
|
|
1070
1101
|
// 容器里有没有注入网关 key(reconcile 自愈用):printenv 看 CICY_AI_GATEWAY_LLM_API_KEY。
|
|
@@ -2016,6 +2016,7 @@ function DockerCard({ dockerTeam, cloudTitle, cloudCode, onOpen, onRename, onRef
|
|
|
2016
2016
|
const [busy, setBusy] = useState(""); // "" | bootstrap | restart | stop | upgrade | probe
|
|
2017
2017
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
2018
2018
|
const [cftOpen, setCftOpen] = useState(false); // Cloudflare 隧道 modal(卡片层,菜单外)
|
|
2019
|
+
const [doodOpen, setDoodOpen] = useState(false); // 容器内使用 Docker(DooD)modal(卡片层,菜单外)
|
|
2019
2020
|
const [confirmRecreate, setConfirmRecreate] = useState(false); // 重建容器 in-app 确认弹窗(不用 native confirm)
|
|
2020
2021
|
const [portsOpen, setPortsOpen] = useState(false); // 端口设置 modal
|
|
2021
2022
|
const [portList, setPortList] = useState([]); // 编辑中的额外端口(字符串数组,便于输入)
|
|
@@ -2411,6 +2412,11 @@ function DockerCard({ dockerTeam, cloudTitle, cloudCode, onOpen, onRename, onRef
|
|
|
2411
2412
|
onClick={(e) => { e.stopPropagation(); setMenuOpen(false); window.cicy?.docker?.openDir?.("projects"); }}>
|
|
2412
2413
|
{tr("docker.openProjectsDir", "打开项目目录")}
|
|
2413
2414
|
</button>
|
|
2415
|
+
<button type="button" data-id="DockerCard-dood" className="bcard__menu-item"
|
|
2416
|
+
title={tr("dood.hint", "把宿主 Docker + docker 客户端挂进容器,容器内 agent 能直接跑 docker(切换会重建容器,秒生效、无需下载)")}
|
|
2417
|
+
onClick={(e) => { e.stopPropagation(); setMenuOpen(false); setDoodOpen(true); }}>
|
|
2418
|
+
{tr("dood.menu", "容器内使用 Docker")}
|
|
2419
|
+
</button>
|
|
2414
2420
|
<button type="button" data-id="DockerCard-billing" className="bcard__menu-item"
|
|
2415
2421
|
onClick={() => { setMenuOpen(false); openCloudPage((cloudCode || dockerTeam?.cloud_team_id) ? `?team=${encodeURIComponent(cloudCode || dockerTeam.cloud_team_id)}` : "?view=usage"); }}>
|
|
2416
2422
|
{tr("docker.billing", "帐单")}
|
|
@@ -2487,6 +2493,7 @@ function DockerCard({ dockerTeam, cloudTitle, cloudCode, onOpen, onRename, onRef
|
|
|
2487
2493
|
<span>{ctaLabel}</span>
|
|
2488
2494
|
</button>
|
|
2489
2495
|
<CftModal open={cftOpen} onClose={() => setCftOpen(false)} toastId="docker-op" />
|
|
2496
|
+
<DoodModal open={doodOpen} onClose={() => setDoodOpen(false)} toastId="docker-op" />
|
|
2490
2497
|
{confirmRecreate && createPortal(
|
|
2491
2498
|
<div data-id="DockerCard-recreate-modal"
|
|
2492
2499
|
style={{ position: "fixed", inset: 0, zIndex: 9999, background: "rgba(0,0,0,0.55)", display: "flex", alignItems: "center", justifyContent: "center" }}
|
|
@@ -3529,6 +3536,70 @@ function CftModal({ open, onClose, toastId = "cft-op" }) {
|
|
|
3529
3536
|
</>
|
|
3530
3537
|
);
|
|
3531
3538
|
}
|
|
3539
|
+
// 容器内使用 Docker(Docker-outside-of-Docker)modal —— 单 checkbox,像 CftModal 一样渲染在
|
|
3540
|
+
// 卡片层(菜单外)。开启 → sidecar.setDood → 重建容器挂 docker.sock + 把 docker CLI 装进容器
|
|
3541
|
+
// 持久卷,下载进度实时显示在弹窗里。DockerCard(Windows 容器)用。
|
|
3542
|
+
function DoodModal({ open, onClose, toastId = "dood-op" }) {
|
|
3543
|
+
const [busy, setBusy] = useState(false);
|
|
3544
|
+
const [err, setErr] = useState("");
|
|
3545
|
+
const [on, setOn] = useState(false);
|
|
3546
|
+
const [line, setLine] = useState("");
|
|
3547
|
+
useEffect(() => {
|
|
3548
|
+
if (!open || !window.cicy?.sidecar?.getDood) return;
|
|
3549
|
+
setErr(""); setLine("");
|
|
3550
|
+
window.cicy.sidecar.getDood().then((r) => setOn(!!r?.dood)).catch(() => {});
|
|
3551
|
+
}, [open]);
|
|
3552
|
+
// 把重建/下载进度实时显示在弹窗(installDockerCli 的每行输出经 op-progress 推来)
|
|
3553
|
+
useEffect(() => {
|
|
3554
|
+
if (!open || !busy || !window.cicy?.sidecar?.onOpProgress) return;
|
|
3555
|
+
return window.cicy.sidecar.onOpProgress((ev) => { if (ev?.message) setLine(String(ev.message)); });
|
|
3556
|
+
}, [open, busy]);
|
|
3557
|
+
const save = async () => {
|
|
3558
|
+
if (busy) return;
|
|
3559
|
+
setBusy(true); setErr(""); setLine("");
|
|
3560
|
+
toast.show({ id: toastId, message: on ? tr("dood.applying", "开启容器 Docker 访问,重建容器中…") : tr("dood.disabling", "关闭中…"), status: "running", progress: undefined });
|
|
3561
|
+
try {
|
|
3562
|
+
const r = await window.cicy.sidecar.setDood(on);
|
|
3563
|
+
if (r?.ok) {
|
|
3564
|
+
onClose && onClose();
|
|
3565
|
+
toast.show({ id: toastId, message: on ? tr("dood.onDone", "容器 Docker 访问已开启") : tr("dood.offDone", "容器 Docker 访问已关闭"), status: "done", ttl: 4000 });
|
|
3566
|
+
} else {
|
|
3567
|
+
setErr(r?.error || tr("dood.failed", "设置失败"));
|
|
3568
|
+
toast.show({ id: toastId, message: tr("dood.failed", "设置失败") + (r?.error ? `: ${r.error}` : ""), status: "error", ttl: 6000 });
|
|
3569
|
+
}
|
|
3570
|
+
} catch (e) { setErr(e?.message || String(e)); toast.show({ id: toastId, message: tr("dood.failed", "设置失败") + `: ${e?.message || e}`, status: "error", ttl: 6000 }); }
|
|
3571
|
+
finally { setBusy(false); }
|
|
3572
|
+
};
|
|
3573
|
+
const setOpenX = (v) => { if (!v) onClose && onClose(); };
|
|
3574
|
+
if (!open) return null;
|
|
3575
|
+
return (
|
|
3576
|
+
<>
|
|
3577
|
+
{createPortal(
|
|
3578
|
+
<div data-id="dood-modal"
|
|
3579
|
+
style={{ position: "fixed", inset: 0, zIndex: 66, display: "flex", alignItems: "center", justifyContent: "center", background: "rgba(0,0,0,.5)" }}
|
|
3580
|
+
onMouseDown={(e) => { if (!busy && e.target === e.currentTarget) setOpenX(false); }}>
|
|
3581
|
+
<div onClick={(e) => e.stopPropagation()}
|
|
3582
|
+
style={{ width: 440, maxWidth: "92vw", background: "var(--card, #1b1d22)", border: "1px solid var(--border, #2c2f36)", borderRadius: 14, padding: 22, boxShadow: "0 20px 60px rgba(0,0,0,.5)" }}>
|
|
3583
|
+
<div style={{ fontSize: 16, fontWeight: 700, marginBottom: 4 }}>{tr("dood.title", "容器内使用 Docker")}</div>
|
|
3584
|
+
<div style={{ fontSize: 12, opacity: .6, marginBottom: 16 }}>{tr("dood.subtitle", "把宿主 Docker + docker 客户端挂进容器,容器内 agent 就能直接跑 docker(秒生效,无需下载)")}</div>
|
|
3585
|
+
<label data-id="dood-toggle-row" style={{ display: "flex", alignItems: "center", gap: 10, cursor: busy ? "default" : "pointer", marginBottom: 12 }}>
|
|
3586
|
+
<input type="checkbox" data-id="dood-toggle" checked={on} disabled={busy}
|
|
3587
|
+
onChange={(e) => setOn(e.target.checked)} />
|
|
3588
|
+
<span style={{ fontSize: 14, fontWeight: 600 }}>{tr("dood.enable", "允许容器内使用 Docker")}</span>
|
|
3589
|
+
</label>
|
|
3590
|
+
{busy && line && <div data-id="dood-progress" style={{ fontSize: 11, opacity: .7, fontFamily: "var(--mono)", wordBreak: "break-all", margin: "0 0 8px", maxHeight: 60, overflow: "hidden" }}>{line}</div>}
|
|
3591
|
+
{err && <div className="error" style={{ marginTop: 8, fontSize: 12 }}>{err}</div>}
|
|
3592
|
+
<div className="modal-actions">
|
|
3593
|
+
<button type="button" className="btn-ghost" disabled={busy} onClick={() => setOpenX(false)}>{tr("common.cancel", "取消")}</button>
|
|
3594
|
+
<button type="button" className="btn-primary" data-id="dood-save" disabled={busy} onClick={save}>{busy ? tr("common.saving", "保存中…") : tr("common.save", "保存")}</button>
|
|
3595
|
+
</div>
|
|
3596
|
+
</div>
|
|
3597
|
+
</div>,
|
|
3598
|
+
document.body,
|
|
3599
|
+
)}
|
|
3600
|
+
</>
|
|
3601
|
+
);
|
|
3602
|
+
}
|
|
3532
3603
|
function TeamAvatar({ avatar, name, teamId, onChanged, size = 34 }) {
|
|
3533
3604
|
const fileRef = useRef(null);
|
|
3534
3605
|
const initial = ((name || "?").trim()[0] || "?").toUpperCase();
|