Files
geo/docs/superpowers/specs/2026-04-18-electron-desktop-client-design.md
T
root ef978ddbbf docs(desktop-client): spec v2.3 from architect-level review
Third-round Claude+Codex+Gemini architect review surfaced foundation-
level issues that prior local patches missed. Codex verdict was
Needs-rework; Gemini verdict was Ready-with-caveats. v2.3 lands all
2 Critical + 9 Warning + 3 Info.

Critical:
- C1 workspace-as-first-class: Prior drafts added a WorkspaceScope
  middleware, but Codex verified code: Actor/Claims carry only tenant_id,
  cache_support keys scope to tenant, workspaces table migration does
  not exist, tenant_monitoring_quotas tracks primary_installation_id.
  Add a new Plan 0 (Workspace foundation uplift, 2-3w, blocks Plan A)
  covering workspaces + memberships migrations, claims extension,
  cache-key rewrite, and the full monitoring-domain migration checklist
  (new §6.1.1). §2 hard-constraint 8 rewritten; new hard-constraint 9
  reserves credential_holder / account_home naming for future evolution.
- C2 CDP stream-capture path was still inconsistent: Fetch.continueResponse
  + takeResponseBodyAsStream cannot chain per CDP semantics. v2.3 moves
  EventSource to Network.eventSourceMessageReceived, fetch-stream to
  Network.streamResourceContent + dataReceived (Chromium >=120, OK on
  Electron 41), and reserves Fetch only for must-mutate scenarios
  without chained continueResponse. Plan A now requires a provider x
  transport spike matrix (0.5w) before any adapter work.

Warning:
- W1 Patch-channel minimal loop (G1) promoted from Plan C to Plan B,
  so Beta exit criteria are satisfiable.
- W2 waiting_user decoupled from lease: becomes a parked state that
  releases the lease, survives hours of human review, and is resumed
  via POST /tasks/{id}/lease?from_parked=true.
- W3 credential_holder/account_home evolution seam formalized.
- W4 §6.1.1 lists 8 monitoring-domain migration items that were
  previously hidden inside "sqlc rename".
- W5 Deep-link nonce becomes a one-time opaque code bound to
  target_client_id, Redis-jti consumed on verify.
- W6 §10.3 block H raised from 1.5w to 2.5-3w after Gemini verified
  PublishArticleModal's accounts[0] assumption needs a real rewrite.
- W7 MarkdownPreview extracted into packages/ui-shared so Web preview
  and Desktop manual review use the same renderer.
- W8 packages/http-client gains a standardized SseClient (backoff +
  Last-Event-ID + snapshot + dedup), shared by both apps.
- W9 Trace viewer now runs inside a CSP-sandboxed iframe with hash
  routing for file:// loads; trace data flows in via postMessage.

Info:
- I1 §14 adapter map appendix; Hunyuan renamed to Yuanbao with the
  correct entry domain (yuanbao.tencent.com).
- I2 UNKNOWN tasks gain a reconcile workflow (mark-succeeded /
  mark-failed / ignore / recreate), plus weekly in-app nudge.
- I3 New §15 listing the 5 Mermaid diagrams Plan A must produce
  (sequence, lease state machine, account resync, multi-instance
  topology, component tree).

Total effort re-estimated from 24-28w to 28-33w; split expands from
3 plans to 4 (Plan 0 -> A -> B -> C).

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

91 KiB
Raw Blame History

Electron 桌面客户端设计(全面替换 WXT 浏览器插件)

状态Draft v2 · 待实施(v1 经 Claude + Codex + Gemini 三方交叉审查后修订) 作者@liangxu + Claudecodex/gemini 交叉审校) 日期2026-04-18 范围:新增 apps/desktop-client//api/desktop/* 服务端路由、相关 DB schema;删除 apps/browser-extension//api/plugin/* 路由及相关代码。

v2 主要修订:修正 CDP 流式抓取方法(C1)、Patch 通道沙箱机制(C2)、SSE 多实例协调(C3)、workspace 独占 DB 落地(C4)、任务租约协议(C5)、老适配器工作量重估(C6);以及 12 条 Warning 逐项落入文档。详见 §13 变更记录。


1. 背景与动机

现有 apps/browser-extension/WXT MV3 Chrome 扩展,"GEO Publisher")同时承担两类工作:

  1. 向 13 家中国媒体平台做内容分发(微信公众号、头条号、百家号、搜狐号、网易号、ZOL、懂车帝、知乎、企鹅号、简书、掘金、B 站、什么值得买)。
  2. 监控主流 LLM(豆包、千问、DeepSeek、混元、Kimi)在给定查询下的输出,判定品牌 是否被引用。

当前问题:

  • LLM 抓取不稳定。监控模块依赖 DOM 选择器与 webRequest.onBeforeRequest 在 MV3 下受限,无法可靠拿到 SSE 响应 Body;平台 UI 一变就挂。
  • 适配器受 MV3 约束。后台 service worker 频繁休眠,长任务难以保证完成;跨平台复杂登录(QR、滑块、SMS)在扩展内体验差。
  • 不可扩展到真正的系统级自动化。系统托盘、开机自启、多账号并发 session 隔离、进程级 CDP 调试,这些 MV3 都做不到。
  • 现有适配器实现覆盖不全。仓库实测:发布侧 13 家里 weixin_gzh / juejin / dongchedi / smzdm / wangyihao / bilibili / zol7 家 仍返回 unsupportedPublishResultdetect-only 占位);监控侧 DeepSeek 是 defaultAdapter 占位没实现 query。这部分要从零实现,不是迁移。

因此:全面替换为一个 Electron 桌面客户端,承担上述全部职责。浏览器扩展停止维护并下线。


2. 设计硬约束(已确认)

  1. 全面替换,不做"先 monitoring 后 publishing"的分阶段替换。
  2. 不做向后兼容。因为是开发版本阶段,服务端协议、DB schema、认证字段按桌面客户端的最佳形态重新设计,不为插件保留旧字段或旧路由。
  3. 登录走嵌入式 Web View。用户在 Electron 真实平台登录页里自己完成扫码/密码/短信/滑块;我们只保存 cookie,不保存账号密码。
  4. Cookie 本地存储 并加密Electron safeStorage)。服务端永不存储 Cookie 或任何平台凭据。Linux 上 safeStorage 可能退化为 basic_text,必须启动时检测并显式降级告警(见 §4.5)。
  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(不支持跨租户共享账号)。该约束必须落到 DB 唯一约束与 API 级冲突校验,不依赖客户端自觉(见 §6.6 platform_accountsplatform_uid 字段 + 唯一索引)。前置条件:workspace 必须先升成后端一等安全边界(见 §12 Plan 0);仅靠新增 middleware 不够,因为现有代码里 Actor/Claims/cache/monitoring 多处仍按 tenant 粒度(v2.2 三轮审查发现的底座问题)。
  9. 演进缝预留:短期 一账号 = 一 workspace = 一台 client 是产品边界,不是代码硬写;DB 列名采用 credential_holder_workspace_id / account_home_client_id 风格而非 owner_*,为将来"共享运营账号池 / 桌面端主备"留演进位置(见 §6.6 DDL 注释)。
  10. 要求最小 electron 体积包

3. 架构总览

3.1 进程拓扑

