cicy-desktop 2.1.278 → 2.1.280

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.
Files changed (48) hide show
  1. package/.github/workflows/linux-app-release.yml +7 -0
  2. package/.github/workflows/mac-app-release.yml +11 -7
  3. package/.github/workflows/npm-publish.yml +21 -0
  4. package/docs/superpowers/plans/2026-08-23-electron-inject-installer.md +79 -0
  5. package/docs/superpowers/plans/2026-08-24-telegram-matrix.md +85 -0
  6. package/docs/superpowers/specs/2026-08-23-electron-inject-installer-design.md +45 -0
  7. package/package.json +6 -5
  8. package/src/backends/docker-audit.js +17 -0
  9. package/src/backends/homepage-preload.js +2 -0
  10. package/src/backends/sidecar-ipc.js +50 -10
  11. package/src/backends/webview-preload.js +10 -1
  12. package/src/chrome/chrome-install-navigation.js +23 -0
  13. package/src/main.js +42 -9
  14. package/src/sidecar/docker-protect.js +57 -0
  15. package/src/sidecar/gateway-key-health.js +14 -0
  16. package/src/sidecar/host-mihomo.js +66 -3
  17. package/src/sidecar/wsl-docker.js +316 -23
  18. package/src/tabbrowser/cicy-code-tab-restore.js +10 -0
  19. package/src/tabbrowser/newtab-protocol.js +4 -1
  20. package/src/tabbrowser/panel-cell-visibility.js +5 -0
  21. package/src/tabbrowser/panel-cells.js +109 -9
  22. package/src/tabbrowser/panel-menu.js +14 -0
  23. package/src/tabbrowser/panel-page-router.js +11 -0
  24. package/src/tabbrowser/panel-preload.js +3 -0
  25. package/src/tabbrowser/panel-presets.js +9 -0
  26. package/src/tabbrowser/tab-shell-preload.js +3 -2
  27. package/src/tabbrowser/tab-shell.html +4 -1
  28. package/src/tabbrowser/telegram-matrix-profiles.js +42 -0
  29. package/src/tabbrowser/telegram-matrix.html +304 -0
  30. package/src/tabbrowser/telegram-web-preferences.js +22 -0
  31. package/src/tools/electron-inject-tools.js +113 -0
  32. package/src/tools/index.js +1 -0
  33. package/src/tools/tab-browser-tools.js +53 -15
  34. package/src/tray.js +14 -1
  35. package/src/utils/rpc-guard.js +1 -0
  36. package/test/chrome-install-navigation.test.js +41 -0
  37. package/test/cicy-code-tab-restore.test.js +21 -0
  38. package/test/docker-audit.test.js +20 -0
  39. package/test/docker-protect-wiring.test.js +37 -0
  40. package/test/docker-protect.test.js +43 -0
  41. package/test/electron-inject-tools.test.js +74 -0
  42. package/test/gateway-key-health.test.js +40 -0
  43. package/test/mihomo-host-sync.test.js +55 -0
  44. package/test/panel-launcher.test.js +36 -0
  45. package/test/start-bat.test.js +21 -0
  46. package/test/telegram-matrix.test.js +137 -0
  47. package/test/webview-preload.test.js +14 -0
  48. package/test/wsl-update-script-permissions.test.js +12 -0
@@ -73,6 +73,13 @@ jobs:
73
73
  CICY_OBFUSCATE: "1" # 发版混淆 JS(prebuild 主进程 + Vite 渲染层),仅 release CI
74
74
  run: |
75
75
  set -euo pipefail
76
+ if [ "$GITHUB_REF_TYPE" = "tag" ]; then
77
+ for attempt in $(seq 1 60); do
78
+ gh release view "$GITHUB_REF_NAME" --repo "${{ github.repository }}" >/dev/null 2>&1 && break
79
+ [ "$attempt" -eq 60 ] && { echo "::error::release was not created by the Windows workflow within 5 minutes"; exit 1; }
80
+ sleep 5
81
+ done
82
+ fi
76
83
  npm run build:linux -- --x64 --config.linux.target=AppImage --config.publish.owner="$PUBLISH_OWNER" --config.publish.repo="$PUBLISH_REPO" --publish always
77
84
 
78
85
  # AppImage + 版本指针传到 OSS —— app-updater 现已全走 OSS(不碰 GitHub),linux 也要在
@@ -133,17 +133,21 @@ jobs:
133
133
  echo "version=${version}" >> "$GITHUB_OUTPUT"
134
134
  echo "tag=v${version}" >> "$GITHUB_OUTPUT"
135
135
 
136
- - name: Ensure draft GitHub release exists
136
+ - name: Wait for Windows workflow to create the GitHub release
137
137
  shell: bash
138
138
  env:
139
139
  GH_TOKEN: ${{ github.token }}
140
140
  run: |
