docs(desktop-client): spec v2.5 closes 5 contract gaps from round-5 review
Fifth-round codex+gemini final verification: Gemini gave Ready-to-plan;
codex gave Ready-with-caveats with 5 surgical contract gaps. All 5
closed in v2.5:
1. reconcile status=retry semantics pinned: original task returns to
queued with attempts+1, no cloning, no new job. §6.4 route note,
§7.5 UNKNOWN reconcile table (renamed from "重新创建任务" to "重新
运行"), and §15.1 Mermaid all agree.
2. content_ref workspace boundary rule (Phase 1): tenant-native
entities (article_draft/template_gen/video/brand_asset) must
belong to actor's primary workspace, otherwise 422. Added to
Plan 0 cross-workspace pentration test exit bar.
3. WorkspaceScope input surface unified: removed workspace_id= query
from §6.8.2 modal example; scope solely derived from Actor.
4. Contract drift cleanup: GET /api/desktop/accounts?client=self
return fields add delete_requested_at; retry path clarified to
JSON body.
5. Parked Web cancel endpoint: new POST /api/tenant/tasks/{id}/cancel
(WorkspaceScope) for aborting parked/queued tasks; §15.1 Mermaid
updated with queued→aborted edge and endpoint labels.
Header upgraded to v2.5.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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=<n> # 软删除写 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:<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. 全部终态后 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 --> [*]
|
||||
|
||||
Reference in New Issue
Block a user