From 75fdc63db2fbd88baaca9df0f55731dc0241f1ca Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Tue, 28 Jul 2026 12:53:45 +0800 Subject: [PATCH] docs(t216): freeze device command delivery contract --- docs/00-ai-start-here.md | 2 +- docs/04-architecture.md | 19 ++++ docs/06-tasks.md | 4 + docs/08-interaction-checklist.md | 2 +- docs/api.md | 20 +++++ docs/current-state.md | 5 +- docs/routes.md | 5 +- docs/tasks/T-216.md | 149 +++++++++++++++++++++++++++++++ 8 files changed, 200 insertions(+), 6 deletions(-) create mode 100644 docs/tasks/T-216.md diff --git a/docs/00-ai-start-here.md b/docs/00-ai-start-here.md index bfb4de1..dddce04 100644 --- a/docs/00-ai-start-here.md +++ b/docs/00-ai-start-here.md @@ -58,7 +58,7 @@ T-205 原子领取/租约状态机、T-206 Android 登录/有限离线、T-207 证据回传、T-211 参考图召回和 SKU 硬匹配、T-212 候选身份映射,以及 T-213 受控 规格组合/价格核验均已完成。T-208 的原始候选观测、模型评估、确定性推荐、逐候选 结构化人工理由和修订历史也已完成。T-214 商品持久身份和重新定位指纹、T-215 -Admin 候选确认与不可变待投递授权也已完成;下一步实现 T-216 设备命令投递与确认。 +Admin 候选确认与不可变待投递授权也已完成;当前实现 T-216 设备命令投递与确认。 不得直接把候选链接或列表 ordinal 当成授权。 手机从管理后端领取任务并回传结果,VLM、拼多多自动化和人工确认在 App 本地完成。 T-206 增加有限离线执行;T-207 已复用 Roubao 端上 OpenAI 兼容适配器并加密本地 Key。 diff --git a/docs/04-architecture.md b/docs/04-architecture.md index 8ec1452..ed7cdb7 100644 --- a/docs/04-architecture.md +++ b/docs/04-architecture.md @@ -589,6 +589,25 @@ Admin task detail 追加新版本并把旧记录标记为 `SUPERSEDED`,设备领取后必须走显式停止/撤销协议。 浏览器不持有设备 claim token,授权表也不保存拼多多登录、支付或收货凭证。 +### 设备命令投递(T-216) + +Roubao 复用前台服务的 30 秒 heartbeat 主动 pull,不依赖 Android 后台推送: + +```text +flush candidate outbox + -> task heartbeat keeps lease + -> POST commands/next + -> backend validates claim + execution + active authorization + -> PENDING_DELIVERY -> DELIVERED and returns canonical command + -> App validates and saves encrypted PendingOrderCommand + -> POST commands/{id}/ack with same hash/idempotency key + -> DELIVERED -> ACKNOWLEDGED +``` + +HTTP 响应不确定时,服务端对 `DELIVERED/ACKNOWLEDGED` 重放相同 command 和 hash; +App 只有在加密状态提交成功后才 ACK。投递/确认与真正页面执行分离,T-216 收到命令 +后仍不打开拼多多。App 本地旧候选选择不再作为后台任务完成条件。 + 人工理由使用版本化 allowlist。接受或拒绝至少有一个理由且指定主要理由; `OTHER` 才要求 4-200 字备注。拒绝推荐后改选必须同时产生一条原推荐项负标签和一条 替代项正标签;全部无匹配时每个曝光候选都有负标签。人工修正追加新 review 并引用 diff --git a/docs/06-tasks.md b/docs/06-tasks.md index 3620e47..a260c00 100644 --- a/docs/06-tasks.md +++ b/docs/06-tasks.md @@ -48,6 +48,10 @@ | T-213 | 拼多多候选 SKU 组合与价格核验 | T-212 | 受控选择唯一颜色/尺码并读取组合价;不确认购买,不进入订单或支付 | | T-214 | 建立候选商品持久身份与重新定位指纹 | T-208、T-213 | candidate key 绑定原 observation、卡片/详情/双证据指纹;不伪造平台链接 | | T-215 | Admin 候选确认与一次性下单授权 | T-214 | 非空候选进入等待确认;Admin 按 candidate key 提交逐项理由并生成不可变待投递授权 | +| T-216 | 设备下单命令投递、确认与恢复 | T-215 | App 主动拉取;先加密落盘再 ACK;断网/重启重放同一授权且不执行拼多多动作 | +| T-217 | 已授权商品重新定位与订单 dry-run | T-216 | 重新核对持久指纹并唯一选择 SKU/数量,停在最终提交前 | +| T-218 | 单次订单提交与订单回读 | T-217 | 一次性提交后从订单列表读取订单号/时间并对账,不支付 | +| T-219 | Admin 待付款提醒与端到端验收 | T-218 | 展示可核对订单并提醒采购员去拼多多确认付款 | ## Phase 3:端到端验证 diff --git a/docs/08-interaction-checklist.md b/docs/08-interaction-checklist.md index f1da538..cf43e60 100644 --- a/docs/08-interaction-checklist.md +++ b/docs/08-interaction-checklist.md @@ -17,7 +17,7 @@ | IX-008 | US-006 | App/管理端错误状态 | 自动失败、取消、重试上传 | 显示结构化原因和恢复动作 | P0 | 已定 | | IX-009 | US-008 | App 候选理由/管理端决策详情 | 接受、拒绝、改选或修正 | 保存逐候选结构化人工标签 | P1 | T-208 已实现 | | IX-010 | US-009 | App 独立执行设置/同步状态 | 配置模式、离线执行或补报 | 授权内独立执行并可审计同步 | P0 | T-206 离线控制已实现,结果补报待 T-207 | -| IX-011 | US-010 | Admin 候选授权/App 订单执行 | 选择候选并授权、设备领取执行 | 创建一笔可对账的待付款订单并提醒人工付款 | P0 | T-215 至 T-219 | +| IX-011 | US-010 | Admin 候选授权/App 订单执行 | 选择候选并授权、设备领取执行 | 创建一笔可对账的待付款订单并提醒人工付款 | P0 | T-215 已完成;T-216 进行中 | ## IX-001 管理 Web 登录 diff --git a/docs/api.md b/docs/api.md index aa4281f..09e5c1a 100644 --- a/docs/api.md +++ b/docs/api.md @@ -279,6 +279,26 @@ Admin `GET /api/v1/tasks/{task_id}` 同时返回当前及历史 `order_authorizations`。授权仅允许 T-216 设备命令领取,不代表浏览器可以直接操作 手机,也不授权付款。 +### `POST /api/v1/tasks/{task_id}/commands/next` + +T-216 起由原 BUYER/设备主动拉取当前 execution 的下单命令。请求带 bearer、 +`X-Claim-Token`,body 提交 `device_id`、`execution_id` 和 `claim_generation`。 +服务端校验有效 claim/租约、`WAITING_CONFIRMATION`、未取消和同一 active +authorization。没有命令返回 `204`;首次投递把授权置为 `DELIVERED`,之后对 +`DELIVERED/ACKNOWLEDGED` 返回同一 schema v1 command。 + +命令只含 task/execution/content hash、原始 SKU/数量、candidate key、观测标题/ +规格/价格和 T-214 四个指纹,不含第三方 URL。`observed_ordinal` 仅是重新定位提示。 +`command_sha256` 固定 canonical payload,投递重试不得变化。 + +### `POST /api/v1/tasks/{task_id}/commands/{command_id}/ack` + +App 严格校验并写入 Keystore-backed 加密状态后,使用 bearer、claim token、 +`Idempotency-Key` 提交 execution、generation 和 `command_sha256`。成功把 +`DELIVERED -> ACKNOWLEDGED`;相同 key/body 和同 command/hash 可安全重试,其他 +command/hash/设备或无效租约返回冲突。ACK 只表示命令已可靠保存,不表示开始操作、 +创建订单或付款。 + ## 设备与领取 ### `POST /api/v1/devices/heartbeat` diff --git a/docs/current-state.md b/docs/current-state.md index 9fe2194..53cee50 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -5,7 +5,7 @@ ## 当前快照 - 日期:2026-07-28 -- 阶段:T-215 Admin 候选确认与一次性下单授权已完成,准备 T-216 +- 阶段:T-216 设备下单命令投递、确认与恢复进行中 - Git:当前分支为 `main`;T-001 至 T-004、T-101 至 T-104、T-201 至 T-207、T-209、 T-208、T-210、T-211、T-212、T-213、T-214 均已纳入 Git 历史 - 生产代码:`android-buyer/` 已接入 Roubao Android 源码 @@ -128,6 +128,7 @@ | `docs/tasks/T-208.md` | DONE | 归一化候选决策数据并增加结构化人工 review | | `docs/tasks/T-214.md` | DONE | 建立 execution-scoped candidate key 与设备采集指纹 | | `docs/tasks/T-215.md` | DONE | Admin 按 candidate key 选择并创建不可变待投递授权 | +| `docs/tasks/T-216.md` | DOING | App 主动拉取并先加密落盘再确认同一条下单命令 | | `docs/design/` | 已确认 | T-202 原型索引、4 个管理页和 7 个 Android 页面 | | `deepseek总结.txt` | 已有 | 历史讨论摘要,不是正式需求权威 | | `android-buyer/` | 已有 | Roubao `main` 固定 commit 的 Android 基线 | @@ -139,7 +140,7 @@ ## 任务摘要 - 已完成:T-001 至 T-004、T-101 至 T-104、T-201 至 T-215。 -- 正在进行:准备 T-216 设备命令投递与确认。 +- 正在进行:T-216 设备下单命令投递、确认与恢复。 - 下一步:依次实现设备命令、订单 dry-run、单次提交对账和 付款提醒。 diff --git a/docs/routes.md b/docs/routes.md index 32483f2..044a968 100644 --- a/docs/routes.md +++ b/docs/routes.md @@ -45,7 +45,8 @@ Android 导航名称是逻辑目的地,具体 Compose/Fragment 形式待接入 | `POST /api/v1/tasks/{id}/cancel-ack` | 安全停止后确认 `CANCELED` 并结束 execution | | `POST /api/v1/tasks/{id}/events` | 幂等补报本地执行事件 | | `POST /api/v1/tasks/{id}/candidates` | 保存最多 5 个候选、模型判断和本地推荐 | -| `POST /api/v1/tasks/{id}/commands/next` | T-216 起领取 Admin 已创建的设备命令 | +| `POST /api/v1/tasks/{id}/commands/next` | 拉取或重放 Admin 已创建的同一条下单命令 | +| `POST /api/v1/tasks/{id}/commands/{command_id}/ack` | App 加密落盘后幂等确认命令 | | `POST /api/v1/tasks/{id}/complete` | 提交人工结果,强制 `order_submitted=false` | | `POST /api/v1/tasks/{id}/fail` | 提交结构化失败和受控证据 | @@ -59,7 +60,7 @@ T-207 的 events/candidates/complete/fail 接受原设备对授权内已产生 ## 导航规则 - App 有活跃任务时启动后优先恢复该任务,不允许直接领取下一条。 -- `CLAIMED` 进入预览;`RUNNING` 进入执行;`WAITING_CONFIRMATION` 进入候选确认; +- `CLAIMED` 进入预览;`RUNNING` 进入执行;`WAITING_CONFIRMATION` 进入等待 Admin; 终态进入结果。 - 从执行页切换拼多多后,使用前台服务通知返回执行页。 - 候选确认页返回不能恢复自动化。 diff --git a/docs/tasks/T-216.md b/docs/tasks/T-216.md new file mode 100644 index 0000000..d66a137 --- /dev/null +++ b/docs/tasks/T-216.md @@ -0,0 +1,149 @@ +--- +id: T-216 +title: 设备下单命令投递、确认与恢复 +phase: 2 +deps: + - T-215 +status: DOING +created: 2026-07-28 +context_ref: 827afc7 +work_branch: null +write_paths: + - docs/tasks/T-216.md + - docs/00-ai-start-here.md + - docs/02-requirements.md + - docs/04-architecture.md + - docs/06-tasks.md + - docs/08-interaction-checklist.md + - docs/api.md + - docs/current-state.md + - docs/routes.md + - backend-api/migrations/** + - backend-api/internal/domain/** + - backend-api/internal/usecase/** + - backend-api/internal/repository/sqlite/** + - backend-api/internal/transport/httpapi/** + - android-buyer/app/src/main/** + - android-buyer/app/src/test/** +--- + +## 问题 / 背景 + +T-215 只生成 `PENDING_DELIVERY` 下单授权。Android 仍停留在旧的手机人工选择流程, +也没有可靠方式知道 Admin 已授权哪个 candidate。直接在 Admin 浏览器推送或收到 HTTP +响应后立即执行都无法处理 Android 后台限制、断网、进程重启和响应丢失。 + +本任务把授权作为同一 execution 的可重放设备命令,由 Roubao 的现有前台服务主动 +拉取。App 必须先把完整命令写入 Keystore-backed 加密状态,再确认收妥。T-216 不 +执行拼多多页面动作。 + +## 冻结合约 + +### 拉取而非服务端推送 + +1. App 在非终态 execution 的 30 秒 heartbeat 后,以及用户点击“立即同步”时调用 + `POST /api/v1/tasks/{task_id}/commands/next`。不增加 WebSocket、FCM 或浏览器到 + 手机的直连通道。 +2. 请求必须携带 BUYER bearer、`X-Claim-Token`,并提交当前 `device_id`、 + `execution_id`、`claim_generation`。服务端校验 task、user、device、execution、 + generation、token、有效租约、`WAITING_CONFIRMATION` 和未请求取消。 +3. 没有命令返回 `204`。当前 active authorization 为 + `PENDING_DELIVERY/DELIVERED/ACKNOWLEDGED` 时返回同一命令;一个 execution 不会 + 返回第二条 active 命令。 + +### 命令内容与持久身份 + +命令 `schema_version=1`、`type=CREATE_PENDING_ORDER`,只包含服务端快照和受控 +observation,不包含第三方 URL: + +```json +{ + "id": "authorization-uuid", + "schema_version": 1, + "type": "CREATE_PENDING_ORDER", + "authorization_version": 1, + "task_id": "uuid", + "execution_id": "uuid", + "task_content_sha256": "64-char-hex", + "original_sku": "雾霾蓝 / L", + "quantity": 2, + "candidate": { + "candidate_key": "64-char-hex", + "observed_ordinal": 1, + "title": "实际观测标题", + "sku_text": "雾霾蓝 / L", + "price_text": "39.80", + "card_signature": "64-char-hex", + "detail_signature": "64-char-hex", + "detail_evidence_sha256": "64-char-hex", + "specification_evidence_sha256": "64-char-hex" + }, + "command_sha256": "64-char-hex", + "authorization_status": "DELIVERED" +} +``` + +1. `observed_ordinal` 只用于重新定位提示,不能直接作为点击索引;T-217 必须重新核对 + card/detail/SKU/证据。 +2. `command_sha256` 是 schema v1 除自身和投递状态外的 canonical JSON SHA-256。 + 首次投递在事务内写入该 hash、`delivered_at` 和 + `ORDER_AUTHORIZATION_DELIVERED` 事件;重投不新增事件。 +3. 已 `ACKNOWLEDGED` 但 App 本地状态丢失时仍允许同一身份重新拉取,以便安全恢复; + 不允许因此改写 hash 或创建新命令。 + +### App 先持久化后确认 + +1. 新增加密 `PendingOrderCommand`,保存完整快照、command hash、ACK 幂等键和确认 + 状态。先验证 schema/type、task/execution/content hash、数量和所有指纹,再一次 + `store.save`;保存失败不得发送 ACK。 +2. 保存成功后调用 + `POST /api/v1/tasks/{task_id}/commands/{command_id}/ack`,请求包含 execution、 + generation 和 command hash,并带 claim token 与 `Idempotency-Key`。 +3. ACK 在事务内把 `DELIVERED -> ACKNOWLEDGED`,写 `acknowledged_at`、幂等记录和 + `ORDER_AUTHORIZATION_ACKNOWLEDGED` 事件。相同 key/body 或同 command/hash 重试 + 返回相同结果;hash、归属、状态或身份不匹配返回冲突。 +4. HTTP ACK 响应丢失时 App 保留同一 ACK key 重试。只有后端确认后本地 + `acknowledged=true`,UI 显示“后台已授权,等待订单核验”;进程重启继续 ACK, + 不能重新采集或领取其他任务。 + +### 旧手机人工确认收口 + +1. 后台任务非空候选上传成功后,App 进入 `WAITING_ADMIN_CONFIRMATION`,不再显示 + 本地接受/拒绝候选按钮,也不提交旧的 `HUMAN_REVIEW + COMPLETE`。 +2. 空候选仍可按现有安全终态回传;独立本地探针不受后台任务模式影响。 +3. 前台服务在等待 Admin 时继续 heartbeat、同步 outbox 和拉取命令;租约到期、 + 取消、认证失效或命令校验失败时保持安全停止。 + +## 数据模型 + +v9: + +- `order_authorizations` 增加不可变 `command_sha256` 和投递计数/最后投递时间; +- `device_order_command_ack_requests` 保存 device、幂等键、请求 hash、command 和时间; +- `task_events` 增加 `ORDER_AUTHORIZATION_DELIVERED` 与 + `ORDER_AUTHORIZATION_ACKNOWLEDGED`。 + +存在命令投递/ACK 数据或新事件时 destructive down 必须失败。 + +## 验收要点 + +- [ ] 只有原 user/device/execution/generation/token 且租约有效时能拉取命令。 +- [ ] 首次拉取原子进入 `DELIVERED`;网络重放返回相同 command/hash 且不重复事件。 +- [ ] App 严格校验并先加密持久化,存储失败不 ACK,重启可恢复同一命令。 +- [ ] ACK 幂等进入 `ACKNOWLEDGED`,错误 hash/command/设备和并发请求被拒绝。 +- [ ] 后台候选流程等待 Admin,不再允许手机本地选择并提前完成任务。 +- [ ] App 等待/已授权状态可见,但不打开拼多多、不选择 SKU/数量、不提交订单。 +- [ ] v9 migration、Go test/race/vet、Android test/Debug/Release 和根验证通过。 + +## 边界 + +- 不重新打开图片搜索或定位 Admin 选择的商品;属于 T-217。 +- 不选择 SKU/数量、进入确认订单页或比较最终金额;属于 T-217。 +- 不点击确认购买、读取订单号或处理付款;属于 T-218/T-219。 +- 不新增 FCM/WebSocket,不把第三方 URL 当命令,不授权支付。 + +## 执行记录 + +- 2026-07-28:T-215 实现提交 `827afc7` 后领取。复用现有 30 秒前台 heartbeat 和 + Keystore-backed 状态,选择 pull + durable-before-ACK,避免 Android 后台推送和 + HTTP 不确定结果造成命令丢失。