# @ffai/agent-desktop

FFAI Agent 的 **Electron Desktop 壳**（Windows + macOS）。装 Chromium webview 跑同一份 `frontend/` React UI；额外暴露 HostBridge IPC 通道，让后端 `client:` 工具（PR12/PR13）反向调用本地能力。

> Phase 2 PR11 落地，外部依赖：`packages/agent-protocol`（HostBridge 接口契约）。

## 目录

```
desktop/
├── src/
│   ├── main.ts        Electron 主进程（BrowserWindow + IPC + Tray）
│   └── preload.ts     contextBridge 暴露 window.ffaiHostBridge
├── dist/              tsc 输出（git ignored）
├── package.json
└── tsconfig.json
```

## 本地起

```bash
# 1) 起前端
cd ../frontend && npm run dev   # http://localhost:3000

# 2) 装 Electron（首次，约 ~250MB）
cd ../desktop && npm install

# 3) 起壳（main + preload + Chromium 装 frontend）
npm run dev
```

## HostBridge 调用

renderer 端：

```ts
const result = await window.ffaiHostBridge.invoke("client:notify", {
  title: "任务完成",
  body: "查看结果"
});
```

支持的 capability 见 `@ffai/agent-protocol` 的 `DESKTOP_ELECTRON_CAPABILITIES`。

| 工具 | 状态 | 落点 PR |
|---|---|---|
| `client:notify` | ✅ 已实现（PR11.2 smoke） | PR11.2 |
| `client:shell.openExternal` | ✅ 已实现（PR11.2 smoke） | PR11.2 |
| `client:fs.read/write/list` | 🔜 未实现 | PR12 |
| `client:clipboard.read/write` | 🔜 未实现 | PR12 |
| `client:shell.exec` | 🔜 未实现（需 OS 沙盒 + 红队 0 逃逸） | PR13 |

## 浏览器独占 API 审计（PR11.4）

frontend 全量扫一遍 `navigator.*` / `window.show*Picker` / `new Notification(...)` 等浏览器独占 API。结果：

| API | 命中数 | 在 Electron 里 | 处理 |
|---|---|---|---|
| `navigator.clipboard.writeText` | 18 | ✅ Chromium 全支持（HTTPS / file:// / localhost 触发的 user gesture context 内可用） | PR12 切走 HostBridge.clipboard，本 PR 不阻塞 |
| `navigator.geolocation` | 4 | ⚠ Electron 默认无；需 `app.setPermissionRequestHandler` | 全是 siteattendance 移动打卡，桌面端不预期走到 |
| `showOpenFilePicker` / `showSaveFilePicker` / `usb` / `bluetooth` / `serviceWorker` | 0 | — | 无 |

**结论**：frontend 装到 Electron 默认可跑，无致命依赖。clipboard 切 HostBridge 的工作集中到 PR12。

## i18n（PR11.5）

主进程文案（tray menu / 窗口标题 / OS Notification 系统级标题）走 `src/i18n.ts`：

- 启动时 `app.getLocale()` 选 `zh-CN` / `en-US`，fallback `en-US`
- 字符串放 `locales/{locale}.json`
- 校验 `npm run check:i18n` —— 扫 `t("...")` 调用 vs locale 文件，任一 missing 退出非零

renderer 端的 i18n 仍是 `frontend/` 自己的 `react-i18next` 体系，本目录不重复造轮子。

## 打包（PR11.6）

`electron-builder.yml` 已配 macOS + Windows 双平台：

```bash
npm run dist:mac      # .dmg arm64 + x64
npm run dist:win      # NSIS .exe x64 + arm64
```

签名 / notarize 走环境变量驱动（凭据**不进仓库**）：

| 平台 | 必备 env | 缺失时 |
|---|---|---|
| macOS | `APPLE_ID` + `APPLE_APP_SPECIFIC_PASSWORD` + `APPLE_TEAM_ID` | 自动跳过签名 + notarize（仅 dev / 内部分发用） |
| Windows | `CSC_LINK` + `CSC_KEY_PASSWORD` | 自动跳过签名 |

**PR11.6 当前限制**：

- 图标资产 (`build/icon.icns` / `build/icon.ico`) 待设计同学补；缺失时 builder 报错。**PR11 不实际跑 dist**，配置就位等 PR11 合并后单独 PR 补 icon + 首次签名。
- macOS Hardened Runtime entitlements 在 `build/entitlements.mac.plist`，已预开 fs / network 项避免 PR12 二次公证。
