feat:恢复mcp模块 动态agent

This commit is contained in:
evo
2026-03-08 22:41:24 +08:00
parent f160ec714b
commit 84dbc2cfbf
54 changed files with 1150 additions and 793 deletions

View File

@@ -1,50 +1,41 @@
package org.ruoyi.service.chat.impl;
import dev.langchain4j.agentic.AgenticServices;
import dev.langchain4j.agentic.supervisor.SupervisorAgent;
import dev.langchain4j.agentic.supervisor.SupervisorResponseStrategy;
import dev.langchain4j.community.model.dashscope.QwenChatModel;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.mcp.McpToolProvider;
import dev.langchain4j.mcp.client.DefaultMcpClient;
import dev.langchain4j.mcp.client.McpClient;
import dev.langchain4j.mcp.client.transport.McpTransport;
import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.tool.ToolProvider;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.agent.ChartGenerationAgent;
import org.ruoyi.agent.SqlAgent;
import org.ruoyi.agent.WebSearchAgent;
import org.ruoyi.agent.tool.ExecuteSqlQueryTool;
import org.ruoyi.agent.tool.QueryAllTablesTool;
import org.ruoyi.agent.tool.QueryTableSchemaTool;
import org.ruoyi.agent.McpAgent;
import org.ruoyi.common.chat.base.ThreadContext;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.dto.request.ReSumeRunner;
import org.ruoyi.common.chat.domain.dto.request.WorkFlowRunner;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.common.chat.entity.chat.ChatContext;
import org.ruoyi.common.chat.enums.RoleType;
import org.ruoyi.common.chat.service.chat.IChatService;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.entity.chat.ChatContext;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.common.chat.service.chatMessage.AbstractChatMessageService;
import org.ruoyi.common.chat.service.workFlow.IWorkFlowStarterService;
import org.ruoyi.common.core.utils.ObjectUtils;
import org.ruoyi.common.core.utils.SpringUtils;
import org.ruoyi.common.core.utils.StringUtils;
import org.ruoyi.common.sse.utils.SseMessageUtils;
import org.ruoyi.mcp.service.core.ToolProviderFactory;
import org.ruoyi.service.chat.impl.memory.PersistentChatMemoryStore;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
@@ -213,17 +204,6 @@ public abstract class AbstractStreamingChatService extends AbstractChatMessageSe
});
}
/**
* 清理指定会话的内存缓存(可选)
* 在会话结束时调用,释放内存资源
*
* @param sessionId 会话ID
*/
public static void clearChatMemory(Object sessionId) {
memoryCache.remove(sessionId);
log.debug("已清理会话 {} 的内存缓存", sessionId);
}
/**
* 执行聊天(钩子方法 - 子类必须实现)
* 注意messages 已包含完整的历史上下文和当前消息
@@ -232,7 +212,8 @@ public abstract class AbstractStreamingChatService extends AbstractChatMessageSe
* @param chatRequest 聊天请求
* @param handler 响应处理器
*/
protected abstract void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List<ChatMessage> messagesWithMemory, StreamingChatResponseHandler handler);
protected abstract void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest,
List<ChatMessage> messagesWithMemory, StreamingChatResponseHandler handler);
/**
* 创建标准的响应处理器
@@ -302,103 +283,80 @@ public abstract class AbstractStreamingChatService extends AbstractChatMessageSe
};
}
/**
* 构建具体厂商的 StreamingChatModel
* 子类必须实现此方法,返回对应厂商的模型实例
*/
protected abstract StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest);
/**
* 获取提供者名称(子类必须实现)
*/
public abstract String getProviderName();
protected String doAgent(String userMessage, ChatModelVo chatModelVo) {
// 步骤1: 配置MCP传输层 - 连接到bing-cn-mcp服务器
// 该服务提供两个工具: bing_search (必应搜索) 和 crawl_webpage (网页抓取)
// McpTransport transport = new StdioMcpTransport.Builder()
// .command(List.of("C:\\Program Files\\nodejs\\npx.cmd", "-y",
// "bing-cn-mcp"
// ))
// .logEvents(true)
// .build();
// // 步骤2: 创建MCP客户端
// McpClient mcpClient = new DefaultMcpClient.Builder()
// .transport(transport)
// .build();
// // 步骤3: 配置工具提供者
// ToolProvider toolProvider = McpToolProvider.builder()
// .mcpClients(List.of(mcpClient))
// .build();
McpTransport transport1 = new StdioMcpTransport.Builder()
.command(List.of("npx", "-y",
"mcp-echarts"
))
.logEvents(true)
.build();
// 步骤2: 创建MCP客户端
McpClient mcpClient1 = new DefaultMcpClient.Builder()
.transport(transport1)
.build();
// 步骤3: 配置工具提供者
ToolProvider toolProvider1 = McpToolProvider.builder()
.mcpClients(List.of(mcpClient1))
.build();
// 步骤4: 配置OpenAI模型
// OpenAiChatModel PLANNER_MODEL = OpenAiChatModel.builder()
// .baseUrl(chatModelVo.getApiHost())
// .apiKey(chatModelVo.getApiKey())
// .modelName(chatModelVo.getModelName())
// .build();
QwenChatModel qwenChatModel = QwenChatModel.builder()
// .baseUrl(chatModelVo.getApiHost())
.apiKey(chatModelVo.getApiKey())
.modelName(chatModelVo.getModelName())
.build();
SqlAgent sqlAgent = AgenticServices.agentBuilder(SqlAgent.class)
.chatModel(
qwenChatModel)
.tools(
SpringUtils.getBean(QueryAllTablesTool.class), // 必须通过 getBean 获取
SpringUtils.getBean(QueryTableSchemaTool.class),
SpringUtils.getBean(ExecuteSqlQueryTool.class)
)
.build();
// WebSearchAgent searchAgent = AgenticServices.agentBuilder(WebSearchAgent.class)
// .chatModel(PLANNER_MODEL)
// .toolProvider(toolProvider)
// .build();
ChartGenerationAgent chartGenerationAgent = AgenticServices.agentBuilder(ChartGenerationAgent.class)
.chatModel(
qwenChatModel)
.toolProvider(toolProvider1)
.build();
String res = sqlAgent.getData(userMessage);
String res1 = chartGenerationAgent.generateChart(res);
System.out.println(res1);
System.out.println(res);
SupervisorAgent supervisor = AgenticServices
.supervisorBuilder()
.chatModel(qwenChatModel)
.subAgents(sqlAgent, chartGenerationAgent)
.responseStrategy(SupervisorResponseStrategy.LAST)
.build();
String invoke = supervisor.invoke(userMessage);
System.out.println(invoke);
return res1;
log.info("执行Agent任务消息: {}", userMessage);
// 加载所有可用的 Agent让 Supervisor 根据任务类型自动选择
return doAgentWithAllAgents(userMessage, chatModelVo);
}
/**
* 使用单一 Agent 处理所有任务
* 不使用 Supervisor 模式,而是使用 MCP Agent 来处理所有任务
*
* @param userMessage 用户消息
* @param chatModelVo 聊天模型配置
* @return Agent 响应结果
*/
protected String doAgentWithAllAgents(String userMessage, ChatModelVo chatModelVo) {
try {
// 1. 加载 LLM 模型
QwenChatModel qwenChatModel = QwenChatModel.builder()
.apiKey(chatModelVo.getApiKey())
.modelName(chatModelVo.getModelName())
.build();
// 2. 获取统一工具提供工厂
ToolProviderFactory toolProviderFactory = SpringUtils.getBean(ToolProviderFactory.class);
// 3. 获取所有可用的工具
// 3.1 添加 BUILTIN 工具对象(包括 SQL 工具)
List<Object> builtinTools = toolProviderFactory.getAllBuiltinToolObjects();
List<Object> allTools = new ArrayList<>(builtinTools);
log.debug("Loaded {} builtin tools (including SQL tools)", builtinTools.size());
log.debug("Total tools: {}", allTools.size());
// 4. 获取 MCP 工具提供者
ToolProvider mcpToolProvider = toolProviderFactory.getAllEnabledMcpToolsProvider();
// 5. 创建 MCP Agent包含所有工具
var agentBuilder = AgenticServices.agentBuilder(McpAgent.class).chatModel(qwenChatModel);
// 添加所有工具
if (!allTools.isEmpty()) {
agentBuilder.tools(allTools.toArray(new Object[0]));
}
// 添加 MCP 工具
if (mcpToolProvider != null) {
agentBuilder.toolProvider(mcpToolProvider);
}
McpAgent mcpAgent = agentBuilder.build();
// 6. 调用大模型LLM
String result = mcpAgent.callMcpTool(userMessage);
log.info("Agent 执行完成,结果长度: {}", result.length());
return result;
} catch (Exception e) {
log.error("Agent 模式执行失败: {}", e.getMessage(), e);
}
return null;
}
/**
* 创建流式聊天模型
* 子类必须实现此方法,返回对应厂商的模型实例
*/
protected abstract StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest);
}