单 Electron 进程树(方案 A)。所有视图宿主统一用 WebContentsViewElectron 已把 BrowserView 标记为 deprecated)。

  • 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 分钟有任务或保活的账号)常驻 WebContentsViewCold-tier 账号只保留 session partition 在磁盘,WebContentsView lazy-create。阈值默认 30 min,可配置。避免 10 账号场景下 RAM 被固定占用。
  • 仅在实测 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 → (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 预算
    circuit-breaker.ts    # 平台级熔断
    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(备用通道)
    tracing/
      recorder.ts         # 结构化日志 + 关键截屏(Alpha 范围)
      trace-writer.ts     # 轻量 zipAlpha 仅 JSON 日志 + 截屏)
      redactor.ts         # Cookie / Authorization / password 字段自动脱敏
      # Playwright 兼容导出作为 Phase 2 能力,见 §5.6
    transport/
      api-client.ts       # 复用 packages/http-client,注入 client_token
      sse-client.ts       # /api/desktop/events 长连接 + 60s pull 兜底
  preload/
    bridge.ts             # contextBridge 窄 API
  renderer/               # 极简 Vite 项目,仅消费 packages/ui-shared
    main.ts
    routes.ts
    views/                # 桌面端专属视图(账号管理/任务中心/事件中心/诊断)

3.3 关键复用(更务实的口径)

可复用

  • packages/shared-types:平台枚举、任务类型、DTO。需要扩展:加入租约、platform_uid、patch manifest 等新类型。
  • packages/http-clientHTTP 基座直接用;v2.3 新增 SseClient 类——内置指数退避重连、Last-Event-ID 处理、首帧 snapshot 消费、事件去重。Web 和 Desktop 两端共用。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 系统。
  • 老适配器里的选择器字符串业务流程描述("打开编辑器 → 填标题 → 粘正文 → 上传封面 → 点发布"这种语义)。

不可复用、必须重写或新建

  • 所有适配器代码——老代码硬编码 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 抽取——全部新建。

4. 账号 & 会话模型

4.1 数据结构

type Account = {
  id: string                 // uuid,本地生成
  platform: PlatformKind     // 'wechat' | 'toutiao' | ... | 'doubao' | 'qwen' | 'deepseek'
  platformUid?: string       // 平台侧真实账号 ID / OpenID / UIN;登录探针抓取
  accountFingerprint?: string // 额外防伪:昵称/手机号尾号/账号主页 URL hash
  purpose: 'publish' | 'monitor'
  displayName: string        // 用户自填,仅作本地展示
  partitionKey: string       // 'persist:acc-<id>'Electron session 隔离
  proxy?: ProxyConfig        // 可选 session-level 代理
  createdAt: Date
  lastSeenAt: Date
  verifiedAt?: Date          // 上次成功 probe 的时间
  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       // 默认 60/小时,含业务+保活+探活合计
    usedThisHour: number
    resetAt: Date
  }
  tags?: string[]              // 用户可自定义分组标签(如"品牌号"/"矩阵号"/"测试号"),Modal 按 tag 批量勾选
  syncVersion: number          // 单调递增,每次写入 +1,用于 client-server 一致性 CAS
  deletedAt?: Date             // tombstone,软删除,resync 时用于判断 "server 已删"
}

type ProxyConfig = {
  scheme: 'http' | 'socks5'
  host: string
  port: number
  auth?: { user: string; pass: string }  // 存入 vault,不明文落盘
}

4.2 登录流程

  1. 用户在 UI 点「新增账号 → 选平台」。
  2. 主进程 session-registry.create(platform) 分配新 partition,打开一个可见 BrowserWindow(登录专用),加载该平台登录页。
  3. 用户自己用扫码 / 密码 / 短信 / 滑块登录。
  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 分配 WebContentsViewcold-tier 仅保留 partition;按需激活 CDP。

4.3 多账号并存

同一平台多账号完全隔离——partition 不同、cookies 不同、UA 不同、可通过 session 级 proxy 走不同 IP。代理来自 Account.proxy 字段,由 _shared/proxy-helper.ts 统一调用 ses.setProxy(§5.4)。调度器按账号维度限流(默认同一账号并发 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
元宝(腾讯) 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/ChatgRPC-Web binary-framed非 EventSourceparser 需先 framing 解包,见 §5.2 接入时按 §5.2 spike 矩阵复测;默认主动保活 8 min + 30 min 探活

实施细节:

  • 错峰:同一平台 N 个账号按 hash(accountId) mod period 分布时间槽 + ±30s 抖动;当 N > 槽数时使用随机顺延。
  • 统一 token bucket:每个 (platform, account) 维度一只 leaky bucket,桶容量 = hourlyBudget业务任务、保活、探活同桶扣减——避免保活把业务流量配额吃掉或反之。
  • 优先级:业务任务 > 保活 > 探活;同槽冲突由优先级决定是否放行。
  • 失败升级:保活失败 1 次立即重试;连 2 次失败降级为探活一次确认状态;探活失败则 health: expired,托盘红点 + OS 通知 → 用户点击弹登录窗(partition 复用,不需要重建账号)。
  • 429/验证码退避:任何响应识别为 429/风控页/captcha 时,该账号进入指数退避(基础 30s,因子 2,上限 30 min),期间完全暂停保活,只保留"按需激活"(业务任务到来时才 pre-check)。
  • 静音时段:默认 0:007:00用户系统本地时区)不保活,可配置关闭。
  • 休眠感知powerMonitor.on('suspend'|'resume'),休眠暂停全部保活;唤醒后错峰 warm-up——按 hash(accountId) 分片,每 10s 一批探活(每批 ≤3 账号),避免同步流量尖峰。
  • 配额池:同一 LLM 多账号时,调度器按"最近使用 + 剩余配额"挑账号。

4.5 凭据粒度与备份

  • 保存账号密码。
  • 只保存 Cookie + 平台侧必要 token(如 Doubao 的 ms_tokenfp)。
  • safeStorage + SQLite本机恢复缓存:userData 被清/重装应用可恢复,不保证可迁机器。
  • 跨机器迁移走独立的用户交互式导出流程(vault-export.ts):本机解密后用用户输入的口令二次加密导出为 .vault 文件;新机器同样输入口令解密、用新环境的 safeStorage 重加密落盘。导出的 .vault 文件不会自动上传服务端。
  • 服务端永远不持有任何形式的 cookie 备份(硬约束 4)。

5. 适配器模型

5.1 接口定义

interface AdapterContext {
  accountId: string
  session: Session
  view: WebContentsView             // 统一使用 WebContentsView
  debugger: DebuggerClient
  rateLimit: RateLimitHandle        // 业务调用前 acquire
  logger: ScopedLogger
  signal: AbortSignal
  reportProgress: (stage: string) => void  // 供状态机向 lease-manager 报进度
}

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 监控适配器:流式 SSE 抓取技术路径(先 spike 后定案

v2.2 review 发现 Fetch.takeResponseBodyAsStreamFetch.continueResponse 在 CDP 语义上互斥:前者独占响应 body,后者让页面继续消费。因此不能写成"连续调用链"。v2.3 改为"分流 + 实测矩阵 + spike gate"。

总方针:不同平台用不同 domain不劫持能不劫持(劫持会阻塞页面继续跑),只在必须看响应 body 时才用 Fetch 劫持。

按传输类型分流(每条路线的可用性需在 Plan A 先做 spike 矩阵确认):

页面使用的机制 推荐 CDP 路径 约束
new EventSource(url) Network.enableNetwork.eventSourceMessageReceived 监听每条 message 仅监听、不劫持。最干净。
fetch(url) + text/event-stream + streaming body Network.enableNetwork.streamResourceContent(requestId) + Network.dataReceived 事件流式拼接 Chromium ≥ 120Electron 41+ 满足)。仅监听、不阻断页面。
fetch(url) + chunked transfer + 自定义协议 同上 streamResourceContent + dataReceived 同上
gRPC-Web / binary-framed fetch-streamKimi /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.requestPausedFetch.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 的实现细节

