cicy-desktop 2.1.78 → 2.1.79
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,453 @@
|
|
|
1
|
+
// tab-browser-tools.js — Chrome-like tabbed browser, one window per profile,
|
|
2
|
+
// built on BrowserView tabs (NOT <webview>), so each tab is a full webContents
|
|
3
|
+
// that can itself host <webview> (the cicy-code team app's artifact frame, the
|
|
4
|
+
// homepage's team-assistant drawer) — nested webviews work.
|
|
5
|
+
//
|
|
6
|
+
// Model (additive — the existing win_id BrowserWindow tools are untouched):
|
|
7
|
+
// • one profile (accountIdx → persist:sandbox-N) == one tabbed BrowserWindow
|
|
8
|
+
// whose chrome (tab strip + toolbar) is tab-shell.html; each tab is a
|
|
9
|
+
// BrowserView positioned below the 80px chrome.
|
|
10
|
+
// • every "open" goes in as a TAB, never a new window (homepage / teams /
|
|
11
|
+
// window.open all become tabs).
|
|
12
|
+
// • each tab is addressed by its webContents.id (like a Chrome target).
|
|
13
|
+
// • per-tab preload: home → homepage-preload (window.cicy bridges),
|
|
14
|
+
// trusted (team) → webview-preload (electronRPC), plain site → none.
|
|
15
|
+
const path = require("path");
|
|
16
|
+
const { z } = require("zod");
|
|
17
|
+
const { app, BrowserWindow, BrowserView, webContents, ipcMain } = require("electron");
|
|
18
|
+
const { attachContextMenu } = require("../utils/context-menu-options");
|
|
19
|
+
|
|
20
|
+
const SHELL_HTML = path.join(__dirname, "..", "tabbrowser", "tab-shell.html");
|
|
21
|
+
const SHELL_PRELOAD = path.join(__dirname, "..", "tabbrowser", "tab-shell-preload.js");
|
|
22
|
+
const HOMEPAGE_PRELOAD = path.join(__dirname, "..", "backends", "homepage-preload.js");
|
|
23
|
+
const WEBVIEW_PRELOAD = path.join(__dirname, "..", "backends", "webview-preload.js");
|
|
24
|
+
const CHROME_H = 80; // tab strip (40) + toolbar (40) — must match tab-shell.html
|
|
25
|
+
const STRIP_H = 40; // tab strip only; the homepage tab hides the toolbar (no address bar)
|
|
26
|
+
|
|
27
|
+
const managers = new Map(); // accountIdx -> TabManager
|
|
28
|
+
const managerByHost = new Map(); // shell webContents.id -> TabManager
|
|
29
|
+
|
|
30
|
+
function stripVol(u) { try { const x = new URL(u); return x.origin + x.pathname; } catch (e) { return u || ""; } }
|
|
31
|
+
|
|
32
|
+
const { NEWTAB_URL, ensureForPartition } = require("../tabbrowser/newtab-protocol");
|
|
33
|
+
|
|
34
|
+
// ── Per-profile privilege gate ────────────────────────────────────────────────
|
|
35
|
+
// Security model: accountIdx 0 is the SYSTEM profile (homepage + team apps) and
|
|
36
|
+
// is the ONLY profile allowed to run privileged tabs — Node-capable preloads
|
|
37
|
+
// (homepage-preload / webview-preload → window.electronRPC), <webview>, and
|
|
38
|
+
// insecure content. Profile-0 tabs carry the electronRPC bridge, but it is gated
|
|
39
|
+
// at CALL TIME: a non-allowlisted origin must pass the rpc:guarded consent modal
|
|
40
|
+
// (ensureOriginAuthorized) before any tool runs. Every other profile (accountIdx
|
|
41
|
+
// ≥ 1) is a HARD-SANDBOXED web browser: contextIsolation on, nodeIntegration off,
|
|
42
|
+
// OS sandbox on, NO preload, NO webviewTag — so a sandbox profile can never reach
|
|
43
|
+
// the electronRPC bridge, Node, or nest another webview, whatever flags a caller
|
|
44
|
+
// passes.
|
|
45
|
+
function buildTabWebPreferences(accountIdx, partition, target, opts = {}) {
|
|
46
|
+
const wp = { partition, contextIsolation: true, nodeIntegration: false, sandbox: true };
|
|
47
|
+
if (accountIdx !== 0) return wp; // sandbox profiles: locked baseline, no exceptions
|
|
48
|
+
// homepage-preload does Node require()s (../i18n, path) → must run unsandboxed.
|
|
49
|
+
// home is system-driven (openHomeWindow), never caller-URL-reachable, so it
|
|
50
|
+
// keeps its privileges without a URL check.
|
|
51
|
+
if (opts.home) { wp.preload = HOMEPAGE_PRELOAD; wp.webviewTag = true; wp.allowRunningInsecureContent = true; wp.sandbox = false; }
|
|
52
|
+
// Every other profile-0 tab carries the electronRPC bridge (WEBVIEW_PRELOAD),
|
|
53
|
+
// but the bridge is INERT until the page's origin is authorized: the first
|
|
54
|
+
// rpc:guarded call from a non-allowlisted origin pops a consent modal
|
|
55
|
+
// (ensureOriginAuthorized) that can deny, allow once, or add the domain to the
|
|
56
|
+
// trusted-origins allowlist. Trust moved from inject-time to call-time so the
|
|
57
|
+
// user can grant it on demand instead of hitting a silent "electronRPC not
|
|
58
|
+
// available". The page itself still can't reach Node (contextIsolation on,
|
|
59
|
+
// nodeIntegration off) — only the preload runs privileged, and it exposes
|
|
60
|
+
// nothing but the gated bridge.
|
|
61
|
+
else { wp.preload = WEBVIEW_PRELOAD; wp.webviewTag = true; wp.sandbox = false; }
|
|
62
|
+
return wp;
|
|
63
|
+
}
|
|
64
|
+
function startPageUrl(_accountIdx) {
|
|
65
|
+
// clean cicy://newtab instead of a giant data: URL (served by newtab-protocol).
|
|
66
|
+
return NEWTAB_URL;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class TabManager {
|
|
70
|
+
constructor(accountIdx) {
|
|
71
|
+
this.accountIdx = accountIdx;
|
|
72
|
+
this.partition = `persist:sandbox-${accountIdx}`;
|
|
73
|
+
ensureForPartition(this.partition); // register cicyui://newtab on this session
|
|
74
|
+
this.tabs = []; // [{ id(=webContents.id), view, title, url }]
|
|
75
|
+
this.activeId = null;
|
|
76
|
+
// profile 0 is the primary window (its first tab is the resident homepage),
|
|
77
|
+
// so give it the app title/icon; other profiles are sandbox browsers.
|
|
78
|
+
const STRIP_BG = "#1c1c20"; // matches tab-shell.html --strip so the titlebar merges in
|
|
79
|
+
const winOpts = {
|
|
80
|
+
width: 1180,
|
|
81
|
+
height: 820,
|
|
82
|
+
backgroundColor: STRIP_BG,
|
|
83
|
+
title: accountIdx === 0 ? "CiCy Desktop" : `CiCy Browser · sandbox-${accountIdx}`,
|
|
84
|
+
// Drop the native title-bar row — the tab strip becomes the top of the window
|
|
85
|
+
// (Chrome-style). mac: keep traffic lights, inset into the strip. win: keep the
|
|
86
|
+
// min/max/close as an overlay tinted to the strip color so it merges. (Linux:
|
|
87
|
+
// leave the default frame.)
|
|
88
|
+
...(process.platform === "darwin"
|
|
89
|
+
? { titleBarStyle: "hidden", trafficLightPosition: { x: 12, y: 13 } }
|
|
90
|
+
: {}),
|
|
91
|
+
...(process.platform === "win32"
|
|
92
|
+
? { titleBarStyle: "hidden", titleBarOverlay: { color: STRIP_BG, symbolColor: "#e8eaed", height: 40 } }
|
|
93
|
+
: {}),
|
|
94
|
+
// win/linux: hide the native application menu bar too (mac keeps it in the
|
|
95
|
+
// global bar). Without this, Windows draws the File/Edit/View row where the
|
|
96
|
+
// titlebar was, so the strip never reaches the top edge. Alt still reveals it.
|
|
97
|
+
autoHideMenuBar: true,
|
|
98
|
+
webPreferences: { preload: SHELL_PRELOAD, contextIsolation: true, nodeIntegration: false },
|
|
99
|
+
};
|
|
100
|
+
try { winOpts.icon = require("../utils/app-icon").appIconPath(); } catch (e) {}
|
|
101
|
+
this.win = new BrowserWindow(winOpts);
|
|
102
|
+
// profile 0 is the system tab window (teams only) — no manual "+" new tab.
|
|
103
|
+
this.win.loadFile(SHELL_HTML, { query: { p: String(accountIdx), noNew: accountIdx === 0 ? "1" : "0", plat: process.platform } });
|
|
104
|
+
managerByHost.set(this.win.webContents.id, this);
|
|
105
|
+
// Fullscreen hides the OS window controls (mac traffic lights / win caption
|
|
106
|
+
// buttons) → tell the shell so it reclaims the reserved gutter (CSS
|
|
107
|
+
// body.is-fullscreen). Fires on both mac and Windows.
|
|
108
|
+
this.win.on("enter-full-screen", () => { try { this.win.webContents.send("window:fullscreen", true); } catch (e) {} });
|
|
109
|
+
this.win.on("leave-full-screen", () => { try { this.win.webContents.send("window:fullscreen", false); } catch (e) {} });
|
|
110
|
+
this.win.on("resize", () => this.layout());
|
|
111
|
+
this.win.on("closed", () => {
|
|
112
|
+
managers.delete(accountIdx);
|
|
113
|
+
managerByHost.delete(this.win.webContents.id);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
pushState() {
|
|
118
|
+
const active = this.tabs.find((t) => t.id === this.activeId);
|
|
119
|
+
let wc = null;
|
|
120
|
+
try { wc = active ? active.view.webContents : null; } catch (e) {}
|
|
121
|
+
const s = {
|
|
122
|
+
tabs: this.tabs.map((t) => ({
|
|
123
|
+
id: t.id,
|
|
124
|
+
// fixedTitle (team title / homepage) wins over the page's own document.title
|
|
125
|
+
title: t.fixedTitle || t.title || "新标签页",
|
|
126
|
+
url: t.url || "",
|
|
127
|
+
active: t.id === this.activeId,
|
|
128
|
+
loading: !!t.loading,
|
|
129
|
+
favicon: t.favicon || "",
|
|
130
|
+
home: !!t.home,
|
|
131
|
+
})),
|
|
132
|
+
nav: {
|
|
133
|
+
canBack: wc ? wc.canGoBack() : false,
|
|
134
|
+
canFwd: wc ? wc.canGoForward() : false,
|
|
135
|
+
loading: active ? !!active.loading : false,
|
|
136
|
+
url: active ? active.url || "" : "",
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
try { this.win.webContents.send("tabwin:state", s); } catch (e) {}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
layout() {
|
|
143
|
+
const t = this.tabs.find((x) => x.id === this.activeId);
|
|
144
|
+
if (!t) return;
|
|
145
|
+
const [w, h] = this.win.getContentSize();
|
|
146
|
+
// Hide the toolbar (address bar) ONLY on the homepage tab — its full-page UI
|
|
147
|
+
// covers y=40 down. Every other tab, INCLUDING profile 0's team tabs, keeps
|
|
148
|
+
// the address bar (主人令: profile 0 的地址栏不再隐藏 / 不限制).
|
|
149
|
+
const top = t.home ? STRIP_H : CHROME_H;
|
|
150
|
+
try { t.view.setBounds({ x: 0, y: top, width: w, height: Math.max(0, h - top) }); } catch (e) {}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
addTab(url, opts = {}) {
|
|
154
|
+
const target = (url && String(url).trim()) || startPageUrl(this.accountIdx);
|
|
155
|
+
// reuse an existing tab with the same origin+pathname
|
|
156
|
+
const key = url ? stripVol(target) : null;
|
|
157
|
+
if (key) {
|
|
158
|
+
const ex = this.tabs.find((t) => stripVol(t.url) === key);
|
|
159
|
+
if (ex) { this.activate(ex.id); return ex.id; }
|
|
160
|
+
}
|
|
161
|
+
// Privilege gate: only the system profile (accountIdx 0) may get a Node-capable
|
|
162
|
+
// preload / <webview>; all other profiles are forced to the sandbox baseline.
|
|
163
|
+
const wp = buildTabWebPreferences(this.accountIdx, this.partition, target, opts);
|
|
164
|
+
const view = new BrowserView({ webPreferences: wp });
|
|
165
|
+
const wc = view.webContents;
|
|
166
|
+
const id = wc.id;
|
|
167
|
+
// BrowserView tabs aren't auto-covered by the global contextMenu() (it only
|
|
168
|
+
// attaches to BrowserWindows + webviews), so give each tab exactly one
|
|
169
|
+
// right-click menu (copy/paste/inspect) — without this, right-click did nothing.
|
|
170
|
+
try { attachContextMenu(wc); } catch (e) {}
|
|
171
|
+
// home = the resident homepage tab (pinned, first, user-icon, no close).
|
|
172
|
+
// fixedTitle = a caller-supplied tab name (e.g. the team title) that the
|
|
173
|
+
// page's own document.title must NOT override.
|
|
174
|
+
const tab = { id, view, title: "", url: target, home: !!opts.home, fixedTitle: opts.title || "" };
|
|
175
|
+
if (opts.home) this.tabs.unshift(tab); else this.tabs.push(tab);
|
|
176
|
+
wc.on("page-title-updated", (_e, title) => { if (!tab.fixedTitle) { tab.title = title; this.pushState(); } });
|
|
177
|
+
wc.on("page-favicon-updated", (_e, favs) => { tab.favicon = (favs && favs[0]) || ""; this.pushState(); });
|
|
178
|
+
wc.on("did-start-loading", () => { tab.loading = true; this.pushState(); });
|
|
179
|
+
wc.on("did-stop-loading", () => { tab.loading = false; this.pushState(); });
|
|
180
|
+
wc.on("did-navigate", (_e, u) => { tab.url = u; tab.favicon = ""; this.pushState(); });
|
|
181
|
+
wc.on("did-navigate-in-page", (_e, u) => { tab.url = u; this.pushState(); });
|
|
182
|
+
// popups / window.open → open as a new tab. In profile 0 the new tab carries
|
|
183
|
+
// the (inert) electronRPC bridge like any other profile-0 tab; its origin is
|
|
184
|
+
// still gated by the rpc:guarded consent modal before any tool runs. Sandbox
|
|
185
|
+
// profiles (accountIdx ≥ 1) never get the bridge, popup or not.
|
|
186
|
+
try { wc.setWindowOpenHandler(({ url: u }) => {
|
|
187
|
+
try { this.addTab(u); } catch (e) {}
|
|
188
|
+
return { action: "deny" };
|
|
189
|
+
}); } catch (e) {}
|
|
190
|
+
wc.loadURL(target);
|
|
191
|
+
this.activate(id);
|
|
192
|
+
return id;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
activate(id) {
|
|
196
|
+
const t = this.tabs.find((x) => x.id === id);
|
|
197
|
+
if (!t) return false;
|
|
198
|
+
if (this.activeId != null && this.activeId !== id) {
|
|
199
|
+
const cur = this.tabs.find((x) => x.id === this.activeId);
|
|
200
|
+
if (cur) { try { this.win.removeBrowserView(cur.view); } catch (e) {} }
|
|
201
|
+
}
|
|
202
|
+
this.activeId = id;
|
|
203
|
+
try { this.win.addBrowserView(t.view); } catch (e) {}
|
|
204
|
+
this.layout();
|
|
205
|
+
this.pushState();
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
close(id) {
|
|
210
|
+
const i = this.tabs.findIndex((x) => x.id === id);
|
|
211
|
+
if (i < 0) return false;
|
|
212
|
+
const t = this.tabs[i];
|
|
213
|
+
if (t.home) return false; // the resident homepage tab can't be closed
|
|
214
|
+
try { this.win.removeBrowserView(t.view); } catch (e) {}
|
|
215
|
+
try { t.view.webContents.close(); } catch (e) { try { t.view.webContents.destroy(); } catch (_) {} }
|
|
216
|
+
this.tabs.splice(i, 1);
|
|
217
|
+
if (this.activeId === id) {
|
|
218
|
+
this.activeId = null;
|
|
219
|
+
const n = this.tabs[Math.min(i, this.tabs.length - 1)];
|
|
220
|
+
if (n) this.activate(n.id);
|
|
221
|
+
else if (this.accountIdx !== 0) this.addTab(); // non-system: keep a start page
|
|
222
|
+
else this.pushState(); // profile 0: leave empty (tabs only come from homepage)
|
|
223
|
+
} else {
|
|
224
|
+
this.pushState();
|
|
225
|
+
}
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
list() { return this.tabs.map((t) => ({ webContentsId: t.id, title: t.title, url: t.url, active: t.id === this.activeId })); }
|
|
230
|
+
|
|
231
|
+
activeWc() { const t = this.tabs.find((x) => x.id === this.activeId); return t ? t.view.webContents : null; }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function ensureManager(accountIdx) {
|
|
235
|
+
let m = managers.get(accountIdx);
|
|
236
|
+
if (m && !m.win.isDestroyed()) return m;
|
|
237
|
+
m = new TabManager(accountIdx);
|
|
238
|
+
managers.set(accountIdx, m);
|
|
239
|
+
return m;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function findManagerByTab(webContentsId) {
|
|
243
|
+
for (const m of managers.values()) {
|
|
244
|
+
if (m.tabs.some((t) => t.id === webContentsId)) return m;
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── programmatic API (team-open reroute / homepage) ──────────────────────────
|
|
250
|
+
// profile 0 is the system tab window: tabs may ONLY be added by the homepage
|
|
251
|
+
// (team open passes systemOpen:true). The "+" button, the electron_tab_open
|
|
252
|
+
// tool and the panel can't add tabs to it.
|
|
253
|
+
async function openTab(accountIdx, url, opts = {}) {
|
|
254
|
+
if (accountIdx === 0 && !opts.systemOpen) {
|
|
255
|
+
throw new Error("profile 0 的标签只能从首页点开 team");
|
|
256
|
+
}
|
|
257
|
+
const m = ensureManager(accountIdx);
|
|
258
|
+
const id = m.addTab(url, { trusted: !!opts.trusted, home: !!opts.home, title: opts.title || "" });
|
|
259
|
+
try { m.win.show(); m.win.focus(); } catch (e) {}
|
|
260
|
+
return { winId: m.win.id, accountIdx, tabId: id };
|
|
261
|
+
}
|
|
262
|
+
// Open (or focus) the resident homepage tab of a profile's tab window. Returns
|
|
263
|
+
// { win, wc } so the homepage module can track the tab's webContents for the
|
|
264
|
+
// main→renderer pushes (auth:complete, update-state) that used to target the
|
|
265
|
+
// standalone homepage window.
|
|
266
|
+
function openHomeWindow(accountIdx, homeUrl) {
|
|
267
|
+
const m = ensureManager(accountIdx);
|
|
268
|
+
let tab = m.tabs.find((t) => t.home);
|
|
269
|
+
if (tab) { m.activate(tab.id); }
|
|
270
|
+
else { const id = m.addTab(homeUrl, { home: true }); tab = m.tabs.find((t) => t.id === id); }
|
|
271
|
+
try { m.win.show(); m.win.focus(); } catch (e) {}
|
|
272
|
+
let wc = null; try { wc = tab ? tab.view.webContents : null; } catch (e) {}
|
|
273
|
+
return { win: m.win, wc };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ── shell IPC (registered once) ──────────────────────────────────────────────
|
|
277
|
+
let ipcInstalled = false;
|
|
278
|
+
function installIpc() {
|
|
279
|
+
if (ipcInstalled) return;
|
|
280
|
+
ipcInstalled = true;
|
|
281
|
+
const mgr = (e) => managerByHost.get(e.sender.id);
|
|
282
|
+
ipcMain.on("tabwin:ready", (e) => { const m = mgr(e); if (m) m.pushState(); });
|
|
283
|
+
ipcMain.on("tabwin:new", (e, { url }) => { const m = mgr(e); if (m && m.accountIdx !== 0) m.addTab(url || ""); });
|
|
284
|
+
ipcMain.on("tabwin:activate", (e, { id }) => { const m = mgr(e); if (m) m.activate(id); });
|
|
285
|
+
ipcMain.on("tabwin:close", (e, { id }) => { const m = mgr(e); if (m) m.close(id); });
|
|
286
|
+
ipcMain.on("tabwin:navigate", (e, { url }) => { const m = mgr(e); const wc = m && m.activeWc(); if (wc && url) wc.loadURL(String(url)); });
|
|
287
|
+
ipcMain.on("tabwin:back", (e) => { const m = mgr(e); const wc = m && m.activeWc(); if (wc && wc.canGoBack()) wc.goBack(); });
|
|
288
|
+
ipcMain.on("tabwin:fwd", (e) => { const m = mgr(e); const wc = m && m.activeWc(); if (wc && wc.canGoForward()) wc.goForward(); });
|
|
289
|
+
ipcMain.on("tabwin:reload", (e) => { const m = mgr(e); const wc = m && m.activeWc(); if (wc) wc.reload(); });
|
|
290
|
+
}
|
|
291
|
+
installIpc();
|
|
292
|
+
|
|
293
|
+
// CDP captureScreenshot — works for background tabs (capturePage blanks when the
|
|
294
|
+
// BrowserView isn't the attached/visible one).
|
|
295
|
+
async function tabScreenshot(webContentsId, format) {
|
|
296
|
+
const wc = webContents.fromId(webContentsId);
|
|
297
|
+
if (!wc) throw new Error(`tab ${webContentsId} not found`);
|
|
298
|
+
const fmt = format === "png" ? "png" : "jpeg";
|
|
299
|
+
let attached = false;
|
|
300
|
+
try {
|
|
301
|
+
try { wc.debugger.attach("1.3"); attached = true; } catch (e) {}
|
|
302
|
+
const res = await wc.debugger.sendCommand("Page.captureScreenshot", { format: fmt, quality: 70 });
|
|
303
|
+
return `data:image/${fmt};base64,${res.data}`;
|
|
304
|
+
} finally {
|
|
305
|
+
if (attached) { try { wc.debugger.detach(); } catch (e) {} }
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function registerTabBrowserTools(registerTool) {
|
|
310
|
+
const ok = (obj, isErr = false) => ({
|
|
311
|
+
content: [{ type: "text", text: JSON.stringify(obj, null, 2) }],
|
|
312
|
+
...(isErr ? { isError: true } : {}),
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
registerTool(
|
|
316
|
+
"electron_tabwin_open",
|
|
317
|
+
"打开/前置某 profile 的标签浏览器窗口(一个 profile 一个窗口,accountIdx → persist:sandbox-N,BrowserView tab)。",
|
|
318
|
+
z.object({ accountIdx: z.number().describe("账户索引(profile)") }),
|
|
319
|
+
async ({ accountIdx }) => {
|
|
320
|
+
try {
|
|
321
|
+
const m = ensureManager(accountIdx);
|
|
322
|
+
if (m.tabs.length === 0 && accountIdx !== 0) m.addTab();
|
|
323
|
+
try { m.win.show(); m.win.focus(); } catch (e) {}
|
|
324
|
+
return ok({ success: true, accountIdx, winId: m.win.id });
|
|
325
|
+
} catch (e) { return ok({ error: e.message }, true); }
|
|
326
|
+
},
|
|
327
|
+
{ tag: "TabBrowser" }
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
registerTool(
|
|
331
|
+
"electron_tab_open",
|
|
332
|
+
"在某 profile 的标签窗口新开一个标签(窗口不在则先建)。打开=开 tab,不弹新窗口。trusted=true 给 cicy-code 等受信任页注入 electronRPC 桥。",
|
|
333
|
+
z.object({
|
|
334
|
+
accountIdx: z.number().describe("账户索引(profile)"),
|
|
335
|
+
url: z.string().optional().describe("网址;省略则起始页"),
|
|
336
|
+
trusted: z.boolean().optional().describe("受信任页面才注入桥;默认 false"),
|
|
337
|
+
}),
|
|
338
|
+
async ({ accountIdx, url, trusted }) => {
|
|
339
|
+
try {
|
|
340
|
+
const r = await openTab(accountIdx, url, { trusted: !!trusted });
|
|
341
|
+
return ok({ success: true, accountIdx, winId: r.winId, tabId: r.tabId, tabs: ensureManager(accountIdx).list() });
|
|
342
|
+
} catch (e) { return ok({ error: e.message }, true); }
|
|
343
|
+
},
|
|
344
|
+
{ tag: "TabBrowser" }
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
registerTool(
|
|
348
|
+
"electron_tabs",
|
|
349
|
+
"列出某 profile 标签窗口的所有标签(每个 = 一个 webContentsId,可单独控制,像 Chrome target)。",
|
|
350
|
+
z.object({ accountIdx: z.number().describe("账户索引(profile)") }),
|
|
351
|
+
async ({ accountIdx }) => {
|
|
352
|
+
try {
|
|
353
|
+
const m = managers.get(accountIdx);
|
|
354
|
+
return ok({ accountIdx, tabs: m && !m.win.isDestroyed() ? m.list() : [] });
|
|
355
|
+
} catch (e) { return ok({ error: e.message }, true); }
|
|
356
|
+
},
|
|
357
|
+
{ tag: "TabBrowser" }
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
registerTool(
|
|
361
|
+
"electron_tab_navigate",
|
|
362
|
+
"让某个标签(按 webContentsId)导航到一个网址。",
|
|
363
|
+
z.object({ webContentsId: z.number(), url: z.string() }),
|
|
364
|
+
async ({ webContentsId, url }) => {
|
|
365
|
+
try {
|
|
366
|
+
const wc = webContents.fromId(webContentsId);
|
|
367
|
+
if (!wc) throw new Error(`tab ${webContentsId} not found`);
|
|
368
|
+
await wc.loadURL(String(url));
|
|
369
|
+
return ok({ success: true, webContentsId, url });
|
|
370
|
+
} catch (e) { return ok({ error: e.message }, true); }
|
|
371
|
+
},
|
|
372
|
+
{ tag: "TabBrowser" }
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
registerTool(
|
|
376
|
+
"electron_tab_eval",
|
|
377
|
+
"在某个标签(按 webContentsId)的页面执行 JS 并返回结果。",
|
|
378
|
+
z.object({ webContentsId: z.number(), code: z.string() }),
|
|
379
|
+
async ({ webContentsId, code }) => {
|
|
380
|
+
try {
|
|
381
|
+
const wc = webContents.fromId(webContentsId);
|
|
382
|
+
if (!wc) throw new Error(`tab ${webContentsId} not found`);
|
|
383
|
+
const result = await wc.executeJavaScript(String(code), true);
|
|
384
|
+
return ok({ success: true, webContentsId, result });
|
|
385
|
+
} catch (e) { return ok({ error: e.message }, true); }
|
|
386
|
+
},
|
|
387
|
+
{ tag: "TabBrowser" }
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
registerTool(
|
|
391
|
+
"electron_tab_screenshot",
|
|
392
|
+
"截取某个标签(按 webContentsId)。走 CDP,后台标签也能截。",
|
|
393
|
+
z.object({ webContentsId: z.number(), format: z.enum(["png", "jpeg"]).optional().default("jpeg") }),
|
|
394
|
+
async ({ webContentsId, format }) => {
|
|
395
|
+
try {
|
|
396
|
+
const dataUrl = await tabScreenshot(webContentsId, format);
|
|
397
|
+
return ok({ success: true, webContentsId, format, base64: dataUrl });
|
|
398
|
+
} catch (e) { return ok({ error: e.message }, true); }
|
|
399
|
+
},
|
|
400
|
+
{ tag: "TabBrowser" }
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
registerTool(
|
|
404
|
+
"electron_tab_activate",
|
|
405
|
+
"把某个标签(按 webContentsId)切到前台。",
|
|
406
|
+
z.object({ webContentsId: z.number() }),
|
|
407
|
+
async ({ webContentsId }) => {
|
|
408
|
+
try {
|
|
409
|
+
const m = findManagerByTab(webContentsId);
|
|
410
|
+
if (!m) throw new Error(`tab ${webContentsId} not found`);
|
|
411
|
+
return ok({ success: m.activate(webContentsId), webContentsId });
|
|
412
|
+
} catch (e) { return ok({ error: e.message }, true); }
|
|
413
|
+
},
|
|
414
|
+
{ tag: "TabBrowser" }
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
registerTool(
|
|
418
|
+
"electron_tab_close",
|
|
419
|
+
"关闭某个标签(按 webContentsId)。",
|
|
420
|
+
z.object({ webContentsId: z.number() }),
|
|
421
|
+
async ({ webContentsId }) => {
|
|
422
|
+
try {
|
|
423
|
+
const m = findManagerByTab(webContentsId);
|
|
424
|
+
if (!m) throw new Error(`tab ${webContentsId} not found`);
|
|
425
|
+
return ok({ success: m.close(webContentsId), webContentsId });
|
|
426
|
+
} catch (e) { return ok({ error: e.message }, true); }
|
|
427
|
+
},
|
|
428
|
+
{ tag: "TabBrowser" }
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Reload the profile-N tab whose URL matches (origin+pathname); if none is open,
|
|
433
|
+
// open it. Used by the homepage cloud-team card's ⋯ "刷新窗口".
|
|
434
|
+
async function reloadTabByUrl(accountIdx, url, opts = {}) {
|
|
435
|
+
const m = managers.get(accountIdx);
|
|
436
|
+
if (m && !m.win.isDestroyed()) {
|
|
437
|
+
const key = stripVol(url);
|
|
438
|
+
const tab = m.tabs.find((t) => stripVol(t.url) === key);
|
|
439
|
+
if (tab) {
|
|
440
|
+
try { tab.view.webContents.reload(); } catch (e) {}
|
|
441
|
+
try { m.activate(tab.id); m.win.show(); m.win.focus(); } catch (e) {}
|
|
442
|
+
return { ok: true, winId: m.win.id, reloaded: true };
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const r = await openTab(accountIdx, url, { systemOpen: true, trusted: !!opts.trusted, title: opts.title || "" });
|
|
446
|
+
return { ok: true, winId: r.winId, opened: true };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
registerTabBrowserTools.openTab = openTab;
|
|
450
|
+
registerTabBrowserTools.reloadTabByUrl = reloadTabByUrl;
|
|
451
|
+
registerTabBrowserTools.openHomeWindow = openHomeWindow;
|
|
452
|
+
registerTabBrowserTools.ensureManager = ensureManager;
|
|
453
|
+
module.exports = registerTabBrowserTools;
|
|
@@ -39,10 +39,15 @@ function resolveWindowId(input = {}, context = {}) {
|
|
|
39
39
|
function registerTools(registerTool) {
|
|
40
40
|
registerTool(
|
|
41
41
|
"get_windows",
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
`获取窗口列表(活动窗口 + 已关闭但持久化保留的窗口)。是窗口管理和自动化操作的基础工具。
|
|
43
|
+
|
|
44
|
+
- status:"open" 的是当前活动窗口;status:"closed" 的是关闭后仍保留在持久化注册表里的记录(可用 reopen_window + windowKey 原样重开)。
|
|
45
|
+
- windowKey: 跨重启稳定的持久标识(reopen_window 用);id 仅活动窗口有,重启后会变。
|
|
46
|
+
|
|
44
47
|
返回信息包括:
|
|
45
|
-
- id:
|
|
48
|
+
- id: 活动窗口的运行时标识符(已关闭窗口为 null;调用其他 invoke_window 工具时必需)
|
|
49
|
+
- windowKey: 持久窗口标识(重开/跨重启用)
|
|
50
|
+
- status: "open"(活动)| "closed"(已关闭,记录保留)
|
|
46
51
|
- title/url: 窗口当前的标题和网址
|
|
47
52
|
- debuggerIsAttached: 调试器是否已附加
|
|
48
53
|
- isActive/isVisible: 窗口焦点和可见性状态
|
|
@@ -58,9 +63,31 @@ function registerTools(registerTool) {
|
|
|
58
63
|
z.object({}),
|
|
59
64
|
async () => {
|
|
60
65
|
try {
|
|
61
|
-
const
|
|
66
|
+
const registry = require("../utils/window-registry");
|
|
67
|
+
// Live windows (status "open"), annotated with their persistent windowKey.
|
|
68
|
+
const live = BrowserWindow.getAllWindows()
|
|
62
69
|
.map(getWindowInfo)
|
|
63
|
-
.filter((w) => w !== null)
|
|
70
|
+
.filter((w) => w !== null)
|
|
71
|
+
.map((w) => ({ ...w, status: "open", windowKey: registry.keyForLiveId(w.id) }));
|
|
72
|
+
// Closed windows from the persistent registry — kept (not deleted) so
|
|
73
|
+
// the list survives close + restart and can be re-opened (reopen_window).
|
|
74
|
+
const liveKeys = new Set(live.map((w) => w.windowKey).filter(Boolean));
|
|
75
|
+
const closed = registry
|
|
76
|
+
.list()
|
|
77
|
+
.filter((e) => e.status === "closed" && !liveKeys.has(e.windowKey))
|
|
78
|
+
.map((e) => ({
|
|
79
|
+
id: null,
|
|
80
|
+
windowKey: e.windowKey,
|
|
81
|
+
status: "closed",
|
|
82
|
+
title: e.title,
|
|
83
|
+
url: e.url,
|
|
84
|
+
accountIdx: e.accountIdx,
|
|
85
|
+
bounds: e.bounds,
|
|
86
|
+
isVisible: false,
|
|
87
|
+
isDestroyed: true,
|
|
88
|
+
closedAt: e.closedAt,
|
|
89
|
+
}));
|
|
90
|
+
const windows = [...live, ...closed];
|
|
64
91
|
return { content: [{ type: "text", text: JSON.stringify(windows, null, 2) }] };
|
|
65
92
|
} catch (error) {
|
|
66
93
|
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
@@ -94,8 +121,11 @@ function registerTools(registerTool) {
|
|
|
94
121
|
accountIdx: z
|
|
95
122
|
.number()
|
|
96
123
|
.optional()
|
|
97
|
-
.default(
|
|
98
|
-
.describe(
|
|
124
|
+
.default(1)
|
|
125
|
+
.describe(
|
|
126
|
+
"窗口所在帐户。账户 0 是系统级保留(平台 homepage / 系统窗口),agent 开窗请用 >0;不指定时默认 1。" +
|
|
127
|
+
"帐户可以是 1,2,3...每个相同帐户下面的所有窗口共享缓存/cookie/session/proxy。"
|
|
128
|
+
),
|
|
99
129
|
reuseWindow: z
|
|
100
130
|
.boolean()
|
|
101
131
|
.optional()
|
|
@@ -159,6 +189,33 @@ function registerTools(registerTool) {
|
|
|
159
189
|
{ tag: "Window" }
|
|
160
190
|
);
|
|
161
191
|
|
|
192
|
+
registerTool(
|
|
193
|
+
"reopen_window",
|
|
194
|
+
"重新打开一个已关闭(status:closed)的窗口。窗口关闭后记录仍保留在持久化注册表里,用 get_windows 返回的 windowKey 即可原样重开(沿用原 url / 帐户 / 位置大小)。",
|
|
195
|
+
z.object({ window_key: z.string().describe("窗口的 windowKey(来自 get_windows)") }),
|
|
196
|
+
async ({ window_key }) => {
|
|
197
|
+
try {
|
|
198
|
+
const registry = require("../utils/window-registry");
|
|
199
|
+
const entry = registry.getByKey(window_key);
|
|
200
|
+
if (!entry) throw new Error(`windowKey ${window_key} not found in registry`);
|
|
201
|
+
const opts = { url: entry.url };
|
|
202
|
+
if (entry.bounds && typeof entry.bounds === "object") Object.assign(opts, entry.bounds);
|
|
203
|
+
const win = createWindow(opts, entry.accountIdx || 0, true);
|
|
204
|
+
return {
|
|
205
|
+
content: [
|
|
206
|
+
{
|
|
207
|
+
type: "text",
|
|
208
|
+
text: `Reopened ${entry.url} as window ${win.id} (windowKey ${window_key})`,
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
};
|
|
212
|
+
} catch (error) {
|
|
213
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
{ tag: "Window" }
|
|
217
|
+
);
|
|
218
|
+
|
|
162
219
|
registerTool(
|
|
163
220
|
"load_url",
|
|
164
221
|
"加载URL",
|
|
@@ -60,6 +60,26 @@ function ourIcns() {
|
|
|
60
60
|
return fs.existsSync(p) ? p : null;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// Declare the desktop's URL schemes (cicy-desktop://, cicy://) in the borrowed
|
|
64
|
+
// Electron.app Info.plist so LaunchServices routes deeplinks (cicy-desktop://
|
|
65
|
+
// addTeam?…) to this app. An UNPACKAGED app CANNOT register a scheme at runtime
|
|
66
|
+
// on macOS — `setAsDefaultProtocolClient()` is a no-op unless the bundle DECLARES
|
|
67
|
+
// the scheme here. Idempotent: skips when our schemes are already declared.
|
|
68
|
+
function ensureURLSchemes(plist) {
|
|
69
|
+
try {
|
|
70
|
+
if (plistGet(plist, "CFBundleURLTypes:0:CFBundleURLSchemes:0") === "cicy-desktop") return false;
|
|
71
|
+
const pb = (cmd) => { try { cp.execFileSync("/usr/libexec/PlistBuddy", ["-c", cmd, plist]); } catch {} };
|
|
72
|
+
pb("Delete :CFBundleURLTypes"); // clear any partial/stale block
|
|
73
|
+
pb("Add :CFBundleURLTypes array");
|
|
74
|
+
pb("Add :CFBundleURLTypes:0 dict");
|
|
75
|
+
pb("Add :CFBundleURLTypes:0:CFBundleURLName string com.cicy.desktop");
|
|
76
|
+
pb("Add :CFBundleURLTypes:0:CFBundleURLSchemes array");
|
|
77
|
+
pb("Add :CFBundleURLTypes:0:CFBundleURLSchemes:0 string cicy-desktop");
|
|
78
|
+
pb("Add :CFBundleURLTypes:0:CFBundleURLSchemes:1 string cicy");
|
|
79
|
+
return true;
|
|
80
|
+
} catch { return false; }
|
|
81
|
+
}
|
|
82
|
+
|
|
63
83
|
// Patch name + icon. Returns true if the bundle NAME changed (caller relaunches).
|
|
64
84
|
function brandMac() {
|
|
65
85
|
const contents = macBundleContents();
|
|
@@ -97,6 +117,11 @@ function brandMac() {
|
|
|
97
117
|
}
|
|
98
118
|
}
|
|
99
119
|
|
|
120
|
+
// 3) Declare cicy-desktop:// / cicy:// so deeplinks route to this app (the
|
|
121
|
+
// lsregister below refreshes LaunchServices to pick it up; no relaunch).
|
|
122
|
+
const urlChanged = ensureURLSchemes(plist);
|
|
123
|
+
if (urlChanged) log.info("[Brand] declared URL schemes cicy-desktop:// / cicy:// in Info.plist");
|
|
124
|
+
|
|
100
125
|
// Nudge LaunchServices / Finder / Dock to drop the cached icon + name.
|
|
101
126
|
try {
|
|
102
127
|
const bundle = path.dirname(contents); // .../Electron.app
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Shared electron-context-menu (ecm) config + a universal attach helper so the
|
|
2
|
+
// SAME i18n'd right-click menu — 重新加载 / 复制 / 粘贴 / 检查元素(DevTools)— applies to
|
|
3
|
+
// EVERY surface: the tab-browser SHELL window (host/"BaseWindow"), BrowserView
|
|
4
|
+
// tabs, <webview> guests, popups, the homepage window. ecm only auto-attaches to
|
|
5
|
+
// a BrowserWindow's MAIN webContents, so the shell window and guests otherwise
|
|
6
|
+
// fall back to the OS-native menu; attachContextMenu() (wired on app
|
|
7
|
+
// 'web-contents-created' in main.js) closes that gap, guarded so nothing
|
|
8
|
+
// double-pops.
|
|
9
|
+
const { default: contextMenu } = require("electron-context-menu");
|
|
10
|
+
|
|
11
|
+
// ecm hands prepend/append the same `win` it was attached with — a BrowserWindow
|
|
12
|
+
// in auto-attach mode, or the raw webContents when we pass `{ window: wc }`.
|
|
13
|
+
// Resolve to the webContents either way.
|
|
14
|
+
function wcOf(win) {
|
|
15
|
+
try { return win && win.webContents ? win.webContents : win; } catch (e) { return win; }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const OPTIONS = {
|
|
19
|
+
showLookUpSelection: true,
|
|
20
|
+
showSearchWithGoogle: true,
|
|
21
|
+
showCopyImage: true,
|
|
22
|
+
showCopyImageAddress: true,
|
|
23
|
+
showSaveImageAs: true,
|
|
24
|
+
showCopyVideoAddress: true,
|
|
25
|
+
showSaveVideoAs: true,
|
|
26
|
+
showCopyLink: true,
|
|
27
|
+
showSaveLinkAs: true,
|
|
28
|
+
// 检查元素 → webContents.inspectElement(x, y): opens DevTools focused on the
|
|
29
|
+
// node under the cursor. Works on host windows AND <webview> guests.
|
|
30
|
+
showInspectElement: true,
|
|
31
|
+
showServices: true,
|
|
32
|
+
// ecm has no built-in Reload item — add ONLY 重新加载 at the top. NO 切换开发者工具
|
|
33
|
+
// anywhere (removed per master). <webview> guests keep their own custom menu —
|
|
34
|
+
// see attachContextMenu.
|
|
35
|
+
prepend: (_defaultActions, _params, win) => {
|
|
36
|
+
const wc = wcOf(win);
|
|
37
|
+
return [
|
|
38
|
+
{ label: "重新加载", click: () => { try { if (wc) wc.reload(); } catch (e) {} } },
|
|
39
|
+
{ type: "separator" },
|
|
40
|
+
];
|
|
41
|
+
},
|
|
42
|
+
labels: {
|
|
43
|
+
cut: "剪切",
|
|
44
|
+
copy: "复制",
|
|
45
|
+
paste: "粘贴",
|
|
46
|
+
selectAll: "全选",
|
|
47
|
+
inspectElement: "检查元素",
|
|
48
|
+
services: "服务",
|
|
49
|
+
lookUpSelection: "查找选中内容",
|
|
50
|
+
searchWithGoogle: "用 Google 搜索",
|
|
51
|
+
copyImage: "复制图片",
|
|
52
|
+
copyImageAddress: "复制图片地址",
|
|
53
|
+
saveImage: "保存图片",
|
|
54
|
+
copyVideoAddress: "复制视频地址",
|
|
55
|
+
saveVideo: "保存视频",
|
|
56
|
+
copyLink: "复制链接",
|
|
57
|
+
saveLinkAs: "链接另存为...",
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Attach the menu to the host window + BrowserView tabs only. <webview> guests
|
|
62
|
+
// are EXCLUDED on purpose: they carry their OWN custom context menu (e.g.
|
|
63
|
+
// cicy-code's WebFrame / the gotty terminal), and an ecm native menu would cover
|
|
64
|
+
// it. Idempotent via __cicyCtxMenu so the web-contents-created hook + the
|
|
65
|
+
// tab-browser's per-tab call never double-pop.
|
|
66
|
+
function attachContextMenu(wc) {
|
|
67
|
+
try {
|
|
68
|
+
if (!wc || (wc.isDestroyed && wc.isDestroyed())) return;
|
|
69
|
+
if (wc.__cicyCtxMenu) return;
|
|
70
|
+
const t = wc.getType && wc.getType();
|
|
71
|
+
if (t !== "window" && t !== "browserView") return; // skip <webview> + others
|
|
72
|
+
wc.__cicyCtxMenu = true;
|
|
73
|
+
contextMenu({ ...OPTIONS, window: wc });
|
|
74
|
+
} catch (e) {}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = OPTIONS;
|
|
78
|
+
// Non-enumerable so spreading the options object (`{ ...CTX_MENU_OPTS }`) doesn't
|
|
79
|
+
// leak a function into ecm's option set.
|
|
80
|
+
Object.defineProperty(module.exports, "attachContextMenu", { value: attachContextMenu, enumerable: false });
|