Files
th-hotel-simple/docs/project/requirements/M002-superagent-task-result-api-contract.md

487 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# M002 SuperAgent Task Result API Contract
> 文档状态:阶段记录。本文记录 M002 阶段 SuperAgent 提交 AI 任务结果的入站接口设计。
> 当前对外接口总契约以 `../integrations/superagent-api-contract.md` 为准;
> 如字段、错误码、鉴权或请求范围出现重复描述,以总契约和当前代码实现为准。
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 文档版本 | 0.6 |
| 日期 | 2026-07-12 |
| 状态 | V2 兼容 + M002 V3 CP1-CP7 入站解析、同卡复核、P0 fixtures 回归和 P0.1 Parent Group 路由修订;后续业务流程以 `M002-order-task-workflow-v3.md` 为准 |
| 适用范围 | SuperAgent / Main Agent 调用本系统提交 AI 任务结果 |
| 主要读者 | 后端、SuperAgent 对接方、测试、后续协作 agent |
## 1. 文档定位
本文定义 SuperAgent / Main Agent 向本系统提交 V3 `source_message + message_events[]` 业务根、结构化 S10/S99 入口通知、V2 `ai_task_results[]` 兼容结果,以及旧 S000/S999 特殊入口结果的后端接口契约。
本文承接 `M002-order-task-workflow-v2.md`,只定义本系统入站接口、鉴权、幂等、请求响应和技术校验边界,不定义 SuperAgent 内部 prompt、Skill 实现、OPERA 真实接口或前端展示细节。
2026-07-11 后M002 后续开发基线已迁移到 `M002-order-task-workflow-v3.md`。2026-07-12 起Parent Group / Allotment 路由采用 P0.1 增量修订:当前后端目标为结构化 `S10/S99` 入站、V3 业务根基础解析、40 路由枚举 / 稳定配置、`UNHANDLED_CURRENT_INTENT` 和 route 相关字段最小落库、type-known manual review 同卡解阻第一版、typed `infrastructure_input_error` 响应、P0 fixtures 回归基线和 Parent split `Cancel Allotment` 路由。旧 `S000/S999``ai_task_results[]` 仍作为兼容路径保留。对外联调以 `docs/project/integrations/superagent-api-contract.md` 为准。
## 2. 接口概览
| 项目 | 内容 |
| --- | --- |
| Method | `POST` |
| Path | `/api/integrations/superagent/task-results` |
| Content-Type | `application/json``text/plain` |
| 响应格式 | `application/json` |
| 一次请求范围 | 只能包含一个 `source_message_id` |
| 业务动作 | 接收 AI 结果或入口结果、写入 AI 过渡层,按可支持路由生成订单 / 任务 / 任务卡 |
| 鉴权方式 | HMAC-SHA256 签名 |
中文说明:
- 该接口是服务到服务的入站接口,不给前端直接调用。
- SuperAgent 不直连数据库,只能通过本接口提交任务结果或入口处理结果。
- 本接口只做技术校验和系统接收,不替代用户确认和 OPERA 模拟操作。
- `application/json` 用于 V3 结构化 `S10/S99`、V3 业务根或 V2 `normal_task` / `manual_review` 兼容结构化任务。
- `text/plain` 用于旧 `S000,source_message_id` / `S999,source_message_id` 特殊入口结果兼容。
- P0.1 后V3 业务根中的完整 Parent split 父事件必须使用 `event_type=Cancel Allotment``task_subtype=cancel_allotment_control_block`;当前新入站不接受 `Cancel Booking + linked_parent_release_after_child_split` 作为合法业务任务。V3 `message_events[]` 中的旧三元组按 event 写入 adapter contract error旧 V2 `ai_task_results[]` 兼容入口提交该三元组时按请求级 `ADAPTER_CONTRACT_ERROR` 拒绝。
### 2.1 SourceMessage ID 口径
本接口请求体中的 `source_message_id` 是 SuperAgent / Main Agent 从通用溯源字段中原样带回的外部来源消息 ID对应 AgentBus 邮件 payload 的 `source.external_message_id`,不是 `platform_source_message_inbox.id`
后端正式处理路径:
```text
系统酒店 + source_message_id
→ platform_source_message_inbox.external_message_id
→ platform_source_message_inbox.id
→ workflow_* 表 source_message_id 内部外键
```
数据库 `workflow_*` 表中的 `source_message_id` 仍然保存内部 SourceMessage Inbox ID。只有对外接口的 `source_message_id` 使用外部来源消息 ID。任务结果通知不要求 SuperAgent 传数据库层 `source_provider` / `source_channel`;后端按系统酒店和外部消息 ID 查唯一 Inbox 记录,真实 provider/channel 以 SourceMessage Inbox 入库值为准。无 `hotel_id` 时仅兼容本地旧夹具使用内部数字 ID正式 SuperAgent 调用不得依赖该兼容路径。
## 3. 鉴权方案
第一版使用 HMAC-SHA256 签名,做到简单、可实现、可防重放。
### 3.1 环境变量
| 环境变量 | 是否必填 | 中文说明 |
| --- | --- | --- |
| `SUPERAGENT_DEV_TASK_RESULT_HMAC_SECRET` | dev 必填 | dev SuperAgent 任务结果入站签名密钥,只能通过 Secret 注入 |
| `SUPERAGENT_TEST_TASK_RESULT_HMAC_SECRET` | test 必填 | test SuperAgent 任务结果入站签名密钥,只能通过 Secret 注入 |
| `SUPERAGENT_PROD_TASK_RESULT_HMAC_SECRET` | prod 必填 | prod SuperAgent 任务结果入站签名密钥,只能通过 Secret 注入 |
| `SUPERAGENT_TASK_RESULT_HMAC_SECRET` | 兼容兜底 | 旧通用 HMAC 变量;新环境优先配置环境专属变量 |
| `SUPERAGENT_TASK_RESULT_CLOCK_SKEW_SECONDS` | 否 | 请求时间允许偏移,默认 `300` 秒 |
| `SUPERAGENT_TASK_RESULT_NONCE_TTL_SECONDS` | 否 | Nonce 去重窗口,默认 `600` 秒 |
| `SUPERAGENT_TASK_RESULT_MAX_BODY_BYTES` | 否 | 请求体最大字节数,默认 `1048576` |
如果当前 profile 对应的 `SUPERAGENT_*_TASK_RESULT_HMAC_SECRET` 和旧通用变量都为空,生产环境应拒绝接口调用。
### 3.2 请求 Header
| Header | 是否必填 | 中文说明 |
| --- | --- | --- |
| `X-TH-Hotel-SuperAgent-Client-Id` | 是 | 调用方客户端 ID用于区分不同 SuperAgent 调用方 |
| `X-TH-Hotel-SuperAgent-Timestamp` | 是 | UTC 时间ISO-8601 格式,例如 `2026-07-07T08:30:00Z` |
| `X-TH-Hotel-SuperAgent-Nonce` | 是 | 每次请求唯一随机值,用于防重放 |
| `X-TH-Hotel-SuperAgent-Signature` | 是 | HMAC 签名,格式 `sha256=<lowercase-hex>` |
| `X-TH-Hotel-Request-Id` | 否 | 调用方请求 ID用于排查和日志串联 |
### 3.3 签名串
签名使用原始请求体字节计算 SHA-256再参与 HMAC。
规范签名串:
```text
POST
/api/integrations/superagent/task-results
<X-TH-Hotel-SuperAgent-Timestamp>
<X-TH-Hotel-SuperAgent-Nonce>
<X-TH-Hotel-SuperAgent-Client-Id>
<lowercase-hex-sha256-of-raw-body>
```
签名算法:
```text
signature = HMAC_SHA256(当前 profile 生效的 SuperAgent HMAC secret, canonical_string)
```
Header 写法:
```text
X-TH-Hotel-SuperAgent-Signature: sha256=<lowercase-hex-signature>
```
### 3.4 服务端校验
服务端必须按顺序完成以下校验:
1. 校验必要 Header 是否存在。
2. 校验 timestamp 可解析且在允许时间窗口内。
3. 校验同一个 `client_id + nonce` 在 TTL 窗口内没有被使用过。
4. 计算原始请求体 SHA-256。
5. 使用 HMAC secret 重新计算签名。
6. 使用常量时间比较签名。
7. 校验 `Content-Type` 是否为 `application/json``text/plain`,避免不支持的媒体类型消耗 nonce。
8. 鉴权通过后再按 body 内容解析 JSON 或 S000/S999 文本。
日志和错误响应不得输出 secret、签名原文、完整请求体、邮件正文、附件 URL 或个人敏感信息。
## 4. 请求体
### 4.1 JSON 结构化任务请求体
JSON 请求体沿用 AI 导入文档定义的聚合结构。
```json
{
"hotel_id": "HOTEL-TEST",
"source_message_id": "mail-20260708-0001",
"ai_task_results": [
{
"source_event_index": 1,
"catalog_code": "S01",
"skill_id": "S01_new_booking_skill",
"result_type": "normal_task",
"task_type": "New Booking",
"task_subtype": "new_fit_reservation",
"current_or_history": "current",
"case_keys": {
"group_code": null,
"confirmation_number": null
},
"visible_reason": "邮件正文包含新建预订请求。",
"relevant_message_excerpt": "Please create a new booking...",
"attachments": [],
"file_references": [],
"context_used": {},
"extracted_fields": {},
"manual_review": null,
"informational_message": null,
"additional_operations": [],
"idempotency_key": null
}
],
"extraction_warnings": []
}
```
### 4.2 顶层字段
| 字段 | 是否必填 | 中文说明 |
| --- | --- | --- |
| `hotel_id` | 否 | 酒店上下文 IDSuperAgent 默认不传,单酒店阶段由后端解析系统酒店 |
| `source_message_id` | 是 | 外部来源消息 ID对应 AgentBus `source.external_message_id`;一次请求只能有一个 |
| `source_provider` | 否 | V2 兼容字段;通常不传。写入定位不使用该字段,真实 provider 以 SourceMessage Inbox 入库值为准 |
| `source_channel` | 否 | V2 兼容字段通常不传。写入定位不使用该字段AgentBus 邮件真实入库渠道可能是 `OUTLOOK` |
| `ai_task_results[]` | 是 | AI 拆分出的任务结果列表,必须保留数组顺序 |
| `extraction_warnings[]` | 否 | 抽取警告;不直接等同于业务任务 |
V2 第一版要求 `ai_task_results[]` 至少包含一条记录。没有业务动作的纯信息类邮件不要提交空数组,也不要新生成 `informational_message`;新数据优先使用 V3 结构化 `S10/S99`,旧联调或兼容场景仍可使用 `S000,source_message_id` 文本结果。
### 4.3 `ai_task_results[]` 字段
| 字段 | 是否必填 | 中文说明 |
| --- | --- | --- |
| `source_event_index` | 是 | AI current 事件序号,建议从 1 开始 |
| `catalog_code` | 是 | Skill 目录代码,例如 S01、S02 |
| `skill_id` | 是 | Skill 标识 |
| `result_type` | 是 | V2 当前代码契约只接受 `normal_task``manual_review``informational_message` 仅历史兼容 |
| `task_type` | 是 | AI 原始任务类型 |
| `task_subtype` | 否 | 业务动作 subtype有则用于任务卡路由 |
| `current_or_history` | 否 | 当前或历史标识 |
| `case_keys` | 否 | 订单关联候选键 |
| `visible_reason` | 否 | 给用户看的生成原因 |
| `relevant_message_excerpt` | 否 | 相关邮件片段,注意不要超长 |
| `attachments` / `file_references` | 否 | 附件和文件引用 |
| `context_used` | 否 | AI 使用的上下文 |
| `extracted_fields` | 否 | 业务字段主体 |
| `manual_review` | 条件必填 | `result_type=manual_review` 时应提供 |
| `informational_message` | 条件必填 | 仅历史兼容;新数据不再生成 |
| `additional_operations` | 否 | 附加动作建议 |
| `idempotency_key` | 否 | 可忽略;本系统第一版自行生成幂等键 |
### 4.4 S000/S999 文本结果请求体
当 SuperAgent 入口阶段没有结构化任务 JSON 时,可以直接提交纯文本 body
```text
S000,mail-20260708-0001
```
或:
```text
S999,mail-20260708-0001
```
字段说明:
| 片段 | 中文说明 |
| --- | --- |
| `S000` | 纯信息类邮件,不形成业务素材包 |
| `S999` | 入口阶段无法形成业务素材包 |
| `source_message_id` | 逗号后面的值,外部来源消息 ID对应 AgentBus `source.external_message_id` |
处理规则:
- 第一版使用系统默认酒店反查 SourceMessage Inbox不要求文本 body 携带 `hotel_id`
- 后端按 `默认酒店 + external_message_id` 查询唯一 SourceMessage真实 provider/channel 以 Inbox 入库值为准。
- 命中后创建 `SOURCE_MESSAGE_ONLY` 只读任务。
- 任务列表可见,订单列表不可见。
- 不允许编辑、确认、转换订单、执行 OPERA 或重试 OPERA。
- 同一 SourceMessage 重复提交相同文本 body 返回幂等重放。
- 同一 SourceMessage 已经存在不同 AI 结果请求时返回幂等冲突。
## 5. 技术校验边界
本接口只做技术校验,不做业务合法性判断。
### 5.1 必须校验
- 鉴权签名合法。
- 请求体大小不超过限制。
- JSON body 可解析S000/S999 文本 body 必须符合 `结果码,source_message_id`
- JSON body 中 `hotel_id` 可不传;正式 REST / MCP 调用由后端解析系统酒店。仅本地旧夹具在显式开启兼容开关时允许缺少 `hotel_id``source_message_id` 为内部数字 ID 的调用。
- S000/S999 文本 body 第一版使用系统默认酒店,不读取 `hotel_id`
- 顶层只有一个 `source_message_id`
- `source_message_id` 对应的外部来源消息已经写入 SourceMessage Inbox。
- `ai_task_results[]` 是非空数组。
- JSON `result_type` 属于允许值。
- `task_type` 属于当前系统可识别的稳定值或可进入 Fallback 处理。
- 同一个请求内 `source_event_index` 和数组顺序可保存。
- 关键字符串长度不超过数据库限制。
- V3 P0.1 Parent split 当前合法结构必须是 `Cancel Allotment + cancel_allotment_control_block``Cancel Booking + linked_parent_release_after_child_split` 属于当前 producer 契约错误,只能作为历史 payload 只读兼容。
- Parent 只提供 `case_keys.group_code` 或只提供 `case_keys.block_code` 时,后端会在 adapter 派生副本中补齐另一边,不回写原始 payload。
- Parent `case_keys.group_code``case_keys.block_code` 原始候选冲突时SuperAgent 应输出 `manual_review.reason_code=target_object_unclear` 和非空 `context_used.parent_identity_candidates[]`;后端会创建同卡 type-known manual review。缺少该复核结构时按 adapter contract error 处理。
- 同一个 Parent split cluster 只能有一个 Parent 候选;重复 Parent 候选不创建第二张业务任务卡。
### 5.2 不在本接口判断
- 不判断 AI 任务类型是否业务正确。
- 不判断房型、价格、日期、Rate Code 是否合理。
- 不判断 Cancel Booking 前是否已经有 New Booking。
- 不因为字段缺失自动改成 Fallback。
- 不直接执行 OPERA 模拟。
- 不直接把 AI 原始值写入 OPERA 参数。
- 不把 Parent split 的 adapter 契约校验理解为业务成功;即使生成 `Cancel Allotment` 任务也必须等待用户确认、Preflight 和后续 OPERA/OHIP 接入。
## 6. 幂等设计
`idempotency_key` 第一版由系统生成,不依赖 SuperAgent 传值。
### 6.1 请求哈希
系统必须保存原始请求体的 SHA-256
```text
request_payload_sha256 = sha256(raw_request_body)
```
### 6.2 批次幂等键
批次幂等键建议:
```text
batch_idempotency_key =
sha256(
"superagent-task-result-batch:v1"
+ "|" + source_message_internal_id
+ "|" + request_payload_sha256
)
```
作用:
- 相同请求体重复提交时识别为幂等重放。
- 同一个外部 `source_message_id` 解析到的内部 SourceMessage 如果提交了不同请求体,不会被误认为同一个批次。
- S000/S999 文本 body 也使用同一批次幂等规则。
### 6.3 item 幂等键
每条 `ai_task_results[]` 的幂等键建议:
```text
item_idempotency_key =
sha256(
"superagent-task-result-item:v1"
+ "|" + source_message_internal_id
+ "|" + source_event_index
+ "|" + array_index
+ "|" + catalog_code
+ "|" + skill_id
+ "|" + result_type
+ "|" + task_type
+ "|" + task_subtype
+ "|" + item_payload_sha256
)
```
说明:
- `array_index` 按 AI 返回列表顺序保存,建议从 1 开始。
- `item_payload_sha256` 是单个 item 规范 JSON 或原始片段的 SHA-256。
- 数据库应对 `hotel_id + item_idempotency_key` 建唯一约束。
### 6.4 重复提交处理
| 场景 | 处理方式 |
| --- | --- |
| 完全相同请求体重复提交 | 返回已有 batch 和 item不重复创建任务 |
| 同一外部 `source_message_id` 不同请求体 | 作为潜在冲突处理,第一版建议拒绝并返回 `IDEMPOTENCY_CONFLICT`,除非后续明确支持重新抽取版本 |
| 同一请求内 item 幂等键重复 | 拒绝请求,返回 `DUPLICATE_TASK_RESULT_ITEM` |
## 7. 系统映射规则
接口接收后,应将 AI 字段映射到系统字段。
| AI 字段 | 系统字段 | 中文说明 |
| --- | --- | --- |
| `task_type` | `ai_task_type` | 保留 AI 原始任务类型 |
| `result_type` | `result_type` | 保留 AI 结果类型 |
| `task_type + result_type` | `system_task_type` | 映射为系统主任务类型 |
| `task_type + task_subtype` | `task_card_type` | 映射为任务卡类型 |
| `source_event_index + array_index` | `execution_order` | 生成同订单任务顺序 |
| 完整 item JSON | `ai_payload_json` | 保存 AI 原始 payload |
| `case_keys` | `case_keys_json` | 保存订单候选键 |
| `extracted_fields` | `extracted_fields_json` | 保存业务字段 |
| `manual_review` | `manual_review_json` | 保存人工复核结构 |
| `informational_message` | `informational_message_json` | 仅历史兼容的信息提醒结构 |
| `S000/S999` 文本结果 | `SOURCE_MESSAGE_ONLY` 只读特殊任务 | 保存入口阶段原始结果,不创建真实业务订单 |
系统主任务类型映射以 `M002-order-task-workflow-v2.md` 为准。
## 8. 响应体
### 8.1 创建成功
首次成功创建时返回 `201 Created`
```json
{
"request_id": "req-20260707-0001",
"source_message_id": "mail-20260708-0001",
"batch_id": "1900000000000001001",
"idempotent_replay": false,
"accepted_count": 1,
"items": [
{
"source_event_index": 1,
"array_index": 1,
"ai_transition_id": "1900000000000002001",
"order_id": "1900000000000003001",
"task_id": "1900000000000004001",
"system_task_type": "NEW_BOOKING",
"task_card_type": "NEW_BOOKING",
"task_status": "PENDING_CONFIRM",
"order_status": "TEMPORARY",
"execution_order": 1
}
],
"warnings": []
}
```
S000 / S999 文本结果创建成功时,同样返回 `201 Created`。这类结果会创建只读特殊任务和隐藏技术订单,供任务列表展示和任务详情查看来源邮件;该隐藏技术订单不会出现在订单列表。
```json
{
"request_id": "req-20260707-0003",
"source_message_id": "mail-20260708-0001",
"batch_id": "1900000000000005001",
"idempotent_replay": false,
"accepted_count": 1,
"items": [
{
"source_event_index": 1,
"array_index": 1,
"ai_transition_id": "1900000000000006001",
"order_id": "1900000000000007001",
"task_id": "1900000000000008001",
"system_task_type": "SOURCE_MESSAGE_ONLY",
"task_card_type": "SOURCE_MESSAGE_ONLY",
"task_status": "COMPLETED",
"order_status": "TEMPORARY",
"execution_order": 1
}
],
"warnings": []
}
```
前端通过任务详情查看该类任务时,字段列表和 OPERA 操作列表为空,按钮应全部只读;`source_message_only_result` 会返回 `entry_result_code``entry_result_meaning``entry_result_description``entry_result_source_message_id``result_type``route_code``agent_assessment``notification``manual_review``raw_answer`
### 8.2 幂等重放
相同请求体重复提交时返回 `200 OK`
```json
{
"request_id": "req-20260707-0002",
"source_message_id": "mail-20260708-0001",
"batch_id": "1900000000000001001",
"idempotent_replay": true,
"accepted_count": 1,
"items": [],
"warnings": [
{
"code": "IDEMPOTENT_REPLAY",
"message": "相同请求已经处理,本次未重复创建任务。"
}
]
}
```
## 9. 错误响应
错误响应统一结构:
```json
{
"request_id": "req-20260707-0003",
"error_code": "AUTH_SIGNATURE_INVALID",
"message": "签名校验失败。",
"details": []
}
```
常见错误码:
| HTTP 状态 | error_code | 中文说明 |
| --- | --- | --- |
| 401 | `AUTH_HEADER_MISSING` | 鉴权 Header 缺失 |
| 401 | `AUTH_TIMESTAMP_INVALID` | 请求时间无效或超出窗口 |
| 401 | `AUTH_SIGNATURE_INVALID` | 签名不匹配 |
| 409 | `AUTH_NONCE_REPLAY` | Nonce 重放 |
| 413 | `REQUEST_BODY_TOO_LARGE` | 请求体过大 |
| 400 | `INVALID_JSON` | JSON 不可解析 |
| 400 | `HOTEL_ID_MISMATCH` | 显式 `hotel_id` 或历史内部 SourceMessage ID 所属酒店与系统酒店不一致 |
| 409 | `SYSTEM_HOTEL_NOT_CONFIGURED` | 平台酒店表没有 ACTIVE 酒店,无法解析系统酒店 |
| 409 | `SYSTEM_HOTEL_AMBIGUOUS` | 单酒店阶段平台酒店表存在多家 ACTIVE 酒店 |
| 400 | `SOURCE_MESSAGE_REQUIRED` | `source_message_id` 缺失 |
| 404 | `SOURCE_MESSAGE_NOT_FOUND` | 外部来源消息尚未写入 SourceMessage Inbox |
| 409 | `SOURCE_MESSAGE_AMBIGUOUS` | 同一系统酒店下存在多条相同外部 `source_message_id` 的 Inbox 记录,后端拒绝随机选择 |
| 400 | `TASK_RESULTS_EMPTY` | `ai_task_results[]` 为空 |
| 400 | `TASK_RESULT_UNSUPPORTED_TYPE` | `result_type``task_type` 不可识别 |
| 400 | `DUPLICATE_TASK_RESULT_ITEM` | 同一请求内 item 重复 |
| 409 | `IDEMPOTENCY_CONFLICT` | 同一 SourceMessage 出现不同请求体重复提交 |
| 500 | `INTERNAL_ERROR` | 系统内部错误 |
错误响应不得返回原始请求体、邮件正文、附件 URL、Token、签名 secret 或完整个人敏感信息。
## 10. 验收标准
第一版接口实现完成时至少满足:
- 缺失 HMAC Header 时返回 `401`
- timestamp 超出窗口时返回 `401`
- nonce 重放时返回 `409`
- 签名错误时返回 `401`
- 一个请求只能包含一个 `source_message_id`
- 外部 `source_message_id` 未匹配 SourceMessage Inbox 时返回 `404`
- 相同请求重复提交不会重复创建任务。
- 同一外部 `source_message_id` 不同请求体重复提交返回 `409`
- 成功接收后能保存 AI 过渡层记录,并返回 batch、task 和 order 标识。
- 日志和响应不暴露 secret、签名原文、完整邮件正文或附件 URL。