cicy-desktop 2.1.42 → 2.1.44
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/CLAUDE.md +35 -29
- package/README.md +2 -3
- package/bin/cicy-desktop +50 -14
- package/package.json +1 -1
- package/src/backends/artifact-ipc.js +142 -0
- package/src/backends/homepage-preload.js +5 -0
- package/src/backends/homepage-react/assets/index-BqRSij9W.js +49 -0
- package/src/backends/homepage-react/index.html +1 -1
- package/src/backends/homepage-window.js +16 -14
- package/src/main.js +96 -7
- package/src/utils/window-utils.js +38 -0
- package/workers/render/src/App.jsx +52 -0
- package/src/backends/homepage-react/assets/index-BhLjpIIu.js +0 -49
package/CLAUDE.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
4
|
|
|
5
|
-
`cicy-desktop` is an Electron app that exposes ~
|
|
5
|
+
`cicy-desktop` is an Electron app that exposes ~100+ system tools (Chrome control, CDP, clipboard, screenshot, shell/node/python exec, system info, file ops, ...) over MCP and REST/RPC. The app runs in two roles — **worker** (the Electron process exposing tools) and **master** (a thin control plane that routes tool calls across workers). It does **not** bundle a `cicy-code` binary — the sidecar daemon is acquired at runtime (`npx cicy-code` on mac/linux, Docker on Windows; see [Sidecar](#sidecar-cicy-code-daemon)).
|
|
6
|
+
|
|
7
|
+
**Distribution (2026-06):** end users run via npm, not a packaged installer — `npx cicy-desktop` (mac/linux) / `npm i -g cicy-desktop && cicy-desktop` (Windows). First launch drops a desktop shortcut and auto-acquires the sidecar. CN needs `ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/` so electron's binary postinstall doesn't hit GitHub. The electron-builder packaged build (dmg/NSIS) still exists but is secondary. To discover tools at runtime, call the `list_tools` meta-tool (`electronRPC("list_tools")` / `agent-desktop rpc list_tools`).
|
|
6
8
|
|
|
7
9
|
## Development workflow rules (read first)
|
|
8
10
|
|
|
@@ -208,21 +210,17 @@ Supporting modules:
|
|
|
208
210
|
- `src/cluster/worker-client.js` — worker registration + heartbeat to the master
|
|
209
211
|
- `src/cluster/worker-identity.js` — identity payload advertised to the master
|
|
210
212
|
|
|
211
|
-
Worker
|
|
213
|
+
Worker tool surface (how the ~100 tools are reached):
|
|
212
214
|
|
|
213
|
-
- `
|
|
214
|
-
-
|
|
215
|
-
-
|
|
215
|
+
- **IPC** — `ipcMain.handle("rpc", (e, toolName, args) => executeTool(...))` (`src/main.js`). This is what `window.electronRPC(tool, args)` and the cicy-code `desktop_event` bridge ride. No HTTP, no port.
|
|
216
|
+
- **MCP** — every registered tool is exposed via the MCP server (`src/server/mcp-server.js`, `/mcp` + `/messages`).
|
|
217
|
+
- **HTTP index** — `GET /openapi.json` (dynamic, every tool + inputSchema) and the Swagger UI at `/docs`.
|
|
218
|
+
- **`list_tools`** — a meta-tool (`src/tools/list-tools.js`) that returns the live catalog over the IPC/RPC side, so callers that can't reach `/openapi.json` (renderer, `<webview>`, `agent-desktop rpc list_tools`) can still enumerate.
|
|
219
|
+
- **REST `POST /rpc/:toolName`** — served by the **master** (`src/master/master-routes.js`), which forwards to a worker. The worker process itself does not mount `/rpc/*` execution routes.
|
|
216
220
|
|
|
217
221
|
### Tool system
|
|
218
222
|
|
|
219
|
-
Tool implementations live in `src/tools/*.js
|
|
220
|
-
|
|
221
|
-
- MCP tool registration
|
|
222
|
-
- `GET /rpc/tools`
|
|
223
|
-
- OpenAPI generation in `/openapi.json`
|
|
224
|
-
|
|
225
|
-
A tool definition change affects all three surfaces at once.
|
|
223
|
+
Tool implementations live in `src/tools/*.js`, listed in `src/tools/index.js`, loaded via `require("../tools")` from `src/server/tool-catalog.js`. Each module exports a function receiving `registerTool(name, description, schema, handler, options)`. The catalog (grouped by `tag`) is reused for MCP registration, `list_tools`, and OpenAPI generation in `/openapi.json` — **a tool definition change affects all of them at once.**
|
|
226
224
|
|
|
227
225
|
Tools use **CommonJS** and **Zod schemas**.
|
|
228
226
|
|
|
@@ -386,6 +384,18 @@ Only the **token** HMRs cleanly. If you also rebuilt the helper image with new A
|
|
|
386
384
|
|
|
387
385
|
`HELPER_URL_BASE` (App.jsx) is the **other** half of the pairing. It currently points at `http://43.99.56.150:8011` (a long-running shared helper). If you want to swap to a locally rebuilt container, change it to `http://localhost:8011` and `ssh -fNR 8011:127.0.0.1:8011 mac` so the Mac can reach your dev box's helper. Same token-grab step still applies.
|
|
388
386
|
|
|
387
|
+
### Deep links (`cicy://`) and local-team add/rename
|
|
388
|
+
|
|
389
|
+
Protocol `cicy://` is registered (`package.json` build `protocols` + `setAsDefaultProtocolClient("cicy")` in `src/main.js`). The handler is `src/main.js::handleDeepLink(url)`, fed by `app.on("open-url")` (mac), `second-instance`, and cold-start `argv` (win/linux).
|
|
390
|
+
|
|
391
|
+
`cicy://addTeam?title=<t>&url=<base_url>&token=<api_token>`:
|
|
392
|
+
|
|
393
|
+
- **handleDeepLink adds the team directly in the main process** via `local-teams.addTeam({base_url, api_token, name})` — it does NOT rely on the renderer. (An earlier version only broadcast `deeplink:addTeam` to the renderer, but App.jsx never wired `window.cicy.deeplink.onAddTeam`, so the team was silently dropped.) The homepage polls `localTeams:list` every few seconds, so the new team shows up on its own.
|
|
394
|
+
- **Upsert is keyed by `base_url`** (`local-teams.js::addTeam`): re-adding a known URL refreshes `api_token` + install meta but **keeps the existing (possibly user-renamed) name** — only a brand-new team takes the provided title. No-name → i18n default `localTeams.unnamed` (`Unnamed`/`未命名`/…).
|
|
395
|
+
- The URL value must be **single** percent-encoded; raw CJK in `title` makes macOS `open` re-encode the whole thing (double-encoding → `bad base_url`).
|
|
396
|
+
|
|
397
|
+
**Rename**: every local team is renamable. `LocalTeamCard` (App.jsx) has inline edit (double-click name / ✎) → `window.cicy.localTeams.update(id, {name})` → `local-teams.js::updateTeam` (whitelists `name`). Labels go through `window.cicyI18n.t` (preload-exposed i18n; keys in `src/i18n/locales/*.json` under `localTeams`).
|
|
398
|
+
|
|
389
399
|
### Backends launcher (legacy `src/backends/`)
|
|
390
400
|
|
|
391
401
|
`src/backends/homepage.html` + `cicy.backends.{list, add, remove, …}` is the **pre-Vite** launcher. It still ships and still works (the bundled sidecar and Add-by-URL flow live here), but it is **not** what new users see — `pickHomepageURL` prefers the Vite/React entry. Touch this only when you're working on the old launcher path. Registry file: `<userData>/backends.json` (`src/backends/registry.js`). IPC handlers: `src/backends/ipc.js`.
|
|
@@ -424,7 +434,7 @@ Implication: anything that calls `window.electronRPC` from outside the cicy-code
|
|
|
424
434
|
|
|
425
435
|
### On-disk layout — `~/.local/bin/cicy-code` symlink → versioned binary
|
|
426
436
|
|
|
427
|
-
|
|
437
|
+
The cloud Team Helper agent writes the daemon into the user's `~/.local/bin/` with this shape (the npx sidecar path uses the npm cache instead, but a Helper-installed `~/.local/bin/cicy-code` on `:8008` is still reused first via `probeExisting`, and `upgradeNative` manages it):
|
|
428
438
|
|
|
429
439
|
```
|
|
430
440
|
~/.local/bin/cicy-code-2.1.8 (actual binary, +x)
|
|
@@ -451,23 +461,16 @@ Upgrade flow inside `src/backends/local-teams.js::upgradeNative`:
|
|
|
451
461
|
|
|
452
462
|
The cloud helper agent uses the **same layout** when it does the initial install (`AGENTS.md` step 1A.2/1A.3 in `cicy-cloud/workers/helper/`). It writes `cicy-code-<ver>` + `ln -sfn` + spawns via the symlink, then registers the team with `install_path=~/.local/bin/cicy-code`. This way the agent's install path and the desktop's upgrade path are identical — no special-case wiring needed.
|
|
453
463
|
|
|
454
|
-
### Sidecar cicy-code daemon
|
|
455
|
-
|
|
456
|
-
`cicy-desktop` no longer ships a `cicy-code` binary in the `.app`. The daemon is acquired one of three ways:
|
|
457
|
-
|
|
458
|
-
1. **Already running on `:8008`** — left over from a previous session, started by the user, or installed by the cloud Team Helper. `src/sidecar/cicy-code.js::probeExisting` detects it and `start()` reuses without re-spawning.
|
|
459
|
-
2. **In-app installer** — `src/sidecar/installer.js` downloads the platform-matching binary from `cicy-ai/cicy-code` GitHub releases into `<userData>/cicy-code/<platform>-<arch>/cicy-code`. Triggered from the homepage when no daemon is running. `userBinary()` returns this path; `bundledBinaryPath()` only checks this single source — there is intentionally no `<App>/Contents/Resources/cicy-code` fallback.
|
|
460
|
-
3. **Cloud Team Helper** — the trial helper container walks the user through installing cicy-code on their own machine, then registers it via `window.cicy.localTeams.add({...})`. Today the helper writes to `~/Downloads/cicy-code`; the in-app installer location is preferred (`<userData>/cicy-code/...`) so `userBinary()` discovers it on the next desktop launch with no extra wiring.
|
|
464
|
+
### Sidecar cicy-code daemon
|
|
461
465
|
|
|
462
|
-
|
|
463
|
-
- `package.json` no longer has `extraResources` entries for `vendor/cicy-code/*/cicy-code`
|
|
464
|
-
- `package.json` no longer runs `prepare:sidecar` in `prebuild*`
|
|
465
|
-
- `scripts/prepare-cicy-code-sidecar.js` is dormant (kept for reference; can be deleted)
|
|
466
|
-
- `vendor/cicy-code/` directory is no longer touched by the build
|
|
466
|
+
`cicy-desktop` never bundles a `cicy-code` binary. `src/sidecar/cicy-code.js::start()` acquires the daemon at runtime:
|
|
467
467
|
|
|
468
|
-
|
|
468
|
+
1. **Already running on `:8008`** — left over from a previous session, started by the user, or installed by the cloud Team Helper. `probeExisting()` detects it and `start()` reuses without re-spawning. This wins on every platform.
|
|
469
|
+
2. **mac / linux → `npx cicy-code`** — `start()` spawns `npx -y cicy-code` (default registry npmmirror for CN; override with `CICY_NPM_REGISTRY`, pin with `CICY_CODE_VERSION`). The launcher fetches the per-version binary from npm and does its own `:8008` port hygiene. No download/installer code in cicy-desktop.
|
|
470
|
+
3. **Windows → Docker** — `start()` delegates to `src/sidecar/docker.js` (`docker run` of the cicy-code image whose entrypoint `npx`-installs cicy-code). The image is loaded from R2 when absent.
|
|
471
|
+
4. **Cloud Team Helper** — the trial helper container walks the user through installing cicy-code on their machine, then registers the team via `window.cicy.localTeams.add({...})`.
|
|
469
472
|
|
|
470
|
-
|
|
473
|
+
**Retired (do not look for these):** the old in-app downloader `src/sidecar/installer.js` and the WSL path `src/sidecar/wsl.js` are **deleted**, along with their IPC handlers and the in-app install UI. There is no `bundledBinaryPath()`/`userBinary()`/`extraResources`/`vendor/cicy-code/` anymore. If no daemon is on `:8008` and the npx/Docker spawn can't run, `start()` returns `null` and the homepage's Team Helper card is the path from "no daemon" to "daemon running".
|
|
471
474
|
|
|
472
475
|
#### What broke that motivated the principle (2026-05-29)
|
|
473
476
|
|
|
@@ -536,7 +539,10 @@ The master uses `CICY_MASTER_TOKEN` directly or falls back to `MasterTokenManage
|
|
|
536
539
|
| backends launcher UI | `src/backends/homepage.html` |
|
|
537
540
|
| backends preload bridge | `src/backends/homepage-preload.js` |
|
|
538
541
|
| BrowserWindow trust + auto-inject | `src/utils/window-utils.js` |
|
|
539
|
-
| sidecar
|
|
542
|
+
| sidecar acquisition (npx / docker) | `src/sidecar/cicy-code.js`, `src/sidecar/docker.js` |
|
|
543
|
+
| desktop shortcut + deep links (`cicy://`) | `bin/cicy-desktop` (shortcut gen), `src/main.js` (handleDeepLink) |
|
|
544
|
+
| local teams (add/upsert/rename) + i18n | `src/backends/local-teams.js`, `src/i18n/locales/*.json` |
|
|
545
|
+
| tool list module | `src/tools/index.js`, `src/tools/list-tools.js` |
|
|
540
546
|
| RPC test files | `tests/rpc/master-routes.test.js`, `tests/rpc/cicy-rpc.test.js` |
|
|
541
547
|
|
|
542
548
|
## Debugging via remote-debugging-port 9221
|
|
@@ -601,7 +607,7 @@ When touching any of these, expect ripple effects:
|
|
|
601
607
|
- adding a tool → touches MCP server, REST/RPC, OpenAPI all at once via the catalog
|
|
602
608
|
- changing trust criteria → changes `nodeIntegration` for whole classes of windows; verify with the cicy-code bridge still works
|
|
603
609
|
- changing the homepage entry flow → check that cold-launch and "back to launcher" paths both behave (history.length / sessionStorage gates)
|
|
604
|
-
- changing the sidecar
|
|
610
|
+
- changing the sidecar acquisition → there is no bundled binary; verify `npx cicy-code` (mac/linux) / Docker (win) actually spawns and binds `:8008`, and that `probeExisting` reuse still works
|
|
605
611
|
- changing `chrome_*` tools → both master injection (`effectiveChromeProfile`) and worker fallback (`~/cicy-ai/db/chrome.json`) paths still need to work
|
|
606
612
|
- changing **any preload file** → `⌘+Q` + reopen Electron is mandatory; HMR / `⌘+R` won't reload it. Confirm via CDP `Runtime.evaluate` on the target window
|
|
607
613
|
- changing **`src/backends/local-teams.js`** → it's `require`d by main; full Electron restart needed. Don't forget to also expose any new methods through both `homepage-preload.js` (full surface) and `webview-preload.js` (relay)
|
package/README.md
CHANGED
|
@@ -295,9 +295,8 @@ Current source of truth in code:
|
|
|
295
295
|
- tool implementations: `src/tools/*`
|
|
296
296
|
|
|
297
297
|
At a high level:
|
|
298
|
-
- `src/main.js
|
|
299
|
-
- `src/
|
|
300
|
-
- `src/main.js:294` exposes `POST /rpc/:toolName`
|
|
298
|
+
- the **worker** dispatches tools two ways: in-process via `ipcMain.handle("rpc", …)` (`src/main.js`, what `window.electronRPC(tool, args)` rides) and over HTTP. The full, live tool index is `GET /openapi.json` (browse at `/docs`); call `list_tools` to enumerate from the RPC/IPC side.
|
|
299
|
+
- REST tool invocation (`POST /rpc/:toolName`, used by `cicy-rpc`) is served by the **master** (`src/master/master-routes.js`), which forwards to the selected worker.
|
|
301
300
|
- RPC routes are protected by auth and return `401 Unauthorized` when the token is wrong or missing
|
|
302
301
|
- the desktop CLI starts a local master + worker cluster and provides status/log management
|
|
303
302
|
|
package/bin/cicy-desktop
CHANGED
|
@@ -20,15 +20,35 @@ const ELECTRON_MIRROR = process.env.ELECTRON_MIRROR || "https://npmmirror.com/mi
|
|
|
20
20
|
const NPM_REGISTRY = process.env.CICY_NPM_REGISTRY || process.env.npm_config_registry || "https://registry.npmmirror.com";
|
|
21
21
|
const MASTER_ENTRY = path.join(PACKAGE_ROOT, "src", "master", "master-main.js");
|
|
22
22
|
|
|
23
|
-
// Resolve
|
|
24
|
-
//
|
|
25
|
-
//
|
|
23
|
+
// Resolve a globally-installed electron (`npm i -g electron`), so the ~200MB
|
|
24
|
+
// binary is downloaded once and SHARED across cicy tools / desktop versions
|
|
25
|
+
// instead of one copy per package install. Returns null if not present.
|
|
26
|
+
let _globalElectronBin; // memoized (avoid re-spawning `npm prefix -g`)
|
|
27
|
+
function globalElectronBin() {
|
|
28
|
+
if (_globalElectronBin !== undefined) return _globalElectronBin;
|
|
29
|
+
_globalElectronBin = null;
|
|
30
|
+
try {
|
|
31
|
+
const prefix = execSync("npm prefix -g", { stdio: ["ignore", "pipe", "ignore"] })
|
|
32
|
+
.toString().trim();
|
|
33
|
+
if (prefix) {
|
|
34
|
+
const p = isWindows
|
|
35
|
+
? path.join(prefix, "electron.cmd")
|
|
36
|
+
: path.join(prefix, "bin", "electron");
|
|
37
|
+
if (fs.existsSync(p)) _globalElectronBin = p;
|
|
38
|
+
}
|
|
39
|
+
} catch { /* keep null */ }
|
|
40
|
+
return _globalElectronBin;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Resolve the electron binary to spawn. Order: (C) a shared GLOBAL electron,
|
|
44
|
+
// then the project-local copy, then `npx electron` as a last resort.
|
|
26
45
|
// Windows uses `npx.cmd electron` to handle the shim correctly.
|
|
27
46
|
function resolveElectronSpawn() {
|
|
47
|
+
const g = globalElectronBin();
|
|
48
|
+
if (g) return { cmd: g, prefixArgs: [] };
|
|
28
49
|
if (isWindows) return { cmd: "npx.cmd", prefixArgs: ["electron"] };
|
|
29
50
|
const local = path.join(PACKAGE_ROOT, "node_modules", ".bin", "electron");
|
|
30
51
|
if (fs.existsSync(local)) return { cmd: local, prefixArgs: [] };
|
|
31
|
-
// Fall back to a globally-installed electron resolved via PATH.
|
|
32
52
|
return { cmd: "npx", prefixArgs: ["electron"] };
|
|
33
53
|
}
|
|
34
54
|
const PROJECT_COMMAND_FILE = path.join(PACKAGE_ROOT, "cicy-dektop.command");
|
|
@@ -358,6 +378,17 @@ function nodeBinDir() {
|
|
|
358
378
|
try { return path.dirname(process.execPath); } catch { return ""; }
|
|
359
379
|
}
|
|
360
380
|
|
|
381
|
+
// npm's global bin dir (<npm prefix -g>[/bin]), where `npm i -g cicy-desktop`
|
|
382
|
+
// installs the launcher. The GUI shortcut runs that global bin directly so
|
|
383
|
+
// startup doesn't pay an npx registry round-trip on every launch.
|
|
384
|
+
function npmGlobalBinDir() {
|
|
385
|
+
try {
|
|
386
|
+
const prefix = execSync("npm prefix -g", { stdio: ["ignore", "pipe", "ignore"] })
|
|
387
|
+
.toString().trim();
|
|
388
|
+
return prefix ? (isWindows ? prefix : path.join(prefix, "bin")) : "";
|
|
389
|
+
} catch { return ""; }
|
|
390
|
+
}
|
|
391
|
+
|
|
361
392
|
function ensureDesktopCommandFile() {
|
|
362
393
|
try {
|
|
363
394
|
if (process.platform === "darwin") return ensureMacDesktopApp();
|
|
@@ -373,8 +404,8 @@ function ensureMacDesktopApp() {
|
|
|
373
404
|
// executable is a shell script triggers "CiCy Desktop is not responding"
|
|
374
405
|
// (LaunchServices waits for the script to register as a Cocoa app and times
|
|
375
406
|
// out). An osacompile applet's executable IS the AppleScript runtime — a
|
|
376
|
-
// proper Cocoa app — so it launches cleanly, backgrounds
|
|
377
|
-
// via `do shell script`, then quits. Icon =
|
|
407
|
+
// proper Cocoa app — so it launches cleanly, backgrounds the global
|
|
408
|
+
// `cicy-desktop` bin via `do shell script`, then quits. Icon = applet.icns.
|
|
378
409
|
const { execFileSync } = require("child_process");
|
|
379
410
|
const appDir = path.join(DESKTOP_DIR, "CiCy Desktop.app");
|
|
380
411
|
// Idempotent — create only when missing. When the app is launched FROM this
|
|
@@ -384,16 +415,21 @@ function ensureMacDesktopApp() {
|
|
|
384
415
|
// ever appears ("打不开"). Skip if it already exists.
|
|
385
416
|
if (fs.existsSync(appDir)) return;
|
|
386
417
|
const nodeDir = nodeBinDir();
|
|
387
|
-
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
|
|
392
|
-
|
|
418
|
+
const gbin = npmGlobalBinDir();
|
|
419
|
+
// Launch the globally-installed `cicy-desktop` bin DIRECTLY (npm i -g) — no
|
|
420
|
+
// per-launch `npx` registry round-trip. Use the absolute path when resolvable
|
|
421
|
+
// (GUI launch has a minimal PATH); fall back to the bare name otherwise.
|
|
422
|
+
const cicyBin = gbin && fs.existsSync(path.join(gbin, "cicy-desktop"))
|
|
423
|
+
? path.join(gbin, "cicy-desktop")
|
|
424
|
+
: "cicy-desktop";
|
|
425
|
+
// do shell script needs node + the global bin on PATH (GUI launch has a
|
|
426
|
+
// minimal PATH); trailing & + nohup + </dev/null lets the shell return so the
|
|
427
|
+
// applet quits. ELECTRON_MIRROR + npmmirror only matter if electron ever
|
|
428
|
+
// (re)downloads — harmless otherwise.
|
|
393
429
|
const ascript =
|
|
394
|
-
`do shell script "export PATH=\\"${nodeDir}:/usr/local/bin:/opt/homebrew/bin:$PATH\\" ; ` +
|
|
430
|
+
`do shell script "export PATH=\\"${nodeDir}:${gbin}:/usr/local/bin:/opt/homebrew/bin:$PATH\\" ; ` +
|
|
395
431
|
`export ELECTRON_MIRROR=\\"${ELECTRON_MIRROR}\\" ; export npm_config_registry=\\"${NPM_REGISTRY}\\" ; ` +
|
|
396
|
-
`nohup
|
|
432
|
+
`nohup '${cicyBin}' > /tmp/cicy-desktop.log 2>&1 < /dev/null &"`;
|
|
397
433
|
const tmp = path.join(os.tmpdir(), `cicy-launch-${process.pid}.applescript`);
|
|
398
434
|
fs.writeFileSync(tmp, ascript);
|
|
399
435
|
try {
|
package/package.json
CHANGED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// artifact-ipc.js — main-process side of the window.cicy.artifact bridge.
|
|
2
|
+
//
|
|
3
|
+
// The cicy-code app (loaded in a trusted team BrowserWindow) hosts a
|
|
4
|
+
// <webview id="cicy-artifact-webview"> for the 产物 (artifact) tab. Its
|
|
5
|
+
// renderer bridge (cicy-code app/src/lib/artifactBridge.ts) drives that
|
|
6
|
+
// webview's *guest* webContents through `window.cicy.artifact.{invoke,cdp}` —
|
|
7
|
+
// injected in window-utils.js — which round-trips to the handlers here.
|
|
8
|
+
//
|
|
9
|
+
// We resolve the guest via webContents.fromId(guestId) (the renderer passes the
|
|
10
|
+
// element's getWebContentsId()), call webContents methods / drive its debugger,
|
|
11
|
+
// and forward the guest's console / navigation / CDP-message events back to the
|
|
12
|
+
// host renderer as 'artifact:event' (re-dispatched there as the
|
|
13
|
+
// CustomEvent('cicy-artifact-event') the bridge buffers).
|
|
14
|
+
|
|
15
|
+
const { ipcMain, webContents } = require("electron");
|
|
16
|
+
const log = require("electron-log");
|
|
17
|
+
|
|
18
|
+
// guest webContents.id → host renderer webContents that should receive events.
|
|
19
|
+
// Updated on every invoke/attach so events follow host reloads.
|
|
20
|
+
const guestHost = new Map();
|
|
21
|
+
// guest webContents.id we've already wired passive (console/nav) listeners on.
|
|
22
|
+
const wiredPassive = new Set();
|
|
23
|
+
// guest webContents.id we've already wired the debugger 'message'/'detach' on.
|
|
24
|
+
const wiredDebugger = new Set();
|
|
25
|
+
|
|
26
|
+
function getGuest(guestId) {
|
|
27
|
+
const wc = typeof guestId === "number" ? webContents.fromId(guestId) : null;
|
|
28
|
+
if (!wc || wc.isDestroyed()) {
|
|
29
|
+
throw new Error(`artifact guest webContents ${guestId} not found (open the 产物 tab once)`);
|
|
30
|
+
}
|
|
31
|
+
return wc;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function emit(wc, detail) {
|
|
35
|
+
try {
|
|
36
|
+
const host = guestHost.get(wc.id);
|
|
37
|
+
if (host && !host.isDestroyed()) host.send("artifact:event", detail);
|
|
38
|
+
} catch {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function rememberHost(wc, hostSender) {
|
|
42
|
+
guestHost.set(wc.id, hostSender);
|
|
43
|
+
if (!wc.__artifactCleanup) {
|
|
44
|
+
wc.__artifactCleanup = true;
|
|
45
|
+
wc.once("destroyed", () => {
|
|
46
|
+
guestHost.delete(wc.id);
|
|
47
|
+
wiredPassive.delete(wc.id);
|
|
48
|
+
wiredDebugger.delete(wc.id);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// console-message + navigation → host renderer. Wired once per guest.
|
|
54
|
+
function wirePassive(wc) {
|
|
55
|
+
if (wiredPassive.has(wc.id)) return;
|
|
56
|
+
wiredPassive.add(wc.id);
|
|
57
|
+
wc.on("console-message", (_e, level, message, line, sourceId) => {
|
|
58
|
+
emit(wc, { source: "console", level, message, line, sourceId });
|
|
59
|
+
});
|
|
60
|
+
wc.on("did-navigate", (_e, url, httpResponseCode) => {
|
|
61
|
+
emit(wc, { source: "navigation", kind: "did-navigate", url, httpResponseCode });
|
|
62
|
+
});
|
|
63
|
+
wc.on("did-navigate-in-page", (_e, url, isMainFrame) => {
|
|
64
|
+
emit(wc, { source: "navigation", kind: "did-navigate-in-page", url, isMainFrame });
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// debugger 'message' (the CDP event stream) + 'detach' → host renderer. Wired
|
|
69
|
+
// once per guest; survives attach/detach cycles (only fires while attached).
|
|
70
|
+
function wireDebugger(wc) {
|
|
71
|
+
if (wiredDebugger.has(wc.id)) return;
|
|
72
|
+
wiredDebugger.add(wc.id);
|
|
73
|
+
wc.debugger.on("message", (_e, method, params) => {
|
|
74
|
+
emit(wc, { source: "cdp", method, params });
|
|
75
|
+
});
|
|
76
|
+
wc.debugger.on("detach", (_e, reason) => {
|
|
77
|
+
emit(wc, { source: "cdp", method: "__detached", params: { reason } });
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function register() {
|
|
82
|
+
// Call any webContents method on the artifact guest. capturePage/printToPDF
|
|
83
|
+
// are normalized to dataURL / base64 so the result survives JSON/WS.
|
|
84
|
+
ipcMain.handle("artifact:invoke", async (e, payload) => {
|
|
85
|
+
const { guestId, method, args = [] } = payload || {};
|
|
86
|
+
const wc = getGuest(guestId);
|
|
87
|
+
rememberHost(wc, e.sender);
|
|
88
|
+
wirePassive(wc);
|
|
89
|
+
if (typeof wc[method] !== "function") {
|
|
90
|
+
throw new Error(`artifact.invoke: webContents has no method '${method}'`);
|
|
91
|
+
}
|
|
92
|
+
if (method === "capturePage") {
|
|
93
|
+
const img = await wc.capturePage(...(args || []));
|
|
94
|
+
return img && typeof img.toDataURL === "function" ? img.toDataURL() : null;
|
|
95
|
+
}
|
|
96
|
+
if (method === "printToPDF") {
|
|
97
|
+
const buf = await wc.printToPDF((args && args[0]) || {});
|
|
98
|
+
return Buffer.from(buf).toString("base64");
|
|
99
|
+
}
|
|
100
|
+
return await wc[method](...(args || []));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
ipcMain.handle("artifact:cdp-attach", async (e, payload) => {
|
|
104
|
+
const { guestId, protocolVersion } = payload || {};
|
|
105
|
+
const wc = getGuest(guestId);
|
|
106
|
+
rememberHost(wc, e.sender);
|
|
107
|
+
// DevTools and the debugger are mutually exclusive on one webContents —
|
|
108
|
+
// close a manually-opened inspector first, else attach() throws.
|
|
109
|
+
try { if (wc.isDevToolsOpened()) wc.closeDevTools(); } catch {}
|
|
110
|
+
if (!wc.debugger.isAttached()) {
|
|
111
|
+
wc.debugger.attach(protocolVersion || "1.3");
|
|
112
|
+
}
|
|
113
|
+
wireDebugger(wc);
|
|
114
|
+
wirePassive(wc);
|
|
115
|
+
return { ok: true, attached: true };
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
ipcMain.handle("artifact:cdp-detach", async (_e, payload) => {
|
|
119
|
+
const { guestId } = payload || {};
|
|
120
|
+
const wc = getGuest(guestId);
|
|
121
|
+
if (wc.debugger.isAttached()) wc.debugger.detach();
|
|
122
|
+
return { ok: true, attached: false };
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
ipcMain.handle("artifact:cdp-is-attached", async (_e, payload) => {
|
|
126
|
+
const { guestId } = payload || {};
|
|
127
|
+
try { return getGuest(guestId).debugger.isAttached(); } catch { return false; }
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
ipcMain.handle("artifact:cdp-send", async (_e, payload) => {
|
|
131
|
+
const { guestId, method, params } = payload || {};
|
|
132
|
+
const wc = getGuest(guestId);
|
|
133
|
+
if (!wc.debugger.isAttached()) {
|
|
134
|
+
throw new Error("artifact.cdp: debugger not attached (call cdp.attach first)");
|
|
135
|
+
}
|
|
136
|
+
return await wc.debugger.sendCommand(method, params || {});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
log.info("[artifact] window.cicy.artifact IPC handlers registered");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = { register };
|
|
@@ -150,6 +150,11 @@ contextBridge.exposeInMainWorld("cicy", {
|
|
|
150
150
|
auth: {
|
|
151
151
|
loginStart: () => logInvoke("auth:login-start"),
|
|
152
152
|
loginCancel: () => logInvoke("auth:login-cancel"),
|
|
153
|
+
// Durable, origin-independent login persisted in main (global.json).
|
|
154
|
+
// getSaved() restores it when this origin's localStorage is empty;
|
|
155
|
+
// logout() clears it (the only thing that should).
|
|
156
|
+
getSaved: () => logInvoke("auth:get-saved"),
|
|
157
|
+
logout: () => logInvoke("auth:logout"),
|
|
153
158
|
onComplete: (cb) => {
|
|
154
159
|
const handler = (_e, payload) => { try { cb(payload); } catch {} };
|
|
155
160
|
ipcRenderer.on("auth:complete", handler);
|