cicy-desktop 2.1.95 → 2.1.96
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 +2 -0
- package/src/backends/homepage-react/assets/index-BtVG2Py6.js +365 -0
- package/src/backends/homepage-react/index.html +1 -1
- package/src/backends/sidecar-ipc.js +79 -20
- package/src/i18n/locales/en.json +32 -0
- package/src/i18n/locales/fr.json +32 -0
- package/src/i18n/locales/ja.json +32 -0
- package/src/i18n/locales/zh-CN.json +32 -0
- package/src/sidecar/docker.js +83 -19
- package/workers/render/src/App.jsx +108 -8
- package/src/backends/homepage-react/assets/index-C7gQsfPP.js +0 -365
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
|
|
7
7
|
<link rel="icon" type="image/png" sizes="256x256" href="./favicon-256.png" />
|
|
8
8
|
<title>CiCy Desktop</title>
|
|
9
|
-
<script type="module" crossorigin src="./assets/index-
|
|
9
|
+
<script type="module" crossorigin src="./assets/index-BtVG2Py6.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="./assets/index-CKpaMBKz.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
// along with src/sidecar/installer.js and src/sidecar/wsl.js.)
|
|
14
14
|
|
|
15
15
|
const { ipcMain } = require("electron");
|
|
16
|
+
const fs = require("fs");
|
|
17
|
+
const os = require("os");
|
|
16
18
|
const path = require("path");
|
|
17
19
|
const sidecar = require("../sidecar/cicy-code");
|
|
18
20
|
const docker = require("../sidecar/docker");
|
|
@@ -23,10 +25,23 @@ const PORT = Number(process.env.CICY_CODE_PORT || 8008);
|
|
|
23
25
|
// :8009 (its own container + volume), alongside the native local daemon on
|
|
24
26
|
// :8008. The homepage "Docker cicy-code" card owns its lifecycle; if Docker
|
|
25
27
|
// Desktop is missing the card installs it first (installer downloads to the
|
|
26
|
-
// user's Desktop).
|
|
28
|
+
// user's Desktop). The whole cicy home is persisted to a named volume so the
|
|
29
|
+
// entire container state survives recreation (主人: "把整个 docker 挂出来").
|
|
27
30
|
const APP_PORT = Number(process.env.CICY_DOCKER_APP_PORT || 8009);
|
|
28
31
|
const APP_CONTAINER = process.env.CICY_DOCKER_APP_CONTAINER || "cicy-code-docker";
|
|
29
|
-
const APP_VOLUME = process.env.CICY_DOCKER_APP_VOLUME || "cicy-
|
|
32
|
+
const APP_VOLUME = process.env.CICY_DOCKER_APP_VOLUME || "cicy-team";
|
|
33
|
+
const APP_MOUNT = process.env.CICY_DOCKER_APP_MOUNT || "/home/cicy";
|
|
34
|
+
// The Docker-版 instance reaches the LLM through the cicy gateway, authenticated
|
|
35
|
+
// with the LOCAL 8008 team's api_token (主人: "key 用 local team 8008 的"). 8008
|
|
36
|
+
// is started by default on Windows and its token is already minted by the time
|
|
37
|
+
// the user opens the Docker card.
|
|
38
|
+
const GATEWAY_ENDPOINT = process.env.CICY_AI_GATEWAY_LLM_ENDPOINT || "https://gateway.cicy-ai.com";
|
|
39
|
+
function readLocalApiToken() {
|
|
40
|
+
try {
|
|
41
|
+
const p = path.join(os.homedir(), "cicy-ai", "global.json");
|
|
42
|
+
return String(JSON.parse(fs.readFileSync(p, "utf8")).api_token || "");
|
|
43
|
+
} catch { return ""; }
|
|
44
|
+
}
|
|
30
45
|
|
|
31
46
|
let registered = false;
|
|
32
47
|
|
|
@@ -104,42 +119,86 @@ function register({ sidecarLogPath } = {}) {
|
|
|
104
119
|
}
|
|
105
120
|
});
|
|
106
121
|
|
|
122
|
+
// Common run options for the :8009 instance: its own container/volume, the
|
|
123
|
+
// whole-home mount, and the LLM gateway env keyed by the 8008 team's token.
|
|
124
|
+
const appOpts = () => {
|
|
125
|
+
const token = readLocalApiToken();
|
|
126
|
+
const env = { CICY_AI_GATEWAY_LLM_ENDPOINT: GATEWAY_ENDPOINT };
|
|
127
|
+
if (token) env.CICY_AI_GATEWAY_LLM_API_KEY = token;
|
|
128
|
+
return { port: APP_PORT, container: APP_CONTAINER, volume: APP_VOLUME, mountTarget: APP_MOUNT, env };
|
|
129
|
+
};
|
|
130
|
+
// Register the running :8009 instance as a (custom) team so the card's "打开"
|
|
131
|
+
// reuses the token-injected open/reload flow. addTeam dedups by host:port.
|
|
132
|
+
const registerAppTeam = async () => {
|
|
133
|
+
try {
|
|
134
|
+
const lt = require("./local-teams");
|
|
135
|
+
const tok = await docker.readContainerToken(APP_PORT);
|
|
136
|
+
await lt.addTeam({ base_url: `http://127.0.0.1:${APP_PORT}`, name: "Docker cicy-code", ...(tok ? { api_token: tok } : {}) });
|
|
137
|
+
} catch { /* best-effort — the container itself is up */ }
|
|
138
|
+
};
|
|
139
|
+
|
|
107
140
|
// One-click bootstrap of the Docker-版 instance: install Docker Desktop if
|
|
108
|
-
// missing (installer →
|
|
109
|
-
//
|
|
110
|
-
// 'docker:app-progress' so the card's modal mirrors the
|
|
141
|
+
// missing (installer → Desktop) WHILE downloading the R2 image (→ ~/Downloads)
|
|
142
|
+
// in parallel, import it, start the :8009 container, wait for health. Streams
|
|
143
|
+
// phase/progress on 'docker:app-progress' so the card's modal mirrors the
|
|
144
|
+
// cicy-code 升级 modal. Idempotent + resumable → the modal's 重试 just re-runs.
|
|
111
145
|
ipcMain.handle("docker:app-bootstrap", async (e) => {
|
|
112
146
|
if (process.platform !== "win32") return { ok: false, error: "Docker cicy-code is Windows-only" };
|
|
113
147
|
try {
|
|
114
148
|
const installDest = path.join(docker.desktopDir(), "Docker Desktop Installer.exe");
|
|
115
149
|
const result = await docker.bootstrap({
|
|
116
|
-
|
|
150
|
+
...appOpts(), installDest,
|
|
117
151
|
onProgress: (ev) => { try { e.sender.send("docker:app-progress", ev); } catch {} },
|
|
118
152
|
});
|
|
119
|
-
|
|
120
|
-
// the token-injected open/reload flow. addTeam dedups by host:port.
|
|
121
|
-
if (result && result.ok) {
|
|
122
|
-
try {
|
|
123
|
-
const lt = require("./local-teams");
|
|
124
|
-
const tok = await docker.readContainerToken(APP_PORT);
|
|
125
|
-
await lt.addTeam({
|
|
126
|
-
base_url: `http://127.0.0.1:${APP_PORT}`, name: "Docker cicy-code",
|
|
127
|
-
...(tok ? { api_token: tok } : {}),
|
|
128
|
-
});
|
|
129
|
-
} catch { /* best-effort — the container itself is up */ }
|
|
130
|
-
}
|
|
153
|
+
if (result && result.ok) await registerAppTeam();
|
|
131
154
|
return result;
|
|
132
155
|
} catch (err) {
|
|
133
156
|
return { ok: false, error: err.message };
|
|
134
157
|
}
|
|
135
158
|
});
|
|
136
159
|
|
|
137
|
-
//
|
|
160
|
+
// ⋯ menu → 重启: `docker restart` the :8009 container, wait for health.
|
|
161
|
+
ipcMain.handle("docker:app-restart", async () => {
|
|
162
|
+
try {
|
|
163
|
+
await docker.restart({ container: APP_CONTAINER });
|
|
164
|
+
const ok = await docker.waitUntil(() => docker.probeHealth(APP_PORT), { totalMs: 60000, everyMs: 2000 });
|
|
165
|
+
return { ok };
|
|
166
|
+
} catch (e) { return { ok: false, error: e.message }; }
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ⋯ menu → 停止: graceful `docker stop` (keeps the container; data persists in
|
|
170
|
+
// the named volume). The card's 启动 path re-creates/starts it.
|
|
138
171
|
ipcMain.handle("docker:app-stop", async () => {
|
|
139
|
-
try { await docker.
|
|
172
|
+
try { await docker.stopContainer({ container: APP_CONTAINER }); return { ok: true }; }
|
|
140
173
|
catch (e) { return { ok: false, error: e.message }; }
|
|
141
174
|
});
|
|
142
175
|
|
|
176
|
+
// ⋯ menu → 升级: re-pull the latest R2 image (→ ~/Downloads, resume/skip + bad-
|
|
177
|
+
// partial delete), import it, re-create the :8009 container on the new image.
|
|
178
|
+
// Streams on 'docker:app-progress' so the same modal shows the upgrade.
|
|
179
|
+
ipcMain.handle("docker:app-upgrade", async (e) => {
|
|
180
|
+
if (process.platform !== "win32") return { ok: false, error: "Docker cicy-code is Windows-only" };
|
|
181
|
+
const emit = (ev) => { try { e.sender.send("docker:app-progress", ev); } catch {} };
|
|
182
|
+
try {
|
|
183
|
+
if (!(await docker.dockerOk())) { emit({ phase: "done", status: "error", message: "Docker 未运行" }); return { ok: false, error: "docker_not_running" }; }
|
|
184
|
+
const tmp = await docker.downloadImageTarball({ emit });
|
|
185
|
+
await docker.loadImageFromTarball(tmp, { emit });
|
|
186
|
+
emit({ phase: "image", status: "done", message: "镜像已更新" });
|
|
187
|
+
emit({ phase: "container", status: "running", message: "用新镜像重建容器…" });
|
|
188
|
+
await docker.stop({ container: APP_CONTAINER });
|
|
189
|
+
const child = await docker.start(appOpts());
|
|
190
|
+
if (!child) { emit({ phase: "done", status: "error", message: "容器启动失败" }); return { ok: false, error: "container_start_failed" }; }
|
|
191
|
+
emit({ phase: "health", status: "running", message: "等待就绪…" });
|
|
192
|
+
const ok = await docker.waitUntil(() => docker.probeHealth(APP_PORT), { totalMs: 120000, everyMs: 3000 });
|
|
193
|
+
emit({ phase: "done", status: ok ? "done" : "error", message: ok ? "升级完成 🎉" : "启动了但 :8009 还没响应" });
|
|
194
|
+
if (ok) await registerAppTeam();
|
|
195
|
+
return { ok };
|
|
196
|
+
} catch (err) {
|
|
197
|
+
emit({ phase: "done", status: "error", message: `升级失败:${err.message}` });
|
|
198
|
+
return { ok: false, error: err.message };
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
143
202
|
// Start (or reuse) the cicy-code daemon. probeExisting inside start() reuses
|
|
144
203
|
// a healthy :8008; otherwise it spawns `npx cicy-code` / the Docker container.
|
|
145
204
|
ipcMain.handle("sidecar:start", async () => {
|
package/src/i18n/locales/en.json
CHANGED
|
@@ -121,5 +121,37 @@
|
|
|
121
121
|
"scrollHint": "Please read to the end to continue",
|
|
122
122
|
"menu": "Terms of Use",
|
|
123
123
|
"close": "Close"
|
|
124
|
+
},
|
|
125
|
+
"docker": {
|
|
126
|
+
"title": "Docker cicy-code",
|
|
127
|
+
"install": "Download & Install",
|
|
128
|
+
"start": "Start",
|
|
129
|
+
"running": "Running · :8009",
|
|
130
|
+
"notRunning": "Stopped · click Start",
|
|
131
|
+
"notInstalled": "Docker Desktop not installed",
|
|
132
|
+
"working": "Working…",
|
|
133
|
+
"ready": "Docker cicy-code is ready",
|
|
134
|
+
"failed": "Install failed",
|
|
135
|
+
"upgraded": "Upgraded to latest",
|
|
136
|
+
"upgradeFailed": "Upgrade failed",
|
|
137
|
+
"manage": "Manage Docker cicy-code",
|
|
138
|
+
"restart": "Restart",
|
|
139
|
+
"restarted": "Restarted",
|
|
140
|
+
"restarting": "Restarting…",
|
|
141
|
+
"stop": "Stop",
|
|
142
|
+
"stopped": "Stopped",
|
|
143
|
+
"stoping": "Stopping…",
|
|
144
|
+
"upgrade": "Upgrade (pull latest image)",
|
|
145
|
+
"opFailed": "Operation failed",
|
|
146
|
+
"setupTitle": "Install Docker cicy-code",
|
|
147
|
+
"busy": "In progress",
|
|
148
|
+
"preparing": "Preparing…",
|
|
149
|
+
"installing2": "Installing…",
|
|
150
|
+
"background": "Continue in background"
|
|
151
|
+
},
|
|
152
|
+
"common": {
|
|
153
|
+
"close": "Close",
|
|
154
|
+
"retry": "Retry",
|
|
155
|
+
"done": "Done"
|
|
124
156
|
}
|
|
125
157
|
}
|
package/src/i18n/locales/fr.json
CHANGED
|
@@ -120,5 +120,37 @@
|
|
|
120
120
|
"scrollHint": "Veuillez lire jusqu'au bout pour continuer",
|
|
121
121
|
"menu": "Conditions d'utilisation",
|
|
122
122
|
"close": "Fermer"
|
|
123
|
+
},
|
|
124
|
+
"docker": {
|
|
125
|
+
"title": "Docker cicy-code",
|
|
126
|
+
"install": "Télécharger et installer",
|
|
127
|
+
"start": "Démarrer",
|
|
128
|
+
"running": "En cours · :8009",
|
|
129
|
+
"notRunning": "Arrêté · cliquez sur Démarrer",
|
|
130
|
+
"notInstalled": "Docker Desktop non installé",
|
|
131
|
+
"working": "En cours…",
|
|
132
|
+
"ready": "Docker cicy-code est prêt",
|
|
133
|
+
"failed": "Échec de l'installation",
|
|
134
|
+
"upgraded": "Mis à jour",
|
|
135
|
+
"upgradeFailed": "Échec de la mise à niveau",
|
|
136
|
+
"manage": "Gérer Docker cicy-code",
|
|
137
|
+
"restart": "Redémarrer",
|
|
138
|
+
"restarted": "Redémarré",
|
|
139
|
+
"restarting": "Redémarrage…",
|
|
140
|
+
"stop": "Arrêter",
|
|
141
|
+
"stopped": "Arrêté",
|
|
142
|
+
"stoping": "Arrêt…",
|
|
143
|
+
"upgrade": "Mettre à niveau (dernière image)",
|
|
144
|
+
"opFailed": "Échec de l'opération",
|
|
145
|
+
"setupTitle": "Installer Docker cicy-code",
|
|
146
|
+
"busy": "En cours",
|
|
147
|
+
"preparing": "Préparation…",
|
|
148
|
+
"installing2": "Installation…",
|
|
149
|
+
"background": "Continuer en arrière-plan"
|
|
150
|
+
},
|
|
151
|
+
"common": {
|
|
152
|
+
"close": "Fermer",
|
|
153
|
+
"retry": "Réessayer",
|
|
154
|
+
"done": "Terminé"
|
|
123
155
|
}
|
|
124
156
|
}
|
package/src/i18n/locales/ja.json
CHANGED
|
@@ -120,5 +120,37 @@
|
|
|
120
120
|
"scrollHint": "続行するには最後までお読みください",
|
|
121
121
|
"menu": "利用規約",
|
|
122
122
|
"close": "閉じる"
|
|
123
|
+
},
|
|
124
|
+
"docker": {
|
|
125
|
+
"title": "Docker cicy-code",
|
|
126
|
+
"install": "ダウンロードしてインストール",
|
|
127
|
+
"start": "起動",
|
|
128
|
+
"running": "実行中 · :8009",
|
|
129
|
+
"notRunning": "停止中 · 「起動」をクリック",
|
|
130
|
+
"notInstalled": "Docker Desktop 未インストール",
|
|
131
|
+
"working": "処理中…",
|
|
132
|
+
"ready": "Docker cicy-code の準備完了",
|
|
133
|
+
"failed": "インストール失敗",
|
|
134
|
+
"upgraded": "最新へ更新しました",
|
|
135
|
+
"upgradeFailed": "アップグレード失敗",
|
|
136
|
+
"manage": "Docker cicy-code を管理",
|
|
137
|
+
"restart": "再起動",
|
|
138
|
+
"restarted": "再起動しました",
|
|
139
|
+
"restarting": "再起動中…",
|
|
140
|
+
"stop": "停止",
|
|
141
|
+
"stopped": "停止しました",
|
|
142
|
+
"stoping": "停止中…",
|
|
143
|
+
"upgrade": "アップグレード(最新イメージ取得)",
|
|
144
|
+
"opFailed": "操作に失敗しました",
|
|
145
|
+
"setupTitle": "Docker cicy-code をインストール",
|
|
146
|
+
"busy": "進行中",
|
|
147
|
+
"preparing": "準備中…",
|
|
148
|
+
"installing2": "インストール中…",
|
|
149
|
+
"background": "バックグラウンドで継続"
|
|
150
|
+
},
|
|
151
|
+
"common": {
|
|
152
|
+
"close": "閉じる",
|
|
153
|
+
"retry": "再試行",
|
|
154
|
+
"done": "完了"
|
|
123
155
|
}
|
|
124
156
|
}
|
|
@@ -121,5 +121,37 @@
|
|
|
121
121
|
"scrollHint": "请阅读至底部以继续",
|
|
122
122
|
"menu": "用户协议",
|
|
123
123
|
"close": "关闭"
|
|
124
|
+
},
|
|
125
|
+
"docker": {
|
|
126
|
+
"title": "Docker cicy-code",
|
|
127
|
+
"install": "下载安装",
|
|
128
|
+
"start": "启动",
|
|
129
|
+
"running": "运行中 · :8009",
|
|
130
|
+
"notRunning": "未启动 · 点「启动」",
|
|
131
|
+
"notInstalled": "Docker Desktop 未安装",
|
|
132
|
+
"working": "处理中…",
|
|
133
|
+
"ready": "Docker cicy-code 已就绪",
|
|
134
|
+
"failed": "安装失败",
|
|
135
|
+
"upgraded": "已升级到最新",
|
|
136
|
+
"upgradeFailed": "升级失败",
|
|
137
|
+
"manage": "管理 Docker cicy-code",
|
|
138
|
+
"restart": "重启",
|
|
139
|
+
"restarted": "已重启",
|
|
140
|
+
"restarting": "重启中…",
|
|
141
|
+
"stop": "停止",
|
|
142
|
+
"stopped": "已停止",
|
|
143
|
+
"stoping": "停止中…",
|
|
144
|
+
"upgrade": "升级(拉取最新镜像)",
|
|
145
|
+
"opFailed": "操作失败",
|
|
146
|
+
"setupTitle": "安装 Docker cicy-code",
|
|
147
|
+
"busy": "进行中",
|
|
148
|
+
"preparing": "准备中…",
|
|
149
|
+
"installing2": "安装进行中…",
|
|
150
|
+
"background": "在后台继续"
|
|
151
|
+
},
|
|
152
|
+
"common": {
|
|
153
|
+
"close": "关闭",
|
|
154
|
+
"retry": "重试",
|
|
155
|
+
"done": "完成"
|
|
124
156
|
}
|
|
125
157
|
}
|
package/src/sidecar/docker.js
CHANGED
|
@@ -157,13 +157,23 @@ function headSize(url, hops = 5) {
|
|
|
157
157
|
// Download `url`→`dest` but: SKIP if the file is already complete, RESUME if it's
|
|
158
158
|
// a partial, retry with progress, fall back to `mirror`. This is the core of the
|
|
159
159
|
// user's "下载了就不重复下载 / 步骤走过的不要再走".
|
|
160
|
-
async function ensureDownloaded(url, dest, mirror, { emit, phase, label } = {}) {
|
|
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
164
|
if (expected > 0 && have === expected) {
|
|
164
165
|
emit && emit({ phase, status: "skip", message: `${label}:已下载,跳过`, progress: 100 });
|
|
165
166
|
return dest;
|
|
166
167
|
}
|
|
168
|
+
// A partial left by a PREVIOUS, interrupted/restarted session can be corrupt;
|
|
169
|
+
// when freshOnIncomplete, delete it and start clean rather than range-resuming
|
|
170
|
+
// onto a possibly-bad file (主人: 下载被重启打断的残包要删掉重下). Within THIS
|
|
171
|
+
// session, retries still resume the part we wrote ourselves.
|
|
172
|
+
if (freshOnIncomplete && have > 0 && expected > 0 && have !== expected) {
|
|
173
|
+
try { fs.unlinkSync(dest); } catch {}
|
|
174
|
+
have = 0;
|
|
175
|
+
emit && emit({ phase, status: "running", message: `${label}:删除不完整的旧包,重新下载`, progress: 0 });
|
|
176
|
+
}
|
|
167
177
|
const sources = mirror ? [url, mirror] : [url];
|
|
168
178
|
let lastPct = -1; // throttle: chunks arrive dozens/s — only emit on whole-percent change
|
|
169
179
|
const attempted = withRetry(async (attempt) => {
|
|
@@ -226,11 +236,28 @@ function probeHealth(port = 8008, timeoutMs = 2500) {
|
|
|
226
236
|
});
|
|
227
237
|
}
|
|
228
238
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
239
|
+
// The R2 image tarball downloads to ~/Downloads (主人: docker image 下到
|
|
240
|
+
// ~/Downloads — visible, like the Docker installer on the Desktop). STABLE name
|
|
241
|
+
// (no pid) so a re-run reuses an existing partial/complete file (resume-friendly
|
|
242
|
+
// on a flaky network).
|
|
243
|
+
function imageTarballPath() {
|
|
244
|
+
const dir = path.join(process.env["USERPROFILE"] || os.homedir(), "Downloads");
|
|
245
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
246
|
+
return path.join(dir, "cicy-code-latest.tar.gz");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Download the R2 base-env image tarball (no docker needed yet). Split out of
|
|
250
|
+
// loadImage so bootstrap can run this IN PARALLEL with the Docker Desktop
|
|
251
|
+
// install (主人: 装 Docker 的同时下载 R2 镜像). Returns the tarball path.
|
|
252
|
+
async function downloadImageTarball({ emit } = {}) {
|
|
253
|
+
const dest = imageTarballPath();
|
|
254
|
+
await ensureDownloaded(R2_TARBALL, dest, null, { emit, phase: "image", label: "下载镜像", freshOnIncomplete: true });
|
|
255
|
+
return dest;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// `docker load` an already-downloaded tarball + re-tag to IMAGE. Needs the
|
|
259
|
+
// daemon up, so this runs AFTER Docker is ready (主人: 再导入 docker).
|
|
260
|
+
async function loadImageFromTarball(tmp, { emit } = {}) {
|
|
234
261
|
emit && emit({ phase: "image", status: "running", message: "docker load…", progress: 100 });
|
|
235
262
|
console.log(`[docker-sidecar] docker load…`);
|
|
236
263
|
const { stdout } = await run(["load", "-i", tmp], { timeout: 300000 });
|
|
@@ -242,9 +269,23 @@ async function loadImage({ emit } = {}) {
|
|
|
242
269
|
try { await run(["tag", m[1], IMAGE]); console.log(`[docker-sidecar] tagged ${m[1]} -> ${IMAGE}`); }
|
|
243
270
|
catch (e) { console.warn(`[docker-sidecar] re-tag failed: ${e.message}`); }
|
|
244
271
|
}
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
|
|
272
|
+
// Keep the tarball in ~/Downloads (主人: 下到 Downloads) — it's a visible,
|
|
273
|
+
// resume-friendly cache; imagePresent() gates re-entry so we don't re-load it.
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Download-then-import in one shot (sequential). Used when Docker is already up.
|
|
277
|
+
async function loadImage({ emit } = {}) {
|
|
278
|
+
const tmp = await downloadImageTarball({ emit });
|
|
279
|
+
await loadImageFromTarball(tmp, { emit });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// `docker restart` / graceful `docker stop` for a given container — the Docker-版
|
|
283
|
+
// card's ⋯ menu (重启 / 停止), mirroring the 8008 local card's lifecycle menu.
|
|
284
|
+
async function restart({ container = CONTAINER } = {}) {
|
|
285
|
+
await run(["restart", container], { timeout: 60000 });
|
|
286
|
+
}
|
|
287
|
+
async function stopContainer({ container = CONTAINER } = {}) {
|
|
288
|
+
try { await run(["stop", container], { timeout: 30000 }); } catch {}
|
|
248
289
|
}
|
|
249
290
|
|
|
250
291
|
async function checkStatus() {
|
|
@@ -264,7 +305,7 @@ function desktopDir() {
|
|
|
264
305
|
// Docker Desktop). `container`/`volume` are parameterized so a SECOND instance
|
|
265
306
|
// (the Docker-版 cicy-code on :8009) can run alongside the native local one
|
|
266
307
|
// without a name/volume collision.
|
|
267
|
-
async function start({ port = 8008, container = CONTAINER, volume = VOLUME } = {}) {
|
|
308
|
+
async function start({ port = 8008, container = CONTAINER, volume = VOLUME, mountTarget = "/home/cicy/cicy-ai", env = {} } = {}) {
|
|
268
309
|
// Something already serves a healthy cicy-code on :port (a legacy-named
|
|
269
310
|
// container auto-revived by `--restart unless-stopped`, a manual run…).
|
|
270
311
|
// Adopt it — `docker run` would just lose the port-bind fight.
|
|
@@ -283,14 +324,23 @@ async function start({ port = 8008, container = CONTAINER, volume = VOLUME } = {
|
|
|
283
324
|
// Replace any stale container of the same name.
|
|
284
325
|
try { await run(["rm", "-f", container]); } catch {}
|
|
285
326
|
|
|
327
|
+
// mountTarget defaults to /home/cicy/cicy-ai (legacy local-team layout); the
|
|
328
|
+
// Docker-版 instance passes /home/cicy to persist the WHOLE cicy home (主人:
|
|
329
|
+
// "把整个 docker 挂出来" — everything mutable lives under /home/cicy: global.json,
|
|
330
|
+
// db, agents, files, the npm-installed cicy-code itself).
|
|
286
331
|
const args = [
|
|
287
332
|
"run", "-d", "--name", container, "--restart", "unless-stopped",
|
|
288
333
|
"-p", `${port}:8008`,
|
|
289
|
-
"-v", `${volume}
|
|
334
|
+
"-v", `${volume}:${mountTarget}`,
|
|
290
335
|
];
|
|
291
336
|
for (const k of PASS_ENV) {
|
|
292
337
|
if (process.env[k]) args.push("-e", `${k}=${process.env[k]}`);
|
|
293
338
|
}
|
|
339
|
+
// Caller-supplied env (e.g. the LLM gateway endpoint + key for the Docker-版
|
|
340
|
+
// instance, which bills through the 8008 local team's token).
|
|
341
|
+
for (const [k, v] of Object.entries(env || {})) {
|
|
342
|
+
if (v != null && v !== "") args.push("-e", `${k}=${v}`);
|
|
343
|
+
}
|
|
294
344
|
args.push(IMAGE);
|
|
295
345
|
|
|
296
346
|
const { stdout } = await run(args, { timeout: 60000 });
|
|
@@ -314,7 +364,7 @@ async function installDocker({ emit, dest } = {}) {
|
|
|
314
364
|
try { fs.mkdirSync(path.dirname(target), { recursive: true }); } catch {}
|
|
315
365
|
e({ phase: "install-docker", status: "running", message: "下载 Docker Desktop 安装包…", progress: 0 });
|
|
316
366
|
await ensureDownloaded(DOCKER_DESKTOP_URL, target, DOCKER_DESKTOP_MIRROR, {
|
|
317
|
-
emit, phase: "install-docker", label: "下载 Docker Desktop",
|
|
367
|
+
emit, phase: "install-docker", label: "下载 Docker Desktop", freshOnIncomplete: true,
|
|
318
368
|
});
|
|
319
369
|
e({ phase: "install-docker", status: "running", message: "安装 Docker Desktop(请在弹出的授权框点「是」,装完可能需重启)…" });
|
|
320
370
|
await new Promise((resolve) => {
|
|
@@ -333,16 +383,22 @@ async function installDocker({ emit, dest } = {}) {
|
|
|
333
383
|
// start the container → wait for :8008. Every step CHECKS first and SKIPS if
|
|
334
384
|
// already done, emits coarse phase events + byte progress, and the downloads
|
|
335
385
|
// resume. Safe to call again after a failure — it picks up where it left off.
|
|
336
|
-
async function bootstrap({ onProgress, port = 8008, container = CONTAINER, volume = VOLUME, installDest } = {}) {
|
|
386
|
+
async function bootstrap({ onProgress, port = 8008, container = CONTAINER, volume = VOLUME, mountTarget, env, installDest } = {}) {
|
|
337
387
|
const emit = (ev) => { try { onProgress && onProgress(ev); } catch {} };
|
|
338
388
|
|
|
389
|
+
// Decide up-front whether the base image needs fetching, so we can download
|
|
390
|
+
// the R2 tarball IN PARALLEL with the Docker Desktop install below.
|
|
391
|
+
const needImage = !(await imagePresent());
|
|
392
|
+
let imgDl = null; // Promise<tarballPath|null> when downloading in parallel
|
|
393
|
+
|
|
339
394
|
// 1) Docker present?
|
|
340
395
|
if (await dockerOk()) {
|
|
341
396
|
emit({ phase: "install-docker", status: "skip", message: "Docker 已安装,跳过" });
|
|
342
397
|
} else if (dockerDesktopExe()) {
|
|
343
398
|
// Installed but the daemon is down — just launch Docker Desktop, never
|
|
344
|
-
// re-download/re-run the installer (
|
|
399
|
+
// re-download/re-run the installer (主人: 装了就别再下 Docker Desktop 了).
|
|
345
400
|
emit({ phase: "install-docker", status: "running", message: "Docker 已安装,正在启动 Docker Desktop…" });
|
|
401
|
+
if (needImage) imgDl = downloadImageTarball({ emit }).catch((e) => { emit({ phase: "image", status: "error", message: `镜像下载失败:${e.message}` }); return null; });
|
|
346
402
|
startDockerDesktop();
|
|
347
403
|
const up = await waitUntil(dockerOk, { totalMs: 300000, everyMs: 5000 });
|
|
348
404
|
if (!up) {
|
|
@@ -351,6 +407,9 @@ async function bootstrap({ onProgress, port = 8008, container = CONTAINER, volum
|
|
|
351
407
|
}
|
|
352
408
|
emit({ phase: "install-docker", status: "done", message: "Docker 就绪" });
|
|
353
409
|
} else {
|
|
410
|
+
// Docker missing → download the R2 image IN PARALLEL with the installer
|
|
411
|
+
// running + the daemon coming up (主人: 装 Docker 的同时下载 R2 镜像).
|
|
412
|
+
if (needImage) imgDl = downloadImageTarball({ emit }).catch((e) => { emit({ phase: "image", status: "error", message: `镜像下载失败:${e.message}` }); return null; });
|
|
354
413
|
await installDocker({ emit, dest: installDest });
|
|
355
414
|
emit({ phase: "install-docker", status: "running", message: "等待 Docker 启动(如需授权/重启,完成后会自动继续)…" });
|
|
356
415
|
const up = await waitUntil(dockerOk, { totalMs: 900000, everyMs: 6000 });
|
|
@@ -361,12 +420,16 @@ async function bootstrap({ onProgress, port = 8008, container = CONTAINER, volum
|
|
|
361
420
|
emit({ phase: "install-docker", status: "done", message: "Docker 就绪" });
|
|
362
421
|
}
|
|
363
422
|
|
|
364
|
-
// 2) Base image
|
|
365
|
-
|
|
423
|
+
// 2) Base image — import it (docker load). If pre-downloaded in parallel, just
|
|
424
|
+
// load; otherwise (re)download now. Downloads resume/skip + delete bad partials.
|
|
425
|
+
if (!needImage) {
|
|
366
426
|
emit({ phase: "image", status: "skip", message: "镜像已就绪,跳过" });
|
|
367
427
|
} else {
|
|
368
428
|
try {
|
|
369
|
-
await
|
|
429
|
+
let tmp = imgDl ? await imgDl : null;
|
|
430
|
+
if (!tmp) tmp = await downloadImageTarball({ emit }); // not pre-dl'd / parallel dl failed → fetch now
|
|
431
|
+
emit({ phase: "image", status: "running", message: "导入 Docker 镜像…", progress: 100 });
|
|
432
|
+
await loadImageFromTarball(tmp, { emit });
|
|
370
433
|
emit({ phase: "image", status: "done", message: "镜像就绪" });
|
|
371
434
|
} catch (e) {
|
|
372
435
|
emit({ phase: "image", status: "error", message: `镜像加载失败:${e.message}(点重试,下载会续传)` });
|
|
@@ -381,7 +444,7 @@ async function bootstrap({ onProgress, port = 8008, container = CONTAINER, volum
|
|
|
381
444
|
} else {
|
|
382
445
|
emit({ phase: "container", status: "running", message: "启动 cicy-code 容器…" });
|
|
383
446
|
let child = null;
|
|
384
|
-
try { child = await start({ port, container, volume }); }
|
|
447
|
+
try { child = await start({ port, container, volume, mountTarget, env }); }
|
|
385
448
|
catch (e) { emit({ phase: "container", status: "error", message: `容器启动失败:${e.message}` }); return { ok: false, reason: "container_start_failed" }; }
|
|
386
449
|
if (!child) {
|
|
387
450
|
emit({ phase: "container", status: "error", message: "容器启动失败" });
|
|
@@ -397,7 +460,8 @@ async function bootstrap({ onProgress, port = 8008, container = CONTAINER, volum
|
|
|
397
460
|
}
|
|
398
461
|
|
|
399
462
|
module.exports = {
|
|
400
|
-
start, stop,
|
|
463
|
+
start, stop, stopContainer, restart, checkStatus, loadImage, loadImageFromTarball,
|
|
464
|
+
downloadImageTarball, imagePresent, dockerOk, installDocker,
|
|
401
465
|
bootstrap, probeHealth, readContainerToken, dockerDesktopExe, desktopDir,
|
|
402
466
|
// platform-agnostic download/retry primitives, reused by native.js
|
|
403
467
|
ensureDownloaded, withRetry, waitUntil, run,
|