新增SuperAgent MCP入站诊断链路

This commit is contained in:
andy
2026-07-22 21:10:49 +07:00
parent 76d253afc4
commit 5712f03d4f
21 changed files with 1163 additions and 14 deletions

View File

@@ -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

View File

@@ -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
) {
}

View File

@@ -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
) {
}

View File

@@ -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
) {
}

View File

@@ -0,0 +1,14 @@
package cn.nianxx.thhotel.integrations.mcp.superagent.common.enums;
/**
* SuperAgent MCP 调用诊断状态。只表示诊断记录生命周期,不参与业务任务状态流转。
*/
public enum SuperAgentMcpCallDiagnosticStatus {
/** 已接收并完成基础解析。 */
RECEIVED,
/** MCP 调用已成功返回。 */
SUCCEEDED,
/** MCP 调用返回协议错误或工具级错误。 */
FAILED
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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> {
}

View File

@@ -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()));
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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
) {
}
}

View File

@@ -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()));

View File

@@ -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 和安全错误摘要';