cicy-desktop 2.1.217 → 2.1.219
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.
|
|
3
|
+
"version": "2.1.219",
|
|
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.
|
|
144
|
-
"cicy-code-darwin-arm64": "2.3.
|
|
145
|
-
"cicy-code-linux-x64": "2.3.
|
|
146
|
-
"cicy-code-linux-arm64": "2.3.
|
|
147
|
-
"cicy-code-windows-x64": "2.3.
|
|
143
|
+
"cicy-code-darwin-x64": "2.3.138",
|
|
144
|
+
"cicy-code-darwin-arm64": "2.3.138",
|
|
145
|
+
"cicy-code-linux-x64": "2.3.138",
|
|
146
|
+
"cicy-code-linux-arm64": "2.3.138",
|
|
147
|
+
"cicy-code-windows-x64": "2.3.138",
|
|
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",
|
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
|
|
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;
|
|
@@ -279,7 +279,24 @@ async function ensurePageTargets({ debuggerPort, url, activateIfRunning, deps =
|
|
|
279
279
|
|
|
280
280
|
const pageTargets = targets.filter((target) => target.type === "page");
|
|
281
281
|
const targetUrl = typeof url === "string" && url.length ? url : null;
|
|
282
|
-
|
|
282
|
+
// Match normalized, not by raw ===. launchChrome already opened `url` as the
|
|
283
|
+
// startup tab, but Chrome canonicalizes it (adds a trailing slash: passed
|
|
284
|
+
// "https://x.com" loads as "https://x.com/"), so an exact compare misses the
|
|
285
|
+
// tab we just launched → we'd createTarget a duplicate. Normalize a trailing
|
|
286
|
+
// slash on the path + a bare "#" so the existing tab is recognized.
|
|
287
|
+
const normUrl = (u) => {
|
|
288
|
+
if (typeof u !== "string" || !u) return null;
|
|
289
|
+
try {
|
|
290
|
+
const x = new URL(u);
|
|
291
|
+
return (x.origin + x.pathname.replace(/\/+$/, "") + x.search + (x.hash === "#" ? "" : x.hash)).toLowerCase();
|
|
292
|
+
} catch (_) {
|
|
293
|
+
return u.trim().replace(/\/+$/, "").toLowerCase();
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
const targetUrlNorm = normUrl(targetUrl);
|
|
297
|
+
const matchingTarget = targetUrlNorm
|
|
298
|
+
? pageTargets.find((target) => normUrl(target.url) === targetUrlNorm) || null
|
|
299
|
+
: null;
|
|
283
300
|
|
|
284
301
|
if (!pageTargets.length) {
|
|
285
302
|
try {
|
|
@@ -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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
|
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
|
};
|