# 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` | 否 | 酒店上下文 ID；SuperAgent 默认不传，单酒店阶段由后端解析系统酒店 |
| `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。
