package com.vincent.rsf.server.ai.service.mcp; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.vincent.rsf.server.ai.constant.AiMcpConstants; import com.vincent.rsf.server.ai.constant.AiSceneCode; import com.vincent.rsf.server.ai.model.AiDiagnosticToolResult; import com.vincent.rsf.server.ai.model.AiMcpToolDescriptor; import com.vincent.rsf.server.ai.model.AiPromptContext; import com.vincent.rsf.server.ai.service.provider.AiDiagnosticDataProvider; import com.vincent.rsf.server.system.entity.AiDiagnosticToolConfig; import com.vincent.rsf.server.system.entity.AiMcpMount; import com.vincent.rsf.server.system.service.AiDiagnosticToolConfigService; import com.vincent.rsf.server.system.service.AiMcpMountService; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.ArrayList; import java.util.Comparator; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Service public class AiMcpRegistryService { private static final long EXTERNAL_TOOL_CACHE_TTL_MS = 30000L; private final List providers; private final Map externalToolCache = new ConcurrentHashMap<>(); @Resource private AiMcpMountService aiMcpMountService; @Resource private AiDiagnosticToolConfigService aiDiagnosticToolConfigService; @Resource private AiMcpHttpClient aiMcpHttpClient; @Resource private AiMcpSseClient aiMcpSseClient; @Resource private AiMcpPayloadMapper aiMcpPayloadMapper; public AiMcpRegistryService(List providers) { this.providers = providers == null ? new ArrayList<>() : providers; } /** * 枚举租户下可见的 MCP 工具目录。 * 包括内部工具和所有启用中的外部挂载工具。 */ public List listTools(Long tenantId, Long mountId) { List output = new ArrayList<>(); List mounts; if (mountId == null) { mounts = aiMcpMountService.list(new LambdaQueryWrapper() .eq(AiMcpMount::getTenantId, tenantId) .eq(AiMcpMount::getEnabledFlag, 1) .eq(AiMcpMount::getStatus, 1) .orderByAsc(AiMcpMount::getMountCode, AiMcpMount::getId)); } else { mounts = new ArrayList<>(); AiMcpMount mount = aiMcpMountService.getTenantMount(tenantId, mountId); if (mount != null && Integer.valueOf(1).equals(mount.getEnabledFlag()) && Integer.valueOf(1).equals(mount.getStatus())) { mounts.add(mount); } } for (AiMcpMount mount : mounts) { try { if (AiMcpConstants.TRANSPORT_INTERNAL.equalsIgnoreCase(mount.getTransportType())) { output.addAll(buildInternalTools(tenantId, mount)); } else if (AiMcpConstants.TRANSPORT_HTTP.equalsIgnoreCase(mount.getTransportType())) { output.addAll(loadCachedExternalTools(tenantId, mount)); } else if (AiMcpConstants.TRANSPORT_SSE.equalsIgnoreCase(mount.getTransportType())) { output.addAll(loadCachedExternalTools(tenantId, mount)); } } catch (Exception ignore) { } } return output; } /** * 仅返回系统内置工具目录。 */ public List listInternalTools(Long tenantId) { AiMcpMount mount = aiMcpMountService.getTenantMountByCode(tenantId, AiMcpConstants.DEFAULT_LOCAL_MOUNT_CODE); if (mount == null || !Integer.valueOf(1).equals(mount.getEnabledFlag()) || !Integer.valueOf(1).equals(mount.getStatus())) { return new ArrayList<>(); } return buildInternalTools(tenantId, mount); } /** * 测试指定挂载的连通性与工具发现能力,并把结果回写到挂载测试状态字段。 */ public Map testMount(Long tenantId, Long mountId) { AiMcpMount mount = aiMcpMountService.getTenantMount(tenantId, mountId); if (mount == null) { throw new IllegalArgumentException("MCP挂载不存在"); } Map payload = new LinkedHashMap<>(); payload.put("mountCode", mount.getMountCode()); payload.put("transportType", mount.getTransportType()); if (AiMcpConstants.TRANSPORT_INTERNAL.equalsIgnoreCase(mount.getTransportType())) { List tools = buildInternalTools(tenantId, mount); payload.put("success", true); payload.put("toolCount", tools.size()); payload.put("tools", tools); updateTestState(mount, 1, "内部工具挂载正常", tools.size()); return payload; } ExternalToolsResult testResult = loadExternalToolsWithTransport(mount); List tools = testResult.tools; payload.put("success", true); payload.put("toolCount", tools.size()); payload.put("tools", tools); payload.put("resolvedTransportType", testResult.transportType); payload.put("recommendedTransportType", testResult.transportType); payload.put("message", buildExternalSuccessMessage(testResult.transportType, tools.size())); updateTestState(mount, 1, String.valueOf(payload.get("message")), tools.size()); return payload; } /** * 以“预览”模式执行一次工具调用,便于后台页面调试工具返回内容。 */ public AiDiagnosticToolResult previewTool(Long tenantId, String mountCode, String toolCode, String sceneCode, String question) { AiMcpMount mount = aiMcpMountService.getTenantMountByCode(tenantId, mountCode); if (mount == null) { throw new IllegalArgumentException("MCP挂载不存在"); } AiPromptContext context = new AiPromptContext() .setTenantId(tenantId) .setSceneCode(sceneCode == null || sceneCode.trim().isEmpty() ? AiSceneCode.SYSTEM_DIAGNOSE : sceneCode) .setQuestion(question == null || question.trim().isEmpty() ? "请执行一次MCP工具预览" : question); if (AiMcpConstants.TRANSPORT_HTTP.equalsIgnoreCase(mount.getTransportType()) || AiMcpConstants.TRANSPORT_SSE.equalsIgnoreCase(mount.getTransportType())) { return executeExternalTool(mount, toolCode, context, buildToolArguments(context, null)); } return executeInternalTool(mountCode, toolCode, context); } /** * 根据工具描述符执行一次工具调用。 */ public AiDiagnosticToolResult executeTool(Long tenantId, AiMcpToolDescriptor descriptor, AiPromptContext context) { if (descriptor == null) { throw new IllegalArgumentException("MCP工具不存在"); } if (AiMcpConstants.TRANSPORT_HTTP.equalsIgnoreCase(descriptor.getTransportType()) || AiMcpConstants.TRANSPORT_SSE.equalsIgnoreCase(descriptor.getTransportType())) { AiMcpMount mount = aiMcpMountService.getTenantMountByCode(tenantId, descriptor.getMountCode()); if (mount == null) { throw new IllegalArgumentException("MCP挂载不存在"); } return executeExternalTool(mount, descriptor.getToolCode(), context, buildToolArguments(context, descriptor)); } return executeInternalTool(descriptor.getMountCode(), descriptor.getToolCode(), context); } /** * 根据完整 MCP 工具名执行一次工具调用,通常供协议层直接使用。 */ public AiDiagnosticToolResult executeTool(Long tenantId, String mcpToolName, AiPromptContext context, Map arguments) { AiMcpToolDescriptor descriptor = findDescriptor(tenantId, mcpToolName); if (descriptor == null) { throw new IllegalArgumentException("MCP工具不存在"); } if (AiMcpConstants.TRANSPORT_HTTP.equalsIgnoreCase(descriptor.getTransportType()) || AiMcpConstants.TRANSPORT_SSE.equalsIgnoreCase(descriptor.getTransportType())) { AiMcpMount mount = aiMcpMountService.getTenantMountByCode(tenantId, descriptor.getMountCode()); if (mount == null) { throw new IllegalArgumentException("MCP挂载不存在"); } return executeExternalTool(mount, descriptor.getToolCode(), context, arguments); } return executeInternalTool(descriptor.getMountCode(), descriptor.getToolCode(), context); } /** * 确保租户存在默认本地 MCP 挂载。 */ public void ensureDefaultMount(Long tenantId, Long userId) { if (aiMcpMountService.getTenantMountByCode(tenantId, AiMcpConstants.DEFAULT_LOCAL_MOUNT_CODE) != null) { return; } Date now = new Date(); AiMcpMount mount = new AiMcpMount() .setUuid(String.valueOf(System.currentTimeMillis())) .setName(AiMcpConstants.DEFAULT_LOCAL_MOUNT_NAME) .setMountCode(AiMcpConstants.DEFAULT_LOCAL_MOUNT_CODE) .setTransportType(AiMcpConstants.TRANSPORT_INTERNAL) .setUrl("/ai/mcp") .setEnabledFlag(1) .setTimeoutMs(10000) .setStatus(1) .setDeleted(0) .setTenantId(tenantId) .setCreateBy(userId) .setCreateTime(now) .setUpdateBy(userId) .setUpdateTime(now) .setMemo("默认挂载当前 WMS AI 内置工具集合"); aiMcpMountService.save(mount); } /** * 为内部工具结果补齐挂载编码和标准 MCP 工具名。 */ public AiDiagnosticToolResult decorateResult(AiDiagnosticToolResult result) { if (result == null) { return null; } return decorateResult(result, AiMcpConstants.DEFAULT_LOCAL_MOUNT_CODE, findProvider(result.getToolCode())); } private AiDiagnosticToolResult decorateResult(AiDiagnosticToolResult result, String mountCode, AiDiagnosticDataProvider provider) { String actualMountCode = mountCode == null || mountCode.trim().isEmpty() ? AiMcpConstants.DEFAULT_LOCAL_MOUNT_CODE : mountCode; result.setMountCode(actualMountCode); result.setMcpToolName(aiMcpPayloadMapper.buildMcpToolName(actualMountCode, result.getToolCode())); if ((result.getToolName() == null || result.getToolName().trim().isEmpty()) && provider != null) { result.setToolName(provider.getToolName()); } return result; } /** * 按当前租户的工具配置生成内部 MCP 工具目录。 */ private List buildInternalTools(Long tenantId, AiMcpMount mount) { Map configMap = buildInternalConfigMap(tenantId); List sortedProviders = new ArrayList<>(providers); sortedProviders.sort(Comparator.comparingInt(AiDiagnosticDataProvider::getOrder)); List output = new ArrayList<>(); for (AiDiagnosticDataProvider provider : sortedProviders) { AiDiagnosticToolConfig config = configMap.get(provider.getToolCode()); String usageScope = aiMcpPayloadMapper.resolveUsageScope( config == null ? null : config.getSceneCode(), config == null ? 1 : config.getEnabledFlag(), config == null ? null : config.getUsageScope() ); output.add(new AiMcpToolDescriptor() .setMountCode(mount.getMountCode()) .setMountName(mount.getName()) .setToolCode(provider.getToolCode()) .setMcpToolName(aiMcpPayloadMapper.buildMcpToolName(mount.getMountCode(), provider.getToolCode())) .setToolName(provider.getToolName()) .setSceneCode(aiMcpPayloadMapper.resolveSceneCode(usageScope)) .setDescription(provider.getDefaultToolPrompt()) .setEnabledFlag(AiMcpConstants.USAGE_SCOPE_DISABLED.equals(usageScope) ? 0 : (config == null ? 1 : config.getEnabledFlag())) .setPriority(config == null ? provider.getOrder() : config.getPriority()) .setToolPrompt(config == null ? provider.getDefaultToolPrompt() : config.getToolPrompt()) .setUsageScope(usageScope) .setTransportType(mount.getTransportType()) .setInputSchema(aiMcpPayloadMapper.defaultInputSchema(true))); } return output; } /** * 为内置工具目录选择一条最合适的有效配置。 * 目录层只有一条工具描述,因此这里优先保留启用且状态正常的配置, * 并优先选择“聊天与诊断都可用”的配置,再退回到“仅诊断”配置。 */ private Map buildInternalConfigMap(Long tenantId) { Map configMap = new LinkedHashMap<>(); for (AiDiagnosticToolConfig item : aiDiagnosticToolConfigService.listTenantConfigs(tenantId)) { if (item == null || !Integer.valueOf(1).equals(item.getStatus())) { continue; } AiDiagnosticToolConfig existed = configMap.get(item.getToolCode()); if (existed == null || preferInternalConfig(item, existed)) { configMap.put(item.getToolCode(), item); } } return configMap; } /** * 内置工具目录优先展示用途更广的配置,其次比较优先级。 */ private boolean preferInternalConfig(AiDiagnosticToolConfig candidate, AiDiagnosticToolConfig current) { String candidateUsageScope = aiMcpPayloadMapper.resolveUsageScope( candidate.getSceneCode(), candidate.getEnabledFlag(), candidate.getUsageScope() ); String currentUsageScope = aiMcpPayloadMapper.resolveUsageScope( current.getSceneCode(), current.getEnabledFlag(), current.getUsageScope() ); int candidateRank = usageScopeRank(candidateUsageScope); int currentRank = usageScopeRank(currentUsageScope); if (candidateRank != currentRank) { return candidateRank < currentRank; } Integer candidatePriority = candidate.getPriority() == null ? Integer.MAX_VALUE : candidate.getPriority(); Integer currentPriority = current.getPriority() == null ? Integer.MAX_VALUE : current.getPriority(); return candidatePriority < currentPriority; } private int usageScopeRank(String usageScope) { if (AiMcpConstants.USAGE_SCOPE_CHAT_AND_DIAGNOSE.equals(usageScope)) { return 0; } if (AiMcpConstants.USAGE_SCOPE_DIAGNOSE_ONLY.equals(usageScope)) { return 1; } return 2; } /** * 按工具编码定位内部工具实现。 */ private AiDiagnosticDataProvider findProvider(String toolCode) { for (AiDiagnosticDataProvider provider : providers) { if (provider.getToolCode().equals(toolCode)) { return provider; } } return null; } /** * 更新挂载测试结果和工具数量,并清理缓存。 */ private void updateTestState(AiMcpMount mount, Integer result, String message, Integer toolCount) { mount.setLastTestResult(result); mount.setLastTestMessage(message); mount.setLastToolCount(toolCount); mount.setLastTestTime(new Date()); mount.setUpdateTime(new Date()); aiMcpMountService.updateById(mount); if (mount.getId() != null) { externalToolCache.remove(buildCacheKey(mount.getTenantId(), mount)); } } /** * 执行内部 MCP 工具。 */ private AiDiagnosticToolResult executeInternalTool(String mountCode, String toolCode, AiPromptContext context) { AiDiagnosticDataProvider provider = findProvider(toolCode); if (provider == null) { throw new IllegalArgumentException("MCP工具不存在"); } AiDiagnosticToolResult result = provider.buildDiagnosticData(context); if (result == null) { return new AiDiagnosticToolResult() .setToolCode(toolCode) .setMountCode(mountCode) .setMcpToolName(aiMcpPayloadMapper.buildMcpToolName(mountCode, toolCode)) .setToolName(provider.getToolName()) .setSeverity("WARN") .setSummaryText("工具没有返回结果"); } return decorateResult(result, mountCode, provider); } /** * 执行外部 MCP 工具,并补齐本地系统所需的统一字段。 */ private AiDiagnosticToolResult executeExternalTool(AiMcpMount mount, String toolCode, AiPromptContext context, Map arguments) { ExternalToolCallResult callResult = callExternalTool(mount, toolCode, arguments); AiDiagnosticToolResult result = callResult.result; return result .setToolCode(toolCode) .setMountCode(mount.getMountCode()) .setMcpToolName(aiMcpPayloadMapper.buildMcpToolName(mount.getMountCode(), toolCode)) .setToolName(result.getToolName() == null || result.getToolName().trim().isEmpty() ? toolCode : result.getToolName()) .setRawMeta(result.getRawMeta() == null ? new LinkedHashMap() : result.getRawMeta()); } /** * 真正加载外部工具目录,不带缓存。 */ private List loadExternalTools(AiMcpMount mount) { return loadExternalToolsWithTransport(mount).tools; } /** * 带短期缓存地加载外部工具目录,避免同一挂载在短时间内重复握手。 */ private List loadCachedExternalTools(Long tenantId, AiMcpMount mount) { String cacheKey = buildCacheKey(tenantId, mount); CachedTools cached = externalToolCache.get(cacheKey); long now = System.currentTimeMillis(); if (cached != null && cached.expireAt > now) { return new ArrayList<>(cached.tools); } List tools = loadExternalTools(mount); externalToolCache.put(cacheKey, new CachedTools(new ArrayList<>(tools), now + EXTERNAL_TOOL_CACHE_TTL_MS)); return tools; } /** * 生成外部工具目录缓存 key。 */ private String buildCacheKey(Long tenantId, AiMcpMount mount) { return String.valueOf(tenantId) + ":" + mount.getId() + ":" + (mount.getUpdateTime() == null ? 0L : mount.getUpdateTime().getTime()); } /** * 按完整 MCP 工具名反查工具描述符。 */ private AiMcpToolDescriptor findDescriptor(Long tenantId, String mcpToolName) { for (AiMcpToolDescriptor descriptor : listTools(tenantId, null)) { if (descriptor != null && mcpToolName.equals(descriptor.getMcpToolName())) { return descriptor; } } return null; } /** * 根据上下文和工具信息构造远程调用参数。 */ private Map buildToolArguments(AiPromptContext context, AiMcpToolDescriptor descriptor) { Map arguments = new LinkedHashMap<>(); if (context != null) { arguments.put("tenantId", context.getTenantId()); arguments.put("sceneCode", context.getSceneCode()); arguments.put("question", context.getQuestion()); arguments.put("sessionId", context.getSessionId()); } if (descriptor != null) { arguments.put("toolName", descriptor.getToolName()); arguments.put("mountCode", descriptor.getMountCode()); } return arguments; } /** * 生成外部挂载测试成功文案。 */ private String buildExternalSuccessMessage(String transportType, int toolCount) { String prefix = AiMcpConstants.TRANSPORT_SSE.equalsIgnoreCase(transportType) ? "远程SSE MCP工具加载成功" : "远程Streamable HTTP MCP工具加载成功"; return prefix + ",发现 " + toolCount + " 个工具"; } /** * 根据挂载配置选择 HTTP / SSE 客户端去加载外部工具目录。 * AUTO 模式会依次尝试 Streamable HTTP 和 SSE。 */ private ExternalToolsResult loadExternalToolsWithTransport(AiMcpMount mount) { if (AiMcpConstants.TRANSPORT_SSE.equalsIgnoreCase(mount.getTransportType())) { return new ExternalToolsResult(AiMcpConstants.TRANSPORT_SSE, aiMcpSseClient.listTools(mount)); } if (AiMcpConstants.TRANSPORT_HTTP.equalsIgnoreCase(mount.getTransportType())) { return new ExternalToolsResult(AiMcpConstants.TRANSPORT_HTTP, aiMcpHttpClient.listTools(mount)); } List errors = new ArrayList<>(); try { return new ExternalToolsResult(AiMcpConstants.TRANSPORT_HTTP, aiMcpHttpClient.listTools(copyMountWithTransport(mount, AiMcpConstants.TRANSPORT_HTTP))); } catch (Exception e) { errors.add("HTTP: " + e.getMessage()); } try { return new ExternalToolsResult(AiMcpConstants.TRANSPORT_SSE, aiMcpSseClient.listTools(copyMountWithTransport(mount, AiMcpConstants.TRANSPORT_SSE))); } catch (Exception e) { errors.add("SSE: " + e.getMessage()); } throw new IllegalStateException(String.join(";", errors)); } /** * 根据挂载配置选择 HTTP / SSE 客户端执行远程工具。 */ private ExternalToolCallResult callExternalTool(AiMcpMount mount, String toolCode, Map arguments) { if (AiMcpConstants.TRANSPORT_SSE.equalsIgnoreCase(mount.getTransportType())) { return new ExternalToolCallResult(AiMcpConstants.TRANSPORT_SSE, aiMcpSseClient.callTool(mount, toolCode, arguments)); } if (AiMcpConstants.TRANSPORT_HTTP.equalsIgnoreCase(mount.getTransportType())) { return new ExternalToolCallResult(AiMcpConstants.TRANSPORT_HTTP, aiMcpHttpClient.callTool(mount, toolCode, arguments)); } List errors = new ArrayList<>(); try { return new ExternalToolCallResult( AiMcpConstants.TRANSPORT_HTTP, aiMcpHttpClient.callTool(copyMountWithTransport(mount, AiMcpConstants.TRANSPORT_HTTP), toolCode, arguments) ); } catch (Exception e) { errors.add("HTTP: " + e.getMessage()); } try { return new ExternalToolCallResult( AiMcpConstants.TRANSPORT_SSE, aiMcpSseClient.callTool(copyMountWithTransport(mount, AiMcpConstants.TRANSPORT_SSE), toolCode, arguments) ); } catch (Exception e) { errors.add("SSE: " + e.getMessage()); } throw new IllegalStateException(String.join(";", errors)); } /** * 复制一份挂载对象,并覆盖指定传输协议,供 AUTO 探测流程使用。 */ private AiMcpMount copyMountWithTransport(AiMcpMount mount, String transportType) { return new AiMcpMount() .setId(mount.getId()) .setUuid(mount.getUuid()) .setName(mount.getName()) .setMountCode(mount.getMountCode()) .setTransportType(transportType) .setUrl(mount.getUrl()) .setAuthType(mount.getAuthType()) .setAuthValue(mount.getAuthValue()) .setUsageScope(mount.getUsageScope()) .setEnabledFlag(mount.getEnabledFlag()) .setTimeoutMs(mount.getTimeoutMs()) .setStatus(mount.getStatus()) .setTenantId(mount.getTenantId()) .setCreateBy(mount.getCreateBy()) .setCreateTime(mount.getCreateTime()) .setUpdateBy(mount.getUpdateBy()) .setUpdateTime(mount.getUpdateTime()) .setMemo(mount.getMemo()); } private static class CachedTools { private final List tools; private final long expireAt; /** * 保存一次外部工具目录缓存内容及其失效时间。 */ private CachedTools(List tools, long expireAt) { this.tools = tools; this.expireAt = expireAt; } } private static class ExternalToolsResult { private final String transportType; private final List tools; /** * 保存一次外部工具目录加载结果及实际命中的传输协议。 */ private ExternalToolsResult(String transportType, List tools) { this.transportType = transportType; this.tools = tools; } } private static class ExternalToolCallResult { private final String transportType; private final AiDiagnosticToolResult result; /** * 保存一次外部工具调用结果及实际命中的传输协议。 */ private ExternalToolCallResult(String transportType, AiDiagnosticToolResult result) { this.transportType = transportType; this.result = result; } } }