2026-04-18 20:28:42 +08:00
# Electron 桌面客户端设计(全面替换 WXT 浏览器插件)
2026-04-20 09:52:48 +08:00
**状态 ** : Draft v2.6 · 已按 2026-04-20 当前实现口径修订
2026-04-18 21:05:41 +08:00
**作者 ** : @liangxu + Claude( codex/gemini 交叉审校)
2026-04-18 20:28:42 +08:00
**日期 ** : 2026-04-18
**范围 ** :新增 `apps/desktop-client/` 、`/api/desktop/*` 服务端路由、相关 DB schema;删除 `apps/browser-extension/` 与 `/api/plugin/*` 路由及相关代码。
2026-04-20 09:52:48 +08:00
> **v2.6 当前实现口径**:平台侧二次人工审核设计已废弃。SaaS 完成人工审核后才创建 publish job; desktop client 只负责执行发布与回写结果。`manual / waiting_user / parked / lease?from_parked=true / review-window` 相关设计全部作废。桌面端新增“发布管理”视图,用于查看待发布队列、历史发送结果,并允许对成功/失败/unknown/aborted 的发布任务手动再次发送。文档中若在历史修订记录里仍出现 parked/manual,均以本段为准。
2026-04-18 21:05:41 +08:00
2026-04-18 20:28:42 +08:00
---
## 1. 背景与动机
现有 `apps/browser-extension/` ( WXT MV3 Chrome 扩展,"GEO Publisher")同时承担两类工作:
1. 向 13 家中国媒体平台做内容分发(微信公众号、头条号、百家号、搜狐号、网易号、ZOL、懂车帝、知乎、企鹅号、简书、掘金、B 站、什么值得买)。
2026-04-18 21:22:38 +08:00
2. 监控主流 LLM(豆包、千问、DeepSeek、混元、Kimi)在给定查询下的输出,判定品牌 是否被引用。
2026-04-18 20:28:42 +08:00
当前问题:
- **LLM 抓取不稳定**。监控模块依赖 DOM 选择器与 `webRequest.onBeforeRequest` 在 MV3 下受限,无法可靠拿到 SSE 响应 Body;平台 UI 一变就挂。
- **适配器受 MV3 约束**。后台 service worker 频繁休眠,长任务难以保证完成;跨平台复杂登录(QR、滑块、SMS)在扩展内体验差。
- **不可扩展到真正的系统级自动化**。系统托盘、开机自启、多账号并发 session 隔离、进程级 CDP 调试,这些 MV3 都做不到。
2026-04-18 21:05:41 +08:00
- **现有适配器实现覆盖不全**。仓库实测:发布侧 13 家里 `weixin_gzh / juejin / dongchedi / smzdm / wangyihao / bilibili / zol` 共 **7 家 ** 仍返回 `unsupportedPublishResult` ( detect-only 占位);监控侧 DeepSeek 是 `defaultAdapter` 占位没实现 query。这部分要**从零实现**,不是迁移。
2026-04-18 20:28:42 +08:00
因此:**全面替换**为一个 Electron 桌面客户端,承担上述全部职责。浏览器扩展停止维护并下线。
---
## 2. 设计硬约束(已确认)
1. **全面替换 ** ,不做"先 monitoring 后 publishing"的分阶段替换。
2. **不做向后兼容 ** 。因为是开发版本阶段,服务端协议、DB schema、认证字段按桌面客户端的最佳形态重新设计,不为插件保留旧字段或旧路由。
3. **登录走嵌入式 Web View ** 。用户在 Electron 真实平台登录页里自己完成扫码/密码/短信/滑块;我们只保存 cookie,不保存账号密码。
2026-04-18 21:22:38 +08:00
4. **Cookie 本地存储 并加密 ** ( Electron `safeStorage` )。服务端**永不存储** Cookie 或任何平台凭据。Linux 上 `safeStorage` 可能退化为 `basic_text` ,必须启动时检测并显式降级告警(见 §4.5)。
2026-04-18 20:28:42 +08:00
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。
2026-04-18 22:12:53 +08:00
8. **一个平台账号只绑定一个 workspace ** (不支持跨租户共享账号)。该约束必须落到 DB 唯一约束与 API 级冲突校验,不依赖客户端自觉(见 §6.6 `platform_accounts` 的 `platform_uid` 字段 + 唯一索引)。**前置条件**:workspace 必须先升成后端一等安全边界(见 §12 Plan 0);仅靠新增 middleware 不够,因为现有代码里 `Actor/Claims/cache/monitoring` 多处仍按 tenant 粒度(v2.2 三轮审查发现的底座问题)。
2026-04-18 23:19:49 +08:00
9. **演进缝预留 ** :短期 `一账号 = 一 workspace = 一台 client` 是产品边界,不是代码硬写。本版 DDL 继续使用 `workspace_id` / `client_id` / `target_client_id` 命名(不提前引入 `credential_holder_*` / `account_home_*` 命名以避免 v2.4 前 spec 与 DDL 不一致);未来真正做"共享运营账号池 / 桌面端主备"时,通过 rename migration + 兼容视图平滑演进。
2026-04-18 22:12:53 +08:00
10. 要求最小 electron 体积包
2026-04-18 20:28:42 +08:00
---
## 3. 架构总览
### 3.1 进程拓扑
2026-04-18 21:05:41 +08:00
单 Electron 进程树(方案 A)。所有视图宿主统一用 `WebContentsView` ( Electron 已把 `BrowserView` 标记为 deprecated)。
2026-04-18 20:28:42 +08:00
2026-04-18 21:05:41 +08:00
- **Main 进程**( Node):任务调度器、Session 注册表、加密凭据仓库、CDP debugger 管理、租约状态机、与后端 REST/SSE 通信、Tray & autoUpdater。
- **Renderer 进程**(托盘窗口 / 登录窗口 / 发布预览窗口):Vue UI,基于 `packages/ui-shared` (见 §3.3)。禁用 `nodeIntegration` ,通过 `contextBridge` 暴露窄接口。
- **Utility 进程**:为 Patch 可执行代码沙箱化而预留(见 §5.5)。`utilityProcess` 不开 Node 能力,仅以 IPC 与主进程通信。
- **工作 WebContentsView**:每个登录账号一个 `persist:<account-id>` session,挂在离屏 `BaseWindow` 上。
- **生命周期**: **两档**。Hot-tier(最近 N 分钟有任务或保活的账号)常驻 WebContentsView; Cold-tier 账号只保留 session partition 在磁盘,WebContentsView lazy-create。阈值默认 30 min,可配置。避免 10 账号场景下 RAM 被固定占用。
- 仅在实测 CDP 抓流阻塞 UI 线程时,才把 debugger 交互迁到 `utilityProcess` ,其他模块保持不动。
2026-04-18 20:28:42 +08:00
### 3.2 模块分层
新增 `apps/desktop-client/` ,作为 pnpm workspace 的新包:
```
apps/desktop-client/
main/
bootstrap.ts # app ready, single-instance-lock, tray, autoUpdater
2026-04-18 21:05:41 +08:00
session-registry.ts # account-id → (WebContentsView|null) + session
view-pool.ts # Hot/Cold 两档 WebContentsView 生命周期
vault.ts # safeStorage 包 cookie/token + SQLite(本机恢复缓存)
vault-export.ts # 用户交互式导出(口令二次加密)用于手工迁机器
scheduler.ts # 本地 cron + 服务端 SSE/pull 合流
lease-manager.ts # 任务租约:ack→attempt_id+lease_token→extend/cancel/result
rate-limiter.ts # 每平台/账号 token bucket,共享 business/keep-alive/probe 预算
2026-04-18 20:28:42 +08:00
circuit-breaker.ts # 平台级熔断
2026-04-18 21:05:41 +08:00
keep-alive.ts # 探活 / 保活 / resume warm-up 错峰
crash-recovery.ts # 启动时按 task.kind 分派恢复策略
patch-loader/
registry.ts # 已加载 patch 版本表
signer.ts # manifest + Ed25519 + hash + 回滚序号校验
parser-dsl.ts # 受限 DSL 解释器(默认通道)
vm-host.ts # utilityProcess 沙箱启动 + 冻结 global(备用通道)
2026-04-18 20:28:42 +08:00
tracing/
2026-04-18 21:05:41 +08:00
recorder.ts # 结构化日志 + 关键截屏(Alpha 范围)
trace-writer.ts # 轻量 zip( Alpha 仅 JSON 日志 + 截屏)
2026-04-18 20:28:42 +08:00
redactor.ts # Cookie / Authorization / password 字段自动脱敏
2026-04-18 21:05:41 +08:00
# Playwright 兼容导出作为 Phase 2 能力,见 §5.6
2026-04-18 20:28:42 +08:00
transport/
api-client.ts # 复用 packages/http-client,注入 client_token
2026-04-18 21:05:41 +08:00
sse-client.ts # /api/desktop/events 长连接 + 60s pull 兜底
2026-04-18 20:28:42 +08:00
preload/
2026-04-18 21:05:41 +08:00
bridge.ts # contextBridge 窄 API
renderer/ # 极简 Vite 项目,仅消费 packages/ui-shared
main.ts
routes.ts
2026-04-20 09:52:48 +08:00
views/ # 桌面端专属视图(控制台/发布管理/账号管理/任务中心/诊断)
2026-04-18 20:28:42 +08:00
```
2026-04-18 21:05:41 +08:00
### 3.3 关键复用(更务实的口径)
**可复用 ** :
2026-04-18 22:12:53 +08:00
2026-04-18 21:05:41 +08:00
- `packages/shared-types` :平台枚举、任务类型、DTO。需要**扩展**:加入租约、platform_uid、patch manifest 等新类型。
2026-04-18 22:12:53 +08:00
- `packages/http-client` : HTTP 基座直接用;**v2.3 新增** `SseClient` 类——内置指数退避重连、`Last-Event-ID` 处理、首帧 snapshot 消费、事件去重。Web 和 Desktop 两端共用。[apps/admin-web/src/lib/api.ts ](apps/admin-web/src/lib/api.ts ) 现有的简陋 `subscribeSSE` 迁移到这里并补能力。
- **新建 `packages/ui-shared` **:从 `apps/admin-web` 抽出**通用组件、i18n、样式、utils**。admin-web 与 desktop-client 两边都只消费共享包。Renderer 是一个极简 Vite 项目,不是克隆+删页面。**必须抽出**的组件:
- `MarkdownPreview.vue` + `markdown-it` 渲染器(让 Web 发布前预览和 Desktop manual-review 用同一套渲染,避免"Web 看到的"和"桌面端审核看到的"不一致)。
- `AccountCard.vue` / `TagMultiSelect.vue` / `HealthBadge.vue` (账号管理 + 发布 Modal 共用)。
- `SseSubscription.vue` composable(封装 `SseClient` 给 Vue 用)。
- i18n 文案、主题变量、toast/dialog 系统。
2026-04-18 21:05:41 +08:00
- 老适配器里的**选择器字符串**和**业务流程描述**("打开编辑器 → 填标题 → 粘正文 → 上传封面 → 点发布"这种语义)。
2026-04-18 20:28:42 +08:00
2026-04-18 21:05:41 +08:00
**不可复用、必须重写或新建 ** :
- 所有适配器代码——老代码硬编码 `chrome.tabs / chrome.storage / chrome.runtime / chrome.webRequest / wxt` 框架依赖,全部换成 Electron `session / WebContentsView / webContents.debugger / Fetch domain` 。
- **发布适配器中 7 家 detect-only 的平台**(微信公众号、掘金、懂车帝、什么值得买、网易号、B 站、ZOL)**需从零实现 publish**。
- **DeepSeek 监控**需从零实现 query。
- Trace 基建、Patch 通道、租约管理器、LeaseManager + sandbox-loader、ui-shared 抽取——全部新建。
2026-04-18 20:28:42 +08:00
---
## 4. 账号 & 会话模型
### 4.1 数据结构
``` ts
type Account = {
id : string // uuid,本地生成
platform : PlatformKind // 'wechat' | 'toutiao' | ... | 'doubao' | 'qwen' | 'deepseek'
2026-04-18 21:05:41 +08:00
platformUid? : string // 平台侧真实账号 ID / OpenID / UIN;登录探针抓取
accountFingerprint? : string // 额外防伪:昵称/手机号尾号/账号主页 URL hash
2026-04-18 20:28:42 +08:00
purpose : 'publish' | 'monitor'
2026-04-18 21:05:41 +08:00
displayName : string // 用户自填,仅作本地展示
2026-04-18 20:28:42 +08:00
partitionKey : string // 'persist:acc-<id>', Electron session 隔离
2026-04-18 21:05:41 +08:00
proxy? : ProxyConfig // 可选 session-level 代理
2026-04-18 20:28:42 +08:00
createdAt : Date
lastSeenAt : Date
2026-04-18 21:05:41 +08:00
verifiedAt? : Date // 上次成功 probe 的时间
2026-04-18 20:28:42 +08:00
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 : {
2026-04-18 21:05:41 +08:00
hourlyBudget : number // 默认 60/小时,含业务+保活+探活合计
2026-04-18 20:28:42 +08:00
usedThisHour : number
resetAt : Date
}
2026-04-18 21:48:02 +08:00
tags? : string [ ] // 用户可自定义分组标签(如"品牌号"/"矩阵号"/"测试号"),Modal 按 tag 批量勾选
syncVersion : number // 单调递增,每次写入 +1,用于 client-server 一致性 CAS
deletedAt? : Date // tombstone,软删除,resync 时用于判断 "server 已删"
2026-04-18 20:28:42 +08:00
}
2026-04-18 21:05:41 +08:00
type ProxyConfig = {
scheme : 'http' | 'socks5'
host : string
port : number
auth ? : { user : string ; pass : string } // 存入 vault,不明文落盘
}
2026-04-18 20:28:42 +08:00
```
### 4.2 登录流程
1. 用户在 UI 点「新增账号 → 选平台」。
2. 主进程 `session-registry.create(platform)` 分配新 partition,打开一个**可见** `BrowserWindow` (登录专用),加载该平台登录页。
3. 用户自己用扫码 / 密码 / 短信 / 滑块登录。
2026-04-18 21:05:41 +08:00
4. 登录探针(preload 脚本 + URL 规则 + 成功态 DOM 标记)检测到已登录态后,在窗口顶部叠加"登录成功"遮罩层 1.5 s,然后关窗。
5. 探针在关窗前抓取平台侧 `platformUid` (如微信公众号的 `fakeid` / B 站的 `mid` / 知乎的 `hash_id` ),写入 `Account.platformUid` 。
6. Electron 自动把 cookies 持久化到 `userData/Partitions/<key>/` ;同时主进程用 `safeStorage` 加密写本地 SQLite 作为**本机恢复缓存**(跨账号解绑、重装应用时 restore 用;**非迁机器备份**)。
7. 启动时强制 `safeStorage.isEncryptionAvailable()` ;若在 Linux 上 `safeStorage.getSelectedStorageBackend()` 返回 `basic_text` ,UI 显著告警、默认关闭加密快照(避免把平台 cookie 明文写盘)。
8. 账号进入"常驻池", hot-tier 分配 WebContentsView, cold-tier 仅保留 partition;按需激活 CDP。
2026-04-18 20:28:42 +08:00
### 4.3 多账号并存
2026-04-18 21:05:41 +08:00
同一平台多账号完全隔离——partition 不同、cookies 不同、UA 不同、可通过 session 级 proxy 走不同 IP。代理来自 `Account.proxy` 字段,由 `_shared/proxy-helper.ts` 统一调用 `ses.setProxy` (§5.4)。调度器按账号维度限流(默认同一账号并发 1、同一平台并发 3,可配置)。
2026-04-18 20:28:42 +08:00
### 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 |
2026-04-18 22:12:53 +08:00
| **元宝 ** (腾讯)| Yuanbao · 入口 `yuanbao.tencent.com` ( **不是** hunyuan.tencent.com,后者是开发者/模型页不是对话入口)· 登录方式:微信扫码 / QQ / 邮箱(微信涉及 `open.weixin.qq.com` iframe + 本地 loopback 检查)| 接入时按 §5.2 spike 矩阵复测;默认主动保活 | 8 min + 30 min 探活 |
| Kimi | Moonshot · 入口 `www.kimi.com` · 登录方式:微信扫码 / 手机号快捷登录。实测请求是 `POST /apiv2/kimi.gateway.chat.v1.ChatService/Chat` ( gRPC-Web binary-framed, **非 EventSource**, parser 需先 framing 解包,见 §5.2) | 接入时按 §5.2 spike 矩阵复测;默认主动保活 | 8 min + 30 min 探活 |
2026-04-18 20:28:42 +08:00
实施细节:
2026-04-18 21:05:41 +08:00
- **错峰**:同一平台 N 个账号按 `hash(accountId) mod period` 分布时间槽 + ±30s 抖动;当 N > 槽数时使用随机顺延。
- **统一 token bucket**:每个 `(platform, account)` 维度一只 leaky bucket,桶容量 = `hourlyBudget` 。**业务任务、保活、探活同桶扣减**——避免保活把业务流量配额吃掉或反之。
- **优先级**:业务任务 > 保活 > 探活;同槽冲突由优先级决定是否放行。
2026-04-18 20:28:42 +08:00
- **失败升级**:保活失败 1 次立即重试;连 2 次失败降级为探活一次确认状态;探活失败则 `health: expired` ,托盘红点 + OS 通知 → 用户点击弹登录窗(partition 复用,不需要重建账号)。
2026-04-18 21:05:41 +08:00
- **429/验证码退避**:任何响应识别为 429/风控页/captcha 时,该账号进入**指数退避**(基础 30s,因子 2,上限 30 min),期间**完全暂停保活**,只保留"按需激活"(业务任务到来时才 pre-check)。
- **静音时段**:默认 0:00–7:00(**用户系统本地时区**)不保活,可配置关闭。
- **休眠感知**: `powerMonitor.on('suspend'|'resume')` ,休眠暂停全部保活;**唤醒后错峰 warm-up**——按 `hash(accountId)` 分片,每 10s 一批探活(每批 ≤3 账号),避免同步流量尖峰。
2026-04-18 20:28:42 +08:00
- **配额池**:同一 LLM 多账号时,调度器按"最近使用 + 剩余配额"挑账号。
2026-04-18 21:05:41 +08:00
### 4.5 凭据粒度与备份
2026-04-18 20:28:42 +08:00
- **不**保存账号密码。
- 只保存 Cookie + 平台侧必要 token(如 Doubao 的 `ms_token` 、`fp` )。
2026-04-18 21:05:41 +08:00
- **`safeStorage` + SQLite** 是**本机恢复缓存**: userData 被清/重装应用可恢复,不保证可迁机器。
- **跨机器迁移**走独立的**用户交互式导出**流程(`vault-export.ts` ):本机解密后用**用户输入的口令**二次加密导出为 `.vault` 文件;新机器同样输入口令解密、用新环境的 `safeStorage` 重加密落盘。导出的 `.vault` 文件不会自动上传服务端。
- 服务端**永远不**持有任何形式的 cookie 备份(硬约束 4)。
2026-04-18 20:28:42 +08:00
---
## 5. 适配器模型
### 5.1 接口定义
``` ts
interface AdapterContext {
accountId : string
session : Session
2026-04-18 21:05:41 +08:00
view : WebContentsView // 统一使用 WebContentsView
2026-04-18 20:28:42 +08:00
debugger : DebuggerClient
2026-04-18 21:05:41 +08:00
rateLimit : RateLimitHandle // 业务调用前 acquire
2026-04-18 20:28:42 +08:00
logger : ScopedLogger
signal : AbortSignal
2026-04-18 21:05:41 +08:00
reportProgress : ( stage : string ) = > void // 供状态机向 lease-manager 报进度
2026-04-18 20:28:42 +08:00
}
interface PublishAdapter {
platform : PlatformKind
capabilities : { images : boolean ; video : boolean ; markdown : boolean ; /* ... */ }
probe ( ctx : AdapterContext ) : Promise < HealthState >
2026-04-18 21:05:41 +08:00
keepAlive ( ctx : AdapterContext ) : Promise < void > // 默认 no-op
2026-04-18 20:28:42 +08:00
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 >
}
```
2026-04-18 22:12:53 +08:00
### 5.2 监控适配器:流式 SSE 抓取技术路径(**先 spike 后定案**)
2026-04-18 21:05:41 +08:00
2026-04-18 22:12:53 +08:00
**v2.2 review 发现 `Fetch.takeResponseBodyAsStream` 与 `Fetch.continueResponse` 在 CDP 语义上互斥 ** :前者独占响应 body,后者让页面继续消费。因此**不能**写成"连续调用链"。v2.3 改为"分流 + 实测矩阵 + spike gate"。
2026-04-18 21:05:41 +08:00
2026-04-18 22:12:53 +08:00
**总方针 ** :不同平台用不同 domain,**不劫持能不劫持**(劫持会阻塞页面继续跑),只在必须看响应 body 时才用 Fetch 劫持。
2026-04-18 21:05:41 +08:00
2026-04-18 22:12:53 +08:00
**按传输类型分流 ** (每条路线的可用性需在 Plan A 先做 **spike 矩阵**确认):
| 页面使用的机制 | 推荐 CDP 路径 | 约束 |
|---|---|---|
| `new EventSource(url)` | `Network.enable` → `Network.eventSourceMessageReceived` 监听每条 message | 仅监听、不劫持。最干净。 |
| `fetch(url)` + `text/event-stream` + streaming body | `Network.enable` → `Network.streamResourceContent(requestId)` + `Network.dataReceived` 事件流式拼接 | **Chromium ≥ 120 ** ( Electron 41+ 满足)。仅监听、不阻断页面。|
| `fetch(url)` + chunked transfer + 自定义协议 | 同上 `streamResourceContent + dataReceived` | 同上 |
| **gRPC-Web / binary-framed fetch-stream ** ( Kimi `/apiv2/kimi.gateway.chat.v1.ChatService/Chat` ) | 同上 `streamResourceContent + dataReceived` ,拿到 raw bytes 后 **provider parser 先做 gRPC-Web framing 解包 ** ( 5-byte 前缀:1B compressed flag + 4B message length)再解 payload | 引入 `adapters/_shared/framer.ts` 公共工具 |
| 必须**劫持 / 替换** 响应(一般不需要,只在改写响应或注入时)| `Fetch.enable({patterns})` → `Fetch.requestPaused` → `Fetch.fulfillRequest` ( **不再调 continueResponse**);若需读 body 再处理,用 `Fetch.getResponseBody` ( **等响应结束**)后统一返回 | `takeResponseBodyAsStream` 后页面**不能继续正常消费该响应**,故仅用于"必须替换响应"场景,监控场景不选 |
**Spike 矩阵(Plan A 必须先跑) ** :
```
provider × transport × Electron 版本 × domain 可用性 × 实际效果
豆包 千问 DeepSeek 元宝/混元 Kimi
EventSource ? ? ? ? ?
streamResource ? ? ? ? ?
dataReceived ? ? ? ? ?
```
每个 cell 在开工前用 `electron/chrome://devtools` 实跑一次登录后的真实聊天请求,记录实际的 request 种类与可抓到的 body 行为。**没有 spike 结果就不能定任何 adapter 的实现细节**。
2026-04-18 21:05:41 +08:00
2026-04-18 22:12:53 +08:00
**Chromium/CDP 兼容矩阵文件 ** : `docs/superpowers/specs/electron-chromium-cdp-matrix.md` (Plan A 阶段新建),记录每次升 Electron 大版本的 domain 可用性回归结果。截至 2026-04-18 Electron stable 41.2.0( Chromium 146),上述三类 domain 全部可用。
2026-04-18 21:05:41 +08:00
实施步骤:
2026-04-18 20:28:42 +08:00
```
2026-04-18 21:05:41 +08:00
1. ensureLoggedIn(ctx) → probe / 必要时触发保活
2. openChat(ctx) → 在 WebContentsView 里导航或复用 tab
3. detect protocol → 通过 DevTools Protocol 注入一小段探测脚本,返回 EventSource / fetch-stream
4. attach CDP Fetch or Network → 按 §5.2 表格选 domain
5. submit query → 触发网页发出请求
6. collect streaming body → eventSourceMessageReceived 或 IO.read
7. parse provider-specific payload → normalized MonitorResult
8. detach CDP( finally)
2026-04-18 20:28:42 +08:00
```
2026-04-18 21:05:41 +08:00
**Provider-specific parser ** :每家 LLM 的 payload schema 不同(豆包 `{event: 'data', data: {text: ...}}` 这种结构),放 `adapters/doubao.ts` 等私有文件。Schema 变化通过 Patch 通道下发新 `parser` (§5.5)。
2026-04-18 20:28:42 +08:00
2026-04-18 21:05:41 +08:00
### 5.3 发布适配器状态机 + 租约绑定
2026-04-18 20:28:42 +08:00
每个发布任务或每个账号带 `PublishMode` :
``` ts
2026-04-18 21:05:41 +08:00
type PublishState =
| 'filling' // adapter 正在往编辑器填内容
| 'submitting' // 已经点了发布按钮,等平台回应
| 'done' // 平台明确返回成功
| 'failed' // 明确失败(可重试)
| 'aborted' // 用户或系统取消
| 'unknown' // 崩溃/超时/无法判断(人工对账,见 §7.5)
2026-04-18 20:28:42 +08:00
```
2026-04-18 21:05:41 +08:00
**状态机与租约的绑定 ** (见 §6 协议):
2026-04-18 22:12:53 +08:00
2026-04-20 09:52:48 +08:00
- SaaS 中的文章审核在创建 publish job 之前完成;desktop client **不再承接二次人工审核 ** 。
2026-04-18 22:12:53 +08:00
- 进入 `filling / submitting` 时,适配器必须每 60 s 调 `lease-manager.extend(lease_token)` 续租(lease TTL 默认 10 min)。
2026-04-18 21:05:41 +08:00
- 用户取消或 signal abort → `aborted` 。
- adapter 抛异常 → `failed` ,带错误码。
2026-04-18 22:12:53 +08:00
- 进程崩溃 → 启动时按 §7.5 分派为 `unknown` (仅限 publish 任务的 `submitting` 阶段)。
2026-04-20 09:52:48 +08:00
- 需要再次发送的文章,不走 parked/resume,而是在桌面端“发布管理”里基于原 publish task 重新创建一次新的 publish job。
2026-04-18 23:19:49 +08:00
2026-04-18 20:28:42 +08:00
文件上传:`protocol.handle('file')` + CDP `Input.dispatchDragEvent` 或直接喂 `<input type=file>` ,不经服务端中转。
### 5.4 公共能力
`adapters/_shared/` :
2026-04-18 21:05:41 +08:00
- `debugger-helper.ts` :封装 CDP attach/detach、Fetch/Network domain 选择、SSE 流式拼接、超时。
- `dom-helper.ts` :语义化选择器、重试、容错。
2026-04-18 20:28:42 +08:00
- `upload-helper.ts` :文件上传统一入口。
2026-04-18 21:05:41 +08:00
- `captcha-pause.ts` :遇滑块/点击验证时把 WebContentsView 转前台让用户过,通过后续跑。
- **`proxy-helper.ts` **(新):统一走 `Session.setProxy(Account.proxy)` ,支持 socks5/http + 凭据存 vault。
2026-04-18 20:28:42 +08:00
2026-04-18 21:05:41 +08:00
### 5.5 双轨更新:局部热更 + 全量(安全收紧)
2026-04-18 20:28:42 +08:00
| 通道 | 内容 | 节奏 | 形式 |
|---|---|---|---|
2026-04-18 21:05:41 +08:00
| **Patch 通道(默认·纯数据 DSL) ** | 适配器的 `selectors.json` + `parser.dsl` (受限的**声明式解析 DSL**,非 JS)| 分钟级按需 | 服务端签名 manifest + 补丁包;客户端 DSL 解释器运行,**无任意 JS 执行** |
| **Patch 通道(备用·隔离执行) ** | 个别场景确需写 JS parser 时的"逃生舱" | 仅在评审后发布 | 加载到 `utilityProcess` (无 Node 能力)+ `isolated-vm` 或 `vm.createContext` + 冻结 global + 禁用 `eval/Function/WebAssembly/dynamic import` + 仅允许消息传递返回结构化结果 |
2026-04-18 20:28:42 +08:00
| **Release 通道 ** | 二进制、主进程、UI、适配器宿主框架 | 周/双周 | electron-updater 差分包 |
2026-04-18 21:05:41 +08:00
**默认走纯数据 DSL 通道 ** ——安全、可审、无 RCE 面。仅当平台响应结构复杂到 DSL 难以表达时,才考虑升级到备用通道,且每个 parser 必须经过安全评审。
2026-04-18 20:28:42 +08:00
2026-04-18 21:05:41 +08:00
**签名链(TUF 思路简化版) ** :
2026-04-18 20:28:42 +08:00
2026-04-18 21:05:41 +08:00
每个 patch 打包为 `manifest.json + patch.data` , manifest 至少包含:
2026-04-18 20:28:42 +08:00
2026-04-18 21:05:41 +08:00
``` json
{
"platform" : "doubao" ,
"patch_seq" : 47 , // 单调递增,防回滚
"hash" : "sha256:<of patch.data>" ,
"expires_at" : "2026-05-18T00:00:00Z" ,
"min_client_version" : "1.2.0" ,
"max_client_version" : null , // 可选
"key_id" : "root-2026-01" , // 支持根密钥轮换
"revokes" : [ ] , // 撤销之前的 patch_seq 列表
"sig" : "<Ed25519 over manifest+hash>"
}
```
2026-04-18 20:28:42 +08:00
2026-04-18 21:05:41 +08:00
客户端约束:
- 内置 root key(可通过 Release 通道更新),支持 `key_id` 轮换。
- 下载时校验签名;**每次加载前再次校验 hash + 过期时间 + 未被 `revokes` 列表覆盖**。
- `patch_seq` 必须严格 > 已加载值,否则拒绝(防降级重放)。
- 服务端下发一个 `revoke <platform> <patch_seq>` 指令 → 客户端停用对应 patch、降级为前一版本,直到收到下一批 manifest。
- UI 每个适配器显示:`client 1.2.0 · doubao patch #47 (via DSL)` 。
2026-04-18 21:48:02 +08:00
### 5.6 Trace 基础设施(Alpha 范围 + Phase 2 兼容 Playwright)(不着急)
2026-04-18 20:28:42 +08:00
2026-04-18 21:05:41 +08:00
**Alpha 范围(MVP) ** :
2026-04-18 20:28:42 +08:00
2026-04-18 21:05:41 +08:00
- 结构化日志(pino JSON)滚动写入 `userData/logs/` 。
- 任务失败时自动截屏 3–5 张 + 存 Network 响应摘要。
2026-04-18 22:12:53 +08:00
- 本地内置极简 viewer(Vue 单页,离线),能读 `.trace.json` 文件。**加载与隔离要求**:
- Viewer Vite 构建用 **hash 路由 ** ( `createWebHashHistory` ),避免 `file://` 协议下 history 路由 404。
- Viewer 跑在 **独立的 sandboxed `<iframe>` ** ( CSP `sandbox="allow-scripts"` ,不开 `allow-same-origin` ),挂在主 renderer 的"开发者"菜单下;trace 数据通过 `postMessage` 从主进程 → 主 renderer → iframe 单向传递。
- 防御目标:trace 里如果夹带了恶意 HTML/script(比如平台响应里注入的脚本),也无法从 iframe 逃到主 renderer 或主进程。
2026-04-18 21:05:41 +08:00
- 脱敏:Cookie / Set-Cookie / Authorization / x-xsrf-token / password / 手机号 / email → `<redacted>` 。
- 存储:`userData/traces/<accountId>/<taskId>.trace.zip` ,配额默认 **50 份 ** 或 **500 MB ** (先到先清)。
- **默认不上传服务端**;用户手动"发送诊断包"才上传。
2026-04-18 20:28:42 +08:00
2026-04-18 21:05:41 +08:00
**Phase 2( Beta 之后) ** :
2026-04-18 20:28:42 +08:00
2026-04-18 21:05:41 +08:00
- Playwright Trace Viewer 格式兼容导出(为方便外部开发者用 `npx playwright show-trace` 打开)。因格式私有、跟随 Playwright 版本演进,仅作为**导出**选项。
- CDP 事件流完整记录 + 2fps 截屏 + DOM 快照(内存 rolling buffer 30s,失败时 flush)。
2026-04-18 20:28:42 +08:00
---
## 6. 服务端协议
### 6.1 命名(全部重命名)
老 `plugin_installations` / `/api/plugin/*` / `installation_token` → 新 `desktop_clients` / `/api/desktop/*` / `client_token` 。因为是开发版本、无生产流量,DB 迁移直接 `drop` 旧表、`create` 新表,不做导入或双写。
2026-04-18 21:05:41 +08:00
**sqlc 协同 ** :重命名会让 `server/internal/tenant/repository/generated/` 里所有相关 struct 和 query 方法名重生成——Service 层也要改。实施计划必须**显式列出**:
- `server/internal/tenant/repository/queries/*.sql` 里要改的查询文件清单
- 所有持有 `PluginInstallation` / `InstallationToken` 类型的 `server/internal/tenant/app/*.go` service 文件
- `server/internal/tenant/transport/*.go` handler
- 对应 repo 层单元测试
2026-04-18 22:12:53 +08:00
#### 6.1.1 监控域存量改造清单(不是 rename 能完成的)
v2.2 review 发现监控子系统存量耦合比插件本身深。Plan A/B 前必须 spike 这张表:
| 层次 | 文件 / 表 | 现状 | 改造动作 |
|---|---|---|---|
| 迁移 | `server/migrations/20260410103000_create_monitoring_tables.up.sql` | `tenant_monitoring_quotas` 有 `collection_mode='plugin'` + `primary_installation_id` | 改 `collection_mode='desktop'` + `primary_client_id` (引用 `desktop_clients` );加 `workspace_id` 列 |
| Repo | `server/internal/tenant/repository/queries/monitoring*.sql` | 多处 JOIN `plugin_installations` | 全量改 JOIN `desktop_clients` + 加 `workspace_id` 条件 |
| Service | `server/internal/tenant/app/monitoring_service.go` / `monitoring_callback_service.go` | 直接查 `plugin_installations` 、按 tenant 选 primary installation | 改查 `desktop_clients` ; `primary` 概念在 workspace 粒度(Plan 0 完成后)|
| Transport | `server/internal/tenant/transport/monitoring_handler.go` | `/api/plugin/monitoring/*` callback | 对应 `/api/desktop/monitoring/*` (或合并进 `/api/desktop/tasks/*` result 回写,具体在 Plan A spike 决定)|
| Projection | `monitoring_query_results` / `monitoring_callback_logs` | 按 installation_id 索引 | 按 `(workspace_id, client_id)` 重索引;旧数据因开发版本无需保留,直接 drop |
| Dashboard | admin-web 监控结果页 | 按 tenant 汇总 | 改 workspace 汇总 |
| Seed | `server/cmd/dev-seed/main.go` 等 | 生成 plugin_installation 示例 | 生成 desktop_client 示例 |
2026-04-20 09:52:48 +08:00
| 定时清理 | (目前没有 tombstone / unknown / trace 清理)| — | 新建:unknown 任务对账提醒与归档策略、`desktop_task_traces` 超期清理、tombstone > 30 天的 account 清理 |
2026-04-18 22:12:53 +08:00
**行动 ** :Plan A 启动前用 0.5 周做一次 impact spike,核对以上每一行都有具体 owner、估时、依赖;**没有确认就不开工**。监控域存量改造是 Plan A 工期的隐藏成本。
2026-04-18 20:28:42 +08:00
### 6.2 认证
| Token | 代表 | 发放 | 用途 |
|---|---|---|---|
| **User JWT ** | 人类用户 | `/api/auth/login` | 用户在 Electron 内的交互(新增账号、创建监控计划、看报表) |
2026-04-18 21:05:41 +08:00
| **Client Token ** | 一台 Electron 安装(与 client_version 解耦)| 登录后 `/api/desktop/clients/register` 换取 | 后台任务回调、任务拉取、心跳 |
**Client Token 绑定字段 ** : `client_id + tenant_id + workspace_id` (稳定维度)。**`client_version / os / cpu_arch` 仅作为 heartbeat metadata 上报,不入 token 绑定**——autoUpdater 升版后 token 仍有效。
Token TTL = 30 天,支持滚动刷新:`POST /api/desktop/clients/rotate` 用旧 token 换新 token(剩余有效期 < 7 天时自动调用)。
撤销:`POST /api/desktop/clients/revoke` 由 Web 端用户主动撤销(如丢失设备)或管理员强制;被撤销的 client 任何接口都返 401 + 引导用户重新登录换新 client。
### 6.3 任务分发:SSE + 60s pull 兜底 · 多实例协调
**SSE 仅作为通知信道,不直接承载任务归属。 **
多实例部署下:
2026-04-18 20:28:42 +08:00
2026-04-18 21:05:41 +08:00
```
Web UI / Scheduler creates task
↓
DB insert desktop_tasks(queued)
↓
publish task_available event
↓
RabbitMQ fanout → every server instance
↓
each instance looks up its local SSE hub:
"do I hold any client that can handle this task's platform+workspace?"
↓
2026-04-18 21:48:02 +08:00
if target_client_id is online: push {type: 'task_available', task_id} via SSE to *that* client only
2026-04-18 21:05:41 +08:00
↓
2026-04-18 21:48:02 +08:00
client POST /api/desktop/tasks/lease → server atomically verifies (target_client_id == requester) + claims task
2026-04-18 21:05:41 +08:00
↓
server returns {task, attempt_id, lease_token, lease_expires_at}
```
2026-04-18 20:28:42 +08:00
2026-04-18 21:05:41 +08:00
要点:
2026-04-18 20:28:42 +08:00
2026-04-18 21:48:02 +08:00
- **任务是定向的**。每条 `desktop_tasks` 都有 `target_client_id + target_account_id` (§6.6);SSE 只推给 `target_client_id` ,其他 client 看不到、lease 不到。
2026-04-18 21:05:41 +08:00
- **SSE 只推 `task_available` 信号**,携带 `task_id` 而非任务正文,避免跨实例推送导致 payload 在错误客户端落地。
2026-04-18 21:48:02 +08:00
- **任务归属由 `POST /tasks/lease` 原子决定**( DB `UPDATE ... WHERE status='queued' AND target_client_id = :requester_client_id RETURNING ...` ,或 Postgres advisory lock)。即便被错误广播,非目标 client 的 lease 会被 `target_client_id` 检查拒绝。
2026-04-18 21:05:41 +08:00
- **60 s pull** 作为 SSE 失联兜底;和 SSE 都打同一个 lease endpoint,天然幂等。
2026-04-18 21:48:02 +08:00
- **presence registry**:每个 server 实例维护本地 `client_id → SSE connection` ,并把 `(client_id, instance_id)` 写到 Redis( TTL 30 s, SSE 心跳续期)。Scheduler 按 `target_client_id` 查 presence 决定是否广播或挂队。
- **offline→online 主动补领**: server 检测到某 `client_id` 从 offline 转 online( Redis TTL 从无到有),扫描 `desktop_tasks WHERE target_client_id = $1 AND status = 'queued'` ,对命中的每条重推 `task_available` ; client 侧也会在 heartbeat 后进入 `lease(kind?, limit=20)` 循环到空,两头夹逼避免任务"永远待领"。
2026-04-18 20:28:42 +08:00
### 6.4 路由表
```
# 配对
POST /api/auth/login
2026-04-18 21:05:41 +08:00
POST /api/desktop/clients/register # 换 client_token
POST /api/desktop/clients/rotate # 滚动刷新 client_token
POST /api/desktop/clients/revoke # 主动撤销
POST /api/desktop/clients/heartbeat # 心跳 + 上报 client_version/os/arch
2026-04-18 20:28:42 +08:00
POST /api/desktop/clients/metrics # 轻量非敏感聚合指标
2026-04-18 21:05:41 +08:00
# 事件 & 任务租约
2026-04-18 20:28:42 +08:00
GET /api/desktop/events # SSE, client_token
2026-04-20 09:52:48 +08:00
GET /api/desktop/publish-tasks # 当前 client 的发布队列 + 历史发送结果
POST /api/desktop/publish-tasks/{id}/retry # 基于历史 publish task 重新创建一次 publish job
2026-04-18 21:05:41 +08:00
POST /api/desktop/tasks/lease # {task_id?} 原子领取;kind=publish|monitor 过滤
2026-04-18 22:12:53 +08:00
POST /api/desktop/tasks/{id}/extend # body: {lease_token},续租(短时占用用)
POST /api/desktop/tasks/{id}/cancel # body: {lease_token}(或无 lease 时带 client_token 由 target_client 主动取消)
2026-04-18 21:05:41 +08:00
POST /api/desktop/tasks/{id}/result # body: {lease_token, status, payload}
2026-04-18 20:28:42 +08:00
POST /api/desktop/tasks/{id}/trace # 用户主动上传诊断包
2026-04-18 23:19:49 +08:00
# Web 端对账(UNKNOWN 任务 / 账号删除意图,见 §7.5 和 §6.8.6)
POST /api/tenant/tasks/{id}/reconcile # body: {status: 'succeeded'|'failed'|'aborted'|'retry', external_url?, reason?} · 仅 Web 用户可调、需 WorkspaceScope
2026-04-18 23:32:03 +08:00
# retry 语义:原 task status → queued + attempts + 1(不复制、不新建 job);与 §15.1 状态图一致
2026-04-20 09:52:48 +08:00
POST /api/tenant/tasks/{id}/cancel # body: {reason?} · 对 queued 任务主动取消 → status=aborted; WorkspaceScope + 审计
2026-04-18 23:19:49 +08:00
POST /api/tenant/accounts/{id}/request-delete # body: {} 或 ?undo=true · 写 delete_requested_at
2026-04-18 20:28:42 +08:00
# 账号元数据(不传 cookie)
2026-04-18 23:32:03 +08:00
GET /api/desktop/accounts?client=self # resync 用:返回 {id, platform, platform_uid, display_name, health, client_id, sync_version, deleted_at, delete_requested_at, tags}
2026-04-18 21:48:02 +08:00
POST /api/desktop/accounts # upsert:以 (workspace_id, platform, platform_uid) 为键;body 带 if_sync_version 做 CAS
PATCH /api/desktop/accounts/{id} # 改 displayName / health / quota / tags; body 带 if_sync_version 做 CAS
DELETE /api/desktop/accounts/{id}?if_sync_version=<n> # 软删除写 tombstone,非 hard delete
2026-04-18 20:28:42 +08:00
2026-04-18 21:05:41 +08:00
# Patch 通道
GET /api/desktop/patches/manifest # 返回当前适用的 manifest 列表 + revoke 指令
2026-04-18 20:28:42 +08:00
# 管理 UI( Web / Electron UI 共用)
GET /api/tenant/monitoring-plans
POST /api/tenant/publish-jobs
# ...
```
2026-04-20 09:52:48 +08:00
**幂等契约 ** :所有持有活动 lease 的写操作(`extend` / `cancel` / `result` / `trace` ) **必须携带 `lease_token` **;服务端校验 `lease_token` 与当前 `active_attempt_id` 绑定,否则返 409 Conflict。`POST /tasks/lease` 本身是 lease 发放者,不需要 `lease_token` 。客户端收到 409 = 租约已失效 / 已被抢占 / 任务已取消,**停止动作 + 触发 resync + 提示人工对账**。
2026-04-18 21:05:41 +08:00
2026-04-18 22:12:53 +08:00
**中间件与 workspace 一等化要求 ** ( **先决条件**,不是实施细节):
v2.2 review 发现现有 `server/internal/shared/auth/claims.go` 只有 `user_id/tenant_id/role` , `server/internal/shared/middleware/tenant_scope.go` 只注入 `tenant_id` , `server/internal/tenant/app/cache_support.go` workspace cache key 按 tenantID 生成,仓库**没有 workspaces 表迁移**,监控表 `tenant_monitoring_quotas` 按 tenant 存 `primary_installation_id` 。**单加一个 `WorkspaceScope` middleware 解决不了底座问题**。
正确顺序:**Plan 0(§12)必须先完成 workspace 底座一等化**——
1. `workspaces` 表 + `workspace_memberships` 表 DB 迁移。
2. `Actor/Claims` 结构扩 `workspace_id` ; `client_token` 也绑定 `workspace_id` (稳定维度)。
3. `TenantScope` 之上新加 `WorkspaceScope` middleware,派生 topic/过滤条件从**服务端 Actor 派生**,不接受 query 参数自由选择。
4. 所有 repo 层 query 注入 `workspace_id = :actor_workspace_id` 。
5. `cache_support.go` 的所有 workspace-sensitive key 加 `workspace_id` 。
6. 监控域 `tenant_monitoring_quotas` / `monitoring_service.go` / `monitoring_callback_service.go` 迁到 workspace 粒度(见 §6.1 子节"监控域存量改造清单")。
只有 Plan 0 完成后,`/api/desktop/*` 和 `/api/tenant/*` 的 workspace 隔离承诺才能真正落地。
2026-04-18 21:48:02 +08:00
2026-04-18 20:28:42 +08:00
### 6.5 服务端边界
服务端只存**任务意图**("账号 A 去豆包问 Q")与**结果**("答案文本 + 品牌是否出现")。**永不**存 Cookie、**永不**直连平台帮客户端跑。客户端失联 = 任务排队;长时间拿不到 = 超时失败。硬边界,防止 Cookie 从客户端外泄。
### 6.6 DB Schema(关键表)
2026-04-18 21:05:41 +08:00
``` sql
-- 客户端身份
CREATE TABLE desktop_clients (
id UUID PRIMARY KEY ,
tenant_id UUID NOT NULL ,
workspace_id UUID NOT NULL ,
user_id UUID NOT NULL ,
token_hash BYTEA NOT NULL ,
device_name TEXT ,
os TEXT ,
cpu_arch TEXT ,
client_version TEXT , -- 仅作 metadata,不入 token 绑定
channel TEXT , -- alpha/beta/stable
created_at TIMESTAMPTZ NOT NULL ,
last_seen_at TIMESTAMPTZ ,
last_rotated_at TIMESTAMPTZ ,
revoked_at TIMESTAMPTZ
) ;
2026-04-18 21:48:02 +08:00
-- 平台账号(workspace 独占约束 + 一致性协议)
2026-04-18 21:05:41 +08:00
CREATE TABLE platform_accounts (
id UUID PRIMARY KEY ,
tenant_id UUID NOT NULL ,
workspace_id UUID NOT NULL ,
client_id UUID NOT NULL REFERENCES desktop_clients ( id ) ,
platform TEXT NOT NULL ,
platform_uid TEXT NOT NULL , -- 平台侧真实 ID(必填)
account_fingerprint TEXT ,
display_name TEXT NOT NULL ,
health TEXT NOT NULL ,
verified_at TIMESTAMPTZ ,
2026-04-18 21:48:02 +08:00
tags JSONB , -- 用户自定义标签数组,Modal 分组筛选用
-- 一致性协议(见 §6.8.6)
sync_version BIGINT NOT NULL DEFAULT 1 , -- 单调递增,每次写入 +1
deleted_at TIMESTAMPTZ , -- tombstone,软删除;非 NULL 时 Modal 和 /accounts 默认过滤掉
2026-04-18 23:19:49 +08:00
delete_requested_at TIMESTAMPTZ , -- Web 端写"请求删除"意图;由 client resync 时坐实 DELETE(见 §6.8.6)
2026-04-18 21:05:41 +08:00
created_at TIMESTAMPTZ NOT NULL ,
last_seen_at TIMESTAMPTZ ,
2026-04-18 21:48:02 +08:00
updated_at TIMESTAMPTZ NOT NULL ,
-- 硬约束 8 的 DB 落地;同一物理账号只能被一个 workspace 活跃持有
UNIQUE ( platform , platform_uid ) WHERE deleted_at IS NULL
2026-04-18 21:05:41 +08:00
) ;
2026-04-18 21:48:02 +08:00
CREATE INDEX idx_platform_accounts_workspace ON platform_accounts ( workspace_id ) WHERE deleted_at IS NULL ;
CREATE INDEX idx_platform_accounts_client ON platform_accounts ( client_id ) WHERE deleted_at IS NULL ;
2026-04-18 21:05:41 +08:00
2026-04-18 21:48:02 +08:00
-- 任务 + 租约 + 定向路由
2026-04-18 21:05:41 +08:00
CREATE TABLE desktop_tasks (
id UUID PRIMARY KEY ,
2026-04-18 21:48:02 +08:00
-- 定向路由(C1:一等列)
job_id UUID NOT NULL , -- 同一 publish-job 展开的 N 条子任务共享;monitor 任务可 = id
target_account_id UUID NOT NULL REFERENCES platform_accounts ( id ) ,
target_client_id UUID NOT NULL REFERENCES desktop_clients ( id ) ,
platform TEXT NOT NULL , -- 冗余列,便于按平台索引/路由
-- 业务
kind TEXT NOT NULL , -- 'publish' | 'monitor'
2026-04-18 21:05:41 +08:00
payload JSONB NOT NULL ,
2026-04-20 09:52:48 +08:00
status TEXT NOT NULL , -- queued | in_progress | succeeded | failed | unknown | aborted
2026-04-18 21:05:41 +08:00
dedup_key TEXT ,
-- 租约
2026-04-18 21:48:02 +08:00
active_attempt_id UUID , -- null 表示无租约持有者
2026-04-18 21:05:41 +08:00
lease_token_hash BYTEA ,
lease_expires_at TIMESTAMPTZ ,
attempts INT NOT NULL DEFAULT 0 ,
-- 结果
result JSONB ,
error JSONB ,
created_at TIMESTAMPTZ NOT NULL ,
updated_at TIMESTAMPTZ NOT NULL
) ;
2026-04-18 21:48:02 +08:00
CREATE INDEX idx_tasks_job ON desktop_tasks ( job_id ) ;
2026-04-20 09:52:48 +08:00
CREATE INDEX idx_tasks_target_client_status ON desktop_tasks ( target_client_id , status ) WHERE status IN ( ' queued ' , ' in_progress ' ) ;
2026-04-18 21:48:02 +08:00
CREATE INDEX idx_tasks_platform_status ON desktop_tasks ( platform , status ) ;
-- 发布 Job 聚合
CREATE TABLE desktop_publish_jobs (
id UUID PRIMARY KEY ,
tenant_id UUID NOT NULL ,
workspace_id UUID NOT NULL ,
created_by_user_id UUID NOT NULL ,
title TEXT NOT NULL ,
content_ref JSONB NOT NULL , -- 多态引用(§6.8.2):{type, id}
scheduled_at TIMESTAMPTZ ,
created_at TIMESTAMPTZ NOT NULL ,
updated_at TIMESTAMPTZ NOT NULL
) ;
2026-04-18 21:05:41 +08:00
-- 历次 attempt 的审计(调试用,可选保留 30 天)
CREATE TABLE desktop_task_attempts (
id UUID PRIMARY KEY ,
task_id UUID NOT NULL REFERENCES desktop_tasks ( id ) ,
client_id UUID NOT NULL REFERENCES desktop_clients ( id ) ,
started_at TIMESTAMPTZ NOT NULL ,
ended_at TIMESTAMPTZ ,
final_status TEXT ,
error JSONB
) ;
CREATE TABLE desktop_task_traces (
task_id UUID NOT NULL REFERENCES desktop_tasks ( id ) ,
attempt_id UUID ,
size BIGINT ,
uploaded_at TIMESTAMPTZ ,
object_key TEXT
) ;
```
2026-04-18 20:28:42 +08:00
2026-04-18 21:05:41 +08:00
### 6.7 任务生命周期(含租约细节)
2026-04-18 20:28:42 +08:00
```
Web UI 创建 MonitoringPlan("brand=X on doubao,qwen daily 9am")
↓
2026-04-18 21:05:41 +08:00
Scheduler (server cron) 展开成 N 条 desktop_tasks (status=queued)
↓
SSE push 'task_available' → 任意实例 → 对应 SSE 连接 → client
↓
Client POST /tasks/lease → DB atomic claim →
返回 {task, attempt_id, lease_token, lease_expires_at=+10min}
2026-04-18 20:28:42 +08:00
↓
2026-04-20 09:52:48 +08:00
Client 执行(publish: filling→submitting; monitor: querying),每 60s POST /extend
2026-04-18 20:28:42 +08:00
↓
2026-04-18 21:05:41 +08:00
POST /result(lease_token, status=succeeded|failed) → server 校验 lease_token 后存结果
↓
2026-04-20 09:52:48 +08:00
需要再次发送的文章 → desktop client “发布管理”调用 `POST /api/desktop/publish-tasks/{id}/retry`
→ server 基于原 publish task 重新创建新的 publish job + queued task
↓
2026-04-18 21:05:41 +08:00
Web UI 可视化
2026-04-18 20:28:42 +08:00
```
2026-04-20 09:52:48 +08:00
**超时处理 ** : `lease_expires_at` 到期后按 §7.5 的任务恢复策略处理;若老 client 恢复后再 POST `/result(lease_token=过期值)` ,返 409 → client 本地记为 `unknown` 提示人工。发布任务的重新发送统一走桌面端“发布管理”,避免因重复点击造成重复发文。
2026-04-18 21:05:41 +08:00
2026-04-18 21:22:38 +08:00
### 6.8 admin-web 协同(发布 Modal + 媒体数据面板 + 离线降级)
桌面客户端是执行者与凭据持有者,但用户日常操作的入口仍然是 `apps/admin-web` ( SaaS Web 端)。`/api/desktop/*` 面向桌面端(§6.4),`/api/tenant/*` 面向 Web 端;server 内部共用 §6.3 的 RabbitMQ + DB。
#### 6.8.1 角色分工
| 角色 | 职责 | 数据主权 |
|---|---|---|
| **admin-web( Web SaaS) ** | 内容编辑、创建任务、展示结果、总览媒体数据 | Task 定义与结果的 SoT |
| **desktop-client ** | 凭据持有、页面自动化、执行任务、上报结果 | Account existence + cookie 的 SoT |
#### 6.8.2 发布 Modal 交互流(**账号级**选号,非平台级)
**核心前提 ** :一个 workspace 下同一平台通常有**多个账号**(例如 5 个微信公众号 + 3 个头条号 + 2 个知乎号)。Modal 的勾选粒度是**账号**,用户可以挑"平台 A 的账号 1/2/3 + 平台 B 的账号 5"这种跨平台跨账号组合;一个 publish job 展开为 **N 条 ** `desktop_tasks` ,每条一个 `(platform, account)` 组合。
流程:
2026-04-18 23:32:03 +08:00
1. 打开:`GET /api/tenant/accounts?kind=publish` 拉回 workspace 下所有发布账号(**workspace scope 由 WorkspaceScope middleware 从 Actor 派生,不接受 query/body 选择**——见 §6.4 中间件要求)。响应按 `(platform, account)` 两层结构组织,每条带:
2026-04-18 21:22:38 +08:00
- `online` (由 server 根据该账号所属 `client_id` 的 `last_seen_at < 5 min` 判定)
- `health` ( `live / expired / captcha / risk` )
- `lastPublishAt` (上次成功发布时间)
2. Modal UI:
- 平台折叠块,标题显示"选中 X / 共 Y"
2026-04-18 21:48:02 +08:00
- 每账号一行:`checkbox · displayName · tags chips · health badge · online badge · 最近发布时间`
- **按 tag 批量勾选**:顶部 tag 多选器(`tags` 来自 `platform_accounts.tags` ,用户在"账号管理"里自定义,如"品牌号"/"矩阵号"/"测试号"),勾选 tag 后批量勾选所有带该 tag 的账号
- 搜索框(按 displayName / platform_uid 匹配)
- 全选 / 反选(按平台、按 tag、全局)
2026-04-20 09:52:48 +08:00
- 时间(立刻 / 定时)、素材策略(封面默认 / 指定)
3. 提交:`POST /api/tenant/publish-jobs` , body 示例(**`content_ref` 多态**以适配现有 admin-web 的 KOL 模板/品牌素材/图文/视频等多种素材来源)。SaaS 侧的人审在此之前已完成,因此 publish job 只描述执行信息,不再传 `manual/auto mode` :
2026-04-18 21:22:38 +08:00
```json
{
"title": "2026 Q2 新品发布",
2026-04-18 21:48:02 +08:00
"content_ref": {
"type": "article_draft",
"id": "draft_1234"
},
2026-04-18 21:22:38 +08:00
"accounts": [
2026-04-20 09:52:48 +08:00
{"account_id": "acc-a1"},
{"account_id": "acc-a2"},
{"account_id": "acc-b5"}
2026-04-18 21:22:38 +08:00
],
2026-04-18 21:48:02 +08:00
"scheduled_at": null
2026-04-18 21:22:38 +08:00
}
` ``
2026-04-18 21:48:02 +08:00
` content_ref.type` 支持 ` article_draft` / ` template_gen`( KOL 模板生成)/ ` video` / ` brand_asset` 等,后续按需扩枚举;server 根据 type 在解析阶段拉对应实体并附到每条 desktop_task 的 payload 里。
2026-04-18 23:32:03 +08:00
**Phase 1 workspace 边界规则**(窄 workspace 化策略下的硬约束):上述 tenant-native 实体(` article_draft` / ` template_gen` / ` video` / ` brand_asset`) **只允许 actor 的 primary workspace 使用**。server 在 ` POST /api/tenant/publish-jobs` 的校验阶段查 content_ref 关联实体的创建者 ` primary_workspace_id`, **必须等于** ` actor.workspace_id`(由 WorkspaceScope 派生),否则返 **422 Unprocessable Entity** + ` {code: 'content_ref_not_in_primary_workspace'}`。这样既保证 Phase 1 用户体验(仅单 workspace 用户无感知)、又在 Phase 2 接入多 workspace 时有明确的升级路径(届时规则放宽为"同 tenant 即可"或加独立授权模型)。该规则必须**落到 ` POST /publish-jobs` 校验测试**( Plan 0 退出 bar 的 cross-workspace 防穿透用例之一)。
2026-04-18 21:48:02 +08:00
server 展开成 3 条 ` desktop_tasks(kind=publish)`,共享同一 ` job_id`;每条绑定一个 ` target_account_id` 与其所属 ` target_client_id`(一等列见 §6.6)。
2026-04-20 09:52:48 +08:00
4. Modal 切到"进度面板",订阅面向 Web 的 SSE ` GET /api/tenant/events?topic=job:<job_id>`;接收每子任务状态变化(` queued → in_progress → succeeded/failed/unknown/aborted`)。
5. 若目标账号所属桌面端离线,子任务保持 ` queued`;对应 desktop client 上线后自动领取。运营也可以在 desktop client 的“发布管理”里查看当前待发布队列与历史发送结果。
6. 全部终态后 Modal 汇总:每账号结果 URL / 错误码。若某条需要补发,统一在 desktop client “发布管理”中对原 publish task 点“再次发送”;服务端会保留原记录,并基于原 task 新建下一次 publish job。
2026-04-18 21:22:38 +08:00
#### 6.8.3 媒体数据面板(账号级粒度)
admin-web 的"账号总览 / 发布历史 / 监控结果"三个页面全部按 ` (platform, account)` 两层粒度:
| 页面 | API | 行粒度 |
|---|---|---|
| **账号总览** | ` GET /api/tenant/accounts` | ` (platform, platform_uid)` |
| **发布历史** | ` GET /api/tenant/publish-jobs?...` | job 分组,展开后每行 ` (job, account, status)` |
| **监控结果** | ` GET /api/tenant/monitoring-results?plan_id=&range=` | 每行 ` (plan, query, llm_account, answer, hit)` |
2026-04-18 21:48:02 +08:00
统计卡片(按信息密度排序,突出"需要马上关注"的内容):
1. **🔴 待关注账号**(醒目卡):点击筛选出 ` health != 'live' OR online = false` 的账号,支持一键进入账号管理页处理。
2. **按平台汇总**:每个平台一个迷你卡(账号数 / 健康数 / 近 7d 发布成功率),点击钻取到该平台的账号列表。
3. workspace 在线桌面端数。
4. 近 7 天发布成功率(按账号,低于阈值的账号红色高亮)。
5. 近 7 天监控完成率。
6. 过去 24h 异常账号数 / 异常平台数。
**监控计划创建页**的 LLM 多选 UI 改 Grid 布局或多选 Tag 模式(不硬编码 3 列),为将来扩到 10+ 家 LLM 留余地。每家 LLM 显示已连接账号数,未连接时灰化提示"需先在桌面端登录 Kimi 账号"。
#### 6.8.4 admin-web 新增 / 扩充 API + 与现有路由的迁移关系
2026-04-18 21:22:38 +08:00
2026-04-18 21:48:02 +08:00
**新增路由**:
2026-04-18 21:22:38 +08:00
` ``
2026-04-18 21:48:02 +08:00
GET /api/tenant/accounts # 带 online + health + lastPublishAt + tags 聚合
2026-04-18 21:22:38 +08:00
GET /api/tenant/accounts/{id}
GET /api/tenant/clients # 本 workspace 的桌面端在线情况
GET /api/tenant/publish-jobs # 发布批次列表
GET /api/tenant/publish-jobs/{id} # 批次详情含子任务
POST /api/tenant/publish-jobs # 创建(见 §6.8.2 step 3)
GET /api/tenant/monitoring-results # 支持 filter / range / 按 llm / 按账号
2026-04-18 21:48:02 +08:00
GET /api/tenant/events # SSE, topic 参数仅作提示,实际 topic 服务端派生
2026-04-18 21:22:38 +08:00
` ``
2026-04-18 21:48:02 +08:00
**与现有路由的迁移表**(核对 ` server/internal/tenant/transport/router.go`):
2026-04-18 21:22:38 +08:00
2026-04-18 21:48:02 +08:00
| 现有路由 | 处置 | 新路由 / 迁移策略 |
|---|---|---|
| ` GET /api/tenant/media/platform-accounts` | **删除** | ` GET /api/tenant/accounts` 替代。旧响应里缺 ` platform_uid`;因是开发版本无生产数据,DB schema 直接 drop 旧表 create 新表(§6.1)。 |
| ` DELETE /api/tenant/media/platform-accounts/:id` | **删除** | ` DELETE /api/desktop/accounts/{id}?if_sync_version=...` 由桌面端发起;Web 端想删需先弹"先在桌面端解绑"引导,或通过 ` POST /api/tenant/accounts/{id}/request-delete` 写 tombstone,下次桌面端 resync 时同步(见 §6.8.6)。 |
| ` POST /api/tenant/articles/:id/publish-batches` | **重命名 + 语义扩展** | ` POST /api/tenant/publish-jobs`, body 里 ` content_ref: {type: "article_draft", id: "<article_id>"}`。旧接口一次只绑一篇文章,新接口支持多态 ` content_ref`。 |
| ` GET /api/tenant/articles/:id/publish-records` | **重命名 + 视角变换** | ` GET /api/tenant/publish-jobs/{job_id}` 的子任务列表。若要按文章查,加 filter:` GET /api/tenant/publish-jobs?content_ref_id=<article_id>`。 |
| ` POST /api/tenant/media/plugin-installations/register` 等 plugin 路由 | **删除** | 全部被 ` /api/desktop/*` 替代(§10.2)。|
**SSE 通道交付契约**( ` /api/tenant/events` 与 ` /api/desktop/events` 对等):
| 能力 | ` /api/desktop/events` | ` /api/tenant/events` |
|---|---|---|
| 多实例 fanout | ✅ RabbitMQ + 本地 SSE hub + Redis presence | ✅ 同机制,topic 路由不同 |
| 订阅建立时首帧 | — | **发送 ` snapshot` 事件**(当前 job/account 快照)再追增量 |
| 事件 ` event_id` 单调递增 | ✅ | ✅ |
| ` Last-Event-ID` header 重连 replay | ✅ | ✅(至少 5 min 窗口) |
| 客户端去重(按 event_id) | ✅ | ✅ |
| 鉴权 | ` client_token` + TenantScope | ` User JWT` + **新增 ` WorkspaceScope`**(见 §6.4 中间件要求)|
| Topic 过滤 | ` client_id` 派生 | **服务端按 ` actor + workspace_id` 派生**, query param ` topic` 仅提示、必须经服务端校验归属后才订阅 |
**关键**:两通道**在同一个 RabbitMQ fanout 之上分主题分流**——一条 task 状态变化同时广播给 Web 端(通过 ` topic=job:<id>` 派生)和 Desktop 端(通过 ` topic=client:<id>` 派生),订阅者拿到的是各自视角的事件流。
#### 6.8.5 离线降级 UX 与上线补领
**离线态展示**:
2026-04-18 21:22:38 +08:00
- **某桌面端离线**( ` last_seen_at > 5 min`):
2026-04-18 21:48:02 +08:00
- Modal 里所属账号标"离线(上次 X 分钟前)· 待执行队列 N",仍可勾选,顶部提示"任务将在该端上线后自动执行"。
2026-04-18 21:22:38 +08:00
- 账号总览 / 监控结果用灰色 badge。
2026-04-18 21:48:02 +08:00
- 若同 workspace 有其他在线客户端但它没持有该账号 → Modal 显式提示"该账号绑定的 ` <device_name>` 离线,其他客户端无法代跑(Cookie 本地隔离)"。
2026-04-18 21:22:38 +08:00
- **workspace 所有桌面端都离线**:
2026-04-18 21:48:02 +08:00
- Modal 顶部 banner: "当前 workspace 无在线桌面端。任务会排队(队列 N 条,预计等待 > M min)。[下载桌面端]"
2026-04-18 21:22:38 +08:00
- 监控计划若连续 N 次未执行,站内通知 + 邮件给 workspace owner。
2026-04-18 21:48:02 +08:00
- **容量/ETA 反馈**: ` GET /api/tenant/publish-jobs/{id}` 返回 ` queue_ahead`(该任务前面还有几条排队)和 ` eta_seconds`(基于近期吞吐估算);Modal 进度面板显示为"前面还有 3 条任务 · 预计 2 min 后开始"。
**上线补领机制**(**双向夹逼**,避免任务永远待领):
2026-04-18 21:22:38 +08:00
2026-04-18 21:48:02 +08:00
- **Server 侧**: ` desktop-clients` 的 Redis presence TTL 从无到有时(offline→online 转换),scheduler 扫描 ` desktop_tasks WHERE target_client_id = $1 AND status = 'queued'`,对命中的每条重发 ` task_available` 到该 client 的 SSE。
- **Client 侧**: ` POST /api/desktop/clients/heartbeat` + §6.8.6 resync 完成后,**立即进入 ` lease(limit=20)` 循环**直到返回空;之后才回到 SSE-driven 被动模式。
- **event 刷新**: admin-web 的 ` /api/tenant/events` 通道同时收到 ` client_online` 事件,前端自动刷新账号 badge 与 job 状态,无需用户 reload。
2026-04-18 21:22:38 +08:00
2026-04-18 21:48:02 +08:00
#### 6.8.6 Account 数据一致性(CAS + Tombstone 协议)
2026-04-18 21:22:38 +08:00
2026-04-18 21:48:02 +08:00
**关键原则**(取代 v2.1 中模糊的"client = SoT / server = 投影"措辞,因其在并发场景下语义不一致):
2026-04-18 21:22:38 +08:00
2026-04-18 21:48:02 +08:00
1. **Account 的生命周期由 server 的 ` sync_version` 单调序列与 ` deleted_at` tombstone 共同定义**; client 持有本地 partition 与 cookie。server 仍不持有任何 cookie 原文。
2. **所有写操作都是 CAS( Compare-And-Swap) **: client 请求必须带 ` if_sync_version=<n>`, server 仅当 ` current_version == n` 时接受写入并自增 version;否则返 409 Conflict, client 据此触发 resync。
3. **删除永远是 tombstone( soft delete) **: server 不 hard delete,写 ` deleted_at = now() + sync_version += 1`; hard delete 由周期任务在 tombstone 保留 ≥ 30 天后清理。
**写路径**:
| 操作 | 发起方 | 端点 | 语义 |
|---|---|---|---|
| 登录新账号 | client | ` POST /api/desktop/accounts` body 带 ` if_sync_version=null` | 首次创建,失败(已存在且未 tombstone)返 409 |
| 健康 / 昵称 / tags 变化 | client | ` PATCH /api/desktop/accounts/{id}?if_sync_version=<n>` | CAS 更新 |
| 客户端主动解绑 | client | ` DELETE /api/desktop/accounts/{id}?if_sync_version=<n>` | 写 tombstone + sync_version 自增 + 级联未完成 task → ` aborted` |
| Web 端请求删除 | admin-web | ` POST /api/tenant/accounts/{id}/request-delete` | **不直接删**,只在行上标 ` delete_requested_at`;下次 client resync 看到后本地清 partition + 走上面的 DELETE 把删除坐实 |
**resync 流程**( client 启动时):
` ``
1. client 本地:SELECT id, sync_version FROM local_accounts
2. client → GET /api/desktop/accounts?client=self
→ 返回 [{id, platform, platform_uid, display_name, health, tags,
client_id, sync_version, deleted_at, delete_requested_at}...]
3. 三方合并:
- 本地无 + server 无 deleted_at → 本地插入
- 本地有 + server 无记录(极少见,异常日志)→ 本地发 POST 同步回去
- 本地有 + server.deleted_at != null → 本地清 partition,删本地记录
- 本地有 + server.delete_requested_at != null → 本地清 partition + POST DELETE 坐实
- 双方 sync_version 不一致 → 以 server 为准覆盖本地元数据(cookie 不动)
- (platform, platform_uid) 冲突(同一 UID 出现在两个 workspace)→ §6.6 唯一索引
直接拒绝新 workspace 的 upsert, client 收 409, UI 提示"该账号已在其他 workspace"
4. 合并结束,client 切入业务。
` ``
**并发案例(误操作撤销)**:
| 时刻 | Client A(原持有)| Server | 说明 |
|---|---|---|---|
| T0 | ` acc-x` 登录,local.sync_version=5 | sync_version=5 | 稳态 |
| T1 | — | delete_requested_at=now | Web 端用户误点"请求删除" |
| T2 | A PATCH tags( if=5) | accepted → version=6 | CAS 通过 |
| T3 | A 收到 ` delete_requested` 广播 → UI 询问用户 | — | — |
| T4 | 用户撤销 → ` POST /api/tenant/accounts/{id}/request-delete?undo=true` | clear delete_requested_at, version=7 | — |
**语义**:没有一方能单独 hard-delete;任何分歧都可通过 server 的单调 version 序列还原正确状态。
admin-web 侧 "Modal 里账号消失" = client 已走完 DELETE 路径且 tombstone 落地;秒级一致性延迟可接受。
2026-04-18 21:22:38 +08:00
#### 6.8.7 深链接 ` georankly://`
2026-04-18 21:48:02 +08:00
| 深链 | 场景 | 参数 | Fallback |
|---|---|---|---|
| ` georankly://accounts` | "前往桌面端管理账号" | — | 未装 → 下载页 |
| ` georankly://accounts/add?platform=wechat` | "一键添加某平台账号" | platform | 同上 |
2026-04-18 22:12:53 +08:00
| ` georankly://tasks/<id>/review?nonce=<opaque_code>` | Manual 模式等审核 | **必须带一次性 nonce**: server 签发的**短期 opaque code**(不是 JWT——避免把 claims 放进 URL)。TTL 60 s,后端存入 Redis 带 ` jti`;绑定 ` (task_id, target_client_id, actor_user_id)`。客户端打开深链时先回调 ` POST /api/desktop/links/verify` 换取访问凭证:服务端校验 ` target_client_id == 当前 client_token 的 client_id` 且 nonce 未被消费过,通过后**立即 delete jti 失效**。同机恶意进程即使抓到 URL 也无法重放(不同 client_id / 已消费)。 | 同上 |
2026-04-18 21:48:02 +08:00
**点击交互链路**(解决跨浏览器行为差异 + 协议未注册 fallback):
` ``
user clicks deep link in admin-web
↓
1. admin-web 先 GET /api/tenant/clients 检查该 workspace 是否有在线桌面端
├─ 无 → 直接弹"请先安装并登录桌面客户端"引导页,不尝试唤起协议
└─ 有 → 继续
↓
2. 弹出"正在打开桌面客户端..."中间态 modal(带 loading),同时:
- 触发 window.location = 'georankly://...'
- 设置 2s 超时检测(window.blur / visibilitychange 是否触发)
↓
3. 2s 内检测到窗口失焦 → 深链成功唤起 → 关闭 modal
超时未唤起(协议未注册 / 浏览器阻拦 / 用户取消确认):
- modal 切到"未检测到桌面客户端启动"状态
- 提供两个按钮:[手动启动桌面客户端] [复制任务链接到桌面端地址栏]
- 仍显示下载链接兜底
` ``
这套做法解决了 Chrome/Edge/Safari/360/QQ 浏览器对自定义协议首次点击时行为不一致的问题(部分会弹"允许打开"、部分直接跳转、部分静默拦截)。
2026-04-18 21:22:38 +08:00
2026-04-18 21:48:02 +08:00
**Windows 协议注册**:安装 package 时通过 NSIS 脚本写入 ` HKCU\Software\Classes\georankly` 注册表项;卸载时清理。macOS 通过 ` Info.plist` 的 ` CFBundleURLTypes` 声明;Linux 通过 ` .desktop` 文件的 ` MimeType=x-scheme-handler/georankly`。这些具体步骤在 Plan A 的打包任务里明确落实。
2026-04-18 21:22:38 +08:00
2026-04-18 20:28:42 +08:00
---
## 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 结构化日志
2026-04-18 21:05:41 +08:00
` pino` JSON,字段约定:` ts, level, module, accountId, taskId, attemptId, leaseToken, stage, event, durationMs, bytes`。本地 rolling file( 10 MB × 20 份)。**不默认上传**。脱敏字段:Cookie / Set-Cookie / Authorization / x-xsrf-token / password / 手机号 / email。
2026-04-18 20:28:42 +08:00
### 7.3 指标上报
2026-04-18 21:05:41 +08:00
5 min 一次上报 ` /api/desktop/clients/metrics`,只含非敏感聚合:CPU/内存/事件循环延迟、每平台每账号的任务成功率 + 平均耗时、SSE 连接状态、pull 回退次数、token bucket 拒绝率。**不含**查询内容、回答内容、Cookie。
2026-04-18 20:28:42 +08:00
### 7.4 用户可见反馈
- 托盘图标三色(绿/黄/红)表示账号健康。
2026-04-18 21:05:41 +08:00
- OS 通知**只**在需要用户动作时弹(登录失效、捕获到验证码、租约失效导致 UNKNOWN 需人工对账)。
2026-04-18 20:28:42 +08:00
- UI "事件中心":列最近 50 次失败,点进去直接"重新运行"或"启用录屏再跑"。
- "发送诊断包"按钮:最近 1 小时日志 + trace 打包脱敏上传,返回 ` diagnostic_id`。
2026-04-18 21:05:41 +08:00
### 7.5 崩溃恢复(按 task.kind 分治)
2026-04-20 09:52:48 +08:00
启动时扫描 ` desktop_tasks` 里 ` target_client_id = self AND status IN (in_progress)` 的任务:
2026-04-18 21:05:41 +08:00
**Monitor 任务**(幂等只读,可安全重跑):
2026-04-18 20:28:42 +08:00
2026-04-18 21:05:41 +08:00
- 任何阶段崩溃 → 丢弃本地状态 → 放回队列 → 下次被分派重跑。日志里记录 ` previous_attempt_aborted_by_crash` 用于对账。
- 不标 UNKNOWN,不骚扰用户。
2026-04-18 20:28:42 +08:00
2026-04-18 21:05:41 +08:00
**Publish 任务**(不可逆副作用,保守处理):
2026-04-20 09:52:48 +08:00
- **未进入 ` submitting` 阶段**(即还在 ` filling`):重新 ack → 把 state 重置到 ` filling` 从头跑,因为还没点过“发布”按钮。
2026-04-18 22:12:53 +08:00
- **已进入 ` submitting`**: **放弃任务,标 ` unknown`**。避免重复发布。
**UNKNOWN 任务的 UI 对账工作流**(不只是"提示一下"就完事):
UNKNOWN 任务在 admin-web "事件中心"和"发布历史"页有专门区块,黄色高亮 + 顶部固定。每条带一个 Action Dropdown:
| 用户动作 | 后端行为 | 语义 |
|---|---|---|
2026-04-18 23:19:49 +08:00
| **"已确认发布成功"** | ` POST /api/tenant/tasks/{id}/reconcile {status: 'succeeded', external_url}` | 把状态坐实为 ` succeeded`,计入成功统计 |
| **"已确认发布失败"** | ` POST /api/tenant/tasks/{id}/reconcile {status: 'failed', reason}` | 状态改 ` failed`,允许触发 retry |
| **"忽略此任务"** | ` POST /api/tenant/tasks/{id}/reconcile {status: 'aborted'}` | 状态改 ` aborted`,从待办清单撤离 |
2026-04-18 23:32:03 +08:00
| **"重新运行"** | ` POST /api/tenant/tasks/{id}/reconcile {status: 'retry'}` | **原 task status → ` queued`, ` attempts + 1`, ` active_attempt_id = null`**;等 target_client 通过 §6.3 lease 流程重新领取。不复制、不新建 job,任务 ID 保持原值 → 历史可追溯 |
2026-04-18 22:12:53 +08:00
2026-04-18 23:32:03 +08:00
**关键**:UNKNOWN 不会自动消失——用户必须显式选一项。超过 7 天没处理的 UNKNOWN 任务每周触发一次站内通知给 workspace owner。"重新运行"的 retry 语义与 §15.1 Mermaid 状态图 ` unknown → queued` 一致。
2026-04-18 20:28:42 +08:00
---
## 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**。
2026-04-18 21:05:41 +08:00
**native deps 多架构注意**: ` better-sqlite3` / ` keytar` 等在 universal Mac build 里需通过 ` electron-builder` 的 ` buildDependenciesFromSource` 或 pre-built multi-arch binaries 处理,CI 会增加一步 ` @electron/rebuild ` 针对每个 target。
2026-04-18 20:28:42 +08:00
### 8.3 CI
2026-04-18 21:05:41 +08:00
GitHub Actions, runner 矩阵:` macos-14 (arm64)` + ` macos-13 (x64)` 合并 universal; ` windows-latest` 出 x64; Windows arm64 用 cross-compile; Linux x64 ` ubuntu-latest`; Linux arm64 优先用 ` ubuntu-22.04-arm` 原生 runner,不可用时退化到 QEMU( 2026-04 GitHub arm64 runner 仍在限量可用)。
发布 tag ` v1.2.3` → 打包 → 上传对象存储 + 生成 ` latest-*.yml` → ` autoUpdater.checkForUpdates()` 看到。
2026-04-18 20:28:42 +08:00
### 8.4 更新 UX
- 默认"自动下载 + 手动安装"。下好后 Tray 角标提示"新版本可用,点击重启安装"。不打断正在跑的任务。
2026-04-18 21:05:41 +08:00
- 强制更新:服务端下发 ` minClientVersion`;低于该版本拒绝启动,引导升级(配合 30 分钟宽限期 + 本地可见的手动下载链接兜底)。
- 降级:使用 ` electron-updater` 的 ` allowDowngrade: true`(**注意正确拼写**,旧 spec 写的 ` downgrade` 属性不存在)。服务端可下发"建议版本号",客户端比建议版本新时允许一键回滚。
- **N-1 回退**:对象存储长期保留上一版安装包 + manifest,UI 提供"手动下载上一版本"链接,作为 autoUpdater 失败的人工兜底。
- pre-release 通道每次打包都跑一次**updater smoke 自动化**(见 §9.7)。
2026-04-18 20:28:42 +08:00
### 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 → 系统浏览器。
2026-04-18 21:05:41 +08:00
- 禁用 ` <webview>` tag,第三方平台一律用 ` WebContentsView`。
2026-04-18 20:28:42 +08:00
---
## 9. 测试策略
### 9.1 金字塔
` ``
┌──────────────┐ 手工 + 冒烟 Playbook(每 release)
│ E2E 烟雾 │
├──────────────┤ Playwright @ Electron + fake server + fake platforms(每 PR)
│ 集成 / 行为 │
├──────────────┤ Vitest(每 commit)
│ 单元(adapter)│
├──────────────┤ tsc --noEmit + eslint + typecheck(每保存)
│ 类型 / 静态 │
└──────────────┘
` ``
### 9.2 单元
2026-04-18 21:05:41 +08:00
- 对象:适配器的 **SSE parser**(纯函数)、**发布状态机 + 租约交互逻辑**、**lease-manager 本地状态机**、**autoUpdater 核心逻辑**(版本比较、签名验签、` allowDowngrade` 决策、` minClientVersion` 校验)、**patch-loader 签名与序号校验**。
- Fixtures: ` tests/fixtures/sse/doubao-2026-04.txt`、` tests/fixtures/manifest/valid-*.json` + ` invalid-*.json`。
2026-04-18 20:28:42 +08:00
- 平台 payload 变动 → 补 fixture → 测试跟着更新。
### 9.3 集成
2026-04-18 21:22:38 +08:00
用 Playwright ` _electron.launch` 驱动 Electron,对 **本地 fake server + 本地 fake 平台**( HTTP server 喂 fixture SSE)。13+5 适配器全跑一遍,并行约 3–5 分钟。
2026-04-18 20:28:42 +08:00
### 9.4 集成 · 协议
2026-04-20 09:52:48 +08:00
客户端 + 真实 server + fake 平台:验证**任务分发 / 租约 (` lease` / ` extend` / ` cancel` / ` result`) / 多实例广播 / 幂等 / 熔断 / 断线重连 / queued 任务在 target_client 下线后重新补领 / desktop publish management 的列表与再次发送路径**。
2026-04-18 20:28:42 +08:00
### 9.5 人工冒烟 Playbook
每次 release 用测试账号:
- 每发布平台跑一个 draft/private publish → 立即删。
- 每 LLM 跑 3 条标准查询,对比上版本抓取结构一致性。
记成 md 文档由 QA 跟着跑,半小时完成。
### 9.6 Trace 回归套件
工具 ` pnpm dev:replay <trace.zip>`:把真实 trace 里的 network responses 重放到 fake 平台,跑 adapter。历史错过的输入永远不再破解析器。
2026-04-18 21:05:41 +08:00
### 9.7 autoUpdater Smoke( pre-release 必跑)
不是"完整升级 E2E",但必须覆盖:
2026-04-18 20:28:42 +08:00
2026-04-18 21:05:41 +08:00
- 旧版本打包 + 新版本打包 + 从旧版本启动 → 拉 manifest → 下载 → 安装 → 启动新版本(headless 跑通即 pass)。
- 签名不一致、manifest 过期、` minClientVersion` 不满足 → 各一条负面用例。
- ` allowDowngrade: true` 与 ` false` 各一条正向用例。
2026-04-18 20:28:42 +08:00
### 9.8 门禁
- PR:单元 + 集成(fake)全过。
2026-04-18 21:05:41 +08:00
- Release tag: + 冒烟 Playbook 签字 + updater smoke 全过。
2026-04-18 20:28:42 +08:00
- 每周:trace 诊断报告 review。
---
## 10. 上线节奏与回滚
### 10.1 阶段
| 阶段 | 受众 | 签名 | 退出门槛 |
|---|---|---|---|
2026-04-18 21:22:38 +08:00
| **Alpha(内部)** | 开发 + QA(≤5 人)| Mac 公证 / Win 无签名 / Linux 裸 AppImage | 13+5 适配器 fake + 真实账号成功率 ≥ 90%;lease 协议在混沌测试下无脏写 |
2026-04-18 22:12:53 +08:00
| **Beta(邀请制)** | 公司内部 + 合作客户(≤30 人)| 同上 | 连续 7 天无 P0;保活命中率 > 95%; **patch 通道最小闭环(Plan B 块 G1)已上线,应急修复跑通 ≥ 1 次**; autoUpdater smoke 每 release 全过 |
2026-04-18 20:28:42 +08:00
| **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 老表,不写自动迁移。
2026-04-18 22:12:53 +08:00
### 10.3 工作量重估(10 块)
2026-04-18 21:05:41 +08:00
粗估(单人工程周):
| 块 | 范围 | 估时 |
|---|---|---|
2026-04-18 22:12:53 +08:00
| **0. Workspace 底座一等化**( v2.3 新增 · Plan 0) | ` workspaces` + ` workspace_memberships` 表迁移;` Actor/Claims` 加 ` workspace_id`; ` WorkspaceScope` middleware;所有 repo query 注入 ` workspace_id`; ` cache_support.go` key 改 workspace 粒度;监控域存量改造清单(§6.1.1)完整迁 | **2– 3 周(阻塞 Plan A) ** |
2026-04-20 09:52:48 +08:00
| **A. Desktop runtime 基座** | Main 进程、session-registry、view-pool、vault、lease-manager、rate-limiter、circuit-breaker、crash-recovery、tracing( Alpha 版)、transport (SSE + pull + RabbitMQ presence) | 3– 4 周 |
2026-04-18 22:12:53 +08:00
| **B. LLM 监控适配器** | Doubao + Qwen + **DeepSeek(从零)** + **元宝(从零)** + **Kimi(从零)**; **前置 § 5.2 spike 矩阵 0.5 周** | 4.5– 5.5 周 |
2026-04-18 21:05:41 +08:00
| **C. 已有真实发布能力的平台迁移** | 约 6 家(头条 / 百家 / 搜狐 / 知乎 / 企鹅 / 简书)从 chrome.* 迁到 Electron 抽象 | 3 周 |
| **D. Detect-only 平台从零实现 publish** | 7 家(微信公众号 / 掘金 / 懂车帝 / 什么值得买 / 网易 / B 站 / ZOL) | 5– 7 周 |
2026-04-20 09:52:48 +08:00
| **E. 服务端协议 + DB + sqlc 重生 + Service 层重构** | ` /api/desktop/*`、租约、发布管理(publish-tasks 列表/再次发送)、presence、patch manifest、reconcile | 2.5 周 |
2026-04-18 21:05:41 +08:00
| **F. 打包 5 arch + Mac 公证 + autoUpdater + updater smoke** | — | 1.5 周 |
2026-04-18 22:12:53 +08:00
| **G. Patch 通道** | G1( **最小闭环**:服务端签名 manifest + 客户端 Ed25519 验签 + DSL 解释器 + revoke + rollback)前移到 Plan B, 1.5 周;G2(备用隔离执行通道 + 签名链全套 TUF 要素 + 密钥轮换)留在 Plan C, 1 周 | 2.5 周(拆 1.5 + 1) |
| **H. packages/ui-shared 抽取 + desktop renderer** | 抽 ` MarkdownPreview / AccountCard / TagMultiSelect / HealthBadge / SseSubscription composable` 等;**PublishArticleModal 重构为账号级多选(原 accounts[0] 假设要彻底改)**; i18n、主题 | **2.5– 3 周(v2.2 review 低估了 1– 1.5 周)** |
| **I. 测试基建** | fake server + fake 平台 + replay + autoUpdater 核心逻辑单测 | 1.5 周 |
2026-04-18 21:05:41 +08:00
2026-04-18 22:12:53 +08:00
合计约 **28– 33 工程周**( v2.3 新增 Plan 0 前置 + Modal 重构工期修正 + G 拆前后期)。这份 spec 因此**拆 4 个 plan**(见 §12)。
2026-04-18 21:05:41 +08:00
### 10.4 三层回滚
2026-04-18 20:28:42 +08:00
| 情景 | 动作 |
|---|---|
2026-04-18 21:05:41 +08:00
| 某 patch 导致单平台适配器坏 | 服务端下发 revoke ` patch_seq` + 上一版本可用 manifest;客户端下次任务自动降回前一版 patch(秒级) |
| 某 release 中等问题 | 下发"建议版本 ≤ X"; ` electron-updater` 用 ` allowDowngrade: true` 降级(分钟级);+ 手动 N-1 下载链接 |
| 某 release 严重安全问题 | 下发 ` minClientVersion = Y`;低于 Y 的客户端 30 分钟宽限后拒绝启动,UI 引导升级 |
2026-04-18 20:28:42 +08:00
所有回滚**只在服务端配置**,不动客户端代码。
2026-04-18 21:05:41 +08:00
### 10.5 运营指标
2026-04-18 20:28:42 +08:00
- 每平台适配器每日成功率(target:发布 > 95%、监控 > 90%)
- 每次 crash 的 top 堆栈
- SSE 长连接平均保持时长(target > 30 min)
2026-04-18 21:05:41 +08:00
- Patch 推送后"多久 80% 客户端加载新 patch"( target < 30 min)
- 租约冲突率(409 / 百次 lease; target < 0.5%)
2026-04-18 20:28:42 +08:00
---
## 11. 已知风险
2026-04-18 21:05:41 +08:00
1. **Win 无签名期间**( Alpha/Beta),用户首次安装会遇 SmartScreen 警告,且有机率被 Defender / 360 / 国内安全软件主动隔离。Alpha 期间通过白名单与用户手动放行缓解。
2. **豆包/千问网页结构变**,最坏情况紧急手搓 patch,SLA 按小时算。DSL 能力有限时退化到备用隔离执行通道。
3. **用户跨 OS 切换**(Mac → Win 再回来)不迁移登录态(local-only cookies),需全部重登。或使用 ` vault-export` 手工带密口令迁移。
2026-04-18 20:28:42 +08:00
4. **账号被平台风控**无法自动恢复;UI 引导用户换号。
5. **开机自启 + 托盘常驻**可能被企业 EDR 误报恶意软件;需准备白名单说明材料。
2026-04-18 21:05:41 +08:00
6. **Electron 基础 RAM 占用**~300 MB + 每 hot-tier WebContentsView ~50 MB。默认 Hot/Cold 分档后,10 账号活跃场景约 600–800 MB。
7. **Electron CVE 维护节奏**: Electron 约每 4–6 周发安全版本,需对应 release 通道的补丁节奏。滞后发版 > 2 周的 Electron 主线安全问题需主动升级。
8. **平台 ToS 风险**:部分媒体平台(尤其微信公众号)明文禁止自动化操作。Alpha/Beta 阶段限定给明确授权账号;商业化前评估法务暴露面。
9. **CDP domain 可用性跟随 Chromium 版本**(如 ` Network.streamResourceContent` ≥ 120)。升 Electron 大版本前必须跑一遍 ` electron-chromium-cdp-matrix.md` 回归。
---
## 12. Plan 拆分建议
2026-04-18 22:12:53 +08:00
本 spec 包含约 28–33 周的工作量,拆 **4 个独立 plan** 执行。**Plan 0 是 Plan A 的硬前置**( workspace 底座不一等化,后面所有协议/鉴权/缓存/监控承诺都无法真正落地)。
### Plan 0 · Workspace 底座一等化(2– 3 周 · 阻塞 Plan A)
2026-04-18 23:19:49 +08:00
#### 范围策略:Phase 1 **窄 workspace 化**( v2.4 明确决策)
v2.2 review 发现 workspace 不是后端一等安全边界。但**当前代码完整 tenant-native**( articles / brands / kol-templates / tenant_monitoring_quotas / publish_batches / images / queues 全都挂 tenant 主键),若 Plan 0 要求"现有 admin-web 所有页面都在 workspace 粒度工作",实际工作量是 5–8 周而非 2–3 周。因此本 spec 明确采用**两阶段策略**:
- **Phase 1( Plan 0,本轮)**:只在桌面客户端新引入的三条线上做 workspace 一等化——` desktop_clients` / ` platform_accounts` / ` desktop_tasks` / ` desktop_publish_jobs` / ` tenant_monitoring_quotas` 及其周边代码路径。所有**现有** ` articles / brands / kol-templates / publish-batches / images` 等业务线**继续保持 ` tenant == workspace`**(即 ` workspace_id` 默认 = 用户的 primary workspace,不开放多 workspace 切换 UI)。
- **Phase 2(后续独立 plan,不在本 spec 范围)**:当产品真的需要同一租户多 workspace 场景时,再把现有业务线全量 workspace 化。那时会拉一轮独立的 migration + repo 重构。
这样 Plan 0 的 exit bar 能真的在 2–3 周内完成;也不对现有线做伤筋动骨的改造。**"窄 workspace 化"作为本版硬约束写死在此处**——任何声称"admin-web 全量 workspace 工作"的说法视为范围蠕变(scope creep),需重新评审。
#### Plan 0 内容
2026-04-18 22:12:53 +08:00
2026-04-18 23:19:49 +08:00
1. ` workspaces` + ` workspace_memberships` 表迁移(server-side, shared across both phases)。
2. ` Actor/Claims` 结构扩 ` workspace_id`; User JWT 也加;登录时 client token issuance 绑定 ` primary workspace_id`。
3. ` TenantScope` 之上新加 ` WorkspaceScope` middleware; **只挂在**:
- ` /api/tenant/events`
- ` /api/tenant/accounts*`
- ` /api/tenant/publish-jobs*`
- ` /api/tenant/clients`
- ` /api/tenant/monitoring-*`
- ` /api/tenant/tasks/{id}/reconcile`
(其他 ` /api/tenant/*` 在 Phase 1 仍挂 TenantScope, ` workspace_id` 内部 default 为用户 primary。)
4. 以上列表路由的 repo query 显式注入 ` workspace_id = :actor_workspace_id`。
5. ` cache_support.go` 仅对 **desktop/account/monitoring 相关 cache key** 加 ` workspace_id`;其余 cache key 暂保持 tenant 粒度。
6. **监控域存量改造**(§6.1.1 全量):
- ` tenant_monitoring_quotas` 加 ` workspace_id` 列 + 改 ` primary_installation_id` → ` primary_client_id`
- ` monitoring_service.go` / ` monitoring_callback_service.go` / ` monitoring_handler.go` 改造
- 现有监控 projection / recovery / dashboard query 一并迁移
- seed data + 定时清理任务同步调整
7. admin-web 调用链:**仅**上述列表涉及的页面(账号总览、发布 Modal、监控结果、客户端总览、事件中心)在 API 层加 workspace context header;其他页面不动。
8. **CI guard**:加一个 lint rule 或自动化检查,拒绝新代码在上述三条线里写没有 ` workspace_id` 过滤的 repo query。
2026-04-18 21:05:41 +08:00
2026-04-18 23:19:49 +08:00
#### 退出目标(revised)
- **desktop/account/monitoring 三线** workspace 一等化落地并通过 cross-workspace 防穿透单元测试(至少 10 条针对 ` GET/POST/DELETE /api/tenant/accounts|publish-jobs|monitoring-results|tasks/.../reconcile` 的越权用例)。
- ` /api/tenant/events` 的 topic 服务端派生机制实现并覆盖测试。
- 现有 admin-web 功能**无回归**(现有页面继续按 tenant 粒度工作,不需新增 workspace 切换 UI)。
- §6.1.1 监控存量 8 行改造全部完成并 code review 通过。
2026-04-18 21:05:41 +08:00
2026-04-18 22:12:53 +08:00
### Plan A · 桌面端骨架穿透(5–6 周,Alpha 上限)
2026-04-20 09:52:48 +08:00
- 块 A( Desktop runtime 基座 + crash-recovery + publish queue execution)
- 块 E(服务端协议 + DB + ` /tasks/{id}/reconcile` endpoint + ` /api/desktop/publish-tasks` 列表/再次发送)
2026-04-18 23:19:49 +08:00
- 块 H(拆两阶段,见下)
- **H1 · 前期(Plan A 第 1– 2 周)**: ` packages/ui-shared` 骨架 + **design tokens**( CSS variables light/dark + Ant Design Theme Config) + ` useAccountSelection` composable(账号级多选逻辑)+ ` HealthBadge / AccountCard / TagMultiSelect / MarkdownPreview` 组件迁入。
- **H2 · 后期(Plan A 第 4– 5 周)**: ` PublishArticleModal` 重构为账号级多选(基于 H1 的 composable),接入新 ` /api/tenant/publish-jobs` 展开语义,进度面板 SSE 订阅。
- **§5.2 spike 矩阵**( 0.5 周,开 Plan A 前先做,见下文)
2026-04-18 22:12:53 +08:00
- **1 个** 监控适配器(Doubao)+ **1 个** 发布适配器(头条号,已有 publish 能力)
2026-04-18 21:05:41 +08:00
- 块 I(测试基建)
2026-04-18 22:12:53 +08:00
- 块 F **仅 Mac 公证**
2026-04-18 21:05:41 +08:00
2026-04-18 23:19:49 +08:00
**Spike 矩阵操作规格**( v2.4 补齐):
- **Owner**: runtime owner(负责 CDP / Fetch 基建)+ 对应 provider owner( Doubao 为主)联签。
- **产物**: ` docs/superpowers/specs/electron-chromium-cdp-matrix.md`——表格列 ` provider × transport × Electron/Chromium 版本 × CDP domain 结论`,每格附抓包/DevTools 截图或日志片段。
- **三态结论**:
- ` supported`:该 transport 在目标 Electron 版本下 CDP 完整可用,直接用。
- ` degraded`:部分工作(比如 ` Network.streamResourceContent` 可用但偶发 chunk 丢失),记录 workaround 并定 known-issue issue。
- ` blocked`:完全不工作(页面改用 WebTransport / WebSocket / WebRTC 等 CDP 未覆盖路径)。进入 fallback 决策树:① DOM-only 降级(只从渲染后文本抽取,保留但标"degraded quality")② 移出本版承诺范围(产品决策,工期不赔)③ 等待 Chromium 版本升级。
- **Gate**: spike 未出具三态结论前,块 B 的新 provider 接入**不开工**; spike 只对 Plan A 承诺的 Doubao 做完整验证即可解锁 Plan A,其余 4 家 LLM 在 Plan B 启动前各自跑一次。
2026-04-20 09:52:48 +08:00
**退出目标**:端到端跑通一条 Doubao 监控任务 + 一条头条发布任务;desktop client 提供“发布管理”视图(待发布队列 + 历史结果 + 再次发送);Mac 公证版可安装;lease 协议在 fake 多实例 + 混沌测试下无脏写;UNKNOWN 任务的 reconcile 工作流闭环。
2026-04-18 21:05:41 +08:00
2026-04-18 22:12:53 +08:00
### Plan B · 广度铺开(12– 14 周,Beta 上限)
2026-04-18 21:05:41 +08:00
2026-04-18 22:12:53 +08:00
- 块 B( Qwen + DeepSeek + 元宝 + Kimi 从零)
2026-04-18 21:05:41 +08:00
- 块 C(其余 5 家已有真实发布能力的平台迁移)
- 块 D( 7 家 detect-only 从零实现 publish)
2026-04-18 22:12:53 +08:00
- **块 G1 · Patch 通道最小闭环**(服务端签名 manifest + 客户端 Ed25519 验签 + DSL 解释器 + revoke + rollback, 1.5 周)——前移到此,满足 §10.1 Beta 退出门槛"patch 通道应急修复跑通 ≥ 1 次"
2026-04-18 21:05:41 +08:00
- 块 F 完整:Win/Linux 全架构 + 签名(Win 签名到位)+ autoUpdater 完整
- 块 I 补齐:多适配器回归 + 冒烟 Playbook
2026-04-18 22:12:53 +08:00
**退出目标**: 13+5 适配器全量可用;autoUpdater smoke 每 release 全过;5 arch × 3 OS 全量打包签名;patch 通道应急修复在 Beta 期至少跑通一次。
2026-04-18 21:05:41 +08:00
2026-04-18 22:12:53 +08:00
### Plan C · 运维闭环与高级能力(4–5 周,GA 上限)
2026-04-18 21:05:41 +08:00
2026-04-18 22:12:53 +08:00
- **块 G2 · Patch 通道高级能力**:备用隔离执行通道(utilityProcess + isolated-vm)、根密钥轮换、TUF 风格完整 manifest( 1 周)
- Phase 2 Trace 基建(CDP 全事件流 + 2 fps 截屏 rolling buffer + Playwright Trace Viewer 兼容导出)
- Metrics dashboard + 报警规则(crash rate / SSE 保持时长 / adapter 日成功率 / patch 覆盖率)
- 回滚剧本与混沌演练
2026-04-18 23:19:49 +08:00
- 账号共享 / 桌面端主备的演进位置预留(命名软约定,不在本版 DDL 落名;见 §2 硬约束 9)
2026-04-18 21:05:41 +08:00
2026-04-18 22:12:53 +08:00
**退出目标**:patch 热更经过混沌演练;trace 能被外部 Playwright tooling 打开;运营指标全套接入报警;回滚剧本有实战演练记录。
2026-04-18 21:05:41 +08:00
---
## 13. 变更记录
2026-04-18 23:32:03 +08:00
- **v2.5( 2026-04-18) **: v2.4 final-verification 后的契约收尾——Codex 精准命中 5 个契约口子全部 close( Gemini 本轮已给 Ready-to-plan)。
- **Fix 1 · retry 语义**:§6.4 路由表 ` reconcile status=retry` 注释改成"原 task status → queued + attempts+1,不复制、不新建 job";§7.5 UNKNOWN 工作流表里"重新创建任务"改为"重新运行",后端改成 ` POST /reconcile status=retry`;§15.1 Mermaid 对应边加上 attempts+1 说明。三处一致。
- **Fix 2 · content_ref workspace 边界**:§6.8.2 增硬规则——tenant-native 实体(article_draft/template_gen/video/brand_asset)只允许 actor 的 primary workspace 引用,否则 422;该规则落到 Plan 0 退出 bar 的 cross-workspace 防穿透测试。
- **Fix 3 · WorkspaceScope 输入面**:删 §6.8.2 示例里的 ` workspace_id=` query,明确 scope 由 Actor 派生,不接受 query/body 选择。
- **Fix 4 · contract drift**:§6.4 ` GET /api/desktop/accounts?client=self` 返回字段补 ` delete_requested_at`;§6.8.2 步骤 6 retry 改 JSON body。
- **Fix 5 · parked Web cancel 端点**:§6.4 新增 ` POST /api/tenant/tasks/{id}/cancel`(对 parked 或 queued 任务主动取消 → ` aborted`),WorkspaceScope + 审计;§15.1 Mermaid 加 ` queued → aborted: Web cancel` 边,已有的 ` waiting_user → aborted` 标注具体端点。
2026-04-18 23:19:49 +08:00
- **v2.4( 2026-04-18)**:第四轮架构师视角交叉审查后的修订。**Codex verdict: Needs-rework**(剩余底座与一致性问题)、**Gemini verdict: Ready-with-caveats**。合计 1 Critical + 7 Warning。按"全修进 spec"方案逐条落地:
- **Plan 0 范围**( Codex C1 Partial):§12 **窄 workspace 化**策略写死——Phase 1 只在 ` desktop_clients / platform_accounts / desktop_tasks / desktop_publish_jobs / tenant_monitoring_quotas` 三条线上 workspace 一等化;articles/brands/kol-templates/publish_batches/images 仍保持 ` tenant == workspace`; WorkspaceScope middleware 仅挂在列表内的路由。列出 8 条 Plan 0 明确动作 + 3 类 exit bar。
- **Spec 一致性清扫**( Codex W3):删 §6.4 幂等契约里 ` pause/resume` 残留、改 §7.5 ` lease_client_id → target_client_id`、§6.6 ` platform_accounts` DDL 补 ` delete_requested_at` 列、§6.4 路由表新增 ` POST /api/tenant/tasks/{id}/reconcile` 与 ` POST /api/tenant/accounts/{id}/request-delete`、§14 术语表"Waiting-user"定义改为释放 lease + 严格 CAS 恢复、§13 v2.2 changelog 加注"以 v2.3 及以上为准"。
- **Parked / Reconcile 协议闭环**( Codex W2 Partial):§5.3 补"Parked 恢复协议"—— ` POST /tasks/{id}/lease?from_parked` 的严格 CAS SQL 示意 + 5 类错误情景 + 重复请求幂等规则;新增"target_client 离线/换机/revoke/72h 超时"5 类恢复策略表。Parked **从不自动 rebind**写死为核心原则。自动 abort 阈值从 24h 调到 72h 避免长假误杀。
- **Reconcile 归属**: ` /reconcile` 从 ` /api/desktop/*` 迁到 ` /api/tenant/*`( Web 对账归 Web,需 WorkspaceScope)。
- **Mermaid 图前移**( Gemini W10):§15.1 Lease 状态机 + §15.2 Account CAS resync **内联**到 spec,在 Plan 0 前画完(不再等 Plan A);§15.3 剩三张图保留 Plan A 期补。
- **Spike 矩阵操作化**( Codex W4):§12 补 Spike owner( runtime owner + provider owner 联签)、产物(` electron-chromium-cdp-matrix.md` + 抓包/截图)、**三态结论**( supported / degraded / blocked)、blocked 时 fallback 决策树(DOM-only 降级 / 移出承诺范围 / 等 Chromium 升级)。
- **Modal 重构分阶段**( Gemini C1):块 H 拆 H1( Plan A 第 1– 2 周:ui-shared 骨架 + design tokens + ` useAccountSelection` composable) + H2(第 4– 5 周:PublishArticleModal 基于 composable 重构)。
- **Design tokens 显式**( Gemini W2):H1 产物明确包含 CSS variables (light/dark) + Ant Design Theme Config。
- **credential_holder DDL 承诺撤回**( Codex W3 Partial):§2 硬约束 9 改成"命名软约定"——本版 DDL 继续 ` workspace_id / client_id / target_client_id`;§11 相应软化。
- **工期微调**: Plan A 5–6 周不变(H 拆两阶段但总量不变);总工期 28–33 周不变。
2026-04-18 22:12:53 +08:00
- **v2.3( 2026-04-18) **: Claude + Codex + Gemini 第三轮(架构师视角)交叉审查后的修订。**Codex verdict: Needs-rework**, **Gemini verdict: Ready-with-caveats**——两家合计 2 Critical + 9 Warning + 3 Info。按"全修进 spec + 新增 Plan 0 前置改造"方案落地。
- **C1 · Workspace 升成后端一等安全边界**: v2.2 的"新增 WorkspaceScope middleware"是局部补丁,实际需要 JWT claims / repo query / cache / 监控域全链条改造。新增 **Plan 0 · Workspace 底座一等化**(§12, 2– 3 周,阻塞 Plan A),覆盖 ` workspaces` + ` workspace_memberships` 表迁移、` Actor/Claims` 加 ` workspace_id`、所有 repo query 显式注入 workspace_id、` cache_support.go` key 改粒度、§6.1.1 监控域存量改造清单 8 行。§2 硬约束 8 重写 + 新增硬约束 9(演进缝:` credential_holder` / ` account_home` 命名预留)。
- **C2 · §5.2 CDP 路线改 "先 spike 后定案"**: v2.2 里 ` Fetch.continueResponse` + ` Fetch.takeResponseBodyAsStream` 连续调用语义互斥(CDP 文档明文:"Only available in response stage" + "the request can't be continued as is")。v2.3 改为:EventSource 走 ` Network.eventSourceMessageReceived`; fetch-stream 优先 ` Network.streamResourceContent + Network.dataReceived`( Chromium ≥ 120 已满足);仅"必须劫持/替换"才用 Fetch 且**不再连续调** continueResponse。Plan A 前置 **provider × transport 实测矩阵 spike**( 0.5 周),结果入 ` electron-chromium-cdp-matrix.md`。
- **W1 · Plan 依赖顺序调整**: Beta 退出门槛依赖 patch 通道,但 patch 通道在 Plan C——依赖矛盾。v2.3 把 **块 G 拆为 G1(最小闭环:签名 manifest + Ed25519 验签 + DSL 解释器 + revoke + rollback, 1.5 周)前移到 Plan B**,G2(备用隔离执行 + 根密钥轮换 + TUF 全要素,1 周)留在 Plan C。§10.1 Beta 门槛相应加备注。
- **W2 · ` waiting_user` 从 lease 拆成 parked state**: lease 适合短时占有,不适合 2 小时人审。v2.3 在 ` readyToReview` 进入时 **释放 lease**( ` active_attempt_id=null`)、状态置 ` waiting_user`、任务仍绑定 ` target_client_id` 不被抢;审核通过时 client 走 ` POST /tasks/{id}/lease?from_parked=true` 重新领取新 attempt; parked 超 24h 自动 abort。§5.3 / §6.4 / §6.7 同步,删 ` /pause` + ` /resume`,改 ` /park` + ` /lease?from_parked`。
- **W3 · 演进缝预留** → §2 硬约束 9 明写 ` credential_holder` / ` account_home` 抽象;Plan C 含演进位置预留。
- **W4 · 监控域存量改造清单** → §6.1.1 新小节,列 8 行改造盘点(迁移 / Repo / Service / Transport / Projection / Dashboard / Seed / 定时清理)。
- **W5 · 深链 nonce 一次性消费 + client 绑定**:改为短期 opaque code(非 JWT),Redis 存 jti,绑 ` (task_id, target_client_id, actor_user_id)`, ` /links/verify` 成功后立即 delete 失效;同机重放被 client_id mismatch 拦下(§6.8.7)。
- **W6 · PublishArticleModal 重构**: Gemini 实测 ` apps/admin-web/src/components/PublishArticleModal.vue` 的 ` accountCards` 只取 ` accounts[0]`——要改成账号级多选需彻底重写。§10.3 块 H 工期 1.5 → 2.5– 3 周。
- **W7 · ` MarkdownPreview.vue` 抽 ` packages/ui-shared`**: Web 发布前预览与 Desktop manual-review 用同一渲染器,避免不一致(§3.3)。
- **W8 · ` packages/http-client` 新增 ` SseClient` 类**:标准化指数退避重连 + ` Last-Event-ID` + 首帧 snapshot + 事件去重;两端共用(§3.3)。
- **W9 · Trace Viewer sandboxed iframe + hash 路由**: ` createWebHashHistory` 适配 ` file://` 加载;viewer 跑在 CSP sandbox iframe 中,trace 数据通过 ` postMessage` 传入,隔离恶意 DOM(§5.6)。
- **I1 · LLM 命名 & 产品入口修正**: **Hunyuan → Yuanbao 元宝**( ` yuanbao.tencent.com` 才是对话入口,不是 ` hunyuan.tencent.com`);新增 **§14 适配器映射表附录**( adapter_id / 公司 / 模型家族 / 产品名 / 官方域名 / 登录方式 / transport 观察)。
2026-04-18 23:19:49 +08:00
- **I2 · UNKNOWN 任务 UI 对账工作流**:§7.5 加 ` POST /api/tenant/tasks/{id}/reconcile` 端点 + 四种用户动作(已成功 / 已失败 / 忽略 / 重新创建);超 7 天未处理触发站内通知。
2026-04-18 22:12:53 +08:00
- **I3 · 新增 §15「需要补的图」**:列 5 张 Mermaid 图(全链路时序 / lease 状态机 / Account CAS resync / 多实例部署 / 前端组件层级),Plan A 期补齐放 ` diagrams/`。
- **工期重估**:§10.3 从 9 块扩到 10 块(加块 0);总工期 24–28 周 → **28– 33 周**( Plan 0 新增 2– 3 周 + 块 H 工期修正 +1 周 + G 拆前后期)。§12 从 3 plan 拆成 4 plan。
2026-04-18 21:48:02 +08:00
- **v2.2( 2026-04-18) **: Claude + Codex + Gemini 对 v2.1 的二轮交叉审查后的修订。修复 4 Critical + 9 Warning。
- **C1 多账号路由语义落 schema**:§6.6 ` desktop_tasks` 新增一等列 ` job_id / target_account_id / target_client_id / platform` + 相关索引;新建 ` desktop_publish_jobs` 聚合表;§6.3 SSE 只向 ` target_client_id` 推送,lease 仅允许目标 client 原子领取;错 client 会被服务端拒绝。
- **C2 Account 一致性 CAS + Tombstone**:§4.1 Account 增 ` syncVersion / deletedAt / tags`;§6.6 ` platform_accounts` 增 ` sync_version / deleted_at / tags` 列与条件唯一索引;§6.8.6 全文重写为 CAS(` if_sync_version`) + tombstone( soft delete)协议 + resync 三方合并规则 + 并发案例表;新增 ` POST /api/tenant/accounts/{id}/request-delete` 由 Web 写删除意图、client 坐实的两段式删除。
- **C3 ` /api/tenant/events` 鉴权链**:§6.4 明确要求新增 ` WorkspaceScope` middleware(现有 ` TenantScope` 不足);topic 必须服务端按 ` actor + workspace_id` 派生,不接受 query param 自由选择;两通道对等契约表(多实例 fanout / 首帧 snapshot / ` Last-Event-ID` replay / 事件 id 单调递增 / 客户端去重)放在 §6.8.4。
- **C4 与现有 ` /api/tenant/media/*` 的迁移表**:§6.8.4 新增完整迁移映射表,列出 ` media/platform-accounts` 删除、` articles/:id/publish-batches` → ` publish-jobs` 重命名、` publish-records` → ` publish-jobs/{id}` 子任务视图等。
- **W1** §6.8.4 SSE 契约对等(见 C3 合并)。
- **W2** §5.2 传输表加 **gRPC-Web / binary-framed fetch-stream** 分支(Kimi 实测路径 ` /apiv2/kimi.gateway.chat.v1.ChatService/Chat`);§4.4 两家 LLM 行补登录方式(Kimi:微信扫码 / 手机号;混元:微信/QQ/邮箱 + loopback 检查);保活策略改为"接入时以实际 transport 为准"。
- **W3** §6.3 + §6.8.5 明确 offline→online 双向夹逼补领:server 在 presence 从无到有时主动扫描 queued 重推;client 在 heartbeat+resync 后立即 ` lease(limit=20)` 循环到空。
- **W4** §6.4 补 ` GET /api/desktop/accounts?client=self` 端点(resync 所需)。
- **W5** §4.1 Account 加 ` tags` 字段;§6.8.2 Modal 支持按 tag 多选 + 搜索框 + 三级批量勾选。
- **W6** §6.8.5 增 ETA / 队列深度 / 容量反馈:` /publish-jobs/{id}` 返回 ` queue_ahead` + ` eta_seconds`; Modal 显示"前面还有 N 条 · 预计 M min 后开始"。
- **W7** §6.8.7 深链加中间态 modal + 手动启动 / 复制链接 fallback; ` georankly://tasks/<id>/review` 带一次性 nonce( 60s TTL JWT),通过 ` POST /api/desktop/links/verify` 换取凭证;补 Windows/mac/Linux 协议注册方式说明。
- **W8** §6.8.2 ` content_ref` 改多态:` {type: 'article_draft'|'template_gen'|'video'|'brand_asset', id: ...}`,适配 KOL 模板 / 品牌素材等多种素材来源。
- **W9** §6.8.4 ` /publish-jobs/{id}/retry` 改 JSON body ` {"task_ids": [...]}`(不用 query string)。
- **Info**:§10.3 标题 ` 4 块` → ` 9 块`;§12 开头 22– 26 → 24– 28(与 §10.3 一致);§6.8.3 统计卡加"🔴 待关注账号"与"按平台汇总"视图;监控计划 UI 改 Grid/Tag 多选布局为未来扩充留余地。
2026-04-18 21:22:38 +08:00
- **v2.1( 2026-04-18) **:用户增量反馈后的小修订。
- **LLM 监控 3 家 → 5 家**:新增混元(Tencent Hunyuan)、Kimi( Moonshot),§4.4 保活表增行(两家默认走 8 min 主动保活,实际节奏接入时复测);§10.3 块 B 估时 2–3 周 → 4–5 周;总工期 22–26 周 → 24– 28 周;§12 Plan B 范围更新。
- **新增 §6.8「admin-web 协同」**:覆盖发布 Modal 账号级多选语义、媒体数据面板(账号级粒度)、` /api/tenant/*` 聚合与 SSE 通道、离线降级 UX、Account 数据一致性(client = existence SoT / server = 投影)、` georankly://` 深链接。明确 ` /api/tenant/events` 与 ` /api/desktop/events` 是两条独立 SSE 通道。
- **发布 Modal 选号粒度固化为账号级**(非平台级):一个 job 展开为 N 条 desktop_tasks,每条绑定一个 ` (platform, account)`。
2026-04-18 21:05:41 +08:00
- **v2( 2026-04-18) **: Claude + Codex + Gemini 三方交叉审查后的修订版。主要修订:
- **C1** 修正 CDP 流式 SSE 抓取方法(§5.2),改为按 EventSource / fetch-stream 分流;维护 Electron Chromium CDP 版本矩阵。
- **C2** Patch 通道默认改为**纯数据 DSL**(无 JS 执行),可执行代码作为备用通道并限定在 ` utilityProcess + isolated-vm` 沙箱;引入 TUF 风格 manifest + patch_seq + 根密钥轮换 + 撤销机制(§5.5)。
- **C3** 补齐 SSE 多实例协调方案:RabbitMQ 广播 + Redis presence registry, SSE 仅推通知、任务归属由 lease endpoint 原子决定(§6.3)。
- **C4** ` platform_accounts` 新增 ` platform_uid` + 唯一索引 ` (platform, platform_uid)`,落实硬约束 8(§6.6)。
2026-04-18 23:19:49 +08:00
- **C5** 任务协议引入 ` attempt_id + lease_token` + extend/cancel; manual 模式引入 ` waiting_user` 状态(§5.3/§6.4/§6.6/§6.7)。*(注:v2.2 原设计含 pause/resume 并延长租约,v2.3 修订为 park + lease-from-parked,释放 lease 改 parked state;以 v2.3 及以上为准)*
2026-04-18 21:05:41 +08:00
- **C6** §3.3 重写工作量与可复用性口径,显式列出 7 家 detect-only 发布适配器 + DeepSeek 监控占位;§10.3 拆 9 个工作块重估总时;§12 新增 3-plan 拆分建议。
- **W1** ` BrowserView` 全局替换为 ` WebContentsView`。
- **W2** autoUpdater 配置名修正 ` downgrade` → ` allowDowngrade`;加 N-1 手动下载链接 + pre-release updater smoke(§8.4/§9.7)。
- **W3** Client Token 解绑 ` client_version/os/cpu_arch`,新增 ` /rotate` 和 ` /revoke`(§6.2/§6.4)。
- **W4** Patch 签名链补全 manifest 字段、根密钥轮换、运行前 hash 二次校验、revoke 指令(§5.5)。
- **W5** ` safeStorage` 启动时检测可用性;Linux ` basic_text` 降级告警;跨机迁移改为独立的 ` vault-export` 用户交互式流程(§4.5)。
- **W6** 崩溃恢复按 ` task.kind` 分治:monitor 幂等重跑,publish 按 ` submitting` 边界决定人工对账(§7.5)。
- **W7** 保活引入 per-(platform,account) token bucket(业务+保活+探活同桶);429/captcha 指数退避;resume 错峰 warm-up(§4.4)。
- **W8** ` apps/admin-web` 不再被 renderer 直接复用;抽 ` packages/ui-shared` 作为共享包;renderer 是极简 Vite 项目(§3.3)。
- **W9** 发布状态机加 ` failed / aborted / unknown` 终止态(§5.3)。
- **W10** §6.1 显式列出 sqlc 重生成与 Service/Handler 受影响面。
- **W11** Trace 拆 Alpha(结构化日志 + 失败截屏)vs Phase 2( CDP 全事件流 + Playwright 兼容导出)(§5.6)。
- **W12** §9.2 + §9.7 新增 autoUpdater 核心逻辑单元测试 + updater smoke 自动化覆盖。
- **Info 合并**: ` view-pool` Hot/Cold 分档、` proxy-helper` 新建、登录 1.5s 遮罩层、时区明确为本地、配额默认值、` vault-export` 备份流程、universal Mac 原生依赖、arm64 runner 兜底、平台 ToS / Electron CVE / Win Defender 三条新风险(§3.1/§4.2/§4.4/§4.5/§5.4/§8.2/§8.3/§11)。
2026-04-18 20:28:42 +08:00
---
2026-04-18 22:12:53 +08:00
## 14. 附录:LLM 适配器映射表(I1)
截至 2026-04-18 的对外产品入口与传输观察:
| adapter_id | 公司 | 模型家族 | 消费端产品名 | 官方域名 | 登录方式 | 观察到的 transport |
|---|---|---|---|---|---|---|
| ` doubao` | 字节跳动 | Doubao / 云雀 | 豆包 | ` www.doubao.com` | 微信扫码 / 手机号 / 抖音 | fetch-stream( event-stream)· 已有实测基础 |
| ` qwen` | 阿里 | Qwen / 通义千问 | 通义 | ` tongyi.aliyun.com` | 支付宝 / 钉钉 / 手机号 | fetch-stream,带 ` bx-ua / x-xsrf-token` 等短期 token |
| ` deepseek` | DeepSeek | DeepSeek | DeepSeek | ` chat.deepseek.com` | 邮箱 / 手机号 / 微信 / Google | 待 spike(旧插件是 default placeholder) |
| ` yuanbao` | 腾讯 | Hunyuan 混元 | 元宝 | ` yuanbao.tencent.com`( **不是** hunyuan.tencent.com) | 微信扫码 / QQ / 邮箱(微信涉及 loopback 检查)| 待 spike |
| ` kimi` | Moonshot | Kimi | Kimi | ` www.kimi.com` | 微信扫码 / 手机号快捷登录 | **gRPC-Web binary-framed**( ` POST /apiv2/kimi.gateway.chat.v1.ChatService/Chat`),需 framer 解包 |
这些条目在 Plan A spike 矩阵后更新为稳定值并记入 ` docs/superpowers/specs/electron-chromium-cdp-matrix.md`。
2026-04-18 23:19:49 +08:00
## 15. 附录:架构图
2026-04-20 09:52:48 +08:00
### 15.1 任务状态机(inline · v2.6 当前口径)
2026-04-18 23:19:49 +08:00
` ``mermaid
stateDiagram-v2
[*] --> queued: POST /publish-jobs
queued --> in_progress: lease acquired (target_client_id match)
2026-04-18 23:32:03 +08:00
queued --> aborted: POST /api/tenant/tasks/{id}/cancel (Web)
2026-04-18 23:19:49 +08:00
in_progress --> in_progress: extend (每 60s)
in_progress --> succeeded: POST /result status=succeeded
in_progress --> failed: POST /result status=failed
2026-04-18 23:32:03 +08:00
in_progress --> aborted: POST /api/desktop/tasks/{id}/cancel (client)
2026-04-18 23:19:49 +08:00
in_progress --> unknown: crash during submitting (publish only)
unknown --> succeeded: POST /api/tenant/tasks/{id}/reconcile status=succeeded
unknown --> failed: POST /reconcile status=failed
unknown --> aborted: POST /reconcile status=aborted
2026-04-18 23:32:03 +08:00
unknown --> queued: POST /reconcile status=retry (原 task 回 queued + attempts+1)
2026-04-18 23:19:49 +08:00
succeeded --> [*]
failed --> [*]
aborted --> [*]
note left of unknown
仅 publish 任务
仅 submitting 崩溃
monitor 任务直接重跑
end note
2026-04-20 09:52:48 +08:00
note right of succeeded
Desktop Publish Management
可对 succeeded/failed/unknown/aborted
手动再次发送
服务端会新建下一条 queued publish task
end note
2026-04-18 23:19:49 +08:00
` ``
### 15.2 Account CAS + Tombstone Resync( inline · v2.4 在 Plan 0 前画完)
` ``mermaid
sequenceDiagram
autonumber
participant A as Client A (original holder)
participant S as Server DB
participant W as admin-web (Web UI)
Note over A,S: T0 稳态:acc-x sync_version=5
A->>S: POST /accounts {..., if_sync_version: null} (首次登录)
S-->>A: 200 {sync_version: 5}
Note over W: T1 用户在 Web 误点"请求删除"
W->>S: POST /api/tenant/accounts/{id}/request-delete
S-->>W: 200 {delete_requested_at: now, sync_version: 6}
S->>A: SSE /api/tenant/events 推 delete_requested 事件
Note over A: T2 并发:A 正在改 tags
A->>S: PATCH /accounts/{id} {tags: [...], if_sync_version: 5}
S-->>A: 409 Conflict (current_version=6)
Note over A: T3 A 本地 resync
A->>S: GET /api/desktop/accounts?client=self
S-->>A: [{id, sync_version: 6, delete_requested_at: now, ...}]
A->>A: UI 询问用户:要不要真的删除?
alt 用户撤销
A->>S: POST /api/tenant/accounts/{id}/request-delete?undo=true
S-->>A: 200 {delete_requested_at: null, sync_version: 7}
Note over A,W: 继续正常使用
else 用户确认删除
A->>A: 清 partition / cookie
A->>S: DELETE /accounts/{id}?if_sync_version=6
S-->>A: 200 {deleted_at: now, sync_version: 7}
S->>S: cascade: task.status='aborted' for pending
S->>W: SSE 推 account.deleted
end
` ``
2026-04-18 22:12:53 +08:00
2026-04-18 23:19:49 +08:00
### 15.3 Plan A 期补齐的其他图
2026-04-18 22:12:53 +08:00
2026-04-18 23:19:49 +08:00
以下放在 ` docs/superpowers/specs/diagrams/` 目录:
2026-04-18 22:12:53 +08:00
2026-04-18 23:19:49 +08:00
1. **全链路时序图**( sequenceDiagram)——Web 点击发布 → Server 入库 → SSE 通知 → Desktop 领取 → 适配器执行 → 结果回传 → Web 更新。
2. **多实例部署架构图**——RabbitMQ fanout + 多个 server 实例 + Redis presence registry + 多个 desktop client。
3. **前端组件层级图**( admin-web 与 desktop renderer 共享 ` packages/ui-shared` 的依赖关系)。
2026-04-18 22:12:53 +08:00
## 16. 术语表
2026-04-18 20:28:42 +08:00
2026-04-18 21:05:41 +08:00
- **Patch 通道**:仅下发适配器的声明式数据与受限 DSL(默认)或隔离沙箱执行的纯函数(备用),分钟级生效,不重启。
2026-04-18 20:28:42 +08:00
- **Release 通道**:完整二进制 + 差分升级,通过 electron-updater。
- **探活(probe)**:加载主页 DOM 判断登录态,低代价。
- **保活(keep-alive / heartbeat) **:主动访问平台私有 API 触发 token 刷新。
2026-04-18 21:05:41 +08:00
- **Client Token**:一台 Electron 安装的后台身份,与 User JWT 并列;解耦 client_version,支持 rotate/revoke。
2026-04-18 20:28:42 +08:00
- **Partition**: Electron ` session.fromPartition('persist:<id>')`,每个账号一个隔离的 cookie/storage 容器。
2026-04-18 21:05:41 +08:00
- **Lease**:任务租约。领取任务时服务端返回 ` attempt_id + lease_token + lease_expires_at`;后续所有写操作必须带 ` lease_token` 验证。
2026-04-20 09:52:48 +08:00
- **发布管理(Publish Management) **: desktop client 里的发布运维页,展示当前 client 的发布队列与历史发送结果。支持查看 ` queued / in_progress / succeeded / failed / unknown / aborted` 任务,并基于任一历史 publish task 重新创建一次新的发送任务。
2026-04-18 21:05:41 +08:00
- **Platform UID**:平台侧真实账号 ID(如微信 fakeid、B 站 mid、知乎 hash_id);` (platform, platform_uid)` 在 DB 层唯一,落实硬约束 8。
- **DSL( Patch 默认通道)**:一套受限的声明式解析语言,只能做字符串/JSON 解析与模式匹配,无 JS 执行能力。
- **Presence registry**:多实例服务端共享的 ` client_id → (instance_id, conn_id)` 注册表,用 Redis TTL 30 s 维护;用于跨实例路由 SSE 通知。