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.
@@ -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-CgqXmbpX.js"></script>
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
- const token = node.api_token || "";
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
- if (!spec.api_token) {
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
- api_token: spec.api_token !== undefined ? String(spec.api_token || "") : undefined,
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
- // The Docker-版 instance reaches the LLM through the cicy gateway, authenticated
36
- // with the LOCAL 8008 team's api_token (主人: "key local team 8008 的"). 8008
37
- // is started by default on Windows and its token is already minted by the time
38
- // the user opens the Docker card.
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 readLocalApiToken() {
41
+ function readLocalGatewayKey() {
41
42
  try {
42
43
  const p = path.join(os.homedir(), "cicy-ai", "global.json");
43
- return String(JSON.parse(fs.readFileSync(p, "utf8")).api_token || "");
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 token = readLocalApiToken();
133
+ const gwKey = readLocalGatewayKey(); // 8008's team gateway key (sk-cicy-…)
127
134
  const env = { CICY_AI_GATEWAY_LLM_ENDPOINT: GATEWAY_ENDPOINT };
128
- if (token) env.CICY_AI_GATEWAY_LLM_API_KEY = token;
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 tok = await wslDocker.readContainerToken(APP_PORT);
137
- await lt.addTeam({ base_url: `http://127.0.0.1:${APP_PORT}`, name: "Docker cicy-code", ...(tok ? { api_token: tok } : {}) });
138
- } catch { /* best-effort the container itself is up */ }
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 → 重启 the :8009 container, wait for health.
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 → 停止: graceful `docker stop` (data persists in the named volume).
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 own api_token (its volume-persisted global.json) for the
266
- // team registration — the host token is a different credential.
267
- async function readContainerToken(port = 8009, container = "cicy-code-docker") {
268
- try {
269
- // Look the container up by name and read the token from inside it.
270
- const { stdout } = await wslRun(`docker ps --filter "name=${container}" --format '{{.Names}}'`, { timeout: 10000 });
271
- const name = stdout.trim().split("\n")[0];
272
- if (!name) return "";
273
- const r = await wslRun(`docker exec ${name} cat /home/cicy/cicy-ai/global.json`, { timeout: 10000 });
274
- return JSON.parse(r.stdout).api_token || "";
275
- } catch { return ""; }
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 Windows reboot
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
- async function restart({ container = "cicy-code-docker", port = 8009 } = {}) {
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
- await wslRun(`docker restart ${container}`, { timeout: 60000 });
385
- return await docker.waitUntil(() => probeHealth(port), { totalMs: 60000, everyMs: 2000 });
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={(id) => { if (id) openLocalTeam(id); else window.cicy?.tabs?.open?.("http://127.0.0.1:8009", "Docker cicy-code"); }}
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
- {running && (
1822
- <button type="button" data-id="DockerCard-restart" className="bcard__menu-item"
1823
- onClick={() => runOp("restart", () => window.cicy.docker.appRestart(), tr("docker.restarted", "已重启"))}>
1824
- {tr("docker.restart", "重启")}
1825
- </button>
1826
- )}
1827
- <button type="button" data-id="DockerCard-upgrade" className="bcard__menu-item is-accent" onClick={runUpgrade}>
1828
- {tr("docker.upgrade", "升级(拉取最新镜像)")}
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
  )}