View File

@@ -1,30 +1,21 @@
package org.ruoyi.service.chat.impl.provider;
import dev.langchain4j.agentic.AgenticServices;
import dev.langchain4j.agentic.supervisor.SupervisorAgent;
import dev.langchain4j.agentic.supervisor.SupervisorResponseStrategy;
import dev.langchain4j.community.model.dashscope.QwenChatModel;
import dev.langchain4j.community.model.dashscope.QwenStreamingChatModel;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
import dev.langchain4j.service.tool.ToolProvider;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.agent.McpAgent;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.common.core.utils.SpringUtils;
import org.ruoyi.enums.ChatModeType;
import org.ruoyi.mcp.service.core.ToolProviderFactory;
import org.ruoyi.service.chat.impl.AbstractStreamingChatService;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
/**
* qianWenAI服务调用
@@ -39,9 +30,6 @@ public class QianWenChatServiceImpl extends AbstractStreamingChatService {
// 添加文档解析的前缀字段
private static final String UPLOAD_FILE_API_PREFIX = "fileid";
// 用于线程安全的锁
private final ReentrantLock cacheLock = new ReentrantLock();
@Override
protected StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo,ChatRequest chatRequest) {
return QwenStreamingChatModel.builder()
@@ -91,69 +79,6 @@ public class QianWenChatServiceImpl extends AbstractStreamingChatService {
}).orElse(messagesWithMemory);
}
/**
* 调用MCP服务智能体
* 使用统一的ToolProviderFactory获取所有已配置的工具BUILTIN + MCP
*
* @param userMessage 用户信息
* @param chatModelVo 模型信息
* @return 返回LLM信息
*/
protected String doAgent(String userMessage, ChatModelVo chatModelVo) {
try {
// 步骤1: 获取统一工具提供工厂
ToolProviderFactory toolProviderFactory = SpringUtils.getBean(ToolProviderFactory.class);
// 步骤2: 获取 BUILTIN 工具对象
List<Object> builtinTools = toolProviderFactory.getAllBuiltinToolObjects();
// 步骤3: 获取 MCP 工具提供者
ToolProvider mcpToolProvider = toolProviderFactory.getAllEnabledMcpToolsProvider();
log.info("doAgent: BUILTIN tools count = {}, MCP tools enabled = {}",
builtinTools.size(), mcpToolProvider != null);
// 步骤4: 加载LLM模型
QwenChatModel qwenChatModel = QwenChatModel.builder()
.apiKey(chatModelVo.getApiKey())
.modelName(chatModelVo.getModelName())
.build();
// 步骤5: 创建MCP Agent使用所有已配置的工具
// 使用 .tools() 传入 BUILTIN 工具对象Java 对象,带 @Tool 注解的方法)
// 使用 .toolProvider() 传入 MCP 工具提供者MCP 协议工具)
var agentBuilder = AgenticServices.agentBuilder(McpAgent.class)
.chatModel(qwenChatModel);
// 添加 BUILTIN 工具(如果有)
if (!builtinTools.isEmpty()) {
agentBuilder.tools(builtinTools.toArray(new Object[0]));
log.debug("Added {} BUILTIN tools to agent", builtinTools.size());
}
// 添加 MCP 工具(如果有)
if (mcpToolProvider != null) {
agentBuilder.toolProvider(mcpToolProvider);
log.debug("Added MCP tool provider to agent");
}
McpAgent mcpAgent = agentBuilder.build();
// 步骤6: 创建超级智能体协调MCP Agent
SupervisorAgent supervisor = AgenticServices
.supervisorBuilder()
.chatModel(qwenChatModel)
.subAgents(mcpAgent)
.responseStrategy(SupervisorResponseStrategy.LAST)
.build();
// 步骤7: 调用大模型LLM
return supervisor.invoke(userMessage);
} finally {
cacheLock.unlock();
}
}
@Override
public String getProviderName() {
return ChatModeType.QIAN_WEN.getCode();

View File

@@ -0,0 +1,117 @@
package org.ruoyi.service.mcp;
import org.ruoyi.common.mybatis.core.page.PageQuery;
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
import org.ruoyi.domain.bo.mcp.McpMarketBo;
import org.ruoyi.domain.dto.mcp.McpMarketListResult;
import org.ruoyi.domain.dto.mcp.McpMarketRefreshResult;
import org.ruoyi.domain.dto.mcp.McpMarketToolListResult;
import org.ruoyi.domain.vo.mcp.McpMarketVo;
import java.util.List;
/**
* MCP 市场服务接口
*
* @author ruoyi team
*/
public interface IMcpMarketService {
/**
* 分页查询市场列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 市场分页列表
*/
TableDataInfo<McpMarketVo> selectPageList(McpMarketBo bo, PageQuery pageQuery);
/**
* 查询市场列表(不分页)
*
* @param keyword 关键词
* @param status 状态
* @return 市场列表结果
*/
McpMarketListResult listMarkets(String keyword, String status);
/**
* 查询市场列表(用于导出)
*
* @param bo 查询条件
* @return 市场列表
*/
List<McpMarketVo> queryList(McpMarketBo bo);
/**
* 根据ID查询市场
*
* @param id 市场ID
* @return 市场信息
*/
McpMarketVo selectById(Long id);
/**
* 新增市场
*
* @param bo 市场信息
* @return 新增后的市场ID
*/
String insert(McpMarketBo bo);
/**
* 更新市场
*
* @param bo 市场信息
* @return 结果
*/
String update(McpMarketBo bo);
/**
* 删除市场
*
* @param ids 市场 ID 列表
*/
void deleteByIds(List<Long> ids);
/**
* 更新市场状态
*
* @param id 市场 ID
* @param status 状态
*/
void updateStatus(Long id, String status);
/**
* 获取市场工具列表
*
* @param marketId 市场 ID
* @param page 页码
* @param size 每页大小
* @return 工具列表结果
*/
McpMarketToolListResult getMarketTools(Long marketId, int page, int size);
/**
* 刷新市场工具列表
*
* @param marketId 市场 ID
* @return 刷新结果
*/
McpMarketRefreshResult refreshMarketTools(Long marketId);
/**
* 加载工具到本地
*
* @param toolId 市场工具 ID
*/
void loadToolToLocal(Long toolId);
/**
* 批量加载工具到本地
*
* @param toolIds 工具 ID 列表
* @return 成功加载的数量
*/
int batchLoadTools(List<Long> toolIds);
}

View File

@@ -0,0 +1,92 @@
package org.ruoyi.service.mcp;
import org.ruoyi.common.mybatis.core.page.PageQuery;
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
import org.ruoyi.domain.bo.mcp.McpToolBo;
import org.ruoyi.domain.dto.mcp.McpToolListResult;
import org.ruoyi.domain.dto.mcp.McpToolTestResult;
import org.ruoyi.domain.vo.mcp.McpToolVo;
import java.util.List;
/**
* MCP 工具服务接口
*
* @author ruoyi team
*/
public interface IMcpToolService {
/**
* 分页查询工具列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 工具分页列表
*/
TableDataInfo<McpToolVo> selectPageList(McpToolBo bo, PageQuery pageQuery);
/**
* 查询工具列表(不分页)
*
* @param keyword 关键词
* @param type 类型
* @param status 状态
* @return 工具列表结果
*/
McpToolListResult listTools(String keyword, String type, String status);
/**
* 查询工具列表(用于导出)
*
* @param bo 查询条件
* @return 工具列表
*/
List<McpToolVo> queryList(McpToolBo bo);
/**
* 根据ID查询工具
*
* @param id 工具ID
* @return 工具信息
*/
McpToolVo selectById(Long id);
/**
* 新增工具
*
* @param bo 工具信息
* @return 新增后的工具ID
*/
String insert(McpToolBo bo);
/**
* 更新工具
*
* @param bo 工具信息
* @return 结果
*/
String update(McpToolBo bo);
/**
* 删除工具
*
* @param ids 工具 ID 列表
*/
void deleteByIds(List<Long> ids);
/**
* 更新工具状态
*
* @param id 工具 ID
* @param status 状态
*/
void updateStatus(Long id, String status);
/**
* 测试工具连接
*
* @param id 工具 ID
* @return 测试结果
*/
McpToolTestResult testTool(Long id);
}

View File

@@ -0,0 +1,328 @@
package org.ruoyi.service.mcp.impl;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.core.exception.ServiceException;
import org.ruoyi.common.core.utils.MapstructUtils;
import org.ruoyi.common.mybatis.core.page.PageQuery;
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
import org.ruoyi.domain.bo.mcp.McpMarketBo;
import org.ruoyi.domain.dto.mcp.McpMarketListResult;
import org.ruoyi.domain.dto.mcp.McpMarketRefreshResult;
import org.ruoyi.domain.dto.mcp.McpMarketToolListResult;
import org.ruoyi.domain.entity.mcp.McpMarket;
import org.ruoyi.domain.entity.mcp.McpMarketTool;
import org.ruoyi.domain.entity.mcp.McpTool;
import org.ruoyi.domain.vo.mcp.McpMarketVo;
import org.ruoyi.enums.McpToolStatus;
import org.ruoyi.mapper.mcp.McpMarketMapper;
import org.ruoyi.mapper.mcp.McpMarketToolMapper;
import org.ruoyi.mapper.mcp.McpToolMapper;
import org.ruoyi.service.mcp.IMcpMarketService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* MCP 市场服务实现
*
* @author ruoyi team
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class McpMarketServiceImpl implements IMcpMarketService {
private final McpMarketMapper baseMapper;
private final McpMarketToolMapper mcpMarketToolMapper;
private final McpToolMapper mcpToolMapper;
private final ObjectMapper objectMapper;
@Override
public TableDataInfo<McpMarketVo> selectPageList(McpMarketBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<McpMarket> wrapper = buildQueryWrapper(bo);
Page<McpMarketVo> page = baseMapper.selectVoPage(pageQuery.build(), wrapper);
return TableDataInfo.build(page);
}
@Override
public McpMarketListResult listMarkets(String keyword, String status) {
LambdaQueryWrapper<McpMarket> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(keyword)) {
wrapper.and(w -> w.like(McpMarket::getName, keyword)
.or()
.like(McpMarket::getDescription, keyword));
}
if (StringUtils.hasText(status)) {
wrapper.eq(McpMarket::getStatus, status);
}
wrapper.orderByDesc(McpMarket::getUpdateTime);
List<McpMarket> list = baseMapper.selectList(wrapper);
return McpMarketListResult.of(list);
}
@Override
public List<McpMarketVo> queryList(McpMarketBo bo) {
LambdaQueryWrapper<McpMarket> wrapper = buildQueryWrapper(bo);
return baseMapper.selectVoList(wrapper);
}
@Override
public McpMarketVo selectById(Long id) {
return baseMapper.selectVoById(id);
}
@Override
@Transactional
public String insert(McpMarketBo bo) {
McpMarket market = MapstructUtils.convert(bo, McpMarket.class);
if (market.getStatus() == null) {
market.setStatus(McpToolStatus.ENABLED.getValue());
}
baseMapper.insert(market);
return String.valueOf(market.getId());
}
@Override
@Transactional
public String update(McpMarketBo bo) {
McpMarket market = MapstructUtils.convert(bo, McpMarket.class);
baseMapper.updateById(market);
return String.valueOf(market.getId());
}
@Override
@Transactional
public void deleteByIds(List<Long> ids) {
for (Long id : ids) {
// 先删除关联的市场工具
LambdaQueryWrapper<McpMarketTool> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(McpMarketTool::getMarketId, id);
mcpMarketToolMapper.delete(wrapper);
}
// 删除市场
baseMapper.deleteBatchIds(ids);
}
@Override
@Transactional
public void updateStatus(Long id, String status) {
McpMarket market = new McpMarket();
market.setId(id);
market.setStatus(status);
baseMapper.updateById(market);
}
@Override
public McpMarketToolListResult getMarketTools(Long marketId, int page, int size) {
LambdaQueryWrapper<McpMarketTool> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(McpMarketTool::getMarketId, marketId);
wrapper.orderByDesc(McpMarketTool::getCreateTime);
Page<McpMarketTool> pageResult = mcpMarketToolMapper.selectPage(new Page<>(page, size), wrapper);
return McpMarketToolListResult.of(
pageResult.getRecords(),
pageResult.getTotal(),
(int) pageResult.getCurrent(),
(int) pageResult.getSize()
);
}
@Override
@Transactional
public McpMarketRefreshResult refreshMarketTools(Long marketId) {
McpMarket market = baseMapper.selectById(marketId);
if (market == null) {
throw new ServiceException("市场不存在");
}
int addedCount = 0;
int updatedCount = 0;
try {
// 从市场 URL 获取工具列表使用hutool的HttpUtil
HttpResponse response = HttpRequest.get(market.getUrl())
.timeout(30000) // 30秒超时
.execute();
String responseBody = response.body();
JsonNode rootNode = objectMapper.readTree(responseBody);
// 假设响应格式为 { "data": [...] } 或直接是数组
JsonNode toolsNode = rootNode.has("data") ? rootNode.get("data") : rootNode;
if (toolsNode.isArray()) {
// 获取现有工具
LambdaQueryWrapper<McpMarketTool> existingWrapper = new LambdaQueryWrapper<>();
existingWrapper.eq(McpMarketTool::getMarketId, marketId);
List<McpMarketTool> existingTools = mcpMarketToolMapper.selectList(existingWrapper);
// 创建现有工具的名称到ID映射
Map<String, McpMarketTool> existingToolMap = existingTools.stream()
.collect(Collectors.toMap(McpMarketTool::getToolName, t -> t));
// 处理新工具
for (JsonNode toolNode : toolsNode) {
String toolName = getTextValue(toolNode, "name", "title");
McpMarketTool existingTool = existingToolMap.get(toolName);
if (existingTool != null) {
// 更新现有工具
existingTool.setToolDescription(getTextValue(toolNode, "description", "desc"));
existingTool.setToolVersion(getTextValue(toolNode, "version"));
existingTool.setToolMetadata(toolNode.toString());
mcpMarketToolMapper.updateById(existingTool);
updatedCount++;
} else {
// 插入新工具
McpMarketTool tool = new McpMarketTool();
tool.setMarketId(marketId);
tool.setToolName(toolName);
tool.setToolDescription(getTextValue(toolNode, "description", "desc"));
tool.setToolVersion(getTextValue(toolNode, "version"));
tool.setToolMetadata(toolNode.toString());
tool.setIsLoaded(false);
mcpMarketToolMapper.insert(tool);
addedCount++;
}
}
}
log.info("Successfully refreshed market tools for market: {}, added: {}, updated: {}",
market.getName(), addedCount, updatedCount);
return McpMarketRefreshResult.builder()
.success(true)
.message("刷新成功")
.addedCount(addedCount)
.updatedCount(updatedCount)
.build();
} catch (Exception e) {
log.error("Failed to refresh market tools for market {}: {}", marketId, e.getMessage());
return McpMarketRefreshResult.builder()
.success(false)
.message("刷新市场工具列表失败: " + e.getMessage())
.addedCount(0)
.updatedCount(0)
.build();
}
}
/**
* 从 JSON 节点获取文本值,尝试多个字段名
*/
private String getTextValue(JsonNode node, String... fieldNames) {
for (String fieldName : fieldNames) {
if (node.has(fieldName) && !node.get(fieldName).isNull()) {
return node.get(fieldName).asText();
}
}
return null;
}
@Override
@Transactional
public void loadToolToLocal(Long toolId) {
McpMarketTool marketTool = mcpMarketToolMapper.selectById(toolId);
if (marketTool == null) {
throw new ServiceException("市场工具不存在");
}
if (marketTool.getIsLoaded()) {
throw new ServiceException("工具已加载到本地");
}
try {
// 解析工具元数据
JsonNode metadata = objectMapper.readTree(marketTool.getToolMetadata());
// 创建本地工具
McpTool localTool = new McpTool();
localTool.setName(marketTool.getToolName());
localTool.setDescription(marketTool.getToolDescription());
// 根据元数据判断类型
if (metadata.has("baseUrl") || metadata.has("url")) {
localTool.setType("REMOTE");
String baseUrl = metadata.has("baseUrl") ? metadata.get("baseUrl").asText() :
metadata.has("url") ? metadata.get("url").asText() : null;
localTool.setConfigJson(objectMapper.writeValueAsString(Map.of("baseUrl", baseUrl != null ? baseUrl : "")));
} else {
localTool.setType("LOCAL");
// 构建本地工具配置
Map<String, Object> config = new HashMap<>();
if (metadata.has("command")) {
config.put("command", metadata.get("command").asText());
}
if (metadata.has("args") && metadata.get("args").isArray()) {
config.put("args", objectMapper.convertValue(metadata.get("args"), List.class));
}
if (metadata.has("env") && metadata.get("env").isObject()) {
config.put("env", objectMapper.convertValue(metadata.get("env"), Map.class));
}
// 如果有 npm 包名,使用 npx 启动
if (metadata.has("package") || metadata.has("npmPackage")) {
String packageName = metadata.has("package") ? metadata.get("package").asText() :
metadata.get("npmPackage").asText();
config.put("command", "npx");
config.put("args", List.of("-y", packageName));
}
localTool.setConfigJson(objectMapper.writeValueAsString(config));
}
localTool.setStatus(McpToolStatus.ENABLED.getValue());
mcpToolMapper.insert(localTool);
// 更新市场工具状态
marketTool.setIsLoaded(true);
marketTool.setLocalToolId(localTool.getId());
mcpMarketToolMapper.updateById(marketTool);
log.info("Successfully loaded tool {} to local", marketTool.getToolName());
} catch (Exception e) {
log.error("Failed to load tool to local: {}", e.getMessage());
throw new ServiceException("加载工具到本地失败: " + e.getMessage());
}
}
@Override
@Transactional
public int batchLoadTools(List<Long> toolIds) {
int successCount = 0;
for (Long toolId : toolIds) {
try {
loadToolToLocal(toolId);
successCount++;
} catch (Exception e) {
log.warn("Failed to load tool {}: {}", toolId, e.getMessage());
}
}
return successCount;
}
private LambdaQueryWrapper<McpMarket> buildQueryWrapper(McpMarketBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<McpMarket> wrapper = Wrappers.lambdaQuery();
wrapper.eq(StringUtils.hasText(bo.getStatus()), McpMarket::getStatus, bo.getStatus())
.like(StringUtils.hasText(bo.getName()), McpMarket::getName, bo.getName())
.like(StringUtils.hasText(bo.getDescription()), McpMarket::getDescription, bo.getDescription());
return wrapper;
}
}

