zhou zhou
13 小时以前 80a6d9236ade191a5de0975abe4de5a6e7e63915
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiMcpMountServiceImpl.java
@@ -36,6 +36,7 @@
    private final McpMountRuntimeFactory mcpMountRuntimeFactory;
    private final ObjectMapper objectMapper;
    /** 查询某个租户下当前启用的 MCP 挂载列表。 */
    @Override
    public List<AiMcpMount> listActiveMounts(Long tenantId) {
        ensureTenantId(tenantId);
@@ -47,6 +48,7 @@
                .orderByAsc(AiMcpMount::getId));
    }
    /** 保存前校验 MCP 挂载草稿,并补全运行时默认值。 */
    @Override
    public void validateBeforeSave(AiMcpMount aiMcpMount, Long tenantId) {
        ensureTenantId(tenantId);
@@ -55,6 +57,7 @@
        ensureRequiredFields(aiMcpMount, tenantId);
    }
    /** 更新前校验并锁定记录所属租户,防止跨租户修改。 */
    @Override
    public void validateBeforeUpdate(AiMcpMount aiMcpMount, Long tenantId) {
        ensureTenantId(tenantId);
@@ -67,12 +70,19 @@
        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);
@@ -90,6 +100,7 @@
        }
    }
    /** 对已保存的挂载做真实连通性测试,并把结果回写到运行态字段。 */
    @Override
    public AiMcpConnectivityTestDto testConnectivity(Long mountId, Long userId, Long tenantId) {
        AiMcpMount mount = requireMount(mountId, tenantId);
@@ -117,6 +128,53 @@
        }
    }
    /** 对表单里的草稿配置做临时连通性测试,不落库。 */
    @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) {
@@ -170,6 +228,7 @@
    }
    private void fillDefaults(AiMcpMount aiMcpMount) {
        /** 为挂载草稿补齐统一默认值,保证后续运行时代码不需要重复判断空值。 */
        if (!StringUtils.hasText(aiMcpMount.getTransportType())) {
            aiMcpMount.setTransportType(AiDefaults.MCP_TRANSPORT_SSE_HTTP);
        }
@@ -188,6 +247,10 @@
    }
    private void ensureRequiredFields(AiMcpMount aiMcpMount, Long tenantId) {
        /**
         * 按 transportType 校验挂载必填项。
         * 这里把“字段合法性”和“跨记录冲突”一起收口,避免校验逻辑分散在 controller 层。
         */
        if (!StringUtils.hasText(aiMcpMount.getName())) {
            throw new CoolException("MCP 挂载名称不能为空");
        }
@@ -212,6 +275,7 @@
    }
    private AiMcpMount requireMount(Long mountId, Long tenantId) {
        /** 按租户加载挂载记录,不存在直接抛错。 */
        ensureTenantId(tenantId);
        if (mountId == null) {
            throw new CoolException("MCP 挂载 ID 不能为空");
@@ -228,6 +292,7 @@
    }
    private void ensureBuiltinConflictFree(AiMcpMount aiMcpMount, Long tenantId) {
        /** 校验同租户下是否存在与当前内置编码互斥的启用挂载。 */
        if (aiMcpMount.getStatus() == null || aiMcpMount.getStatus() != StatusType.ENABLE.val) {
            return;
        }
@@ -253,19 +318,10 @@
    }
    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) {
@@ -274,17 +330,30 @@
        }
    }
    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;