Files
cmbuyer/docs/tasks/T-308.md
T

88 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: T-308
title: 客户端提交围栏与结果 sink
phase: 3
deps: [T-208, T-303, T-307]
status: TODO
created: 2026-08-04
vikunja_task_id: 49
context_ref: 8600c33
work_branch: task/t-308-submission-sink
needs_device: false
needs_human_review: true
write_paths:
- docs/tasks/T-308.md
- client/src/cmbuyer_client/submissions/**
- client/src/cmbuyer_client/core/**
- client/src/cmbuyer_client/remote/**
- client/src/cmbuyer_client/localstate/**
- client/tests/submissions/**
- client/tests/core/**
- client/tests/remote/**
- client/tests/localstate/**
---
<!-- BEGIN VIKUNJA EXPORT id=49 synced=2026-08-04T14:51:43Z sha256=ac3851bab830634e65029a7a80985946cb3aa5bf80bff1727e3e7e0fc8c30ad5 -->
## 问题 / 背景
T-303 只提供 claim/renew/evidence 的严格 HTTP、本地 DPAPI/SQLite 与恢复底座,T-307 只实现围栏前 events/fail,二者都明确禁止伪造 fence/result。T-208 将冻结服务端 CREATE_ONCE、LOOKUP_ONLY、一次性 result 与同一 submission 调和语义。T-308 补齐客户端提交围栏与结果 sink,并把许可、点击意图和结果恢复事实写入 T-303 的同一 localstate;本任务不连接真机、不接 UI、不执行或暴露任何点击。
## 关联需求与交互
- 功能:F-007、F-011、F-017。
- 用户故事:US-004、US-005、US-008。
- 交互:IX-011;只实现后台契约和恢复状态,不实现界面。
- 依赖:T-208、T-303、T-307。
- 后续消费者:T-400 离线单次提交安全编排。
- API:CREATE_ONCE / LOOKUP_ONLY submission fence 与一次性 submission result;严格消费 T-208 最终 wire。
## 方案
1. 定义窄 SubmissionSink:create_submission_fence、lookup_submission_fence、prepare_click_intent、report_submission_result。proof/result 值对象只含 T-208 固定 UUID、claim 归属、规范十进制金额、正整数、三闸门 AssetRef、精确 submit 文本/计数和固定 observation;不接收自由页面文本、XML、路径、selector、坐标或动作脚本。
2. HTTP 必须复用 T-303 的精确 loopback、禁代理/重定向、有界请求/响应和严格 JSON 解析。每次方法最多一次传输,不在 transport 或 sink 内隐藏重试。
3. 只扩展 T-303 的同一 SQLite/localstate,不建立第二个数据库。首次网络前原子保存 fence_key、operation、完整规范 proof、active claim 绑定和预期状态;DPAPI、SQLite 或归属校验失败时零 HTTP。
4. CREATE_ONCE 结果不明时只允许以原 fence_key、原 proof 调用 LOOKUP_ONLY。不得换 key、再次 CREATE_ONCE 或根据本地页面猜测围栏是否存在;LOOKUP_ONLY 找到或未找到都不得产生可点击许可。
5. 只有当前进程当前 CREATE_ONCE 调用收到首次、明确、完整且严格核对的 click_permitted=true 响应,并已把 submission 与许可事实 durable 落盘后,才由模块私有工厂返回 opaque SubmissionPermit。响应截断、协议漂移、落盘失败、重放、LOOKUP_ONLY 或 click_permitted=false 均不返回 permit。
6. SubmissionPermit 不提供公共构造器,不可 pickle/JSON/复制,不写入 SQLite、日志或异常。SQLite 只保存围栏和恢复审计事实;重启后无论数据库记录什么都不能重建可点击 permit,只能进入同一 submission 的调和路径。
7. prepare_click_intent 必须消费当前进程唯一 permit,在返回一次性 ArmedSubmission 前原子写入“即将发出唯一点击”。同 permit 二次调用、并发调用、active claim 漂移或已有点击意图均拒绝;重启只看到 reconcile-only 事实,不能重新 arm。
8. result_key、固定 observation 与 submission/attempt/冻结归属在首次 result HTTP 前原子保存。结果不明和重启只能重放原 key、原载荷;明确成功后保存服务端终态。同一 submission 不得创建第二个 observation 或把 UNKNOWN 改写成 SUBMITTED。
9. 401、403、409、非法 2xx、本地状态错误按 T-303 分类停止;网络、超时、5xx 只表示原幂等 HTTP 可调和,绝不表示 fence、permit、点击或页面动作可重试。
10. 构造和 import 闭包不包含 pdd、device、uiautomator2、UI、execution、app、通用 click、SubmitOrderPort、submit_order_once 或任何 payment 能力。
## 验收要点
- 覆盖首次 CREATE_ONCE、同键重放、LOOKUP_ONLY 找到/未找到、响应截断/漂移、落盘失败、401/403/409/503;只有首次明确且 durable 成功返回 opaque permit。
- 覆盖并发围栏、并发 prepare_click_intent、同 permit 二次消费、进程重启和 SQLite 恢复;任一非首次路径均无法获得或重建 permit/ArmedSubmission。
- 证明 fence/result 首次网络前已持久化完整键、载荷与 frozen claim 绑定;不明结果只重放原 HTTP 事实,不产生新 key 或第二 observation。
- 证明 SQLite/WAL、日志、异常和 DTO 不含明文 token、页面正文、XML、本机路径、selector、坐标或可点击对象。
- 静态 import/AST 检查证明 pdd/device/UI/app/execution、真机 click、submit_order_once 和任何付款能力不可达。
- client 全量 unittest、compileall、wheel metadata、完整 init、上下文、Vikunja export 与 diff-check 通过。
- needs_human_review:人工审阅 opaque permit 的构造边界、首次许可与重启不可重建证明;确认前不得标 DONE。
## 执行记录
(暂无)
<!-- END VIKUNJA EXPORT -->
## 边界
- 本任务只实现 T-208 已冻结的 `CREATE_ONCE`、`LOOKUP_ONLY` 与一次性 result 客户端契约,
并复用 T-303 同一 transport/localstate。不得另建 HTTP 客户端、SQLite、DPAPI 封装或第二份
权威恢复状态;本地与服务端事实冲突时只能停止并调和。
- 只有当前进程当前 `CREATE_ONCE` 调用收到首次、明确、完整且已 durable 落盘的
`click_permitted=true` 响应,才可由模块私有工厂产生 opaque `SubmissionPermit`。同键重放、
`LOOKUP_ONLY`、响应不明、解析/落盘失败、并发输家和服务重启均不得产生许可。
- `SubmissionPermit` 与一次性 `ArmedSubmission` 不得公开构造、序列化、复制、写入 SQLite 或从
恢复快照重建。SQLite 只能保存围栏、点击意图与调和事实;进程退出后即使尚不能确认是否点击,
也只能进入 `RECONCILE_ONLY`,不能重新 arm。
- fence/result 的 key、完整规范载荷和 frozen claim 绑定必须先原子持久化再发送。HTTP 结果不明
只能重放原 key、原载荷;不得换 key、再次 `CREATE_ONCE`、覆盖 observation 或把 HTTP 可恢复
错误解释为页面动作可重试。
- 本任务不得导入或修改 `client/src/cmbuyer_client/pdd/**`、`device/**`、`ui/**`、`execution/**`
或 `app.py`,不得连接真机、持有 selector/坐标/节点/点击对象,也不得实现 `SubmitOrderPort`、
`submit_order_once()` 或任何通用/具体点击。
- 本任务不编写支付、免密支付、先用后付或任何扣款能力。外部支付、安全校验与未知结果只能作为
T-208 固定 observation 保存并转调和,不能继续交互、释放围栏、重新授权、重新领取或再次提交。
- `needs_human_review: true`:自动验证完成后仍保持 `DOING`,等待人审 opaque permit 构造边界、
首次许可唯一性和重启不可重建证明;未经明确确认不得标 `DONE`。