| | |
| | | private final McpMountRuntimeFactory mcpMountRuntimeFactory; |
| | | private final ObjectMapper objectMapper; |
| | | |
| | | /** 查询某个租户下当前启用的 MCP 挂载列表。 */ |
| | | @Override |
| | | public List<AiMcpMount> listActiveMounts(Long tenantId) { |
| | | ensureTenantId(tenantId); |
| | |
| | | .orderByAsc(AiMcpMount::getId)); |
| | | } |
| | | |
| | | /** 保存前校验 MCP 挂载草稿,并补全运行时默认值。 */ |
| | | @Override |
| | | public void validateBeforeSave(AiMcpMount aiMcpMount, Long tenantId) { |
| | | ensureTenantId(tenantId); |
| | |
| | | ensureRequiredFields(aiMcpMount, tenantId); |
| | | } |
| | | |
| | | /** 更新前校验并锁定记录所属租户,防止跨租户修改。 */ |
| | | @Override |
| | | public void validateBeforeUpdate(AiMcpMount aiMcpMount, Long tenantId) { |
| | | ensureTenantId(tenantId); |
| | |
| | | ensureRequiredFields(aiMcpMount, tenantId); |
| | | } |
| | | |
| | | /** |
| | | * 预览当前挂载最终会暴露给模型的工具目录。 |
| | | * 对内置 MCP 会额外合并治理目录信息,对外部 MCP 则以实际解析结果为准。 |
| | | */ |
| | | @Override |
| | | public List<AiMcpToolPreviewDto> previewTools(Long mountId, Long userId, Long tenantId) { |
| | | AiMcpMount mount = requireMount(mountId, tenantId); |
| | | long startedAt = System.currentTimeMillis(); |
| | | try (McpMountRuntimeFactory.McpMountRuntime runtime = mcpMountRuntimeFactory.create(List.of(mount), userId)) { |
| | | List<AiMcpToolPreviewDto> tools = buildToolPreviewDtos(runtime.getToolCallbacks()); |
| | | List<AiMcpToolPreviewDto> tools = buildToolPreviewDtos(runtime.getToolCallbacks(), |
| | | AiDefaults.MCP_TRANSPORT_BUILTIN.equals(mount.getTransportType()) |
| | | ? builtinMcpToolRegistry.listBuiltinToolCatalog(mount.getBuiltinCode()) |
| | | : List.of()); |
| | | if (!runtime.getErrors().isEmpty()) { |
| | | String message = String.join(";", runtime.getErrors()); |
| | | updateHealthStatus(mount.getId(), AiDefaults.MCP_HEALTH_UNHEALTHY, message, System.currentTimeMillis() - startedAt); |
| | |
| | | } |
| | | } |
| | | |
| | | /** 对已保存的挂载做真实连通性测试,并把结果回写到运行态字段。 */ |
| | | @Override |
| | | public AiMcpConnectivityTestDto testConnectivity(Long mountId, Long userId, Long tenantId) { |
| | | AiMcpMount mount = requireMount(mountId, tenantId); |
| | |
| | | } |
| | | } |
| | | |
| | | /** 对表单里的草稿配置做临时连通性测试,不落库。 */ |
| | | @Override |
| | | public AiMcpConnectivityTestDto testDraftConnectivity(AiMcpMount mount, Long userId, Long tenantId) { |
| | | ensureTenantId(tenantId); |
| | | if (userId == null) { |
| | | throw new CoolException("当前登录用户不存在"); |
| | | } |
| | | if (mount == null) { |
| | | throw new CoolException("MCP 挂载参数不能为空"); |
| | | } |
| | | mount.setTenantId(tenantId); |
| | | fillDefaults(mount); |
| | | ensureRequiredFields(mount, tenantId); |
| | | long startedAt = System.currentTimeMillis(); |
| | | try (McpMountRuntimeFactory.McpMountRuntime runtime = mcpMountRuntimeFactory.create(List.of(mount), userId)) { |
| | | long elapsedMs = System.currentTimeMillis() - startedAt; |
| | | if (!runtime.getErrors().isEmpty()) { |
| | | return AiMcpConnectivityTestDto.builder() |
| | | .mountId(mount.getId()) |
| | | .mountName(mount.getName()) |
| | | .healthStatus(AiDefaults.MCP_HEALTH_UNHEALTHY) |
| | | .message(String.join(";", runtime.getErrors())) |
| | | .initElapsedMs(elapsedMs) |
| | | .toolCount(runtime.getToolCallbacks().length) |
| | | .testedAt(new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())) |
| | | .build(); |
| | | } |
| | | return AiMcpConnectivityTestDto.builder() |
| | | .mountId(mount.getId()) |
| | | .mountName(mount.getName()) |
| | | .healthStatus(AiDefaults.MCP_HEALTH_HEALTHY) |
| | | .message("草稿连通性测试成功,解析出 " + runtime.getToolCallbacks().length + " 个工具") |
| | | .initElapsedMs(elapsedMs) |
| | | .toolCount(runtime.getToolCallbacks().length) |
| | | .testedAt(new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())) |
| | | .build(); |
| | | } catch (CoolException e) { |
| | | throw e; |
| | | } catch (Exception e) { |
| | | throw new CoolException("草稿连通性测试失败: " + e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 直接执行某一个工具的测试调用。 |
| | | * 该方法主要服务于管理端的“工具测试”面板,不参与正式对话链路。 |
| | | */ |
| | | @Override |
| | | public AiMcpToolTestDto testTool(Long mountId, Long userId, Long tenantId, AiMcpToolTestRequest request) { |
| | | if (userId == null) { |
| | |
| | | } |
| | | |
| | | private void fillDefaults(AiMcpMount aiMcpMount) { |
| | | /** 为挂载草稿补齐统一默认值,保证后续运行时代码不需要重复判断空值。 */ |
| | | if (!StringUtils.hasText(aiMcpMount.getTransportType())) { |
| | | aiMcpMount.setTransportType(AiDefaults.MCP_TRANSPORT_SSE_HTTP); |
| | | } |
| | |
| | | } |
| | | |
| | | private void ensureRequiredFields(AiMcpMount aiMcpMount, Long tenantId) { |
| | | /** |
| | | * 按 transportType 校验挂载必填项。 |
| | | * 这里把“字段合法性”和“跨记录冲突”一起收口,避免校验逻辑分散在 controller 层。 |
| | | */ |
| | | if (!StringUtils.hasText(aiMcpMount.getName())) { |
| | | throw new CoolException("MCP 挂载名称不能为空"); |
| | | } |
| | |
| | | } |
| | | |
| | | private AiMcpMount requireMount(Long mountId, Long tenantId) { |
| | | /** 按租户加载挂载记录,不存在直接抛错。 */ |
| | | ensureTenantId(tenantId); |
| | | if (mountId == null) { |
| | | throw new CoolException("MCP 挂载 ID 不能为空"); |
| | |
| | | } |
| | | |
| | | private void ensureBuiltinConflictFree(AiMcpMount aiMcpMount, Long tenantId) { |
| | | /** 校验同租户下是否存在与当前内置编码互斥的启用挂载。 */ |
| | | if (aiMcpMount.getStatus() == null || aiMcpMount.getStatus() != StatusType.ENABLE.val) { |
| | | return; |
| | | } |
| | |
| | | } |
| | | |
| | | private List<String> resolveConflictCodes(String builtinCode) { |
| | | List<String> codes = new ArrayList<>(); |
| | | if (AiDefaults.MCP_BUILTIN_RSF_WMS.equals(builtinCode)) { |
| | | codes.add(AiDefaults.MCP_BUILTIN_RSF_WMS_STOCK); |
| | | codes.add(AiDefaults.MCP_BUILTIN_RSF_WMS_TASK); |
| | | codes.add(AiDefaults.MCP_BUILTIN_RSF_WMS_BASE); |
| | | return codes; |
| | | return List.of(); |
| | | } |
| | | if (AiDefaults.MCP_BUILTIN_RSF_WMS_STOCK.equals(builtinCode) |
| | | || AiDefaults.MCP_BUILTIN_RSF_WMS_TASK.equals(builtinCode) |
| | | || AiDefaults.MCP_BUILTIN_RSF_WMS_BASE.equals(builtinCode)) { |
| | | codes.add(AiDefaults.MCP_BUILTIN_RSF_WMS); |
| | | } |
| | | return codes; |
| | | throw new CoolException("不支持的内置 MCP 编码: " + builtinCode); |
| | | } |
| | | |
| | | private void ensureTenantId(Long tenantId) { |
| | |
| | | } |
| | | } |
| | | |
| | | private List<AiMcpToolPreviewDto> buildToolPreviewDtos(ToolCallback[] callbacks) { |
| | | private List<AiMcpToolPreviewDto> buildToolPreviewDtos(ToolCallback[] callbacks, List<AiMcpToolPreviewDto> governedCatalog) { |
| | | /** 把底层 ToolCallback 和治理目录信息拼成前端需要的结构化工具卡片数据。 */ |
| | | List<AiMcpToolPreviewDto> tools = new ArrayList<>(); |
| | | Map<String, AiMcpToolPreviewDto> catalogMap = new java.util.LinkedHashMap<>(); |
| | | for (AiMcpToolPreviewDto item : governedCatalog) { |
| | | if (item == null || !StringUtils.hasText(item.getName())) { |
| | | continue; |
| | | } |
| | | catalogMap.put(item.getName(), item); |
| | | } |
| | | for (ToolCallback callback : callbacks) { |
| | | if (callback == null || callback.getToolDefinition() == null) { |
| | | continue; |
| | | } |
| | | AiMcpToolPreviewDto governedItem = catalogMap.get(callback.getToolDefinition().name()); |
| | | tools.add(AiMcpToolPreviewDto.builder() |
| | | .name(callback.getToolDefinition().name()) |
| | | .description(callback.getToolDefinition().description()) |
| | | .inputSchema(callback.getToolDefinition().inputSchema()) |
| | | .returnDirect(callback.getToolMetadata() == null ? null : callback.getToolMetadata().returnDirect()) |
| | | .toolGroup(governedItem == null ? null : governedItem.getToolGroup()) |
| | | .toolPurpose(governedItem == null ? null : governedItem.getToolPurpose()) |
| | | .queryBoundary(governedItem == null ? null : governedItem.getQueryBoundary()) |
| | | .exampleQuestions(governedItem == null ? List.of() : governedItem.getExampleQuestions()) |
| | | .build()); |
| | | } |
| | | return tools; |