Chromium/CDP 兼容矩阵文件docs/superpowers/specs/electron-chromium-cdp-matrix.md(Plan A 阶段新建),记录每次升 Electron 大版本的 domain 可用性回归结果。截至 2026-04-18 Electron stable 41.2.0Chromium 146),上述三类 domain 全部可用。

实施步骤:

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 CDPfinally

Provider-specific parser:每家 LLM 的 payload schema 不同(豆包 {event: 'data', data: {text: ...}} 这种结构),放 adapters/doubao.ts 等私有文件。Schema 变化通过 Patch 通道下发新 parser(§5.5)。

5.3 发布适配器状态机 + 租约绑定

每个发布任务或每个账号带 PublishMode

type PublishMode =
  | { kind: 'manual'; requireUserReview: true }
  | { kind: 'auto' }

type PublishState =
  | 'filling'        // adapter 正在往编辑器填内容
  | 'readyToReview'  // manual 模式下等待用户点发布
  | 'submitting'     // 已经点了发布按钮,等平台回应
  | 'done'           // 平台明确返回成功
  | 'failed'         // 明确失败(可重试)
  | 'aborted'        // 用户或系统取消
  | 'unknown'        // 崩溃/超时/无法判断(人工对账,见 §7.5)

状态机与租约的绑定(见 §6 协议):

  • 进入 filling / submitting 时,适配器必须每 60 s 调 lease-manager.extend(lease_token) 续租(lease TTL 默认 10 min)。
  • 进入 readyToReview 时,人工审核可能持续几分钟到几小时,不能用 lease 续期表达。lease-manager 释放当前 lease,把服务端任务切到 waiting_user parked state
    • 任务归属仍绑定原 target_account_id + target_client_id不会被其它 client 抢走(因为 target_client_id 路由规则)。
    • 任务不持有活动 leaseactive_attempt_id = null),server 不会因 lease 超时回滚状态。
    • 用户点"发布"后,客户端重新获取 leasePOST /tasks/{id}/leasefrom_parked=true),进入 submitting
    • 用户跨重启恢复:同样的路径,因为 parked state 持久在 DB。
    • 服务端可选设置 "parked 超过 N 小时则自动 abort" 的 fallback(默认 24h,可配置)。
  • 用户取消或 signal abort → aborted
  • adapter 抛异常 → failed,带错误码。
  • 进程崩溃 → 启动时按 §7.5 分派为 unknown(仅限 publish 任务的 submitting 阶段)。

文件上传:protocol.handle('file') + CDP Input.dispatchDragEvent 或直接喂 <input type=file>,不经服务端中转。

5.4 公共能力

adapters/_shared/

  • debugger-helper.ts:封装 CDP attach/detach、Fetch/Network domain 选择、SSE 流式拼接、超时。
  • dom-helper.ts:语义化选择器、重试、容错。
  • upload-helper.ts:文件上传统一入口。
  • captcha-pause.ts:遇滑块/点击验证时把 WebContentsView 转前台让用户过,通过后续跑。
  • proxy-helper.ts(新):统一走 Session.setProxy(Account.proxy),支持 socks5/http + 凭据存 vault。

5.5 双轨更新:局部热更 + 全量(安全收紧)

通道 内容 节奏 形式
Patch 通道(默认·纯数据 DSL) 适配器的 selectors.json + parser.dsl(受限的声明式解析 DSL,非 JS 分钟级按需 服务端签名 manifest + 补丁包;客户端 DSL 解释器运行,无任意 JS 执行
Patch 通道(备用·隔离执行) 个别场景确需写 JS parser 时的"逃生舱" 仅在评审后发布 加载到 utilityProcess(无 Node 能力)+ isolated-vmvm.createContext + 冻结 global + 禁用 eval/Function/WebAssembly/dynamic import + 仅允许消息传递返回结构化结果
Release 通道 二进制、主进程、UI、适配器宿主框架 周/双周 electron-updater 差分包

默认走纯数据 DSL 通道——安全、可审、无 RCE 面。仅当平台响应结构复杂到 DSL 难以表达时,才考虑升级到备用通道,且每个 parser 必须经过安全评审。

签名链(TUF 思路简化版)

每个 patch 打包为 manifest.json + patch.datamanifest 至少包含:

{
  "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>"
}

客户端约束:

  • 内置 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)

5.6 Trace 基础设施(Alpha 范围 + Phase 2 兼容 Playwright)(不着急)

Alpha 范围(MVP

  • 结构化日志(pino JSON)滚动写入 userData/logs/
  • 任务失败时自动截屏 3–5 张 + 存 Network 响应摘要。
  • 本地内置极简 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 或主进程。
  • 脱敏:Cookie / Set-Cookie / Authorization / x-xsrf-token / password / 手机号 / email → <redacted>
  • 存储:userData/traces/<accountId>/<taskId>.trace.zip,配额默认 50 份500 MB(先到先清)。
  • 默认不上传服务端;用户手动"发送诊断包"才上传。

Phase 2Beta 之后)

  • Playwright Trace Viewer 格式兼容导出(为方便外部开发者用 npx playwright show-trace 打开)。因格式私有、跟随 Playwright 版本演进,仅作为导出选项。
  • CDP 事件流完整记录 + 2fps 截屏 + DOM 快照(内存 rolling buffer 30s,失败时 flush)。

6. 服务端协议

6.1 命名(全部重命名)

