新增SuperAgent MCP入站诊断链路
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -69,6 +69,7 @@
|
||||
| --- | --- | --- |
|
||||
| `integrations/superagent-api-contract.md` | 权威契约 | 给 SuperAgent 对接方使用的 HTTP 接口总契约,记录查询上下文、对象详情、邮件会话任务、邮件会话正文、任务结果通知和统一 HMAC 规则。 |
|
||||
| `integrations/superagent-mcp/README.md` | 当前有效 | SuperAgent MCP 资料包入口;MCP tools 是 HTTP 总契约的 MCP 映射说明,不单独替代总契约。 |
|
||||
| `integrations/superagent-mcp/inbound-diagnostics.md` | 当前有效 | SuperAgent MCP 入站诊断链路说明,记录受控保存原始工具参数、adapter 后 payload 和安全错误摘要的排障口径。 |
|
||||
| `integrations/superagent-agentbus-project-integration-guide.md` | 当前有效 | 当前项目 SuperAgent 与 AgentBus 验证记录和项目级接入细节。 |
|
||||
| `../../mcp-server/README.md` | 当前有效 | SuperAgent MCP 方案入口指针;当前不单独部署 MCP 服务,运行时代码内嵌在 `server/`。 |
|
||||
|
||||
|
||||
@@ -24,6 +24,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、事件索引映射和提交前校验 |
|
||||
| `inbound-diagnostics.md` | MCP 入站诊断链路,说明如何受控保存原始工具参数、adapter 后 payload 和安全错误摘要 |
|
||||
| `security-policy.md` | 鉴权、权限、正文、附件和日志边界 |
|
||||
| `deployment-guide.md` | 部署参数、环境变量和上线顺序 |
|
||||
| `test-cases.md` | SuperAgent 联调测试用例 |
|
||||
|
||||
128
docs/project/integrations/superagent-mcp/inbound-diagnostics.md
Normal file
128
docs/project/integrations/superagent-mcp/inbound-diagnostics.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# TH Hotel SuperAgent MCP 入站诊断链路
|
||||
|
||||
## 1. 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 文档版本 | 0.1 |
|
||||
| 日期 | 2026-07-22 |
|
||||
| 状态 | 已落地第一版 |
|
||||
| 适用范围 | `POST /mcp` 的 SuperAgent MCP 请求排障、参数还原和安全审计 |
|
||||
|
||||
## 2. 背景
|
||||
|
||||
SuperAgent 通过 MCP `th_hotel_submit_task_results` 提交业务结果时,当前后端会把 MCP
|
||||
`params.arguments` 转换为本系统 `ReservationAiTaskIntakeService` 可接收的业务 payload。
|
||||
当业务入站失败时,如果只看到最终错误,例如 `SOURCE_MESSAGE_NOT_FOUND` 或
|
||||
`MCP_SUBMIT_PAYLOAD_INVALID`,很难判断问题来自:
|
||||
|
||||
- SuperAgent 实际传入的 MCP 参数。
|
||||
- MCP submit adapter 的转换逻辑。
|
||||
- 本系统业务入站校验或数据状态。
|
||||
|
||||
本 checkpoint 增加 MCP 入站诊断链路,只为联调排障和受控审计提供证据,不改变原有业务处理
|
||||
逻辑。
|
||||
|
||||
## 3. 设计原则
|
||||
|
||||
- 诊断记录不是业务事实,不参与订单、任务、卡片、来源通知或 OPERA 流转。
|
||||
- `/mcp` 原有鉴权、body size、tools/call、adapter、业务 Service 调用顺序不变。
|
||||
- 诊断写入失败时不得影响 MCP 原响应;只能记录安全 warn 日志。
|
||||
- 普通日志只输出诊断 ID、tool、外部 source message id、安全错误码和安全错误摘要。
|
||||
- 不在普通前端业务接口暴露 MCP 原始请求体、邮件正文、附件 URL、AI raw payload 或 Secret。
|
||||
- 查询类 MCP tool 的响应可能包含受控正文,第一版不保存完整响应,只保存响应安全摘要。
|
||||
|
||||
## 4. 第一版诊断表
|
||||
|
||||
新增表:
|
||||
|
||||
```text
|
||||
platform_superagent_mcp_call_diagnostic
|
||||
```
|
||||
|
||||
字段口径:
|
||||
|
||||
| 字段 | 中文说明 |
|
||||
| --- | --- |
|
||||
| `id` | MCP 调用诊断 ID |
|
||||
| `jsonrpc_id` | JSON-RPC request id 的安全文本表示 |
|
||||
| `method_name` | JSON-RPC method,例如 `tools/call` |
|
||||
| `tool_name` | MCP tool 名称,例如 `th_hotel_submit_task_results` |
|
||||
| `mcp_client_id` | 调用方机器身份,第一版固定 `superagent-mcp` |
|
||||
| `request_body_bytes` | 原始请求体 UTF-8 字节数 |
|
||||
| `request_body_sha256` | 原始请求体 SHA-256,用于不暴露正文时定位同一次请求 |
|
||||
| `raw_body_json` | 原始 MCP JSON-RPC 请求体,受控诊断字段 |
|
||||
| `arguments_json` | `params.arguments` 原始 JSON,受控诊断字段 |
|
||||
| `adapted_payload_json` | submit adapter 转换后送入业务入站层的 JSON;非 submit 或转换失败为空 |
|
||||
| `mapping_diagnostics_json` | submit adapter 事件索引映射诊断 |
|
||||
| `response_summary_json` | MCP 响应安全摘要,不保存完整查询结果或正文 |
|
||||
| `call_status` | `RECEIVED`、`SUCCEEDED`、`FAILED` |
|
||||
| `safe_error_code` | 安全错误码,例如 `SOURCE_MESSAGE_NOT_FOUND` |
|
||||
| `safe_error_summary` | 安全错误摘要,不包含邮件正文、HTML、附件 URL 或 Secret |
|
||||
| `source_message_external_id` | 从 submit 入参中尽力提取的外部来源消息 ID |
|
||||
| `hotel_id` | 后端解析出的系统酒店 ID;解析失败时为空 |
|
||||
| `created_at` / `updated_at` | UTC 创建 / 更新时间 |
|
||||
|
||||
## 5. 诊断写入时机
|
||||
|
||||
| 阶段 | 行为 |
|
||||
| --- | --- |
|
||||
| 鉴权失败 | 不写诊断表,避免为未授权请求保存原始数据 |
|
||||
| body 超限 | 不写诊断表,直接返回原有 body too large 错误 |
|
||||
| JSON 解析失败 | 鉴权通过后写入失败诊断,保存 body hash 和 raw body |
|
||||
| tools/list / query tool | 写入原始请求和参数,只保存响应安全摘要 |
|
||||
| submit adapter 成功 | 补写 `adapted_payload_json` 和 `mapping_diagnostics_json` |
|
||||
| submit adapter 失败 | 保留 `arguments_json`,记录 adapter 安全错误码 |
|
||||
| 业务入站失败 | 保留 `arguments_json` 和可用的 `adapted_payload_json`,记录业务安全错误码 |
|
||||
| 调用成功 | 标记 `SUCCEEDED`,保存响应安全摘要 |
|
||||
|
||||
## 6. 排障 SQL
|
||||
|
||||
按外部 SourceMessage ID 查询 SuperAgent 传入的原始 MCP 参数:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
id,
|
||||
created_at,
|
||||
call_status,
|
||||
method_name,
|
||||
tool_name,
|
||||
source_message_external_id,
|
||||
safe_error_code,
|
||||
safe_error_summary,
|
||||
arguments_json,
|
||||
adapted_payload_json,
|
||||
mapping_diagnostics_json
|
||||
FROM platform_superagent_mcp_call_diagnostic
|
||||
WHERE source_message_external_id = '<external_message_id>'
|
||||
ORDER BY created_at DESC, id DESC;
|
||||
```
|
||||
|
||||
按诊断 ID 查看一次请求的原始 envelope:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
id,
|
||||
request_body_sha256,
|
||||
raw_body_json,
|
||||
response_summary_json
|
||||
FROM platform_superagent_mcp_call_diagnostic
|
||||
WHERE id = <diagnostic_id>;
|
||||
```
|
||||
|
||||
## 7. 与现有链路关系
|
||||
|
||||
- REST `POST /api/integrations/superagent/task-results` 仍使用现有 batch / transition 追踪。
|
||||
- MCP `th_hotel_submit_task_results` 会同时拥有 MCP 入站诊断和业务 batch / transition 追踪。
|
||||
- Debug EML、AgentBus dispatch run 不改原逻辑;如果 SuperAgent 最终通过 MCP 回写,才进入本诊断表。
|
||||
- 第一版不新增前端页面和查询接口;DB 权限和运维访问由部署环境控制。
|
||||
|
||||
## 8. 安全说明
|
||||
|
||||
`raw_body_json`、`arguments_json` 和 `adapted_payload_json` 可能包含 SuperAgent 产出的邮件摘录、
|
||||
客户姓名、业务线索或附件 ID,因此:
|
||||
|
||||
- 不写入普通应用日志。
|
||||
- 不通过前端业务接口返回。
|
||||
- 不提交为测试夹具中的真实客户数据。
|
||||
- 生产使用时需要控制数据库访问权限,并在上线前确认保留周期。
|
||||
@@ -122,7 +122,7 @@
|
||||
| `POST /api/ai-query/v1/conversation-tasks` | `THIRD_PARTY_SUPERAGENT` | HMAC 鉴权 | 保持 HMAC;不返回邮件原文 | 记录请求 ID、client_id 和安全错误 |
|
||||
| `POST /api/ai-query/v1/conversation-source` | `THIRD_PARTY_SUPERAGENT` | HMAC 鉴权 | 保持 HMAC;只按契约返回需要字段 | 记录请求 ID、client_id 和安全错误 |
|
||||
| `POST /api/integrations/superagent/task-results` | `THIRD_PARTY_SUPERAGENT` | HMAC + nonce + timestamp + body hash | 保持;V4 / V3 / V2 共存期均必须用外部 `source_message_id` 匹配 Inbox,技术契约错误只落 adapter error,不创建用户可处理任务;V4 普通业务包只写 V4 订单任务 / 多卡模型,不再创建旧 `workflow_reservation_task`;V4 S10/S99 写入来源通知且不创建旧任务 | 记录 batch、transition、adapter error、幂等结果和安全错误 |
|
||||
| `/mcp` | `THIRD_PARTY_MCP` | Bearer Token;提交工具可独立开关 | 保持;工具级能力限制,不暴露无关接口 | 记录工具调用结果和业务入站结果 |
|
||||
| `/mcp` | `THIRD_PARTY_MCP` | Bearer Token;提交工具可独立开关 | 保持;工具级能力限制,不暴露无关接口 | 记录工具调用结果、业务入站结果和受控 MCP 入站诊断;诊断原文不进入普通前端接口或普通日志 |
|
||||
| AgentBus WebSocket | `THIRD_PARTY_AGENTBUS` | AgentBus Token + capture 开关 | 保持;只入 SourceMessage,不直接建业务任务 | 记录 SourceMessage、payload hash 和 dispatch run |
|
||||
|
||||
### 3.7 后端内部能力
|
||||
@@ -207,6 +207,7 @@
|
||||
| 业务审计 | `workflow_reservation_audit_log` | 任务确认、人工复核、订单归属确认、V4 来源通知 ack、Rooming List 触发 Group Booking Status 自动 DEF、OPERA 执行 / 重试 |
|
||||
| 邮件原文读取审计 | `platform_source_message_original_access_audit` | 读取邮件正文、HTML、附件外链、Payment 图片预览和非图片下载 |
|
||||
| SuperAgent 入站追踪 | `workflow_reservation_ai_batch`、`workflow_reservation_ai_transition` | task-results / MCP 提交、路由、adapter error |
|
||||
| SuperAgent MCP 入站诊断 | `platform_superagent_mcp_call_diagnostic` | MCP 原始工具参数、submit adapter 后 payload、事件索引映射和安全错误摘要;不保存完整查询 tool 响应 |
|
||||
| AgentBus 分发追踪 | `platform_superagent_dispatch_run` | SourceMessage 自动分发 SuperAgent、重试、失败摘要 |
|
||||
| 安全审计 | 后续可新增平台安全审计表 | 登录失败、签名失败、nonce 重放、越权访问 |
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
"cn.nianxx.thhotel.platform.navigation.mapper",
|
||||
"cn.nianxx.thhotel.platform.audit.mapper",
|
||||
"cn.nianxx.thhotel.workflows.reservation.mapper",
|
||||
"cn.nianxx.thhotel.integrations.ai.superagent.mapper"
|
||||
"cn.nianxx.thhotel.integrations.ai.superagent.mapper",
|
||||
"cn.nianxx.thhotel.integrations.mcp.superagent.mapper"
|
||||
})
|
||||
@EnableScheduling
|
||||
@SpringBootApplication
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package cn.nianxx.thhotel.integrations.mcp.superagent.common.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* SuperAgent MCP submit adapter 诊断更新。
|
||||
*
|
||||
* @param id MCP 调用诊断 ID
|
||||
* @param adaptedPayloadJson adapter 后送入业务入站层的 JSON
|
||||
* @param mappingDiagnosticsJson 事件索引映射诊断 JSON
|
||||
* @param sourceMessageExternalId 外部来源消息 ID
|
||||
* @param hotelId 后端解析出的酒店 ID
|
||||
* @param updatedAt 更新 UTC 时间
|
||||
*/
|
||||
public record SuperAgentMcpCallDiagnosticAdaptedPayloadUpdate(
|
||||
Long id,
|
||||
String adaptedPayloadJson,
|
||||
String mappingDiagnosticsJson,
|
||||
String sourceMessageExternalId,
|
||||
String hotelId,
|
||||
LocalDateTime updatedAt
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package cn.nianxx.thhotel.integrations.mcp.superagent.common.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* SuperAgent MCP 调用完成诊断更新。
|
||||
*
|
||||
* @param id MCP 调用诊断 ID
|
||||
* @param callStatus 最终调用状态
|
||||
* @param responseSummaryJson MCP 响应安全摘要 JSON
|
||||
* @param safeErrorCode 安全错误码
|
||||
* @param safeErrorSummary 安全错误摘要
|
||||
* @param updatedAt 更新 UTC 时间
|
||||
*/
|
||||
public record SuperAgentMcpCallDiagnosticCompletionUpdate(
|
||||
Long id,
|
||||
String callStatus,
|
||||
String responseSummaryJson,
|
||||
String safeErrorCode,
|
||||
String safeErrorSummary,
|
||||
LocalDateTime updatedAt
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package cn.nianxx.thhotel.integrations.mcp.superagent.common.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* SuperAgent MCP 调用诊断创建草稿。
|
||||
*
|
||||
* @param jsonrpcId JSON-RPC request id 的安全文本表示
|
||||
* @param methodName JSON-RPC method
|
||||
* @param toolName MCP tool 名称
|
||||
* @param mcpClientId MCP 调用方机器身份
|
||||
* @param requestBodyBytes 原始请求体 UTF-8 字节数
|
||||
* @param requestBodySha256 原始请求体 SHA-256
|
||||
* @param rawBodyJson 原始 MCP JSON-RPC 请求体
|
||||
* @param argumentsJson params.arguments 原始 JSON
|
||||
* @param callStatus 初始诊断状态
|
||||
* @param safeErrorCode 安全错误码
|
||||
* @param safeErrorSummary 安全错误摘要
|
||||
* @param sourceMessageExternalId 外部来源消息 ID
|
||||
* @param hotelId 酒店 ID
|
||||
* @param createdAt 创建 UTC 时间
|
||||
*/
|
||||
public record SuperAgentMcpCallDiagnosticDraft(
|
||||
String jsonrpcId,
|
||||
String methodName,
|
||||
String toolName,
|
||||
String mcpClientId,
|
||||
Integer requestBodyBytes,
|
||||
String requestBodySha256,
|
||||
String rawBodyJson,
|
||||
String argumentsJson,
|
||||
String callStatus,
|
||||
String safeErrorCode,
|
||||
String safeErrorSummary,
|
||||
String sourceMessageExternalId,
|
||||
String hotelId,
|
||||
LocalDateTime createdAt
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package cn.nianxx.thhotel.integrations.mcp.superagent.common.enums;
|
||||
|
||||
/**
|
||||
* SuperAgent MCP 调用诊断状态。只表示诊断记录生命周期,不参与业务任务状态流转。
|
||||
*/
|
||||
public enum SuperAgentMcpCallDiagnosticStatus {
|
||||
|
||||
/** 已接收并完成基础解析。 */
|
||||
RECEIVED,
|
||||
/** MCP 调用已成功返回。 */
|
||||
SUCCEEDED,
|
||||
/** MCP 调用返回协议错误或工具级错误。 */
|
||||
FAILED
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package cn.nianxx.thhotel.integrations.mcp.superagent.control;
|
||||
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.request.SuperAgentMcpJsonRpcRequest;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.result.SuperAgentMcpJsonRpcResponse;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.service.SuperAgentMcpCallDiagnosticService;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.service.SuperAgentMcpService;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.service.impl.SuperAgentMcpProperties;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
@@ -29,6 +30,7 @@ public class SuperAgentMcpController {
|
||||
private static final String BEARER_PREFIX = "Bearer ";
|
||||
|
||||
private final SuperAgentMcpService mcpService;
|
||||
private final SuperAgentMcpCallDiagnosticService diagnosticService;
|
||||
private final SuperAgentMcpProperties properties;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@@ -37,9 +39,11 @@ public class SuperAgentMcpController {
|
||||
*/
|
||||
public SuperAgentMcpController(
|
||||
SuperAgentMcpService mcpService,
|
||||
SuperAgentMcpCallDiagnosticService diagnosticService,
|
||||
SuperAgentMcpProperties properties,
|
||||
ObjectMapper objectMapper) {
|
||||
this.mcpService = mcpService;
|
||||
this.diagnosticService = diagnosticService;
|
||||
this.properties = properties;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
@@ -68,7 +72,15 @@ public class SuperAgentMcpController {
|
||||
"MCP 鉴权失败。"));
|
||||
}
|
||||
SuperAgentMcpJsonRpcRequest request = readRequest(requestBody);
|
||||
SuperAgentMcpJsonRpcResponse response = mcpService.handle(request);
|
||||
Long diagnosticId = diagnosticService.recordReceived(requestBody, request);
|
||||
SuperAgentMcpJsonRpcResponse response;
|
||||
try {
|
||||
response = mcpService.handle(request, diagnosticId);
|
||||
} catch (RuntimeException exception) {
|
||||
diagnosticService.recordUnhandledFailure(diagnosticId, exception);
|
||||
throw exception;
|
||||
}
|
||||
diagnosticService.recordCompleted(diagnosticId, response);
|
||||
if (response == null) {
|
||||
return ResponseEntity.accepted().build();
|
||||
}
|
||||
@@ -113,6 +125,7 @@ public class SuperAgentMcpController {
|
||||
try {
|
||||
return objectMapper.readValue(rawBody, SuperAgentMcpJsonRpcRequest.class);
|
||||
} catch (JsonProcessingException exception) {
|
||||
diagnosticService.recordInvalidJson(rawBody);
|
||||
throw new SuperAgentMcpRequestException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
package cn.nianxx.thhotel.integrations.mcp.superagent.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* SuperAgent MCP 调用诊断实体。用于受控记录入站请求、adapter 后 payload 和安全错误摘要。
|
||||
*/
|
||||
@TableName("platform_superagent_mcp_call_diagnostic")
|
||||
public class SuperAgentMcpCallDiagnosticEntity {
|
||||
|
||||
/** MCP 调用诊断 ID。 */
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
/** JSON-RPC request id 的安全文本表示。 */
|
||||
private String jsonrpcId;
|
||||
/** JSON-RPC method,例如 tools/call。 */
|
||||
private String methodName;
|
||||
/** MCP tool 名称,例如 th_hotel_submit_task_results。 */
|
||||
private String toolName;
|
||||
/** MCP 调用方机器身份。 */
|
||||
private String mcpClientId;
|
||||
/** 原始请求体 UTF-8 字节数。 */
|
||||
private Integer requestBodyBytes;
|
||||
/** 原始请求体 SHA-256。 */
|
||||
private String requestBodySha256;
|
||||
/** 原始 MCP JSON-RPC 请求体,受控诊断字段。 */
|
||||
private String rawBodyJson;
|
||||
/** params.arguments 原始 JSON,受控诊断字段。 */
|
||||
private String argumentsJson;
|
||||
/** submit adapter 转换后送入业务入站层的 JSON。 */
|
||||
private String adaptedPayloadJson;
|
||||
/** submit adapter 事件索引映射诊断 JSON。 */
|
||||
private String mappingDiagnosticsJson;
|
||||
/** MCP 响应安全摘要 JSON,不保存完整查询工具响应或邮件正文。 */
|
||||
private String responseSummaryJson;
|
||||
/** 调用状态:RECEIVED、SUCCEEDED、FAILED。 */
|
||||
private String callStatus;
|
||||
/** 安全错误码。 */
|
||||
private String safeErrorCode;
|
||||
/** 安全错误摘要,不包含正文、HTML、附件 URL 或 Secret。 */
|
||||
private String safeErrorSummary;
|
||||
/** 从 submit 入参中尽力提取的外部来源消息 ID。 */
|
||||
private String sourceMessageExternalId;
|
||||
/** 后端解析出的酒店 ID;解析失败或不适用时为空。 */
|
||||
private String hotelId;
|
||||
/** 记录创建 UTC 时间。 */
|
||||
private LocalDateTime createdAt;
|
||||
/** 记录更新 UTC 时间。 */
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getJsonrpcId() {
|
||||
return jsonrpcId;
|
||||
}
|
||||
|
||||
public void setJsonrpcId(String jsonrpcId) {
|
||||
this.jsonrpcId = jsonrpcId;
|
||||
}
|
||||
|
||||
public String getMethodName() {
|
||||
return methodName;
|
||||
}
|
||||
|
||||
public void setMethodName(String methodName) {
|
||||
this.methodName = methodName;
|
||||
}
|
||||
|
||||
public String getToolName() {
|
||||
return toolName;
|
||||
}
|
||||
|
||||
public void setToolName(String toolName) {
|
||||
this.toolName = toolName;
|
||||
}
|
||||
|
||||
public String getMcpClientId() {
|
||||
return mcpClientId;
|
||||
}
|
||||
|
||||
public void setMcpClientId(String mcpClientId) {
|
||||
this.mcpClientId = mcpClientId;
|
||||
}
|
||||
|
||||
public Integer getRequestBodyBytes() {
|
||||
return requestBodyBytes;
|
||||
}
|
||||
|
||||
public void setRequestBodyBytes(Integer requestBodyBytes) {
|
||||
this.requestBodyBytes = requestBodyBytes;
|
||||
}
|
||||
|
||||
public String getRequestBodySha256() {
|
||||
return requestBodySha256;
|
||||
}
|
||||
|
||||
public void setRequestBodySha256(String requestBodySha256) {
|
||||
this.requestBodySha256 = requestBodySha256;
|
||||
}
|
||||
|
||||
public String getRawBodyJson() {
|
||||
return rawBodyJson;
|
||||
}
|
||||
|
||||
public void setRawBodyJson(String rawBodyJson) {
|
||||
this.rawBodyJson = rawBodyJson;
|
||||
}
|
||||
|
||||
public String getArgumentsJson() {
|
||||
return argumentsJson;
|
||||
}
|
||||
|
||||
public void setArgumentsJson(String argumentsJson) {
|
||||
this.argumentsJson = argumentsJson;
|
||||
}
|
||||
|
||||
public String getAdaptedPayloadJson() {
|
||||
return adaptedPayloadJson;
|
||||
}
|
||||
|
||||
public void setAdaptedPayloadJson(String adaptedPayloadJson) {
|
||||
this.adaptedPayloadJson = adaptedPayloadJson;
|
||||
}
|
||||
|
||||
public String getMappingDiagnosticsJson() {
|
||||
return mappingDiagnosticsJson;
|
||||
}
|
||||
|
||||
public void setMappingDiagnosticsJson(String mappingDiagnosticsJson) {
|
||||
this.mappingDiagnosticsJson = mappingDiagnosticsJson;
|
||||
}
|
||||
|
||||
public String getResponseSummaryJson() {
|
||||
return responseSummaryJson;
|
||||
}
|
||||
|
||||
public void setResponseSummaryJson(String responseSummaryJson) {
|
||||
this.responseSummaryJson = responseSummaryJson;
|
||||
}
|
||||
|
||||
public String getCallStatus() {
|
||||
return callStatus;
|
||||
}
|
||||
|
||||
public void setCallStatus(String callStatus) {
|
||||
this.callStatus = callStatus;
|
||||
}
|
||||
|
||||
public String getSafeErrorCode() {
|
||||
return safeErrorCode;
|
||||
}
|
||||
|
||||
public void setSafeErrorCode(String safeErrorCode) {
|
||||
this.safeErrorCode = safeErrorCode;
|
||||
}
|
||||
|
||||
public String getSafeErrorSummary() {
|
||||
return safeErrorSummary;
|
||||
}
|
||||
|
||||
public void setSafeErrorSummary(String safeErrorSummary) {
|
||||
this.safeErrorSummary = safeErrorSummary;
|
||||
}
|
||||
|
||||
public String getSourceMessageExternalId() {
|
||||
return sourceMessageExternalId;
|
||||
}
|
||||
|
||||
public void setSourceMessageExternalId(String sourceMessageExternalId) {
|
||||
this.sourceMessageExternalId = sourceMessageExternalId;
|
||||
}
|
||||
|
||||
public String getHotelId() {
|
||||
return hotelId;
|
||||
}
|
||||
|
||||
public void setHotelId(String hotelId) {
|
||||
this.hotelId = hotelId;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package cn.nianxx.thhotel.integrations.mcp.superagent.mapper;
|
||||
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.domain.SuperAgentMcpCallDiagnosticEntity;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* SuperAgent MCP 调用诊断 Mapper。继承 MyBatis-Plus 基础方法。
|
||||
*/
|
||||
@Mapper
|
||||
public interface SuperAgentMcpCallDiagnosticMapper extends BaseMapper<SuperAgentMcpCallDiagnosticEntity> {
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package cn.nianxx.thhotel.integrations.mcp.superagent.repository;
|
||||
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.dto.SuperAgentMcpCallDiagnosticAdaptedPayloadUpdate;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.dto.SuperAgentMcpCallDiagnosticCompletionUpdate;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.dto.SuperAgentMcpCallDiagnosticDraft;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.domain.SuperAgentMcpCallDiagnosticEntity;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.mapper.SuperAgentMcpCallDiagnosticMapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* SuperAgent MCP 调用诊断 MyBatis-Plus 持久化实现。
|
||||
*/
|
||||
@Repository
|
||||
public class MybatisSuperAgentMcpCallDiagnosticRepository implements SuperAgentMcpCallDiagnosticRepository {
|
||||
|
||||
private final SuperAgentMcpCallDiagnosticMapper mapper;
|
||||
|
||||
/**
|
||||
* 注入 MCP 调用诊断 Mapper。
|
||||
*/
|
||||
public MybatisSuperAgentMcpCallDiagnosticRepository(SuperAgentMcpCallDiagnosticMapper mapper) {
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一条 MCP 入站诊断记录。
|
||||
*/
|
||||
@Override
|
||||
public Long insert(SuperAgentMcpCallDiagnosticDraft draft) {
|
||||
SuperAgentMcpCallDiagnosticEntity entity = new SuperAgentMcpCallDiagnosticEntity();
|
||||
entity.setJsonrpcId(draft.jsonrpcId());
|
||||
entity.setMethodName(draft.methodName());
|
||||
entity.setToolName(draft.toolName());
|
||||
entity.setMcpClientId(draft.mcpClientId());
|
||||
entity.setRequestBodyBytes(draft.requestBodyBytes());
|
||||
entity.setRequestBodySha256(draft.requestBodySha256());
|
||||
entity.setRawBodyJson(draft.rawBodyJson());
|
||||
entity.setArgumentsJson(draft.argumentsJson());
|
||||
entity.setCallStatus(draft.callStatus());
|
||||
entity.setSafeErrorCode(draft.safeErrorCode());
|
||||
entity.setSafeErrorSummary(draft.safeErrorSummary());
|
||||
entity.setSourceMessageExternalId(draft.sourceMessageExternalId());
|
||||
entity.setHotelId(draft.hotelId());
|
||||
entity.setCreatedAt(draft.createdAt());
|
||||
entity.setUpdatedAt(draft.createdAt());
|
||||
mapper.insert(entity);
|
||||
return entity.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 补写 submit adapter 后 payload 和映射诊断。
|
||||
*/
|
||||
@Override
|
||||
public void updateAdaptedPayload(SuperAgentMcpCallDiagnosticAdaptedPayloadUpdate update) {
|
||||
mapper.update(null, Wrappers.<SuperAgentMcpCallDiagnosticEntity>lambdaUpdate()
|
||||
.eq(SuperAgentMcpCallDiagnosticEntity::getId, update.id())
|
||||
.set(SuperAgentMcpCallDiagnosticEntity::getAdaptedPayloadJson, update.adaptedPayloadJson())
|
||||
.set(SuperAgentMcpCallDiagnosticEntity::getMappingDiagnosticsJson, update.mappingDiagnosticsJson())
|
||||
.set(SuperAgentMcpCallDiagnosticEntity::getSourceMessageExternalId, update.sourceMessageExternalId())
|
||||
.set(SuperAgentMcpCallDiagnosticEntity::getHotelId, update.hotelId())
|
||||
.set(SuperAgentMcpCallDiagnosticEntity::getUpdatedAt, update.updatedAt()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记 MCP 调用完成并写入响应安全摘要。
|
||||
*/
|
||||
@Override
|
||||
public void updateCompletion(SuperAgentMcpCallDiagnosticCompletionUpdate update) {
|
||||
mapper.update(null, Wrappers.<SuperAgentMcpCallDiagnosticEntity>lambdaUpdate()
|
||||
.eq(SuperAgentMcpCallDiagnosticEntity::getId, update.id())
|
||||
.set(SuperAgentMcpCallDiagnosticEntity::getCallStatus, update.callStatus())
|
||||
.set(SuperAgentMcpCallDiagnosticEntity::getResponseSummaryJson, update.responseSummaryJson())
|
||||
.set(SuperAgentMcpCallDiagnosticEntity::getSafeErrorCode, update.safeErrorCode())
|
||||
.set(SuperAgentMcpCallDiagnosticEntity::getSafeErrorSummary, update.safeErrorSummary())
|
||||
.set(SuperAgentMcpCallDiagnosticEntity::getUpdatedAt, update.updatedAt()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package cn.nianxx.thhotel.integrations.mcp.superagent.repository;
|
||||
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.dto.SuperAgentMcpCallDiagnosticAdaptedPayloadUpdate;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.dto.SuperAgentMcpCallDiagnosticCompletionUpdate;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.dto.SuperAgentMcpCallDiagnosticDraft;
|
||||
|
||||
/**
|
||||
* SuperAgent MCP 调用诊断持久化端口。服务层通过该端口写诊断,不直接访问 Mapper。
|
||||
*/
|
||||
public interface SuperAgentMcpCallDiagnosticRepository {
|
||||
|
||||
/**
|
||||
* 创建一条 MCP 入站诊断记录。
|
||||
*/
|
||||
Long insert(SuperAgentMcpCallDiagnosticDraft draft);
|
||||
|
||||
/**
|
||||
* 补写 submit adapter 后 payload 和映射诊断。
|
||||
*/
|
||||
void updateAdaptedPayload(SuperAgentMcpCallDiagnosticAdaptedPayloadUpdate update);
|
||||
|
||||
/**
|
||||
* 标记 MCP 调用完成并写入响应安全摘要。
|
||||
*/
|
||||
void updateCompletion(SuperAgentMcpCallDiagnosticCompletionUpdate update);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package cn.nianxx.thhotel.integrations.mcp.superagent.service;
|
||||
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.dto.SuperAgentMcpSubmitPayloadAdaptation;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.request.SuperAgentMcpJsonRpcRequest;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.result.SuperAgentMcpJsonRpcResponse;
|
||||
|
||||
/**
|
||||
* SuperAgent MCP 调用诊断服务。只记录排障证据,不参与业务状态判断。
|
||||
*/
|
||||
public interface SuperAgentMcpCallDiagnosticService {
|
||||
|
||||
/**
|
||||
* 记录鉴权通过后的 MCP 入站请求,返回诊断 ID;写入失败时返回 null。
|
||||
*/
|
||||
Long recordReceived(String rawBody, SuperAgentMcpJsonRpcRequest request);
|
||||
|
||||
/**
|
||||
* 记录鉴权通过但 JSON 解析失败的 MCP 请求,便于定位调用方发送的原始内容。
|
||||
*/
|
||||
Long recordInvalidJson(String rawBody);
|
||||
|
||||
/**
|
||||
* submit adapter 成功后补写转换后的业务入站 payload 和事件索引映射诊断。
|
||||
*/
|
||||
void recordAdaptedPayload(Long diagnosticId, SuperAgentMcpSubmitPayloadAdaptation adaptation, String hotelId);
|
||||
|
||||
/**
|
||||
* MCP 调用完成后写入响应安全摘要和最终诊断状态。
|
||||
*/
|
||||
void recordCompleted(Long diagnosticId, SuperAgentMcpJsonRpcResponse response);
|
||||
|
||||
/**
|
||||
* MCP 主链路抛出未处理异常时写入安全失败诊断,然后继续让原异常向上抛出。
|
||||
*/
|
||||
void recordUnhandledFailure(Long diagnosticId, RuntimeException exception);
|
||||
}
|
||||
@@ -11,5 +11,12 @@ public interface SuperAgentMcpService {
|
||||
/**
|
||||
* 处理单个 JSON-RPC 请求。MCP notification 不需要响应时返回 null。
|
||||
*/
|
||||
SuperAgentMcpJsonRpcResponse handle(SuperAgentMcpJsonRpcRequest request);
|
||||
default SuperAgentMcpJsonRpcResponse handle(SuperAgentMcpJsonRpcRequest request) {
|
||||
return handle(request, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单个 JSON-RPC 请求,并关联可选的 MCP 调用诊断 ID。
|
||||
*/
|
||||
SuperAgentMcpJsonRpcResponse handle(SuperAgentMcpJsonRpcRequest request, Long diagnosticId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
package cn.nianxx.thhotel.integrations.mcp.superagent.service.impl;
|
||||
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.dto.SuperAgentMcpCallDiagnosticAdaptedPayloadUpdate;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.dto.SuperAgentMcpCallDiagnosticCompletionUpdate;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.dto.SuperAgentMcpCallDiagnosticDraft;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.dto.SuperAgentMcpSubmitPayloadAdaptation;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.enums.SuperAgentMcpCallDiagnosticStatus;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.request.SuperAgentMcpJsonRpcRequest;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.result.SuperAgentMcpContentItem;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.result.SuperAgentMcpJsonRpcError;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.result.SuperAgentMcpJsonRpcResponse;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.result.SuperAgentMcpToolCallResult;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.repository.SuperAgentMcpCallDiagnosticRepository;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.service.SuperAgentMcpCallDiagnosticService;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* SuperAgent MCP 调用诊断服务实现。诊断写入失败只记录安全日志,不改变 MCP 主链路响应。
|
||||
*/
|
||||
@Service
|
||||
public class SuperAgentMcpCallDiagnosticServiceImpl implements SuperAgentMcpCallDiagnosticService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SuperAgentMcpCallDiagnosticServiceImpl.class);
|
||||
private static final String MCP_CLIENT_ID = "superagent-mcp";
|
||||
private static final int JSONRPC_ID_MAX_LENGTH = 128;
|
||||
private static final int METHOD_MAX_LENGTH = 128;
|
||||
private static final int TOOL_MAX_LENGTH = 128;
|
||||
private static final int SOURCE_MESSAGE_MAX_LENGTH = 256;
|
||||
private static final int HOTEL_ID_MAX_LENGTH = 64;
|
||||
private static final int SAFE_ERROR_CODE_MAX_LENGTH = 128;
|
||||
private static final int SAFE_ERROR_SUMMARY_MAX_LENGTH = 512;
|
||||
|
||||
private final SuperAgentMcpCallDiagnosticRepository repository;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 注入诊断 Repository 和 JSON 工具。
|
||||
*/
|
||||
public SuperAgentMcpCallDiagnosticServiceImpl(
|
||||
SuperAgentMcpCallDiagnosticRepository repository,
|
||||
ObjectMapper objectMapper) {
|
||||
this.repository = repository;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录鉴权通过后的 MCP 入站请求,包含原始 envelope、tool 名称和原始 arguments。
|
||||
*/
|
||||
@Override
|
||||
public Long recordReceived(String rawBody, SuperAgentMcpJsonRpcRequest request) {
|
||||
try {
|
||||
JsonNode arguments = argumentsOf(request);
|
||||
LocalDateTime now = nowUtc();
|
||||
return repository.insert(new SuperAgentMcpCallDiagnosticDraft(
|
||||
safeText(request == null ? null : request.id(), JSONRPC_ID_MAX_LENGTH),
|
||||
trimToNull(request == null ? null : request.method(), METHOD_MAX_LENGTH),
|
||||
trimToNull(toolNameOf(request), TOOL_MAX_LENGTH),
|
||||
MCP_CLIENT_ID,
|
||||
requestBodyBytes(rawBody),
|
||||
sha256(rawBody),
|
||||
rawBody,
|
||||
arguments == null ? null : objectMapper.writeValueAsString(arguments),
|
||||
SuperAgentMcpCallDiagnosticStatus.RECEIVED.name(),
|
||||
null,
|
||||
null,
|
||||
trimToNull(extractSourceMessageExternalId(arguments), SOURCE_MESSAGE_MAX_LENGTH),
|
||||
trimToNull(extractHotelId(arguments), HOTEL_ID_MAX_LENGTH),
|
||||
now));
|
||||
} catch (RuntimeException | JsonProcessingException exception) {
|
||||
log.warn("SuperAgent MCP diagnostic insert failed. request_body_sha256={}, exception={}",
|
||||
safeHash(rawBody),
|
||||
exception.getClass().getSimpleName());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录鉴权通过后的非法 JSON 请求。
|
||||
*/
|
||||
@Override
|
||||
public Long recordInvalidJson(String rawBody) {
|
||||
try {
|
||||
LocalDateTime now = nowUtc();
|
||||
return repository.insert(new SuperAgentMcpCallDiagnosticDraft(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
MCP_CLIENT_ID,
|
||||
requestBodyBytes(rawBody),
|
||||
sha256(rawBody),
|
||||
rawBody,
|
||||
null,
|
||||
SuperAgentMcpCallDiagnosticStatus.FAILED.name(),
|
||||
"MCP_REQUEST_INVALID",
|
||||
"MCP 请求 JSON 不合法。",
|
||||
null,
|
||||
null,
|
||||
now));
|
||||
} catch (RuntimeException exception) {
|
||||
log.warn("SuperAgent MCP invalid-json diagnostic insert failed. request_body_sha256={}, exception={}",
|
||||
safeHash(rawBody),
|
||||
exception.getClass().getSimpleName());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* submit adapter 成功后补写业务入站 payload 和事件索引映射诊断。
|
||||
*/
|
||||
@Override
|
||||
public void recordAdaptedPayload(
|
||||
Long diagnosticId,
|
||||
SuperAgentMcpSubmitPayloadAdaptation adaptation,
|
||||
String hotelId) {
|
||||
if (diagnosticId == null || adaptation == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
JsonNode payload = adaptation.payload();
|
||||
repository.updateAdaptedPayload(new SuperAgentMcpCallDiagnosticAdaptedPayloadUpdate(
|
||||
diagnosticId,
|
||||
payload == null ? null : objectMapper.writeValueAsString(payload),
|
||||
adaptation.mappingDiagnostics() == null ? null
|
||||
: objectMapper.writeValueAsString(adaptation.mappingDiagnostics()),
|
||||
trimToNull(extractSourceMessageExternalId(payload), SOURCE_MESSAGE_MAX_LENGTH),
|
||||
trimToNull(hotelId, HOTEL_ID_MAX_LENGTH),
|
||||
nowUtc()));
|
||||
} catch (RuntimeException | JsonProcessingException exception) {
|
||||
log.warn("SuperAgent MCP diagnostic adapted payload update failed. diagnostic_id={}, exception={}",
|
||||
diagnosticId,
|
||||
exception.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP 调用完成后补写最终状态、响应安全摘要和错误摘要。
|
||||
*/
|
||||
@Override
|
||||
public void recordCompleted(Long diagnosticId, SuperAgentMcpJsonRpcResponse response) {
|
||||
if (diagnosticId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ResponseDiagnosticSummary summary = summarizeResponse(response);
|
||||
repository.updateCompletion(new SuperAgentMcpCallDiagnosticCompletionUpdate(
|
||||
diagnosticId,
|
||||
summary.failed()
|
||||
? SuperAgentMcpCallDiagnosticStatus.FAILED.name()
|
||||
: SuperAgentMcpCallDiagnosticStatus.SUCCEEDED.name(),
|
||||
objectMapper.writeValueAsString(summary.summaryJson()),
|
||||
trimToNull(summary.safeErrorCode(), SAFE_ERROR_CODE_MAX_LENGTH),
|
||||
trimToNull(summary.safeErrorSummary(), SAFE_ERROR_SUMMARY_MAX_LENGTH),
|
||||
nowUtc()));
|
||||
if (summary.failed()) {
|
||||
log.warn("SuperAgent MCP call failed. diagnostic_id={}, safe_error_code={}, safe_error_summary={}",
|
||||
diagnosticId,
|
||||
summary.safeErrorCode(),
|
||||
summary.safeErrorSummary());
|
||||
}
|
||||
} catch (RuntimeException | JsonProcessingException exception) {
|
||||
log.warn("SuperAgent MCP diagnostic completion update failed. diagnostic_id={}, exception={}",
|
||||
diagnosticId,
|
||||
exception.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 主链路出现未处理异常时补写安全失败诊断,避免排障记录停留在 RECEIVED。
|
||||
*/
|
||||
@Override
|
||||
public void recordUnhandledFailure(Long diagnosticId, RuntimeException exception) {
|
||||
if (diagnosticId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String safeSummary = exception == null ? "MCP 主链路未处理异常。" : exception.getClass().getSimpleName();
|
||||
ObjectNode summary = objectMapper.createObjectNode();
|
||||
summary.put("unhandled_exception", true);
|
||||
summary.put("error_code", "MCP_UNHANDLED_EXCEPTION");
|
||||
summary.put("error_message", safeSummary);
|
||||
repository.updateCompletion(new SuperAgentMcpCallDiagnosticCompletionUpdate(
|
||||
diagnosticId,
|
||||
SuperAgentMcpCallDiagnosticStatus.FAILED.name(),
|
||||
objectMapper.writeValueAsString(summary),
|
||||
"MCP_UNHANDLED_EXCEPTION",
|
||||
safeSummary,
|
||||
nowUtc()));
|
||||
log.warn("SuperAgent MCP call failed unexpectedly. diagnostic_id={}, safe_error_code={}, exception={}",
|
||||
diagnosticId,
|
||||
"MCP_UNHANDLED_EXCEPTION",
|
||||
safeSummary);
|
||||
} catch (RuntimeException | JsonProcessingException diagnosticException) {
|
||||
log.warn("SuperAgent MCP unhandled-failure diagnostic update failed. diagnostic_id={}, exception={}",
|
||||
diagnosticId,
|
||||
diagnosticException.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 JSON-RPC 请求中提取 params.arguments。
|
||||
*/
|
||||
private JsonNode argumentsOf(SuperAgentMcpJsonRpcRequest request) {
|
||||
if (request == null || request.params() == null || !request.params().isObject()) {
|
||||
return null;
|
||||
}
|
||||
JsonNode arguments = request.params().get("arguments");
|
||||
if (arguments == null || arguments.isNull()) {
|
||||
return null;
|
||||
}
|
||||
return arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 JSON-RPC 请求中提取 tool 名称。
|
||||
*/
|
||||
private String toolNameOf(SuperAgentMcpJsonRpcRequest request) {
|
||||
if (request == null || request.params() == null || !request.params().isObject()) {
|
||||
return null;
|
||||
}
|
||||
JsonNode nameNode = request.params().get("name");
|
||||
if (nameNode == null || nameNode.isNull()) {
|
||||
return null;
|
||||
}
|
||||
return nameNode.isTextual() ? nameNode.asText() : nameNode.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 submit 入参或 adapter 后 payload 中尽力提取外部 SourceMessage ID。
|
||||
*/
|
||||
private String extractSourceMessageExternalId(JsonNode node) {
|
||||
if (node == null || node.isNull()) {
|
||||
return null;
|
||||
}
|
||||
JsonNode nested = node.path("source_message").path("source_message_id");
|
||||
if (nested.isTextual() && StringUtils.hasText(nested.asText())) {
|
||||
return nested.asText();
|
||||
}
|
||||
JsonNode legacy = node.path("source_message_id");
|
||||
if (legacy.isTextual() && StringUtils.hasText(legacy.asText())) {
|
||||
return legacy.asText();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 MCP 入参中提取兼容 hotel_id;正式业务仍以后端系统酒店解析为准。
|
||||
*/
|
||||
private String extractHotelId(JsonNode node) {
|
||||
if (node == null || node.isNull()) {
|
||||
return null;
|
||||
}
|
||||
JsonNode hotelId = node.path("hotel_id");
|
||||
return hotelId.isTextual() && StringUtils.hasText(hotelId.asText()) ? hotelId.asText() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造响应安全摘要,避免保存查询工具完整响应和邮件正文。
|
||||
*/
|
||||
private ResponseDiagnosticSummary summarizeResponse(SuperAgentMcpJsonRpcResponse response) {
|
||||
ObjectNode summary = objectMapper.createObjectNode();
|
||||
if (response == null) {
|
||||
summary.put("notification", true);
|
||||
return new ResponseDiagnosticSummary(summary, false, null, null);
|
||||
}
|
||||
summary.put("jsonrpc", response.jsonrpc());
|
||||
if (response.id() != null && !response.id().isNull()) {
|
||||
summary.set("id", response.id());
|
||||
}
|
||||
if (response.error() != null) {
|
||||
SuperAgentMcpJsonRpcError error = response.error();
|
||||
String errorCode = error.data() == null ? null : stringValue(error.data().get("code"));
|
||||
summary.put("protocol_error", true);
|
||||
summary.put("rpc_code", error.code());
|
||||
putIfText(summary, "error_code", errorCode);
|
||||
putIfText(summary, "message", error.message());
|
||||
return new ResponseDiagnosticSummary(summary, true, errorCode, error.message());
|
||||
}
|
||||
if (response.result() instanceof SuperAgentMcpToolCallResult toolResult) {
|
||||
String summaryText = firstText(toolResult.content());
|
||||
JsonNode structured = objectMapper.valueToTree(toolResult.structuredContent());
|
||||
String errorCode = structured.path("error").path("code").isMissingNode()
|
||||
? null
|
||||
: structured.path("error").path("code").asText(null);
|
||||
String errorMessage = structured.path("error").path("message").isMissingNode()
|
||||
? null
|
||||
: structured.path("error").path("message").asText(null);
|
||||
summary.put("tool_is_error", toolResult.isError());
|
||||
putIfText(summary, "summary_text", summaryText);
|
||||
putIfText(summary, "error_code", errorCode);
|
||||
putIfText(summary, "error_message", errorMessage);
|
||||
copyIfPresent(structured, summary, "source_message_id");
|
||||
copyIfPresent(structured, summary, "accepted_count");
|
||||
copyIfPresent(structured, summary, "idempotent_replay");
|
||||
return new ResponseDiagnosticSummary(
|
||||
summary,
|
||||
toolResult.isError(),
|
||||
errorCode,
|
||||
StringUtils.hasText(errorMessage) ? errorMessage : summaryText);
|
||||
}
|
||||
summary.put("tool_is_error", false);
|
||||
return new ResponseDiagnosticSummary(summary, false, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制响应摘要中的安全标量字段,不复制完整业务数据。
|
||||
*/
|
||||
private void copyIfPresent(JsonNode source, ObjectNode target, String fieldName) {
|
||||
JsonNode value = source.path(fieldName);
|
||||
if (!value.isMissingNode() && value.isValueNode()) {
|
||||
target.set(fieldName, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 MCP tool result 的第一段文本摘要。
|
||||
*/
|
||||
private String firstText(List<SuperAgentMcpContentItem> content) {
|
||||
if (content == null || content.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return content.stream()
|
||||
.filter(item -> item != null && StringUtils.hasText(item.text()))
|
||||
.findFirst()
|
||||
.map(SuperAgentMcpContentItem::text)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入非空安全文本字段。
|
||||
*/
|
||||
private void putIfText(ObjectNode node, String fieldName, String value) {
|
||||
if (StringUtils.hasText(value)) {
|
||||
node.put(fieldName, trimToNull(value, SAFE_ERROR_SUMMARY_MAX_LENGTH));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 JSON-RPC id 或错误数据转换为可存储的安全文本。
|
||||
*/
|
||||
private String safeText(JsonNode node, int maxLength) {
|
||||
if (node == null || node.isNull()) {
|
||||
return null;
|
||||
}
|
||||
return trimToNull(node.isTextual() ? node.asText() : node.toString(), maxLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将普通对象转换为文本。
|
||||
*/
|
||||
private String stringValue(Object value) {
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 截断并规范化空字符串。
|
||||
*/
|
||||
private String trimToNull(String value, int maxLength) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
return trimmed.length() <= maxLength ? trimmed : trimmed.substring(0, maxLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算原始请求体字节数。
|
||||
*/
|
||||
private int requestBodyBytes(String rawBody) {
|
||||
return (rawBody == null ? "" : rawBody).getBytes(StandardCharsets.UTF_8).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算原始请求体 SHA-256。
|
||||
*/
|
||||
private String sha256(String rawBody) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest((rawBody == null ? "" : rawBody).getBytes(StandardCharsets.UTF_8));
|
||||
return HexFormat.of().formatHex(hash);
|
||||
} catch (NoSuchAlgorithmException exception) {
|
||||
throw new IllegalStateException("SHA-256 algorithm is unavailable.", exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 日志兜底使用安全 hash,避免输出原始请求体。
|
||||
*/
|
||||
private String safeHash(String rawBody) {
|
||||
try {
|
||||
return sha256(rawBody);
|
||||
} catch (RuntimeException exception) {
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前 UTC 时间。
|
||||
*/
|
||||
private LocalDateTime nowUtc() {
|
||||
return LocalDateTime.now(ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP 响应诊断摘要。
|
||||
*/
|
||||
private record ResponseDiagnosticSummary(
|
||||
ObjectNode summaryJson,
|
||||
boolean failed,
|
||||
String safeErrorCode,
|
||||
String safeErrorSummary
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import cn.nianxx.thhotel.integrations.mcp.superagent.common.result.SuperAgentMcp
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.result.SuperAgentMcpToolCallResult;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.result.SuperAgentMcpToolDefinition;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.result.SuperAgentMcpToolsListResult;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.service.SuperAgentMcpCallDiagnosticService;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.service.SuperAgentMcpService;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.service.SuperAgentMcpSubmitPayloadAdapter;
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextService;
|
||||
@@ -57,6 +58,7 @@ public class SuperAgentMcpServiceImpl implements SuperAgentMcpService {
|
||||
private final ObjectMapper objectMapper;
|
||||
private final HotelContextService hotelContextService;
|
||||
private final SuperAgentMcpSubmitPayloadAdapter submitPayloadAdapter;
|
||||
private final SuperAgentMcpCallDiagnosticService diagnosticService;
|
||||
|
||||
/**
|
||||
* 注入已有业务服务和 JSON 工具,MCP 层不直接访问 Mapper 或数据库。
|
||||
@@ -67,20 +69,22 @@ public class SuperAgentMcpServiceImpl implements SuperAgentMcpService {
|
||||
SuperAgentMcpProperties properties,
|
||||
ObjectMapper objectMapper,
|
||||
HotelContextService hotelContextService,
|
||||
SuperAgentMcpSubmitPayloadAdapter submitPayloadAdapter) {
|
||||
SuperAgentMcpSubmitPayloadAdapter submitPayloadAdapter,
|
||||
SuperAgentMcpCallDiagnosticService diagnosticService) {
|
||||
this.aiQueryService = aiQueryService;
|
||||
this.intakeService = intakeService;
|
||||
this.properties = properties;
|
||||
this.objectMapper = objectMapper;
|
||||
this.hotelContextService = hotelContextService;
|
||||
this.submitPayloadAdapter = submitPayloadAdapter;
|
||||
this.diagnosticService = diagnosticService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分发 MCP JSON-RPC 方法。
|
||||
*/
|
||||
@Override
|
||||
public SuperAgentMcpJsonRpcResponse handle(SuperAgentMcpJsonRpcRequest request) {
|
||||
public SuperAgentMcpJsonRpcResponse handle(SuperAgentMcpJsonRpcRequest request, Long diagnosticId) {
|
||||
if (request == null || !StringUtils.hasText(request.method())) {
|
||||
return SuperAgentMcpJsonRpcResponse.error(null, -32600, "MCP_REQUEST_INVALID", "MCP 请求缺少 method。");
|
||||
}
|
||||
@@ -90,7 +94,7 @@ public class SuperAgentMcpServiceImpl implements SuperAgentMcpService {
|
||||
case METHOD_TOOLS_LIST -> SuperAgentMcpJsonRpcResponse.success(
|
||||
request.id(),
|
||||
new SuperAgentMcpToolsListResult(toolDefinitions()));
|
||||
case METHOD_TOOLS_CALL -> callTool(request);
|
||||
case METHOD_TOOLS_CALL -> callTool(request, diagnosticId);
|
||||
default -> SuperAgentMcpJsonRpcResponse.error(
|
||||
request.id(),
|
||||
-32601,
|
||||
@@ -114,7 +118,7 @@ public class SuperAgentMcpServiceImpl implements SuperAgentMcpService {
|
||||
/**
|
||||
* 执行 tools/call。业务异常转换为 tool result,协议参数错误转换为 JSON-RPC error。
|
||||
*/
|
||||
private SuperAgentMcpJsonRpcResponse callTool(SuperAgentMcpJsonRpcRequest request) {
|
||||
private SuperAgentMcpJsonRpcResponse callTool(SuperAgentMcpJsonRpcRequest request, Long diagnosticId) {
|
||||
SuperAgentMcpToolCallParams params;
|
||||
try {
|
||||
params = objectMapper.treeToValue(request.params(), SuperAgentMcpToolCallParams.class);
|
||||
@@ -133,21 +137,24 @@ public class SuperAgentMcpServiceImpl implements SuperAgentMcpService {
|
||||
"MCP 工具名称不能为空。");
|
||||
}
|
||||
|
||||
SuperAgentMcpToolCallResult result = dispatchTool(params.name(), safeArguments(params.arguments()));
|
||||
SuperAgentMcpToolCallResult result = dispatchTool(
|
||||
params.name(),
|
||||
safeArguments(params.arguments()),
|
||||
diagnosticId);
|
||||
return SuperAgentMcpJsonRpcResponse.success(request.id(), result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按工具名分发到已有业务服务。
|
||||
*/
|
||||
private SuperAgentMcpToolCallResult dispatchTool(String toolName, JsonNode arguments) {
|
||||
private SuperAgentMcpToolCallResult dispatchTool(String toolName, JsonNode arguments, Long diagnosticId) {
|
||||
try {
|
||||
return switch (toolName) {
|
||||
case TOOL_QUERY_CASE_CONTEXT -> callQueryCaseContext(arguments);
|
||||
case TOOL_QUERY_OBJECT_DETAIL -> callQueryObjectDetail(arguments);
|
||||
case TOOL_LIST_CONVERSATION_TASKS -> callListConversationTasks(arguments);
|
||||
case TOOL_LIST_CONVERSATION_MESSAGES -> callListConversationMessages(arguments);
|
||||
case TOOL_SUBMIT_TASK_RESULTS -> callSubmitTaskResults(arguments);
|
||||
case TOOL_SUBMIT_TASK_RESULTS -> callSubmitTaskResults(arguments, diagnosticId);
|
||||
default -> SuperAgentMcpToolCallResult.error(
|
||||
"MCP 工具不存在:" + toolName,
|
||||
errorStructuredContent("MCP_TOOL_NOT_FOUND", "MCP 工具不存在。", Map.of("tool", toolName)));
|
||||
@@ -255,7 +262,8 @@ public class SuperAgentMcpServiceImpl implements SuperAgentMcpService {
|
||||
/**
|
||||
* 调用任务结果写入工具。生产是否启用由 MCP 独立开关控制。
|
||||
*/
|
||||
private SuperAgentMcpToolCallResult callSubmitTaskResults(JsonNode arguments) throws JsonProcessingException {
|
||||
private SuperAgentMcpToolCallResult callSubmitTaskResults(JsonNode arguments, Long diagnosticId)
|
||||
throws JsonProcessingException {
|
||||
if (!properties.isEnableSubmitTaskResults()) {
|
||||
return SuperAgentMcpToolCallResult.error(
|
||||
TOOL_SUBMIT_TASK_RESULTS + " 当前未启用。",
|
||||
@@ -265,12 +273,14 @@ public class SuperAgentMcpServiceImpl implements SuperAgentMcpService {
|
||||
Map.of("tool", TOOL_SUBMIT_TASK_RESULTS)));
|
||||
}
|
||||
SuperAgentMcpSubmitPayloadAdaptation adaptation = submitPayloadAdapter.adapt(arguments);
|
||||
String hotelId = hotelContextService.resolveSystemHotelId();
|
||||
diagnosticService.recordAdaptedPayload(diagnosticId, adaptation, hotelId);
|
||||
String rawBody = objectMapper.writeValueAsString(adaptation.payload());
|
||||
SuperAgentTaskResultResponse response = intakeService.accept(
|
||||
rawBody,
|
||||
MCP_CLIENT_ID,
|
||||
null,
|
||||
hotelContextService.resolveSystemHotelId());
|
||||
hotelId);
|
||||
return SuperAgentMcpToolCallResult.success(
|
||||
TOOL_SUBMIT_TASK_RESULTS + " 调用成功。",
|
||||
submitStructuredContent(response, adaptation.mappingDiagnostics()));
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
-- SuperAgent MCP 入站诊断:用于联调排查原始 tools/call 参数、adapter 后 payload 和安全错误摘要。
|
||||
CREATE TABLE platform_superagent_mcp_call_diagnostic (
|
||||
id BIGINT NOT NULL COMMENT 'MCP 调用诊断 ID',
|
||||
jsonrpc_id VARCHAR(128) NULL COMMENT 'JSON-RPC request id 的安全文本表示',
|
||||
method_name VARCHAR(128) NULL COMMENT 'JSON-RPC method,例如 tools/call',
|
||||
tool_name VARCHAR(128) NULL COMMENT 'MCP tool 名称,例如 th_hotel_submit_task_results',
|
||||
mcp_client_id VARCHAR(64) NOT NULL COMMENT 'MCP 调用方机器身份,第一版固定 superagent-mcp',
|
||||
request_body_bytes INT NOT NULL COMMENT '原始请求体 UTF-8 字节数',
|
||||
request_body_sha256 CHAR(64) NOT NULL COMMENT '原始请求体 SHA-256,用于不暴露正文时定位同一次请求',
|
||||
raw_body_json LONGTEXT NULL COMMENT '原始 MCP JSON-RPC 请求体,受控诊断字段,不进入普通接口或日志',
|
||||
arguments_json LONGTEXT NULL COMMENT 'params.arguments 原始 JSON,受控诊断字段',
|
||||
adapted_payload_json LONGTEXT NULL COMMENT 'submit adapter 转换后送入业务入站层的 JSON;非 submit 或转换失败为空',
|
||||
mapping_diagnostics_json LONGTEXT NULL COMMENT 'submit adapter 事件索引映射诊断 JSON',
|
||||
response_summary_json LONGTEXT NULL COMMENT 'MCP 响应安全摘要,不保存完整查询工具响应或邮件正文',
|
||||
call_status VARCHAR(32) NOT NULL COMMENT '调用状态:RECEIVED、SUCCEEDED、FAILED',
|
||||
safe_error_code VARCHAR(128) NULL COMMENT '安全错误码,例如 SOURCE_MESSAGE_NOT_FOUND',
|
||||
safe_error_summary VARCHAR(512) NULL COMMENT '安全错误摘要,不包含正文、HTML、附件 URL 或 Secret',
|
||||
source_message_external_id VARCHAR(256) NULL COMMENT '从 submit 入参中尽力提取的外部来源消息 ID',
|
||||
hotel_id VARCHAR(64) NULL COMMENT '后端解析出的酒店 ID;解析失败或不适用时为空',
|
||||
created_at DATETIME(6) NOT NULL COMMENT '记录创建 UTC 时间',
|
||||
updated_at DATETIME(6) NOT NULL COMMENT '记录更新 UTC 时间',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_mcp_diagnostic_created (created_at, id),
|
||||
KEY idx_mcp_diagnostic_status_created (call_status, created_at),
|
||||
KEY idx_mcp_diagnostic_tool_created (tool_name, created_at),
|
||||
KEY idx_mcp_diagnostic_source_message (source_message_external_id, created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
|
||||
COMMENT='SuperAgent MCP 入站诊断表,受控记录原始工具参数、adapter 后 payload 和安全错误摘要';
|
||||
@@ -18,6 +18,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
@@ -90,6 +91,33 @@ class SuperAgentMcpSubmitEnabledControllerTest {
|
||||
.andExpect(jsonPath("$.result.isError").value(true))
|
||||
.andExpect(jsonPath("$.result.structuredContent.error.code").value("SOURCE_MESSAGE_NOT_FOUND"))
|
||||
.andExpect(content().string(not(containsString("MCP_TOOL_DISABLED"))));
|
||||
|
||||
Map<String, Object> diagnostic = jdbcTemplate.queryForMap("""
|
||||
SELECT
|
||||
call_status,
|
||||
tool_name,
|
||||
source_message_external_id,
|
||||
arguments_json,
|
||||
adapted_payload_json,
|
||||
safe_error_code,
|
||||
safe_error_summary,
|
||||
response_summary_json
|
||||
FROM platform_superagent_mcp_call_diagnostic
|
||||
WHERE jsonrpc_id = ?
|
||||
""", "mcp-submit-enabled-001");
|
||||
assertThat(diagnostic.get("call_status")).isEqualTo("FAILED");
|
||||
assertThat(diagnostic.get("tool_name")).isEqualTo("th_hotel_submit_task_results");
|
||||
assertThat(diagnostic.get("source_message_external_id")).isEqualTo("mail-mcp-enabled-missing-001");
|
||||
assertThat(diagnostic.get("safe_error_code")).isEqualTo("SOURCE_MESSAGE_NOT_FOUND");
|
||||
assertThat((String) diagnostic.get("safe_error_summary")).contains("SourceMessage 不存在");
|
||||
|
||||
JsonNode arguments = objectMapper.readTree((String) diagnostic.get("arguments_json"));
|
||||
assertThat(arguments.path("source_message_id").asText()).isEqualTo("mail-mcp-enabled-missing-001");
|
||||
JsonNode adaptedPayload = objectMapper.readTree((String) diagnostic.get("adapted_payload_json"));
|
||||
assertThat(adaptedPayload.path("source_message_id").asText()).isEqualTo("mail-mcp-enabled-missing-001");
|
||||
JsonNode responseSummary = objectMapper.readTree((String) diagnostic.get("response_summary_json"));
|
||||
assertThat(responseSummary.path("tool_is_error").asBoolean()).isTrue();
|
||||
assertThat(responseSummary.path("error_code").asText()).isEqualTo("SOURCE_MESSAGE_NOT_FOUND");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -261,6 +289,53 @@ class SuperAgentMcpSubmitEnabledControllerTest {
|
||||
assertThat(result.getResponse().getContentAsString()).contains("E_CHILD_1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRecordSubmitArgumentsAndAdaptedPayloadForMcpDiagnostics() throws Exception {
|
||||
String externalId = "mail-mcp-diagnostic-v3-001";
|
||||
captureSourceMessage(externalId);
|
||||
|
||||
mockMvc.perform(post(ENDPOINT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header("Authorization", AUTHORIZATION)
|
||||
.content(toolCall("mcp-diagnostic-v3-001", parentSplitBusinessRoot(externalId))))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.result.isError").value(false));
|
||||
|
||||
Map<String, Object> diagnostic = jdbcTemplate.queryForMap("""
|
||||
SELECT
|
||||
call_status,
|
||||
tool_name,
|
||||
source_message_external_id,
|
||||
arguments_json,
|
||||
adapted_payload_json,
|
||||
mapping_diagnostics_json,
|
||||
response_summary_json
|
||||
FROM platform_superagent_mcp_call_diagnostic
|
||||
WHERE jsonrpc_id = ?
|
||||
""", "mcp-diagnostic-v3-001");
|
||||
assertThat(diagnostic.get("call_status")).isEqualTo("SUCCEEDED");
|
||||
assertThat(diagnostic.get("tool_name")).isEqualTo("th_hotel_submit_task_results");
|
||||
assertThat(diagnostic.get("source_message_external_id")).isEqualTo(externalId);
|
||||
|
||||
JsonNode arguments = objectMapper.readTree((String) diagnostic.get("arguments_json"));
|
||||
assertThat(arguments.path("source_message").path("source_message_id").asText()).isEqualTo(externalId);
|
||||
assertThat(arguments.path("message_events").get(0).path("source_event_index").asText()).isEqualTo("E_CHILD_1");
|
||||
|
||||
JsonNode adaptedPayload = objectMapper.readTree((String) diagnostic.get("adapted_payload_json"));
|
||||
assertThat(adaptedPayload.path("source_message").path("source_message_id").asText()).isEqualTo(externalId);
|
||||
assertThat(adaptedPayload.path("message_events").get(0).path("source_event_index").asInt()).isEqualTo(1);
|
||||
|
||||
JsonNode mappingDiagnostics = objectMapper.readTree((String) diagnostic.get("mapping_diagnostics_json"));
|
||||
assertThat(mappingDiagnostics.path("source_event_index_mapping").get(0).path("original_source_event_index").asText())
|
||||
.isEqualTo("E_CHILD_1");
|
||||
assertThat(mappingDiagnostics.path("source_event_index_mapping").get(0).path("mapped_source_event_index").asInt())
|
||||
.isEqualTo(1);
|
||||
|
||||
JsonNode responseSummary = objectMapper.readTree((String) diagnostic.get("response_summary_json"));
|
||||
assertThat(responseSummary.path("tool_is_error").asBoolean()).isFalse();
|
||||
assertThat(responseSummary.toString()).doesNotContain("MCP submit fixture source message");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMapCrossChildTraceRelationsAndSubmitOnce() throws Exception {
|
||||
String externalId = "mail-mcp-v3-trace-cross-child-001";
|
||||
|
||||
Reference in New Issue
Block a user