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
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
|
|
7
7
|
<link rel="icon" type="image/png" sizes="256x256" href="./favicon-256.png" />
|
|
8
8
|
<title>CiCy Desktop</title>
|
|
9
|
-
<script type="module" crossorigin src="./assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
9
|
+
<script type="module" crossorigin src="./assets/index-CSsNZgC5.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="./assets/index-CKpaMBKz.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
|
@@ -11,15 +11,40 @@ const path = require("path");
|
|
|
11
11
|
const { BrowserWindow } = require("electron");
|
|
12
12
|
const log = require("electron-log");
|
|
13
13
|
|
|
14
|
+
// Fixed homepage window size (主人令: 写死 930*640, 不能 resize).
|
|
15
|
+
const FIXED_WIDTH = 930;
|
|
16
|
+
const FIXED_HEIGHT = 640;
|
|
17
|
+
|
|
14
18
|
const LOCAL_INDEX = path.join(__dirname, "homepage-react", "index.html");
|
|
15
19
|
|
|
16
20
|
function pickHomepageURL() {
|
|
17
21
|
return `file://${LOCAL_INDEX}`;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
let homepage = null;
|
|
24
|
+
let homepage = null; // standalone fallback window (only if the tab engine fails)
|
|
25
|
+
let homeTabWc = null; // the homepage's resident-tab webContents (primary path)
|
|
21
26
|
|
|
27
|
+
// Primary entry: the homepage is the RESIDENT first tab of profile 0's tab
|
|
28
|
+
// browser window (主人令: homepage = profile 0 的起始页). Clicking a team there
|
|
29
|
+
// opens the team as another tab in the same window. Falls back to a standalone
|
|
30
|
+
// window only if the tab engine throws, so the homepage is never unreachable.
|
|
22
31
|
async function openHomepage() {
|
|
32
|
+
try {
|
|
33
|
+
const tabBrowser = require("../tools/tab-browser-tools");
|
|
34
|
+
const { win, wc } = tabBrowser.openHomeWindow(0, pickHomepageURL());
|
|
35
|
+
if (wc) {
|
|
36
|
+
homeTabWc = wc;
|
|
37
|
+
try { wc.once("destroyed", () => { if (homeTabWc === wc) homeTabWc = null; }); } catch {}
|
|
38
|
+
}
|
|
39
|
+
log.info(`[homepage] opened as profile-0 resident tab (wc=${wc && wc.id})`);
|
|
40
|
+
return win;
|
|
41
|
+
} catch (e) {
|
|
42
|
+
log.warn(`[homepage] tab route failed (${e.message}); using standalone window`);
|
|
43
|
+
return openHomepageStandalone();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function openHomepageStandalone() {
|
|
23
48
|
if (homepage && !homepage.isDestroyed()) {
|
|
24
49
|
if (homepage.isMinimized()) homepage.restore();
|
|
25
50
|
homepage.show();
|
|
@@ -27,10 +52,11 @@ async function openHomepage() {
|
|
|
27
52
|
return homepage;
|
|
28
53
|
}
|
|
29
54
|
homepage = new BrowserWindow({
|
|
30
|
-
width:
|
|
31
|
-
height:
|
|
32
|
-
|
|
33
|
-
|
|
55
|
+
width: FIXED_WIDTH,
|
|
56
|
+
height: FIXED_HEIGHT,
|
|
57
|
+
resizable: false,
|
|
58
|
+
maximizable: false,
|
|
59
|
+
fullscreenable: false,
|
|
34
60
|
title: "CiCy Desktop",
|
|
35
61
|
icon: require("../utils/app-icon").appIconPath(), // npx/unpackaged → set the
|
|
36
62
|
// window+taskbar icon ourselves (no .exe to embed it on Windows).
|
|
@@ -90,7 +116,26 @@ async function openHomepage() {
|
|
|
90
116
|
}
|
|
91
117
|
|
|
92
118
|
function isOpen() {
|
|
93
|
-
return !!(homepage && !homepage.isDestroyed());
|
|
119
|
+
return !!((homeTabWc && !homeTabWc.isDestroyed()) || (homepage && !homepage.isDestroyed()));
|
|
94
120
|
}
|
|
95
121
|
|
|
96
|
-
|
|
122
|
+
// A stable shim that always resolves to the live homepage surface — the resident
|
|
123
|
+
// tab's webContents when present, else the standalone window's. Returned object
|
|
124
|
+
// identity is stable so callers that hold it across the async gap before the tab
|
|
125
|
+
// exists (appUpdater.init) still work once the tab loads. Used for main→renderer
|
|
126
|
+
// pushes: auth:complete (main.js), app:update-state (app-updater.js).
|
|
127
|
+
const homeWinShim = {
|
|
128
|
+
get webContents() {
|
|
129
|
+
if (homeTabWc && !homeTabWc.isDestroyed()) return homeTabWc;
|
|
130
|
+
if (homepage && !homepage.isDestroyed()) return homepage.webContents;
|
|
131
|
+
return null;
|
|
132
|
+
},
|
|
133
|
+
isDestroyed() {
|
|
134
|
+
const wc = this.webContents;
|
|
135
|
+
return !wc || (typeof wc.isDestroyed === "function" && wc.isDestroyed());
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
function getHomepageWindow() { return homeWinShim; }
|
|
140
|
+
|
|
141
|
+
module.exports = { openHomepage, isOpen, getHomepageWindow };
|
package/src/backends/ipc.js
CHANGED
|
@@ -214,6 +214,63 @@ function register(opts = {}) {
|
|
|
214
214
|
return { ok: true, ...readTos() };
|
|
215
215
|
});
|
|
216
216
|
ipcMain.handle("logs:tail", (_e, input) => tailLog(input || {}));
|
|
217
|
+
|
|
218
|
+
// ── Trusted origins (Chrome-style site-settings allowlist) ──────────────────
|
|
219
|
+
// The ONLY user-controlled source of "which sites may receive the electronRPC
|
|
220
|
+
// bridge (= run commands locally)". Every write refreshes the cached set in
|
|
221
|
+
// window-utils so isTrustedUrl() takes effect immediately (no restart).
|
|
222
|
+
ipcMain.handle("trustedOrigins:list", () => {
|
|
223
|
+
return require("../profiles/trusted-origins-store").listForUi();
|
|
224
|
+
});
|
|
225
|
+
ipcMain.handle("trustedOrigins:add", (_e, host) => {
|
|
226
|
+
const r = require("../profiles/trusted-origins-store").add(host);
|
|
227
|
+
try { require("../utils/window-utils").refreshTrustedOrigins(); } catch {}
|
|
228
|
+
return r;
|
|
229
|
+
});
|
|
230
|
+
ipcMain.handle("trustedOrigins:remove", (_e, host) => {
|
|
231
|
+
const r = require("../profiles/trusted-origins-store").remove(host);
|
|
232
|
+
try { require("../utils/window-utils").refreshTrustedOrigins(); } catch {}
|
|
233
|
+
return r;
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// ── Open / reload a URL as a TAB in profile 0's tab browser ─────────────────
|
|
237
|
+
// Homepage cards (incl. cloud team cards) open the same way the local card does
|
|
238
|
+
// — a tab in the current profile (0) — instead of a new window / system browser.
|
|
239
|
+
ipcMain.handle("tabs:open", async (_e, input) => {
|
|
240
|
+
try {
|
|
241
|
+
const tb = require("../tools/tab-browser-tools");
|
|
242
|
+
const r = await tb.openTab(0, String((input && input.url) || ""), { systemOpen: true, trusted: false, title: (input && input.title) || "" });
|
|
243
|
+
return { ok: true, winId: r.winId, tabId: r.tabId };
|
|
244
|
+
} catch (e) { return { ok: false, error: String((e && e.message) || e) }; }
|
|
245
|
+
});
|
|
246
|
+
ipcMain.handle("tabs:reload", async (_e, input) => {
|
|
247
|
+
try {
|
|
248
|
+
const tb = require("../tools/tab-browser-tools");
|
|
249
|
+
return await tb.reloadTabByUrl(0, String((input && input.url) || ""), { title: (input && input.title) || "" });
|
|
250
|
+
} catch (e) { return { ok: false, error: String((e && e.message) || e) }; }
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// ── RPC audit log (read-only viewer) ────────────────────────────────────────
|
|
254
|
+
// JSONL at ~/cicy-ai/db/rpc-audit.log (utils/rpc-audit.js): every electronRPC
|
|
255
|
+
// call + every authorization decision (incl. temporary ones) + allowlist edits.
|
|
256
|
+
// Returns the most recent `limit` entries newest-first; merges the rotated .1
|
|
257
|
+
// file when the live log is short. Read-only — the UI reviews, never mutates.
|
|
258
|
+
ipcMain.handle("rpcAudit:tail", (_e, input) => {
|
|
259
|
+
const limit = Math.max(1, Math.min(2000, Number((input && input.limit) || 300)));
|
|
260
|
+
try {
|
|
261
|
+
const { LOG } = require("../utils/rpc-audit");
|
|
262
|
+
const fs = require("fs");
|
|
263
|
+
const read = (p) => { try { return fs.readFileSync(p, "utf-8").split("\n").filter(Boolean); } catch { return []; } };
|
|
264
|
+
let lines = read(LOG);
|
|
265
|
+
if (lines.length < limit) lines = read(LOG + ".1").concat(lines); // older file first
|
|
266
|
+
const entries = [];
|
|
267
|
+
for (const ln of lines.slice(-limit)) { try { entries.push(JSON.parse(ln)); } catch {} }
|
|
268
|
+
entries.reverse(); // newest first
|
|
269
|
+
return { ok: true, entries, path: LOG };
|
|
270
|
+
} catch (e) {
|
|
271
|
+
return { ok: false, error: String((e && e.message) || e), entries: [] };
|
|
272
|
+
}
|
|
273
|
+
});
|
|
217
274
|
}
|
|
218
275
|
|
|
219
276
|
module.exports = { register, openHomepage };
|
|
@@ -116,11 +116,8 @@ function probeHealth(baseUrl, token) {
|
|
|
116
116
|
res.setEncoding("utf8");
|
|
117
117
|
res.on("data", (c) => { body += c; if (body.length > 8192) body = body.slice(0, 8192); });
|
|
118
118
|
res.on("end", () => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const j = JSON.parse(body);
|
|
122
|
-
ver = j?.version || j?.data?.version || null;
|
|
123
|
-
} catch {}
|
|
119
|
+
// 版本解析唯一来源:require("../sidecar/version").parseHealthVersion
|
|
120
|
+
const ver = require("../sidecar/version").parseHealthVersion(body);
|
|
124
121
|
resolve({
|
|
125
122
|
ok: res.statusCode >= 200 && res.statusCode < 300,
|
|
126
123
|
status: res.statusCode,
|
|
@@ -276,14 +273,27 @@ async function openTeam(id) {
|
|
|
276
273
|
}
|
|
277
274
|
}
|
|
278
275
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
276
|
+
// Open the team as a TAB in account 0's tab browser (一个 profile 一个窗口,
|
|
277
|
+
// 不再每次弹新窗口). trusted=true → the tab gets the electronRPC bridge so the
|
|
278
|
+
// cicy-code SPA keeps working. Falls back to a real window on any failure so
|
|
279
|
+
// opening a team is never blocked.
|
|
280
|
+
try {
|
|
281
|
+
const tabBrowser = require("../tools/tab-browser-tools");
|
|
282
|
+
// tab name = the team's title (not the cicy-code SPA's document.title)
|
|
283
|
+
const r = await tabBrowser.openTab(0, url, { trusted: true, systemOpen: true, title: node.name || id });
|
|
284
|
+
log.info(`[local-teams] open ${id} → tab in win.id=${r.winId} (reused=${r.reused})`);
|
|
285
|
+
return { ok: true, windowId: r.winId, reused: !!r.reused, tabbed: true };
|
|
286
|
+
} catch (e) {
|
|
287
|
+
log.warn(`[local-teams] open ${id} → tab failed (${e.message}); falling back to window`);
|
|
288
|
+
const { createWindow } = require("../utils/window-utils");
|
|
289
|
+
const win = createWindow(
|
|
290
|
+
{ url, title: `Local · ${node.name || id}` },
|
|
291
|
+
0, // accountIdx — local teams all share account 0's session partition
|
|
292
|
+
true, // forceNew — we already determined no match above
|
|
293
|
+
);
|
|
294
|
+
log.info(`[local-teams] open ${id} → new win.id=${win.id}`);
|
|
295
|
+
return { ok: true, windowId: win.id, reused: false };
|
|
296
|
+
}
|
|
287
297
|
}
|
|
288
298
|
|
|
289
299
|
// Is this URL served by something on the local machine (the cicy-code sidecar)?
|
|
@@ -409,17 +419,49 @@ async function syncNameToCloud(id) {
|
|
|
409
419
|
if (!cc.loginToken || !cc.loginToken()) return; // not logged in
|
|
410
420
|
const node = readNodes()[id];
|
|
411
421
|
if (!node || !isLocalOrigin(node.base_url || "")) return;
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
log.
|
|
422
|
+
let reg = await cc.registerTeam({ teamId: node.cloud_team_id || null, title: node.name || "", titleVersion: node.titleVersion || 0 });
|
|
423
|
+
// Self-heal a STALE cached cloud_team_id: if we presented a cached id but the
|
|
424
|
+
// cloud returned ok WITHOUT an apiKey (team deleted / rotated / no longer owned
|
|
425
|
+
// cloud-side — e.g. after a cloud wipe), the cached id is dead. Re-register with
|
|
426
|
+
// teamId=null to mint a FRESH team+key instead of silently leaving the gateway
|
|
427
|
+
// key empty (the "apiKey stays empty after a cloud wipe → requests 发不出去" bug).
|
|
428
|
+
// The teamId-changed branch below persists the new id back into teams.json.
|
|
429
|
+
if (reg && reg.ok && !reg.apiKey && node.cloud_team_id) {
|
|
430
|
+
log.warn(`[local-teams] cached cloud_team_id=${node.cloud_team_id} returned no gateway key — re-creating a fresh team`);
|
|
431
|
+
reg = await cc.registerTeam({ teamId: null, title: node.name || "", titleVersion: node.titleVersion || 0 });
|
|
432
|
+
}
|
|
433
|
+
// The cloud assigns this team a sk-cicy- gateway apiKey on register — wire
|
|
434
|
+
// it (full provider items + CLI routing, 主人 spec) into this machine's
|
|
435
|
+
// global.json so cicy-code has an LLM key from the moment it starts.
|
|
436
|
+
// Idempotent: injectGatewayKey no-ops when everything is already in place.
|
|
437
|
+
if (reg && reg.ok && reg.apiKey) {
|
|
438
|
+
try {
|
|
439
|
+
const inj = cc.injectGatewayKey(reg.apiKey, reg.gatewayUrl);
|
|
440
|
+
if (inj && inj.changed) log.info(`[local-teams] gateway key injected into global.json (teamId=${reg.teamId})`);
|
|
441
|
+
} catch (e) { log.warn(`[local-teams] gateway key injection failed: ${e.message}`); }
|
|
442
|
+
}
|
|
443
|
+
if (reg && reg.ok) {
|
|
444
|
+
// 服务端权威版本号裁决(w-10032 契约):响应版本 > 本地 → 采用响应的 title+version。
|
|
445
|
+
// 一条规则覆盖三种情况:(a) 云端/别处改名下行(reg.title=云端名,版本更大);
|
|
446
|
+
// (b) 本端改名被接受(reg.title=本端名,版本=base+1);(c) 冲突被拒(base 落后→
|
|
447
|
+
// reg.title=云端名,版本更大→云端赢)。相同名服务端不 bump→版本不变→不动。
|
|
448
|
+
const respVer = Number(reg.titleVersion) || 0;
|
|
449
|
+
const localVer = Number(node.titleVersion) || 0;
|
|
450
|
+
const adopt = respVer > localVer;
|
|
451
|
+
const teamIdChanged = reg.teamId && reg.teamId !== node.cloud_team_id;
|
|
452
|
+
if (teamIdChanged || adopt) {
|
|
453
|
+
await writeNodes((nodes) => {
|
|
454
|
+
if (nodes[id]) {
|
|
455
|
+
if (teamIdChanged) nodes[id].cloud_team_id = reg.teamId;
|
|
456
|
+
if (adopt) { if (reg.title) nodes[id].name = reg.title; nodes[id].titleVersion = respVer; }
|
|
457
|
+
}
|
|
458
|
+
return nodes;
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
if (adopt) log.info(`[local-teams] cloud title-sync ${id} ← "${reg.title}" v${respVer} (was v${localVer})`);
|
|
462
|
+
else if (teamIdChanged) log.info(`[local-teams] cloud title-sync ${id} → teamId=${reg.teamId}`);
|
|
421
463
|
}
|
|
422
|
-
} catch (e) { log.warn(`[local-teams] cloud
|
|
464
|
+
} catch (e) { log.warn(`[local-teams] cloud title-sync ${id} failed: ${e.message}`); }
|
|
423
465
|
}
|
|
424
466
|
|
|
425
467
|
// Sync EVERY existing local-origin team to cloud. Runs once at startup (after
|
|
@@ -568,17 +610,22 @@ async function updateTeam(id, patch) {
|
|
|
568
610
|
}
|
|
569
611
|
|
|
570
612
|
let existed = false;
|
|
613
|
+
const isRename = filtered.name !== undefined;
|
|
571
614
|
await writeNodes((nodes) => {
|
|
572
615
|
if (Object.prototype.hasOwnProperty.call(nodes, id)) {
|
|
573
616
|
existed = true;
|
|
574
617
|
nodes[id] = { ...nodes[id], ...filtered, updated_at: new Date().toISOString() };
|
|
618
|
+
// 改名:只改 name,titleVersion 保持「最后一次从云端看到的」作为 base 不动。
|
|
619
|
+
// syncNameToCloud 带这个 base 去注册;服务端接受后盖 base+1,响应回来再写回本地
|
|
620
|
+
// (服务端权威,w-10032 契约)。冲突(base 落后)则被拒、采用云端名,见 syncNameToCloud。
|
|
575
621
|
}
|
|
576
622
|
return nodes;
|
|
577
623
|
});
|
|
578
624
|
if (!existed) return { ok: false, error: "team not found" };
|
|
579
625
|
log.info(`[local-teams] update ${id} → ${Object.keys(filtered).join(",")}`);
|
|
580
|
-
// Rename → push the new title to the cloud
|
|
581
|
-
if (
|
|
626
|
+
// Rename → push the new title (with the base titleVersion) to the cloud, and
|
|
627
|
+
// pull down a newer cloud name if there is one (two-way LWW inside syncNameToCloud).
|
|
628
|
+
if (isRename) syncNameToCloud(id).catch(() => {});
|
|
582
629
|
const next = readNodes()[id] || {};
|
|
583
630
|
let port = null;
|
|
584
631
|
try { port = parseInt(new URL(next.base_url || "").port, 10) || null; } catch {}
|
|
@@ -800,7 +847,7 @@ function fetchManifestVersion() {
|
|
|
800
847
|
res.setEncoding("utf8");
|
|
801
848
|
res.on("data", (c) => { body += c; if (body.length > 8192) body = body.slice(0, 8192); });
|
|
802
849
|
res.on("end", () => {
|
|
803
|
-
try { resolve(
|
|
850
|
+
try { resolve(require("../sidecar/version").parseHealthVersion(body)); }
|
|
804
851
|
catch { resolve(null); }
|
|
805
852
|
});
|
|
806
853
|
});
|
|
@@ -28,6 +28,17 @@ function register({ sidecarLogPath } = {}) {
|
|
|
28
28
|
return { running };
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
+
// The ONE place the homepage gets cicy-code versions (主人令:"拿版本就一个方法").
|
|
32
|
+
// running → the live daemon's /api/health version ("正在跑什么"的唯一真相)
|
|
33
|
+
// latest → newest on npm (same number 更新 upgrades to)
|
|
34
|
+
// installed → on-disk binary (manifest)
|
|
35
|
+
// The card derives 更新可用 / 已是最新 from THESE — never from ad-hoc probes.
|
|
36
|
+
ipcMain.handle("sidecar:versions", async () => {
|
|
37
|
+
const version = require("../sidecar/version");
|
|
38
|
+
const [running, latest] = await Promise.all([version.running(PORT), version.latest()]);
|
|
39
|
+
return { running: running || null, latest: latest || null, installed: version.installed() || null };
|
|
40
|
+
});
|
|
41
|
+
|
|
31
42
|
// ---- Windows Docker bootstrap (homepage's "no Docker" setup flow) ----
|
|
32
43
|
// docker:status → what's missing; docker:bootstrap → install Docker (if
|
|
33
44
|
// needed) + load image + start container, streaming progress back to the
|
|
@@ -30,9 +30,11 @@ const relay = (type, payload) =>
|
|
|
30
30
|
// inside the helper agent's exec-js calls) call window.electronRPC("exec_shell",
|
|
31
31
|
// {...}) etc. The Team Helper genuinely needs shell access to download +
|
|
32
32
|
// install cicy-code on the user's machine, so we mirror the same bridge
|
|
33
|
-
// here.
|
|
34
|
-
//
|
|
35
|
-
|
|
33
|
+
// here. Routed through the GUARDED channel ("rpc:guarded") — this is a remote
|
|
34
|
+
// third party, so dangerous tools (exec_*/file_*) prompt the user for a per-page
|
|
35
|
+
// grant before running (a trusted-origin XSS must not be silent RCE). Normal
|
|
36
|
+
// tools pass straight through. The homepage uses the unguarded "rpc" channel.
|
|
37
|
+
const electronRPC = (tool, args) => ipcRenderer.invoke("rpc:guarded", tool, args || {});
|
|
36
38
|
|
|
37
39
|
const cicyApi = {
|
|
38
40
|
platform: process.platform,
|
|
@@ -164,9 +164,19 @@ async function openWindowForBackend(backend, opts = {}) {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
registry.markUsed(backend.id);
|
|
167
|
-
|
|
168
|
-
//
|
|
169
|
-
|
|
167
|
+
const acct = 0; // all teams open as tabs in profile 0's tab window (主人令)
|
|
168
|
+
// Open as a TAB in profile 0's tab window (不弹新窗口). trusted=true so the
|
|
169
|
+
// cicy-code SPA gets its electronRPC bridge. Fallback to a real window on any
|
|
170
|
+
// failure so opening a backend is never blocked.
|
|
171
|
+
try {
|
|
172
|
+
const tabBrowser = require("../tools/tab-browser-tools");
|
|
173
|
+
const { BrowserWindow } = require("electron");
|
|
174
|
+
// tab name = the backend/team's title (not the SPA's document.title)
|
|
175
|
+
const r = await tabBrowser.openTab(acct, url, { trusted: true, systemOpen: true, title: backend.name || "" });
|
|
176
|
+
return BrowserWindow.fromId(r.winId) || createWindow({ url }, acct, true);
|
|
177
|
+
} catch (e) {
|
|
178
|
+
return createWindow({ url }, acct, true);
|
|
179
|
+
}
|
|
170
180
|
}
|
|
171
181
|
|
|
172
182
|
module.exports = { openWindowForBackend, buildLocalUrl, buildRemoteUrl, resolveBackendUrl, readCicyAiApiToken, backendAccountIdx };
|
|
@@ -9,7 +9,7 @@ const { config } = require("../config");
|
|
|
9
9
|
|
|
10
10
|
// Default profile model: one user-data-dir per accountIdx
|
|
11
11
|
// Directory layout:
|
|
12
|
-
// ~/chrome/
|
|
12
|
+
// ~/chrome/profile_<idx>/Default/...
|
|
13
13
|
const DEFAULT_USER_DATA_BASE_ROOT = path.join(os.homedir(), "chrome");
|
|
14
14
|
const DEFAULT_DEBUGGER_BASE_PORT = 9320;
|
|
15
15
|
|
|
@@ -18,11 +18,12 @@ function getProfileDirectory(_accountIdx) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
function getDefaultUserDataDirRoot(accountIdx, baseRoot = DEFAULT_USER_DATA_BASE_ROOT) {
|
|
21
|
-
// If caller passes a concrete
|
|
22
|
-
|
|
21
|
+
// If caller passes a concrete profile dir already, respect it.
|
|
22
|
+
// (account_<n> accepted for backward-compat with pre-rename dirs.)
|
|
23
|
+
if (typeof baseRoot === "string" && /(?:profile|account)_\d+$/.test(baseRoot)) {
|
|
23
24
|
return baseRoot;
|
|
24
25
|
}
|
|
25
|
-
return path.join(baseRoot, `
|
|
26
|
+
return path.join(baseRoot, `profile_${accountIdx}`);
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
function getDefaultDebuggerPort(accountIdx, basePort = DEFAULT_DEBUGGER_BASE_PORT) {
|
|
@@ -12,7 +12,7 @@ function readPrivateChromeConfig() {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
function getConfiguredDebuggerPort(accountIdx, chromeConfig = readPrivateChromeConfig()) {
|
|
15
|
-
const entry = chromeConfig?.[`
|
|
15
|
+
const entry = chromeConfig?.[`profile_${accountIdx}`];
|
|
16
16
|
return typeof entry?.port === "number" ? entry.port : null;
|
|
17
17
|
}
|
|
18
18
|
|