View File

@@ -0,0 +1,226 @@
package org.ruoyi.service.mcp.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.core.exception.ServiceException;
import org.ruoyi.common.core.utils.MapstructUtils;
import org.ruoyi.common.mybatis.core.page.PageQuery;
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
import org.ruoyi.domain.bo.mcp.McpToolBo;
import org.ruoyi.domain.dto.mcp.McpToolListResult;
import org.ruoyi.domain.dto.mcp.McpToolTestResult;
import org.ruoyi.domain.entity.mcp.McpTool;
import org.ruoyi.domain.vo.mcp.McpToolVo;
import org.ruoyi.enums.McpToolStatus;
import org.ruoyi.mapper.mcp.McpToolMapper;
import org.ruoyi.service.mcp.IMcpToolService;
import org.ruoyi.mcp.service.core.BuiltinToolRegistry;
import org.ruoyi.mcp.service.core.LangChain4jMcpToolProviderService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Map;
/**
* MCP 工具服务实现
*
* @author ruoyi team
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class McpToolServiceImpl implements IMcpToolService {
private final McpToolMapper baseMapper;
private final LangChain4jMcpToolProviderService langChain4jMcpToolProviderService;
private final BuiltinToolRegistry builtinToolRegistry;
@Override
public TableDataInfo<McpToolVo> selectPageList(McpToolBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<McpTool> wrapper = buildQueryWrapper(bo);
Page<McpToolVo> page = baseMapper.selectVoPage(pageQuery.build(), wrapper);
return TableDataInfo.build(page);
}
@Override
public McpToolListResult listTools(String keyword, String type, String status) {
LambdaQueryWrapper<McpTool> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(keyword)) {
wrapper.and(w -> w.like(McpTool::getName, keyword)
.or()
.like(McpTool::getDescription, keyword));
}
if (StringUtils.hasText(type)) {
wrapper.eq(McpTool::getType, type);
}
if (StringUtils.hasText(status)) {
wrapper.eq(McpTool::getStatus, status);
}
wrapper.orderByDesc(McpTool::getUpdateTime);
List<McpTool> list = baseMapper.selectList(wrapper);
return McpToolListResult.of(list);
}
@Override
public List<McpToolVo> queryList(McpToolBo bo) {
LambdaQueryWrapper<McpTool> wrapper = buildQueryWrapper(bo);
return baseMapper.selectVoList(wrapper);
}
@Override
public McpToolVo selectById(Long id) {
return baseMapper.selectVoById(id);
}
@Override
@Transactional
public String insert(McpToolBo bo) {
McpTool tool = MapstructUtils.convert(bo, McpTool.class);
if (tool.getStatus() == null) {
tool.setStatus(McpToolStatus.ENABLED.getValue());
}
if (tool.getType() == null) {
tool.setType("LOCAL");
}
baseMapper.insert(tool);
return String.valueOf(tool.getId());
}
@Override
@Transactional
public String update(McpToolBo bo) {
McpTool existingTool = baseMapper.selectById(bo.getId());
if (existingTool != null && BuiltinToolRegistry.TYPE_BUILTIN.equals(existingTool.getType())) {
throw new ServiceException("内置工具不允许编辑");
}
McpTool tool = MapstructUtils.convert(bo, McpTool.class);
baseMapper.updateById(tool);
// 如果工具正在使用中,需要刷新连接
langChain4jMcpToolProviderService.refreshClient(bo.getId());
return String.valueOf(tool.getId());
}
@Override
@Transactional
public void deleteByIds(List<Long> ids) {
// 过滤掉内置工具
List<Long> deletableIds = ids.stream()
.filter(id -> {
McpTool tool = baseMapper.selectById(id);
return tool == null || !BuiltinToolRegistry.TYPE_BUILTIN.equals(tool.getType());
})
.toList();
if (deletableIds.isEmpty()) {
throw new ServiceException("所选工具均为内置工具,不允许删除");
}
// 刷新连接LangChain4j会自动处理
deletableIds.forEach(id -> langChain4jMcpToolProviderService.refreshClient(id));
baseMapper.deleteBatchIds(deletableIds);
}
@Override
@Transactional
public void updateStatus(Long id, String status) {
McpTool tool = new McpTool();
tool.setId(id);
tool.setStatus(status);
baseMapper.updateById(tool);
// 刷新连接
langChain4jMcpToolProviderService.refreshClient(id);
}
@Override
public McpToolTestResult testTool(Long id) {
McpTool tool = baseMapper.selectById(id);
if (tool == null) {
return McpToolTestResult.fail("工具不存在");
}
// 根据工具类型选择不同的测试逻辑
if (BuiltinToolRegistry.TYPE_BUILTIN.equals(tool.getType())) {
// 内置工具 - 直接验证是否在注册表中
return testBuiltinTool(tool);
} else {
// MCP 工具 (LOCAL/REMOTE) - 测试连接
return testMcpTool(tool);
}
}
/**
* 测试内置工具
* 内置工具不需要网络连接,只需验证是否在注册表中
*
* @param tool 工具信息
* @return 测试结果
*/
private McpToolTestResult testBuiltinTool(McpTool tool) {
try {
boolean isRegistered = builtinToolRegistry.hasTool(tool.getName());
if (isRegistered) {
return McpToolTestResult.success(
String.format("内置工具 [%s] 已注册,可正常使用", tool.getName()),
1,
List.of(tool.getName())
);
} else {
return McpToolTestResult.fail(
String.format("内置工具 [%s] 未在注册表中找到,请检查工具名称是否正确", tool.getName())
);
}
} catch (Exception e) {
log.error("测试内置工具失败: {} - {}", tool.getName(), e.getMessage());
return McpToolTestResult.fail("测试失败: " + e.getMessage());
}
}
/**
* 测试MCP工具连接
*
* @param tool 工具信息
* @return 测试结果
*/
private McpToolTestResult testMcpTool(McpTool tool) {
try {
boolean isHealthy = langChain4jMcpToolProviderService.checkToolHealth(tool.getId());
if (isHealthy) {
return McpToolTestResult.success(
String.format("MCP工具 [%s] 连接测试成功", tool.getName()),
1,
List.of(tool.getName())
);
} else {
return McpToolTestResult.fail(
String.format("MCP工具 [%s] 连接测试失败", tool.getName())
);
}
} catch (Exception e) {
log.error("测试MCP工具失败: {} - {}", tool.getName(), e.getMessage());
return McpToolTestResult.fail("测试失败: " + e.getMessage());
}
}
private LambdaQueryWrapper<McpTool> buildQueryWrapper(McpToolBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<McpTool> wrapper = Wrappers.lambdaQuery();
wrapper.eq(StringUtils.hasText(bo.getType()), McpTool::getType, bo.getType())
.eq(StringUtils.hasText(bo.getStatus()), McpTool::getStatus, bo.getStatus())
.like(StringUtils.hasText(bo.getName()), McpTool::getName, bo.getName())
.like(StringUtils.hasText(bo.getDescription()), McpTool::getDescription, bo.getDescription());
return wrapper;
}
}