cicy-desktop 2.1.216 → 2.1.218

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.216",
3
+ "version": "2.1.218",
4
4
  "description": "CiCy - AI-powered operating system browser",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -140,11 +140,11 @@
140
140
  "//optionalDependencies": "(2026-06 回调): mac/linux 改回 native cicy-code(:8008,colima 在 16G mac 上压垮内存)→ 重新内置 cicy-code-<plat> / cicy-mihomo-<plat>,localbin.fromBundle 零网络 seed(npm 仅作更新通道,带 npmmirror→npmjs 回退)。这些由 scripts/sync-runtime-deps.cjs 在 tag-push 时同步到最新版。npm 的 os/cpu 字段保证每个平台只装自己那份。Windows 仍走 docker(WSL :8009),它那份 cicy-code-windows 用不到但 bundle 着无害。",
141
141
  "optionalDependencies": {
142
142
  "electron": "41.0.3",
143
- "cicy-code-darwin-x64": "2.3.98",
144
- "cicy-code-darwin-arm64": "2.3.98",
145
- "cicy-code-linux-x64": "2.3.98",
146
- "cicy-code-linux-arm64": "2.3.98",
147
- "cicy-code-windows-x64": "2.3.98",
143
+ "cicy-code-darwin-x64": "2.3.135",
144
+ "cicy-code-darwin-arm64": "2.3.135",
145
+ "cicy-code-linux-x64": "2.3.135",
146
+ "cicy-code-linux-arm64": "2.3.135",
147
+ "cicy-code-windows-x64": "2.3.135",
148
148
  "cicy-mihomo-darwin-x64": "1.10.4",
149
149
  "cicy-mihomo-darwin-arm64": "1.10.4",
150
150
  "cicy-mihomo-linux-x64": "1.10.4",
@@ -177,7 +177,14 @@ function register({ sidecarLogPath } = {}) {
177
177
  return _dockerStatusCache;
178
178
  }
179
179
  function startDockerStatusDaemon() {
180
- if (!APP_DOCKER_SUPPORTED) return; // mac/linux: native cicy-code,不跑 docker reconcile(否则 colima 反复重试)
180
+ if (!APP_DOCKER_SUPPORTED) {
181
+ // mac/linux: native cicy-code, no docker reconcile. But macOS builds
182
+ // upgraded from the old Colima era still carry a login LaunchAgent that
183
+ // boots an unused `cicy-code` VM every login → tear that leftover down
184
+ // once (no-op when the plist doesn't exist, i.e. fresh/native installs).
185
+ if (process.platform === "darwin") colimaDocker.removeAutostart().catch(() => {});
186
+ return;
187
+ }
181
188
  setTimeout(() => { reconcileDocker().catch(() => {}); }, 2000); // shortly after startup
182
189
  setInterval(() => { reconcileDocker().catch(() => {}); }, 60000); // keep fresh + self-heal
183
190
  }
package/src/main.js CHANGED
@@ -22,6 +22,18 @@ const appUpdater = require("./app-updater");
22
22
  const { attachContextMenu } = require("./utils/context-menu-options");
23
23
  electronApp.on("web-contents-created", (_e, wc) => {
24
24
  attachContextMenu(wc);
25
+ // Security backstop for window.open. createWindow (setupWindowHandlers) and the
26
+ // tab-browser install their OWN windowOpenHandler LATER, which replaces this one
27
+ // for those windows. This default therefore only stays in force for webContents
28
+ // nobody else configured (e.g. a raw agent-electron open). It guarantees
29
+ // window.open can never drop an external site into the opener's session — profile
30
+ // 0 / the default session shares cookies and may carry the electronRPC bridge, so
31
+ // an external popup there is an RCE/creds-leak surface. External links go to the
32
+ // system browser instead; trusted/local/about:blank popups are still allowed.
33
+ try {
34
+ const { windowOpenDecision } = require("./utils/window-utils");
35
+ wc.setWindowOpenHandler(({ url }) => windowOpenDecision(url, { wc }));
36
+ } catch (_) {}
25
37
  // Ctrl/Cmd+C inside a <webview> guest: on Windows the application menu's
26
38
  // role:"copy" accelerator doesn't reach the focused guest, so a selection
27
39
  // couldn't be copied with the keyboard (right-click 复制 is fixed separately via
@@ -65,7 +77,7 @@ if (process.platform === "linux") {
65
77
  const http = require("http");
66
78
  const log = require("electron-log");
67
79
  const { config } = require("./config");
68
- const { createWindow } = require("./utils/window-utils");
80
+ const { createWindow, accountIdxOfWebContents } = require("./utils/window-utils");
69
81
  const { AuthManager } = require("./utils/auth");
70
82
  const { setupElectronFlags, setupErrorHandlers } = require("./server/electron-setup");
71
83
  const { parseArgs } = require("./server/args-parser");
@@ -1329,10 +1341,7 @@ electronApp.whenReady().then(async () => {
1329
1341
  BrowserWindow.getAllWindows()
1330
1342
  .map((w) => {
1331
1343
  try {
1332
- const part = w.webContents.session.partition || "";
1333
- const acc = part.startsWith("persist:sandbox-")
1334
- ? parseInt(part.replace("persist:sandbox-", ""), 10)
1335
- : 0;
1344
+ const acc = accountIdxOfWebContents(w.webContents);
1336
1345
  return `${acc}::${registry.normalizeUrl(w.webContents.getURL())}`;
1337
1346
  } catch {
1338
1347
  return null;
@@ -415,6 +415,25 @@ async function ensureAutostart() {
415
415
  } catch {}
416
416
  }
417
417
 
418
+ // Undo ensureAutostart. macOS moved from Colima to native cicy-code (colima
419
+ // crushed memory on 16G Macs), but builds upgraded from the old Colima era still
420
+ // carry the login LaunchAgent, which starts an unused `cicy-code` VM every boot.
421
+ // Called once on darwin startup to tear that leftover down. Gated on the plist
422
+ // actually existing so fresh/native-only installs never invoke colima at all.
423
+ async function removeAutostart() {
424
+ if (process.platform !== "darwin") return;
425
+ try {
426
+ const label = `com.cicy.colima.${PROFILE}`;
427
+ const plist = path.join(os.homedir(), "Library", "LaunchAgents", `${label}.plist`);
428
+ if (!fs.existsSync(plist)) return; // no leftover → don't touch colima
429
+ try { await sh(`launchctl unload ${JSON.stringify(plist)} 2>/dev/null; true`, { timeout: 15000 }); } catch {}
430
+ try { fs.rmSync(plist, { force: true }); } catch {}
431
+ // Stop ONLY our profile's VM (never the user's default colima). Keep the VM
432
+ // image (no delete) so a rollback to a Colima build can restart it.
433
+ try { await sh(`export PATH=${BREW_PATHS}:$PATH; colima stop -p ${PROFILE} 2>/dev/null; true`, { timeout: 60000 }); } catch {}
434
+ } catch {}
435
+ }
436
+
418
437
  // 桌面文件入口:Mac 上 /home/cicy 是 docker named volume(数据在 Colima VM 内,
419
438
  // 不在 Mac 文件系统上),Finder 没法像 WSL 的 \\wsl$ 那样直接浏览。所以 Mac 端
420
439
  // 暂不放桌面快捷方式(放了也是死链)。要看文件用 `colima ssh -p cicy-code` 进 VM,
@@ -601,5 +620,5 @@ async function readMihomoConfig(container = "cicy-code-docker-8008") {
601
620
  module.exports = {
602
621
  bootstrap, status, restart, stop, dockerRestart, recreate, update, upgrade, runContainer, readContainerToken,
603
622
  vmExists, colimaInstalled, dockerCliInstalled, engineUp, imagePresent, probeHealth, hasGatewayKey, authorizeHostSsh,
604
- readMihomoConfig,
623
+ readMihomoConfig, removeAutostart,
605
624
  };
@@ -214,6 +214,12 @@ class TabManager {
214
214
  const view = new BrowserView({ webPreferences: wp });
215
215
  const wc = view.webContents;
216
216
  const id = wc.id;
217
+ // Tag the tab's webContents with its profile so anything holding just the wc
218
+ // (context menu, window helpers) can resolve the REAL profile deterministically.
219
+ // BrowserView guests don't expose a readable partition (session.partition and
220
+ // getWebPreferences().partition both come back empty), so this tag — set from
221
+ // the owning TabManager's accountIdx — is the only reliable source.
222
+ try { wc.cicyAccountIdx = this.accountIdx; } catch (e) {}
217
223
  // BrowserView tabs aren't auto-covered by the global contextMenu() (it only
218
224
  // attaches to BrowserWindows + webviews), so give each tab exactly one
219
225
  // right-click menu (copy/paste/inspect) — without this, right-click did nothing.
@@ -59,8 +59,20 @@ const OPTIONS = {
59
59
  const url = (() => { try { return wc && wc.getURL ? wc.getURL() : ""; } catch (e) { return ""; } })();
60
60
  const title = (() => { try { return wc && wc.getTitle ? wc.getTitle() : ""; } catch (e) { return ""; } })();
61
61
  // profile id = 它所在 session 的 accountIdx:partition `persist:sandbox-<N>` → N,否则 0(系统槽)。
62
- const part = (() => { try { return wc && wc.session ? (wc.session.partition || "") : ""; } catch (e) { return ""; } })();
63
- const profile = part.startsWith("persist:sandbox-") ? (parseInt(part.replace("persist:sandbox-", ""), 10) || 0) : 0;
62
+ // profile id = the webContents' account. Tab-browser BrowserView guests do NOT
63
+ // expose a readable partition (session.partition AND getWebPreferences().partition
64
+ // both come back empty → everything mis-reported as profile 0), so read the
65
+ // `cicyAccountIdx` tag stamped on the wc at tab creation. Fall back to partition
66
+ // parsing only for non-tab surfaces (BrowserWindow / <webview>) without the tag.
67
+ const profile = (() => {
68
+ try {
69
+ if (wc && typeof wc.cicyAccountIdx === "number") return wc.cicyAccountIdx;
70
+ const wp = wc && wc.getWebPreferences ? wc.getWebPreferences() : null;
71
+ const part = (wp && wp.partition) || (wc && wc.session && wc.session.partition) || "";
72
+ const m = /^persist:sandbox-(\d+)$/.exec(part);
73
+ return m ? parseInt(m[1], 10) : 0;
74
+ } catch (e) { return 0; }
75
+ })();
64
76
  // 给 agent 用的一句话指令:带 webContents id + profile id + url + title,让它用 agent-electron 操作。
65
77
  const skillPrompt = t("ctxMenu.skillPrompt", { id: wcId, profile, url, title });
66
78
  // 导航:优先用 Electron 新 navigationHistory API,旧版兜底 wc.canGoBack/goBack。
@@ -31,7 +31,11 @@ function accountIdxOf(win) {
31
31
  // session (homepage / system windows) maps to 0.
32
32
  if (typeof win.cicyAccountIdx === "number") return win.cicyAccountIdx;
33
33
  try {
34
- const p = win.webContents.session.partition || "";
34
+ // Session has no readable `.partition`; read it off WebPreferences (was
35
+ // session.partition → always undefined → every window mis-tagged profile 0).
36
+ const wc = win.webContents;
37
+ const wp = wc && wc.getWebPreferences ? wc.getWebPreferences() : null;
38
+ const p = (wp && wp.partition) || (wc && wc.session && wc.session.partition) || "";
35
39
  const m = /^persist:sandbox-(\d+)$/.exec(p);
36
40
  if (m) return parseInt(m[1], 10);
37
41
  } catch (_) {}
@@ -1,4 +1,4 @@
1
- const { app, BrowserWindow, Menu, dialog, shell } = require("electron");
1
+ const { app, BrowserWindow, Menu, shell } = require("electron");
2
2
  const { default: contextMenu } = require("electron-context-menu");
3
3
  const contextMenuOptions = require("./context-menu-options");
4
4
  const path = require("path");
@@ -15,39 +15,78 @@ if (app) {
15
15
  app.name = "CiCy Desktop";
16
16
  }
17
17
 
18
- function setupWindowHandlers(win) {
19
- // Hook window.open. App/internal popups (about:blank or trusted/local hosts
20
- // e.g. a popped-out ttyd terminal from CiCy Code) must open as REAL Electron
21
- // windows WITH webviewTag, or any <webview> inside them (terminal / artifact
22
- // guest) collapses to 0x0. Returning action:"deny" (the old behavior) routed
23
- // every popup through a dialog; an *allowed* window.open window inherits
24
- // DEFAULT webPreferences (webviewTag=false) unless we override it here.
25
- // External links (other websites) keep the open-in-browser/app dialog.
26
- win.webContents.setWindowOpenHandler(({ url }) => {
27
- log.info(`[WindowOpen] Intercepted: ${url}`);
28
- if (!url || url === "about:blank" || isTrustedUrl(url)) {
29
- return {
30
- action: "allow",
31
- overrideBrowserWindowOptions: {
32
- autoHideMenuBar: true,
33
- icon: require("./app-icon").appIconPath(),
34
- webPreferences: {
35
- webviewTag: true, // embedded <webview> (ttyd/artifact) must render
36
- // hole #4: trusted pages no longer get raw Node. Their electronRPC
37
- // bridge comes from webview-preload via contextBridge, so a trusted-origin
38
- // XSS can't `require('child_process')` to bypass the rpc:guarded gate.
39
- nodeIntegration: false,
40
- contextIsolation: true,
41
- preload: path.join(__dirname, "../backends/webview-preload.js"),
42
- webSecurity: false,
43
- enableClipboard: true,
44
- },
18
+ // Resolve the profile (accountIdx) a webContents lives in, from its session
19
+ // partition: persist:sandbox-N → N; the default/"" session 0 (profile 0, the
20
+ // privileged/shared session). Mirrors getWindowInfo's derivation.
21
+ function partitionOfWebContents(wc) {
22
+ // Electron's Session has NO readable `.partition`; the partition string lives on
23
+ // the webContents' WebPreferences. Reading session.partition (always undefined)
24
+ // made every profile resolve to 0. Prefer getWebPreferences(), fall back to
25
+ // session.partition just in case.
26
+ try {
27
+ const wp = wc && wc.getWebPreferences ? wc.getWebPreferences() : null;
28
+ if (wp && wp.partition) return wp.partition;
29
+ return (wc && wc.session && wc.session.partition) || "";
30
+ } catch (_) { return ""; }
31
+ }
32
+
33
+ function accountIdxOfWebContents(wc) {
34
+ // Prefer the cicyAccountIdx tag stamped at creation (the only reliable source for
35
+ // BrowserView tab guests, whose partition is never readable). Fall back to
36
+ // partition parsing for surfaces that carry a real partition.
37
+ if (wc && typeof wc.cicyAccountIdx === "number") return wc.cicyAccountIdx;
38
+ const partition = partitionOfWebContents(wc);
39
+ if (partition.startsWith("persist:sandbox-")) {
40
+ const n = parseInt(partition.slice("persist:sandbox-".length), 10);
41
+ return Number.isFinite(n) ? n : 0;
42
+ }
43
+ return 0;
44
+ }
45
+
46
+ // Single source of truth for the window.open policy. App/internal popups
47
+ // (about:blank or trusted/local hosts — e.g. a popped-out ttyd terminal from CiCy
48
+ // Code) open in-place as REAL Electron windows WITH webviewTag (any <webview>
49
+ // inside them — terminal / artifact guest — otherwise collapses to 0x0).
50
+ //
51
+ // Any OTHER (external) URL is routed DETERMINISTICALLY by the OPENER's profile,
52
+ // and never lands in profile 0: an opener in profile 0 (the privileged/shared
53
+ // session — team cookies + trusted-origin RPC surface) has the link FORCED into
54
+ // sandbox profile 1; an opener already in a sandbox profile N opens the link in
55
+ // its OWN profile N. This mirrors the tab-browser policy (profile 0 link →
56
+ // openTab(1); else same profile) — no dialog, no leak into profile 0.
57
+ function windowOpenDecision(url, { wc } = {}) {
58
+ log.info(`[WindowOpen] Intercepted: ${url}`);
59
+ if (!url || url === "about:blank" || isTrustedUrl(url)) {
60
+ return {
61
+ action: "allow",
62
+ overrideBrowserWindowOptions: {
63
+ autoHideMenuBar: true,
64
+ icon: require("./app-icon").appIconPath(),
65
+ webPreferences: {
66
+ webviewTag: true, // embedded <webview> (ttyd/artifact) must render
67
+ // hole #4: trusted pages no longer get raw Node. Their electronRPC
68
+ // bridge comes from webview-preload via contextBridge, so a trusted-origin
69
+ // XSS can't `require('child_process')` to bypass the rpc:guarded gate.
70
+ nodeIntegration: false,
71
+ contextIsolation: true,
72
+ preload: path.join(__dirname, "../backends/webview-preload.js"),
73
+ webSecurity: false,
74
+ enableClipboard: true,
45
75
  },
46
- };
47
- }
48
- showOpenLinkDialog(win, url);
49
- return { action: "deny" };
50
- });
76
+ },
77
+ };
78
+ }
79
+ // External URL: profile 0 → forced sandbox profile 1; any sandbox profile N →
80
+ // its own profile N. Never in-session for profile 0.
81
+ const openerIdx = accountIdxOfWebContents(wc);
82
+ const target = openerIdx === 0 ? 1 : openerIdx;
83
+ try { createWindow({ url }, target, true); }
84
+ catch (e) { log.warn(`[WindowOpen] open external in profile ${target} failed: ${e && e.message}`); }
85
+ return { action: "deny" };
86
+ }
87
+
88
+ function setupWindowHandlers(win) {
89
+ win.webContents.setWindowOpenHandler(({ url }) => windowOpenDecision(url, { wc: win.webContents }));
51
90
  if (!win.webContents.debugger.isAttached()) {
52
91
  win.webContents.debugger.attach("1.3");
53
92
  }
@@ -301,6 +340,12 @@ function createWindow(options = {}, accountIdx = 1, forceNew = false) {
301
340
  },
302
341
  });
303
342
 
343
+ // Stamp the profile on the window + its webContents so any holder of just the wc
344
+ // (context menu, accountIdxOfWebContents, thumbnails) resolves the REAL profile
345
+ // deterministically — session.partition/getWebPreferences().partition are not
346
+ // reliably readable back.
347
+ try { win.cicyAccountIdx = accountIdx; win.webContents.cicyAccountIdx = accountIdx; } catch (e) {}
348
+
304
349
  // 监听窗口状态变化并自动保存(基于URL)
305
350
  watchWindowState(win, accountIdx);
306
351
 
@@ -427,7 +472,7 @@ function getWindowInfo(win) {
427
472
  try {
428
473
  const wc = win.webContents;
429
474
  if (!wc || !wc.session) return null;
430
- const partition = wc.session.partition || "";
475
+ const partition = partitionOfWebContents(wc);
431
476
  // persist:sandbox-N → account N (N>=1 are user profiles). Anything on the
432
477
  // default session (homepage / platform system windows, partition "") maps
433
478
  // to account 0 — the reserved system slot.
@@ -517,34 +562,13 @@ if (app) {
517
562
  });
518
563
  }
519
564
 
520
- function showOpenLinkDialog(parentWin, url) {
521
- const i18n = require("../i18n");
522
- dialog
523
- .showMessageBox(parentWin, {
524
- type: "question",
525
- buttons: [
526
- i18n.t("dialog.openLink.openInBrowser"),
527
- i18n.t("dialog.openLink.openInApp"),
528
- i18n.t("dialog.openLink.cancel"),
529
- ],
530
- defaultId: 0,
531
- title: i18n.t("dialog.openLink.title"),
532
- message: i18n.t("dialog.openLink.message"),
533
- detail: url,
534
- })
535
- .then(({ response }) => {
536
- if (response === 0) {
537
- shell.openExternal(url);
538
- } else if (response === 1) {
539
- createWindow({ url }, 0, true);
540
- }
541
- });
542
- }
543
-
544
565
  module.exports = {
545
566
  createWindow,
546
567
  setupWindowHandlers,
568
+ windowOpenDecision,
547
569
  getWindowInfo,
570
+ partitionOfWebContents,
571
+ accountIdxOfWebContents,
548
572
  refreshTrustedOrigins,
549
573
  isTrustedUrl,
550
574
  };