docs(desktop-client): spec v2.3 from architect-level review

Fourth-round Claude+Codex+Gemini architect review produced 1C + 7W
issues. All applied:

Plan 0 scope lock-in (Codex C1 Partial → Closed):
- §12 Plan 0 explicitly adopts narrow-workspace Phase 1 strategy:
  workspace first-class ONLY on desktop_clients / platform_accounts /
  desktop_tasks / desktop_publish_jobs / tenant_monitoring_quotas.
  Existing articles / brands / kol-templates / publish-batches /
  images lines keep tenant==workspace (no multi-workspace UI).
- WorkspaceScope middleware attached to a specific route whitelist only.
- Eight concrete Plan 0 deliverables with three exit-bar categories.

Spec consistency sweep (Codex W3):
- §6.4 idempotency contract rewritten: pause/resume references
  removed; correct new lease ops enumerated (extend/cancel/result/
  park/trace + lease + lease?from_parked).
- §7.5 crash recovery: lease_client_id → target_client_id (the former
  never existed after the v2.2 schema restructure).
- §6.6 platform_accounts DDL gains delete_requested_at column so
  §6.8.6 two-phase delete protocol actually has its storage.
- §6.4 routes table adds POST /api/tenant/tasks/{id}/reconcile and
  POST /api/tenant/accounts/{id}/request-delete (previously only in
  prose).
- §14 Waiting-user glossary rewritten to reflect parked+CAS model.
- §13 v2.2 changelog annotated with "v2.3+ supersedes".

Parked / reconcile protocol closure (Codex W2 Partial → Closed):
- §5.3 adds Parked Recovery Protocol: strict CAS SQL sketch for
  POST /tasks/{id}/lease?from_parked, five error scenarios with
  client action mapping, idempotent repeat-request rule, and five
  target_client-offline/revoke/swap/72h-timeout strategies with
  a "never auto-rebind" core principle.
