103 lines
9.4 KiB
Markdown
103 lines
9.4 KiB
Markdown
---
|
||
id: T-303
|
||
title: 客户端 HTTP 任务源、证据 sink 与可恢复本地状态
|
||
phase: 3
|
||
deps: [T-002, T-204, T-302]
|
||
status: TODO
|
||
created: 2026-08-04
|
||
vikunja_task_id: 36
|
||
context_ref: bd4c855
|
||
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-04T13:41:52Z sha256=8de92c2f530442762dddf5c3a7b73115fb9c293bb3838ff01485feee0edade48 -->
|
||
## 问题 / 背景
|
||
|
||
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。
|
||
<!-- 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`、提交订单点击、支付、免密支付、先用后付或任何扣款控件代码。
|