# TH Hotel MCP Submit Payload Mapping

## 1. 文档信息

| 项目 | 内容 |
| --- | --- |
| 文档版本 | 0.2 |
| 日期 | 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 业务根 | 包含 `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 |

未知根字段会在 MCP 层被拒绝，错误码为 `MCP_SUBMIT_PAYLOAD_INVALID`。

`source_message_id` 缺失或整个 `source_message` 缺失属于既有基础设施输入错误，MCP adapter 不改写该错误通道；业务入站服务会返回 `MISSING_SOURCE_MESSAGE_ID`。

V3 业务根和 S10/S99 如果携带 MCP schema 中的兼容字段 `hotel_id`、`source_provider`、`source_channel`，adapter 会接受并在转交业务入站层前移除。业务定位仍以系统酒店和 `source_message.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 层拒绝。
- 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 schema，adapter 也会在提交业务层前校验必填字段、字段类型、允许 `result_type`、未知字段和根级 `extraction_warnings`。

必填字段：

- `source_event_index`
- `catalog_code`
- `skill_id`
- `result_type`
- `task_type`

允许的 `result_type`：

- `normal_task`
- `manual_review`
- `informational_message`，仅历史兼容

建议 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"]
}
```

MCP tool result 同时返回非业务诊断：

```json
{
  "mapping_diagnostics": {
    "mapping_policy": "message_events_array_order_1_based",
    "source_event_index_mapping": [
      {
        "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
      }
    ]
  }
}
```

## 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。
- `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`。
