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

394 lines
12 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.1 |
| 日期 | 2026-07-08 |
| 状态 | 第一版后端已实现接口契约 |
| 适用范围 | SuperAgent 调用本系统查询上下文、提交 AI 任务结果 |
| 主要读者 | SuperAgent 对接方、后端、测试、运维 |
## 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_TASK_RESULT_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. 接口 1查询订单上下文
### 3.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 |
### 3.2 请求体
```json
{
"hotel_id": "HOTEL-TEST",
"source_message_id": "1900000000000000001",
"source_event_index": 1,
"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 |
| `source_message_id` | 否 | 当前 SourceMessage ID全局上下文查询时可不传只作为审计和排查字段 |
| `source_event_index` | 否 | 当前 AI 事件序号;全局上下文查询可不传,传入时必须为正整数 |
| `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` 仅用于审计、追踪或排查,不作为查询边界。
### 3.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
}
```
### 3.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 表,固定空数组 |
## 4. 接口 2查询对象详情
### 4.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 |
### 4.2 请求体
```json
{
"hotel_id": "HOTEL-TEST",
"object_id": "ORDER:1900000000000000100",
"object_type": "group_block"
}
```
字段说明:
| 字段 | 是否必填 | 中文说明 |
| --- | --- | --- |
| `hotel_id` | 是 | 酒店上下文 ID |
| `object_id` | 是 | 查询对象 ID第一版只支持 `ORDER:{order_id}` |
| `object_type` | 否 | 调用方对象类型提示,第一版不作为强校验 |
### 4.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:00",
"last_updated_at": "2026-07-08T01:10:00",
"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
}
```
## 5. 接口 3SuperAgent 通知 AI 任务结果
### 5.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 过渡层、订单、任务和任务卡 |
### 5.2 请求体
```json
{
"source_message_id": "1900000000000000001",
"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": []
}
```
字段说明:
| 字段 | 是否必填 | 中文说明 |
| --- | --- | --- |
| `source_message_id` | 是 | 本系统 SourceMessage ID一次请求只能有一个 |
| `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` | 否 | 业务字段主体 |
### 5.3 成功响应
```json
{
"request_id": "req-003",
"source_message_id": "1900000000000000001",
"batch_id": "1900000000000000200",
"idempotent_replay": false,
"accepted_count": 1,
"items": [
{
"source_event_index": 1,
"array_index": 1,
"order_id": "1900000000000000300",
"task_id": "1900000000000000400",
"task_status": "PENDING_CONFIRM",
"order_status": "TEMPORARY",
"system_task_type": "NEW_BOOKING",
"task_card_type": "NEW_BOOKING",
"warnings": []
}
],
"warnings": []
}
```
## 6. 错误响应
### 6.1 查询接口错误响应
```json
{
"success": false,
"request_id": "req-001",
"trace_id": "trace-001",
"data": null,
"warnings": [],
"error": {
"code": "AUTH_SIGNATURE_INVALID",
"message": "签名校验失败。",
"details": {}
}
}
```
### 6.2 任务结果通知接口错误响应
```json
{
"request_id": null,
"error_code": "AUTH_SIGNATURE_INVALID",
"message": "签名校验失败。",
"details": []
}
```
### 6.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 |
| `OBJECT_NOT_FOUND` | 404 | 对象详情查询目标不存在 |
| `SOURCE_MESSAGE_NOT_FOUND` | 400 | 任务结果通知引用的 SourceMessage 不存在 |
## 7. HMAC 上线配置
上线需要配置:
| 配置项 | 是否必填 | 中文说明 |
| --- | --- | --- |
| `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轮换窗口内需要协调发布顺序。