- Reconcile moved from /api/desktop/* to /api/tenant/* (Web owns
  reconciliation).
- Parked auto-abort threshold lifted 24h → 72h to avoid weekend
  false-kills.

Mermaid diagrams pulled forward (Gemini W10):
- §15.1 Lease state machine (Mermaid stateDiagram-v2) inlined.
- §15.2 Account CAS + Tombstone resync sequence diagram inlined.
- §15.3 reserves three remaining diagrams for Plan A.

Spike operability (Codex W4):
- §12 Plan A Spike specs owner pair (runtime + provider owner),
  artifact (electron-chromium-cdp-matrix.md with capture/screenshot
  evidence), three-state outcome (supported / degraded / blocked),
  and fallback decision tree for blocked case.

Modal refactor split (Gemini C1):
- §12 Plan A Block H splits into H1 (weeks 1-2: ui-shared skeleton +
  design tokens + useAccountSelection composable + shared components)
  and H2 (weeks 4-5: PublishArticleModal rewrite on top of H1).

Design tokens explicit (Gemini W2):
- H1 now required to produce CSS variables (light/dark) + Ant Design
  Theme Config as first deliverables of ui-shared.

credential_holder DDL promise softened (Codex W3 Partial → Closed):
- §2 hard constraint 9 reverts to naming soft-convention only; DDL
  keeps workspace_id / client_id / target_client_id.
- §11 risk note adjusted.

Header status updated to v2.4. Total effort unchanged at 28-33 weeks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-18 23:19:49 +08:00
parent 70d1d96c57
commit 256f507bff
@@ -1,11 +1,11 @@
# Electron 桌面客户端设计(全面替换 WXT 浏览器插件)
**状态**Draft v2 · 待实施(v1 经 Claude + Codex + Gemini 三方交叉审查后修订)
**状态**Draft v2.4 · 待实施(历经 4 轮 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 变更记录
> **v2.4 主要修订**Plan 0 **窄 workspace 化** 范围写死 + spec 一致性全量清扫 + Parked 恢复协议闭环(严格 CAS + 5 类恢复策略) + §15 Lease 状态机 & Account CAS resync Mermaid 图内联 + Spike 三态操作化 + 块 H 拆 H1/H2 + Design tokens 显式 + credential_holder DDL 承诺软化。完整历史(v1 → v2.4)详见 §13。
---
@@ -37,7 +37,7 @@
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_accounts``platform_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 注释)
9. **演进缝预留**:短期 `一账号 = 一 workspace = 一台 client` 是产品边界,不是代码硬写。本版 DDL 继续使用 `workspace_id` / `client_id` / `target_client_id` 命名(不提前引入 `credential_holder_*` / `account_home_*` 命名以避免 v2.4 前 spec 与 DDL 不一致);未来真正做"共享运营账号池 / 桌面端主备"时,通过 rename migration + 兼容视图平滑演进
10. 要求最小 electron 体积包
---
@@ -316,15 +316,58 @@ type PublishState =
- 进入 `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 路由规则)
- 任务归属仍绑定原 `target_account_id + target_client_id`**不会被其它 client 抢走**。
- 任务**不持有活动 lease**`active_attempt_id = null`),server 不会因 lease 超时回滚状态。
- 用户点"发布"后,客户端**重新获取 lease**`POST /tasks/{id}/lease``from_parked=true`),进入 `submitting`
- 用户跨重启恢复:同样的路径,因为 parked state 持久在 DB。
- 服务端可选设置 "parked 超过 N 小时则自动 abort" 的 fallback(默认 24h可配置)。
- 用户点"发布"后,client 走 `POST /tasks/{id}/lease?from_parked=true` 严格 CAS 重新领取(见下"parked 恢复协议"
- 用户跨重启/切 window 恢复:同样的路径,parked state 持久在 DB。
- 服务端设置 "parked 超过 N 小时则自动 abort" 的 fallback(默认 **72 h**,v2.4 从 24 h 调大避免长假/周末误杀;可配置)。
- 用户取消或 signal abort → `aborted`
- adapter 抛异常 → `failed`,带错误码。
- 进程崩溃 → 启动时按 §7.5 分派为 `unknown`(仅限 publish 任务的 `submitting` 阶段)。
#### Parked 恢复协议(严格 CAS · v2.4 补齐)
`POST /api/desktop/tasks/{id}/lease?from_parked=true` 的服务端原子语义(单条 SQL 或 advisory lock):
```
UPDATE desktop_tasks
SET active_attempt_id = <new_uuid>,
lease_token_hash = <sha256(new_token)>,
lease_expires_at = now() + 10 min,
status = 'in_progress',
attempts = attempts + 1,
updated_at = now()
WHERE id = $1
AND status = 'waiting_user'
AND target_client_id = $actor_client_id -- 仅 target_client 可调
AND active_attempt_id IS NULL
RETURNING id, new_attempt_id, new_lease_token, lease_expires_at;
```
- **0 行返回 = 409 Conflict**,客户端按下表处理:
| 错误情景 | 服务端返回 | 客户端动作 |
|---|---|---|
| 任务已被其它 attempt 抢先 resume(双击 / 断线重连重发)| 409 + `{reason: "already_in_progress", active_attempt_id}` | 若 attempt_id = self 的待领 → 直接用之前那份;否则忽略(避免双跑)|
| 任务已 abort(超时/Web 端 cancel| 409 + `{reason: "aborted"}` | UI 通知用户 "任务已取消";清理本地 state |
| 任务已 succeeded / failed(其它路径抢结果)| 409 + `{reason: "terminal", status: ...}` | 当作结果回调丢失处理,拉一次状态同步 |
| 调用方不是 `target_client_id`(换机 / 其它 client 误触发)| 403 + `{reason: "not_target_client"}` | **不**允许抢;UI 提示"请到原桌面端 `<device_name>` 继续" |
| 任务不是 `waiting_user` 状态 | 409 + `{reason: "not_parked"}` | 走正常 lease 重新领取 |
- **重复请求(client 断线重发)**:同一个 client 发两次 `from_parked=true`,第一次成功,第二次命中"已被抢先"分支——但 `active_attempt_id` 的持有者就是自己,返回 200 幂等,带当前 `lease_token`。这要求 server 在检测到 `active_attempt_id != null` 时**把请求 client 和当前持有者对比**:相等且未超时 → 200 幂等返回当前 lease;不等 → 409。
#### target_client 离线/换机/revoke 时的 parked 恢复策略
| 情景 | 处理 |
|---|---|
| target_client **离线 >5 min**(正常关机 / 网络断)| parked 状态保持;Web 端 Modal 显示"等待桌面端 `<device_name>` 上线"**不 rebind**client 上线后走 §6.3 的 offline→online 补领路径(scheduler 扫描该 client 的 parked 任务并推 `task_available`)触发前端"继续审核"引导。 |
| target_client **被 revoke**`POST /api/desktop/clients/revoke`| server 级联把该 client 所有 `status IN (waiting_user, in_progress)` 任务置 `unknown`;发通知给 workspace owner 通过 `/api/tenant/tasks/{id}/reconcile` 对账(因为 cookie 已随 client 失效,不能无脑 rebind)。 |
| target_client **被用户主动删除** | 同 revoke。 |
| target_client **换机**(同 workspace 新 client| 不自动 rebindCookie 是本地隔离的,新 client 上没有该 account 的登录态)。Web 端显示"原桌面端已下线,请在新桌面端重新登录此账号后手动重启任务"。 |
| target_client **72h 超时**parked 自动 abort | server `status → aborted` + `error = {code: 'parked_timeout', since: ...}`Web 端 reconcile 工作流处理(可"重新运行"或"忽略")。 |
**核心原则**Parked 任务**从不自动迁移**到新 client(避免"我以为我取消了,它在另一台机器上还发了")。所有看起来像 rebind 的动作都必须是用户显式重新创建任务。
文件上传:`protocol.handle('file')` + CDP `Input.dispatchDragEvent` 或直接喂 `<input type=file>`,不经服务端中转。
### 5.4 公共能力
@@ -486,11 +529,15 @@ 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}/lease?from_parked=true # 从 parked 重新领取(严格 CAS,仅 target_client 可调用,见 §5.3
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 # 用户主动上传诊断包
# Web 端对账(UNKNOWN 任务 / 账号删除意图,见 §7.5 和 §6.8.6)
POST /api/tenant/tasks/{id}/reconcile # body: {status: 'succeeded'|'failed'|'aborted'|'retry', external_url?, reason?} · 仅 Web 用户可调、需 WorkspaceScope
POST /api/tenant/accounts/{id}/request-delete # body: {} 或 ?undo=true · 写 delete_requested_at
# 账号元数据(不传 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
@@ -506,7 +553,7 @@ POST /api/tenant/publish-jobs
# ...
```
**幂等契约**:所有写操作(lease/extend/pause/resume/cancel/result**必须携带 `lease_token`**;服务端校验 `lease_token` 与当前 `active_attempt_id` 绑定,否则返 409 Conflict。客户端收到 409 = 租约已失效,**停止动作 + 提示人工对账**。
**幂等契约**:所有持有活动 lease 的写操作(`extend` / `cancel` / `result` / `park` / `trace`**必须携带 `lease_token`**;服务端校验 `lease_token` 与当前 `active_attempt_id` 绑定,否则返 409 Conflict。`POST /tasks/lease``POST /tasks/{id}/lease?from_parked=true` 本身是 lease 发放者,不需要 `lease_token`;但后者走**严格 CAS**(见 §5.3 parked 恢复协议)。客户端收到 409 = 租约已失效 / 已被抢占 / parked 任务已 abort**停止动作 + 触发 resync + 提示人工对账**。
**中间件与 workspace 一等化要求****先决条件**,不是实施细节):
@@ -564,6 +611,7 @@ CREATE TABLE platform_accounts (
-- 一致性协议(见 §6.8.6
sync_version BIGINT NOT NULL DEFAULT 1, -- 单调递增,每次写入 +1
deleted_at TIMESTAMPTZ, -- tombstone,软删除;非 NULL 时 Modal 和 /accounts 默认过滤掉
delete_requested_at TIMESTAMPTZ, -- Web 端写"请求删除"意图;由 client resync 时坐实 DELETE(见 §6.8.6
created_at TIMESTAMPTZ NOT NULL,
last_seen_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ NOT NULL,
@@ -903,7 +951,7 @@ user clicks deep link in admin-web
### 7.5 崩溃恢复(按 task.kind 分治)
启动时扫描 `desktop_tasks` 里 `lease_client_id = self AND status IN (in_progress, waiting_user)` 的任务:
启动时扫描 `desktop_tasks` 里 `target_client_id = self AND status IN (in_progress, waiting_user)` 的任务:
**Monitor 任务**(幂等只读,可安全重跑):
@@ -921,9 +969,9 @@ UNKNOWN 任务在 admin-web "事件中心"和"发布历史"页有专门区块,
| 用户动作 | 后端行为 | 语义 |
|---|---|---|
| **"已确认发布成功"** | `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/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`,从待办清单撤离 |
| **"重新创建任务"** | `POST /api/tenant/publish-jobs` 复制原 payload | 原任务保留 UNKNOWN 作为审计记录,新建一条跑 |
**关键**:UNKNOWN 不会自动消失——用户必须显式选一项。超过 7 天没处理的 UNKNOWN 任务每周触发一次站内通知给 workspace owner。
@@ -1009,7 +1057,7 @@ GitHub Actionsrunner 矩阵:`macos-14 (arm64)` + `macos-13 (x64)` 合并 un
### 9.4 集成 · 协议
客户端 + 真实 server + fake 平台:验证**任务分发 / 租约 (lease/extend/pause/resume/cancel) / 多实例广播 / 幂等 / 熔断 / 断线重连**。
客户端 + 真实 server + fake 平台:验证**任务分发 / 租约 (`lease` / `extend` / `park` / `lease?from_parked` / `cancel`) / 多实例广播 / 幂等 / 熔断 / 断线重连 / parked 任务在 target_client 下线/换机的恢复路径**。
### 9.5 人工冒烟 Playbook
@@ -1122,28 +1170,66 @@ GitHub Actionsrunner 矩阵:`macos-14 (arm64)` + `macos-13 (x64)` 合并 un
### Plan 0 · Workspace 底座一等化(23 周 · 阻塞 Plan A)
v2.2 review 发现现有代码里 workspace 不是后端一等安全边界(`Actor/Claims` 只有 tenant_id、`cache_support.go` key 按 tenant、没有 workspaces 表迁移、监控表按 tenant)。这些是横切式改造,不能作为局部补丁塞进 Plan A。
#### 范围策略:Phase 1 **窄 workspace 化**v2.4 明确决策)
- `workspaces` + `workspace_memberships` 表迁移
- `Actor/Claims` 结构扩 `workspace_id`User 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_id``monitoring_service.go` / `monitoring_callback_service.go` / `monitoring_handler.go` 全量改造
- admin-web 现有页面的 API 调用补 workspace context
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 明确采用**两阶段策略**:
**退出目标**:现有 admin-web 页面全部在 workspace 粒度正常工作;tenant_monitoring 按 workspace 隔离;`/api/tenant/*` 所有路由挂上 `WorkspaceScope`repo 单元测试覆盖 cross-workspace 防穿透用例
- **Phase 1Plan 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 内容
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。
#### 退出目标(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 通过。
### Plan A · 桌面端骨架穿透(5–6 周,Alpha 上限)
- 块 ADesktop runtime 基座,含 parked state 实现)
- 块 E(服务端协议 + DB + reconcile endpoint
- 块 Hui-shared 抽取 + renderer + **PublishArticleModal 重构为账号级多选**
- **§5.2 spike 矩阵(0.5 周)先做再定适配器实现**
- 块 ADesktop runtime 基座,含 parked state 实现 + parked 恢复协议
- 块 E(服务端协议 + DB + `/tasks/{id}/reconcile` endpoint + `/tasks/{id}/lease?from_parked` 严格 CAS
- 块 H拆两阶段,见下
- **H1 · 前期(Plan A 第 12 周)**`packages/ui-shared` 骨架 + **design tokens**CSS variables light/dark + Ant Design Theme Config+ `useAccountSelection` composable(账号级多选逻辑)+ `HealthBadge / AccountCard / TagMultiSelect / MarkdownPreview` 组件迁入。
- **H2 · 后期(Plan A 第 45 周)**`PublishArticleModal` 重构为账号级多选(基于 H1 的 composable),接入新 `/api/tenant/publish-jobs` 展开语义,进度面板 SSE 订阅。
- **§5.2 spike 矩阵**0.5 周,开 Plan A 前先做,见下文)
- **1 个** 监控适配器(Doubao)+ **1 个** 发布适配器(头条号,已有 publish 能力)
- 块 I(测试基建)
- 块 F **仅 Mac 公证**
**Spike 矩阵操作规格**v2.4 补齐):
- **Owner**runtime owner(负责 CDP / Fetch 基建)+ 对应 provider ownerDoubao 为主)联签。
- **产物**`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 启动前各自跑一次。
**退出目标**:端到端跑通一条 Doubao 监控任务 + 一条头条发布任务(含 manual 模式 parked → resume);Mac 公证版可安装;lease 协议在 fake 多实例 + 混沌测试下无脏写;UNKNOWN 任务的 reconcile 工作流闭环。
### Plan B · 广度铺开(1214 周,Beta 上限)
@@ -1163,7 +1249,7 @@ v2.2 review 发现现有代码里 workspace 不是后端一等安全边界(`Ac
- Phase 2 Trace 基建(CDP 全事件流 + 2 fps 截屏 rolling buffer + Playwright Trace Viewer 兼容导出)
- Metrics dashboard + 报警规则(crash rate / SSE 保持时长 / adapter 日成功率 / patch 覆盖率)
- 回滚剧本与混沌演练
- 账号共享 / 桌面端主备的演进位置预留(`credential_holder` 抽象,§2 硬约束 9
- 账号共享 / 桌面端主备的演进位置预留(命名软约定,不在本版 DDL 落名;见 §2 硬约束 9
**退出目标**:patch 热更经过混沌演练;trace 能被外部 Playwright tooling 打开;运营指标全套接入报警;回滚剧本有实战演练记录。
@@ -1171,6 +1257,17 @@ v2.2 review 发现现有代码里 workspace 不是后端一等安全边界(`Ac
## 13. 变更记录
- **v2.42026-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 ownerruntime owner + provider owner 联签)、产物(`electron-chromium-cdp-matrix.md` + 抓包/截图)、**三态结论**supported / degraded / blocked)、blocked 时 fallback 决策树(DOM-only 降级 / 移出承诺范围 / 等 Chromium 升级)。
- **Modal 重构分阶段**Gemini C1):块 H 拆 H1Plan A 第 12 周:ui-shared 骨架 + design tokens + `useAccountSelection` composable+ H2(第 45 周: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 周不变。
- **v2.32026-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 底座一等化**(§1223 周,阻塞 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`。
@@ -1184,7 +1281,7 @@ v2.2 review 发现现有代码里 workspace 不是后端一等安全边界(`Ac
- **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 天未处理触发站内通知。
- **I2 · UNKNOWN 任务 UI 对账工作流**:§7.5 加 `POST /api/tenant/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 新增 23 周 + 块 H 工期修正 +1 周 + G 拆前后期)。§12 从 3 plan 拆成 4 plan。
- **v2.22026-04-18**Claude + Codex + Gemini 对 v2.1 的二轮交叉审查后的修订。修复 4 Critical + 9 Warning。
@@ -1211,7 +1308,7 @@ v2.2 review 发现现有代码里 workspace 不是后端一等安全边界(`Ac
- **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)。
- **C5** 任务协议引入 `attempt_id + lease_token` + extend/cancelmanual 模式入 `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 及以上为准)*
- **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)。
@@ -1243,17 +1340,91 @@ v2.2 review 发现现有代码里 workspace 不是后端一等安全边界(`Ac
这些条目在 Plan A spike 矩阵后更新为稳定值并记入 `docs/superpowers/specs/electron-chromium-cdp-matrix.md`。
## 15. 附录:需要补的图(Plan A 期补齐)
## 15. 附录:架构图
这是 v2.2 / v2.3 review 共同指出的文档短板,前端 + 新人理解链路需要**视觉化**:
### 15.1 Lease 状态机(inline · v2.4 在 Plan 0 前画完)
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` 的依赖关系)。
```mermaid
stateDiagram-v2
[*] --> queued: POST /publish-jobs
queued --> in_progress: lease acquired (target_client_id match)
in_progress --> in_progress: extend (每 60s)
in_progress --> waiting_user: POST /park (manual readyToReview)
waiting_user --> in_progress: POST /lease?from_parked (严格 CAS)
in_progress --> succeeded: POST /result status=succeeded
in_progress --> failed: POST /result status=failed
in_progress --> aborted: POST /cancel
waiting_user --> aborted: 72h timeout / Web cancel
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
unknown --> queued: POST /reconcile status=retry (新 attempt)
succeeded --> [*]
failed --> [*]
aborted --> [*]
这些图由 Plan A 期负责 spec 维护的同学用 Mermaid 补,放在 `docs/superpowers/specs/diagrams/` 目录,并在本 spec 相关章节里用 `![](diagrams/...)` 引用。
note right of waiting_user
parked state:
active_attempt_id = null
仍绑 target_client_id
不被抢走
end note
note left of unknown
仅 publish 任务
仅 submitting 崩溃
monitor 任务直接重跑
end note
```
### 15.2 Account CAS + Tombstone Resyncinline · 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
```
### 15.3 Plan A 期补齐的其他图
以下放在 `docs/superpowers/specs/diagrams/` 目录:
1. **全链路时序图**sequenceDiagram)——Web 点击发布 → Server 入库 → SSE 通知 → Desktop 领取 → 适配器执行 → 结果回传 → Web 更新。
2. **多实例部署架构图**——RabbitMQ fanout + 多个 server 实例 + Redis presence registry + 多个 desktop client。
3. **前端组件层级图**admin-web 与 desktop renderer 共享 `packages/ui-shared` 的依赖关系)。
## 16. 术语表
@@ -1264,7 +1435,7 @@ v2.2 review 发现现有代码里 workspace 不是后端一等安全边界(`Ac
- **Client Token**:一台 Electron 安装的后台身份,与 User JWT 并列;解耦 client_version,支持 rotate/revoke。
- **Partition**Electron `session.fromPartition('persist:<id>')`,每个账号一个隔离的 cookie/storage 容器。
- **Lease**:任务租约。领取任务时服务端返回 `attempt_id + lease_token + lease_expires_at`;后续所有写操作必须带 `lease_token` 验证。
- **Waiting-user 状态**manual 发布模式下,adapter 暂停等用户审核时,服务端将任务置为此状态并延长租约;在此状态下不会被重分配
- **Waiting-user 状态parked**manual 发布模式下,adapter 暂停等用户审核时,服务端**释放当前 lease**`active_attempt_id = null`)、状态置为 `waiting_user`;任务仍绑 `target_client_id` 不被抢走;用户回来时通过 `POST /tasks/{id}/lease?from_parked=true` 严格 CAS 重新领取(见 §5.3)。72h 超时自动 abort
- **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 通知。