package com.vincent.rsf.server.ai.service.impl.mcp;
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.vincent.rsf.framework.exception.CoolException;
|
import com.vincent.rsf.server.ai.config.AiDefaults;
|
import com.vincent.rsf.server.ai.dto.AiMcpConnectivityTestDto;
|
import com.vincent.rsf.server.ai.dto.AiMcpToolPreviewDto;
|
import com.vincent.rsf.server.ai.dto.AiMcpToolTestDto;
|
import com.vincent.rsf.server.ai.dto.AiMcpToolTestRequest;
|
import com.vincent.rsf.server.ai.entity.AiMcpMount;
|
import com.vincent.rsf.server.ai.mapper.AiMcpMountMapper;
|
import com.vincent.rsf.server.ai.service.BuiltinMcpToolRegistry;
|
import com.vincent.rsf.server.ai.service.McpMountRuntimeFactory;
|
import lombok.RequiredArgsConstructor;
|
import org.springframework.ai.chat.model.ToolContext;
|
import org.springframework.ai.tool.ToolCallback;
|
import org.springframework.stereotype.Service;
|
import org.springframework.util.StringUtils;
|
|
import java.text.SimpleDateFormat;
|
import java.util.ArrayList;
|
import java.util.Arrays;
|
import java.util.Date;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
|
@Service
|
@RequiredArgsConstructor
|
public class AiMcpAdminService {
|
|
private final AiMcpMountMapper aiMcpMountMapper;
|
private final BuiltinMcpToolRegistry builtinMcpToolRegistry;
|
private final McpMountRuntimeFactory mcpMountRuntimeFactory;
|
private final ObjectMapper objectMapper;
|
|
public List<AiMcpToolPreviewDto> previewTools(AiMcpMount mount, Long userId) {
|
long startedAt = System.currentTimeMillis();
|
try (McpMountRuntimeFactory.McpMountRuntime runtime = mcpMountRuntimeFactory.create(List.of(mount), userId)) {
|
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());
|
if (mount.getId() != null) {
|
updateHealthStatus(mount.getId(), AiDefaults.MCP_HEALTH_UNHEALTHY, message, System.currentTimeMillis() - startedAt);
|
}
|
throw new CoolException(message);
|
}
|
if (mount.getId() != null) {
|
updateHealthStatus(mount.getId(), AiDefaults.MCP_HEALTH_HEALTHY,
|
"工具解析成功,共 " + tools.size() + " 个工具", System.currentTimeMillis() - startedAt);
|
}
|
return tools;
|
} catch (CoolException e) {
|
throw e;
|
} catch (Exception e) {
|
if (mount.getId() != null) {
|
updateHealthStatus(mount.getId(), AiDefaults.MCP_HEALTH_UNHEALTHY,
|
"工具解析失败: " + e.getMessage(), System.currentTimeMillis() - startedAt);
|
}
|
throw new CoolException("获取工具列表失败: " + e.getMessage());
|
}
|
}
|
|
public AiMcpConnectivityTestDto testConnectivity(AiMcpMount mount, Long userId, boolean persistHealth) {
|
long startedAt = System.currentTimeMillis();
|
try (McpMountRuntimeFactory.McpMountRuntime runtime = mcpMountRuntimeFactory.create(List.of(mount), userId)) {
|
long elapsedMs = System.currentTimeMillis() - startedAt;
|
if (!runtime.getErrors().isEmpty()) {
|
String message = String.join(";", runtime.getErrors());
|
if (persistHealth && mount.getId() != null) {
|
updateHealthStatus(mount.getId(), AiDefaults.MCP_HEALTH_UNHEALTHY, message, elapsedMs);
|
AiMcpMount latest = requireMount(mount.getId(), mount.getTenantId());
|
return buildConnectivityDto(latest, message, elapsedMs, runtime.getToolCallbacks().length);
|
}
|
return AiMcpConnectivityTestDto.builder()
|
.mountId(mount.getId())
|
.mountName(mount.getName())
|
.healthStatus(AiDefaults.MCP_HEALTH_UNHEALTHY)
|
.message(message)
|
.initElapsedMs(elapsedMs)
|
.toolCount(runtime.getToolCallbacks().length)
|
.testedAt(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()))
|
.build();
|
}
|
String message = persistHealth && mount.getId() != null
|
? "连通性测试成功,解析出 " + runtime.getToolCallbacks().length + " 个工具"
|
: "草稿连通性测试成功,解析出 " + runtime.getToolCallbacks().length + " 个工具";
|
if (persistHealth && mount.getId() != null) {
|
updateHealthStatus(mount.getId(), AiDefaults.MCP_HEALTH_HEALTHY, message, elapsedMs);
|
AiMcpMount latest = requireMount(mount.getId(), mount.getTenantId());
|
return buildConnectivityDto(latest, message, elapsedMs, runtime.getToolCallbacks().length);
|
}
|
return AiMcpConnectivityTestDto.builder()
|
.mountId(mount.getId())
|
.mountName(mount.getName())
|
.healthStatus(AiDefaults.MCP_HEALTH_HEALTHY)
|
.message(message)
|
.initElapsedMs(elapsedMs)
|
.toolCount(runtime.getToolCallbacks().length)
|
.testedAt(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()))
|
.build();
|
} catch (CoolException e) {
|
throw e;
|
} catch (Exception e) {
|
long elapsedMs = System.currentTimeMillis() - startedAt;
|
String message = (persistHealth ? "连通性测试失败: " : "草稿连通性测试失败: ") + e.getMessage();
|
if (persistHealth && mount.getId() != null) {
|
updateHealthStatus(mount.getId(), AiDefaults.MCP_HEALTH_UNHEALTHY, message, elapsedMs);
|
AiMcpMount latest = requireMount(mount.getId(), mount.getTenantId());
|
return buildConnectivityDto(latest, message, elapsedMs, 0);
|
}
|
throw new CoolException(message);
|
}
|
}
|
|
public AiMcpToolTestDto testTool(AiMcpMount mount, Long userId, Long tenantId, AiMcpToolTestRequest request) {
|
if (userId == null) {
|
throw new CoolException("当前登录用户不存在");
|
}
|
if (tenantId == null) {
|
throw new CoolException("当前租户不存在");
|
}
|
if (request == null) {
|
throw new CoolException("工具测试参数不能为空");
|
}
|
if (!StringUtils.hasText(request.getToolName())) {
|
throw new CoolException("工具名称不能为空");
|
}
|
if (!StringUtils.hasText(request.getInputJson())) {
|
throw new CoolException("工具输入 JSON 不能为空");
|
}
|
try {
|
objectMapper.readTree(request.getInputJson());
|
} catch (Exception e) {
|
throw new CoolException("工具输入 JSON 格式错误: " + e.getMessage());
|
}
|
long startedAt = System.currentTimeMillis();
|
try (McpMountRuntimeFactory.McpMountRuntime runtime = mcpMountRuntimeFactory.create(List.of(mount), userId)) {
|
ToolCallback callback = Arrays.stream(runtime.getToolCallbacks())
|
.filter(item -> item != null && item.getToolDefinition() != null)
|
.filter(item -> request.getToolName().equals(item.getToolDefinition().name()))
|
.findFirst()
|
.orElseThrow(() -> new CoolException("未找到要测试的工具: " + request.getToolName()));
|
String output = callback.call(
|
request.getInputJson(),
|
new ToolContext(Map.of("userId", userId, "tenantId", tenantId, "mountId", mount.getId()))
|
);
|
if (mount.getId() != null) {
|
updateHealthStatus(mount.getId(), AiDefaults.MCP_HEALTH_HEALTHY,
|
"工具测试成功: " + request.getToolName(), System.currentTimeMillis() - startedAt);
|
}
|
return AiMcpToolTestDto.builder()
|
.toolName(request.getToolName())
|
.inputJson(request.getInputJson())
|
.output(output)
|
.build();
|
} catch (CoolException e) {
|
if (mount.getId() != null) {
|
updateHealthStatus(mount.getId(), AiDefaults.MCP_HEALTH_UNHEALTHY,
|
"工具测试失败: " + e.getMessage(), System.currentTimeMillis() - startedAt);
|
}
|
throw e;
|
} catch (Exception e) {
|
if (mount.getId() != null) {
|
updateHealthStatus(mount.getId(), AiDefaults.MCP_HEALTH_UNHEALTHY,
|
"工具测试失败: " + e.getMessage(), System.currentTimeMillis() - startedAt);
|
}
|
throw new CoolException("工具测试失败: " + e.getMessage());
|
}
|
}
|
|
public AiMcpMount requireMount(Long mountId, Long tenantId) {
|
if (tenantId == null) {
|
throw new CoolException("当前租户不存在");
|
}
|
if (mountId == null) {
|
throw new CoolException("MCP 挂载 ID 不能为空");
|
}
|
AiMcpMount mount = aiMcpMountMapper.selectOne(new LambdaQueryWrapper<AiMcpMount>()
|
.eq(AiMcpMount::getId, mountId)
|
.eq(AiMcpMount::getTenantId, tenantId)
|
.eq(AiMcpMount::getDeleted, 0)
|
.last("limit 1"));
|
if (mount == null) {
|
throw new CoolException("MCP 挂载不存在");
|
}
|
return mount;
|
}
|
|
private List<AiMcpToolPreviewDto> buildToolPreviewDtos(ToolCallback[] callbacks, List<AiMcpToolPreviewDto> governedCatalog) {
|
List<AiMcpToolPreviewDto> tools = new ArrayList<>();
|
Map<String, AiMcpToolPreviewDto> catalogMap = new 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;
|
}
|
|
private void updateHealthStatus(Long mountId, String healthStatus, String message, Long initElapsedMs) {
|
aiMcpMountMapper.update(null, new LambdaUpdateWrapper<AiMcpMount>()
|
.eq(AiMcpMount::getId, mountId)
|
.set(AiMcpMount::getHealthStatus, healthStatus)
|
.set(AiMcpMount::getLastTestTime, new Date())
|
.set(AiMcpMount::getLastTestMessage, message)
|
.set(AiMcpMount::getLastInitElapsedMs, initElapsedMs));
|
}
|
|
private AiMcpConnectivityTestDto buildConnectivityDto(AiMcpMount mount, String message, Long initElapsedMs, Integer toolCount) {
|
return AiMcpConnectivityTestDto.builder()
|
.mountId(mount.getId())
|
.mountName(mount.getName())
|
.healthStatus(mount.getHealthStatus())
|
.message(message)
|
.initElapsedMs(initElapsedMs)
|
.toolCount(toolCount)
|
.testedAt(mount.getLastTestTime$())
|
.build();
|
}
|
}
|