Files
geo/docs/superpowers/specs/2026-04-18-electron-desktop-client-design.md
T
root 9ec9314aea docs(desktop-client): add Electron 桌面客户端 replacement design spec
Captures the full plan for replacing apps/browser-extension with an
Electron tray client: single main process with multi-session isolation,
embedded web-view login with OS-encrypted local cookie vault, CDP-based
LLM monitoring (no official APIs, cost-driven), unified PublishAdapter
and MonitorAdapter interfaces with dual-track updates (per-adapter patch
channel + full release channel), Playwright-grade tracing, new
/api/desktop/* protocol with SSE + 60s pull, and 5-arch multi-OS
packaging strategy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 20:28:42 +08:00

563 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Electron 桌面客户端设计(全面替换 WXT 浏览器插件)
**状态**Draft · 待实施
**作者**@liangxu + Claude
**日期**2026-04-18
**范围**:新增 `apps/desktop-client/``/api/desktop/*` 服务端路由、相关 DB schema;删除 `apps/browser-extension/``/api/plugin/*` 路由及相关代码。
---
## 1. 背景与动机
现有 `apps/browser-extension/`WXT MV3 Chrome 扩展,"GEO Publisher")同时承担两类工作:
1. 向 13 家中国媒体平台做内容分发(微信公众号、头条号、百家号、搜狐号、网易号、ZOL、懂车帝、知乎、企鹅号、简书、掘金、B 站、什么值得买)。
2. 监控主流 LLM(豆包、千问、DeepSeek)在给定查询下的输出,判定品牌/KOL 是否被引用。
当前问题:
- **LLM 抓取不稳定**。监控模块依赖 DOM 选择器与 `webRequest.onBeforeRequest` 在 MV3 下受限,无法可靠拿到 SSE 响应 Body;平台 UI 一变就挂。
- **适配器受 MV3 约束**。后台 service worker 频繁休眠,长任务难以保证完成;跨平台复杂登录(QR、滑块、SMS)在扩展内体验差。
- **不可扩展到真正的系统级自动化**。系统托盘、开机自启、多账号并发 session 隔离、进程级 CDP 调试,这些 MV3 都做不到。
因此:**全面替换**为一个 Electron 桌面客户端,承担上述全部职责。浏览器扩展停止维护并下线。
---
## 2. 设计硬约束(已确认)
1. **全面替换**,不做"先 monitoring 后 publishing"的分阶段替换。
2. **不做向后兼容**。因为是开发版本阶段,服务端协议、DB schema、认证字段按桌面客户端的最佳形态重新设计,不为插件保留旧字段或旧路由。
3. **登录走嵌入式 Web View**。用户在 Electron 真实平台登录页里自己完成扫码/密码/短信/滑块;我们只保存 cookie,不保存账号密码。
4. **Cookie 本地存储 + OS 级加密**Electron `safeStorage`macOS Keychain / Windows DPAPI / Linux libsecret)。服务端**永不存储** Cookie 或任何平台凭据。
5. **LLM 监控一律走网页 + CDP 拦截**,不调用任何 LLM 官方 API(成本导向;多账号 = 多配额,线性成本比 token 计费友好)。后端 `llm.api_key` 是后端自用管线,与客户端监控完全解耦。
6. **不打包 Playwright**。复用 Electron 自带 Chromium,通过 `webContents.debugger` 直接用 CDP。包体越小越好。
7. **支持 5 目标**Windows x64/arm64、macOS Intel+Apple Silicon、Linux x64/arm64。
8. **一个微信号只给一个 workspace**——不支持跨租户共享账号。
---
## 3. 架构总览
### 3.1 进程拓扑
单 Electron 进程树(方案 A)。
- **Main 进程**Node):任务调度器、Session 注册表、加密凭据仓库、CDP debugger 管理、与后端 REST/SSE 通信、Tray & autoUpdater。
- **Renderer 进程**(托盘窗口 / 发布预览窗口):Vue UI,复用 `apps/admin-web` 的组件库与 i18n。禁用 `nodeIntegration`,通过 `contextBridge` 暴露窄接口。
- **工作 BrowserView**:每个登录账号一个 `persist:<account-id>` session + 一个隐藏 `BrowserView`,挂在离屏 `BrowserWindow` 上。常驻但不可见;需要操作时 `webContents.debugger.attach()`
- 只有当实测 CDP 流式抓取会让 UI 卡顿时,才把 debugger 交互迁到 `utilityProcess`,其他模块保持不动。
### 3.2 模块分层
新增 `apps/desktop-client/`,作为 pnpm workspace 的新包:
```
apps/desktop-client/
main/
bootstrap.ts # app ready, single-instance-lock, tray, autoUpdater
session-registry.ts # account-id → BrowserView + session
vault.ts # safeStorage 包 cookie/tokenSQLite 持久化
scheduler.ts # 本地 cron + 服务端 task lease 合流
circuit-breaker.ts # 平台级熔断
keep-alive.ts # 探活 / 保活调度
crash-recovery.ts # 启动时扫描 in-flight 任务
tracing/
recorder.ts # 启停;订阅 CDP Page/Network/Fetch/Runtime;采样截屏
trace-writer.ts # 流式写入 .trace zip
redactor.ts # Cookie / Authorization / password 字段自动脱敏
viewer-assets/ # 内置 trace viewerVue 单页,离线)
transport/
api-client.ts # 复用 packages/http-client,注入 client_token
sse-client.ts # /api/desktop/events SSE 长连接 + pull 兜底
adapters/
_shared/
debugger-helper.ts
dom-helper.ts
upload-helper.ts
captcha-pause.ts
selectors.json # 可热更的选择器表
base.ts # PublishAdapter / MonitorAdapter 接口
wechat.ts ... smzdm.ts # 13 个发布适配器
doubao.ts qwen.ts deepseek.ts # 3 个 LLM 监控适配器
index.ts # 注册表
preload/
bridge.ts # 暴露给 renderer 的受控 API
renderer/ # 复用 apps/admin-web,打 Electron 专属 build
```
### 3.3 关键复用
- `packages/shared-types`:平台枚举、任务类型、DTO。
- `packages/http-client`HTTP 基座。
- `apps/admin-web` 的 Vue 组件与路由:删掉 SaaS 专属页面后作为 renderer 的基座。
- `apps/browser-extension/src/adapters/*` 的选择器和工作流:批量搬进来,套一层 `runInSession(accountId, fn)` 即可复用。
---
## 4. 账号 & 会话模型
### 4.1 数据结构
```ts
type Account = {
id: string // uuid,本地生成
platform: PlatformKind // 'wechat' | 'toutiao' | ... | 'doubao' | 'qwen' | 'deepseek'
purpose: 'publish' | 'monitor'
displayName: string
partitionKey: string // 'persist:acc-<id>'Electron session 隔离
createdAt: Date
lastSeenAt: Date
health: 'live' | 'expired' | 'captcha' | 'risk'
tenantId: string
workspaceId: string
keepAlive: {
mode: 'probe-only' | 'heartbeat' | 'disabled'
probeIntervalMs: number // 默认 30 min
heartbeatIntervalMs?: number // 默认 8 min,仅 heartbeat 模式
lastProbeAt?: Date
lastHeartbeatAt?: Date
failureStreak: number
}
quota: {
hourlyBudget: number
usedThisHour: number
resetAt: Date
}
}
```
### 4.2 登录流程
1. 用户在 UI 点「新增账号 → 选平台」。
2. 主进程 `session-registry.create(platform)` 分配新 partition,打开一个**可见** `BrowserWindow`(登录专用),加载该平台登录页。
3. 用户自己用扫码 / 密码 / 短信 / 滑块登录。
4. 登录探针(preload 脚本 + URL 规则)检测到已登录态后关闭窗口。
5. Electron 自动把 cookies 持久化到 `userData/Partitions/<key>/`;同时主进程读取 cookies 用 `safeStorage` 加密写本地 SQLite 作为元数据 + 加密快照(用于迁机器、备份、健康检查)。
6. 账号进入"常驻池",分配隐藏 BrowserView 挂在离屏窗口;按需激活 CDP。
### 4.3 多账号并存
同一平台多账号完全隔离——partition 不同、cookies 不同、UA 不同、可通过 session 级 proxy 走不同 IP。调度器按账号维度限流(默认同一账号并发 1、同一平台并发 3,可配置)。
### 4.4 Cookie/Session 保活策略
三个档位:
| 档位 | 动作 | 频率 | 代价 |
|---|---|---|---|
| **探活** | 加载主页,检查 DOM 登录标志 | 30 min | 低 |
| **保活** | 访问平台私有 API 触发 Set-Cookie/token 刷新 | 8 min(豆包/千问)| 中 |
| **按需激活** | 任务到来前 pre-check | 任务驱动 | 低 |
| 平台类型 | 账号示例 | 保活方式 | 节奏 |
|---|---|---|---|
| 媒体发布 | 微信/头条/B 站/知乎 等 13 家 | 只探活 | 30 min |
| 豆包 | Doubao | 主动保活 | 8 min + 30 min 探活 |
| 千问 | Qwen | 主动保活 | 8 min + 30 min 探活 |
| DeepSeek | DeepSeek | 只探活 | 30 min |
实施细节:
- **错峰**:同一平台 N 个账号按 `hash(accountId) mod period` 分布时间槽 + ±30s 抖动。
- **优先级**:业务任务 > 保活 > 探活。
- **失败升级**:保活失败 1 次立即重试;连 2 次失败降级为探活一次确认状态;探活失败则 `health: expired`,托盘红点 + OS 通知 → 用户点击弹登录窗(partition 复用,不需要重建账号)。
- **静音时段**:默认 0:00–7:00 不保活(可配置关闭)。
- **休眠感知**`powerMonitor.on('suspend'|'resume')`,休眠暂停全部保活,唤醒后补一次探活再恢复。
- **配额池**:同一 LLM 多账号时,调度器按"最近使用 + 剩余配额"挑账号。
- **风控回避**:每账号"本小时平台请求数"超阈值就停保活。
### 4.5 凭据粒度
- **不**保存账号密码。
- 只保存 Cookie + 平台侧必要 token(如 Doubao 的 `ms_token``fp`)。
- 重装客户端 → 从本地加密备份恢复,或重登一次。
---
## 5. 适配器模型
### 5.1 接口定义
```ts
interface AdapterContext {
accountId: string
session: Session
view: WebContentsView
debugger: DebuggerClient
logger: ScopedLogger
signal: AbortSignal
}
interface PublishAdapter {
platform: PlatformKind
capabilities: { images: boolean; video: boolean; markdown: boolean; /* ... */ }
probe(ctx: AdapterContext): Promise<HealthState>
keepAlive(ctx: AdapterContext): Promise<void> // 默认 no-op
publish(ctx: AdapterContext, task: PublishTask): Promise<PublishResult>
}
interface MonitorAdapter {
provider: LLMProvider
probe(ctx: AdapterContext): Promise<HealthState>
keepAlive(ctx: AdapterContext): Promise<void>
query(ctx: AdapterContext, task: MonitorQueryTask): Promise<MonitorResult>
}
```
### 5.2 监控适配器标准套路
```
1. ensureLoggedIn(ctx) → probe / 必要时触发保活
2. openChat(ctx) → 在 BrowserView 里导航或复用 tab
3. attach CDP Fetch+Network domain
4. submit query → 触发网页发出请求
5. collect SSE via CDP.Network.responseReceivedExtraInfo + getResponseBody
6. parse provider-specific payload → normalized MonitorResult
7. detach CDPfinally
```
**核心价值**:步骤 5 抓拦截后的原始 API 响应 JSON/SSE,不依赖渲染后 DOM 文本。UI 改变不影响,只有 API schema 变更才需要更新 parser。
### 5.3 发布适配器模式
每个发布任务或每个账号带 `PublishMode`
```ts
type PublishMode =
| { kind: 'manual'; requireUserReview: true } // 填好表单后暂停,等用户点"发布"
| { kind: 'auto' } // 一口气跑到底
```
状态机:`filling → readyToReview → submitting → done`。manual 在 `readyToReview` 挂一个用户 gateauto 不挂。同一份 adapter 代码两种模式共用。
文件上传:`protocol.handle('file')` + CDP `Input.dispatchDragEvent` 或直接喂 `<input type=file>`,不经服务端中转。
### 5.4 公共能力
`adapters/_shared/`
- `debugger-helper.ts`:封装 CDP attach/detach、SSE stream 收集、超时。
- `dom-helper.ts`:语义化选择器("找到登录头像")、重试、容错。
- `upload-helper.ts`:文件上传统一入口。
- `captcha-pause.ts`:遇滑块/点击验证时把 BrowserView 转前台让用户过,通过后继续。
### 5.5 双轨更新:局部热更 + 全量
| 通道 | 内容 | 节奏 | 形式 |
|---|---|---|---|
| **Patch 通道** | 单个适配器的 `selectors.json``parsers.ts`(纯声明 / 纯函数)| 分钟级按需 | 服务端签名补丁包,存 `userData/adapter-patches/<platform>/<version>/`,**下次该适配器的任务开始时动态 import 加载**(不重启客户端)|
| **Release 通道** | 二进制、主进程、UI、适配器宿主框架 | 周/双周 | electron-updater 差分包 |
Patch 通道硬约束:
- 只允许替换**声明式数据**和**纯函数模块**。静态检查拒绝 `fs/child_process/Buffer/require/import.meta.url` 等导入。
- 服务端签名 + 客户端 Ed25519 验签。
- 每个 patch 带 `{platform, version, minClientVersion}`;客户端版本不够忽略。
- UI 每个适配器显示两行版本号:`client 1.2.0 · doubao patch #47`
### 5.6 Trace 基础设施(Playwright Trace Viewer 等级)
记录内容:
| 类别 | 内容 |
|---|---|
| **CDP 事件流** | Network/DOM/Page/Fetch/Runtime 事件时间线 |
| **关键截屏** | 2 fps JPG 或手动截屏 |
| **DOM 快照** | 关键节点 outerHTML |
| **Network** | 请求+响应,包括 SSE 原始 chunks |
| **日志** | console + 结构化 |
触发策略:
- **平时**:只结构化日志,不录 trace。
- **Rolling buffer**:每个适配器保留最近 30 秒内存缓冲;任务失败自动 flush 成 trace,成功则丢弃。
- **强制录制**:UI 开关或启动参数 `--trace=always`
- **日志等级**debug/info/warn/error,按适配器独立调(`DEBUG_ADAPTERS=doubao,qwen`)。
存储:
- 位置:`userData/traces/<accountId>/<taskId>.trace.zip`
- 配额:默认保留 **50 份****500 MB**(先到先清)。
- 脱敏:Cookie / Set-Cookie / Authorization / x-xsrf-token / password / 手机号 / email 自动 `<redacted>`
- **默认不上传服务端**;用户手动点"发送诊断包"时才上传。
- 文件格式兼容 Playwright Trace Viewer`npx playwright show-trace` 可直接打开)。
---
## 6. 服务端协议
### 6.1 命名(全部重命名)
`plugin_installations` / `/api/plugin/*` / `installation_token` → 新 `desktop_clients` / `/api/desktop/*` / `client_token`。因为是开发版本、无生产流量,DB 迁移直接 `drop` 旧表、`create` 新表,不做导入或双写。
### 6.2 认证
| Token | 代表 | 发放 | 用途 |
|---|---|---|---|
| **User JWT** | 人类用户 | `/api/auth/login` | 用户在 Electron 内的交互(新增账号、创建监控计划、看报表) |
| **Client Token** | 一台 Electron 安装 | 登录后 `/api/desktop/clients/register` 换取 | 后台任务回调、任务拉取、心跳 |
Client Token 绑定 `tenant_id + workspace_id + client_id + os + cpu_arch + client_version`,SHA-256 后入库。重装/迁机器 = 重新 register。
### 6.3 任务分发:SSE + 60s pull 兜底
- 客户端启动后对 `/api/desktop/events` 建立 SSE 长连接,server 推 `{type:'task', payload}`
- 网络抖/代理不稳时退化为**每 60 秒** pull `/api/desktop/tasks/lease`
- 两渠道幂等;任务带 `task_id + deduplication_key`
- 为什么不用 WebSocketSSE 单向推已足够,穿透 Nginx/Cloudflare 更稳,代码更简单。
- 为什么不纯 pull:用户在 Web 上点"立即跑一次"需要秒级响应。
### 6.4 路由表
```
# 配对
POST /api/auth/login
POST /api/desktop/clients/register # body: {device_name, os, cpu_arch, client_version}
POST /api/desktop/clients/heartbeat # client_token, 上报活跃度
POST /api/desktop/clients/metrics # 轻量非敏感聚合指标
# 事件 & 任务
GET /api/desktop/events # SSE, client_token
GET /api/desktop/tasks/lease?kind=publish|monitor # pull 兜底
POST /api/desktop/tasks/{id}/ack
POST /api/desktop/tasks/{id}/result
POST /api/desktop/tasks/{id}/trace # 用户主动上传诊断包
# 账号元数据(不传 cookie
POST /api/desktop/accounts
PATCH /api/desktop/accounts/{id}
DELETE /api/desktop/accounts/{id}
# 管理 UIWeb / Electron UI 共用)
GET /api/tenant/monitoring-plans
POST /api/tenant/publish-jobs
# ...
```
### 6.5 服务端边界
服务端只存**任务意图**("账号 A 去豆包问 Q")与**结果**("答案文本 + 品牌是否出现")。**永不**存 Cookie、**永不**直连平台帮客户端跑。客户端失联 = 任务排队;长时间拿不到 = 超时失败。硬边界,防止 Cookie 从客户端外泄。
### 6.6 DB Schema(关键表)
- `desktop_clients`: `id, tenant_id, workspace_id, user_id, token_hash, device_name, os, cpu_arch, client_version, created_at, last_seen_at, revoked_at`
- `platform_accounts`: `id, tenant_id, workspace_id, client_id(FK), platform, display_name, health, created_at, last_seen_at`**无 cookie / token 列**
- `desktop_tasks`: `id, kind, payload(jsonb), status, client_id, lease_expires_at, created_at, result(jsonb), error(jsonb), dedup_key`
- `desktop_task_traces`: `task_id(FK), size, uploaded_at, object_key`
### 6.7 任务生命周期
```
Web UI 创建 MonitoringPlan("brand=X on doubao,qwen daily 9am")
Scheduler (server cron) 展开成 N 条 desktop_tasks (queued)
SSE push → 任意在线的、有对应账号的 desktop_client
Client ack → lease_expires_at=+10min → 执行 → result
POST /result → server 存结果 → Web UI 可视化
```
---
## 7. 错误处理与可观测性
### 7.1 错误分层
| 层 | 例子 | 处理 |
|---|---|---|
| **适配器内部**(可重试)| 选择器暂未命中、SSE 断流、CDP 瞬时断开 | 适配器内部指数退避重试,最多 3 次,不升级 |
| **账号级** | Cookie 过期、风控滑块、封号 | `health=expired/risk`;托盘红点 + OS 通知;任务返回 `ACCOUNT_UNAVAILABLE`;服务端不对该账号 retry,派给池里其它账号 |
| **平台级** | 全账号连续失败 N 次、Cloudflare 5xx | 熔断 15 分钟;新任务返回 `PLATFORM_CIRCUIT_OPEN`;到时半开探活 |
| **客户端级** | 主进程 uncaughtException、渲染进程崩溃 | 落本地 crash log → 可选上报 → 优雅重启 |
### 7.2 结构化日志
`pino` JSON,字段约定:`ts, level, module, accountId, taskId, stage, event, durationMs, bytes`。本地 rolling file10 MB × 20 份)。**不默认上传**。
### 7.3 指标上报
5 min 一次上报 `/api/desktop/clients/metrics`,只含非敏感聚合:CPU/内存/事件循环延迟、每平台每账号的任务成功率 + 平均耗时、SSE 连接状态、pull 回退次数。**不含**查询内容、回答内容、Cookie。
### 7.4 用户可见反馈
- 托盘图标三色(绿/黄/红)表示账号健康。
- OS 通知**只**在需要用户动作时弹(登录失效、捕获到验证码)。
- UI "事件中心":列最近 50 次失败,点进去直接"重新运行"或"启用录屏再跑"。
- "发送诊断包"按钮:最近 1 小时日志 + trace 打包脱敏上传,返回 `diagnostic_id`
### 7.5 崩溃恢复(幂等)
启动时扫描 `desktop_tasks``lease 属于我但 result 为空` 的任务:
- **未进入 submit 阶段**:重新 ack 再跑。
- **已进入 submit 阶段**(已确认):**放弃任务,标 UNKNOWN,由人判断**。UI 提示"上次崩溃时尚不确定是否发布成功,请去平台确认后手动标注",避免重复发布。
---
## 8. 打包、自动更新、签名
### 8.1 工具链
- **electron-builder** 多平台打包 + 差分更新 + 签名。
- **electron-updater** 跑在 Main 进程,`latest-*.yml` + blockmap 差分下发。
- **渠道**`stable/beta/dev`,用户可切。Patch 热更通道独立,不走 electron-updater。
### 8.2 目标矩阵
| OS | Arch | 产物 | 签名 |
|---|---|---|---|
| macOS | universalIntel + Apple Silicon| `.dmg` + `.zip` | **Apple Developer ID + notarytool 公证**(已有 Apple Dev 账号,CI 直接接入)|
| Windows | x64 | `.exe (nsis)` | **暂无,后期购买**(正式发布前阻塞项)|
| Windows | arm64 | `.exe (nsis)` | 同上 |
| Linux | x64 | `.AppImage` + `.deb` | GPG 可选 |
| Linux | arm64 | `.AppImage` + `.deb` | GPG 可选 |
产物大小目标:**单平台 ≤ 120 MB**。
### 8.3 CI
GitHub Actionsrunner 矩阵:`macos-14 (arm64)` + `macos-13 (x64)` 合并 universal`windows-latest` 出 x64 与 arm64`ubuntu-latest` 出 x64`ubuntu-22.04-arm` 出 arm64(或 cross-build)。发布 tag `v1.2.3` → 打包 → 上传对象存储 + 生成 `latest-*.yml``autoUpdater.checkForUpdates()` 看到。
### 8.4 更新 UX
- 默认"自动下载 + 手动安装"。下好后 Tray 角标提示"新版本可用,点击重启安装"。不打断正在跑的任务。
- 强制更新:服务端下发 `minClientVersion`;低于该版本拒绝启动,引导升级。
- 降级:服务端可下发"建议版本号",客户端比建议版本新时允许一键回滚(electron-updater `downgrade: true`)。
### 8.5 应用标识
- `app.requestSingleInstanceLock()`:禁止同用户开两个副本。
- Protocol handler`georankly://`,用于 Web 端深链接到客户端。
- macOS 关闭窗口 ≠ 退出;Windows/Linux 最小化到托盘;退出必须从 Tray 菜单。
### 8.6 安全加固
- `contextIsolation: true``nodeIntegration: false``sandbox: true` for renderer。
- CSP 严格模式;renderer 不允许加载远程 JS。
- `webContents.setWindowOpenHandler` 拦截所有 window.open → 系统浏览器。
- 禁用 `<webview>` tag,第三方平台一律用 `BrowserView`
---
## 9. 测试策略
### 9.1 金字塔
```
┌──────────────┐ 手工 + 冒烟 Playbook(每 release
│ E2E 烟雾 │
├──────────────┤ Playwright @ Electron + fake server + fake platforms(每 PR
│ 集成 / 行为 │
├──────────────┤ Vitest(每 commit
│ 单元(adapter)│
├──────────────┤ tsc --noEmit + eslint + typecheck(每保存)
│ 类型 / 静态 │
└──────────────┘
```
### 9.2 单元
- 对象:适配器的 **SSE parser**(纯函数)与 **发布状态机**
- Fixtures`tests/fixtures/sse/doubao-2026-04.txt`(脱敏后真实抓的 SSE)。
- 平台 payload 变动 → 补 fixture → 测试跟着更新。
### 9.3 集成
用 Playwright `_electron.launch` 驱动 Electron,对 **本地 fake server + 本地 fake 平台**HTTP server 喂 fixture SSE)。13+3 适配器全跑一遍,并行约 3–5 分钟。
### 9.4 集成 · 协议
客户端 + 真实 server + fake 平台:验证**任务分发 / 幂等 / 熔断 / 断线重连**。
### 9.5 人工冒烟 Playbook
每次 release 用测试账号:
- 每发布平台跑一个 draft/private publish → 立即删。
- 每 LLM 跑 3 条标准查询,对比上版本抓取结构一致性。
记成 md 文档由 QA 跟着跑,半小时完成。
### 9.6 Trace 回归套件
工具 `pnpm dev:replay <trace.zip>`:把真实 trace 里的 network responses 重放到 fake 平台,跑 adapter。历史错过的输入永远不再破解析器。
### 9.7 不做的
- 不对 electron-builder 打包产物在 CI 跑 E2E(矩阵太贵)。
- 不对 autoUpdater 真实升级做 CI(单独写 `tests/updater-smoke.md` 手动脚本)。
- 不测真平台滑块 captcha——它是概率事件,只测 gate 机制。
### 9.8 门禁
- PR:单元 + 集成(fake)全过。
- Release tag+ 冒烟 Playbook 签字。
- 每周:trace 诊断报告 review。
---
## 10. 上线节奏与回滚
### 10.1 阶段
| 阶段 | 受众 | 签名 | 退出门槛 |
|---|---|---|---|
| **Alpha(内部)** | 开发 + QA(≤5 人)| Mac 公证 / Win 无签名 / Linux 裸 AppImage | 13+3 适配器 fake + 真实账号成功率 ≥ 90% |
| **Beta(邀请制)** | 公司内部 + 合作客户(≤30 人)| 同上 | 连续 7 天无 P0;保活命中率 > 95%;patch 通道应急修复跑通 ≥ 1 次 |
| **GA 候选** | 放开注册 | **Win 签名必须到位** | 安装成功率 > 99%crash rate < 0.1%/session |
客户端和服务端同加 `channel`alpha/beta/stable),更新按 channel 拉不同 `latest-*.yml`。"关于"里可切。
### 10.2 硬切换落地顺序(开发版本无生产用户)
因为当前还是开发版本,没有真实生产用户在跑旧 `/api/plugin/*`,不需要过渡期:
1. 同一个 PR 里:新增 `/api/desktop/*` 路由 + 新 DB 迁移 + 删除 `/api/plugin/*` 路由与相关 handler。
2. 同一个 PR 里:删除 `apps/browser-extension/` 整个目录。
3. 废弃文档 `docs/media-publisher-extension-runtime-v1.md`,新写桌面客户端运行时文档。
4. 发 Alpha 客户端;内部账号全部用 Electron 重登。
如果未来发现有内部测试账号遗留在老表里:手动导出账号元数据后 drop 老表,不写自动迁移。
### 10.3 三层回滚
| 情景 | 动作 |
|---|---|
| 某 patch 导致单平台适配器坏 | 服务端把该平台 patch 版本号回滚到上一稳定版;客户端下次任务自动用旧 patch(秒级) |
| 某 release 中等问题 | 下发"建议版本 ≤ X"`electron-updater` 帮用户降级(分钟级) |
| 某 release 严重安全问题 | 下发 `minClientVersion = Y`;低于 Y 拒绝启动,UI 引导升级 |
所有回滚**只在服务端配置**,不动客户端代码。
### 10.4 运营指标
- 每平台适配器每日成功率(target:发布 > 95%、监控 > 90%
- 每次 crash 的 top 堆栈
- SSE 长连接平均保持时长(target > 30 min
- patch 推送后"多久 80% 客户端加载新 patch"target < 30 min
---
## 11. 已知风险
1. **Win 无签名期间**(Alpha/Beta),用户首次安装会遇 SmartScreen 警告,转化率会掉。已接受。
2. **豆包/千问网页结构变**,最坏情况紧急手搓 patch,SLA 按小时算。
3. **用户跨 OS 切换**(Mac → Win 再回来)不迁移登录态(local-only cookies),需全部重登。
4. **账号被平台风控**无法自动恢复;UI 引导用户换号。
5. **开机自启 + 托盘常驻**可能被企业 EDR 误报恶意软件;需准备白名单说明材料。
6. **Electron 基础 RAM 占用**~300 MB + 每 BrowserView ~50 MB。10 账号场景约 800 MB,这是 Chromium 本质开销,无法显著优化。
---
## 12. 术语表
- **Patch 通道**:仅下发适配器的声明式数据和纯函数,分钟级生效,不重启。
- **Release 通道**:完整二进制 + 差分升级,通过 electron-updater。
- **探活(probe)**:加载主页 DOM 判断登录态,低代价。
- **保活(keep-alive / heartbeat**:主动访问平台私有 API 触发 token 刷新。
- **Client Token**:一台 Electron 安装的后台身份,与 User JWT 并列。
- **Partition**Electron `session.fromPartition('persist:<id>')`,每个账号一个隔离的 cookie/storage 容器。