cicy-desktop 2.1.258 → 2.1.259

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cicy-desktop",
3
- "version": "2.1.258",
3
+ "version": "2.1.259",
4
4
  "description": "CiCy - AI-powered operating system browser",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -52,10 +52,12 @@ const APP_PORT = Number(process.env.CICY_DOCKER_APP_PORT || 8008);
52
52
  // (含 port)区分。
53
53
  const APP_CONTAINER = process.env.CICY_DOCKER_APP_CONTAINER || `cicy-code-docker-${APP_PORT}`;
54
54
  const APP_VOLUME = process.env.CICY_DOCKER_APP_VOLUME || `cicy-team-${APP_PORT}`;
55
+ const KOUBO_PORT = 8770;
55
56
 
56
57
  // 用户自定义的额外发布端口(除 :8008 外,给容器内 agent 服务从 Windows 直达用)。
57
58
  // 持久化在 userData/docker-ports.json,bootstrap/recreate 都会带上 -p。lazy 取
58
- // 路径(app 未 ready 时不取)
59
+ // 路径(app 未 ready 时不取)。口播在这个容器内按需启动,因此容器创建时必须
60
+ // 预先发布 :8770;服务未启动时映射保持空闲,不会额外启动 koubo。
59
61
  function portsFile() { return path.join(require("electron").app.getPath("userData"), "docker-ports.json"); }
60
62
  function sanitizePorts(arr) {
61
63
  const out = [], seen = new Set([APP_PORT, 8008]);
@@ -67,7 +69,12 @@ function sanitizePorts(arr) {
67
69
  return out;
68
70
  }
69
71
  function readExtraPorts() {
70
- try { const j = JSON.parse(fs.readFileSync(portsFile(), "utf8")); return sanitizePorts(j && j.ports); } catch { return []; }
72
+ try {
73
+ const j = JSON.parse(fs.readFileSync(portsFile(), "utf8"));
74
+ return sanitizePorts([KOUBO_PORT, ...((j && Array.isArray(j.ports)) ? j.ports : [])]);
75
+ } catch {
76
+ return sanitizePorts([KOUBO_PORT]);
77
+ }
71
78
  }
72
79
  function writeExtraPorts(ports) {
73
80
  try { fs.writeFileSync(portsFile(), JSON.stringify({ ports: sanitizePorts(ports) }, null, 2), "utf8"); } catch (e) { log.warn("[docker-ports] write failed:", e.message); }
@@ -37,6 +37,32 @@ const ROOTFS_URL = process.env.CICY_WSL_ROOTFS_URL ||
37
37
  "https://cicy-1372193042-cn.oss-cn-shanghai.aliyuncs.com/rootfs/cicy-wsl-latest.tar.gz";
38
38
 
39
39
  function rootfsPath() { return path.join(docker.downloadsDir(), "cicy-wsl-rootfs.tar.gz"); }
40
+ function shellQuote(value) { return `'${String(value).replace(/'/g, "'\\''")}'`; }
41
+
42
+ // `docker info`/Windows device detection is not enough: the NVIDIA Container
43
+ // Runtime may be missing even when Windows itself has an NVIDIA card. Prove that
44
+ // this Docker Engine can actually expose the GPU to the workload image. Cache
45
+ // briefly so a bootstrap/recreate flow does not launch repeated probe containers.
46
+ const gpuProbeCache = new Map();
47
+ async function dockerGpuAvailable({ image = IMAGE, force = false } = {}) {
48
+ const cacheKey = String(image);
49
+ const cached = gpuProbeCache.get(cacheKey);
50
+ if (!force && cached && Date.now() - cached.ts < 30000) return cached.value;
51
+
52
+ let value = false;
53
+ try {
54
+ await wslRun(
55
+ `docker run --rm --gpus all --entrypoint sh ${shellQuote(image)} -lc ` +
56
+ shellQuote("if command -v nvidia-smi >/dev/null 2>&1; then nvidia-smi -L >/dev/null; else test -c /dev/nvidiactl; fi"),
57
+ { timeout: 30000 }
58
+ );
59
+ value = true;
60
+ } catch (e) {
61
+ log.info(`[wsl-docker] NVIDIA GPU unavailable for ${cacheKey}: ${String(e.stderr || e.message || "").trim().slice(-300)}`);
62
+ }
63
+ gpuProbeCache.set(cacheKey, { ts: Date.now(), value });
64
+ return value;
65
+ }
40
66
  // WSL2 kernel update package (the small ~17MB MSI behind aka.ms/wsl2kernel).
41
67
  const KERNEL_MSI_URL = process.env.CICY_WSL_KERNEL_URL || "https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi";
42
68
 
@@ -251,7 +277,45 @@ function wslTerminate() {
251
277
  });
252
278
  }