141
- gh release create "${{ steps.meta.outputs.tag }}" \
142
- --repo "${{ github.repository }}" \
143
- --title "CiCy Desktop v${{ steps.meta.outputs.version }}" \
144
- --generate-notes \
145
- --draft \
146
- 2>/dev/null || echo "release already exists, continuing"
141
+ if [ "$GITHUB_REF_TYPE" != "tag" ]; then
142
+ gh release create "${{ steps.meta.outputs.tag }}" --repo "${{ github.repository }}" --title "CiCy Desktop v${{ steps.meta.outputs.version }}" --generate-notes --draft 2>/dev/null || true
143
+ exit 0
144
+ fi
145
+ for attempt in $(seq 1 60); do
146
+ gh release view "${{ steps.meta.outputs.tag }}" --repo "${{ github.repository }}" >/dev/null 2>&1 && exit 0
147
+ sleep 5
148
+ done
149
+ echo "::error::release was not created by the Windows workflow within 5 minutes"
150
+ exit 1
147
151
 
148
152
  - name: Build and publish PKG installers
149
153
  shell: bash
@@ -15,7 +15,28 @@ concurrency:
15
15
  cancel-in-progress: true
16
16
 
17
17
  jobs:
18
+ # 单元测试(node:test,零依赖)。发布前必过 —— 这些用例守的是 WSL/Docker sidecar 那条
19
+ # 只在 Windows 真机上才跑得到的路径,本地很难复现,只能靠断言兜住。
20
+ # Node 22:`node --test "<glob>"` 的 glob 展开是 node ≥21 的能力(20 只认目录参数)。
21
+ test:
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - name: Checkout
25
+ uses: actions/checkout@v4
26
+
27
+ - name: Setup Node.js
28
+ uses: actions/setup-node@v4
29
+ with:
30
+ node-version: '22'
31
+
32
+ - name: Install dependencies
33
+ run: npm ci --ignore-scripts
34
+
35
+ - name: Run tests
36
+ run: npm test
37
+
18
38
  publish:
39
+ needs: test
19
40
  runs-on: ubuntu-latest
20
41
  steps:
21
42
  - name: Checkout
