实现MCP任务结果提交稳定性映射

This commit is contained in:
andy
2026-07-12 19:10:03 +08:00
parent 937aa569ff
commit 9d095cfd53
15 changed files with 1468 additions and 17 deletions

View File

@@ -561,6 +561,7 @@ V3 字段说明:
当前已支持的 V3 行为:
- 如果通过 MCP `th_hotel_submit_task_results` 调用MCP adapter 会在进入业务入站服务前按 `message_events[]` 顺序把 Agent 内部事件 ID 映射为本系统一基 `source_event_index`,并校验 `related_source_event_index``parent_source_event_index``related_source_event_indices[]` 是否悬空或重复。
- 40 条 P0.1 路由进入后端枚举 / 稳定配置。
- `route_code` 是稳定代码,不因路由总数从 42 调整为 40 而重编号;联调方不要按数字连续性判断合法性。
- 结构化 `S10/S99` 创建只读 `SOURCE_MESSAGE_ONLY` 任务,任务列表可见,订单列表不可见。

View File

@@ -23,6 +23,7 @@ server/src/main/java/cn/nianxx/thhotel/integrations/mcp/superagent/
| --- | --- |
| `integration-guide.md` | MCP 总体接入说明和调用顺序 |
| `tools.md` | 5 个 MCP tools 的工具契约 |
| `submit-payload-mapping.md` | `th_hotel_submit_task_results` 的 V3/P0.1 payload adapter、事件索引映射和提交前校验 |
| `security-policy.md` | 鉴权、权限、正文、附件和日志边界 |
| `deployment-guide.md` | 部署参数、环境变量和上线顺序 |
| `test-cases.md` | SuperAgent 联调测试用例 |

View File

@@ -132,6 +132,8 @@ REST request
写入工具里的 `source_message_id` 必须来自 AgentBus payload 的 `source.external_message_id`。SuperAgent 不需要传数据库层 provider/channelTH Hotel 后端按系统酒店和外部消息 ID 匹配唯一 SourceMessage Inbox真实 channel 可能是 `OUTLOOK`
M002 V3 后,写入工具优先接收结构化 `S10/S99``source_message + message_events[]` 业务根。`message_events[].source_event_index` 可以是 SuperAgent 内部事件 ID例如 `E_CHILD_1`MCP adapter 会在提交前按数组顺序映射为本系统一基数字索引,并校验跨事件关系是否悬空或重复。详细映射规则见 `submit-payload-mapping.md`
## 9. 当前 checkpoint
当前 checkpoint
@@ -146,5 +148,6 @@ checkpoint-superagent-mcp-embedded-endpoint
- 5 个 MCP tools 均可通过 `tools/list` 发现。
- 只读 tool 直接复用现有查询 Service。
- 写入 tool 受 `MCP_ENABLE_SUBMIT_TASK_RESULTS` 开关控制。
- 写入 tool 已提供 V3/P0.1 submit payload adapter 和提交前 schema validator。
- MCP endpoint 使用 Bearer Token 鉴权。
- 文档不包含生产 Secret、真实客户数据、附件 URL 或原始邮件正文。

View File

@@ -129,6 +129,7 @@ MCP endpoint 日志允许记录:
写入工具:
- 默认不自动重试已发送的写请求。
- MCP submit payload adapter 校验失败时,不调用业务写入 Service也不触发自动重试。
- 如果调用方需要重试,必须依赖后端幂等机制和任务结果中的幂等信息。
- 对于响应丢失但请求可能已到达后端的场景,应优先查询已有任务或人工排查,避免重复写入。

View File