253
279
 
254
- async function installDistro({ emit } = {}) {
280
+ function largestFixedDrive() {
281
+ return new Promise((resolve) => {
282
+ const script = [
283
+ "Get-CimInstance Win32_LogicalDisk -Filter 'DriveType=3'",
284
+ "| Where-Object { $_.FreeSpace -gt 0 }",
285
+ "| Sort-Object FreeSpace -Descending",
286
+ "| Select-Object -First 1 -ExpandProperty DeviceID",
287
+ ].join(" ");
288
+ execFile(
289
+ "powershell",
290
+ ["-NoProfile", "-NonInteractive", "-Command", script],
291
+ { timeout: 15000, windowsHide: true },
292
+ (err, stdout) => {
293
+ const drive = String(stdout || "").trim().toUpperCase();
294
+ resolve(!err && /^[A-Z]:$/.test(drive) ? drive : "");
295
+ },
296
+ );
297
+ });
298
+ }
299
+
300
+ async function resolveWslInstallDir(requested) {
301
+ const fallback = path.join(
302
+ process.env["LOCALAPPDATA"] || path.join(os.homedir(), "AppData", "Local"),
303
+ "cicy-code-wsl",
304
+ );
305
+ const raw = String(requested || process.env.CICY_WSL_INSTALL_DIR || "").trim();
306
+ if (!raw) {
307
+ const drive = await largestFixedDrive();
308
+ return drive ? path.win32.join(`${drive}\\`, "CiCy", "cicy-code-wsl") : fallback;
309
+ }
310
+ // This runs on Windows, but win32.isAbsolute also makes the validation
311
+ // deterministic in CI on other platforms.
312
+ if (!path.win32.isAbsolute(raw)) {
313
+ throw new Error("WSL 安装目录必须是绝对路径,例如 D:\\CiCy\\wsl");
314
+ }
315
+ return path.win32.normalize(raw);
316
+ }
317
+
318
+ async function installDistro({ emit, installDir: requestedInstallDir } = {}) {
255
319
  // 1) Download the PRE-BAKED rootfs (Ubuntu+Docker+image baked in, ~122MB) with
256
320
  // a real progress bar. curl is ~10× faster than node's downloader on OSS.
257
321
  const dest = rootfsPath();
@@ -279,7 +343,7 @@ async function installDistro({ emit } = {}) {
279
343
  // distro to v2 — we do NOT run `--set-default-version` (that would change the
280
344
  // user's global default). The WSL2 kernel is shared; we install it ONLY when
281
345
  // import actually fails for lack of it (never downgrade an existing kernel).
282
- const installDir = path.join(process.env["LOCALAPPDATA"] || path.join(os.homedir(), "AppData", "Local"), "cicy-code-wsl");
346
+ const installDir = await resolveWslInstallDir(requestedInstallDir);
283
347
  try { fs.mkdirSync(installDir, { recursive: true }); } catch {}
284
348
  emit && emit({ phase: "container", status: "running", message: `$ wsl --import ${DISTRO} "${installDir}" "${dest}" --version 2(1-4 分钟,无实时进度,请耐心)` });
285
349
  try {
@@ -601,7 +665,7 @@ function publishArgs(port, extraPorts = []) {
601
665
  return args;
602
666
  }
603
667
 
604
- async function runContainer({ port = 8008, container = "cicy-code-docker", volume = "cicy-team-8008", env = {}, extraPorts = [], dockerSock = false, emit } = {}) {
668
+ async function runContainer({ port = 8008, container = "cicy-code-docker", volume = "cicy-team-8008", env = {}, extraPorts = [], dockerSock = false, gpu = false, gpuProbeImage = IMAGE, emit } = {}) {
605
669
  // 每次容器"启动"(含已在跑被 adopt)都确保桌面快捷方式存在 —— 不存在就建,坏了就修。
606
670
  if (await probeHealth(port)) { ensureDesktopShortcut(volume, port).catch(() => {}); return { adopted: true }; }
607
671
  // Replace any stale same-named container.
@@ -626,7 +690,14 @@ async function runContainer({ port = 8008, container = "cicy-code-docker", volum
626
690
  let doodOn = dockerSock;
627
691
  if (!doodOn) { try { doodOn = require("./cicy-code").isDood(); } catch {} }
628
692
  const sockArg = doodOn ? await dockerSockMountArg() : "";
629
- 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}`;
693
+ const gpuEnabled = (gpu === true || gpu === "auto")
694
+ ? await dockerGpuAvailable({ image: gpuProbeImage })
695
+ : false;
696
+ const gpuArg = gpuEnabled ? "--gpus all" : "";
697
+ if (gpu && !gpuEnabled && emit) {
698
+ emit({ phase: "container", status: "running", message: "未检测到可用的 NVIDIA Container Runtime,按 CPU 模式启动" });
699
+ }
700
+ const cmd = `docker run -d --name ${container} --restart unless-stopped ${gpuArg} --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}`;
630
701
  emit && emit({ phase: "container", status: "running", message: `$ ${cmd.length > 220 ? cmd.slice(0, 220) + " …" : cmd}` });
631
702
  await wslRun(cmd, { timeout: 60000 }); // 失败时 err.stderr 带 docker 真错误 → _bootstrap 的 errTail 显示
632
703
  ensureDesktopShortcut(volume, port).catch(() => {});
@@ -791,7 +862,7 @@ async function bootstrap(opts = {}) {
791
862
  // 注意: 默认容器/卷名保持 cicy-team / cicy-code-docker(回退实测"现在不行了"的改动)。
792
863
  // live 路径(docker:app-bootstrap)始终传显式 APP_*(cicy-team-8008)名,不靠这里的默认;
793
864
  // 改默认会让既有 cicy-team 卷的装机对不上 → 退回原值。
794
- async function _bootstrap({ onProgress, port = 8008, container = "cicy-code-docker", volume = "cicy-team", env = {}, extraPorts = [], dockerSock = false } = {}) {
865
+ async function _bootstrap({ onProgress, port = 8008, container = "cicy-code-docker", volume = "cicy-team", env = {}, extraPorts = [], dockerSock = false, gpu = false, gpuProbeImage = IMAGE, wslInstallDir } = {}) {
795
866
  const emit = (ev) => { try { onProgress && onProgress(ev); } catch {} };
796
867
 
797
868
  // Structured, PERSISTED trace of the whole run (electron-log → main.log) so a
@@ -859,7 +930,7 @@ async function _bootstrap({ onProgress, port = 8008, container = "cicy-code-dock
859
930
  // 2) Ubuntu distro
860
931
  begin("ensure-distro");
861
932
  if (!(await distroInstalled())) {
862
- try { await installDistro({ emit }); } catch (e) { fail("distro_install_failed", e.message); emit({ phase: "install-docker", status: "error", message: `Ubuntu 安装失败:${e.message}(点重试)${errTail(e)}` }); finish(false, "distro_install_failed"); return { ok: false, reason: "distro_install_failed" }; }
933
+ try { await installDistro({ emit, installDir: wslInstallDir }); } catch (e) { fail("distro_install_failed", e.message); emit({ phase: "install-docker", status: "error", message: `Ubuntu 安装失败:${e.message}(点重试)${errTail(e)}` }); finish(false, "distro_install_failed"); return { ok: false, reason: "distro_install_failed" }; }
863
934
  const t0 = Date.now();
864
935
  const ok = await docker.waitUntil(() => distroInstalled(), { totalMs: 600000, everyMs: 5000, onTick: () => emit({ phase: "install-docker", status: "running", message: `正在下载/注册 Ubuntu…(已 ${Math.round((Date.now() - t0) / 1000)}s,首次较慢请耐心)` }) });
865
936
  if (!ok) { fail("distro_not_ready"); emit({ phase: "install-docker", status: "error", message: "Ubuntu 还没装好——稍等或点「重试」" }); finish(false, "distro_not_ready"); return { ok: false, reason: "distro_not_ready" }; }
@@ -902,7 +973,7 @@ async function _bootstrap({ onProgress, port = 8008, container = "cicy-code-dock
902
973
  begin("run-container");
903
974
  if (!(await probeHealth(port))) {
904
975
  emit({ phase: "container", status: "running", message: "启动 cicy-code 服务…" });
905
- try { await runContainer({ port, container, volume, env, extraPorts, dockerSock, emit }); }
976
+ try { await runContainer({ port, container, volume, env, extraPorts, dockerSock, gpu, gpuProbeImage, emit }); }
906
977
  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" }; }
907
978
  done();
908
979
  } else done(true);
@@ -1055,14 +1126,14 @@ async function dockerRestart({ container = "cicy-code-docker-8008" } = {}) {
1055
1126
  // 重建容器:docker rm -f 旧容器 + docker run 新容器(用新 env,如新的 docker team 网关
1056
1127
  // key)。**保留 volume**(数据/api_token/deviceId 不丢),只是换掉容器本身 + env。
1057
1128
  // 破坏性(短暂中断 + 换 key)→ 调用方要 confirm。
1058
- async function recreate({ onProgress, port = 8008, container = "cicy-code-docker-8008", volume = "cicy-team-8008", env = {}, extraPorts = [], dockerSock = false } = {}) {
1129
+ async function recreate({ onProgress, port = 8008, container = "cicy-code-docker-8008", volume = "cicy-team-8008", env = {}, extraPorts = [], dockerSock = false, gpu = false, gpuProbeImage = IMAGE } = {}) {
1059
1130
  const emit = (ev) => { try { onProgress && onProgress(ev); } catch {} };
1060
1131
  // 重建 = 用最新镜像重建。OSS 有更新版先刷新(非破坏性,不删发行版/volume),再 rm + run。
1061
1132
  try { await ensureFreshImage({ emit }); } catch (e) { emit({ phase: "image", status: "running", message: `镜像刷新跳过(${e.message}),用现有镜像重建` }); }
1062
1133
  // 强删占用该端口的**任何**容器(含老名字 cicy-code-docker)+ 目标容器 —— 否则
1063
1134
  // runContainer 开头的 probeHealth 看到旧容器还健康会 adopt 它、不重建,key 就换不了。
1064
1135
  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 {}
1065
- const r = await runContainer({ port, container, volume, env, extraPorts, dockerSock, emit });
1136
+ const r = await runContainer({ port, container, volume, env, extraPorts, dockerSock, gpu, gpuProbeImage, emit });
1066
1137
  try { await ensureDesktopShortcut(volume, port); } catch {}
1067
1138
  // 等 :8008 真正起来再返回——否则 docker run 一返回卡片就探测「没运行」,显示「启动」让
1068
1139
  // 用户再点一下(实测:端口保存后重建,drawer 关了还要手动点启动)。
@@ -1085,13 +1156,13 @@ function unregisterDistro() {
1085
1156
  // downloader, which copes with the flaky CN DNS that bare curl can't) is the
1086
1157
  // only reliable CN update path. This RESETS the distro: the cicy-team volume is
1087
1158
  // re-created and the instance re-seeds (new token) on next boot.
1088
- async function upgrade({ onProgress, port = 8008, container = "cicy-code-docker", volume = "cicy-team", env = {} } = {}) {
1159
+ async function upgrade({ onProgress, port = 8008, container = "cicy-code-docker", volume = "cicy-team", env = {}, extraPorts = [], dockerSock = false, gpu = false, gpuProbeImage = IMAGE, wslInstallDir } = {}) {
1089
1160
  const emit = (ev) => { try { onProgress && onProgress(ev); } catch {} };
1090
1161
  emit({ phase: "install-docker", status: "running", message: "升级 = 拉取最新运行环境并重装(会重置容器数据)…" });
1091
1162
  try { await stop({ container }); } catch {}
1092
1163
  try { await unregisterDistro(); } catch {}
1093
1164
  // Reuse the robust one-shot install flow (download → import → dockerd → run).
1094
- return await _bootstrap({ onProgress, port, container, volume, env, dockerSock });
1165
+ return await _bootstrap({ onProgress, port, container, volume, env, extraPorts, dockerSock, gpu, gpuProbeImage, wslInstallDir });
1095
1166
  }
1096
1167
 
1097
1168
  // 容器里有没有注入网关 key(reconcile 自愈用):printenv 看 CICY_AI_GATEWAY_LLM_API_KEY。
@@ -1109,5 +1180,5 @@ async function readMihomoConfig(container = "cicy-code-docker-8008") {
1109
1180
  module.exports = {
1110
1181
  bootstrap, status, restart, stop, dockerRestart, recreate, update, upgrade, runContainer, readContainerToken,
1111
1182
  distroInstalled, dockerInstalled, dockerEngineUp, imagePresent, probeHealth, wslRun, hasGatewayKey,
1112
- readMihomoConfig, repairWsl, lxssWedged,
1183
+ readMihomoConfig, repairWsl, lxssWedged, dockerGpuAvailable, resolveWslInstallDir,
1113
1184
  };
@@ -12,9 +12,10 @@ module.exports = [
12
12
  require("./exec-js"),
13
13
  require("./clipboard-tools"),
14
14
  require("./exec-tools"),
15
- require("./file-tools"),
16
- require("./system-tools"),
17
- require("./automation-tools"),
15
+ require("./file-tools"),
16
+ require("./system-tools"),
17
+ require("./notify-tools"),
18
+ require("./automation-tools"),
18
19
  require("./account-tools"),
19
20
  require("./device-tools"),
20
21
  require("./desktop-snapshot-tools"),
@@ -0,0 +1,60 @@
1
+ // Copyright 2026 CiCy AI
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ const { Notification, BrowserWindow, shell } = require("electron");
5
+ const { z } = require("zod");
6
+
7
+ // OS desktop notification (mac Notification Center / Windows toast / Linux
8
+ // libnotify). Main-process Electron Notification — no renderer/window needed,
9
+ // so it works even when every window is closed or minimized.
10
+ //
11
+ // Windows: toasts require an AppUserModelId, which main.js already sets
12
+ // ("com.cicy.desktop"). Packaged builds inherit it; dev runs show the toast
13
+ // attributed to Electron until packaged.
14
+ //
15
+ // Click behavior: focus the main CiCy Desktop window (default), then open
16
+ // `url` in the system browser if given. Deep-linking into an in-app tab is
17
+ // deliberately NOT done here — tab routing belongs to tab-browser tools.
18
+ function registerTools(registerTool) {
19
+ registerTool(
20
+ "notify",
21
+ "发送操作系统桌面通知(mac 通知中心 / Windows toast)。点击通知默认聚焦 CiCy Desktop 主窗口;可选 url 在点击时用系统浏览器打开。",
22
+ z.object({
23
+ title: z.string().describe("通知标题(大字)"),
24
+ body: z.string().optional().describe("通知正文(小字,放 prompt/消息摘要)"),
25
+ subtitle: z.string().optional().describe("副标题(仅 macOS 显示)"),
26
+ silent: z.boolean().optional().describe("true = 不播放提示音"),
27
+ url: z.string().optional().describe("点击通知时用系统默认浏览器打开的 URL"),
28
+ focus: z.boolean().optional().describe("点击通知时聚焦主窗口,默认 true"),
29
+ }),
30
+ async ({ title, body, subtitle, silent, url, focus }) => {
31
+ if (!Notification.isSupported()) {
32
+ return {
33
+ content: [{ type: "text", text: JSON.stringify({ ok: false, reason: "notifications not supported on this platform" }) }],
34
+ isError: true,
35
+ };
36
+ }
37
+ const n = new Notification({ title, body: body || "", subtitle, silent: !!silent });
38
+ n.on("click", () => {
39
+ try {
40
+ if (focus !== false) {
41
+ const win = BrowserWindow.getAllWindows().find((w) => !w.isDestroyed());
42
+ if (win) {
43
+ if (win.isMinimized()) win.restore();
44
+ win.show();
45
+ win.focus();
46
+ }
47
+ }
48
+ if (url) shell.openExternal(url);
49
+ } catch {}
50
+ });
51
+ n.show();
52
+ return {
53
+ content: [{ type: "text", text: JSON.stringify({ ok: true, platform: process.platform, title }) }],
54
+ };
55
+ },
56
+ { tag: "System" }
57
+ );
58
+ }
59
+
60
+ module.exports = registerTools;