cicy-desktop 2.1.78 → 2.1.80
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/bin/cicy-desktop +7 -7
- package/package.json +6 -6
- package/src/backends/homepage-preload.js +22 -0
- package/src/backends/homepage-react/assets/index-CKpaMBKz.css +1 -0
- package/src/backends/homepage-react/assets/index-CSsNZgC5.js +365 -0
- package/src/backends/homepage-react/index.html +2 -2
- package/src/backends/homepage-window.js +52 -7
- package/src/backends/ipc.js +57 -0
- package/src/backends/local-teams.js +73 -26
- package/src/backends/sidecar-ipc.js +11 -0
- package/src/backends/webview-preload.js +5 -3
- package/src/backends/window-manager.js +13 -3
- package/src/chrome/chrome-launcher.js +5 -4
- package/src/chrome/debugger-port-resolver.js +1 -1
- package/src/cloud/cloud-client.js +237 -41
- package/src/cluster/types.js +0 -5
- package/src/extension/inject.js +1 -1
- package/src/main.js +282 -88
- package/src/master/chrome-config.js +2 -2
- package/src/preload-rpc.js +1 -1
- package/src/profiles/profile-store.js +321 -0
- package/src/profiles/trusted-origins-store.js +95 -0
- package/src/server/worker-observability-routes.js +0 -2
- package/src/sidecar/cicy-code.js +84 -23
- package/src/sidecar/localbin.js +20 -3
- package/src/sidecar/native.js +3 -3
- package/src/sidecar/version.js +45 -0
- package/src/tabbrowser/newtab-protocol.js +54 -0
- package/src/tabbrowser/tab-browser.html +151 -0
- package/src/tabbrowser/tab-shell-preload.js +28 -0
- package/src/tabbrowser/tab-shell.html +227 -0
- package/src/tools/account-tools.js +191 -25
- package/src/tools/chrome-tools.js +173 -37
- package/src/tools/device-tools.js +25 -0
- package/src/tools/index.js +2 -0
- package/src/tools/tab-browser-tools.js +453 -0
- package/src/tools/window-tools.js +64 -7
- package/src/utils/brand-host-electron.js +25 -0
- package/src/utils/context-menu-options.js +80 -0
- package/src/utils/cookie-logins.js +58 -0
- package/src/utils/ip-probe.js +50 -0
- package/src/utils/rpc-audit.js +53 -0
- package/src/utils/rpc-guard.js +189 -0
- package/src/utils/window-monitor.js +5 -15
- package/src/utils/window-registry.js +210 -0
- package/src/utils/window-thumbnails.js +126 -0
- package/src/utils/window-utils.js +146 -109
- package/workers/render/package-lock.json +6 -6
- package/workers/render/src/App.css +36 -2
- package/workers/render/src/App.jsx +587 -103
- package/src/backends/artifact-ipc.js +0 -142
- package/src/backends/homepage-react/assets/index-DE9m6JTn.css +0 -1
- package/src/backends/homepage-react/assets/index-DLYMzgf5.js +0 -365
- package/src/cluster/artifact-registry.js +0 -61
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// window-thumbnails.js
|
|
2
|
+
// Periodically writes a small JPEG thumbnail of every open BrowserWindow to a
|
|
3
|
+
// folder — like Chrome's tab thumbnails. On-disk so the homepage / agents can
|
|
4
|
+
// read a recent preview of a window without an RPC + capturePage round-trip
|
|
5
|
+
// each time.
|
|
6
|
+
//
|
|
7
|
+
// Reuses the same capture path as `GET /ui/snapshot` (capturePage → resize →
|
|
8
|
+
// toJPEG), just driven on a timer and scaled to a small rect.
|
|
9
|
+
//
|
|
10
|
+
// Layout (<dir> default ~/cicy-files/window-thumbs, override CICY_THUMB_DIR):
|
|
11
|
+
// <dir>/win-<id>.jpg one small JPEG per live window (overwritten each tick)
|
|
12
|
+
// <dir>/index.json manifest: [{ id, title, url, accountIdx, w, h, file, bytes, updatedAt }]
|
|
13
|
+
// Thumbnails for closed windows are pruned each tick.
|
|
14
|
+
|
|
15
|
+
const fs = require("fs");
|
|
16
|
+
const path = require("path");
|
|
17
|
+
const os = require("os");
|
|
18
|
+
const { BrowserWindow } = require("electron");
|
|
19
|
+
|
|
20
|
+
let timer = null;
|
|
21
|
+
let kickTimer = null;
|
|
22
|
+
let running = false; // a tick may outlast the interval — don't overlap captures
|
|
23
|
+
|
|
24
|
+
function thumbDir() {
|
|
25
|
+
const fromEnv = (process.env.CICY_THUMB_DIR || "").trim();
|
|
26
|
+
return fromEnv || path.join(os.homedir(), "cicy-files", "window-thumbs");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function accountIdxOf(win) {
|
|
30
|
+
// Windows tagged at creation expose cicyAccountIdx; otherwise the default
|
|
31
|
+
// session (homepage / system windows) maps to 0.
|
|
32
|
+
if (typeof win.cicyAccountIdx === "number") return win.cicyAccountIdx;
|
|
33
|
+
try {
|
|
34
|
+
const p = win.webContents.session.partition || "";
|
|
35
|
+
const m = /^persist:sandbox-(\d+)$/.exec(p);
|
|
36
|
+
if (m) return parseInt(m[1], 10);
|
|
37
|
+
} catch (_) {}
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function captureOne(win, dir, { maxWidth, quality }) {
|
|
42
|
+
if (win.isDestroyed()) return null;
|
|
43
|
+
const wc = win.webContents;
|
|
44
|
+
if (!wc || wc.isDestroyed() || wc.isCrashed()) return null;
|
|
45
|
+
const image = await wc.capturePage();
|
|
46
|
+
if (image.isEmpty()) return null;
|
|
47
|
+
const { width, height } = image.getSize();
|
|
48
|
+
if (!width || !height) return null;
|
|
49
|
+
const scale = Math.min(1, maxWidth / width);
|
|
50
|
+
const tw = Math.max(1, Math.round(width * scale));
|
|
51
|
+
const th = Math.max(1, Math.round(height * scale));
|
|
52
|
+
const scaled = scale < 1 ? image.resize({ width: tw, height: th, quality: "good" }) : image;
|
|
53
|
+
const buf = scaled.toJPEG(quality);
|
|
54
|
+
const file = path.join(dir, `win-${win.id}.jpg`);
|
|
55
|
+
fs.writeFileSync(file, buf);
|
|
56
|
+
return {
|
|
57
|
+
id: win.id,
|
|
58
|
+
title: win.getTitle(),
|
|
59
|
+
url: wc.getURL(),
|
|
60
|
+
accountIdx: accountIdxOf(win),
|
|
61
|
+
w: tw,
|
|
62
|
+
h: th,
|
|
63
|
+
file,
|
|
64
|
+
bytes: buf.length,
|
|
65
|
+
updatedAt: new Date().toISOString(),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function tick(opts) {
|
|
70
|
+
if (running) return;
|
|
71
|
+
running = true;
|
|
72
|
+
try {
|
|
73
|
+
const dir = thumbDir();
|
|
74
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
75
|
+
const wins = BrowserWindow.getAllWindows().filter((w) => !w.isDestroyed());
|
|
76
|
+
const liveIds = new Set(wins.map((w) => w.id));
|
|
77
|
+
|
|
78
|
+
// Prune thumbnails whose window is gone.
|
|
79
|
+
try {
|
|
80
|
+
for (const f of fs.readdirSync(dir)) {
|
|
81
|
+
const m = /^win-(\d+)\.jpg$/.exec(f);
|
|
82
|
+
if (m && !liveIds.has(parseInt(m[1], 10))) {
|
|
83
|
+
try { fs.unlinkSync(path.join(dir, f)); } catch (_) {}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch (_) {}
|
|
87
|
+
|
|
88
|
+
const manifest = [];
|
|
89
|
+
for (const win of wins) {
|
|
90
|
+
try {
|
|
91
|
+
const entry = await captureOne(win, dir, opts);
|
|
92
|
+
if (entry) manifest.push(entry);
|
|
93
|
+
} catch (_) {
|
|
94
|
+
// one window failing must never stop the rest
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
fs.writeFileSync(path.join(dir, "index.json"), JSON.stringify(manifest, null, 2));
|
|
99
|
+
} catch (_) {}
|
|
100
|
+
} finally {
|
|
101
|
+
running = false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function startWindowThumbnails(options = {}) {
|
|
106
|
+
const opts = {
|
|
107
|
+
intervalMs: options.intervalMs || 4000,
|
|
108
|
+
maxWidth: options.maxWidth || 320, // small rect, chrome-ish
|
|
109
|
+
quality: options.quality || 60,
|
|
110
|
+
};
|
|
111
|
+
stopWindowThumbnails();
|
|
112
|
+
const kick = () => { tick(opts).catch(() => {}); };
|
|
113
|
+
timer = setInterval(kick, opts.intervalMs);
|
|
114
|
+
if (timer.unref) timer.unref();
|
|
115
|
+
// First pass shortly after launch (let windows paint at least one frame).
|
|
116
|
+
kickTimer = setTimeout(kick, 1500);
|
|
117
|
+
if (kickTimer.unref) kickTimer.unref();
|
|
118
|
+
return { dir: thumbDir(), ...opts };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function stopWindowThumbnails() {
|
|
122
|
+
if (timer) { clearInterval(timer); timer = null; }
|
|
123
|
+
if (kickTimer) { clearTimeout(kickTimer); kickTimer = null; }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = { startWindowThumbnails, stopWindowThumbnails, thumbDir };
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const { app, BrowserWindow, Menu, dialog, shell } = require("electron");
|
|
2
|
+
const { default: contextMenu } = require("electron-context-menu");
|
|
3
|
+
const contextMenuOptions = require("./context-menu-options");
|
|
2
4
|
const path = require("path");
|
|
3
5
|
const fs = require("fs");
|
|
4
6
|
const os = require("os");
|
|
@@ -31,8 +33,11 @@ function setupWindowHandlers(win) {
|
|
|
31
33
|
icon: require("./app-icon").appIconPath(),
|
|
32
34
|
webPreferences: {
|
|
33
35
|
webviewTag: true, // embedded <webview> (ttyd/artifact) must render
|
|
34
|
-
|
|
35
|
-
|
|
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,
|
|
36
41
|
preload: path.join(__dirname, "../backends/webview-preload.js"),
|
|
37
42
|
webSecurity: false,
|
|
38
43
|
enableClipboard: true,
|
|
@@ -55,8 +60,22 @@ function setupWindowHandlers(win) {
|
|
|
55
60
|
// homepage is a persistent window; everything created here is disposable and
|
|
56
61
|
// re-openable from the homepage. (Previously these preventDefault()+hide()'d,
|
|
57
62
|
// so "closed" windows lingered hidden forever.)
|
|
63
|
+
const _registryId = win.id;
|
|
58
64
|
win.on("close", () => {
|
|
59
|
-
log.info(`[Window ${
|
|
65
|
+
log.info(`[Window ${_registryId}] Close → destroy: ${win.getTitle()}`);
|
|
66
|
+
});
|
|
67
|
+
// Persistent window registry: a USER/agent close keeps the record (status
|
|
68
|
+
// "closed", re-openable). A close during app QUIT leaves it "open" so it
|
|
69
|
+
// auto-reopens next launch. No-op for unregistered windows (e.g. homepage).
|
|
70
|
+
// "closed" fires for both win.close() and win.destroy().
|
|
71
|
+
win.on("closed", () => {
|
|
72
|
+
try {
|
|
73
|
+
if (!app || !app.isQuitting) {
|
|
74
|
+
require("./window-registry").markClosed(_registryId);
|
|
75
|
+
}
|
|
76
|
+
} catch (e) {
|
|
77
|
+
log.error("[WindowRegistry] markClosed failed:", e.message);
|
|
78
|
+
}
|
|
60
79
|
});
|
|
61
80
|
|
|
62
81
|
// 🔥 全局下载处理 - 自动保存到 ~/Downloads/electron/
|
|
@@ -78,70 +97,10 @@ function setupWindowHandlers(win) {
|
|
|
78
97
|
}
|
|
79
98
|
|
|
80
99
|
win.webContents.on("dom-ready", async () => {
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
const pageUrl = win.webContents.getURL();
|
|
86
|
-
try {
|
|
87
|
-
if (isTrustedUrl(pageUrl)) {
|
|
88
|
-
const rpcCode = `
|
|
89
|
-
if (!window.electronRPC) {
|
|
90
|
-
try {
|
|
91
|
-
const { ipcRenderer } = require('electron');
|
|
92
|
-
window.electronRPC = (tool, args) => ipcRenderer.invoke('rpc', tool, args || {});
|
|
93
|
-
console.log('[RPC] electronRPC ready');
|
|
94
|
-
} catch(e) {}
|
|
95
|
-
}
|
|
96
|
-
// window.cicy.artifact — remote control of the 产物 (artifact) <webview>
|
|
97
|
-
// guest webContents for cicy-code's artifactBridge.ts. Targets the
|
|
98
|
-
// element id 'cicy-artifact-webview'; round-trips to artifact-ipc.js.
|
|
99
|
-
(function(){
|
|
100
|
-
try {
|
|
101
|
-
if (window.cicy && window.cicy.artifact) return;
|
|
102
|
-
const { ipcRenderer } = require('electron');
|
|
103
|
-
window.cicy = window.cicy || {};
|
|
104
|
-
const guestId = () => {
|
|
105
|
-
const el = document.getElementById('cicy-artifact-webview');
|
|
106
|
-
if (!el || typeof el.getWebContentsId !== 'function')
|
|
107
|
-
throw new Error('artifact webview not mounted (open the 产物 tab once)');
|
|
108
|
-
return el.getWebContentsId();
|
|
109
|
-
};
|
|
110
|
-
let _attached = false;
|
|
111
|
-
window.cicy.artifact = {
|
|
112
|
-
invoke: (method, args) =>
|
|
113
|
-
ipcRenderer.invoke('artifact:invoke', { guestId: guestId(), method, args: args || [] }),
|
|
114
|
-
cdp: {
|
|
115
|
-
attach: (protocolVersion) =>
|
|
116
|
-
ipcRenderer.invoke('artifact:cdp-attach', { guestId: guestId(), protocolVersion })
|
|
117
|
-
.then((r) => { _attached = true; return r; }),
|
|
118
|
-
detach: () =>
|
|
119
|
-
ipcRenderer.invoke('artifact:cdp-detach', { guestId: guestId() })
|
|
120
|
-
.then((r) => { _attached = false; return r; }),
|
|
121
|
-
isAttached: () => _attached,
|
|
122
|
-
send: (method, params) =>
|
|
123
|
-
ipcRenderer.invoke('artifact:cdp-send', { guestId: guestId(), method, params: params || {} }),
|
|
124
|
-
},
|
|
125
|
-
};
|
|
126
|
-
try { ipcRenderer.removeAllListeners('artifact:event'); } catch(e) {}
|
|
127
|
-
ipcRenderer.on('artifact:event', (_e, detail) => {
|
|
128
|
-
if (detail && detail.source === 'cdp' && detail.method === '__detached') _attached = false;
|
|
129
|
-
try { window.dispatchEvent(new CustomEvent('cicy-artifact-event', { detail: detail })); } catch(e) {}
|
|
130
|
-
});
|
|
131
|
-
console.log('[artifact] window.cicy.artifact ready');
|
|
132
|
-
} catch(e) { console.error('[artifact] bridge inject failed', e && e.message); }
|
|
133
|
-
})();
|
|
134
|
-
`;
|
|
135
|
-
if (win.webContents.debugger.isAttached()) {
|
|
136
|
-
await win.webContents.debugger.sendCommand("Runtime.evaluate", { expression: rpcCode });
|
|
137
|
-
} else {
|
|
138
|
-
await win.webContents.executeJavaScript(rpcCode);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
} catch (e) {
|
|
142
|
-
log.error("[RPC inject]", e.message);
|
|
143
|
-
}
|
|
144
|
-
|
|
100
|
+
// (Removed: the 产物/artifact bridge injection. electronRPC for trusted pages
|
|
101
|
+
// is provided by webview-preload.js via contextBridge; the artifact webview
|
|
102
|
+
// remote-control feature was deleted — superseded by the electron tab + chrome
|
|
103
|
+
// profile browsers.)
|
|
145
104
|
try {
|
|
146
105
|
// 1. 获取当前页面的根域名
|
|
147
106
|
const currentURL = win.webContents.getURL();
|
|
@@ -203,23 +162,17 @@ function setupWindowHandlers(win) {
|
|
|
203
162
|
// registry.remove call refreshTrustedOrigins() — wired in registry.js).
|
|
204
163
|
let _trustedOriginsCache = null;
|
|
205
164
|
function loadTrustedOrigins() {
|
|
206
|
-
//
|
|
207
|
-
|
|
165
|
+
// The trusted set = the user-managed allowlist in
|
|
166
|
+
// ~/cicy-ai/db/trusted-origins.json (built-ins localhost/127.0.0.1 included by
|
|
167
|
+
// the store). Backends / teams are NO LONGER auto-trusted: "add a server" must
|
|
168
|
+
// never implicitly grant a remote origin the right to run commands locally.
|
|
169
|
+
// Users (incl. self-hosted) add their own domain explicitly in settings.
|
|
208
170
|
try {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const registry = require("../backends/registry");
|
|
212
|
-
for (const b of registry.list()) {
|
|
213
|
-
if (!b || !b.url) continue;
|
|
214
|
-
try {
|
|
215
|
-
const h = new URL(b.url).hostname;
|
|
216
|
-
if (h) set.add(h);
|
|
217
|
-
} catch {}
|
|
218
|
-
}
|
|
171
|
+
const store = require("../profiles/trusted-origins-store");
|
|
172
|
+
return new Set(store.listAll());
|
|
219
173
|
} catch (e) {
|
|
220
|
-
|
|
174
|
+
return new Set(["localhost", "127.0.0.1"]);
|
|
221
175
|
}
|
|
222
|
-
return set;
|
|
223
176
|
}
|
|
224
177
|
function trustedOrigins() {
|
|
225
178
|
if (!_trustedOriginsCache) _trustedOriginsCache = loadTrustedOrigins();
|
|
@@ -233,14 +186,20 @@ function isTrustedUrl(url) {
|
|
|
233
186
|
if (!url) return false;
|
|
234
187
|
try {
|
|
235
188
|
const u = new URL(url);
|
|
236
|
-
|
|
189
|
+
// Exact-hostname match against the user allowlist ONLY. No domain-suffix
|
|
190
|
+
// wildcard — a public-upload host under a trusted suffix (e.g.
|
|
191
|
+
// r2.deepfetch.de5.net, which can serve attacker HTML) must never count as a
|
|
192
|
+
// trusted RPC origin.
|
|
237
193
|
return trustedOrigins().has(u.hostname);
|
|
238
194
|
} catch {
|
|
239
195
|
return false;
|
|
240
196
|
}
|
|
241
197
|
}
|
|
242
198
|
|
|
243
|
-
|
|
199
|
+
// accountIdx default = 1 (user profile space). Account 0 is reserved for the
|
|
200
|
+
// platform's own/system windows; callers that want the system slot pass 0
|
|
201
|
+
// explicitly (e.g. local-teams). Agents opening windows should use >0.
|
|
202
|
+
function createWindow(options = {}, accountIdx = 1, forceNew = false) {
|
|
244
203
|
const { width = 1200, height = 800, url, webPreferences = {}, x, y } = options;
|
|
245
204
|
console.log("[createWindow] url:", url, "isTrusted:", isTrustedUrl(url));
|
|
246
205
|
|
|
@@ -310,13 +269,16 @@ function createWindow(options = {}, accountIdx = 0, forceNew = false) {
|
|
|
310
269
|
autoHideMenuBar: true,
|
|
311
270
|
webPreferences: {
|
|
312
271
|
offscreen: false, // 确保不是离屏渲染
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
//
|
|
316
|
-
//
|
|
272
|
+
// hole #4: trusted pages run with NO raw Node (was nodeIntegration:
|
|
273
|
+
// isTrustedUrl). electronRPC + window.cicy come from webview-preload via
|
|
274
|
+
// contextBridge, so a trusted-origin XSS can't `require('child_process')`
|
|
275
|
+
// past the rpc:guarded gate.
|
|
276
|
+
nodeIntegration: false,
|
|
277
|
+
contextIsolation: true,
|
|
278
|
+
// webview-preload exposes electronRPC + window.cicy for EVERY window
|
|
279
|
+
// open_window creates (contextBridge under isolation). Without it the
|
|
317
280
|
// agent-desktop/agent-electron skills' `desktop_event rpc_call` failed with
|
|
318
|
-
// 'electronRPC not available'.
|
|
319
|
-
// mode (contextBridge when isolated, direct window assign when not).
|
|
281
|
+
// 'electronRPC not available'.
|
|
320
282
|
preload: path.join(__dirname, "../backends/webview-preload.js"),
|
|
321
283
|
partition: `persist:sandbox-${accountIdx}`,
|
|
322
284
|
// 启用剪贴板权限
|
|
@@ -337,16 +299,29 @@ function createWindow(options = {}, accountIdx = 0, forceNew = false) {
|
|
|
337
299
|
// ✅ 核心修正:获取当前窗口真正使用的那个 session
|
|
338
300
|
const ses = win.webContents.session;
|
|
339
301
|
|
|
340
|
-
//
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
302
|
+
// 设置代理:优先用该 profile 持久化的 proxy(account-N.json),否则回退全局 config.proxy。
|
|
303
|
+
// 这样新开窗口会自动套用账号自己保存的代理,无需手动 set_account_proxy。
|
|
304
|
+
let proxyRules = "";
|
|
305
|
+
let proxySource = "";
|
|
306
|
+
try {
|
|
307
|
+
const profileStore = require("../profiles/profile-store");
|
|
308
|
+
const persisted = profileStore.proxyRules(profileStore.getProfile("electron", accountIdx)?.proxy);
|
|
309
|
+
if (persisted) {
|
|
310
|
+
proxyRules = persisted;
|
|
311
|
+
proxySource = "profile";
|
|
312
|
+
}
|
|
313
|
+
} catch (err) {
|
|
314
|
+
log.error(`[Proxy] Account ${accountIdx} 读取持久化代理失败:`, err);
|
|
315
|
+
}
|
|
316
|
+
if (!proxyRules && config.proxy) {
|
|
317
|
+
proxyRules = config.proxy;
|
|
318
|
+
proxySource = "global";
|
|
319
|
+
}
|
|
320
|
+
if (proxyRules) {
|
|
346
321
|
ses
|
|
347
|
-
.setProxy(
|
|
322
|
+
.setProxy({ proxyRules })
|
|
348
323
|
.then(() => {
|
|
349
|
-
log.info(`[Proxy] Account ${accountIdx}
|
|
324
|
+
log.info(`[Proxy] Account ${accountIdx} 已设置代理 (${proxySource}): ${proxyRules}`);
|
|
350
325
|
})
|
|
351
326
|
.catch((err) => {
|
|
352
327
|
log.error(`[Proxy] Account ${accountIdx} 设置代理失败:`, err);
|
|
@@ -385,6 +360,43 @@ function createWindow(options = {}, accountIdx = 0, forceNew = false) {
|
|
|
385
360
|
|
|
386
361
|
setupWindowHandlers(win);
|
|
387
362
|
|
|
363
|
+
// Persistent window registry: record this window (dedup by accountIdx+url),
|
|
364
|
+
// then keep its url/title/bounds fresh so a restart can restore it. The
|
|
365
|
+
// "closed" handler in setupWindowHandlers flips status when it's closed.
|
|
366
|
+
try {
|
|
367
|
+
const registry = require("./window-registry");
|
|
368
|
+
registry.registerOpen({
|
|
369
|
+
accountIdx,
|
|
370
|
+
url: url || "",
|
|
371
|
+
title: win.getTitle(),
|
|
372
|
+
bounds: win.getBounds(),
|
|
373
|
+
liveId: win.id,
|
|
374
|
+
});
|
|
375
|
+
let boundsTimer = null;
|
|
376
|
+
const touchBounds = () => {
|
|
377
|
+
if (boundsTimer) clearTimeout(boundsTimer);
|
|
378
|
+
boundsTimer = setTimeout(() => {
|
|
379
|
+
if (!win.isDestroyed()) registry.touch({ liveId: win.id, bounds: win.getBounds() });
|
|
380
|
+
}, 500);
|
|
381
|
+
};
|
|
382
|
+
win.on("resize", touchBounds);
|
|
383
|
+
win.on("move", touchBounds);
|
|
384
|
+
win.webContents.on("page-title-updated", () => {
|
|
385
|
+
if (!win.isDestroyed())
|
|
386
|
+
registry.touch({
|
|
387
|
+
liveId: win.id,
|
|
388
|
+
title: win.getTitle(),
|
|
389
|
+
url: win.webContents.getURL(),
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
win.webContents.on("did-navigate", () => {
|
|
393
|
+
if (!win.isDestroyed())
|
|
394
|
+
registry.touch({ liveId: win.id, url: win.webContents.getURL() });
|
|
395
|
+
});
|
|
396
|
+
} catch (e) {
|
|
397
|
+
log.error("[WindowRegistry] register failed:", e.message);
|
|
398
|
+
}
|
|
399
|
+
|
|
388
400
|
if (url) {
|
|
389
401
|
win.loadURL(url);
|
|
390
402
|
}
|
|
@@ -397,6 +409,9 @@ function getWindowInfo(win) {
|
|
|
397
409
|
const wc = win.webContents;
|
|
398
410
|
if (!wc || !wc.session) return null;
|
|
399
411
|
const partition = wc.session.partition || "";
|
|
412
|
+
// persist:sandbox-N → account N (N>=1 are user profiles). Anything on the
|
|
413
|
+
// default session (homepage / platform system windows, partition "") maps
|
|
414
|
+
// to account 0 — the reserved system slot.
|
|
400
415
|
const accountIdx = partition.startsWith("persist:sandbox-")
|
|
401
416
|
? parseInt(partition.replace("persist:sandbox-", ""), 10)
|
|
402
417
|
: 0;
|
|
@@ -442,19 +457,40 @@ if (app) {
|
|
|
442
457
|
app.on("web-contents-created", (_e, contents) => {
|
|
443
458
|
try {
|
|
444
459
|
if (contents.getType && contents.getType() === "webview") {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
460
|
+
// NOTE: do NOT attach contextMenu here — the global contextMenu() in
|
|
461
|
+
// main.js now also auto-attaches to <webview> guests, so an explicit
|
|
462
|
+
// attach made them get TWO right-click menus (双重弹窗). Global covers it.
|
|
463
|
+
contents.setWindowOpenHandler(({ url }) => {
|
|
464
|
+
// External (cross-origin http/https) links opened from a <webview> guest
|
|
465
|
+
// — e.g. the gotty terminal's "打开链接" confirm button — go to the user's
|
|
466
|
+
// SYSTEM browser instead of a new in-app window. Same-origin popups (a
|
|
467
|
+
// team app opening its own sub-page) keep the in-app window so embedded
|
|
468
|
+
// app flows aren't disturbed. (master: gotty open link 用系统 browser 打开)
|
|
469
|
+
try {
|
|
470
|
+
if (/^https?:\/\//i.test(url)) {
|
|
471
|
+
let guestOrigin = "";
|
|
472
|
+
try { guestOrigin = new URL(contents.getURL()).origin; } catch (_e) {}
|
|
473
|
+
if (new URL(url).origin !== guestOrigin) {
|
|
474
|
+
shell.openExternal(url);
|
|
475
|
+
return { action: "deny" };
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
} catch (_e) {}
|
|
479
|
+
return {
|
|
480
|
+
action: "allow",
|
|
481
|
+
overrideBrowserWindowOptions: {
|
|
482
|
+
autoHideMenuBar: true,
|
|
483
|
+
webPreferences: {
|
|
484
|
+
webviewTag: true,
|
|
485
|
+
// hole #4: no raw Node for trusted pages (see createWindow above).
|
|
486
|
+
contextIsolation: true,
|
|
487
|
+
nodeIntegration: false,
|
|
488
|
+
webSecurity: false,
|
|
489
|
+
enableClipboard: true,
|
|
490
|
+
},
|
|
455
491
|
},
|
|
456
|
-
}
|
|
457
|
-
})
|
|
492
|
+
};
|
|
493
|
+
});
|
|
458
494
|
}
|
|
459
495
|
} catch (e) {
|
|
460
496
|
log.warn(`[web-contents-created] guest open-handler failed: ${e.message}`);
|
|
@@ -491,4 +527,5 @@ module.exports = {
|
|
|
491
527
|
setupWindowHandlers,
|
|
492
528
|
getWindowInfo,
|
|
493
529
|
refreshTrustedOrigins,
|
|
530
|
+
isTrustedUrl,
|
|
494
531
|
};
|
|
@@ -353,9 +353,9 @@
|
|
|
353
353
|
}
|
|
354
354
|
},
|
|
355
355
|
"node_modules/@emnapi/runtime": {
|
|
356
|
-
"version": "1.11.
|
|
357
|
-
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.
|
|
358
|
-
"integrity": "sha512-
|
|
356
|
+
"version": "1.11.1",
|
|
357
|
+
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
|
|
358
|
+
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
|
|
359
359
|
"dev": true,
|
|
360
360
|
"license": "MIT",
|
|
361
361
|
"optional": true,
|
|
@@ -730,9 +730,9 @@
|
|
|
730
730
|
}
|
|
731
731
|
},
|
|
732
732
|
"node_modules/browserslist/node_modules/caniuse-lite": {
|
|
733
|
-
"version": "1.0.
|
|
734
|
-
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.
|
|
735
|
-
"integrity": "sha512-
|
|
733
|
+
"version": "1.0.30001799",
|
|
734
|
+
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
|
|
735
|
+
"integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
|
|
736
736
|
"dev": true,
|
|
737
737
|
"funding": [
|
|
738
738
|
{
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
}
|
|
17
17
|
* { box-sizing: border-box; }
|
|
18
18
|
html, body, #root { margin: 0; height: 100%; }
|
|
19
|
+
/* Fixed-size window (930*640): the page itself never scrolls — only .main
|
|
20
|
+
scrolls (overflow-y auto, custom scrollbar). 主人令. */
|
|
21
|
+
html, body { overflow: hidden; }
|
|
19
22
|
body {
|
|
20
23
|
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC",
|
|
21
24
|
"Segoe UI", Roboto, "Helvetica Neue", sans-serif;
|
|
@@ -41,7 +44,7 @@ body {
|
|
|
41
44
|
flex: 1 1 auto;
|
|
42
45
|
min-width: 0; /* allow shrink, prevent grid blow-out */
|
|
43
46
|
display: flex; flex-direction: column;
|
|
44
|
-
overflow:
|
|
47
|
+
overflow: hidden; /* topbar fixed; only .main scrolls */
|
|
45
48
|
}
|
|
46
49
|
.helper-aside {
|
|
47
50
|
flex: 0 0 auto;
|
|
@@ -141,7 +144,7 @@ body {
|
|
|
141
144
|
/* ── App shell (logged in) ── */
|
|
142
145
|
.topbar {
|
|
143
146
|
-webkit-app-region: drag;
|
|
144
|
-
position:
|
|
147
|
+
position: relative; flex: 0 0 auto; z-index: 10; /* pinned: .main scrolls under it */
|
|
145
148
|
display: flex; align-items: center; justify-content: space-between;
|
|
146
149
|
padding: 14px 24px 12px;
|
|
147
150
|
background: rgba(8,9,14,.6);
|
|
@@ -267,7 +270,19 @@ body {
|
|
|
267
270
|
padding: 22px 32px 48px;
|
|
268
271
|
width: 100%;
|
|
269
272
|
display: flex; flex-direction: column; gap: 16px;
|
|
273
|
+
flex: 1 1 auto; min-height: 0;
|
|
274
|
+
overflow-y: auto; /* the ONLY scroll container of the page */
|
|
275
|
+
}
|
|
276
|
+
/* Custom scrollbar for .main */
|
|
277
|
+
.main::-webkit-scrollbar { width: 8px; }
|
|
278
|
+
.main::-webkit-scrollbar-track { background: transparent; }
|
|
279
|
+
.main::-webkit-scrollbar-thumb {
|
|
280
|
+
background: rgba(125,135,150,.30);
|
|
281
|
+
border-radius: 999px;
|
|
282
|
+
border: 2px solid transparent;
|
|
283
|
+
background-clip: padding-box;
|
|
270
284
|
}
|
|
285
|
+
.main::-webkit-scrollbar-thumb:hover { background-color: rgba(125,135,150,.55); }
|
|
271
286
|
|
|
272
287
|
/* ── Tabs row ── */
|
|
273
288
|
.app__tabs {
|
|
@@ -312,6 +327,20 @@ body {
|
|
|
312
327
|
text-align: center;
|
|
313
328
|
}
|
|
314
329
|
.app__tab.is-active .app__tab-count { background: rgba(91,141,247,.3); color: #fff; }
|
|
330
|
+
/* 整行:tab 药丸在左,「新加团队」顶到行尾 */
|
|
331
|
+
.app__tabsrow { display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; }
|
|
332
|
+
/* 行尾"新加团队"动作按钮:独立强调色按钮,跟 tab 药丸区分开 */
|
|
333
|
+
.app__add-team {
|
|
334
|
+
-webkit-app-region: no-drag;
|
|
335
|
+
appearance: none; cursor: pointer;
|
|
336
|
+
flex: none;
|
|
337
|
+
color: #5b8df7; font-weight: 600; font-size: 12.5px;
|
|
338
|
+
background: rgba(91,141,247,.10);
|
|
339
|
+
border: 1px solid rgba(91,141,247,.35);
|
|
340
|
+
border-radius: 9px; padding: 7px 14px;
|
|
341
|
+
transition: color 120ms ease, background 120ms ease, border-color 120ms ease;
|
|
342
|
+
}
|
|
343
|
+
.app__add-team:hover { color: #fff; background: rgba(91,141,247,.22); border-color: rgba(91,141,247,.65); }
|
|
315
344
|
|
|
316
345
|
/* ── Card grid ── */
|
|
317
346
|
.app__grid {
|
|
@@ -776,6 +805,11 @@ body {
|
|
|
776
805
|
.bcard__menu-item.is-accent:hover { background: var(--accent-soft); color: #c7dbff; }
|
|
777
806
|
.bcard__menu-item.is-danger { color: #f7a3a3; }
|
|
778
807
|
.bcard__menu-item.is-danger:hover { background: rgba(239,68,68,.16); color: #fff; }
|
|
808
|
+
/* Portaled to document.body to escape .bcard's overflow:hidden (which clipped the
|
|
809
|
+
dropdown). Inline top/left position it under the kebab; here just a high
|
|
810
|
+
stacking order (below toast/drawer) + word-wrapping items so nothing overflows. */
|
|
811
|
+
.bcard__menu--portal { z-index: 9990; }
|
|
812
|
+
.bcard__menu--portal .bcard__menu-item { white-space: normal; word-break: break-word; }
|
|
779
813
|
/* ---- Windows Docker 一键安装/启动卡 (DockerSetup) ---- */
|
|
780
814
|
.docker-setup {
|
|
781
815
|
margin-bottom: 14px;
|