收口MCP提交为V4契约

This commit is contained in:
andy
2026-07-22 23:43:54 +07:00
parent 68ff9e2b38
commit 5df2910465
21 changed files with 897 additions and 1609 deletions

View File

@@ -4,22 +4,24 @@
| 项目 | 内容 |
| --- | --- |
| 文档版本 | 0.2 |
| 日期 | 2026-07-12 |
| 状态 | 已落地第一版 |
| 适用范围 | `th_hotel_submit_task_results`V3/P0.1 payload adapter、schema validator 和一次提交规则 |
| 文档版本 | 0.3 |
| 日期 | 2026-07-22 |
| 状态 | 当前有效M002 V4-only |
| 适用范围 | `th_hotel_submit_task_results`M002 V4 payload adapter、schema validator 和一次提交规则 |
## 2. 核心原则
`th_hotel_submit_task_results` 接收的是 SuperAgent 已经冻结的业务结果。MCP 层只负责把业务结果转换为本系统可接收的提交 payload,不重新解释业务含义不调用 Skill不根据字段名猜测任务类型。
`th_hotel_submit_task_results` 接收 M002 V4 任务结果。MCP 层只做 transport 形态校验和 V4 根结构防错,不重新解释业务含义不调用 Skill不根据字段名猜测任务类型。
当前开发阶段已确认不维护 V2/V3 submit 兼容。旧 `source_message + message_events[] + case_candidates[]`、旧结构化 `S10/S99``source_message_id + ai_task_results[]` 都不会再通过 MCP submit 写入业务层。
目标链路:
```text
SuperAgent 业务结果
SuperAgent M002 V4 业务结果
-> MCP Submit Payload Adapter
-> MCP Submit Schema Validator
-> ReservationAiTaskIntakeService
-> ReservationAiTaskIntakeService V4 入站
-> V4 order task / V4 task card / V4 source notification
-> MCP tool result
```
@@ -34,321 +36,204 @@ server/src/main/java/cn/nianxx/thhotel/integrations/mcp/superagent/service/impl/
| 形态 | 判断方式 | 处理方式 |
| --- | --- | --- |
| V3 业务 | 包含 `message_events` | 校验根结构、source message、event transport 字段和关系索引,再按 `message_events[]` 顺序映射 `source_event_index`event 业务合法性仍交给业务入站层落 `adapter_contract_error` |
| V3 S10/S99 入口通知 | 包含 `route_code` 或入口通知 `result_type` | 校验 S10/S99 根字段和 source message透传业务入站服务 |
| V2 兼容任务结果 | 包含 `ai_task_results` | 保留旧 `source_message_id + ai_task_results[]` 兼容路径,并在 MCP 层校验完整 item schema |
| V4 普通业务 | 根字段包含 `route_code=null``source_message``order_contexts[]``message_events[]` | MCP adapter 校验根结构和 V4 `source_message` 后原样透传;业务入站层创建 V4 订单任务和任务卡 |
| V4 来源通知 | 根字段包含 `route_code=S10/S99``source_message`、空 `order_contexts[]`、空 `message_events[]` | MCP adapter 校验根结构和 V4 `source_message` 后原样透传业务入站层创建 V4 来源通知 |
不再支持的输入形态:
| 旧形态 | 示例特征 | 当前处理 |
| --- | --- | --- |
| V3 业务根 | `case_candidates``extraction_warnings``unhandled_current_intents`、旧 `source_message.from` | 返回 `MCP_SUBMIT_V4_REQUIRED`,不调用业务写入 Service |
| V3 S10/S99 | `handler_type``result_type=source_message_review_notification`、旧 `source_message.received_at` | 返回 `MCP_SUBMIT_V4_REQUIRED`,不调用业务写入 Service |
| V2 任务结果 | 顶层 `source_message_id + ai_task_results[]` | 返回 `MCP_SUBMIT_V4_REQUIRED`,不调用业务写入 Service |
未知根字段会在 MCP 层被拒绝,错误码为 `MCP_SUBMIT_PAYLOAD_INVALID`
`source_message_id` 缺失或整个 `source_message` 缺失属于既有基础设施输入错误MCP adapter 不改写该错误通道;业务入站服务会返回 `MISSING_SOURCE_MESSAGE_ID`
## 4. V4 根结构
V3 业务根和 S10/S99 如果携带 MCP schema 中的兼容字段 `hotel_id``source_provider``source_channel`adapter 会接受并在转交业务入站层前移除。业务定位仍以系统酒店和 `source_message.source_message_id` 为准。
```json
{
"route_code": null,
"source_message": {},
"order_contexts": [],
"message_events": []
}
```
## 4. source_message_id 定义
字段规则:
| 字段 | 规则 |
| --- | --- |
| `source_message.source_message_id` | V3 必填;必须来自 AgentBus 原始 payload 的外部消息 ID对应 `platform_source_message_inbox.external_message_id` |
| `source_message_id` | V2 兼容必填;同样对应 `platform_source_message_inbox.external_message_id` |
| `hotel_id` | SuperAgent 正式提交不需要传;本系统按平台唯一 ACTIVE 酒店解析 |
| `route_code` | 必须存在;普通业务为 `null`,来源通知为 `S10``S99` |
| `source_message` | 必须存在;结构见本文第 5 节 |
| `order_contexts[]` | 必须存在且为数组;普通业务至少一条,来源通知为空数组 |
| `message_events[]` | 必须存在且为数组;普通业务至少一条,来源通知为空数组 |
`source_message_id` 不是本系统数据库主键,也不是 `platform_source_message_inbox.id`
MCP adapter 不再映射 `source_event_index`。V4 `message_events[]` 的数组顺序就是业务处理顺序;业务层使用数组位置派生一基 event index
## 5. source_event_index 映射
SuperAgent 业务结果中的 `E1``E_CHILD_1``E_PARENT` 等值只是 Agent 内部事件 ID不能直接当成本系统的 `source_event_index`
MCP adapter 按 `message_events[]` 数组顺序生成一基数字索引:
| 业务事件 ID | 数组位置 | 本系统 `source_event_index` |
| --- | --- | --- |
| `E_CHILD_1` | 第 1 个 event | `1` |
| `E_CHILD_2` | 第 2 个 event | `2` |
| `E_PARENT` | 第 3 个 event | `3` |
| `E_TRACE` | 第 4 个 event | `4` |
映射规则:
- `message_events[].source_event_index` 输出为数字。
- `related_source_event_index` 输出为字符串数字,例如 `"1"`
- `parent_source_event_index` 输出为数字。
- `related_source_event_indices[]` 输出为字符串数字数组,保持原顺序。
- 悬空引用、重复引用、缺失事件 ID 都在 MCP 层拒绝。
- MCP tool result 会返回 `mapping_diagnostics.source_event_index_mapping[]`,记录原始事件 ID 到本系统索引的映射;该诊断不写入 `workflow_reservation_ai_transition.ai_payload_json`
## 6. ai_task_results[] 兼容 item schema
V2 兼容路径仍保留,`ai_task_results[]` item 以 REST 总契约为准。当前 MCP `tools/list` 已暴露 item schemaadapter 也会在提交业务层前校验必填字段、字段类型、允许 `result_type`、未知字段和根级 `extraction_warnings`
必填字段:
- `source_event_index`
- `catalog_code`
- `skill_id`
- `result_type`
- `task_type`
允许的 `result_type`
- `normal_task`
- `manual_review`
- `informational_message`,仅历史兼容
建议 item 结构:
## 5. source_message
```json
{
"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,
"reservation_number": null,
"block_code": null
},
"relevant_message_excerpt": "Please create a new booking.",
"attachments": [],
"file_references": [],
"context_used": {},
"extracted_fields": {},
"manual_review": null,
"additional_operations": [],
"idempotency_key": null
"source_message_id": "mail-20260722-0001",
"conversation_id": "thread-001",
"subject": "Update Booking",
"sender": "agent@example.test",
"sent_at": "2026-07-22T08:00:00Z",
"body": "Please update the booking.",
"body_content_type": "text/plain",
"attachments": []
}
```
新数据优先使用 V3 业务根或 V3 S10/S99不建议继续新增 V2 `ai_task_results[]`
字段规则:
## 7. extraction_warnings 规则
| 形态 | 规则 |
| 字段 | 规则 |
| --- | --- |
| V3 业务根 | `extraction_warnings` 必须存在且为数组,无警告传 `[]` |
| V2 兼容 | `extraction_warnings` 可存在且为数组;缺省由业务层按空数组处理 |
| `source_message_id` | 必填非空;必须来自 AgentBus 原始 payload 的外部消息 ID对应 `platform_source_message_inbox.external_message_id` |
| `conversation_id` | 可缺省;出现时必须为字符串或 `null` |
| `subject` | 必须出现,可为 `null` |
| `sender` | 必须出现,可为 `null` |
| `sent_at` | 必须出现,可为 `null`;时间点使用 UTC ISO-8601 |
| `body` | 必须出现,可为 `null`;当前邮件原文,不摘要、不翻译、不重排 |
| `body_content_type` | 必填;只能为 `text/plain``text/html` |
| `attachments[]` | 必填数组;附件结构见第 6 节 |
MCP 层不解释 warning 语义,不因为 warning 自动创建任务
旧 V3 的 `from``cc``received_at``source_channel` 不再属于 MCP submit 契约。`hotel_id``source_provider``source_channel` 也不需要 SuperAgent 传入
## 8. 成功示例Parent Split
提交给 MCP tool 的业务结果可以使用 Agent 内部事件 ID
## 6. attachments
```json
{
"id": "att-1",
"name": "payment-slip.jpg",
"content_type": "image/jpeg",
"url": "https://upstream-storage.example/payment-slip.jpg",
"size": 251524
}
```
字段规则:
| 字段 | 规则 |
| --- | --- |
| `id` | 必填非空;包内稳定附件引用 |
| `name` | 必填非空;原始文件名 |
| `content_type` | 必填非空MIME 类型 |
| `url` | 必填非空;上游文件 URL普通 V4 task detail 不直接返回 |
| `size` | 可省略或为 `null`;有值时必须是数字 |
Payment 事件只能通过 `attachment_ids[]` 引用 `source_message.attachments[].id`,不能复制完整附件对象或按文件名猜测。
## 7. 成功示例
### 7.1 普通业务
```json
{
"route_code": null,
"source_message": {
"source_message_id": "mail-20260712-parent-split-001",
"subject": "Parent split booking request",
"from": "agent@example.test",
"cc": [],
"received_at": "2026-07-12T04:00:00Z",
"source_channel": "Email"
},
"message_events": [
{
"event_type": "New Booking",
"event_role": "travel_agent_request",
"source_event_index": "E_CHILD_1",
"current_or_history": "current",
"case_keys": {
"group_code": "CHILD-A",
"confirmation_number": null,
"reservation_number": null,
"block_code": null
},
"relevant_message_excerpt": "Create child group A.",
"attachments": [],
"file_references": [],
"context_used": {},
"extracted_fields": {
"booking_object_type": "Group Block"
},
"manual_review": null
},
{
"event_type": "New Booking",
"event_role": "travel_agent_request",
"source_event_index": "E_CHILD_2",
"current_or_history": "current",
"case_keys": {
"group_code": "CHILD-B",
"confirmation_number": null,
"reservation_number": null,
"block_code": null
},
"relevant_message_excerpt": "Create child group B.",
"attachments": [],
"file_references": [],
"context_used": {},
"extracted_fields": {
"booking_object_type": "Group Block"
},
"manual_review": null
},
{
"event_type": "Cancel Allotment",
"event_role": "travel_agent_request",
"source_event_index": "E_PARENT",
"current_or_history": "current",
"case_keys": {
"group_code": "PARENT",
"confirmation_number": null,
"reservation_number": null,
"block_code": "PARENT"
},
"relevant_message_excerpt": "Release parent after child split.",
"attachments": [],
"file_references": [],
"context_used": {},
"related_event_type": "New Booking",
"requires_downstream_hard_validation": true,
"related_source_event_indices": ["E_CHILD_1", "E_CHILD_2"],
"extracted_fields": {
"relationship_type": "linked_parent_release_after_child_split",
"parent_group_code": "PARENT",
"cancel_scope": "entire_allotment_control_block",
"parent_release_or_cancel_candidate": true,
"release_reason": "parent_to_child_allocation_split",
"allocation_split_from_parent": true,
"child_group_codes": ["CHILD-A", "CHILD-B"]
},
"manual_review": null
}
],
"case_candidates": [],
"extraction_warnings": [],
"unhandled_current_intents": []
}
```
MCP adapter 进入业务层前会把父事件关系映射为:
```json
{
"source_event_index": 3,
"related_source_event_indices": ["1", "2"]
}
```
MCP tool result 同时返回非业务诊断:
```json
{
"mapping_diagnostics": {
"mapping_policy": "message_events_array_order_1_based",
"source_event_index_mapping": [
"source_message_id": "mail-mcp-v4-business-001",
"conversation_id": "thread-mcp-v4-business-001",
"subject": "Group booking and payment",
"sender": "agent@example.test",
"sent_at": "2026-07-18T02:10:00Z",
"body": "Please create group GRP-MCP-V4-001 and note payment attached.",
"body_content_type": "text/plain",
"attachments": [
{
"original_source_event_index": "E_CHILD_1",
"mapped_source_event_index": 1
},
{
"original_source_event_index": "E_CHILD_2",
"mapped_source_event_index": 2
},
{
"original_source_event_index": "E_PARENT",
"mapped_source_event_index": 3
"id": "att-pay-1",
"name": "payment-slip.jpg",
"content_type": "image/jpeg",
"url": "https://upstream-storage.example/payment-slip.jpg",
"size": 251524
}
]
}
}
```
## 9. 成功示例:跨 Child Trace
当 Trace 覆盖完整 Parent split 的全部 Child 时,不能只绑定第一个 Child。业务结果应保留全部 Child 引用:
```json
{
"event_type": "Trace",
"source_event_index": "E_TRACE",
"related_source_event_indices": ["E_CHILD_1", "E_CHILD_2"],
"extracted_fields": {
"trace_subtype": "general_request"
}
}
```
MCP adapter 会把它映射为:
```json
{
"source_event_index": 4,
"related_source_event_indices": ["1", "2"]
}
```
## 10. 拒绝示例
未知字段:
```json
{
"source_message": {},
"message_events": [],
"case_candidates": [],
"extraction_warnings": [],
"unhandled_current_intents": [],
"unexpected_root": true
}
```
返回:
```json
{
"isError": true,
"structuredContent": {
"success": false,
"error": {
"code": "MCP_SUBMIT_PAYLOAD_INVALID",
"details": {
"field": "unexpected_root"
},
"order_contexts": [
{
"order_ref": "order-1",
"basic_information": {
"account_code": "QBD_TRAVEL",
"manual_review": null
}
}
}
],
"message_events": [
{
"order_ref": "order-1",
"event_type": "NEW_BOOKING",
"target_order": {
"booking_type": "GROUP",
"locator_type": "GROUP_CODE",
"locator_value": "GRP-MCP-V4-001"
},
"arrival_date": "2026-07-26",
"departure_date": "2026-07-29",
"rate_code": "GRPA2-850UP",
"booking_scenario": "STANDARD",
"room_items": [
{
"room_type_code": "RM2",
"room_count": 2
}
],
"manual_review": null
},
{
"order_ref": "order-1",
"event_type": "PAYMENT",
"target_order": {
"booking_type": "GROUP",
"locator_type": "GROUP_CODE",
"locator_value": "GRP-MCP-V4-001"
},
"attachment_ids": ["att-pay-1"],
"manual_review": null
}
]
}
```
悬空关系:
### 7.2 S99 来源通知
```json
{
"related_source_event_indices": ["E_CHILD_1", "E_UNKNOWN_CHILD"]
"route_code": "S99",
"source_message": {
"source_message_id": "mail-mcp-v4-s99-001",
"conversation_id": "thread-mcp-v4-s99-001",
"subject": "Cannot form material package",
"sender": "guest@example.test",
"sent_at": "2026-07-18T02:10:00Z",
"body": "The input does not contain enough business material.",
"body_content_type": "text/plain",
"attachments": []
},
"order_contexts": [],
"message_events": []
}
```
同样返回 `MCP_SUBMIT_PAYLOAD_INVALID``details.field=related_source_event_indices`,不会调用业务写入 Service。
## 8. 错误码
## 11. 一次提交和失败不重试
| 错误码 | 中文说明 |
| --- | --- |
| `MCP_SUBMIT_V4_REQUIRED` | MCP submit 收到的不是 M002 V4 根结构,通常是旧 V2/V3 payload |
| `MCP_SUBMIT_PAYLOAD_INVALID` | 已具备 V4 根字段,但字段类型、未知字段或 V4 `source_message` / `attachments` 结构不符合 MCP transport 校验 |
| `SOURCE_MESSAGE_NOT_FOUND` | V4 `source_message.source_message_id` 无法匹配 SourceMessage Inbox |
| `SOURCE_MESSAGE_AMBIGUOUS` | 同一系统酒店下外部 `source_message_id` 匹配多条 Inbox |
| `ADAPTER_CONTRACT_ERROR` | V4 包或 event 通过 MCP transport但不满足业务入站契约业务层记录 adapter contract error |
当前 MCP endpoint 与后端业务 Service 同进程:
MCP adapter 校验失败时不会调用业务写入 Service不会创建 AI batch、V4 order task、V4 card、V4 source notification 或旧任务。
- adapter 校验失败时,不调用 `ReservationAiTaskIntakeService`
- adapter 校验通过后,只调用一次 `ReservationAiTaskIntakeService.accept`
- MCP endpoint 不做自动重试。
- 业务结果不会因为 MCP 响应被回写或污染。
## 9. 诊断
当前回归测试
MCP 入站诊断表仍保存
```text
./mvnw -Dtest=SuperAgentMcpControllerTest,SuperAgentMcpSubmitEnabledControllerTest test
```
- `arguments_json`SuperAgent 调用 MCP tool 的原始 arguments。
- `adapted_payload_json`MCP adapter 后交给业务入站层的 payloadV4-only 模式下通常与 arguments 相同。
- `mapping_diagnostics_json`:当前 V4-only 模式下为空对象;旧 V3 事件 ID 映射已经废弃。
- `safe_error_code` / `safe_error_summary`:安全错误摘要。
覆盖内容:
- `tools/list` 暴露 V3 submit schema。
- `tools/list` 暴露 V2 `ai_task_results[]` item schema 和 V3 `relationship_type`
- 缺失 `source_message_id` 或整个 `source_message` 保持既有 `MISSING_SOURCE_MESSAGE_ID` 错误。
- 未知根字段在业务层前被拒绝。
- V2 item 缺必填字段在业务层前被拒绝。
- 悬空 `related_source_event_indices` 在业务层前被拒绝。
- 重复 `related_source_event_indices` 在业务层前被拒绝。
- `E_CHILD_1/E_CHILD_2/E_PARENT` 按数组顺序映射为 `1/2/3`
- 跨 Child Trace 关系映射为完整 `["1", "2"]`,不压缩到第一个 Child。
- S10/S99 可通过 MCP 写入工具创建只读来源消息任务。
- V3 event 业务契约错误由业务入站层保存为 `adapter_contract_error` transition。
- 合法 Parent split 只创建一个 AI batch并返回 `accepted_count=3`
排障时先对比 `arguments_json``adapted_payload_json`。如果二者均为 V4 且业务层返回 `ADAPTER_CONTRACT_ERROR`,说明问题在 SuperAgent V4 业务契约内容或业务校验;如果错误为 `MCP_SUBMIT_V4_REQUIRED`,说明 SuperAgent 仍在按旧 schema 输出。