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

111 lines
10 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-303
title: 客户端 HTTP 任务源、证据 sink 与可恢复本地状态
phase: 3
deps: [T-002, T-204, T-302]
status: DOING
created: 2026-08-04
vikunja_task_id: 36
context_ref: 3a0a41d
work_branch: task/t-303-http-localstate
needs_device: false
needs_human_review: false
write_paths:
- docs/tasks/T-303.md
- client/src/cmbuyer_client/core/**
- client/src/cmbuyer_client/remote/**
- client/src/cmbuyer_client/localstate/**
- client/src/cmbuyer_client/runtime.py
- client/src/cmbuyer_client/logging_policy.py
- client/tests/core/**
- client/tests/remote/**
- client/tests/localstate/**
- client/tests/test_runtime.py
- client/tests/test_logging_policy.py
- docs/api.md
- docs/03-tech-stack.md
- docs/04-architecture.md
- docs/06-tasks.md
---
<!-- BEGIN VIKUNJA EXPORT id=36 synced=2026-08-04T16:54:47Z sha256=440e657c80301d7ef4cfcddd8a20bcd5b5672c06df30953246766ac02c67007d -->
## 问题 / 背景
T-302 完成后,采购工具需要真实、安全、可恢复地领取/续租任务;T-204 已提供受控截图上传接口。但 client 当前只有真机能力和最小窗口,没有 HTTP 抽象、设备凭据存储或断网/重启幂等状态。T-303 建立 `HttpTaskSource`、窄 `HttpEvidenceSink` 与 Windows 本地恢复底座;事件/fail/fence/result 尚分别依赖 T-205/T-208,本任务不伪造完整 `HttpResultSink` 或占位请求。
## 关联需求与交互
- 功能:F-005、F-007、F-013。
- 用户故事:US-003、US-004、US-007。
- 依赖:T-002、T-204、T-302;T-301/T-203 已由 T-302 传递满足。
- 后续消费者:T-304 定时轮询/配置 UI、T-306 真截图接入;完整结果 sink 在 T-205/T-208 后组合。
- 本任务不修改桌面 UI;只提供核心契约、HTTP 传输和可恢复本地状态。
## 方案
1. 定义不依赖 HTTP/UI/PDD 的 TaskSource、EvidenceSink、任务/授权/attempt/租约值对象;金额保持规范十进制字符串,UUID/时间/整数/布尔严格区分。不要创建运行时 `NotImplementedError` 的假 ResultSink。
2. HTTP 只接受精确 `http://127.0.0.1:8080`,使用标准库直连,不使用系统代理、不跟随重定向。每个方法只发一次请求,不在传输层隐藏重试;设置连接/读取超时和请求/响应上限。Bearer、device id、Content-Type 均由调用边界唯一生成。
3. 严格实现 T-302 最终 wire:claim/renew 的字段、4 KiB JSON 上限、200/204、固定 400/409/413/415、空 401/503;2xx 仍严格校验 Content-Type、UTF-8、重复 key、未知/缺失字段、UUIDv4、UTC `Z` RFC3339、64 位小写 token、金额和整数,超限或 schema 漂移视为结果不明而非成功。
4. `%LOCALAPPDATA%/cmbuyer/state/client-state.sqlite3` 以事务和 `synchronous=FULL` 保存配置、polling session、pending claim/renew/evidence 请求和 active claim。设备 token 与 claim token 只以 Windows 当前用户 DPAPI 密文 BLOB 保存;生产非 Windows 或 DPAPI/SQLite 异常失败闭合,测试注入 fake protector。数据库/WAL、日志、异常均不得出现明文 token。
5. Windows named mutex 保证同一 client 配置只有一个执行进程;服务端的单设备 claim 不能替代本机单实例。配置存在 pending/active claim 时禁止切换 service/device 身份。
6. 领取前先原子持久化 session_id+claim_request_id 再发请求。网络/503/截断/非法 2xx 后只保留并重放同一请求;明确 EMPTY 后下一轮才生成新 key。成功响应先 DPAPI 加密 token,再与完整快照单事务落库;响应与落库间崩溃可凭原 key 从服务端恢复同一 token。active 或未知 claim 结果存在时禁止领取第二条。
7. 续租前持久化 renew_request_id、session、attempt、generation、token 与 expected expiration;结果不明只重放完全相同载荷。成功后原子更新租约,不轮换 token、不递增 generation;过期、CAS/归属/幂等冲突不得换 key 续租、重新领取或自动转领。
8. EvidenceSink 只接收调用方显式传入的单个 PNG 与 T-204 元数据。首次调用前持久化 upload_key、hash 和文件身份;断网只重放同一 key/字节/元数据。文件缺失或 hash/大小变化停止,不枚举目录、不换截图、不上传 XML/manifest/本机路径/外部支付页,也不自行增加 claim token/session 字段。T-306 才把真实流程截图接入。
9. 停止/关闭语义冻结给 T-304:只阻止下一次新领取;已发 claim 必须处理或保留恢复,返回任务必须落 active;pending/active claim、renew 和证据不 release/abandon,不生成新 session/key,不自动恢复真机点击。租约过期交 T-207 人工处理。
10. 错误分类:本地状态/DPAPI 失败零 HTTP;401 停止并等凭据修复;403/协议型 4xx 停止;网络/超时/5xx 只允许同幂等键重放并由 T-304 计数;409 按固定 code 转人工且不换 key;`retryable` 绝不表示页面点击、围栏或提交可重试。
## 验收要点
- 覆盖 claim 成功/EMPTY/结果不明同 key 重放、进程重启恢复、响应漂移拒绝及 active claim 阻止第二次领取。
- 覆盖 renew CAS、同 key 重放、乱序/过期/冲突不复活,generation/token 不变化。
- 覆盖 DPAPI round-trip、损坏密文、SQLite/WAL 无明文 token、pending/active 时禁止换身份和本机双进程唯一。
- 覆盖精确 loopback URL、禁代理/重定向、严格 header/JSON/响应大小/RFC3339/UUID/金额/整数。
- 覆盖 evidence 显式 PNG、hash/大小、首次/重放、文件变化拒绝和绝不枚举/上传 XML/path。
- 覆盖停止发生在 claim 前/中/成功后均不丢 claim、不释放、不再领取;日志不泄露 Authorization/token。
- 静态检查 core/remote/localstate 不导入 PDD 点击、数量、确认页、event/fence/submit/payment 能力。
- client 全量 unittest、compileall、wheel metadata、根目录完整 init、上下文校验与 diff-check 通过。
## 执行记录
### 2026-08-04T13:27:53Z · ila
2026-08-04 T-304 预研反向约束 T-303:localstate 必须提供唯一的原子 API 给 UI 使用,至少覆盖 profile 配置读写、同 profile 单实例 guard、recovery snapshot、polling session start/resume/stop、pending claim/active claim 查询与原 session_id+claim_request_id 恢复;T-304 不得另写 SQLite/DPAPI/mutex。profile 需承载 service_url/device_id+加密 token、adb_path、serial、transport、poll interval、failure threshold、request/step timeout。当前服务端没有 heartbeat,配置页不能拿 claim-next 做无副作用连接探测,只能显示“凭据已安全保存,将在领取时验证”。
### 2026-08-04T13:41:49Z · ila
2026-08-04 T-306 预研反向收紧 T-303:localstate 必须按 (attempt_id, evidence_kind) 暴露唯一原子上传槽。首次 HTTP 前保存 upload_key、完整元数据、文件 identity/hash;结果不明只能恢复同一槽;成功后持久保留 AssetRef,重复调用返回原结果,不能清 pending 后生成新 key 或上传第二张。T-306 不得另建 SQLite。
### 2026-08-04T15:25:33Z · ila
2026-08-04 开始 T-303:基于 main@3a0a41d,在独立 worktree 实现客户端 HTTP 任务源、证据 sink 与可恢复本地状态;严格限制为 localhost 服务端契约,不触碰真机、页面操作、提交订单或付款能力。
### 2026-08-04T16:54:36Z · ila
2026-08-04 T-303 实现完成并进入审阅冻结:已落地严格 localhost HttpTaskSource/HttpEvidenceSink、durable gateway、DPAPI 身份绑定密文、SQLite append-only 状态图与 Global named mutex。claim/renew/evidence 均先持久化后最多一次 HTTP;business snapshot、renew response、evidence receipt 使用不可变约束与摘要,renew 从初始租约锚重放;证据绑定 PNG IHDR 尺寸并精确重放首次 metadata。独立对抗审计已通过。验证:client unittest 189/189,compileall、diff-check、agent-context 通过;根 init.ps1 通过 admin test/vet/build、client install/test/compile 与上下文门禁。任务保持 DOING,等待大脑固定提交复审,不标 DONE。
<!-- END VIKUNJA EXPORT -->
## 边界
- MVP 设备 Bearer 只允许发往精确 `http://127.0.0.1:8080`;不得接受用户信息、远程 host、其他
端口、重定向、系统代理或明文局域网地址。每个 transport 方法最多发送一次请求,重试决策必须
留给持有原幂等键的上层恢复流程。
- 设备 token 与 claim token 只能以当前 Windows 用户范围 DPAPI 密文进入本地 SQLite;不得以明文
进入数据库/WAL、日志、异常、Git、Vikunja 或测试 fixture。DPAPI、SQLite、密文或状态校验失败时
必须在零 HTTP 下失败闭合;生产非 Windows 不得降级为明文存储。
- Claim/renew/evidence 的幂等键和完整载荷必须先持久化再发请求。超时、断网、503、截断或非法 2xx
只能重放同一键同一载荷;不得生成新 key、领取第二条、续租复活、替换截图或覆盖本地 active claim。
- 停止轮询、Esc、关闭窗口和进程退出都不 release/abandon 当前 claim,也不自动恢复真机页面动作。
已发 claim 的未知结果和 active claim 必须跨重启保留;租约过期交 T-207 人工处理,不自动转领。
- T-303 必须向 T-304 暴露唯一的原子 profile/session/recovery API;T-304 不得另建 SQLite、DPAPI
或 mutex 状态源。服务端尚无 heartbeat 时,不得把 `claim-next` 当作配置页连接探测,否则“测试连接”
会产生 EMPTY 幂等事实甚至领取任务;配置页只能说明凭据将在真实领取时验证。
- EvidenceSink 只传调用方显式给出的单个 PNG 和 T-204 固定字段;不得枚举目录、读取或上传 XML、
manifest、本机路径、外部支付页或支付凭据,不得自行向 multipart 增加 claim token/session 字段。
- Localstate 必须按 `(attempt_id, evidence_kind)` 提供唯一、原子的上传槽:首次 HTTP 前持久化 upload key、
完整元数据、文件身份和 hash;结果不明只能恢复同一槽,同一槽成功后继续保留服务端 AssetRef,后续
调用只能返回该结果,不能清掉 pending 后生成新 key 或上传第二张。T-306 不得另建 SQLite 绕过它。
- 本任务不修改 `app.py` 或实现配置/轮询 UI,不连接真机,不导入 PDD 点击能力。T-304 负责桌面交互,
T-306 负责把真实截图接入 sink。
- 不伪造尚不存在的 event/fail/fence/result HTTP,不提供假 `HttpResultSink`,不实现或引用
`click_permitted`、提交订单点击、支付、免密支付、先用后付或任何扣款控件代码。