plugin_installations / /api/plugin/* / installation_token → 新 desktop_clients / /api/desktop/* / client_token。因为是开发版本、无生产流量,DB 迁移直接 drop 旧表、create 新表,不做导入或双写。

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 层单元测试

6.1.1 监控域存量改造清单(不是 rename 能完成的)

v2.2 review 发现监控子系统存量耦合比插件本身深。Plan A/B 前必须 spike 这张表:

层次 文件 / 表 现状 改造动作
迁移 server/migrations/20260410103000_create_monitoring_tables.up.sql tenant_monitoring_quotascollection_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_clientsprimary 概念在 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 示例
定时清理 (目前没有 tombstone / parked 清理) 新建:parked > 24h 的任务 auto-aborttombstone > 30 天的 account 清理

行动:Plan A 启动前用 0.5 周做一次 impact spike,核对以上每一行都有具体 owner、估时、依赖;没有确认就不开工。监控域存量改造是 Plan A 工期的隐藏成本。

6.2 认证

Token 代表 发放 用途
User JWT 人类用户 /api/auth/login 用户在 Electron 内的交互(新增账号、创建监控计划、看报表)
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 仅作为通知信道,不直接承载任务归属。

多实例部署下:

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?"
        ↓
   if target_client_id is online: push {type: 'task_available', task_id} via SSE to *that* client only
        ↓
   client POST /api/desktop/tasks/lease → server atomically verifies (target_client_id == requester) + claims task
        ↓
   server returns {task, attempt_id, lease_token, lease_expires_at}

要点:

  • 任务是定向的。每条 desktop_tasks 都有 target_client_id + target_account_id(§6.6);SSE 只推给 target_client_id,其他 client 看不到、lease 不到。
  • SSE 只推 task_available 信号,携带 task_id 而非任务正文,避免跨实例推送导致 payload 在错误客户端落地。
  • 任务归属由 POST /tasks/lease 原子决定DB UPDATE ... WHERE status='queued' AND target_client_id = :requester_client_id RETURNING ...,或 Postgres advisory lock)。即便被错误广播,非目标 client 的 lease 会被 target_client_id 检查拒绝。
  • 60 s pull 作为 SSE 失联兜底;和 SSE 都打同一个 lease endpoint,天然幂等。
  • presence registry:每个 server 实例维护本地 client_id → SSE connection,并把 (client_id, instance_id) 写到 RedisTTL 30 sSSE 心跳续期)。Scheduler 按 target_client_id 查 presence 决定是否广播或挂队。
  • offline→online 主动补领server 检测到某 client_id 从 offline 转 onlineRedis TTL 从无到有),扫描 desktop_tasks WHERE target_client_id = $1 AND status = 'queued',对命中的每条重推 task_availableclient 侧也会在 heartbeat 后进入 lease(kind?, limit=20) 循环到空,两头夹逼避免任务"永远待领"。

6.4 路由表

# 配对
POST /api/auth/login
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
POST /api/desktop/clients/metrics           # 轻量非敏感聚合指标

# 事件 & 任务租约
GET  /api/desktop/events                    # SSE, client_token
POST /api/desktop/tasks/lease               # {task_id?} 原子领取;kind=publish|monitor 过滤
POST /api/desktop/tasks/{id}/extend         # body: {lease_token},续租(短时占用用)
POST /api/desktop/tasks/{id}/park           # body: {lease_token, reason: 'waiting_user'|'captcha'} 释放 lease、状态 → waiting_user
POST /api/desktop/tasks/{id}/lease?from_parked=true # 从 parked 重新领取(不走 queue,指定回到 target_client
POST /api/desktop/tasks/{id}/cancel         # body: {lease_token}(或无 lease 时带 client_token 由 target_client 主动取消)
POST /api/desktop/tasks/{id}/result         # body: {lease_token, status, payload}
POST /api/desktop/tasks/{id}/trace          # 用户主动上传诊断包

# 账号元数据(不传 cookie
GET    /api/desktop/accounts?client=self    # resync 用:返回 {id, platform, platform_uid, display_name, health, client_id, sync_version, deleted_at, tags}
POST   /api/desktop/accounts                # upsert:以 (workspace_id, platform, platform_uid) 为键;body 带 if_sync_version 做 CAS
PATCH  /api/desktop/accounts/{id}           # 改 displayName / health / quota / tagsbody 带 if_sync_version 做 CAS
DELETE /api/desktop/accounts/{id}?if_sync_version=<n>  # 软删除写 tombstone,非 hard delete

# Patch 通道
GET  /api/desktop/patches/manifest          # 返回当前适用的 manifest 列表 + revoke 指令

# 管理 UIWeb / Electron UI 共用)
GET  /api/tenant/monitoring-plans
POST /api/tenant/publish-jobs
# ...

幂等契约:所有写操作(lease/extend/pause/resume/cancel/result必须携带 lease_token;服务端校验 lease_token 与当前 active_attempt_id 绑定,否则返 409 Conflict。客户端收到 409 = 租约已失效,停止动作 + 提示人工对账

中间件与 workspace 一等化要求先决条件,不是实施细节):

v2.2 review 发现现有 server/internal/shared/auth/claims.go 只有 user_id/tenant_id/roleserver/internal/shared/middleware/tenant_scope.go 只注入 tenant_idserver/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_idclient_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 隔离承诺才能真正落地。

6.5 服务端边界

服务端只存任务意图"账号 A 去豆包问 Q")与结果("答案文本 + 品牌是否出现")。永不存 Cookie、永不直连平台帮客户端跑。客户端失联 = 任务排队;长时间拿不到 = 超时失败。硬边界,防止 Cookie 从客户端外泄。

6.6 DB Schema(关键表)

-- 客户端身份
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
);

-- 平台账号(workspace 独占约束 + 一致性协议)
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,
  tags JSONB,                       -- 用户自定义标签数组,Modal 分组筛选用
  -- 一致性协议(见 §6.8.6
  sync_version BIGINT NOT NULL DEFAULT 1,   -- 单调递增,每次写入 +1
  deleted_at TIMESTAMPTZ,                   -- tombstone,软删除;非 NULL 时 Modal 和 /accounts 默认过滤掉
  created_at TIMESTAMPTZ NOT NULL,
  last_seen_at TIMESTAMPTZ,
  updated_at TIMESTAMPTZ NOT NULL,
  -- 硬约束 8 的 DB 落地;同一物理账号只能被一个 workspace 活跃持有
  UNIQUE (platform, platform_uid) WHERE deleted_at IS NULL
);
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;

-- 任务 + 租约 + 定向路由
CREATE TABLE desktop_tasks (
  id UUID PRIMARY KEY,
  -- 定向路由(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'
  payload JSONB NOT NULL,
  status TEXT NOT NULL,                      -- queued | in_progress | waiting_user | succeeded | failed | unknown | aborted
  dedup_key TEXT,
  -- 租约
  active_attempt_id UUID,                    -- null 表示无租约持有者
  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
);
CREATE INDEX idx_tasks_job ON desktop_tasks (job_id);
CREATE INDEX idx_tasks_target_client_status ON desktop_tasks (target_client_id, status) WHERE status IN ('queued', 'in_progress', 'waiting_user');
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
);

-- 历次 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
);

6.7 任务生命周期(含租约细节)

Web UI 创建 MonitoringPlan("brand=X on doubao,qwen daily 9am")
      ↓
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}
      ↓
Client 执行(filling→submitting),每 60s POST /extend
      ↓
如果 manual 进入审核 → POST /park(lease_token, reason=waiting_user)
   → server 释放 leaseactive_attempt_id=null),状态置 waiting_user
   → 任务仍绑定 target_client_id,不会被抢;不承担 lease 超时压力
   审核通过 → client POST /tasks/{id}/lease?from_parked=true 重新领取
   → 获得新 attempt_id + 新 lease_token,进 submitting
   parked 超时(默认 24h)→ server 自动 abort,状态变 aborted
      ↓
POST /result(lease_token, status=succeeded|failed) → server 校验 lease_token 后存结果
      ↓
Web UI 可视化

超时处理lease_expires_at 到期后 scheduler 把 task 放回 queuedactive_attempt_id 置空;若老 client 恢复后再 POST /result(lease_token=过期值),返 409 → client 本地记为 unknown 提示人工。

6.8 admin-web 协同(发布 Modal + 媒体数据面板 + 离线降级)

桌面客户端是执行者与凭据持有者,但用户日常操作的入口仍然是 apps/admin-webSaaS Web 端)。/api/desktop/* 面向桌面端(§6.4),/api/tenant/* 面向 Web 端;server 内部共用 §6.3 的 RabbitMQ + DB。

6.8.1 角色分工

角色 职责 数据主权
admin-webWeb 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) 组合。

流程:

  1. 打开:GET /api/tenant/accounts?kind=publish&workspace_id=... 拉回 workspace 下所有发布账号。响应按 (platform, account) 两层结构组织,每条带:
    • online(由 server 根据该账号所属 client_idlast_seen_at < 5 min 判定)
    • healthlive / expired / captcha / risk
    • lastPublishAt(上次成功发布时间)
  2. Modal UI
    • 平台折叠块,标题显示"选中 X / 共 Y"
    • 每账号一行:checkbox · displayName · tags chips · health badge · online badge · 最近发布时间
    • 按 tag 批量勾选:顶部 tag 多选器(tags 来自 platform_accounts.tags,用户在"账号管理"里自定义,如"品牌号"/"矩阵号"/"测试号"),勾选 tag 后批量勾选所有带该 tag 的账号
    • 搜索框(按 displayName / platform_uid 匹配)
    • 全选 / 反选(按平台、按 tag、全局)
    • 选模式(manual / auto)、时间(立刻 / 定时)、素材策略(封面默认 / 指定)
  3. 提交:POST /api/tenant/publish-jobsbody 示例(content_ref 多态以适配现有 admin-web 的 KOL 模板/品牌素材/图文/视频等多种素材来源):
    {
      "title": "2026 Q2 新品发布",
      "content_ref": {
        "type": "article_draft",
        "id": "draft_1234"
      },
      "accounts": [
        {"account_id": "acc-a1", "mode": "auto"},
        {"account_id": "acc-a2", "mode": "manual"},
        {"account_id": "acc-b5", "mode": "auto"}
      ],
      "scheduled_at": null
    }
    
    content_ref.type 支持 article_draft / template_genKOL 模板生成)/ video / brand_asset 等,后续按需扩枚举;server 根据 type 在解析阶段拉对应实体并附到每条 desktop_task 的 payload 里。 server 展开成 3 条 desktop_tasks(kind=publish),共享同一 job_id;每条绑定一个 target_account_id 与其所属 target_client_id(一等列见 §6.6)。
  4. Modal 切到"进度面板",订阅面向 Web 的 SSE GET /api/tenant/events?topic=job:<job_id>;接收每子任务状态变化(queued → in_progress → waiting_user → succeeded/failed/unknown/aborted)。
  5. manual 模式任务进入 waiting_user 后,面板上显示"账号 X 等待桌面端审核",带"前往桌面端"按钮 → georankly://tasks/<id>/review(未装客户端时跳下载页)。
  6. 全部终态后 Modal 汇总:每账号结果 URL / 错误码;对失败子任务点"重试" → POST /api/tenant/publish-jobs/{job_id}/retry?task_ids=...

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)

统计卡片(按信息密度排序,突出"需要马上关注"的内容):

  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 + 与现有路由的迁移关系

新增路由

GET  /api/tenant/accounts                        # 带 online + health + lastPublishAt + tags 聚合
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
POST /api/tenant/publish-jobs/{id}/retry         # body: {"task_ids": ["..."]}(不用 query string
GET  /api/tenant/monitoring-results              # 支持 filter / range / 按 llm / 按账号
GET  /api/tenant/events                          # SSEtopic 参数仅作提示,实际 topic 服务端派生

与现有路由的迁移表(核对 server/internal/tenant/transport/router.go):

现有路由 处置 新路由 / 迁移策略
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-jobsbody 里 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 与上线补领

离线态展示

  • 某桌面端离线last_seen_at > 5 min):
    • Modal 里所属账号标"离线(上次 X 分钟前)· 待执行队列 N",仍可勾选,顶部提示"任务将在该端上线后自动执行"。
    • 账号总览 / 监控结果用灰色 badge。
    • 若同 workspace 有其他在线客户端但它没持有该账号 → Modal 显式提示"该账号绑定的 <device_name> 离线,其他客户端无法代跑(Cookie 本地隔离)"。
  • workspace 所有桌面端都离线
    • Modal 顶部 banner"当前 workspace 无在线桌面端。任务会排队(队列 N 条,预计等待 > M min)。[下载桌面端]"
    • 监控计划若连续 N 次未执行,站内通知 + 邮件给 workspace owner。
  • 容量/ETA 反馈GET /api/tenant/publish-jobs/{id} 返回 queue_ahead(该任务前面还有几条排队)和 eta_seconds(基于近期吞吐估算);Modal 进度面板显示为"前面还有 3 条任务 · 预计 2 min 后开始"。

上线补领机制双向夹逼,避免任务永远待领):

  • 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。

6.8.6 Account 数据一致性(CAS + Tombstone 协议)

关键原则(取代 v2.1 中模糊的"client = SoT / server = 投影"措辞,因其在并发场景下语义不一致):

  1. Account 的生命周期由 server 的 sync_version 单调序列与 deleted_at tombstone 共同定义client 持有本地 partition 与 cookie。server 仍不持有任何 cookie 原文。
  2. 所有写操作都是 CASCompare-And-Swapclient 请求必须带 if_sync_version=<n>server 仅当 current_version == n 时接受写入并自增 version;否则返 409 Conflictclient 据此触发 resync。
  3. 删除永远是 tombstonesoft deleteserver 不 hard delete,写 deleted_at = now() + sync_version += 1hard 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 的 upsertclient 收 409UI 提示"该账号已在其他 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 tagsif=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 落地;秒级一致性延迟可接受。

6.8.7 深链接 georankly://

深链 场景 参数 Fallback
georankly://accounts "前往桌面端管理账号" 未装 → 下载页
georankly://accounts/add?platform=wechat "一键添加某平台账号" platform 同上
georankly://tasks/<id>/review?nonce=<opaque_code> Manual 模式等审核 必须带一次性 nonceserver 签发的短期 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 / 已消费)。 同上

点击交互链路(解决跨浏览器行为差异 + 协议未注册 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 浏览器对自定义协议首次点击时行为不一致的问题(部分会弹"允许打开"、部分直接跳转、部分静默拦截)。

Windows 协议注册:安装 package 时通过 NSIS 脚本写入 HKCU\Software\Classes\georankly 注册表项;卸载时清理。macOS 通过 Info.plistCFBundleURLTypes 声明;Linux 通过 .desktop 文件的 MimeType=x-scheme-handler/georankly。这些具体步骤在 Plan A 的打包任务里明确落实。


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, attemptId, leaseToken, stage, event, durationMs, bytes。本地 rolling file10 MB × 20 份)。不默认上传。脱敏字段:Cookie / Set-Cookie / Authorization / x-xsrf-token / password / 手机号 / email。

7.3 指标上报

5 min 一次上报 /api/desktop/clients/metrics,只含非敏感聚合:CPU/内存/事件循环延迟、每平台每账号的任务成功率 + 平均耗时、SSE 连接状态、pull 回退次数、token bucket 拒绝率。不含查询内容、回答内容、Cookie。

7.4 用户可见反馈

  • 托盘图标三色(绿/黄/红)表示账号健康。
  • OS 通知在需要用户动作时弹(登录失效、捕获到验证码、租约失效导致 UNKNOWN 需人工对账)。
  • UI "事件中心":列最近 50 次失败,点进去直接"重新运行"或"启用录屏再跑"。
  • "发送诊断包"按钮:最近 1 小时日志 + trace 打包脱敏上传,返回 diagnostic_id

7.5 崩溃恢复(按 task.kind 分治)

启动时扫描 desktop_taskslease_client_id = self AND status IN (in_progress, waiting_user) 的任务:

Monitor 任务(幂等只读,可安全重跑):

  • 任何阶段崩溃 → 丢弃本地状态 → 放回队列 → 下次被分派重跑。日志里记录 previous_attempt_aborted_by_crash 用于对账。
  • 不标 UNKNOWN,不骚扰用户。

Publish 任务(不可逆副作用,保守处理):

  • 未进入 submitting 阶段(即还在 fillingwaiting_user parked):重新 ack → 把 state 重置到 filling 从头跑,因为还没点过"发布"按钮。
  • 已进入 submitting放弃任务,标 unknown。避免重复发布。

UNKNOWN 任务的 UI 对账工作流(不只是"提示一下"就完事):

UNKNOWN 任务在 admin-web "事件中心"和"发布历史"页有专门区块,黄色高亮 + 顶部固定。每条带一个 Action Dropdown

用户动作 后端行为 语义
"已确认发布成功" POST /tasks/{id}/reconcile {status: 'succeeded', external_url} 把状态坐实为 succeeded,计入成功统计
"已确认发布失败" POST /tasks/{id}/reconcile {status: 'failed', reason} 状态改 failed,允许触发 retry
"忽略此任务" POST /tasks/{id}/reconcile {status: 'aborted'} 状态改 aborted,从待办清单撤离
"重新创建任务" POST /api/tenant/publish-jobs 复制原 payload 原任务保留 UNKNOWN 作为审计记录,新建一条跑

关键:UNKNOWN 不会自动消失——用户必须显式选一项。超过 7 天没处理的 UNKNOWN 任务每周触发一次站内通知给 workspace owner。


8. 打包、自动更新、签名

8.1 工具链

  • electron-builder 多平台打包 + 差分更新 + 签名。
  • electron-updater 跑在 Main 进程,latest-*.yml + blockmap 差分下发。
  • 渠道stable/beta/dev,用户可切。Patch 热更通道独立,不走 electron-updater。

8.2 目标矩阵

OS Arch 产物 签名
macOS universalIntel + Apple Silicon .dmg + .zip Apple Developer ID + notarytool 公证(已有 Apple Dev 账号,CI 直接接入)
Windows x64 .exe (nsis) 暂无,后期购买(正式发布前阻塞项)
Windows arm64 .exe (nsis) 同上
Linux x64 .AppImage + .deb GPG 可选
Linux arm64 .AppImage + .deb GPG 可选

产物大小目标:单平台 ≤ 120 MB

native deps 多架构注意better-sqlite3 / keytar 等在 universal Mac build 里需通过 electron-builderbuildDependenciesFromSource 或 pre-built multi-arch binaries 处理,CI 会增加一步 @electron/rebuild 针对每个 target。

8.3 CI

GitHub Actionsrunner 矩阵:macos-14 (arm64) + macos-13 (x64) 合并 universalwindows-latest 出 x64Windows arm64 用 cross-compileLinux x64 ubuntu-latestLinux arm64 优先用 ubuntu-22.04-arm 原生 runner,不可用时退化到 QEMU2026-04 GitHub arm64 runner 仍在限量可用)。

发布 tag v1.2.3 → 打包 → 上传对象存储 + 生成 latest-*.ymlautoUpdater.checkForUpdates() 看到。

8.4 更新 UX

  • 默认"自动下载 + 手动安装"。下好后 Tray 角标提示"新版本可用,点击重启安装"。不打断正在跑的任务。
  • 强制更新:服务端下发 minClientVersion;低于该版本拒绝启动,引导升级(配合 30 分钟宽限期 + 本地可见的手动下载链接兜底)。
  • 降级:使用 electron-updaterallowDowngrade: true注意正确拼写,旧 spec 写的 downgrade 属性不存在)。服务端可下发"建议版本号",客户端比建议版本新时允许一键回滚。
  • N-1 回退:对象存储长期保留上一版安装包 + manifest,UI 提供"手动下载上一版本"链接,作为 autoUpdater 失败的人工兜底。
  • pre-release 通道每次打包都跑一次updater smoke 自动化(见 §9.7)。

8.5 应用标识

  • app.requestSingleInstanceLock():禁止同用户开两个副本。
  • Protocol handlergeorankly://,用于 Web 端深链接到客户端。
  • macOS 关闭窗口 ≠ 退出;Windows/Linux 最小化到托盘;退出必须从 Tray 菜单。

8.6 安全加固

  • contextIsolation: truenodeIntegration: falsesandbox: true for renderer。
  • CSP 严格模式;renderer 不允许加载远程 JS。
  • webContents.setWindowOpenHandler 拦截所有 window.open → 系统浏览器。
  • 禁用 <webview> tag,第三方平台一律用 WebContentsView

9. 测试策略

9.1 金字塔

┌──────────────┐  手工 + 冒烟 Playbook(每 release
│  E2E 烟雾      │
├──────────────┤  Playwright @ Electron + fake server + fake platforms(每 PR
│  集成 / 行为    │
├──────────────┤  Vitest(每 commit
│  单元(adapter)│
├──────────────┤  tsc --noEmit + eslint + typecheck(每保存)
│  类型 / 静态     │
└──────────────┘

9.2 单元

  • 对象:适配器的 SSE parser(纯函数)、发布状态机 + 租约交互逻辑lease-manager 本地状态机autoUpdater 核心逻辑(版本比较、签名验签、allowDowngrade 决策、minClientVersion 校验)、patch-loader 签名与序号校验
  • Fixturestests/fixtures/sse/doubao-2026-04.txttests/fixtures/manifest/valid-*.json + invalid-*.json
  • 平台 payload 变动 → 补 fixture → 测试跟着更新。

9.3 集成

用 Playwright _electron.launch 驱动 Electron,对 本地 fake server + 本地 fake 平台HTTP server 喂 fixture SSE)。13+5 适配器全跑一遍,并行约 3–5 分钟。

9.4 集成 · 协议

客户端 + 真实 server + fake 平台:验证任务分发 / 租约 (lease/extend/pause/resume/cancel) / 多实例广播 / 幂等 / 熔断 / 断线重连

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 autoUpdater Smokepre-release 必跑)

不是"完整升级 E2E",但必须覆盖:

  • 旧版本打包 + 新版本打包 + 从旧版本启动 → 拉 manifest → 下载 → 安装 → 启动新版本(headless 跑通即 pass)。
  • 签名不一致、manifest 过期、minClientVersion 不满足 → 各一条负面用例。
  • allowDowngrade: truefalse 各一条正向用例。

9.8 门禁

  • PR:单元 + 集成(fake)全过。
  • Release tag+ 冒烟 Playbook 签字 + updater smoke 全过。
  • 每周:trace 诊断报告 review。

10. 上线节奏与回滚

10.1 阶段

阶段 受众 签名 退出门槛
Alpha(内部) 开发 + QA(≤5 人) Mac 公证 / Win 无签名 / Linux 裸 AppImage 13+5 适配器 fake + 真实账号成功率 ≥ 90%;lease 协议在混沌测试下无脏写
Beta(邀请制) 公司内部 + 合作客户(≤30 人) 同上 连续 7 天无 P0;保活命中率 > 95%;patch 通道最小闭环(Plan B 块 G1)已上线,应急修复跑通 ≥ 1 次autoUpdater smoke 每 release 全过
GA 候选 放开注册 Win 签名必须到位 安装成功率 > 99%crash rate < 0.1%/session

客户端和服务端同加 channelalpha/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 工作量重估(10 块)

粗估(单人工程周):

范围 估时
0. Workspace 底座一等化v2.3 新增 · Plan 0 workspaces + workspace_memberships 表迁移;Actor/Claimsworkspace_idWorkspaceScope middleware;所有 repo query 注入 workspace_idcache_support.go key 改 workspace 粒度;监控域存量改造清单(§6.1.1)完整迁 23 周(阻塞 Plan A
A. Desktop runtime 基座 Main 进程、session-registry、view-pool、vault、lease-manager(含 parked state)、rate-limiter、circuit-breaker、crash-recovery、tracingAlpha 版)、transport (SSE + pull + RabbitMQ presence) 34 周
B. LLM 监控适配器 Doubao + Qwen + DeepSeek(从零) + 元宝(从零) + Kimi(从零)前置 § 5.2 spike 矩阵 0.5 周 4.55.5 周
C. 已有真实发布能力的平台迁移 约 6 家(头条 / 百家 / 搜狐 / 知乎 / 企鹅 / 简书)从 chrome.* 迁到 Electron 抽象 3 周
D. Detect-only 平台从零实现 publish 7 家(微信公众号 / 掘金 / 懂车帝 / 什么值得买 / 网易 / B 站 / ZOL) 57 周
E. 服务端协议 + DB + sqlc 重生 + Service 层重构 /api/desktop/*、租约(含 park/lease-from-parked)、presence、patch manifest、reconcile 2.5 周
F. 打包 5 arch + Mac 公证 + autoUpdater + updater smoke 1.5 周
G. Patch 通道 G1最小闭环:服务端签名 manifest + 客户端 Ed25519 验签 + DSL 解释器 + revoke + rollback)前移到 Plan B1.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.53 周(v2.2 review 低估了 11.5 周)
I. 测试基建 fake server + fake 平台 + replay + autoUpdater 核心逻辑单测 1.5 周

合计约 2833 工程周v2.3 新增 Plan 0 前置 + Modal 重构工期修正 + G 拆前后期)。这份 spec 因此拆 4 个 plan(见 §12)。

10.4 三层回滚

情景 动作
某 patch 导致单平台适配器坏 服务端下发 revoke patch_seq + 上一版本可用 manifest;客户端下次任务自动降回前一版 patch(秒级)
某 release 中等问题 下发"建议版本 ≤ X"electron-updaterallowDowngrade: true 降级(分钟级);+ 手动 N-1 下载链接
某 release 严重安全问题 下发 minClientVersion = Y;低于 Y 的客户端 30 分钟宽限后拒绝启动,UI 引导升级

所有回滚只在服务端配置,不动客户端代码。

10.5 运营指标

  • 每平台适配器每日成功率(target:发布 > 95%、监控 > 90%
  • 每次 crash 的 top 堆栈
  • SSE 长连接平均保持时长(target > 30 min
  • Patch 推送后"多久 80% 客户端加载新 patch"target < 30 min
  • 租约冲突率(409 / 百次 leasetarget < 0.5%

11. 已知风险

  1. Win 无签名期间(Alpha/Beta),用户首次安装会遇 SmartScreen 警告,且有机率被 Defender / 360 / 国内安全软件主动隔离。Alpha 期间通过白名单与用户手动放行缓解。
  2. 豆包/千问网页结构变,最坏情况紧急手搓 patch,SLA 按小时算。DSL 能力有限时退化到备用隔离执行通道。
  3. 用户跨 OS 切换(Mac → Win 再回来)不迁移登录态(local-only cookies),需全部重登。或使用 vault-export 手工带密口令迁移。
  4. 账号被平台风控无法自动恢复;UI 引导用户换号。
  5. 开机自启 + 托盘常驻可能被企业 EDR 误报恶意软件;需准备白名单说明材料。
  6. Electron 基础 RAM 占用~300 MB + 每 hot-tier WebContentsView ~50 MB。默认 Hot/Cold 分档后,10 账号活跃场景约 600–800 MB。
  7. Electron CVE 维护节奏Electron 约每 46 周发安全版本,需对应 release 通道的补丁节奏。滞后发版 > 2 周的 Electron 主线安全问题需主动升级。
  8. 平台 ToS 风险:部分媒体平台(尤其微信公众号)明文禁止自动化操作。Alpha/Beta 阶段限定给明确授权账号;商业化前评估法务暴露面。
  9. CDP domain 可用性跟随 Chromium 版本(如 Network.streamResourceContent ≥ 120)。升 Electron 大版本前必须跑一遍 electron-chromium-cdp-matrix.md 回归。

12. Plan 拆分建议

本 spec 包含约 28–33 周的工作量,拆 4 个独立 plan 执行。Plan 0 是 Plan A 的硬前置(workspace 底座不一等化,后面所有协议/鉴权/缓存/监控承诺都无法真正落地)。

Plan 0 · Workspace 底座一等化(23 周 · 阻塞 Plan A)

v2.2 review 发现现有代码里 workspace 不是后端一等安全边界(Actor/Claims 只有 tenant_id、cache_support.go key 按 tenant、没有 workspaces 表迁移、监控表按 tenant)。这些是横切式改造,不能作为局部补丁塞进 Plan A。

  • workspaces + workspace_memberships 表迁移
  • Actor/Claims 结构扩 workspace_idUser JWT 也加
  • TenantScope 之上新加 WorkspaceScope middleware
  • 所有 repo layer query 显式注入 workspace_id = :actor_workspace_id
  • cache_support.go workspace-sensitive key 加 workspace_id
  • 监控域存量改造清单(§6.1.1)全量迁:tenant_monitoring_quotas → 加 workspace_id + 改 primary_client_idmonitoring_service.go / monitoring_callback_service.go / monitoring_handler.go 全量改造
  • admin-web 现有页面的 API 调用补 workspace context

退出目标:现有 admin-web 页面全部在 workspace 粒度正常工作;tenant_monitoring 按 workspace 隔离;/api/tenant/* 所有路由挂上 WorkspaceScoperepo 单元测试覆盖 cross-workspace 防穿透用例。

Plan A · 桌面端骨架穿透(5–6 周,Alpha 上限)

  • 块 ADesktop runtime 基座,含 parked state 实现)
  • 块 E(服务端协议 + DB + reconcile endpoint
  • 块 Hui-shared 抽取 + renderer + PublishArticleModal 重构为账号级多选
  • §5.2 spike 矩阵(0.5 周)先做再定适配器实现
  • 1 个 监控适配器(Doubao+ 1 个 发布适配器(头条号,已有 publish 能力)
  • 块 I(测试基建)
  • 块 F 仅 Mac 公证

退出目标:端到端跑通一条 Doubao 监控任务 + 一条头条发布任务(含 manual 模式 parked → resume);Mac 公证版可安装;lease 协议在 fake 多实例 + 混沌测试下无脏写;UNKNOWN 任务的 reconcile 工作流闭环。

Plan B · 广度铺开(1214 周,Beta 上限)

  • 块 BQwen + DeepSeek + 元宝 + Kimi 从零)
  • 块 C(其余 5 家已有真实发布能力的平台迁移)
  • 块 D7 家 detect-only 从零实现 publish
  • 块 G1 · Patch 通道最小闭环(服务端签名 manifest + 客户端 Ed25519 验签 + DSL 解释器 + revoke + rollback1.5 周)——前移到此,满足 §10.1 Beta 退出门槛"patch 通道应急修复跑通 ≥ 1 次"
  • 块 F 完整:Win/Linux 全架构 + 签名(Win 签名到位)+ autoUpdater 完整
  • 块 I 补齐:多适配器回归 + 冒烟 Playbook

退出目标13+5 适配器全量可用;autoUpdater smoke 每 release 全过;5 arch × 3 OS 全量打包签名;patch 通道应急修复在 Beta 期至少跑通一次。

Plan C · 运维闭环与高级能力(4–5 周,GA 上限)

  • 块 G2 · Patch 通道高级能力:备用隔离执行通道(utilityProcess + isolated-vm)、根密钥轮换、TUF 风格完整 manifest1 周)
  • Phase 2 Trace 基建(CDP 全事件流 + 2 fps 截屏 rolling buffer + Playwright Trace Viewer 兼容导出)
  • Metrics dashboard + 报警规则(crash rate / SSE 保持时长 / adapter 日成功率 / patch 覆盖率)
  • 回滚剧本与混沌演练
  • 账号共享 / 桌面端主备的演进位置预留(credential_holder 抽象,§2 硬约束 9

退出目标:patch 热更经过混沌演练;trace 能被外部 Playwright tooling 打开;运营指标全套接入报警;回滚剧本有实战演练记录。


13. 变更记录

  • v2.32026-04-18Claude + Codex + Gemini 第三轮(架构师视角)交叉审查后的修订。Codex verdict: Needs-reworkGemini 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/Claimsworkspace_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.eventSourceMessageReceivedfetch-stream 优先 Network.streamResourceContent + Network.dataReceivedChromium ≥ 120 已满足);仅"必须劫持/替换"才用 Fetch 且不再连续调 continueResponse。Plan A 前置 provider × transport 实测矩阵 spike0.5 周),结果入 electron-chromium-cdp-matrix.md
    • W1 · Plan 依赖顺序调整Beta 退出门槛依赖 patch 通道,但 patch 通道在 Plan C——依赖矛盾。v2.3 把 块 G 拆为 G1(最小闭环:签名 manifest + Ed25519 验签 + DSL 解释器 + revoke + rollback1.5 周)前移到 Plan B,G2(备用隔离执行 + 根密钥轮换 + TUF 全要素,1 周)留在 Plan C。§10.1 Beta 门槛相应加备注。
    • W2 · waiting_user 从 lease 拆成 parked state:lease 适合短时占有,不适合 2 小时人审。v2.3 在 readyToReview 进入时 释放 leaseactive_attempt_id=null)、状态置 waiting_user、任务仍绑定 target_client_id 不被抢;审核通过时 client 走 POST /tasks/{id}/lease?from_parked=true 重新领取新 attemptparked 超 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.vueaccountCards 只取 accounts[0]——要改成账号级多选需彻底重写。§10.3 块 H 工期 1.5 → 2.53 周。
    • W7 · MarkdownPreview.vuepackages/ui-sharedWeb 发布前预览与 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 观察)。
    • I2 · UNKNOWN 任务 UI 对账工作流:§7.5 加 POST /tasks/{id}/reconcile 端点 + 四种用户动作(已成功 / 已失败 / 忽略 / 重新创建);超 7 天未处理触发站内通知。
    • I3 · 新增 §15「需要补的图」:列 5 张 Mermaid 图(全链路时序 / lease 状态机 / Account CAS resync / 多实例部署 / 前端组件层级),Plan A 期补齐放 diagrams/
    • 工期重估:§10.3 从 9 块扩到 10 块(加块 0);总工期 24–28 周 → 2833 周Plan 0 新增 2–3 周 + 块 H 工期修正 +1 周 + G 拆前后期)。§12 从 3 plan 拆成 4 plan。
  • v2.22026-04-18Claude + 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_accountssync_version / deleted_at / tags 列与条件唯一索引;§6.8.6 全文重写为 CAS(if_sync_version+ tombstonesoft 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-batchespublish-jobs 重命名、publish-recordspublish-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_secondsModal 显示"前面还有 N 条 · 预计 M min 后开始"。
    • W7 §6.8.7 深链加中间态 modal + 手动启动 / 复制链接 fallback;georankly://tasks/<id>/review 带一次性 nonce60s 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 开头 2226 → 2428(与 §10.3 一致);§6.8.3 统计卡加"🔴 待关注账号"与"按平台汇总"视图;监控计划 UI 改 Grid/Tag 多选布局为未来扩充留余地。
  • v2.12026-04-18:用户增量反馈后的小修订。
    • LLM 监控 3 家 → 5 家:新增混元(Tencent Hunyuan)、KimiMoonshot),§4.4 保活表增行(两家默认走 8 min 主动保活,实际节奏接入时复测);§10.3 块 B 估时 2–3 周 → 4–5 周;总工期 22–26 周 → 2428 周;§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)
  • v22026-04-18Claude + 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 registrySSE 仅推通知、任务归属由 lease endpoint 原子决定(§6.3)。
    • C4 platform_accounts 新增 platform_uid + 唯一索引 (platform, platform_uid),落实硬约束 8(§6.6)。
    • C5 任务协议引入 attempt_id + lease_token + extend/pause/resume/cancelmanual 模式进入 waiting_user 状态并延长租约(§5.3/§6.4/§6.6/§6.7)。
    • C6 §3.3 重写工作量与可复用性口径,显式列出 7 家 detect-only 发布适配器 + DeepSeek 监控占位;§10.3 拆 9 个工作块重估总时;§12 新增 3-plan 拆分建议。
    • W1 BrowserView 全局替换为 WebContentsView
    • W2 autoUpdater 配置名修正 downgradeallowDowngrade;加 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 2CDP 全事件流 + 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)。

14. 附录:LLM 适配器映射表(I1)

截至 2026-04-18 的对外产品入口与传输观察:

adapter_id 公司 模型家族 消费端产品名 官方域名 登录方式 观察到的 transport
doubao 字节跳动 Doubao / 云雀 豆包 www.doubao.com 微信扫码 / 手机号 / 抖音 fetch-streamevent-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-framedPOST /apiv2/kimi.gateway.chat.v1.ChatService/Chat),需 framer 解包

这些条目在 Plan A spike 矩阵后更新为稳定值并记入 docs/superpowers/specs/electron-chromium-cdp-matrix.md

15. 附录:需要补的图(Plan A 期补齐)

这是 v2.2 / v2.3 review 共同指出的文档短板,前端 + 新人理解链路需要视觉化

  1. 全链路时序图Mermaid sequenceDiagram)——Web 点击发布 → Server 入库 → SSE 通知 → Desktop 领取 → 适配器执行 → 结果回传 → Web 更新,含正常流、离线补领流、租约冲突流、parked / resume 流。
  2. Lease 状态机图Mermaid stateDiagram-v2)——queued → in_progress → waiting_user(parked) → in_progress → succeeded / failed / unknown / aborted
  3. Account CAS + Tombstone resync 时序图Mermaid sequenceDiagram)——client/server/Web 三方,含"Web 写 delete_requested + client 坐实"的两段式删除。
  4. 多实例部署架构图——RabbitMQ fanout + 多个 server 实例 + Redis presence registry + 多个 desktop client。
  5. 前端组件层级图admin-web 与 desktop renderer 共享 packages/ui-shared 的依赖关系)。

这些图由 Plan A 期负责 spec 维护的同学用 Mermaid 补,放在 docs/superpowers/specs/diagrams/ 目录,并在本 spec 相关章节里用 ![](diagrams/...) 引用。

16. 术语表

  • Patch 通道:仅下发适配器的声明式数据与受限 DSL(默认)或隔离沙箱执行的纯函数(备用),分钟级生效,不重启。
  • Release 通道:完整二进制 + 差分升级,通过 electron-updater。
  • 探活(probe:加载主页 DOM 判断登录态,低代价。
  • 保活(keep-alive / heartbeat:主动访问平台私有 API 触发 token 刷新。
  • Client Token:一台 Electron 安装的后台身份,与 User JWT 并列;解耦 client_version,支持 rotate/revoke。
  • PartitionElectron session.fromPartition('persist:<id>'),每个账号一个隔离的 cookie/storage 容器。
  • Lease:任务租约。领取任务时服务端返回 attempt_id + lease_token + lease_expires_at;后续所有写操作必须带 lease_token 验证。
  • Waiting-user 状态manual 发布模式下,adapter 暂停等用户审核时,服务端将任务置为此状态并延长租约;在此状态下不会被重分配。
  • Platform UID:平台侧真实账号 ID(如微信 fakeid、B 站 mid、知乎 hash_id);(platform, platform_uid) 在 DB 层唯一,落实硬约束 8。
  • DSLPatch 默认通道):一套受限的声明式解析语言,只能做字符串/JSON 解析与模式匹配,无 JS 执行能力。
  • Presence registry:多实例服务端共享的 client_id → (instance_id, conn_id) 注册表,用 Redis TTL 30 s 维护;用于跨实例路由 SSE 通知。