feat: 定义运行时规格解析契约与审计模型 (#254)

This commit is contained in:
chengma
2026-08-17 16:53:11 +08:00
parent b11a586527
commit 12414658d0
8 changed files with 736 additions and 16 deletions
+26 -1
View File
@@ -200,7 +200,7 @@ CREATE INDEX idx_pdd_tasks_visible_list
|---|---|
| `[必须]` 状态机没有 `pending` | 起点是 `claimed`,见 §7 |
| `[必须]` 已完成任务永久保留 | `succeeded` / `failed` / `cancelled` 的记录**不删**。崩溃恢复防重复下单依赖历史记录,见 §7.3 |
| `[必须]` 本地不保存 Admin 侧状态 | Client 拿到任务就做完,中途不查 Admin 怎么想。Admin 取消了、重派了,Client 一律不感知,照做完照提交,见 [04 接口契约](04-admin-api-contract.md) §1 |
| `[必须]` 本地不保存 Admin 侧状态 | Client 拿到任务就做完,中途不查 Admin 调度状态。采购规格无法精确选择时可以提交一次候选解析命令,但它不返回取消、重派等状态,见 [04 接口契约](04-admin-api-contract.md) §1、§7.1 |
| `[必须]` 提交一定会被接受 | Admin 必须无条件接受已派发过的结果,见 [04](04-admin-api-contract.md) §6.1。所以本地不需要"提交被拒"的处理分支 |
**关于"永久保留":** 任务记录不设保留期,一直留着。界面上的“删除”只写入
@@ -274,6 +274,27 @@ CREATE INDEX idx_task_runs_task
`result_data` 保存这次成功采集的完整结果;`pdd_tasks.pdd_data` 只保存最新结果。
这样重新采集可以更新当前数据,同时仍能按 `attempt_no` 追查旧结果。
### 4.1 采购运行时规格解析的持久化边界
[接口契约 §7.1](04-admin-api-contract.md) 定义了一个与 `task_runs.attempt_id` 绑定的一次性
规格解析命令。Admin 的 `purchase_spec_resolutions` 是服务端候选观察和最终决策的权威
审计;Client 本地仍必须在发送前保存以下最小信息,后续由工单 #257 增加 SQLite migration
和 Repository:
- `task_id`(本地外键)与 `attempt_id`;
- 完整且大小受限的请求 JSON、确定性 `Idempotency-Key` 和请求哈希;
- `candidate_snapshot_hash`、Admin `resolution_id`、outcome、source、可空置信度;
- matched 时 Admin 返回的候选短编号、原始文字和 options,以及收到时间。
这类请求需要同步取得响应才能决定本次采购是否继续,**不能伪装成普通结果 Outbox**;
但发送前仍必须持久化,网络重试只能重放相同键和相同内容。原始
`pdd_tasks.admin_payload`、`target_color/target_size` 永远不覆盖;解析结果只是当前
`task_runs` 的执行期有效规格。进入过 `irreversible_action_at` 的运行不得新建或重放解析
以继续采购,只能核对订单。
#254 只冻结接口和 Admin 审计模型,当前 Client SQLite schema 不在本工单改动。未实现
#257 的旧 Client 继续按“规格不匹配即失败”运行,不调用新接口,也不会出现半持久化状态。
## 5. `outbox_events`
保存等待提交 Admin 的可靠事件。
@@ -369,6 +390,10 @@ def save_result_and_enqueue(
<remote_task_id>:<attempt_id>:failure-v1 # 提交失败/人工处理
```
运行时规格解析不进入本表的普通结果 Outbox。它的确定性键是
`spec-resolution-v1:<identity-sha256>`,完整算法见 [接口契约 §7.1](04-admin-api-contract.md);
#257 必须把该键和原请求保存到独立的执行期解析记录后才发送。
```python
_KEY_SUFFIX = {
"collect_result": "result-v1",