diff --git a/docs/superpowers/specs/2026-04-18-electron-desktop-client-design.md b/docs/superpowers/specs/2026-04-18-electron-desktop-client-design.md index 1a71870..1c75345 100644 --- a/docs/superpowers/specs/2026-04-18-electron-desktop-client-design.md +++ b/docs/superpowers/specs/2026-04-18-electron-desktop-client-design.md @@ -1,11 +1,11 @@ # Electron 桌面客户端设计(全面替换 WXT 浏览器插件) -**状态**:Draft v2.4 · 待实施(历经 4 轮 Claude + Codex + Gemini 交叉审查后修订) +**状态**:Draft v2.5 · 待实施(历经 5 轮 Claude + Codex + Gemini 交叉审查后修订) **作者**:@liangxu + Claude(codex/gemini 交叉审校) **日期**:2026-04-18 **范围**:新增 `apps/desktop-client/`、`/api/desktop/*` 服务端路由、相关 DB schema;删除 `apps/browser-extension/` 与 `/api/plugin/*` 路由及相关代码。 -> **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。 +> **v2.5 主要修订**(基于 v2.4 final verification Codex 审查的 5 个契约口子收尾):`reconcile status=retry` 语义写死(原 task 回 queued)+ Phase 1 下 `content_ref` 引用 tenant-native 实体的 workspace 边界规则(只允许 primary workspace) + WorkspaceScope 输入面一致性(删 `workspace_id=` query 示例)+ 补 `delete_requested_at` 返回字段 + 新增 `POST /api/tenant/tasks/{id}/cancel` 端点 + §15.1 Mermaid 与正文一致性修正。完整历史(v1 → v2.5)详见 §13。 --- @@ -536,10 +536,12 @@ 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 + # retry 语义:原 task status → queued + attempts + 1(不复制、不新建 job);与 §15.1 状态图一致 +POST /api/tenant/tasks/{id}/cancel # body: {reason?} · 对 parked(waiting_user)或 queued 任务主动取消 → status=aborted;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} +GET /api/desktop/accounts?client=self # resync 用:返回 {id, platform, platform_uid, display_name, health, client_id, sync_version, deleted_at, delete_requested_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 / tags;body 带 if_sync_version 做 CAS DELETE /api/desktop/accounts/{id}?if_sync_version= # 软删除写 tombstone,非 hard delete @@ -727,7 +729,7 @@ Web UI 可视化 流程: -1. 打开:`GET /api/tenant/accounts?kind=publish&workspace_id=...` 拉回 workspace 下所有发布账号。响应按 `(platform, account)` 两层结构组织,每条带: +1. 打开:`GET /api/tenant/accounts?kind=publish` 拉回 workspace 下所有发布账号(**workspace scope 由 WorkspaceScope middleware 从 Actor 派生,不接受 query/body 选择**——见 §6.4 中间件要求)。响应按 `(platform, account)` 两层结构组织,每条带: - `online`(由 server 根据该账号所属 `client_id` 的 `last_seen_at < 5 min` 判定) - `health`(`live / expired / captcha / risk`) - `lastPublishAt`(上次成功发布时间) @@ -755,10 +757,12 @@ Web UI 可视化 } ``` `content_ref.type` 支持 `article_draft` / `template_gen`(KOL 模板生成)/ `video` / `brand_asset` 等,后续按需扩枚举;server 根据 type 在解析阶段拉对应实体并附到每条 desktop_task 的 payload 里。 + + **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 防穿透用例之一)。 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:`;接收每子任务状态变化(`queued → in_progress → waiting_user → succeeded/failed/unknown/aborted`)。 5. manual 模式任务进入 `waiting_user` 后,面板上显示"账号 X 等待桌面端审核",带"前往桌面端"按钮 → `georankly://tasks//review`(未装客户端时跳下载页)。 -6. 全部终态后 Modal 汇总:每账号结果 URL / 错误码;对失败子任务点"重试" → `POST /api/tenant/publish-jobs/{job_id}/retry?task_ids=...`。 +6. 全部终态后 Modal 汇总:每账号结果 URL / 错误码;对失败子任务点"重试" → `POST /api/tenant/publish-jobs/{job_id}/retry` body `{"task_ids": [...]}`。 #### 6.8.3 媒体数据面板(账号级粒度) @@ -972,9 +976,9 @@ UNKNOWN 任务在 admin-web "事件中心"和"发布历史"页有专门区块, | **"已确认发布成功"** | `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 作为审计记录,新建一条跑 | +| **"重新运行"** | `POST /api/tenant/tasks/{id}/reconcile {status: 'retry'}` | **原 task status → `queued`,`attempts + 1`,`active_attempt_id = null`**;等 target_client 通过 §6.3 lease 流程重新领取。不复制、不新建 job,任务 ID 保持原值 → 历史可追溯 | -**关键**:UNKNOWN 不会自动消失——用户必须显式选一项。超过 7 天没处理的 UNKNOWN 任务每周触发一次站内通知给 workspace owner。 +**关键**:UNKNOWN 不会自动消失——用户必须显式选一项。超过 7 天没处理的 UNKNOWN 任务每周触发一次站内通知给 workspace owner。"重新运行"的 retry 语义与 §15.1 Mermaid 状态图 `unknown → queued` 一致。 --- @@ -1257,6 +1261,12 @@ v2.2 review 发现 workspace 不是后端一等安全边界。但**当前代码 ## 13. 变更记录 +- **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` 标注具体端点。 - **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 及以上为准"。 @@ -1348,18 +1358,19 @@ v2.2 review 发现 workspace 不是后端一等安全边界。但**当前代码 stateDiagram-v2 [*] --> queued: POST /publish-jobs queued --> in_progress: lease acquired (target_client_id match) + queued --> aborted: POST /api/tenant/tasks/{id}/cancel (Web) in_progress --> in_progress: extend (每 60s) in_progress --> waiting_user: POST /park (manual readyToReview) - waiting_user --> in_progress: POST /lease?from_parked (严格 CAS) + waiting_user --> in_progress: POST /lease?from_parked=true (严格 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 --> aborted: POST /api/desktop/tasks/{id}/cancel (client) + waiting_user --> aborted: 72h timeout / POST /api/tenant/tasks/{id}/cancel (Web) 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) + unknown --> queued: POST /reconcile status=retry (原 task 回 queued + attempts+1) succeeded --> [*] failed --> [*] aborted --> [*]