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
package/src/main.js
CHANGED
|
@@ -14,40 +14,13 @@ const { setupAppIcons } = require("./tray");
|
|
|
14
14
|
const { brandHostElectron } = require("./utils/brand-host-electron");
|
|
15
15
|
const appUpdater = require("./app-updater");
|
|
16
16
|
|
|
17
|
-
// 🎯
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
showCopyVideoAddress: true,
|
|
25
|
-
showSaveVideoAs: true,
|
|
26
|
-
showCopyLink: true,
|
|
27
|
-
showSaveLinkAs: true,
|
|
28
|
-
showInspectElement: true,
|
|
29
|
-
showServices: true,
|
|
30
|
-
labels: {
|
|
31
|
-
cut: "剪切",
|
|
32
|
-
copy: "复制",
|
|
33
|
-
paste: "粘贴",
|
|
34
|
-
selectAll: "全选",
|
|
35
|
-
reload: "重新加载",
|
|
36
|
-
forceReload: "强制重新加载",
|
|
37
|
-
toggleDevTools: "切换开发者工具",
|
|
38
|
-
inspectElement: "检查元素",
|
|
39
|
-
services: "服务",
|
|
40
|
-
lookUpSelection: "查找选中内容",
|
|
41
|
-
searchWithGoogle: "用 Google 搜索",
|
|
42
|
-
copyImage: "复制图片",
|
|
43
|
-
copyImageAddress: "复制图片地址",
|
|
44
|
-
saveImage: "保存图片",
|
|
45
|
-
copyVideoAddress: "复制视频地址",
|
|
46
|
-
saveVideo: "保存视频",
|
|
47
|
-
copyLink: "复制链接",
|
|
48
|
-
saveLinkAs: "链接另存为...",
|
|
49
|
-
},
|
|
50
|
-
});
|
|
17
|
+
// 🎯 Right-click menu — attach the SAME menu to EVERY webContents (host window,
|
|
18
|
+
// tab, <webview>, popup) via 'web-contents-created'. ecm only auto-attaches to a
|
|
19
|
+
// BrowserWindow's MAIN webContents, so the tab-browser SHELL window ("BaseWindow")
|
|
20
|
+
// and guests fell back to the OS-native menu; this unifies them and adds 重新加载
|
|
21
|
+
// + 切换开发者工具 + 检查元素 everywhere (see utils/context-menu-options.js).
|
|
22
|
+
const { attachContextMenu } = require("./utils/context-menu-options");
|
|
23
|
+
electronApp.on("web-contents-created", (_e, wc) => attachContextMenu(wc));
|
|
51
24
|
|
|
52
25
|
// Setup Electron flags IMMEDIATELY after require
|
|
53
26
|
electronApp.commandLine.appendSwitch("ignore-certificate-errors");
|
|
@@ -89,7 +62,6 @@ const { loadToolCatalog } = require("./server/tool-catalog");
|
|
|
89
62
|
const { executeTool } = require("./server/tool-executor");
|
|
90
63
|
const { getWorkerIdentity } = require("./cluster/worker-identity");
|
|
91
64
|
const { listLocalAgents } = require("./cluster/local-agent-registry");
|
|
92
|
-
const { listArtifacts } = require("./cluster/artifact-registry");
|
|
93
65
|
const { WorkerClient } = require("./cluster/worker-client");
|
|
94
66
|
const { getChromeRuntimeRegistry } = require("./chrome/runtime-registry");
|
|
95
67
|
|
|
@@ -147,15 +119,30 @@ for (const s of DEEPLINK_SCHEMES) {
|
|
|
147
119
|
// Linux when the URL is in argv). Queue them and flush whenever a window
|
|
148
120
|
// finishes loading. The renderer subscribes via window.cicy.deeplink.onAddTeam.
|
|
149
121
|
const __pendingDeepLinks = [];
|
|
150
|
-
|
|
122
|
+
// Deep-link delivery targets = every BrowserWindow's webContents PLUS the
|
|
123
|
+
// homepage tab's webContents. The homepage is now a BrowserView tab (not a
|
|
124
|
+
// BrowserWindow), so getAllWindows() alone would miss it and cicy://addTeam
|
|
125
|
+
// would only refresh on the next poll instead of instantly.
|
|
126
|
+
function deepLinkTargets() {
|
|
151
127
|
const { BrowserWindow } = require("electron");
|
|
152
|
-
const
|
|
153
|
-
|
|
128
|
+
const targets = BrowserWindow.getAllWindows()
|
|
129
|
+
.filter((w) => !w.isDestroyed())
|
|
130
|
+
.map((w) => w.webContents);
|
|
131
|
+
try {
|
|
132
|
+
const hw = require("./backends/homepage-window").getHomepageWindow();
|
|
133
|
+
const wc = hw && hw.webContents;
|
|
134
|
+
if (wc && !wc.isDestroyed() && !targets.includes(wc)) targets.push(wc);
|
|
135
|
+
} catch {}
|
|
136
|
+
return targets;
|
|
137
|
+
}
|
|
138
|
+
function broadcastDeepLink(channel, payload) {
|
|
139
|
+
const targets = deepLinkTargets();
|
|
140
|
+
if (targets.length === 0) {
|
|
154
141
|
__pendingDeepLinks.push({ channel, payload });
|
|
155
142
|
return;
|
|
156
143
|
}
|
|
157
|
-
for (const
|
|
158
|
-
try {
|
|
144
|
+
for (const wc of targets) {
|
|
145
|
+
try { wc.send(channel, payload); } catch {}
|
|
159
146
|
}
|
|
160
147
|
}
|
|
161
148
|
|
|
@@ -164,13 +151,12 @@ function broadcastDeepLink(channel, payload) {
|
|
|
164
151
|
// even when it was the URL that started the app in the first place.
|
|
165
152
|
function flushPendingDeepLinks() {
|
|
166
153
|
if (__pendingDeepLinks.length === 0) return;
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
if (wins.length === 0) return;
|
|
154
|
+
const targets = deepLinkTargets();
|
|
155
|
+
if (targets.length === 0) return;
|
|
170
156
|
const drained = __pendingDeepLinks.splice(0, __pendingDeepLinks.length);
|
|
171
157
|
for (const { channel, payload } of drained) {
|
|
172
|
-
for (const
|
|
173
|
-
try {
|
|
158
|
+
for (const wc of targets) {
|
|
159
|
+
try { wc.send(channel, payload); } catch {}
|
|
174
160
|
}
|
|
175
161
|
}
|
|
176
162
|
}
|
|
@@ -270,6 +256,17 @@ const {
|
|
|
270
256
|
chromeUserDataRoot,
|
|
271
257
|
chromeDebuggerBasePort,
|
|
272
258
|
} = parseArgs();
|
|
259
|
+
|
|
260
|
+
// The two remote-control surfaces are the HTTP/MCP tool server (PORT, default
|
|
261
|
+
// 8101) and the Chrome DevTools Protocol debug port (9221). CDP has NO
|
|
262
|
+
// authentication — anyone who can reach 9221 can drive every Electron window
|
|
263
|
+
// (run arbitrary JS, read the DOM, navigate). So gate BOTH behind the same
|
|
264
|
+
// opt-in: a default desktop install (no --mcp, no CICY_DESKTOP_HTTP, no
|
|
265
|
+
// CICY_MASTER_URL) opens neither, removing the unauthenticated CDP surface
|
|
266
|
+
// entirely unless automation is actually wanted.
|
|
267
|
+
const automationEnabled =
|
|
268
|
+
process.env.CICY_DESKTOP_HTTP === "1" || enableMcp || !!process.env.CICY_MASTER_URL;
|
|
269
|
+
|
|
273
270
|
config.port = PORT;
|
|
274
271
|
if (chromeBinary) {
|
|
275
272
|
config.chromeBinary = chromeBinary;
|
|
@@ -399,7 +396,6 @@ function getWorkerSnapshot(authManager) {
|
|
|
399
396
|
.map((tool) => tool.name),
|
|
400
397
|
agents: listLocalAgents(),
|
|
401
398
|
chromeProfiles: chromeRuntimeRegistry.list(),
|
|
402
|
-
artifacts: listArtifacts(),
|
|
403
399
|
resources: {
|
|
404
400
|
pid: process.pid,
|
|
405
401
|
memory: process.memoryUsage(),
|
|
@@ -446,10 +442,6 @@ app.get("/api/agents", authMiddleware, (req, res) => {
|
|
|
446
442
|
res.json({ agents: listLocalAgents() });
|
|
447
443
|
});
|
|
448
444
|
|
|
449
|
-
app.get("/api/artifacts", authMiddleware, (req, res) => {
|
|
450
|
-
res.json({ artifacts: listArtifacts() });
|
|
451
|
-
});
|
|
452
|
-
|
|
453
445
|
app.use(
|
|
454
446
|
"/api/chrome",
|
|
455
447
|
createChromeManagementRoutes({
|
|
@@ -464,34 +456,143 @@ app.use(
|
|
|
464
456
|
// Start server
|
|
465
457
|
const server = http.createServer(app);
|
|
466
458
|
|
|
467
|
-
// 必须在 whenReady
|
|
468
|
-
|
|
469
|
-
|
|
459
|
+
// 必须在 whenReady 之前设置调试端口。CDP 无鉴权,仅在自动化启用时才开,并显式
|
|
460
|
+
// 绑回环地址(默认已是 127.0.0.1,显式设置防止意外暴露到 0.0.0.0)。
|
|
461
|
+
if (automationEnabled) {
|
|
462
|
+
electronApp.commandLine.appendSwitch("remote-debugging-port", "9221");
|
|
463
|
+
electronApp.commandLine.appendSwitch("remote-debugging-address", "127.0.0.1");
|
|
464
|
+
log.info("[MCP] Remote debugging enabled on 127.0.0.1:9221 (automation enabled)");
|
|
465
|
+
} else {
|
|
466
|
+
log.info("[MCP] Remote debugging port NOT opened (automation disabled — set --mcp / CICY_DESKTOP_HTTP=1 / CICY_MASTER_URL to enable)");
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Register the cicy:// scheme (tab-browser start page) — must run before ready.
|
|
470
|
+
require("./tabbrowser/newtab-protocol").registerScheme();
|
|
470
471
|
|
|
471
472
|
// IPC Bridge: expose all RPC tools to renderer via ipcMain.handle
|
|
472
473
|
const { ipcMain } = require("electron");
|
|
474
|
+
const { isDangerousTool, ensureRpcGrant, ensureOriginAuthorized, originDecision, startOriginModal } = require("./utils/rpc-guard");
|
|
475
|
+
// Sentinels returned (as normal tool results, so useDesktopEvents forwards their
|
|
476
|
+
// text verbatim) to a NON-BLOCKING caller while origin consent is undecided/denied.
|
|
477
|
+
// The agent/skill CLI polls on __CICY_AUTH_PENDING__ and stops on __CICY_AUTH_DENIED__.
|
|
478
|
+
const AUTH_PENDING_RESULT = { content: [{ type: "text", text: "__CICY_AUTH_PENDING__" }], isError: false };
|
|
479
|
+
const AUTH_DENIED_RESULT = { content: [{ type: "text", text: "__CICY_AUTH_DENIED__" }], isError: false };
|
|
480
|
+
const { audit, argsPreview } = require("./utils/rpc-audit");
|
|
481
|
+
function rpcOrigin(event) {
|
|
482
|
+
try { return new URL(event.sender.getURL()).origin; } catch { return (event && event.sender && event.sender.getURL && event.sender.getURL()) || "(unknown)"; }
|
|
483
|
+
}
|
|
484
|
+
async function dispatchRpc(event, toolName, args) {
|
|
485
|
+
const result = await executeTool(toolName, args || {}, {
|
|
486
|
+
transport: "ipc",
|
|
487
|
+
toolName,
|
|
488
|
+
controlSessionId: args?.controlSessionId || null,
|
|
489
|
+
agentId: args?.agentId || null,
|
|
490
|
+
runtimeSessionId: args?.runtimeSessionId || null,
|
|
491
|
+
windowRef: args?.windowRef || null,
|
|
492
|
+
accountIdx: args?.accountIdx,
|
|
493
|
+
worker: getWorkerIdentity(),
|
|
494
|
+
webContentsId: event.sender.id,
|
|
495
|
+
});
|
|
496
|
+
return result;
|
|
497
|
+
}
|
|
498
|
+
// "rpc" — unguarded full bridge for the first-party homepage system UI only.
|
|
473
499
|
ipcMain.handle("rpc", async (event, toolName, args) => {
|
|
474
500
|
console.log("[IPC Bridge] called:", toolName, JSON.stringify(args));
|
|
501
|
+
const origin = rpcOrigin(event);
|
|
502
|
+
const danger = isDangerousTool(toolName);
|
|
475
503
|
try {
|
|
476
|
-
const result = await
|
|
477
|
-
transport: "ipc",
|
|
478
|
-
toolName,
|
|
479
|
-
controlSessionId: args?.controlSessionId || null,
|
|
480
|
-
agentId: args?.agentId || null,
|
|
481
|
-
runtimeSessionId: args?.runtimeSessionId || null,
|
|
482
|
-
windowRef: args?.windowRef || null,
|
|
483
|
-
accountIdx: args?.accountIdx,
|
|
484
|
-
worker: getWorkerIdentity(),
|
|
485
|
-
webContentsId: event.sender.id,
|
|
486
|
-
});
|
|
504
|
+
const result = await dispatchRpc(event, toolName, args);
|
|
487
505
|
console.log("[IPC Bridge] success:", toolName);
|
|
506
|
+
audit({ kind: "rpc", channel: "rpc", origin, tool: toolName, dangerous: danger, ok: true, args: argsPreview(toolName, args) });
|
|
488
507
|
return result;
|
|
489
508
|
} catch (e) {
|
|
490
509
|
console.error("[IPC Bridge] error:", toolName, e.message);
|
|
510
|
+
audit({ kind: "rpc", channel: "rpc", origin, tool: toolName, dangerous: danger, ok: false, error: e.message, args: argsPreview(toolName, args) });
|
|
511
|
+
throw e;
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
// FIFO concurrency limiter for the guarded path. While the consent modal is open
|
|
515
|
+
// a page (or the cloud rpc_call bridge) can pile up many RPC calls all awaiting
|
|
516
|
+
// the same authorization promise; the moment the user clicks 允许 they would ALL
|
|
517
|
+
// resolve and dispatch in one microtask flush, pegging the main thread for a beat
|
|
518
|
+
// ("点完之后卡一阵"). The limiter spreads that burst over a few in-flight slots —
|
|
519
|
+
// a single call still runs immediately (slot is free), only true bursts queue.
|
|
520
|
+
function makeLimiter(max) {
|
|
521
|
+
let active = 0;
|
|
522
|
+
const q = [];
|
|
523
|
+
const pump = () => {
|
|
524
|
+
while (active < max && q.length) {
|
|
525
|
+
active++;
|
|
526
|
+
const { fn, resolve, reject } = q.shift();
|
|
527
|
+
Promise.resolve().then(fn).then(
|
|
528
|
+
(v) => { active--; resolve(v); pump(); },
|
|
529
|
+
(e) => { active--; reject(e); pump(); },
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
return (fn) => new Promise((resolve, reject) => { q.push({ fn, resolve, reject }); pump(); });
|
|
534
|
+
}
|
|
535
|
+
const guardedDispatchLimit = makeLimiter(6);
|
|
536
|
+
|
|
537
|
+
// "rpc:guarded" — for every non-homepage renderer (team-helper webview, trusted
|
|
538
|
+
// remote pages, injected scripts). Dangerous tools (exec_*/file_*) require an
|
|
539
|
+
// explicit per-page user grant so a trusted-origin XSS can't silently run code.
|
|
540
|
+
ipcMain.handle("rpc:guarded", async (event, toolName, args) => {
|
|
541
|
+
console.log("[IPC Bridge] guarded call:", toolName);
|
|
542
|
+
// Domain-allowlist gate: a non-allowlisted origin must be authorized via a
|
|
543
|
+
// consent modal (deny / allow once / add to allowlist) before it can use the
|
|
544
|
+
// bridge at all. Allowlisted origins pass straight through.
|
|
545
|
+
const origin = rpcOrigin(event);
|
|
546
|
+
const danger = isDangerousTool(toolName);
|
|
547
|
+
// Agent/skill calls tag args with __cicyAuthNonBlocking: their transport is one
|
|
548
|
+
// fixed-timeout HTTP request, so they can't sit on the blocking consent modal.
|
|
549
|
+
// For an undecided origin we pop the modal in the BACKGROUND and hand back a
|
|
550
|
+
// PENDING sentinel the caller polls on; a denied origin gets a DENIED sentinel.
|
|
551
|
+
// In-page callers (no tag) keep the original blocking behaviour.
|
|
552
|
+
let nonBlockingAuth = false;
|
|
553
|
+
if (args && typeof args === "object" && args.__cicyAuthNonBlocking) {
|
|
554
|
+
nonBlockingAuth = true;
|
|
555
|
+
args = { ...args };
|
|
556
|
+
delete args.__cicyAuthNonBlocking;
|
|
557
|
+
}
|
|
558
|
+
if (nonBlockingAuth) {
|
|
559
|
+
const decision = originDecision(event); // "allow" | "deny" | "unknown" (no prompt)
|
|
560
|
+
if (decision === "deny") {
|
|
561
|
+
audit({ kind: "rpc", channel: "rpc:guarded", origin, tool: toolName, dangerous: danger, ok: false, error: "origin-denied", args: argsPreview(toolName, args) });
|
|
562
|
+
return AUTH_DENIED_RESULT;
|
|
563
|
+
}
|
|
564
|
+
if (decision === "unknown") {
|
|
565
|
+
startOriginModal(event); // background consent, deduped per origin
|
|
566
|
+
audit({ kind: "rpc", channel: "rpc:guarded", origin, tool: toolName, dangerous: danger, ok: false, error: "origin-pending", args: argsPreview(toolName, args) });
|
|
567
|
+
return AUTH_PENDING_RESULT;
|
|
568
|
+
}
|
|
569
|
+
// "allow" → fall through to the normal path
|
|
570
|
+
} else {
|
|
571
|
+
const originOk = await ensureOriginAuthorized(event);
|
|
572
|
+
if (!originOk) {
|
|
573
|
+
audit({ kind: "rpc", channel: "rpc:guarded", origin, tool: toolName, dangerous: danger, ok: false, error: "origin-unauthorized", args: argsPreview(toolName, args) });
|
|
574
|
+
throw new Error(`未授权站点访问桌面 RPC(rpc:guarded:域名未加入白名单)`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (danger) {
|
|
578
|
+
const ok = await ensureRpcGrant(event, toolName, args);
|
|
579
|
+
if (!ok) {
|
|
580
|
+
audit({ kind: "rpc", channel: "rpc:guarded", origin, tool: toolName, dangerous: true, ok: false, error: "grant-denied", args: argsPreview(toolName, args) });
|
|
581
|
+
throw new Error(`已拒绝敏感操作 ${toolName}(rpc:guarded:来源未获授权)`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
try {
|
|
585
|
+
// Throttled so a post-allow backlog drains a few at a time, not all at once.
|
|
586
|
+
const result = await guardedDispatchLimit(() => dispatchRpc(event, toolName, args));
|
|
587
|
+
audit({ kind: "rpc", channel: "rpc:guarded", origin, tool: toolName, dangerous: danger, ok: true, args: argsPreview(toolName, args) });
|
|
588
|
+
return result;
|
|
589
|
+
} catch (e) {
|
|
590
|
+
console.error("[IPC Bridge] guarded error:", toolName, e.message);
|
|
591
|
+
audit({ kind: "rpc", channel: "rpc:guarded", origin, tool: toolName, dangerous: danger, ok: false, error: e.message, args: argsPreview(toolName, args) });
|
|
491
592
|
throw e;
|
|
492
593
|
}
|
|
493
594
|
});
|
|
494
|
-
console.log("[IPC Bridge]
|
|
595
|
+
console.log("[IPC Bridge] RPC tools available via ipcRenderer.invoke('rpc'|'rpc:guarded', toolName, args)");
|
|
495
596
|
|
|
496
597
|
const workerClient = maybeCreateWorkerClient(authManager);
|
|
497
598
|
|
|
@@ -691,6 +792,11 @@ function startSidecarWatchdog({ intervalMs = 30_000 } = {}) {
|
|
|
691
792
|
|
|
692
793
|
const tick = async () => {
|
|
693
794
|
try {
|
|
795
|
+
// An update() in progress intentionally stops cicy-code (to swap the binary)
|
|
796
|
+
// and starts the new one itself — DON'T let the watchdog respawn the OLD
|
|
797
|
+
// binary into that gap (it would race the swap and the update would "finish"
|
|
798
|
+
// still on the old version). Pause until the update releases the flag.
|
|
799
|
+
if (cicyCodeSidecar.isUpdating && cicyCodeSidecar.isUpdating()) { consecutiveFailures = 0; return; }
|
|
694
800
|
const ok = await cicyCodeSidecar.probeExisting();
|
|
695
801
|
if (ok) { consecutiveFailures = 0; return; }
|
|
696
802
|
consecutiveFailures++;
|
|
@@ -721,6 +827,9 @@ function startSidecarWatchdog({ intervalMs = 30_000 } = {}) {
|
|
|
721
827
|
}
|
|
722
828
|
|
|
723
829
|
electronApp.whenReady().then(async () => {
|
|
830
|
+
// Serve cicy://newtab (tab-browser start page) — must be after ready.
|
|
831
|
+
require("./tabbrowser/newtab-protocol").installHandler();
|
|
832
|
+
|
|
724
833
|
// Re-init i18n now that app is ready — getLocale() returns reliable values
|
|
725
834
|
// only after the ready event. The module-load init may have picked English
|
|
726
835
|
// on platforms (e.g. Windows) where LANG env is unset.
|
|
@@ -746,15 +855,34 @@ electronApp.whenReady().then(async () => {
|
|
|
746
855
|
.catch((e) => log.warn(`[Sidecar] cicy-code start failed: ${e.message}`));
|
|
747
856
|
startSidecarWatchdog();
|
|
748
857
|
|
|
858
|
+
// Auto-register the local sidecar as 本地团队 once :8008 answers (主人:
|
|
859
|
+
// "本地团队没有占位" — a fresh install must show its local team without any
|
|
860
|
+
// manual step). addTeam upserts by host:port + auto-fills api_token from
|
|
861
|
+
// global.json, so re-runs are no-ops; addTeam itself then triggers the
|
|
862
|
+
// cloud team register + gateway-key injection when logged in. A fresh boot
|
|
863
|
+
// may npm-seed the runtime first, so probe for up to ~90s before giving up.
|
|
864
|
+
(async () => {
|
|
865
|
+
const sidecarPort = Number(process.env.CICY_CODE_PORT || 8008);
|
|
866
|
+
const lt = require("./backends/local-teams");
|
|
867
|
+
for (let i = 0; i < 30; i++) {
|
|
868
|
+
try {
|
|
869
|
+
if (await cicyCodeSidecar.probeExisting(sidecarPort)) {
|
|
870
|
+
const r = await lt.addTeam({ base_url: `http://127.0.0.1:${sidecarPort}`, name: "本地团队" });
|
|
871
|
+
if (r && r.ok) log.info(`[Sidecar] local team ${r.upserted ? "refreshed" : "registered"} (${r.id})`);
|
|
872
|
+
else log.warn(`[Sidecar] local team auto-register failed: ${r && r.error}`);
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
} catch (e) { log.warn(`[Sidecar] local team auto-register error: ${e.message}`); }
|
|
876
|
+
await new Promise((res) => setTimeout(res, 3000));
|
|
877
|
+
}
|
|
878
|
+
log.warn(`[Sidecar] local team auto-register gave up — :${sidecarPort} never came up`);
|
|
879
|
+
})();
|
|
880
|
+
|
|
749
881
|
// Backend launcher: app menu + IPC handlers. Menu adds a Backends top-level
|
|
750
882
|
// entry; IPC powers the launcher window (src/backends/launcher.html).
|
|
751
883
|
backendsIPC.register({ sidecarLogPath: path.join(os.homedir(), "logs", "cicy-code-sidecar.log") });
|
|
752
884
|
require("./backends/sidecar-ipc").register({ sidecarLogPath: path.join(os.homedir(), "logs", "cicy-code-sidecar.log") });
|
|
753
885
|
|
|
754
|
-
// window.cicy.artifact bridge — CDP/webContents control of the cicy-code
|
|
755
|
-
// 产物 (artifact) <webview> guest. Injected renderer-side in window-utils.js.
|
|
756
|
-
require("./backends/artifact-ipc").register();
|
|
757
|
-
|
|
758
886
|
// Browser-login loopback listener. Renderer calls auth:login-start when
|
|
759
887
|
// the user clicks Login; main opens a 127.0.0.1 server + the browser,
|
|
760
888
|
// and broadcasts auth:complete back to the homepage window once the
|
|
@@ -799,13 +927,15 @@ electronApp.whenReady().then(async () => {
|
|
|
799
927
|
// machine to the cloud (best-effort; safe to call repeatedly —
|
|
800
928
|
// cloud upserts by (owner, deviceId)).
|
|
801
929
|
try {
|
|
930
|
+
// Order matters: register the DEVICE FIRST, THEN the local team(s).
|
|
931
|
+
// POST /api/team/register 404s when the device isn't registered yet,
|
|
932
|
+
// so firing both concurrently (the old bug) let team-sync race ahead
|
|
933
|
+
// of device-register → 404 → gateway key never injected → apiKey 空.
|
|
934
|
+
// Chain them so syncAllLocalTeams only runs after the device exists.
|
|
802
935
|
require("./cloud/cloud-client")
|
|
803
936
|
.registerDevice()
|
|
804
|
-
.
|
|
805
|
-
|
|
806
|
-
require("./backends/local-teams")
|
|
807
|
-
.syncAllLocalTeams()
|
|
808
|
-
.catch((e) => log.warn(`[cloud] local-team sync (on login) failed: ${e.message}`));
|
|
937
|
+
.then(() => require("./backends/local-teams").syncAllLocalTeams())
|
|
938
|
+
.catch((e) => log.warn(`[cloud] device/team register (on login) failed: ${e.message}`));
|
|
809
939
|
} catch (e) { log.warn(`[cloud] device register hook failed: ${e.message}`); }
|
|
810
940
|
}
|
|
811
941
|
const hw = require("./backends/homepage-window");
|
|
@@ -879,16 +1009,44 @@ electronApp.whenReady().then(async () => {
|
|
|
879
1009
|
// Best-effort and fully non-blocking; a no-op when not logged in (the login
|
|
880
1010
|
// onResult hook above covers the log-in-later case).
|
|
881
1011
|
try {
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
//
|
|
886
|
-
//
|
|
887
|
-
//
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1012
|
+
// 1) Detect egress IP + IP region + system language and persist to
|
|
1013
|
+
// global.json (deviceInfo) — the single source the get_device_info RPC,
|
|
1014
|
+
// the chat-WS register, and the cloud report all read. The detection goes
|
|
1015
|
+
// DIRECT (no proxy), each request times out, and the whole thing is
|
|
1016
|
+
// fire-and-forget (never awaited → does not block startup). Runs even when
|
|
1017
|
+
// not logged in so local config is always populated.
|
|
1018
|
+
// 2) THEN register the device with the cloud (no-op if not logged in) — it
|
|
1019
|
+
// reads the just-persisted ip/region/syslang.
|
|
1020
|
+
// 3) THEN sync local teams (device must register first or /api/team/register 404s).
|
|
1021
|
+
const cc = require("./cloud/cloud-client");
|
|
1022
|
+
let sysLang = "";
|
|
1023
|
+
try { sysLang = (electronApp.getLocale && electronApp.getLocale()) || ""; } catch (_) {}
|
|
1024
|
+
cc.detectAndPersistDeviceInfo({ systemLanguage: sysLang })
|
|
1025
|
+
.then(() => cc.registerDevice())
|
|
1026
|
+
.then(() => require("./backends/local-teams").syncAllLocalTeams())
|
|
1027
|
+
.catch((e) => log.warn(`[cloud] device-info/register (on launch) failed: ${e.message}`));
|
|
1028
|
+
} catch (e) { log.warn(`[cloud] device-info launch hook failed: ${e.message}`); }
|
|
1029
|
+
|
|
1030
|
+
// Cloud↔desktop title reconcile. The homepage drives the FAST cadence by window
|
|
1031
|
+
// visibility (聚焦 ~3s / 切回立即,见 App.jsx + localTeams:syncCloud IPC). This
|
|
1032
|
+
// 30s timer is just the SLOW fallback for when no homepage window is open / it's
|
|
1033
|
+
// hidden / logged-in-but-idle. Best-effort, no-op when logged out.
|
|
1034
|
+
if (!global.__cicyTitleSyncTimer) {
|
|
1035
|
+
global.__cicyTitleSyncTimer = setInterval(() => {
|
|
1036
|
+
try { require("./backends/local-teams").syncAllLocalTeams().catch(() => {}); } catch {}
|
|
1037
|
+
}, 30_000);
|
|
1038
|
+
if (global.__cicyTitleSyncTimer.unref) global.__cicyTitleSyncTimer.unref();
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// Periodic per-window thumbnails → ~/cicy-files/window-thumbs (chrome-style
|
|
1042
|
+
// small previews on disk; override dir via CICY_THUMB_DIR). Best-effort.
|
|
1043
|
+
if (!global.__cicyThumbStarted) {
|
|
1044
|
+
global.__cicyThumbStarted = true;
|
|
1045
|
+
try {
|
|
1046
|
+
const info = require("./utils/window-thumbnails").startWindowThumbnails();
|
|
1047
|
+
log.info(`[thumbs] window thumbnails → ${info.dir} (every ${info.intervalMs}ms, maxW ${info.maxWidth})`);
|
|
1048
|
+
} catch (e) { log.warn(`[thumbs] start failed: ${e.message}`); }
|
|
1049
|
+
}
|
|
892
1050
|
|
|
893
1051
|
// Local-team discovery — reads ~/cicy-ai/global.json's cicyDesktopNodes
|
|
894
1052
|
// and probes each via /api/health. Pure local, never talks to the cloud
|
|
@@ -903,6 +1061,9 @@ electronApp.whenReady().then(async () => {
|
|
|
903
1061
|
__ipcLT.handle("localTeams:remove", (_e, id) => lt.removeTeam(id));
|
|
904
1062
|
__ipcLT.handle("localTeams:update", (_e, payload) => lt.updateTeam(payload?.id, payload?.patch || {}));
|
|
905
1063
|
__ipcLT.handle("localTeams:upgrade", (_e, id) => lt.upgradeTeam(id));
|
|
1064
|
+
// Pull cloud title NOW (homepage calls this on window focus so a dash rename
|
|
1065
|
+
// reflects immediately instead of waiting for the 15s background tick).
|
|
1066
|
+
__ipcLT.handle("localTeams:syncCloud", async () => { try { await lt.syncAllLocalTeams(); return { ok: true }; } catch (e) { return { ok: false, error: e.message }; } });
|
|
906
1067
|
|
|
907
1068
|
// Webview → host-renderer relay. The Team Helper <webview> can't
|
|
908
1069
|
// directly mutate localTeams: instead its preload (webview-preload.js)
|
|
@@ -1074,9 +1235,8 @@ electronApp.whenReady().then(async () => {
|
|
|
1074
1235
|
// are required for the homepage UI itself, which talks to the main
|
|
1075
1236
|
// process via Electron IPC. So skip the listen by default; opt in with
|
|
1076
1237
|
// CICY_DESKTOP_HTTP=1 (or CICY_DESKTOP_HTTP_PORT set explicitly).
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|| !!process.env.CICY_MASTER_URL;
|
|
1238
|
+
// Same opt-in as the CDP debug port above (see `automationEnabled`).
|
|
1239
|
+
const httpEnabled = automationEnabled;
|
|
1080
1240
|
|
|
1081
1241
|
// Code that used to live inside server.listen(...) — startup work that
|
|
1082
1242
|
// needs to happen after whenReady. Pulled out so we can run it whether
|
|
@@ -1085,6 +1245,40 @@ electronApp.whenReady().then(async () => {
|
|
|
1085
1245
|
if (START_URL) {
|
|
1086
1246
|
createWindow({ url: START_URL }, ACCOUNT);
|
|
1087
1247
|
}
|
|
1248
|
+
|
|
1249
|
+
// Persistent window registry: re-open windows that were still open when the
|
|
1250
|
+
// app last quit. Windows the user/agent closed stay "closed" and are not
|
|
1251
|
+
// reopened. Skip any url already live this session (homepage / START_URL).
|
|
1252
|
+
try {
|
|
1253
|
+
const { BrowserWindow } = require("electron");
|
|
1254
|
+
const registry = require("./utils/window-registry");
|
|
1255
|
+
const liveSet = new Set(
|
|
1256
|
+
BrowserWindow.getAllWindows()
|
|
1257
|
+
.map((w) => {
|
|
1258
|
+
try {
|
|
1259
|
+
const part = w.webContents.session.partition || "";
|
|
1260
|
+
const acc = part.startsWith("persist:sandbox-")
|
|
1261
|
+
? parseInt(part.replace("persist:sandbox-", ""), 10)
|
|
1262
|
+
: 0;
|
|
1263
|
+
return `${acc}::${registry.normalizeUrl(w.webContents.getURL())}`;
|
|
1264
|
+
} catch {
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1267
|
+
})
|
|
1268
|
+
.filter(Boolean)
|
|
1269
|
+
);
|
|
1270
|
+
for (const e of registry.staleOpenEntries()) {
|
|
1271
|
+
if (!e.url) continue;
|
|
1272
|
+
if (liveSet.has(`${e.accountIdx || 0}::${registry.normalizeUrl(e.url)}`)) continue;
|
|
1273
|
+
log.info(`[WindowRegistry] Reopening ${e.url} (account ${e.accountIdx || 0})`);
|
|
1274
|
+
const opts = { url: e.url };
|
|
1275
|
+
if (e.bounds && typeof e.bounds === "object") Object.assign(opts, e.bounds);
|
|
1276
|
+
createWindow(opts, e.accountIdx || 0, true);
|
|
1277
|
+
}
|
|
1278
|
+
} catch (err) {
|
|
1279
|
+
log.error(`[WindowRegistry] reopen failed: ${err.message}`);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1088
1282
|
if (workerClient) {
|
|
1089
1283
|
try {
|
|
1090
1284
|
await workerClient.start();
|
|
@@ -1097,7 +1291,7 @@ electronApp.whenReady().then(async () => {
|
|
|
1097
1291
|
|
|
1098
1292
|
if (!httpEnabled) {
|
|
1099
1293
|
log.info(`[MCP] HTTP server skipped (set CICY_DESKTOP_HTTP=1 or pass --mcp to enable)`);
|
|
1100
|
-
log.info(`[MCP] Remote debugger
|
|
1294
|
+
log.info(`[MCP] Remote debugger NOT running (automation disabled)`);
|
|
1101
1295
|
onAppStarted();
|
|
1102
1296
|
} else {
|
|
1103
1297
|
server.listen(PORT, async () => {
|
|
@@ -48,7 +48,7 @@ function getMasterChromeAccountEntry(accountIdx) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
const data = readMasterChromeConfig();
|
|
51
|
-
const key = `
|
|
51
|
+
const key = `profile_${accountIdx}`;
|
|
52
52
|
const entry = data?.[key] || null;
|
|
53
53
|
if (!entry) {
|
|
54
54
|
throw new ChromeProfileResolutionError(`Missing chrome.json entry on master: ${key}`);
|
|
@@ -66,7 +66,7 @@ function normalizeEffectiveChromeProfile({ accountIdx, entry }) {
|
|
|
66
66
|
const rpaDirRaw =
|
|
67
67
|
typeof safeEntry.rpaDir === "string" && safeEntry.rpaDir.length
|
|
68
68
|
? safeEntry.rpaDir
|
|
69
|
-
: `~/chrome/
|
|
69
|
+
: `~/chrome/profile_${accountIdx}`;
|
|
70
70
|
|
|
71
71
|
const orgPathRaw = typeof safeEntry.orgPath === "string" && safeEntry.orgPath.length ? safeEntry.orgPath : null;
|
|
72
72
|
|
package/src/preload-rpc.js
CHANGED