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>
This commit is contained in:
@@ -0,0 +1,562 @@
|
||||
# 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/token,SQLite 持久化
|
||||
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 viewer(Vue 单页,离线)
|
||||
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 CDP(finally)
|
||||
```
|
||||
|
||||
**核心价值**:步骤 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` 挂一个用户 gate,auto 不挂。同一份 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`。
|
||||
- 为什么不用 WebSocket:SSE 单向推已足够,穿透 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}
|
||||
|
||||
# 管理 UI(Web / 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 file(10 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 | universal(Intel + 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 Actions,runner 矩阵:`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 容器。
|
||||
Reference in New Issue
Block a user