@@ -0,0 +1,79 @@
1
+ # Electron Inject Installer Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Add a restricted Desktop RPC and public `agent-electron` CLI that install JavaScript into `~/data/electron/extension/inject/`.
6
+
7
+ **Architecture:** Desktop owns path validation and atomic host writes through one `electron_inject` tool. The public Skill reads a caller-provided source file and forwards its UTF-8 content without printing it.
8
+
9
+ **Tech Stack:** Electron/Node.js CommonJS Desktop tools, Zod schemas, Node test runner, ESM public Skill CLI.
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-08-23-electron-inject-installer-design.md`
12
+
13
+ ## Global Constraints
14
+
15
+ - Fixed Desktop root: `path.join(os.homedir(), "data", "electron", "extension", "inject")`.
16
+ - Accept only filenames matching `^[a-z0-9][a-z0-9._-]*\.js$`.
17
+ - Reject payloads larger than 1 MiB and never return JavaScript content.
18
+ - Use atomic sibling-temp write, mode `0600`, and SHA-256 verification.
19
+ - Do not use the existing arbitrary-path `file_write` tool.
20
+
21
+ ---
22
+
23
+ ### Task 1: Desktop `electron_inject` RPC
24
+
25
+ **Files:**
26
+ - Create: `src/tools/electron-inject-tools.js`
27
+ - Modify: `src/tools/index.js`
28
+ - Modify: `src/utils/rpc-guard.js`
29
+ - Test: `test/electron-inject-tools.test.js`
30
+
31
+ **Interfaces:**
32
+ - Consumes: `{operation:"install"|"status"|"uninstall", name:string, content?:string}`.
33
+ - Produces: `{operation,name,path,exists,size,sha256}` encoded in the standard tool text envelope.
34
+
35
+ - [ ] Write tests registering the tool with `CICY_ELECTRON_INJECT_DIR` set to a temporary directory. Assert install, status, replacement, mode `0600`, SHA-256, uninstall, traversal rejection, extension rejection, symlink rejection, and the 1 MiB limit.
36
+ - [ ] Run `node --test test/electron-inject-tools.test.js` and confirm RED because the tool does not exist.
37
+ - [ ] Implement `electron-inject-tools.js` with exported validation helpers and `registerTools`; resolve and realpath-check the fixed root, use `openSync(temp,"wx",0o600)`, `writeFileSync`, `renameSync`, and clean the temp on errors.
38
+ - [ ] Register it in `src/tools/index.js` and add `electron_inject` to the RPC guard's host-write list.
39
+ - [ ] Run the focused test, full `node --test test/*.test.js`, and `git diff --check`.
40
+ - [ ] Commit, fetch/rebase, and push `cicy-ai/cicy-desktop` `main`.
41
+
42
+ ### Task 2: Public `agent-electron inject` CLI
43
+
44
+ **Files:**
45
+ - Modify: `skills/agent-electron/bin/agent-electron`
46
+ - Modify: `skills/agent-electron/manifest.json`
47
+ - Modify: `skills/agent-electron/SKILL.md`
48
+ - Modify: `skills/agent-electron/README.md`
49
+ - Modify: `skills/agent-electron/references/help.md`
50
+ - Modify: `skills/agent-electron/references/help.en.md`
51
+ - Modify: `skills/agent-electron/references/help.cn.md`
52
+ - Modify: `skills/agent-electron/references/tools.md`
53
+ - Modify: `skills/agent-electron/references/tools.en.md`
54
+ - Modify: `skills/agent-electron/references/tools.cn.md`
55
+ - Modify: `skills/agent-electron/test/test.js`
56
+
57
+ **Interfaces:**
58
+ - Consumes: `inject install <name> --source <file>`, `inject status <name>`, and `inject uninstall <name>`.
59
+ - Produces: the unwrapped `electron_inject` Desktop result with no source content in stdout/stderr.
60
+
61
+ - [ ] Add CLI tests using a temporary source containing a sentinel secret and a fake local cicy-code HTTP server. Assert the RPC payload selects `electron_inject`, forwards content, and output never contains the sentinel.
62
+ - [ ] Run `node tools/test-skill.js skills/agent-electron` and confirm RED.
63
+ - [ ] Add the `inject` dispatcher and parser. Require `--source` only for install, read UTF-8 with a 1 MiB pre-check, then call `rpc("electron_inject", payload)`.
64
+ - [ ] Update help and metadata; bump `agent-electron` from `1.0.17` to `1.1.0` because this is backward-compatible functionality.
65
+ - [ ] Run the Skill test, validator, sensitive-path scan, and `git diff --check`.
66
+ - [ ] Commit, fetch/rebase, push `cicy-ai/cicy-skills` `main`, tag `agent-electron-v1.1.0`, and monitor the public release and registry.
67
+
68
+ ### Task 3: Integration verification
69
+
70
+ **Files:**
71
+ - Verify both repositories at remote `main`.
72
+
73
+ **Interfaces:**
74
+ - Consumes: a running new Desktop plus `agent-electron@1.1.0`.
75
+ - Produces: local tests, releases, and registry evidence; live host installation only when a Desktop containing the new RPC is connected.
76
+
77
+ - [ ] Re-run both focused/full suites from clean `main` checkouts.
78
+ - [ ] Confirm Desktop commit is on remote `main`, Skill workflow succeeded, Release ZIP exists, and registry reports `agent-electron@1.1.0`.
79
+ - [ ] If no updated Desktop is connected, report live installation as pending instead of claiming it ran.
@@ -0,0 +1,85 @@
1
+ # Telegram Matrix Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Turn the `Telegram 矩阵` preset into a profile manager with a left-side profile table and a phone-sized Telegram Web preview on the right.
6
+
7
+ **Architecture:** Keep the system profile panel as renderer chrome and place one main-process `BrowserView` above its preview slot. The selected item supplies `persist:sandbox-N`; profile creation and proxy changes go through `profile-store`, and the BrowserView is rebuilt/reloaded when its profile or proxy changes.
8
+
9
+ **Tech Stack:** Electron BrowserView, IPC/contextBridge, CommonJS, HTML/CSS/JavaScript, Node test runner.
10
+
11
+ **Spec:** Approved in chat on 2026-08-24: unlimited Electron profiles, one item per profile, custom per-profile proxy, phone-sized Telegram preview.
12
+
13
+ ## Global Constraints
14
+
15
+ - Ordinary `面板` behavior remains unchanged.
16
+ - Telegram profile data uses `persist:sandbox-<accountIdx>` and existing `~/data/electron/account-<N>.json` persistence.
17
+ - New profiles default to `http://127.0.0.1:20001`; each profile can use a custom `http://`, `https://`, or `socks5://` proxy, or direct mode.
18
+ - The Telegram target is `https://web.telegram.org/k/`.
19
+ - Profile 0 and reserved profile 9 must not appear in the matrix.
20
+
21
+ ---
22
+
23
+ ### Task 1: Matrix preset routing
24
+
25
+ **Files:**
26
+ - Modify: `src/tabbrowser/panel-presets.js`
27
+ - Modify: `src/tools/tab-browser-tools.js`
28
+ - Modify: `src/tabbrowser/newtab-protocol.js`
29
+ - Create: `src/tabbrowser/telegram-matrix.html`
30
+ - Test: `test/telegram-matrix.test.js`
31
+
32
+ **Interfaces:**
33
+ - Produces: `resolvePanelPreset("telegram-matrix")` with a `query` field; `cicyui://panel/<id>?preset=telegram-matrix` serves `telegram-matrix.html`.
34
+
35
+ - [ ] Write a failing test asserting the Telegram preset URL query and dedicated HTML routing contract.
36
+ - [ ] Run `node --test test/telegram-matrix.test.js` and confirm the missing query/page behavior fails.
37
+ - [ ] Add the preset query, append it in `openPanelTab`, and select `telegram-matrix.html` in the protocol handler.
38
+ - [ ] Run the test and confirm it passes.
39
+
40
+ ### Task 2: Profile creation and custom proxy service
41
+
42
+ **Files:**
43
+ - Create: `src/tabbrowser/telegram-matrix-profiles.js`
44
+ - Modify: `src/tabbrowser/panel-cells.js`
45
+ - Modify: `src/tabbrowser/panel-preload.js`
46
+ - Test: `test/telegram-matrix.test.js`
47
+
48
+ **Interfaces:**
49
+ - Produces: `listTelegramProfiles()`, `addTelegramProfile(defaultProxy)`, `setTelegramProfileProxy(accountIdx, proxy)`.
50
+ - IPC: `panelcells:profiles`, `panelcells:add-profile`, `panelcells:set-profile-proxy`.
51
+
52
+ - [ ] Write failing tests for next-ID allocation, default proxy persistence, accepted proxy schemes, direct mode, and invalid proxy rejection.
53
+ - [ ] Run the focused test and confirm each new behavior fails for the expected reason.
54
+ - [ ] Implement the pure validation/allocation service and IPC handlers backed by `profile-store`.
55
+ - [ ] Apply proxy immediately with `session.setProxy`, rebuild the selected profile view when needed, and expose methods through `panelAPI`.
56
+ - [ ] Run focused and profile-store tests and confirm they pass.
57
+
58
+ ### Task 3: Table and phone preview UI
59
+
60
+ **Files:**
61
+ - Modify: `src/tabbrowser/telegram-matrix.html`
62
+ - Test: `test/telegram-matrix.test.js`
63
+
64
+ **Interfaces:**
65
+ - Consumes: `panelAPI.profiles()`, `panelAPI.addProfile()`, `panelAPI.setProfileProxy()`, and `panelAPI.sync([{ id: "telegram-preview", url, profile, rect }])`.
66
+
67
+ - [ ] Write a failing DOM-contract test for the profile table, add button, proxy editor, empty state, and phone preview slot.
68
+ - [ ] Run the test and confirm the absent controls fail.
69
+ - [ ] Implement rendering, first-profile selection, add-profile flow, per-row proxy editing, status feedback, and a single responsive 390-by-844 preview slot.
70
+ - [ ] Sync only the selected profile to the native BrowserView and preserve the list selection locally.
71
+ - [ ] Run the focused test and confirm it passes.
72
+
73
+ ### Task 4: Integration verification
74
+
75
+ **Files:**
76
+ - Test: `test/panel-launcher.test.js`
77
+ - Test: `test/telegram-matrix.test.js`
78
+
79
+ **Interfaces:**
80
+ - Verifies the complete menu-to-matrix flow without changing ordinary panels.
81
+
82
+ - [ ] Run `node --test test/panel-launcher.test.js test/telegram-matrix.test.js`.
83
+ - [ ] Run syntax checks for every changed JavaScript file and `git diff --check`.
84
+ - [ ] Restart `C:\projects\start-cicy-desktop-win.bat` and wait for the source Electron client to reconnect.
85
+ - [ ] Open `Telegram 矩阵`, inspect its DOM via `agent-electron`, and confirm profile selection produces one Telegram BrowserView in the selected partition.
@@ -0,0 +1,45 @@
1
+ # Electron Inject Installer Design
2
+
3
+ ## Goal
4
+
5
+ Allow a Skill running outside the desktop host, including inside WSL Docker, to install and manage an Electron Desktop Client injection script at:
6
+
7
+ ```text
8
+ ~/data/electron/extension/inject/telegram.org.js
9
+ ```
10
+
11
+ The interface must not expose arbitrary host filesystem writes.
12
+
13
+ ## Desktop Tool
14
+
15
+ Add an `electron_inject` RPC tool with three operations:
16
+
17
+ - `install`: accept a safe filename and UTF-8 JavaScript content, write it atomically, set file mode `0600`, and return path, byte size, and SHA-256.
18
+ - `status`: return existence, path, byte size, and SHA-256 without returning file content.
19
+ - `uninstall`: remove the named file and report whether it existed.
20
+
21
+ Resolve the root from `os.homedir()` plus `data/electron/extension/inject`. Accept only a basename matching `^[a-z0-9][a-z0-9._-]*\.js$`; reject separators, traversal, symlinks, non-JavaScript names, and targets outside the fixed root. Write to a temporary sibling with exclusive creation, rename atomically, and reject payloads larger than 1 MiB.
22
+
23
+ Register the tool in the normal Desktop tool catalog and classify it as a guarded host-write operation. Audit logging remains handled by the existing RPC guard.
24
+
25
+ ## Public Skill Interface
26
+
27
+ Extend the public `agent-electron` Skill with:
28
+
29
+ ```text
30
+ agent-electron inject install telegram.org.js --source <file>
31
+ agent-electron inject status telegram.org.js
32
+ agent-electron inject uninstall telegram.org.js
33
+ ```
34
+
35
+ The CLI reads `--source` locally, sends content through the Desktop RPC, and never prints content. `install` requires an explicit source file; no WSL Hook or generated placeholder is substituted for the Mac injection script. JSON output preserves Desktop path, size, and SHA-256.
36
+
37
+ ## Testing
38
+
39
+ - Desktop unit tests use a temporary home/root override and prove fixed-root resolution, safe-name validation, traversal rejection, size limit, atomic replacement, hash reporting, mode `0600`, status, and uninstall.
40
+ - `agent-electron` tests use a fake RPC transport and prove argv parsing, source reading, content forwarding without output leakage, stable JSON, and error handling.
41
+ - Existing Desktop and Skill suites must remain green.
42
+
43
+ ## Release
44
+
45
+ Commit and push Desktop independently after its tests pass. Then bump and publish `agent-electron` with a tag-driven public Skill release. A Desktop application release is a separate explicit step because packaging and distributing a new Desktop binary affects all clients.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cicy-desktop",
3
- "version": "2.1.278",
3
+ "version": "2.1.280",
4
4
  "description": "CiCy - AI-powered operating system browser",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node ./bin/cicy-desktop",
11
+ "test": "node --test \"test/**/*.test.js\"",
11
12
  "start:master": "node src/master/master-main.js",
