# Electron 桌面壳跨机 smoke 测两个反复踩的坑（PR11.2）

**日期**：2026-05-16
**触发**：FFAI Agent PR11.2 `desktop/` 脚手架；Linux 开发机起不了 GUI，通过 SSH 反向隧道（`localhost:2222 → Chentao Mac`）在 Mac 上跑 smoke。
**结论**：Electron 主进程成功启动（main + GPU helper + Renderer + Network service 4 进程齐活），但路上撞了两个**与代码逻辑无关的环境坑**，未来任何在公司机器装 Electron 的项目都可能撞。

---

## 坑 1：Mac 公司网络挡 GitHub releases CDN，`npm install electron` 后 postinstall 静默失败

### 现象

- `npm install electron@33` 在 Mac 上 "added 72 packages" 成功结束
- 但 `node_modules/electron/dist/` **不存在**，`path.txt` **不存在**
- 跑 `electron` 报 `Error: Electron failed to install correctly, please delete node_modules/electron and try installing again`
- 手跑 `node node_modules/electron/install.js` → `RequestError: connect ETIMEDOUT 20.205.243.166:443`（Azure / GitHub Releases CDN IP）

### 元根因

Electron npm 包**本体**很小（几 MB），二进制（200+ MB Chromium runtime）由 `install.js` postinstall 钩子单独从 `https://github.com/electron/electron/releases` 下载。公司网络对 GitHub releases CDN 有出口限制 → 下载 ETIMEDOUT → postinstall **失败但 npm install 仍标 success**（"added 72 packages" 是误导性的）。

### 解法

设 `ELECTRON_MIRROR` 走 npmmirror（阿里 CDN，国内可达）：

```bash
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ \
  node node_modules/electron/install.js
```

→ Electron.app + 全套二进制就位。

### 永久化建议（未做）

`desktop/package.json` 加 `.npmrc` 或 install script 写死镜像：

```ini
# desktop/.npmrc
electron_mirror=https://npmmirror.com/mirrors/electron/
```

待 PR11.6 打包阶段一并落，避免下次任何人 clone 后 install 又撞。

---

## 坑 2：`packages/agent-protocol` 是 `"type":"module"` ESM-only，desktop tsconfig 编 CJS，运行时 require 直接炸

### 现象

Electron 主进程启动报：

```
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]:
  No "exports" main defined in node_modules/@ffai/agent-protocol/package.json
```

### 元根因

- `packages/agent-protocol/package.json` 是：
  ```json
  "type": "module",
  "exports": { ".": { "types": "...d.ts", "import": "...js" } }
  ```
  **只有 `import` 条目，没有 `require` 条目**。
- `desktop/tsconfig.json` 用 `"module": "CommonJS"`（Electron main process 默认 CJS 体感更稳，preload 也是 CJS）。
- tsc 把 `import { DESKTOP_ELECTRON_CAPABILITIES } from "@ffai/agent-protocol"` 编成 `require("@ffai/agent-protocol")` → Node ESM resolver 找不到 CJS 入口 → 抛 `ERR_PACKAGE_PATH_NOT_EXPORTED`。
- 这条**类型 import 不会炸**（tsc 编译期擦除），**只有运行时值 import 炸**。

### 解法（临时）

把 `DESKTOP_ELECTRON_CAPABILITIES` 这个 runtime 常量**就地内联**到 `desktop/src/main.ts`，类型用 `import type` 拉（保证 tsc 擦除）：

```ts
import type { HostCapability, HostInvokeResult } from "@ffai/agent-protocol";

const DESKTOP_ELECTRON_CAPABILITIES: readonly HostCapability[] = [
  "fs.read", "fs.write", ...
];
```

trade-off：常量在 `agent-protocol` 和 `desktop/main.ts` 各一份，**HostCapability 类型仍是单一事实源**——常量漂移会被 TS 类型系统在 desktop 编译期捕获（任何不属于 union 的字符串会报错）。

### 元根因解（PR15.5）

切真正 monorepo workspace（pnpm/turbo）+ 把 `agent-protocol` 改 dual ESM/CJS（加 `exports.require` 条目）或全仓库统一 ESM。在那之前所有跨包 runtime import **只走类型**，runtime 值在消费侧重新声明。

### 复用建议

- 任何 desktop / cli / 服务端代码消费 `agent-protocol` 时，先查 protocol 的 `exports` 是否含 `require` 条目
- 不含 → 仅 `import type`，runtime 值就地写
- 撞到 `ERR_PACKAGE_PATH_NOT_EXPORTED` 立刻往这两个方向看，不要去改 protocol 包加 require 条目（那会引发 dual package hazard）

---

## 复用脚本（Mac SSH 反向隧道一键起 Electron smoke）

如果未来 PR12/13/15 还要在 Mac 上跑 Electron smoke，下面这串命令能直接用（替换路径）：

```bash
# 1. rsync 代码（不含 node_modules）
rsync -az --delete -e "ssh -p 2222" --exclude node_modules \
  desktop packages/agent-protocol Chentao@localhost:~/ffai-desktop-smoke/

# 2. Mac 上重整目录（match desktop 的 file:../packages/.. 链接）
ssh -p 2222 Chentao@localhost "cd ~/ffai-desktop-smoke && mkdir -p packages && mv agent-protocol packages/ 2>/dev/null"

# 3. 装 + 下载 electron 二进制（必须走镜像）
ssh -p 2222 Chentao@localhost "cd ~/ffai-desktop-smoke/packages/agent-protocol && npm i && npm run build"
ssh -p 2222 Chentao@localhost "cd ~/ffai-desktop-smoke/desktop && npm i"
ssh -p 2222 Chentao@localhost "cd ~/ffai-desktop-smoke/desktop/node_modules/electron \
  && ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ node install.js"

# 4. 编译 + 起
ssh -p 2222 Chentao@localhost "cd ~/ffai-desktop-smoke/desktop && npm run build \
  && (nohup ./node_modules/.bin/electron dist/main.js > /tmp/electron-smoke.log 2>&1 &) \
  && sleep 5 && ps aux | grep 'Electron.app/Contents/MacOS/Electron' | grep -v grep | head -3 \
  && tail -20 /tmp/electron-smoke.log"

# 5. 收工杀进程
ssh -p 2222 Chentao@localhost "pkill -f 'Electron.app/Contents/MacOS/Electron'"
```

---

## 何时升级为 skill / standard

- 公司网络 GitHub releases 阻塞已经在**多个项目**踩过（earlier learnings 有相关线索）→ 可以提到 `docs/standards/` 加一条「内部网络下载二进制的强制镜像表」standard（electron / chromium / playwright browsers / puppeteer 同类资源）。
- Mac SSH smoke 流程若 Phase 2 后续 PR 反复用，可以提到 `.agents/skills/electron-smoke-via-mac/`。

当前先沉淀为 learning，不急升级。
