Files
th-hotel-simple/docs/project/integrations/superagent-api-contract.md
2026-07-09 16:08:37 +08:00

598 lines
22 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.

# TH Hotel SuperAgent API 对接契约
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 文档版本 | 0.3 |
| 日期 | 2026-07-09 |
| 状态 | 已增加邮件会话任务和受控正文查询接口 |
| 适用范围 | SuperAgent 调用本系统查询上下文、查询邮件会话、提交 AI 任务结果 |
| 主要读者 | SuperAgent 对接方、后端、测试、运维 |
## 联调参数区
以下参数供 SuperAgent 联调时手动修改。该区块只用于 dev/test 联调,不作为生产 Secret 管理方式。
| 参数 | 当前联调值 | 中文说明 |
| --- | --- | --- |
| `TH_HOTEL_API_BASE_URL` | `http://8.138.234.141:18087` | 本系统后端基础地址;本地联调用 8080部署环境改为实际网关或服务地址。 |
| `HOTEL_ID` | `HOTEL-DEV` | 当前 dev profile 下 AgentBus 入库默认酒店 ID调用五个 SuperAgent 接口时均应传入请求体 `hotel_id`。 |
| `SUPERAGENT_CLIENT_ID` | `superagent-debug` | SuperAgent 调用方 ID对应 Header `X-TH-Hotel-SuperAgent-Client-Id`。 |
| `SUPERAGENT_HMAC_SECRET` | `th-hotel-superagent-debug-20260709-change-before-prod` | dev/test 联调临时 HMAC 密钥;生产上线前必须更换为新的高强度随机密钥。 |
生产注意:
- 生产环境必须更换 `SUPERAGENT_HMAC_SECRET`,不得继续使用上述联调临时密钥。
- 生产密钥不得写入 SuperAgent skill 文件、仓库文档、前端代码、镜像或普通日志,只能通过部署 Secret / 环境变量注入。
- 本系统后端按 profile 优先读取环境专属密钥dev 使用 `SUPERAGENT_DEV_TASK_RESULT_HMAC_SECRET`test 使用 `SUPERAGENT_TEST_TASK_RESULT_HMAC_SECRET`prod 使用 `SUPERAGENT_PROD_TASK_RESULT_HMAC_SECRET`;旧通用变量 `SUPERAGENT_TASK_RESULT_HMAC_SECRET` 仅作为兼容兜底。当前查询接口和任务结果通知接口共用同一个 HMAC secret。
## 1. 基础约定
请求地址先使用占位符:
```text
{TH_HOTEL_API_BASE_URL}
```
上线或联调时由环境提供实际域名,例如 UAT、生产内网域名或 API Gateway 地址。本文所有接口均为 SuperAgent 到本系统的服务到服务调用,不给前端直接调用。
## 2. 通用 HMAC 鉴权
查询接口和任务结果通知接口使用同一套 HMAC-SHA256 规则。
### 2.1 通用 Header
| Header | 是否必填 | 中文说明 |
| --- | --- | --- |
| `Content-Type` | 是 | 固定 `application/json` |
| `X-TH-Hotel-SuperAgent-Client-Id` | 是 | SuperAgent 调用方客户端 ID |
| `X-TH-Hotel-SuperAgent-Timestamp` | 是 | UTC ISO-8601 时间,例如 `2026-07-08T01:30:00Z` |
| `X-TH-Hotel-SuperAgent-Nonce` | 是 | 每次请求唯一随机值,用于防重放 |
| `X-TH-Hotel-SuperAgent-Signature` | 是 | HMAC 签名,格式 `sha256=<lowercase-hex>` |
| `X-TH-Hotel-Request-Id` | 否 | 调用方请求 ID用于日志串联 |
| `X-TH-Hotel-AI-Trace-Id` | 否 | AI 运行链路 ID查询接口会原样带回 `trace_id` |
### 2.2 签名串
签名串使用接口 path不包含域名、query string 或 fragment。
```text
POST
<request_path>
<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(SUPERAGENT_HMAC_SECRET, canonical_string)
```
Header 写法:
```text
X-TH-Hotel-SuperAgent-Signature: sha256=<lowercase-hex-signature>
```
### 2.3 服务端校验顺序
1. 校验请求体大小。
2. 校验 HMAC 相关 Header 是否存在。
3. 校验 timestamp 是否在允许时间窗口内。
4. 计算原始请求体 SHA-256。
5. 使用共享 secret 重新计算 HMAC。
6. 常量时间比较签名。
7. 校验并记录 `client_id + nonce`,防止重放。
8. 查询接口校验 `Content-Type` 是否为 `application/json`
9. 鉴权和协议校验通过后再解析业务 JSON。
## 3. SourceMessage ID 口径
本系统存在两个容易混淆的 ID
| 名称 | 中文说明 | 使用位置 |
| --- | --- | --- |
| 外部来源消息 ID | AgentBus 邮件 payload 中的 `source.external_message_id`SuperAgent / Main Agent 在最终 JSON 中原样带回为 `source_message_id` | SuperAgent 任务结果通知接口入参和响应回显 |
| 内部 SourceMessage Inbox ID | `platform_source_message_inbox.id`,本系统数据库内部主键 | `workflow_*` 表的 `source_message_id` 外键、前端和运维排查 |
SuperAgent 不应知道或依赖内部 SourceMessage Inbox ID。任务结果通知接口收到外部 `source_message_id` 后,后端使用 `hotel_id + provider + channel + external_message_id` 反查内部 Inbox 记录,再用内部 ID 写入业务表。
查询接口 1、2 在 SuperAgent 查询阶段不依赖当前邮件是否已经入库。若请求体兼容旧契约传入 `source_message_id``source_event_index`,第一版后端会接收但忽略,不校验它们的格式,也不把它们作为查询边界。
查询接口 3、4 面向已经入库的邮件会话:`external_conversation_id``hotel_id + source_provider + source_channel + external_conversation_id` 查询;`source_message_id` 表示外部来源消息 ID可作为锚点反查该邮件所属会话。
## 4. 接口 1查询订单上下文
### 4.1 请求
| 项目 | 内容 |
| --- | --- |
| Method | `POST` |
| URL | `{TH_HOTEL_API_BASE_URL}/api/ai-query/v1/case-context` |
| request_path | `/api/ai-query/v1/case-context` |
| Content-Type | `application/json` |
| 业务动作 | 只读查询,不创建任务、不修改订单、不写 OPERA |
### 4.2 请求体
```json
{
"hotel_id": "<HOTEL_ID>",
"group_code": "GRP-001",
"confirmation_number": null,
"reservation_no": null,
"object_type_hint": "group_block",
"target_key_source": "body_current",
"body_thread_used_only_as_evidence": false
}
```
字段说明:
| 字段 | 是否必填 | 中文说明 |
| --- | --- | --- |
| `hotel_id` | 是 | 酒店上下文 ID |
| `group_code` | 条件必填 | Group / Allotment 查询 key |
| `confirmation_number` | 条件必填 | FIT Confirmation Number 查询 key |
| `reservation_no` | 条件必填 | OPERA reservation no当前系统无可靠表源只传该字段时会返回人工复核原因 |
| `object_type_hint` | 否 | 调用方推测的对象类型,只作为提示 |
| `target_key_source` | 否 | key 来源,例如 `body_current``body_thread_evidence` |
| `body_thread_used_only_as_evidence` | 否 | 历史线程 key 是否仅作为证据 |
`group_code``confirmation_number``reservation_no` 至少一个非空。当前稳定查询能力优先支持 `group_code``confirmation_number`
全局上下文查询只依赖 `hotel_id + 业务 key``source_message_id``source_event_index` 不作为查询边界,传入时也不会影响查询结果。
### 4.3 成功响应
```json
{
"success": true,
"request_id": "req-001",
"trace_id": "trace-001",
"data": {
"matched_order_records": [],
"pending_or_open_tasks": [],
"active_workflows": [],
"terminated_records": [],
"target_object_validation": {
"status": "none",
"matched_object_id": null,
"matched_object_type": null,
"can_create_new_booking_task": true,
"can_create_update_task": false,
"can_create_cancel_task": false,
"can_attach_voucher": false,
"can_attach_rooming_list": false,
"needs_manual_review_reason": null
},
"key_relationships": {
"group_code_and_confirmation_same_object": null,
"relationship_evidence": ""
}
},
"warnings": [],
"error": null
}
```
### 4.4 主要数据来源
| 返回字段 | 来源 |
| --- | --- |
| `matched_order_records[]` | `workflow_reservation_order` |
| `pending_or_open_tasks[]` | `workflow_reservation_task` + `workflow_reservation_ai_transition` |
| `terminated_records[]` | 订单 `ENDED` / `LOGIC_DELETED`,任务 `FAILED` / `COMPLETED` |
| `active_workflows[]` | 当前无独立 workflow 表,固定空数组 |
## 5. 接口 2查询对象详情
### 5.1 请求
| 项目 | 内容 |
| --- | --- |
| Method | `POST` |
| URL | `{TH_HOTEL_API_BASE_URL}/api/ai-query/v1/object-detail` |
| request_path | `/api/ai-query/v1/object-detail` |
| Content-Type | `application/json` |
| 业务动作 | 只读查询对象详情,不创建任务、不修改订单、不写 OPERA |
### 5.2 请求体
```json
{
"hotel_id": "<HOTEL_ID>",
"object_id": "ORDER:1900000000000000100",
"object_type": "group_block"
}
```
字段说明:
| 字段 | 是否必填 | 中文说明 |
| --- | --- | --- |
| `hotel_id` | 是 | 酒店上下文 ID |
| `object_id` | 是 | 查询对象 ID第一版只支持 `ORDER:{order_id}` |
| `object_type` | 否 | 调用方对象类型提示,第一版不作为强校验 |
### 5.3 成功响应
```json
{
"success": true,
"request_id": "req-002",
"trace_id": "trace-001",
"data": {
"object_id": "ORDER:1900000000000000100",
"object_type": "group_block",
"order_id": "1900000000000000100",
"order_key_type": "GROUP_CODE",
"group_code": "GRP-001",
"confirmation_number": null,
"reservation_no": null,
"block_id": null,
"temporary_order_code": "TMP-1900000000000000100",
"display_name": "GRP-001",
"status": "ACTIVE",
"source_message_id": "1900000000000000001",
"created_from_task_id": null,
"created_at": "2026-07-08T01:00:00Z",
"last_updated_at": "2026-07-08T01:10:00Z",
"arrival_date": null,
"departure_date": null,
"nights": null,
"guest_count": null,
"room_items": [],
"rate_code": null,
"rate_code_price": null,
"reservation_type": null,
"cancel_status": "not_cancelled",
"can_update": true,
"can_cancel": true,
"hard_validation_warnings": [
{
"code": "OPERA_PROJECTION_UNAVAILABLE",
"message": "当前系统尚未接入 OPERA 对象投影,日期、房型、房价等字段无法确认。"
}
]
},
"warnings": [],
"error": null
}
```
说明:接口 2 响应中的 `source_message_id` 当前是本系统内部 SourceMessage Inbox ID用于对象溯源和排查不要把该字段当作 SuperAgent 任务结果通知接口的外部 `source_message_id` 使用。
## 6. 接口 3查询邮件会话下所有任务
### 6.1 请求
| 项目 | 内容 |
| --- | --- |
| Method | `POST` |
| URL | `{TH_HOTEL_API_BASE_URL}/api/ai-query/v1/message-conversation/tasks` |
| request_path | `/api/ai-query/v1/message-conversation/tasks` |
| Content-Type | `application/json` |
| 业务动作 | 只读查询邮件会话下任务,不创建任务、不修改订单、不写 OPERA |
### 6.2 请求体
按外部邮件会话 ID 查询:
```json
{
"hotel_id": "<HOTEL_ID>",
"source_provider": "AGENTBUS",
"source_channel": "EMAIL",
"external_conversation_id": "thread-20260708-0001"
}
```
按外部来源消息 ID 作为锚点反查会话:
```json
{
"hotel_id": "<HOTEL_ID>",
"source_provider": "AGENTBUS",
"source_channel": "EMAIL",
"source_message_id": "mail-20260708-0001"
}
```
字段说明:
| 字段 | 是否必填 | 中文说明 |
| --- | --- | --- |
| `hotel_id` | 是 | 酒店上下文 ID |
| `source_provider` | 否 | 来源提供方,按会话 ID 查询和按 `source_message_id` 反查时都参与隔离,缺省为 `AGENTBUS` |
| `source_channel` | 否 | 来源渠道,按会话 ID 查询和按 `source_message_id` 反查时都参与隔离,缺省为 `EMAIL` |
| `external_conversation_id` | 条件必填 | 外部邮件会话 ID对应 AgentBus `source.external_conversation_id` |
| `source_message_id` | 条件必填 | 外部来源消息 ID对应 AgentBus `source.external_message_id`,不是内部 Inbox ID |
`external_conversation_id``source_message_id` 至少一个非空。两者同时传入时,第一版以后端直接按 `external_conversation_id` 查询为准。
### 6.3 成功响应
```json
{
"success": true,
"request_id": "req-003",
"trace_id": "trace-001",
"data": {
"hotel_id": "HOTEL-DEV",
"external_conversation_id": "thread-20260708-0001",
"task_count": 2,
"tasks": [
{
"task_id": "1900000000000000400",
"order_id": "1900000000000000300",
"external_source_message_id": "mail-20260708-0001",
"external_conversation_id": "thread-20260708-0001",
"source_received_at": "2026-07-08T01:00:00Z",
"source_event_index": 1,
"catalog_code": "S02",
"skill_id": "update_booking_amendment_skill",
"result_type": "normal_task",
"task_type": "Update Booking",
"system_task_type": "UPDATE_BOOKING",
"task_card_type": "UPDATE_BOOKING",
"task_subtype": "update_stay_dates",
"task_status": "PENDING_CONFIRM",
"queue_participation": true,
"execution_order": 1,
"parent_task_id": null,
"parent_source_event_index": null,
"linked_task_group_id": null,
"blocked_until_parent_completed": false,
"completed_at": null,
"task_created_at": "2026-07-08T01:01:00Z",
"task_updated_at": "2026-07-08T01:01:00Z"
}
]
},
"warnings": [],
"error": null
}
```
排序规则:
1. 先按邮件 `received_at` 正序。
2. 同一封邮件下,再按任务 `created_at` 正序。
3. 若时间相同,再按 `task_id` 正序稳定排序。
## 7. 接口 4查询邮件会话下所有受控正文
### 7.1 请求
| 项目 | 内容 |
| --- | --- |
| Method | `POST` |
| URL | `{TH_HOTEL_API_BASE_URL}/api/ai-query/v1/message-conversation/messages` |
| request_path | `/api/ai-query/v1/message-conversation/messages` |
| Content-Type | `application/json` |
| 业务动作 | 只读查询邮件会话受控正文,不返回附件 URL 或原始未清洗 HTML |
### 7.2 请求体
请求体字段与接口 3 相同,可按 `external_conversation_id` 查询,也可按外部 `source_message_id` 锚点反查会话。
```json
{
"hotel_id": "<HOTEL_ID>",
"source_provider": "AGENTBUS",
"source_channel": "EMAIL",
"source_message_id": "mail-20260708-0001"
}
```
### 7.3 成功响应
```json
{
"success": true,
"request_id": "req-004",
"trace_id": "trace-001",
"data": {
"hotel_id": "HOTEL-DEV",
"external_conversation_id": "thread-20260708-0001",
"message_count": 2,
"messages": [
{
"external_source_message_id": "mail-20260708-0001",
"external_conversation_id": "thread-20260708-0001",
"sender_summary": "guest@example.test",
"subject": "Booking update",
"received_at": "2026-07-08T01:00:00Z",
"source_sent_at": "2026-07-08T00:59:00Z",
"text_body": "Please update arrival date...",
"html_body_sanitized": "<html><body>Please update arrival date...</body></html>",
"html_sanitize_required": true,
"html_render_mode": "SANITIZED_HTML"
}
]
},
"warnings": [],
"error": null
}
```
安全边界:
- `messages[]` 按邮件 `received_at` 正序返回。
- 不返回 `html_body` 原始未清洗 HTML。
- 不返回 `attachments``inline_images``external_url`、附件 URL 或 HTML 中的 `href/src` 外链属性。
- 后端读取正文时会写入 SourceMessage 原文访问审计。
## 8. 接口 5SuperAgent 通知 AI 任务结果
### 8.1 请求
| 项目 | 内容 |
| --- | --- |
| Method | `POST` |
| URL | `{TH_HOTEL_API_BASE_URL}/api/integrations/superagent/task-results` |
| request_path | `/api/integrations/superagent/task-results` |
| Content-Type | `application/json` |
| 业务动作 | 接收 AI 任务结果,写入 AI 过渡层、订单、任务和任务卡 |
### 8.2 请求体
```json
{
"hotel_id": "<HOTEL_ID>",
"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": []
}
```
字段说明:
| 字段 | 是否必填 | 中文说明 |
| --- | --- | --- |
| `hotel_id` | 是 | 酒店上下文 ID用于反查 SourceMessage Inbox 幂等键 |
| `source_message_id` | 是 | SuperAgent / Main Agent 原样带回的外部来源消息 ID对应 AgentBus `source.external_message_id`,一次请求只能有一个 |
| `source_provider` | 否 | 来源提供方,第一版缺省为 `AGENTBUS` |
| `source_channel` | 否 | 来源渠道,第一版缺省为 `EMAIL` |
| `ai_task_results[]` | 是 | AI 拆分出的任务结果列表,必须保留数组顺序 |
| `ai_task_results[].source_event_index` | 是 | AI current 事件序号 |
| `ai_task_results[].catalog_code` | 是 | Skill 目录代码 |
| `ai_task_results[].skill_id` | 是 | Skill 标识 |
| `ai_task_results[].result_type` | 是 | `normal_task``manual_review``informational_message` |
| `ai_task_results[].task_type` | 是 | AI 原始任务类型 |
| `ai_task_results[].task_subtype` | 否 | 业务动作 subtype |
| `ai_task_results[].case_keys` | 否 | 订单关联候选键 |
| `ai_task_results[].extracted_fields` | 否 | 业务字段主体 |
正式联调时,后端通过 `hotel_id + source_provider(默认 AGENTBUS) + source_channel(默认 EMAIL) + source_message_id` 查找 `platform_source_message_inbox.external_message_id`。如果没有找到,返回 `SOURCE_MESSAGE_NOT_FOUND`。本地旧夹具允许在缺少 `hotel_id` 时使用内部数字 SourceMessage ID但该兼容路径不作为 SuperAgent 正式契约。
### 8.3 成功响应
```json
{
"request_id": "req-003",
"source_message_id": "mail-20260708-0001",
"batch_id": "1900000000000000200",
"idempotent_replay": false,
"accepted_count": 1,
"items": [
{
"source_event_index": 1,
"array_index": 1,
"ai_transition_id": "1900000000000000250",
"order_id": "1900000000000000300",
"task_id": "1900000000000000400",
"system_task_type": "NEW_BOOKING",
"task_card_type": "NEW_BOOKING",
"task_status": "PENDING_CONFIRM",
"order_status": "TEMPORARY",
"execution_order": 1
}
],
"warnings": []
}
```
## 9. 错误响应
### 9.1 查询接口错误响应
```json
{
"success": false,
"request_id": "req-001",
"trace_id": "trace-001",
"data": null,
"warnings": [],
"error": {
"code": "AUTH_SIGNATURE_INVALID",
"message": "签名校验失败。",
"details": {}
}
}
```
### 9.2 任务结果通知接口错误响应
```json
{
"request_id": null,
"error_code": "AUTH_SIGNATURE_INVALID",
"message": "签名校验失败。",
"details": []
}
```
### 9.3 常见错误码
| 错误码 | HTTP 状态 | 中文说明 |
| --- | --- | --- |
| `AUTH_HEADER_MISSING` | 401 | HMAC 必要 Header 缺失 |
| `AUTH_HEADER_INVALID` | 401 | HMAC Header 格式或长度无效 |
| `AUTH_TIMESTAMP_INVALID` | 401 | timestamp 格式错误或超出时间窗口 |
| `AUTH_SIGNATURE_INVALID` | 401 | 签名不匹配或服务端未配置 secret |
| `AUTH_NONCE_REPLAY` | 409 | nonce 已被使用 |
| `REQUEST_BODY_TOO_LARGE` | 413 | 请求体超过大小限制 |
| `REQUEST_CONTENT_TYPE_UNSUPPORTED` | 415 | `Content-Type` 不是 `application/json` |
| `REQUEST_BODY_INVALID` | 400 | JSON 不合法 |
| `QUERY_KEY_REQUIRED` | 400 | 查询接口缺少可用业务 key |
| `MESSAGE_CONVERSATION_QUERY_KEY_REQUIRED` | 400 | 会话查询缺少 `external_conversation_id``source_message_id` |
| `OBJECT_NOT_FOUND` | 404 | 对象详情查询目标不存在 |
| `MESSAGE_CONVERSATION_NOT_FOUND` | 404 | 外部邮件会话尚未写入 SourceMessage Inbox |
| `HOTEL_ID_REQUIRED` | 400 | 五个 SuperAgent 接口缺少必填 `hotel_id` |
| `SOURCE_MESSAGE_NOT_FOUND` | 404 | 任务结果通知或会话锚点引用的外部来源消息尚未写入 SourceMessage Inbox |
## 10. HMAC 上线配置
上线需要配置:
| 配置项 | 是否必填 | 中文说明 |
| --- | --- | --- |
| `SUPERAGENT_DEV_TASK_RESULT_HMAC_SECRET` | dev 必填 | dev HMAC 共享密钥;查询接口和任务结果通知接口共用 |
| `SUPERAGENT_TEST_TASK_RESULT_HMAC_SECRET` | test 必填 | test HMAC 共享密钥;查询接口和任务结果通知接口共用 |
| `SUPERAGENT_PROD_TASK_RESULT_HMAC_SECRET` | prod 必填 | prod HMAC 共享密钥;生产不能为空,只能通过 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` |
上线注意事项:
- 本系统和 SuperAgent 必须配置同一个 HMAC secret。
- 生产 secret 只能放在部署平台 Secret 或环境变量中,不能写入仓库、镜像、前端配置或普通文档。
- SuperAgent 必须使用原始请求体计算 SHA-256不能使用格式化后 JSON。
- HMAC canonical string 的第二行必须使用 `request_path`,例如 `/api/ai-query/v1/case-context`
- SuperAgent 每次请求必须生成全新的 nonce同一个 `client_id + nonce` 在 TTL 窗口内不能重复使用。
- 调用双方服务器时间必须同步,建议使用 NTP。
- 建议所有接口只暴露在 HTTPS 和可信网络边界内。
- 轮换 secret 时需要安排双写或短窗口切换;当前第一版后端只支持一个 secret轮换窗口内需要协调发布顺序。