12
13
  "format": "prettier --write \"src/**/*.js\"",
13
14
  "format:check": "prettier --check \"src/**/*.js\"",
@@ -143,10 +144,10 @@
143
144
  "//optionalDependencies": "(2026-06 回调): mac/linux 改回 native cicy-code(:8008,colima 在 16G mac 上压垮内存)→ 重新内置 cicy-code-<plat> / cicy-mihomo-<plat>,localbin.fromBundle 零网络 seed(npm 仅作更新通道,带 npmmirror→npmjs 回退)。这些由 scripts/sync-runtime-deps.cjs 在 tag-push 时同步到最新版。npm 的 os/cpu 字段保证每个平台只装自己那份。Windows 仍走 docker(WSL :8009),它那份 cicy-code-windows 用不到但 bundle 着无害。",
144
145
  "optionalDependencies": {
145
146
  "electron": "41.0.3",
146
- "cicy-code-darwin-x64": "2.3.563",
147
- "cicy-code-darwin-arm64": "2.3.563",
148
- "cicy-code-linux-x64": "2.3.563",
149
- "cicy-code-linux-arm64": "2.3.563",
147
+ "cicy-code-darwin-x64": "2.3.568",
148
+ "cicy-code-darwin-arm64": "2.3.568",
149
+ "cicy-code-linux-x64": "2.3.568",
150
+ "cicy-code-linux-arm64": "2.3.568",
150
151
  "cicy-code-windows-x64": "2.3.193",
151
152
  "cicy-mihomo-darwin-x64": "1.10.4",
152
153
  "cicy-mihomo-darwin-arm64": "1.10.4",
@@ -0,0 +1,17 @@
1
+ function createDockerAuditRecord(channel, event, args = {}) {
2
+ return {
3
+ ts: new Date().toISOString(),
4
+ channel,
5
+ webContentsId: event?.sender?.id ?? null,
6
+ senderUrl: event?.senderFrame?.url || event?.sender?.getURL?.() || "",
7
+ args,
8
+ };
9
+ }
10
+
11
+ function auditDestructiveIpc(log, channel, event, args = {}) {
12
+ const record = createDockerAuditRecord(channel, event, args);
13
+ log.warn(`[docker-audit] ${JSON.stringify(record)}`);
14
+ return record;
15
+ }
16
+
17
+ module.exports = { createDockerAuditRecord, auditDestructiveIpc };
@@ -178,6 +178,8 @@ contextBridge.exposeInMainWorld("cicy", {
178
178
  appUpdate: () => logInvoke("docker:app-update"),
179
179
  appStop: () => logInvoke("docker:app-stop"),
180
180
  appUpgrade: () => logInvoke("docker:app-upgrade"),
181
+ getProtect: () => logInvoke("docker:get-protect"), // 容器保护开关(默认开)
182
+ setProtect: (on) => logInvoke("docker:set-protect", !!on),
181
183
  getPorts: () => logInvoke("docker:get-ports"), // 读已发布的额外端口
182
184
  setPorts: (ports) => logInvoke("docker:set-ports", { ports }), // 存端口 + recreate 带 -p
183
185
  // Open :8008 with the live container token (refuses if it can't read it).
@@ -20,6 +20,9 @@ const fs = require("fs");
20
20
  const os = require("os");
21
21
  const path = require("path");
22
22
  const log = require("electron-log");
23
+ const { auditDestructiveIpc } = require("./docker-audit");
24
+ const { nextGatewayKeyHealth } = require("../sidecar/gateway-key-health");
25
+ const dockerProtect = require("../sidecar/docker-protect"); // 容器保护开关(默认开)
23
26
  // Background-computed docker status lives here; the homepage READS this (never
24
27
  // probes WSL live → never blocks the UI / strands it on 「重试检测」).
25
28
  const DOCKER_STATUS_FILE = path.join(os.homedir(), "cicy-ai", "db", "docker-status.json");
@@ -137,6 +140,8 @@ function register({ sidecarLogPath } = {}) {
137
140
  // docker:app-status handler just returns the cache — instant, never blocks.
138
141
  let _dockerStatusCache = null;
139
142
  let _dockerDaemonBusy = false;
143
+ let _gatewayKeyMissingChecks = 0;
144
+ let _autostartEnsured = false;
140
145
  async function refreshDockerStatus() {
141
146
  try {
142
147
  const s = await appDocker.status(APP_PORT); // { wsl, distro, engineUp, running, unknown }
@@ -183,14 +188,31 @@ function register({ sidecarLogPath } = {}) {
183
188
  // 数据保留)。mac/win 同一套(hasGatewayKey 两个 docker 模块都实现了)。
184
189
  try {
185
190
  const opts = await appOpts();
186
- if (opts.env && opts.env.CICY_AI_GATEWAY_LLM_API_KEY && appDocker.hasGatewayKey
187
- && !(await appDocker.hasGatewayKey(APP_CONTAINER))) {
188
- log.info("[docker-daemon] 容器在跑但没网关 key key 重建(数据保留)");
189
- await appDocker.recreate(opts);
190
- try { await registerAppTeam(); } catch {}
191
- await refreshDockerStatus();
191
+ if (opts.env && opts.env.CICY_AI_GATEWAY_LLM_API_KEY && appDocker.hasGatewayKey && dockerProtect.isProtected()) {
192
+ _gatewayKeyMissingChecks = 0; // 容器保护:绝不因缺 key 自动重建;用户可从卡片菜单手动「重建」
193
+ } else if (opts.env && opts.env.CICY_AI_GATEWAY_LLM_API_KEY && appDocker.hasGatewayKey) {
194
+ const keyState = await appDocker.hasGatewayKey(APP_CONTAINER);
195
+ const health = nextGatewayKeyHealth(_gatewayKeyMissingChecks, keyState);
196
+ _gatewayKeyMissingChecks = health.missingChecks;
197
+ if (!health.shouldRecreate) {
198
+ if (keyState === false) log.warn(`[docker-daemon] 网关 key 明确缺失 (${_gatewayKeyMissingChecks}/3),暂不重建`);
199
+ } else {
200
+ log.warn("[docker-daemon] 网关 key 连续 3 次明确缺失 → 带 key 重建(数据保留)");
201
+ auditDestructiveIpc(log, "docker:self-heal-missing-gateway-key", null, {
202
+ container: APP_CONTAINER,
203
+ confirmedMissingChecks: 3,
204
+ });
205
+ await appDocker.recreate(opts);
206
+ try { await registerAppTeam(); } catch {}
207
+ await refreshDockerStatus();
208
+ }
192
209
  }
193
210
  } catch (e) { log.warn(`[docker-daemon] key self-heal failed: ${e.message}`); }
211
+ // 开机自启链:每次会话确认一次登录任务 + wsl.conf [boot](幂等;老装机也能补上)。
212
+ if (!_autostartEnsured && appDocker.ensureAutostart) {
213
+ _autostartEnsured = true;
214
+ try { await appDocker.ensureAutostart(); } catch (err) { log.warn(`[docker-daemon] ensureAutostart failed: ${err.message}`); }
215
+ }
194
216
  // Chrome 代理:开关开着但宿主 mihomo 没跑 → 拉起(从容器同步配置,幂等)。
195
217
  try { await maybeStartChromeProxy(); } catch (e) { log.warn(`[chrome-proxy] auto-start failed: ${e.message}`); }
196
218
  }
@@ -214,12 +236,16 @@ function register({ sidecarLogPath } = {}) {
214
236
  // 已在跑也同步一次配置(容器侧节点可能被云端更新),变了才重启。幂等。二进制不在会自动下。
215
237
  async function maybeStartChromeProxy() {
216
238
  if (!APP_DOCKER_SUPPORTED) return;
217
- const yaml = await appDocker.readMihomoConfig(APP_CONTAINER);
239
+ const [yaml, selections] = await Promise.all([
240
+ appDocker.readMihomoConfig(APP_CONTAINER),
241
+ appDocker.readMihomoSelections(APP_CONTAINER),
242
+ ]);
218
243
  if (hostMihomo.binPresent() && hostMihomo.running()) {
219
244
  if (hostMihomo.writeConfig(yaml)) hostMihomo.start({ force: true });
245
+ await hostMihomo.syncSelections(selections);
220
246
  return;
221
247
  }
222
- await hostMihomo.enable({ containerYaml: yaml });
248
+ await hostMihomo.enable({ containerYaml: yaml, selections });
223
249
  }
224
250
 
225
251
  ipcMain.handle("sidecar:status", async () => {
@@ -464,10 +490,12 @@ function register({ sidecarLogPath } = {}) {
464
490
  // ⋯ menu → 重建 Docker:删容器 + 用新 env(docker team 网关 key)重新 docker run(保留
465
491
  // volume 数据)。换 key 的唯一途径。渲染端已 confirm。
466
492
  ipcMain.handle("docker:app-recreate", async (e) => {
493
+ auditDestructiveIpc(log, "docker:app-recreate", e);
467
494
  try {
468
495
  await ensureDockerTeam();
469
496
  await appDocker.recreate({
470
497
  ...(await appOpts()),
498
+ force: true, // 用户在卡片菜单明确点击并 confirm → 放行
471
499
  onProgress: (ev) => { try { e.sender.send("docker:app-progress", ev); } catch {} },
472
500
  });
473
501
  await registerAppTeam();
@@ -509,11 +537,13 @@ function register({ sidecarLogPath } = {}) {
509
537
 
510
538
  // ⋯ menu → 升级: re-pull the latest R2 image, re-create the :8008 container.
511
539
  ipcMain.handle("docker:app-upgrade", async (e) => {
540
+ auditDestructiveIpc(log, "docker:app-upgrade", e);
512
541
  if (!APP_DOCKER_SUPPORTED) return { ok: false, error: "Docker cicy-code 仅支持 Windows" };
513
542
  try {
514
543
  await ensureDockerTeam();
515
544
  const result = await appDocker.upgrade({
516
545
  ...(await appOpts()),
546
+ force: true, // 用户显式升级
517
547
  onProgress: (ev) => { try { e.sender.send("docker:app-progress", ev); } catch {} },
518
548
  });
519
549
  if (result && result.ok) await registerAppTeam();
@@ -524,18 +554,27 @@ function register({ sidecarLogPath } = {}) {
524
554
  });
525
555
 
526
556
  // 端口设置:读当前已发布的额外端口(:8008 固定,不在列)。
557
+ // 容器保护开关:开启时任何自动流程都不得 rm/recreate 容器或 wsl --shutdown。
558
+ ipcMain.handle("docker:get-protect", () => ({ ok: true, enabled: dockerProtect.isProtected() }));
559
+ ipcMain.handle("docker:set-protect", (e, on) => {
560
+ auditDestructiveIpc(log, "docker:set-protect", e, { enabled: !!on });
561
+ try { return { ok: true, enabled: dockerProtect.setProtected(!!on) }; } catch (err) { return { ok: false, error: err.message }; }
562
+ });
563
+
527
564
  ipcMain.handle("docker:get-ports", () => {
528
565
  return { ok: true, mainPort: APP_PORT, ports: readExtraPorts() };
529
566
  });
530
567
  // 保存端口 → 持久化 → recreate 容器带上新的 -p(:8008 + 各额外端口)。volume 保留。
531
568
  ipcMain.handle("docker:set-ports", async (e, input) => {
569
+ auditDestructiveIpc(log, "docker:set-ports", e, { ports: input?.ports });
532
570
  if (!APP_DOCKER_SUPPORTED) return { ok: false, error: "Docker cicy-code 仅支持 Windows" };
533
571
  const ports = sanitizePorts(input && input.ports);
534
572
  writeExtraPorts(ports);
535
573
  try {
536
574
  await ensureDockerTeam();
537
575
  const result = await appDocker.recreate({
538
- ...(await appOpts()), // 已含 extraPorts: readExtraPorts()(刚写入的)
576
+ ...(await appOpts()),
577
+ force: true, // 用户显式改端口 // 已含 extraPorts: readExtraPorts()(刚写入的)
539
578
  onProgress: (ev) => { try { e.sender.send("docker:app-progress", ev); } catch {} },
540
579
  });
541
580
  await registerAppTeam();
@@ -655,11 +694,12 @@ function register({ sidecarLogPath } = {}) {
655
694
  try { return { ok: true, dood: sidecar.isDood() }; } catch (e) { return { ok: false, dood: false, error: e.message }; }
656
695
  });
657
696
  ipcMain.handle("sidecar:set-dood", async (e, on) => {
697
+ auditDestructiveIpc(log, "sidecar:set-dood", e, { enabled: !!on });
658
698
  const emit = (ev) => { try { e.sender.send("sidecar:op-progress", { op: "restart", ...ev }); } catch {} };
659
699
  try {
660
700
  sidecar.setDood(!!on);
661
701
  emit({ phase: "swap", status: "running", message: `容器 Docker 访问已${on ? "开启" : "关闭"},正在重建容器…` });
662
- try { await appDocker.recreate({ ...(await appOpts()), onProgress: emit }); }
702
+ try { await appDocker.recreate({ ...(await appOpts()), force: true, onProgress: emit }); }
663
703
  catch (err) { try { await appDocker.dockerRestart?.({ onProgress: emit }); } catch {} }
664
704
  let up = false;
665
705
  for (let i = 0; i < 240; i++) { if ((await appDocker.probeHealth?.(PORT)) || (await sidecar.probeExisting(PORT))) { up = true; break; } await new Promise((r) => setTimeout(r, 500)); }
@@ -24,7 +24,16 @@
24
24
  // webview gets the awaited promise
25
25
 
26
26
  const { contextBridge, ipcRenderer } = require("electron");
27
- const { resolveReportedCicyTheme } = require("../tabbrowser/tab-theme");
27
+ // NOTE: this preload runs in sandboxed renderers (<webview>, trusted popups),
28
+ // where `require` only resolves Electron built-ins — a relative require of
29
+ // ../tabbrowser/tab-theme aborts the whole preload. Keep the helper inline and
30
+ // in sync with src/tabbrowser/tab-theme.js (covered by test/webview-preload.test.js).
31
+ function normalizeCicyTheme(theme) {
32
+ return theme === "light" || theme === "dark" ? theme : null;
33
+ }
34
+ function resolveReportedCicyTheme(documentTheme, savedTheme) {
35
+ return normalizeCicyTheme(documentTheme) || normalizeCicyTheme(savedTheme) || "light";
36
+ }
28
37
 
29
38
  // cicy-code owns the theme preference (`cicy_theme`) and publishes the applied
30
39
  // value on <html data-theme>. Report that contract to the owning tab window so
@@ -0,0 +1,23 @@
1
+ // Copyright 2026 CiCy AI
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ const CHROME_INSTALL_URL = "https://www.google.com/chrome/";
5
+
6
+ function isChromeBinaryMissingError(error) {
7
+ return /Chrome\/Chromium binary not found/i.test(String((error && error.message) || error || ""));
8
+ }
9
+
10
+ async function handleChromeLaunchError(error, { openInstallPage, showError }) {
11
+ if (isChromeBinaryMissingError(error)) {
12
+ await openInstallPage(CHROME_INSTALL_URL);
13
+ return true;
14
+ }
15
+ await showError(error);
16
+ return false;
17
+ }
18
+
19
+ module.exports = {
20
+ CHROME_INSTALL_URL,
21
+ handleChromeLaunchError,
22
+ isChromeBinaryMissingError,
23
+ };
package/src/main.js CHANGED
@@ -14,6 +14,7 @@ const { openWindowForBackend } = require("./backends/window-manager");
14
14
  const { Menu } = require("electron");
15
15
  const { dialog } = require("electron");
16
16
  const { setupAppIcons } = require("./tray");
17
+ const { handleChromeLaunchError } = require("./chrome/chrome-install-navigation");
17
18
  const { brandHostElectron } = require("./utils/brand-host-electron");
18
19
  const appUpdater = require("./app-updater");
19
20
 
@@ -733,22 +734,31 @@ function ensureAutoLaunch() {
733
734
  // cicy-desktop bin) instead of process.execPath, which is a transient
734
735
  // electron path that breaks on every version/cache change (the old auto-
735
736
  // start bug). Skip a pure source checkout so devs aren't auto-added.
736
- const installed = electronApp.isPackaged ||
737
- __dirname.includes(`${path.sep}node_modules${path.sep}`);
738
- if (!installed) return;
737
+ // 源码 checkout 也注册(要求:登录即自启 cicy-desktop → :8008)。applet 不存在时
738
+ // ensureMacLoginItem 自己会跳过并在下次重试。
739
739
  ensureMacLoginItem(want);
740
740
  return;
741
741
  }
742
742
 
743
- // win/linux: unchanged — only manage login items for packaged builds.
744
- if (!electronApp.isPackaged) return;
745
743
  if (process.platform === "win32") {
746
- const cur = electronApp.getLoginItemSettings();
744
+ // 打包版:登录项指向自身 exe。源码运行(npm start):必须走 bin/cicy-desktop 启动器
745
+ // (它先起 master 再起 worker,直接起 electron.exe 会缺 master)。登录项指向一个
746
+ // wscript 隐藏启动的 VBS → `cmd /c node .\bin\cicy-desktop`,全程无控制台窗口。
747
+ const fromSource = !electronApp.isPackaged;
748
+ let opts;
749
+ if (fromSource) {
750
+ const vbs = writeSourceAutostartVbs();
751
+ opts = { openAtLogin: want, path: "wscript.exe", args: ["//B", "//Nologo", vbs] };
752
+ } else {
753
+ opts = { openAtLogin: want, args: ["--hidden"] };
754
+ }
755
+ const cur = electronApp.getLoginItemSettings(fromSource ? { path: opts.path, args: opts.args } : undefined);
747
756
  if (cur.openAtLogin !== want) {
748
- electronApp.setLoginItemSettings({ openAtLogin: want, args: ["--hidden"] });
749
- log.info(`[autostart] openAtLogin → ${want}`);
757
+ electronApp.setLoginItemSettings(opts);
758
+ log.info(`[autostart] openAtLogin → ${want}${fromSource ? ` (source: ${opts.path} ${opts.args.join(" ")})` : ""}`);
750
759
  }
751
760
  } else if (process.platform === "linux") {
761
+ if (!electronApp.isPackaged) return;
752
762
  ensureLinuxAutostart(want);
753
763
  }
754
764
  } catch (e) {
@@ -756,6 +766,24 @@ function ensureAutoLaunch() {
756
766
  }
757
767
  }
758
768
 
769
+ // 源码模式 Windows 登录自启的隐藏启动器(VBS)。写到 %LOCALAPPDATA%\cicy-desktop,幂等覆盖。
770
+ // 继承当前的 CICY_DEBUG(用 start-cicy-desktop-win.bat 起的就带 =1),其余环境用登录时的用户环境。
771
+ function writeSourceAutostartVbs() {
772
+ const dir = path.join(process.env.LOCALAPPDATA || os.homedir(), "cicy-desktop");
773
+ fs.mkdirSync(dir, { recursive: true });
774
+ const vbs = path.join(dir, "autostart.vbs");
775
+ const appDir = electronApp.getAppPath();
776
+ const q = (s) => String(s).replace(/"/g, '""');
777
+ const lines = [
778
+ 'Set sh = CreateObject("WScript.Shell")',
779
+ `sh.CurrentDirectory = "${q(appDir)}"`,
780
+ ];
781
+ if (process.env.CICY_DEBUG) lines.push(`sh.Environment("Process")("CICY_DEBUG") = "${q(process.env.CICY_DEBUG)}"`);
782
+ lines.push(`sh.Run "cmd /c node .\\bin\\cicy-desktop", 0, False`);
783
+ fs.writeFileSync(vbs, lines.join("\r\n") + "\r\n");
784
+ return vbs;
785
+ }
786
+
759
787
  // mac login item pointing at the Desktop applet (~/Desktop/CiCy Desktop.app).
760
788
  // The applet is created by the launcher (bin/cicy-desktop ensureMacDesktopApp)
761
789
  // BEFORE electron spawns, so it exists by the time this runs. Idempotent:
@@ -1353,7 +1381,12 @@ electronApp.whenReady().then(async () => {
1353
1381
  accountIdx,
1354
1382
  activateIfRunning: true,
1355
1383
  });
1356
- } catch (e) { showProfileError("Chrome", e); }
1384
+ } catch (e) {
1385
+ await handleChromeLaunchError(e, {
1386
+ openInstallPage: (url) => openElectronProfile(1, url),
1387
+ showError: (error) => showProfileError("Chrome", error),
1388
+ });
1389
+ }
1357
1390
  };
1358
1391
  const openNativeChrome = () => {
1359
1392
  try { require("./tools/chrome-tools").launchNativeChrome(); }