# TH Hotel SuperAgent API 对接契约 ## 文档信息 | 项目 | 内容 | | --- | --- | | 文档版本 | 0.2 | | 日期 | 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=` | | `X-TH-Hotel-Request-Id` | 否 | 调用方请求 ID,用于日志串联 | | `X-TH-Hotel-AI-Trace-Id` | 否 | AI 运行链路 ID,查询接口会原样带回 `trace_id` | ### 2.2 签名串 签名串使用接口 path,不包含域名、query string 或 fragment。 ```text POST ``` 签名算法: ```text signature = HMAC_SHA256(SUPERAGENT_TASK_RESULT_HMAC_SECRET, canonical_string) ``` Header 写法: ```text X-TH-Hotel-SuperAgent-Signature: sha256= ``` ### 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`,第一版后端会接收但忽略,不校验它们的格式,也不把它们作为查询边界。 ## 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-TEST", "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-TEST", "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:SuperAgent 通知 AI 任务结果 ### 6.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 过渡层、订单、任务和任务卡 | ### 6.2 请求体 ```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": [] } ``` 字段说明: | 字段 | 是否必填 | 中文说明 | | --- | --- | --- | | `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 正式契约。 ### 6.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, "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": [] } ``` ## 7. 错误响应 ### 7.1 查询接口错误响应 ```json { "success": false, "request_id": "req-001", "trace_id": "trace-001", "data": null, "warnings": [], "error": { "code": "AUTH_SIGNATURE_INVALID", "message": "签名校验失败。", "details": {} } } ``` ### 7.2 任务结果通知接口错误响应 ```json { "request_id": null, "error_code": "AUTH_SIGNATURE_INVALID", "message": "签名校验失败。", "details": [] } ``` ### 7.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 | 对象详情查询目标不存在 | | `HOTEL_ID_REQUIRED` | 400 | 任务结果通知使用外部 `source_message_id` 时缺少 `hotel_id` | | `SOURCE_MESSAGE_NOT_FOUND` | 404 | 任务结果通知引用的外部来源消息尚未写入 SourceMessage Inbox | ## 8. 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,轮换窗口内需要协调发布顺序。