# 人工核查、人工重试与任务操作审计 状态:P3 已实现服务端、Go SDK 和[可运行场景](../examples/recovery.md)。与[自动安全重试](retries.md)不同,人工操作需要专门权限、原因、证据引用和当前版本。验证范围见[阶段报告](../reports/phase-3-task-recovery.md)。 ## 1. 公共规则 - HTTPS `Authorization: Bearer `,写入带 `Content-Type: application/json` 与稳定 `Idempotency-Key`;查询不带幂等键。 - 权限分别是 `tasks:review`、`tasks:retry`、`tasks:audit:read`,均校验任务当前命名空间和队列范围。默认提交及 Worker 凭据不包含它们;开发初始化显式 `--recovery-access` 才添加。真实生产应分离执行与管理身份。 - ID 为32位小写十六进制;`expected_revision` 是非零十进制字符串。正文最多512 KiB,拒绝未知字段、重复键、大小写别名及非允许的null。 - 写操作幂等作用域:命名空间 + `task.review` / `task.retry` + 原任务ID + key。同键不同内容409;当前鉴权通过后优先返回原回执,不因任务已终结或额度已满重新执行。幂等保留约定与[任务受理](tasks.md)相同。 - 网络/提交结果不明时持久保留**原键、原版本和完整正文**核对;不要换键。回执是操作当时快照,最新状态用 `GetTask`。SDK不自动生成身份或自动重试。 - `reason`:非空白UTF-8,1–1024个Unicode字符,无NUL。`evidence_ref`:1–512个Unicode字符,无空白/控制字符;使用受控记录引用,禁止放入凭据秘密。平台只保存引用,不请求地址或自动判定证据真实性。 ## 2. POST /v1/tasks/{task_id}:review 权限 `tasks:review`。仅 `NEEDS_REVIEW` 且版本匹配、无当前租约、无活跃尝试和未释放许可时允许。普通独立任务可核查;合法计划任务需同事务核验计划与触发记录归属。流程所属任务或有门控的任务拒绝,恢复须经对应上层控制器。 | 字段 | 类型/要求 | 含义 | | --- | --- | --- | | expected_revision | string,必填 | 当前待核查任务版本 | | reason / evidence_ref | string,必填 | 上述原因和证据边界 | | execution_settled | bool,必须显式true | 声明已核实全部历史执行已结束、不会继续产生效果 | | outcome | string,必填 | 仅SUCCEEDED或FAILED | | result | JSON,必填非null | 核查结论或业务结果引用,最多256 KiB,保留精确数值 | 先收集执行历史、迟到证据及外部系统凭证,核实所有旧执行和实际效果,再提交结论。仅“数据库租约已过期”不足以声明执行已经物理停止;平台无法杀死远程goroutine,也无法证明外部系统没有副作用。 请求示例,原状态为 NEEDS_REVIEW/revision=4: ```json { "expected_revision":"4", "reason":"已核实旧执行全部结束,操作未完成", "evidence_ref":"evidence:reconciliation-001", "execution_settled":true, "outcome":"FAILED", "result":{"confirmed":true,"sequence":9007199254740993} } ``` 首次及去重均返回HTTP200;完整示意响应: ```json { "request_id": "44444444444444444444444444444444", "data": { "task": { "id": "22222222222222222222222222222222", "namespace_id": "33333333333333333333333333333333", "queue_id": "11111111111111111111111111111111", "state": "FAILED", "revision": "5", "handler": "object.prepare", "handler_version": "v1", "payload_version": "v1", "payload": { "object_ref": "objects/input", "sequence": 9007199254740993 }, "business_key": "object:operation-001", "available_at": "2026-09-17T00:00:00Z", "deadline_at": "2026-09-17T00:01:00Z", "recovery_safety": "RECONCILE_ONLY", "retry": { "max_attempts": 1, "initial_delay_ms": 0, "max_delay_ms": 0, "attempt_timeout_ms": 10000 }, "attempt_count": 1, "result": { "confirmed": true, "sequence": 9007199254740993 }, "completed_at": "2026-09-17T00:00:20Z", "created_at": "2026-09-17T00:00:00Z", "updated_at": "2026-09-17T00:00:20Z" }, "audit_id": "55555555555555555555555555555555", "deduplicated": false } } ``` 事务内保存新终态/result、清除effect_uncertain、revision加一、设置completed_at,释放一次未终结额度;同时保存幂等回执、不可变审计及新的结果事件。失败整体回滚,不能只更新状态后补写审计。旧尝试及UNKNOWN结果不改写,迟到报告仍只追加证据;已有needs_review结果事件保持冻结,订阅会再收到更高版本的最终结果。串行计划可以在核查终结后推进后续触发。 SDK:`ReviewTask(ctx, taskID, key, orbit.ReviewTaskRequest)` 返回 `ReviewTaskResult`。 ## 3. POST /v1/tasks/{task_id}:retry 权限 `tasks:retry`。只接受**没有上层所有者的独立任务**,原状态FAILED或CANCELLED,effect_uncertain=false,版本匹配,无租约/活跃尝试/未释放许可。NEEDS_REVIEW先核查,SUCCEEDED不能重试;计划任务和流程任务均拒绝直接生成后继。 | 字段 | 类型/要求 | 含义 | | --- | --- | --- | | expected_revision | string,必填 | 原终态任务的当前版本 | | reason / evidence_ref | string,必填 | 重执行原因与安全依据 | | safe_to_retry | bool,必须显式true | 声明已确认旧执行结束,按原业务身份重新执行安全 | | task_timeout_ms | uint64,必填1–2592000000 | 新任务从受理起的总预算,最多30天;至少覆盖冻结的attempt_timeout_ms | | available_at | time/null,可选 | 新任务最早可执行时间,默认当前数据库时间 | | start_deadline_at | time/null,可选 | 新任务最晚开始时间,默认无独立限制,受新总截止时间约束 | 时间采用UTC RFC3339、微秒精度、年份2000–9998。available_at只允许最多1秒过去容差,必须早于新总截止;start_deadline_at必须晚于现在、不早于available_at、不晚于新总截止,详见[受理时间约束](tasks.md)。不继承原任务已经过期的绝对时间。 ```json { "expected_revision":"5", "reason":"已核实旧执行不会继续产生效果,允许重新执行", "evidence_ref":"evidence:safe-retry-001", "safe_to_retry":true, "task_timeout_ms":60000 } ``` 首次HTTP201,原键重报HTTP200且deduplicated=true。完整示意响应: ```json { "request_id": "77777777777777777777777777777777", "data": { "source_task_id": "22222222222222222222222222222222", "task": { "id": "66666666666666666666666666666666", "namespace_id": "33333333333333333333333333333333", "queue_id": "11111111111111111111111111111111", "state": "WAITING", "revision": "1", "handler": "object.prepare", "handler_version": "v1", "payload_version": "v1", "payload": { "object_ref": "objects/input", "sequence": 9007199254740993 }, "business_key": "object:operation-001", "available_at": "2026-09-17T00:00:30Z", "deadline_at": "2026-09-17T00:01:30Z", "recovery_safety": "RECONCILE_ONLY", "retry": { "max_attempts": 1, "initial_delay_ms": 0, "max_delay_ms": 0, "attempt_timeout_ms": 10000 }, "attempt_count": 0, "created_at": "2026-09-17T00:00:30Z", "updated_at": "2026-09-17T00:00:30Z", "retry_of": "22222222222222222222222222222222" }, "audit_id": "88888888888888888888888888888888", "deduplicated": false } } ``` 新任务保留原队列、处理器与输入版本、payload、business_key、concurrency_key、priority、recovery_safety和冻结重试策略;新ID、revision=1、attempt_count=0和新时间预算,`retry_of`指回原任务。原任务终态、版本、结果和尝试次数均保持不变。 每个原任务最多一个直接重试后继;不同键竞争时只有一个成功,其余409,继续人工重试应沿失败/取消的后继任务操作。同键重报返回同一子任务和audit_id。唯一索引 `(namespace_id,retry_of)` 兜底,关联存在与归属由事务校验,**无数据库外键**。 新任务占用原队列与应用额度,仍受背压/暂停/并发/限速/资源键控制;满额不创建子任务、不遗留幂等身份。人工声明不能替代业务幂等:业务操作身份仍须保持稳定,新taskID不能直接用作外部业务的去重身份。 `task.retry`审计记在原任务上,after包含retry_task_id;子任务的task.accepted事件包含retry_of和audit_id。任务详情、单项回执和Worker分配中的retry_of为新增可选字段;任务摘要列表没有该字段。 SDK:`RetryTask(ctx, taskID, key, orbit.RetryTaskRequest)` 返回 `RetryTaskResult`。 ## 4. 查询不可变操作审计 权限 `tasks:audit:read`,与普通tasks:read分开;当前凭据与队列范围逐次校验,不返回其他任务的审计或秘密。 `GET /v1/tasks/{task_id}/operations?limit=20&cursor=...`:limit默认20,1–100,cursor最长1024;参数只能出现一次且不能传未知筛选。按(created_at DESC,id DESC)分页;游标绑定命名空间、任务和查询种类。不是跨页快照,新的并发操作需从首页重新查询。 ```json { "request_id":"44444444444444444444444444444444", "data":{ "items":[{ "id":"88888888888888888888888888888888", "task_id":"22222222222222222222222222222222", "operation_id":"99999999999999999999999999999999", "actor_id":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "action":"task.retry", "created_at":"2026-09-17T00:00:30Z" }], "next_cursor":"<服务端返回的不透明游标>" } } ``` `GET /v1/tasks/{task_id}/operations/{audit_id}`:200详情在上述摘要基础上增加reason、可选evidence_ref、before和after;不存在或不属该任务返回404。 ```json { "request_id":"44444444444444444444444444444444", "data":{ "id":"88888888888888888888888888888888", "task_id":"22222222222222222222222222222222", "operation_id":"99999999999999999999999999999999", "actor_id":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "action":"task.retry", "created_at":"2026-09-17T00:00:30Z", "reason":"已核实旧执行不会继续产生效果,允许重新执行", "evidence_ref":"evidence:safe-retry-001", "before":{"state":"FAILED","revision":"5"}, "after":{ "source_task_id":"22222222222222222222222222222222", "retry_task_id":"66666666666666666666666666666666", "safe_to_retry":true, "task_timeout_ms":60000 } } } ``` 核查after包含state、revision、effect_uncertain=false、execution_settled=true及result_digest。摘要不复制完整业务输入;详情也不返回租约/凭据秘密。核查/重试的actor_id由实际认证凭据确定,不能在正文自报。审计可含受理、取消、执行结果等其他动作;查询操作记录不等于获得任务执行权。 SDK:`ListTaskOperations(ctx, taskID, HistoryPageRequest)`、`GetTaskOperation(ctx, taskID, auditID)`。 ## 5. 错误与兼容 | HTTP/错误码 | 处理 | | --- | --- | | 400 INVALID_ARGUMENT | 修正字段、显式确认、预算、JSON或游标,不触及任务 | | 401 UNAUTHENTICATED | 核对凭据到期/撤销与所属应用启用情况 | | 403 PERMISSION_DENIED | 当前凭据未获该管理/审计权限 | | 404 NOT_FOUND | 任务/操作不存在,或命名空间/队列/任务归属不匹配 | | 409 STATE_CONFLICT | 状态/版本/所有者/门控不允许,或已有直接重试后继;先读当前状态和审计 | | 409 IDEMPOTENCY_CONFLICT | 同操作身份正文变化,核对已保存原请求 | | 429 QUEUE_FULL / APPLICATION_FULL | 新任务受理背压,释放额度后原请求重试;错误携带retry_after_ms提示 | | 503 UNAVAILABLE 或网络错误 | 操作可能已提交,保持原身份核对 | 完整通用错误边界与请求大小限制见[任务接口](tasks.md)。新接口调用旧服务返回404,不静默改成重新SubmitTask。旧SDK忽略retry_of响应字段;使用additionalProperties=false的旧版严格Schema验证器需更新Schema。需要恢复接口与关联追踪时升级当前SDK。Worker协议仍为v1,retry_of是可选新增字段。 迁移000015不新增模型字段:明确中文retry_of语义,将相同列的历史非唯一索引替换为唯一索引,并增加禁止自关联CHECK。停止旧写入节点后迁移并统一升级;迁移遇到历史非法重复或自关联应失败,不能自动删除数据。跨版本混合负载和生产滚动发布尚未验收。