@@ -0,0 +1,308 @@
# TH Hotel MCP Submit Payload Mapping
## 1. 文档信息
| 项目 | 内容 |
| --- | --- |
| 文档版本 | 0.1 |
| 日期 | 2026-07-12 |
| 状态 | 已落地第一版 |
| 适用范围 | `th_hotel_submit_task_results` 的 V3/P0.1 payload adapter、schema validator 和一次提交规则 |
## 2. 核心原则
`th_hotel_submit_task_results` 接收的是 SuperAgent 已经冻结的业务结果。MCP 层只负责把业务结果转换为本系统可接收的提交 payload不重新解释业务含义不调用 Skill不根据字段名猜测任务类型。
目标链路:
```text
SuperAgent 业务结果
-> MCP Submit Payload Adapter
-> MCP Submit Schema Validator
-> ReservationAiTaskIntakeService
-> MCP tool result
```
实现位置:
```text
server/src/main/java/cn/nianxx/thhotel/integrations/mcp/superagent/service/SuperAgentMcpSubmitPayloadAdapter.java
server/src/main/java/cn/nianxx/thhotel/integrations/mcp/superagent/service/impl/SuperAgentMcpSubmitPayloadAdapterImpl.java
```
## 3. 支持的输入形态
| 形态 | 判断方式 | 处理方式 |
| --- | --- | --- |
| V3 业务根 | 同时包含 `source_message``message_events` | 先校验根结构、source message、event 必填字段和关系索引,再按 `message_events[]` 顺序映射 `source_event_index` |
| V3 S10/S99 入口通知 | 同时包含 `source_message``route_code` | 校验 S10/S99 根字段和 source message 后透传给业务入站服务 |
| V2 兼容任务结果 | 包含 `ai_task_results` | 保留旧 `source_message_id + ai_task_results[]` 兼容路径 |
未知根字段会在 MCP 层被拒绝,错误码为 `MCP_SUBMIT_PAYLOAD_INVALID`
`source_message_id` 缺失属于既有基础设施输入错误MCP adapter 不改写该错误通道;业务入站服务会返回 `MISSING_SOURCE_MESSAGE_ID`
## 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 酒店解析 |
`source_message_id` 不是本系统数据库主键,也不是 `platform_source_message_inbox.id`
## 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 层拒绝。
## 6. ai_task_results[] 兼容 item schema
V2 兼容路径仍保留,`ai_task_results[]` item 以 REST 总契约为准,第一版 MCP schema 只暴露 object但业务入站会校验核心字段。
建议 item 结构:
```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
}
```
新数据优先使用 V3 业务根或 V3 S10/S99不建议继续新增 V2 `ai_task_results[]`
## 7. extraction_warnings 规则
| 形态 | 规则 |
| --- | --- |
| V3 业务根 | `extraction_warnings` 必须存在且为数组,无警告传 `[]` |
| V2 兼容 | `extraction_warnings` 可存在且为数组;缺省由业务层按空数组处理 |
MCP 层不解释 warning 语义,不因为 warning 自动创建任务。
## 8. 成功示例Parent Split
提交给 MCP tool 的业务结果可以使用 Agent 内部事件 ID
```json
{
"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"]
}
```
## 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"
}
}
}
}
```
悬空关系:
```json
{
"related_source_event_indices": ["E_CHILD_1", "E_UNKNOWN_CHILD"]
}
```
同样返回 `MCP_SUBMIT_PAYLOAD_INVALID``details.field=related_source_event_indices`,不会调用业务写入 Service。
## 11. 一次提交和失败不重试
当前 MCP endpoint 与后端业务 Service 同进程:
- adapter 校验失败时,不调用 `ReservationAiTaskIntakeService`
- adapter 校验通过后,只调用一次 `ReservationAiTaskIntakeService.accept`
- MCP endpoint 不做自动重试。
- 业务结果不会因为 MCP 响应被回写或污染。
当前回归测试:
```text
./mvnw -Dtest=SuperAgentMcpControllerTest,SuperAgentMcpSubmitEnabledControllerTest test
```
覆盖内容:
- `tools/list` 暴露 V3 submit schema。
- 缺失 `source_message_id` 保持既有 `MISSING_SOURCE_MESSAGE_ID` 错误。
- 未知根字段在业务层前被拒绝。
- 悬空 `related_source_event_indices` 在业务层前被拒绝。
- 重复 `related_source_event_indices` 在业务层前被拒绝。
- `E_CHILD_1/E_CHILD_2/E_PARENT` 按数组顺序映射为 `1/2/3`
- 合法 Parent split 只创建一个 AI batch并返回 `accepted_count=3`

View File

@@ -93,12 +93,21 @@
| MCP-T05-007 | hotel id 不一致 | 显式传非系统酒店 `hotel_id` | 返回 `HOTEL_ID_MISMATCH` |
| MCP-T05-008 | source message 入库渠道为 OUTLOOK | 不传 `source_channel`,只传外部 `source_message_id` | 后端按真实 Inbox 渠道写入成功 |
| MCP-T05-009 | source message 多渠道重复 | 同一酒店存在相同外部 `source_message_id` 的多条 Inbox | 返回 `SOURCE_MESSAGE_AMBIGUOUS` |
| MCP-T05-010 | V3 Parent split 使用 Agent 内部事件 ID | `message_events[].source_event_index=E_CHILD_1/E_CHILD_2/E_PARENT` | MCP adapter 按数组顺序映射为 `1/2/3`,返回 `accepted_count=3` |
| MCP-T05-011 | V3 缺失 source message id | `source_message.source_message_id` 缺失 | 返回既有 `MISSING_SOURCE_MESSAGE_ID` typed infrastructure error |
| MCP-T05-012 | V3 多事件关系悬空 | `related_source_event_indices` 引用不存在的事件 ID | 返回 `MCP_SUBMIT_PAYLOAD_INVALID`,不写 AI batch、订单或任务 |
| MCP-T05-013 | V3 多事件关系重复 | `related_source_event_indices=["E_CHILD_1","E_CHILD_1"]` | 返回 `MCP_SUBMIT_PAYLOAD_INVALID`,不写 AI batch、订单或任务 |
| MCP-T05-014 | V3 根节点未知字段 | 根节点存在 `unexpected_root` | 返回 `MCP_SUBMIT_PAYLOAD_INVALID`,不调用业务写入 Service |
| MCP-T05-015 | V3 跨 Child Trace | Trace event 关联全部 Child event | `related_source_event_indices[]` 保留全量关系并映射为真实索引,不压缩到第一个 Child |
写入验证:
- `ai_task_results[]` 顺序不能被 MCP endpoint 改变。
- V3 `message_events[]` 顺序是 `source_event_index` 的唯一基准MCP endpoint 不能重排。
- `E1/E2/E_PARENT` 等 Agent 内部事件 ID 只能由 MCP adapter 转换,不能作为本系统最终 `source_event_index`
- MCP endpoint 不能在日志输出完整 `extracted_fields` 中的敏感内容。
- 失败响应应保留后端错误码和 message。
- MCP adapter 校验失败时不进入业务写入 Service且不自动重试。
## 9. MCP 鉴权和开关测试
@@ -127,6 +136,7 @@
- 5 个工具正常成功调用。
- 查询接口错误 envelope 不丢失。
- 任务结果写入成功和幂等重放正常。
- V3 submit payload adapter 的事件索引映射、悬空关系拒绝、重复关系拒绝和未知字段拒绝正常。
- provider/channel 隔离正常。
- 受控正文不返回附件 URL。
- MCP auth 失败不进入业务 Service。

View File

@@ -347,7 +347,13 @@ POST /api/ai-query/v1/message-conversation/messages
### 7.4 输入 Schema
迁移提醒:当前 MCP tool 仍对应 M002 V2 的 `ai_task_results[]` 阶段契约。M002 V3 已确认迁移到结构化 `S10/S99` 和业务根 `message_events[]`,后续 MCP tool schema 必须跟随 `docs/project/integrations/superagent-api-contract.md``docs/project/requirements/M002-order-task-workflow-v3.md` 同步更新;在实现前不要把下方 schema 当作 V3 新入口。
当前 MCP tool 已支持三种输入形态:
1. V3 业务根:`source_message + message_events[]`
2. V3 S10/S99 入口通知:`source_message + route_code`
3. V2 兼容任务结果:`source_message_id + ai_task_results[]`
MCP 层会先调用 `SuperAgentMcpSubmitPayloadAdapter` 做提交前校验和事件索引映射。`E1``E_CHILD_1``E_PARENT` 等 Agent 内部事件 ID 不会直接进入业务层adapter 会按 `message_events[]` 顺序生成本系统一基数字 `source_event_index`,并同步映射 `related_source_event_index``parent_source_event_index``related_source_event_indices[]`
```json
{
@@ -366,36 +372,115 @@ POST /api/ai-query/v1/message-conversation/messages
"type": ["string", "null"],
"description": "兼容字段写入工具通常不需要传后端写入定位不使用该字段AgentBus 实际入库渠道可能是 OUTLOOK"
},
"source_message": {
"type": "object",
"description": "V3 来源邮件元数据source_message_id 对应 AgentBus source.external_message_id",
"properties": {
"source_message_id": {
"type": "string"
},
"subject": {
"type": ["string", "null"]
},
"from": {
"type": ["string", "null"]
},
"cc": {
"type": "array",
"items": {
"type": "string"
}
},
"received_at": {
"type": ["string", "null"]
},
"source_channel": {
"type": "string",
"enum": ["Email"]
}
},
"required": ["source_message_id", "subject", "from", "cc", "received_at", "source_channel"]
},
"route_code": {
"type": ["string", "null"],
"description": "V3 S10/S99 入口通知路由码"
},
"handler_type": {
"type": ["string", "null"],
"description": "V3 Main Agent 输出处理器类型"
},
"result_type": {
"type": ["string", "null"],
"description": "V3 入口通知或 V2 任务结果类型"
},
"current_or_history": {
"type": ["string", "null"],
"description": "V3 current/history 标记"
},
"agent_assessment": {
"type": "object",
"description": "V3 S10/S99 入口判断摘要"
},
"notification": {
"type": "object",
"description": "V3 S10/S99 通知展示信息"
},
"manual_review": {
"type": ["object", "null"],
"description": "V3 人工复核对象S10 可为空S99 必须完整"
},
"message_events": {
"type": "array",
"description": "V3 业务事件数组MCP Adapter 会按数组顺序生成一基 source_event_index",
"items": {
"type": "object"
}
},
"case_candidates": {
"type": "array",
"description": "V3 订单候选数组,无候选传空数组",
"items": {
"type": "object"
}
},
"unhandled_current_intents": {
"type": "array",
"description": "V3 未覆盖当前意图数组,无意图传空数组",
"items": {
"type": "object"
}
},
"source_message_id": {
"type": "string",
"description": "外部来源消息 ID对应 AgentBus source.external_message_id"
"description": "V2 兼容字段:外部来源消息 ID对应 AgentBus source.external_message_id"
},
"ai_task_results": {
"type": "array",
"description": "AI 拆分出的任务结果,必须保留数组顺序",
"description": "V2 兼容字段:AI 拆分出的任务结果,必须保留数组顺序",
"items": {
"type": "object"
}
},
"extraction_warnings": {
"type": "array",
"description": "AI 抽取警告",
"description": "AI 抽取警告V3/V2 都允许,缺省按空数组处理",
"items": {
"type": "object"
}
}
},
"required": ["source_message_id", "ai_task_results"]
"required": []
}
```
说明:
- `ai_task_results[]` 内部字段较多,完整结构`superagent-api-contract.md` 第 8 节为准。
- MCP endpoint 不应重排 `ai_task_results[]`
- V3 业务根字段、S10/S99 字段和 V2 `ai_task_results[]` item 完整语义`superagent-api-contract.md` 第 8 节为准。
- BusinessResult 到 MCP payload 的稳定映射、`source_event_index` 正式定义、跨 Child Trace 示例和拒绝示例见 `submit-payload-mapping.md`
- MCP endpoint 不应重排 V2 `ai_task_results[]`,也不应重排 V3 `message_events[]`
- `source_message_id` 必须是 AgentBus payload 的 `source.external_message_id`,不是内部 `platform_source_message_inbox.id`;写入工具不要求 SuperAgent 知道 Inbox 的真实 channel。
- 同一系统酒店下如果外部 `source_message_id` 匹配多条 Inbox业务 Service 返回 `SOURCE_MESSAGE_AMBIGUOUS`MCP tool result 应原样保留该错误码和 message。
- 如后续需要强 schema 校验,可在 MCP endpoint 内复制 REST 契约中的细粒度字段约束
- MCP adapter 校验失败时返回 `MCP_SUBMIT_PAYLOAD_INVALID`,不会调用业务写入 Service也不会自动重试
### 7.5 输出
@@ -448,4 +533,5 @@ MCP 层新增错误建议:
| `MCP_REQUEST_BODY_TOO_LARGE` | MCP 请求体超过 10MB 默认限制或环境配置限制 |
| `MCP_METHOD_NOT_FOUND` | MCP 方法不存在 |
| `MCP_TOOL_NOT_FOUND` | MCP 工具不存在 |
| `MCP_SUBMIT_PAYLOAD_INVALID` | 写入工具 payload 未通过 MCP adapter 提交前校验 |
| `MCP_INTERNAL_ERROR` | MCP endpoint 内部异常 |