feat(tasks): implement atomic claims and leases

This commit is contained in:
QiuSW
2026-07-26 16:16:36 +08:00
parent 49db5b8305
commit ce875af889
50 changed files with 7589 additions and 207 deletions
+87 -18
View File
@@ -254,8 +254,10 @@ T-204 已实现的 `TASK_CREATED`、`TASK_CANCELED` 事件包含可空
}
```
T-203 只实现 `PENDING -> CANCELED`;其他状态返回 `409`。执行中设置取消请求及 App
安全检查点响应属于 T-205。
`PENDING/CLAIMED` 立即进入 `CANCELED`。`RUNNING/WAITING_CONFIRMATION` 只记录
取消请求并保持原状态;重复请求不重复写事件。App 的任务 heartbeat 会返回
`cancel_requested=true`,只有 App 在安全检查点调用 `cancel-ack` 后才进入
`CANCELED` 并结束 execution。
## 设备与领取
@@ -265,8 +267,8 @@ App 空闲或运行时上报设备状态;运行时任务续租使用任务专
```json
{
"device_id": "f0c3d438-1c44-4f80-b898-c645afe7eaa8",
"app_version": "0.1.0",
"android_version": "16",
"pdd_version": "device-observed-value",
"readiness": {
"accessibility_enabled": true,
@@ -276,14 +278,20 @@ App 空闲或运行时上报设备状态;运行时任务续租使用任务专
}
```
`device_id` 可以省略;若提供,必须与 Bearer token 中的设备一致。服务端时间是唯一
租约时钟。响应返回服务端认定的 `active_task_id`、`client_state_matches`、
readiness 上报时间和 `server_time`。heartbeat 超过配置 TTL 或任一就绪位为 false
时,claim 返回 `409 DEVICE_NOT_READY`。
### `POST /api/v1/tasks/claim-next`
由用户点击触发,在单个数据库事务中领取下一条任务。必须带 `Idempotency-Key`。
由用户点击触发,在单个 SQLite immediate transaction 中领取下一条任务。App 必须
在请求前生成并安全保存相互独立的 256 bit Raw URL `X-Claim-Token` 和
`Idempotency-Key`;网络结果不确定时复用同一组值。服务端只保存 token 的
SHA-256,响应和日志都不回显原 token 或 hash。
```json
{
"device_id": "f0c3d438-1c44-4f80-b898-c645afe7eaa8"
}
{}
```
有任务时:
@@ -297,22 +305,50 @@ App 空闲或运行时上报设备状态;运行时任务续租使用任务专
"sku": "BLACK-20L",
"description": "容量约20L,外观接近参考图",
"image_asset_id": "2bbc1bf2-30f7-497e-a52b-bcb4c61d57c3",
"reference_image_url": "/api/v1/tasks/37c9c715-9b51-4ed5-984e-66dad2710c71/reference-image?claim_generation=1",
"quantity": 2,
"max_budget": "200.00"
"max_budget": "200.00",
"currency": "CNY",
"version": 2,
"claim_generation": 1,
"claim_issued_at": "2026-07-25T08:30:00Z",
"claim_expires_at": "2026-07-25T08:40:00Z"
},
"claim_token": "returned-once",
"claim_expires_at": "2026-07-25T08:40:00Z"
"replayed": false,
"server_time": "2026-07-25T08:30:00Z"
}
```
没有任务返回 `204`。设备未就绪或已有活跃任务返回 `409`。幂等重放返回同一领取
结果;claim token 的安全重放形式在实现前通过测试固定。
当前设备自己有已过期 `CLAIMED` 时优先回收该任务,避免设备唯一归属冲突;否则按
`created_at ASC, id ASC` 选择 `PENDING` 或已过期 `CLAIMED`。没有任务返回 `204`;
同 key 的无任务重放始终保持 `204`。同 key、同 token 的活跃 claim 重放返回同一
任务,即使状态已进入 `RUNNING/WAITING_CONFIRMATION`;请求不同返回
`409 IDEMPOTENCY_CONFLICT`;原 claim 已释放、取消或被其他领取回收时返回
`409 CLAIM_REPLAY_EXPIRED`。同一设备不能再领取第二条活跃任务。
### `GET /api/v1/tasks/{task_id}/reference-image`
claim 响应给出的受保护参考图地址。请求使用 BUYER Bearer token、
`X-Claim-Token` 和 URL 中的 `claim_generation`;只有当前用户、当前设备、匹配
generation/token 且租约未过期的活跃任务可以读取。成功返回匿名规范化 JPEG,
设置 `private, no-store`、`nosniff`、长度和 ETag,不返回存储路径。
### `POST /api/v1/tasks/{task_id}/start`
把当前设备持有的 `CLAIMED` 任务改为 `RUNNING` 并创建 execution。请求带
`X-Claim-Token` 和 `Idempotency-Key`。
```json
{
"claim_generation": 1,
"expected_version": 2
}
```
同 key、同请求重放返回同一 execution;错误用户/设备/token 返回 `403`,过期租约、
状态或版本冲突返回 `409`。成功响应包含更新后的 `task`、`execution`、`replayed`
和 `server_time`,运行租约使用配置的 running lease。
### `POST /api/v1/tasks/{task_id}/heartbeat`
运行时续租并返回是否请求取消:
@@ -320,21 +356,56 @@ App 空闲或运行时上报设备状态;运行时任务续租使用任务专
```json
{
"execution_id": "e3190742-a24b-441e-b5c1-c7ed10ed342f",
"step": "SCAN_RESULTS",
"client_time": "2026-07-25T08:33:30Z"
"claim_generation": 1,
"step": "SCAN_RESULTS"
}
```
```json
{
"claim_expires_at": "2026-07-25T08:43:30Z",
"cancel_requested": false
"task": {
"id": "37c9c715-9b51-4ed5-984e-66dad2710c71",
"status": "RUNNING",
"version": 4,
"claim_generation": 1,
"claim_expires_at": "2026-07-25T08:35:00Z"
},
"execution": {
"id": "e3190742-a24b-441e-b5c1-c7ed10ed342f",
"current_step": "SCAN_RESULTS",
"order_submitted": false
},
"cancel_requested": false,
"server_time": "2026-07-25T08:33:30Z"
}
```
只接受 1 至 64 字节的大写 ASCII step。heartbeat 使用服务端 UTC 更新 execution、
设备最近在线时间、任务 version 和租约,不写高频任务事件;过期运行租约拒绝续租,
任务保持原非终态且绝不回到领取队列。
### `POST /api/v1/tasks/{task_id}/release`
只允许尚未开始的 `CLAIMED` 任务释放回 `PENDING`。运行中使用取消/失败流程。
请求带 `X-Claim-Token`、`Idempotency-Key`,body 与 start 相同。成功后清除当前
用户、设备、token hash 和租约,但保留递增过的 `claim_generation` 供审计。
### `POST /api/v1/tasks/{task_id}/cancel-ack`
App 收到取消请求并在安全检查点停止后调用:
```json
{
"execution_id": "e3190742-a24b-441e-b5c1-c7ed10ed342f",
"claim_generation": 1,
"expected_version": 5
}
```
请求带 `X-Claim-Token` 和 `Idempotency-Key`。只有匹配的未结束 execution、有效
运行租约和已存在的管理取消请求可以确认;成功把任务置为 `CANCELED`、结束
execution、清除 claim 秘密并追加带用户/设备 actor 的事件。同 key 重放不产生第二
个事件。
## AI 合约
@@ -488,7 +559,5 @@ App 空闲或运行时上报设备状态;运行时任务续租使用任务专
## 待实现前固定
- 上传大小、像素和保留期限的具体数值。
- access token、管理会话和 claim lease 的最终时长。
- claim token 幂等重放与轮换细节。
- VLM `confidence` 阈值、模型和提示词版本记录格式。
- 外部管理后台的服务账号认证方式和调用频率。