feat(client): add durable HTTP task state
This commit is contained in:
@@ -31,7 +31,10 @@
|
||||
| 传输 | ADB(USB 或 WiFi) | 已定 | `uiautomator2` 3.x 走 adb 通道,`ip:port` 与 USB serial 同等对待 |
|
||||
| 桌面 GUI | `PySide6` | 已定 | 前序项目已验证;执行员需要看设备状态和批次进度 |
|
||||
| 截图处理 | `Pillow` | 已定 | 判断页面是否渲染完成,避免保存白屏壳层 |
|
||||
| HTTP 客户端 | 标准库 `urllib` 或 `httpx` | **待定** | 先用标准库;确有重试/连接池需求再评估 |
|
||||
| HTTP 客户端 | 标准库 `http.client` | 已定 | 只直连 `127.0.0.1:8080`;不读代理、不跟随重定向、不做隐藏重试或连接池 |
|
||||
| 可恢复状态 | 标准库 `sqlite3`(WAL / FULL) | 已定 | request/slot 先落库再 HTTP;每个操作独立连接,恢复读取使用单一事务快照 |
|
||||
| 秘密保护 | Windows Current User DPAPI | 已定 | token 原始 32 字节只以绑定 profile+device/attempt context 的密文 BLOB 入库;非 Windows 不降级 |
|
||||
| 单实例 | Windows `Global\` named mutex | 已定 | 以规范数据库路径 hash 命名,先于 DPAPI/SQLite 取得,覆盖同用户跨 session |
|
||||
| Excel | 不引入 | 已定 | Excel 解析移到采购服务;采购工具不再直接读表 |
|
||||
| 测试 | `unittest`(标准库) | 已定 | 前序项目 171 项测试均用标准库,无需 pytest |
|
||||
| 打包 | `pyinstaller` | 已定 | 交付给运营电脑;开发期依赖 |
|
||||
@@ -55,6 +58,10 @@
|
||||
Go 侧重写表头校验和行级报错,不能直接复用前序项目的 Python 实现。
|
||||
- **采购工具不持有业务权威。** 金额上限、授权有效性、任务状态流转的判定权在采购服务;
|
||||
采购工具本地校验只作为第二道防线,两边不一致时一律转人工。
|
||||
- **HTTP 不做自动重试。** claim/renew/evidence 的重放权属于持有 durable 幂等槽的恢复门面;底层每次
|
||||
方法最多一个请求。401 修复 Bearer 后、结果不明或重启恢复都必须复用原 key 与原 body/file。
|
||||
- **客户端 SQLite 只保存恢复事实。** append-only session/claim/renew/evidence history 不是服务端任务
|
||||
权威;它的作用是阻止崩溃、并发或状态损坏导致第二次领取、换图或换 key。
|
||||
- **不引入 pytest / 不引入 ORM。** 同一职责不并存两套方案。
|
||||
|
||||
## 五、构建与运行命令
|
||||
|
||||
+20
-1
@@ -381,6 +381,24 @@ DRAFT / PENDING / NEEDS_MANUAL ─管理员取消(围栏前)→ CANCELED
|
||||
- 围栏建立后即使租约过期也只恢复同一 `order_submission` 的调和,不能回到可领取队列。
|
||||
- 每种非终态都必须给出安全下一步,不能出现隐藏表单导致任务永久锁死。
|
||||
|
||||
#### 采购工具本地恢复状态
|
||||
|
||||
- 生产入口先取得基于规范数据库路径的 Windows `Global\` named mutex,再构造 DPAPI 和 SQLite;不能
|
||||
以服务端“单设备最多一个 claim”替代本机单实例。
|
||||
- profile、polling session、claim/renew request、open/historical claim、evidence marker/slot 使用 WAL、
|
||||
`synchronous=FULL` 与 append-only/单调关闭约束。当前 session/claim 由 `closed_at IS NULL` partial
|
||||
unique 保证唯一,历史关闭后不阻塞下一条,但不得删除或复活。
|
||||
- `DurableClientGateway` 是轮询和截图接入的唯一顺序入口:先 durable prepare,再一次 HTTP,最后原子
|
||||
commit。停止只把当前 session 的 `accept_new` 设为 false;飞行中响应仍提交,pending/open 不清除。
|
||||
- 每次发送前校验 profile/session/request/active immutable business snapshot、renew history、evidence marker/slot/
|
||||
receipt 的完整状态图;snapshot、renew response 与 evidence receipt 还保存不可变摘要。任一冗余事实不一致、
|
||||
DPAPI context 不匹配、数据库缺行或文件 identity 变化时零 HTTP 停止,不能自行“修复”。
|
||||
- 设备 Bearer 的 401 发生在服务端读取 body/写幂等事实前,因此槽保持 `PENDING`;只允许同 device id
|
||||
更新 Bearer,并在用户再次开始后用原 key/body/file 尝试。claim token 和其他 frozen 配置不变。
|
||||
- 原始 token 以 DPAPI current-user context 密文保存:device token 绑定 profile+device,claim/renew token
|
||||
绑定 profile+attempt,密文不能跨行复用;日志 formatter 对 Bearer、裸 64 位 token 和
|
||||
traceback 做最终脱敏。异常对象也只保留固定 reason,不挂接含响应 body/partial/path 的异常上下文。
|
||||
|
||||
### 5.4 证据分层
|
||||
|
||||
| 数据 | 位置 | 边界 |
|
||||
@@ -458,7 +476,8 @@ cmbuyer/
|
||||
└── scripts/
|
||||
```
|
||||
|
||||
执行器依赖 `TaskSource` / `ResultSink`,不直接读取 Excel 或拼接 HTTP。来源变化不得改变安全执行器。
|
||||
执行器依赖核心端口,不直接读取 Excel 或拼接 HTTP。T-303 只提供 `TaskSource` / `EvidenceSink`;完整
|
||||
`ResultSink` 在服务端 events/fail/fence/result 契约落地后分阶段组合,来源变化不得改变安全执行器。
|
||||
|
||||
## 九、架构纪律
|
||||
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ T-106 / T-107 是发布前只读验证,不是业务任务的第一趟。首次
|
||||
| --- | --- | --- | --- |
|
||||
| T-301 | 设备凭据与身份隔离(F-013) | T-201 | Bearer 不能建单/授权;管理会话不能领任务;凭据可撤销 |
|
||||
| T-302 | 已授权任务原子领取与租约(F-005) | T-301, T-203 | 只领 PENDING+有效授权;并发唯一;重放同一 attempt;claim token/generation 有效 |
|
||||
| T-303 | HTTP 任务源、证据 sink 与可恢复本地状态 | T-002, T-204, T-302 | 严格 claim/renew/evidence HTTP、DPAPI/SQLite、单实例和原子恢复槽;不伪造完整 ResultSink |
|
||||
| T-303 | HTTP 任务源、证据 sink 与可恢复本地状态 | T-002, T-204, T-302 | `http.client` 单次回环请求、DPAPI/SQLite append-only 状态图、Global 单实例和 durable gateway;不伪造完整 ResultSink |
|
||||
| T-304 | 定时轮询与会话边界 | T-303, T-006 | 人启动后轮询;停止只阻止下次领取;连续失败停;当前任务/记录详情双视图 |
|
||||
| T-306 | 规格面板原始截图与可靠证据上传 | T-104, T-303 | 只发布 Gate1 的显式原始 PNG;唯一恢复槽;可含页面地址/手机号;不上传 XML/路径或扩 kind |
|
||||
| T-307 | 客户端 attempt 事件与失败 sink | T-205, T-303 | 窄 events/fail HTTP 与同键恢复;不实现 ResultSink、围栏、结果、PDD 或 UI |
|
||||
|
||||
+36
-11
@@ -372,23 +372,48 @@ claim/renew 的格式错误固定为 `400 {"error":"invalid_request"}`,超限
|
||||
|
||||
## 三、采购工具本地模块合约
|
||||
|
||||
### `TaskSource` / `ResultSink`
|
||||
### T-303 已实现端口
|
||||
|
||||
```python
|
||||
class TaskSource(Protocol):
|
||||
def claim_next(self, session: Session) -> ClaimedPurchase | None: ...
|
||||
def renew_lease(self, claim: Claim) -> Lease: ...
|
||||
def claim_next(self, credentials: DeviceCredentials, request: ClaimRequest) -> ClaimedTask | None: ...
|
||||
def renew(self, credentials: DeviceCredentials, request: RenewRequest) -> RenewResult: ...
|
||||
|
||||
class ResultSink(Protocol):
|
||||
def append_events(self, claim: Claim, events: list[TaskEvent]) -> None: ...
|
||||
def upload_screenshot(self, claim: Claim, asset: ScreenshotAsset) -> AssetRef: ...
|
||||
def fail_attempt(self, claim: Claim, failure: AttemptFailure) -> None: ...
|
||||
def create_submission_fence(self, claim: Claim, proof: SubmissionProof) -> SubmissionPermit: ...
|
||||
def report_submission_result(self, permit: SubmissionPermit, result: SubmissionResult) -> None: ...
|
||||
class EvidenceSink(Protocol):
|
||||
def upload(self, credentials: DeviceCredentials, evidence: EvidenceUpload) -> AssetReceipt: ...
|
||||
```
|
||||
|
||||
执行器不能依赖具体 HTTP 或 Excel 实现。`SubmissionPermit` 只能由 `ResultSink` 的服务端成功响应构造,
|
||||
业务代码不能手工 new 一个许可。
|
||||
T-303 只实现 claim/renew 与 `SKU_PANEL_GATE_1` 单张 PNG;不得用运行时 `NotImplementedError` 伪造
|
||||
events/fail/fence/result 或完整 `ResultSink`。T-304/T-306 必须通过 `DurableClientGateway` 调用:它先把
|
||||
同一个 request/upload key 与完整载荷写入 SQLite,再最多发送一次 HTTP;401 保留 `PENDING`,由用户修复
|
||||
同一 device id 的 Bearer 后显式重放;网络、超时、503、截断、非法/未知 2xx 同样只保留原槽。协议/409
|
||||
终止槽但不换 key。成功响应落库失败时,重启仍用原 key 向服务端恢复事实。
|
||||
|
||||
金额按服务端合法域接受规范 ASCII 十进制正数字符串(最低 `0.01`,恰好两位小数、无前导零);wire
|
||||
整数为正 int64,拒绝 bool。claim 成功响应总上限 32 KiB;因此客户端不额外发明 goods/title/SKU/金额
|
||||
的单字段业务上限。RFC3339Nano 按 0--9 位小数的纳秒时间轴比较,不能用 Python 微秒精度截断。
|
||||
|
||||
### 本地恢复合约
|
||||
|
||||
- 数据库固定为 `%LOCALAPPDATA%\cmbuyer\state\client-state.sqlite3`,WAL + `synchronous=FULL`;Windows
|
||||
缺少 `LOCALAPPDATA` 时失败,不回退到 home 创建第二套状态。
|
||||
- device token 与 claim token 的原始 32 字节只以当前用户 DPAPI 密文 BLOB 入库;前者 context 绑定
|
||||
profile+device,后者绑定 profile+attempt,跨行交换密文会解密失败。claim token 不可更换;有
|
||||
pending/open 状态时冻结 service/device/ADB/transport/轮询与超时配置,仅允许同 device id 修复 Bearer;
|
||||
idle 时切换 device id 也必须同时提供新 token。
|
||||
- `Global\cmbuyer-<db-path-hash>` named mutex 在任何 SQLite/DPAPI 打开前取得;同一状态库跨 Windows
|
||||
session 只允许一个采购工具进程。
|
||||
- 运行根目录与数据库路径在构造时固化为绝对路径;之后 cwd 改变不得打开第二套库或绕过原 mutex。
|
||||
- polling session、claim history、renew request 和 evidence marker/slot 都是 append-only 历史;当前行用
|
||||
`closed_at IS NULL` partial unique 表示。恢复或发送前在同一 SQLite 读快照校验整张状态图,冗余列、
|
||||
snapshot、request、claim token、marker、slot、receipt 任一不一致都零 HTTP 失败闭合。
|
||||
- evidence 槽唯一键是 `(attempt_id, kind)`。首次发送前固定显式路径的 regular/non-reparse 文件 identity、
|
||||
size、mtime、SHA-256、IHDR 尺寸与字节;pending 时变化即停,并始终用首次保存的 exact metadata 重放
|
||||
相同 multipart。成功后 receipt 成为事实,源文件变化或删除只返回原 receipt,不再次上传;receipt
|
||||
尺寸必须与本地 IHDR 一致。客户端做签名/IHDR/尺寸/hash 防御;采购服务仍负责完整 PNG 解码权威校验。
|
||||
|
||||
完整 `ResultSink` 只有在 T-205/T-208 服务端契约完成后才由后续任务组合;`SubmissionPermit` 只能由
|
||||
服务端首次明确成功响应构造,业务代码不能手工创建。
|
||||
|
||||
### 真机能力分层
|
||||
|
||||
|
||||
+5
-1
@@ -28,7 +28,7 @@ write_paths:
|
||||
- docs/06-tasks.md
|
||||
---
|
||||
|
||||
<!-- BEGIN VIKUNJA EXPORT id=36 synced=2026-08-04T15:26:26Z sha256=c2a6a90372ea64ffd52684cc3832d25cfb6c0160bfe03463b7d67351964dc72c -->
|
||||
<!-- 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` 或占位请求。
|
||||
@@ -78,6 +78,10 @@ T-302 完成后,采购工具需要真实、安全、可恢复地领取/续租
|
||||
### 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 -->
|
||||
|
||||
## 边界
|
||||
|
||||
Reference in New Issue
Block a user