From 593a0d0049d8e4eaf3ce140ba37aa8df5f0c4eff Mon Sep 17 00:00:00 2001 From: evo <446796145@qq.com> Date: Mon, 23 Feb 2026 16:07:13 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E5=A2=9E=E5=8A=A0mcp=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/script/sql/ruoyi-ai-v3_mysql8.sql | 159 +++++++ pom.xml | 16 + ruoyi-admin/pom.xml | 6 + ruoyi-common/ruoyi-common-excel/pom.xml | 6 + .../common/sse/utils/SseMessageUtils.java | 3 - ruoyi-modules/pom.xml | 1 + .../workflow/workflow/WorkflowStarter.java | 1 - .../workflow/node/AbstractWfNode.java | 1 - .../KeywordExtractorNode.java | 1 - ruoyi-modules/ruoyi-chat/pom.xml | 5 + .../ruoyi/agent/tool/QueryAllTablesTool.java | 3 - .../knowledge/KnowledgeGraphInstanceVo.java | 2 - .../vo/knowledge/KnowledgeGraphSegmentVo.java | 2 - .../impl/provider/QianWenChatServiceImpl.java | 76 ++- .../ruoyi/generator/util/VelocityUtils.java | 2 - ruoyi-modules/ruoyi-mcp/pom.xml | 77 +++ .../org/ruoyi/mcp/config/McpProperties.java | 40 ++ .../mcp/config/SystemToolInitializer.java | 93 ++++ .../mcp/controller/McpMarketController.java | 171 +++++++ .../mcp/controller/McpToolController.java | 136 ++++++ .../org/ruoyi/mcp/domain/bo/McpMarketBo.java | 55 +++ .../org/ruoyi/mcp/domain/bo/McpToolBo.java | 59 +++ .../mcp/domain/dto/McpMarketListResult.java | 44 ++ .../domain/dto/McpMarketRefreshResult.java | 38 ++ .../domain/dto/McpMarketToolListResult.java | 63 +++ .../mcp/domain/dto/McpToolListResult.java | 44 ++ .../mcp/domain/dto/McpToolTestResult.java | 56 +++ .../ruoyi/mcp/domain/entity/McpMarket.java | 51 ++ .../mcp/domain/entity/McpMarketTool.java | 61 +++ .../org/ruoyi/mcp/domain/entity/McpTool.java | 55 +++ .../org/ruoyi/mcp/domain/vo/McpMarketVo.java | 74 +++ .../org/ruoyi/mcp/domain/vo/McpToolVo.java | 74 +++ .../org/ruoyi/mcp/enums/McpToolStatus.java | 47 ++ .../org/ruoyi/mcp/mapper/McpMarketMapper.java | 15 + .../ruoyi/mcp/mapper/McpMarketToolMapper.java | 14 + .../org/ruoyi/mcp/mapper/McpToolMapper.java | 15 + .../ruoyi/mcp/service/IMcpMarketService.java | 117 +++++ .../ruoyi/mcp/service/IMcpToolService.java | 92 ++++ .../service/core/BuiltinToolDefinition.java | 13 + .../mcp/service/core/BuiltinToolProvider.java | 55 +++ .../mcp/service/core/BuiltinToolRegistry.java | 129 +++++ .../LangChain4jMcpToolProviderService.java | 445 ++++++++++++++++++ .../mcp/service/core/ToolProviderFactory.java | 171 +++++++ .../service/impl/McpMarketServiceImpl.java | 328 +++++++++++++ .../mcp/service/impl/McpToolServiceImpl.java | 226 +++++++++ .../org/ruoyi/mcp/tools/EditFileTool.java | 151 ++++++ .../ruoyi/mcp/tools/ListDirectoryTool.java | 285 +++++++++++ .../org/ruoyi/mcp/tools/ReadFileTool.java | 121 +++++ 48 files changed, 3643 insertions(+), 56 deletions(-) create mode 100644 ruoyi-modules/ruoyi-mcp/pom.xml create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/config/McpProperties.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/config/SystemToolInitializer.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/controller/McpMarketController.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/controller/McpToolController.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/bo/McpMarketBo.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/bo/McpToolBo.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketListResult.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketRefreshResult.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketToolListResult.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpToolListResult.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpToolTestResult.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpMarket.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpMarketTool.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpTool.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/vo/McpMarketVo.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/vo/McpToolVo.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/enums/McpToolStatus.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpMarketMapper.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpMarketToolMapper.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpToolMapper.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/IMcpMarketService.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/IMcpToolService.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolDefinition.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolProvider.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolRegistry.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/LangChain4jMcpToolProviderService.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/ToolProviderFactory.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/impl/McpMarketServiceImpl.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/impl/McpToolServiceImpl.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/EditFileTool.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/ListDirectoryTool.java create mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/ReadFileTool.java diff --git a/docs/script/sql/ruoyi-ai-v3_mysql8.sql b/docs/script/sql/ruoyi-ai-v3_mysql8.sql index e7aec981..b1528ce2 100644 --- a/docs/script/sql/ruoyi-ai-v3_mysql8.sql +++ b/docs/script/sql/ruoyi-ai-v3_mysql8.sql @@ -3647,3 +3647,162 @@ INSERT INTO `test_tree` VALUES (12, '000000', 10, 108, 3, '子节点88', 0, 103, INSERT INTO `test_tree` VALUES (13, '000000', 10, 108, 3, '子节点99', 0, 103, '2026-02-03 05:14:54', 1, NULL, NULL, 0); SET FOREIGN_KEY_CHECKS = 1; + + +-- MCP 模块数据库表结构 +-- 版本: V3.0.0 +-- 描述: MCP 工具管理和 MCP 市场管理表 + +-- ---------------------------- +-- MCP 工具表 +-- ---------------------------- +DROP TABLE IF EXISTS `mcp_tool_info`; +CREATE TABLE `mcp_tool_info` +( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '工具ID', + `name` varchar(200) NOT NULL COMMENT '工具名称', + `description` text COMMENT '工具描述', + `type` varchar(20) DEFAULT 'LOCAL' COMMENT '工具类型:LOCAL-本地, REMOTE-远程, BUILTIN-内置', + `status` varchar(20) DEFAULT 'ENABLED' COMMENT '状态:ENABLED-启用, DISABLED-禁用', + `config_json` text COMMENT '配置信息(JSON格式)', + `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `create_dept` bigint DEFAULT NULL COMMENT '创建部门', + `create_by` bigint DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` bigint DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)', + PRIMARY KEY (`id`), + KEY `idx_name` (`name`), + KEY `idx_type` (`type`), + KEY `idx_status` (`status`), + KEY `idx_tenant_id` (`tenant_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='MCP工具表'; + +-- ---------------------------- +-- MCP 市场表 +-- ---------------------------- +DROP TABLE IF EXISTS `mcp_market_info`; +CREATE TABLE `mcp_market_info` +( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '市场ID', + `name` varchar(200) NOT NULL COMMENT '市场名称', + `url` varchar(500) NOT NULL COMMENT '市场URL', + `description` text COMMENT '市场描述', + `auth_config` text COMMENT '认证配置(JSON格式)', + `status` varchar(20) DEFAULT 'ENABLED' COMMENT '状态:ENABLED-启用, DISABLED-禁用', + `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `create_dept` bigint DEFAULT NULL COMMENT '创建部门', + `create_by` bigint DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` bigint DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)', + PRIMARY KEY (`id`), + KEY `idx_name` (`name`), + KEY `idx_status` (`status`), + KEY `idx_tenant_id` (`tenant_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='MCP市场表'; + +-- ---------------------------- +-- MCP 市场工具关联表 +-- ---------------------------- +DROP TABLE IF EXISTS `mcp_market_tool`; +CREATE TABLE `mcp_market_tool` +( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID', + `market_id` bigint NOT NULL COMMENT '市场ID', + `tool_name` varchar(200) NOT NULL COMMENT '工具名称', + `tool_description` text COMMENT '工具描述', + `tool_version` varchar(50) COMMENT '工具版本', + `tool_metadata` json COMMENT '工具元数据(JSON格式)', + `is_loaded` tinyint(1) DEFAULT 0 COMMENT '是否已加载到本地', + `local_tool_id` bigint COMMENT '关联的本地工具ID', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`id`), + KEY `idx_market_id` (`market_id`), + KEY `idx_tool_name` (`tool_name`), + KEY `idx_is_loaded` (`is_loaded`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='MCP市场工具关联表'; + + + +-- MCP 模块菜单权限 SQL +-- 版本: V3.0.1 +-- 描述: MCP 工具管理和 MCP 市场管理菜单权限 +-- 菜单 ID 规划: 2000-2199 + +-- ---------------------------- +-- MCP 主菜单 +-- ---------------------------- +INSERT INTO sys_menu +VALUES (2000, 'MCP管理', 0, 5, 'mcp', '', '', 1, 0, 'M', '0', '0', '', + 'mdi:robot-industrial', 103, 1, NOW(), NULL, NULL, 'MCP模块管理菜单'); + +-- ---------------------------- +-- MCP 工具管理 +-- ---------------------------- +INSERT INTO sys_menu +VALUES (2001, 'MCP工具管理', 2000, 1, 'tool', 'mcp/tool/index', '', 1, 0, 'C', '0', + '0', 'mcp:tool:list', 'material-symbols:tools-hammer-outline', 103, 1, NOW(), NULL, + NULL, 'MCP工具管理菜单'); + +-- MCP 工具管理按钮权限 +INSERT INTO sys_menu +VALUES (2002, 'MCP工具查询', 2001, 1, '#', '', '', 1, 0, 'F', '0', '0', + 'mcp:tool:query', '#', 103, 1, NOW(), NULL, NULL, ''); +INSERT INTO sys_menu +VALUES (2003, 'MCP工具新增', 2001, 2, '#', '', '', 1, 0, 'F', '0', '0', + 'mcp:tool:add', '#', 103, 1, NOW(), NULL, NULL, ''); +INSERT INTO sys_menu +VALUES (2004, 'MCP工具修改', 2001, 3, '#', '', '', 1, 0, 'F', '0', '0', + 'mcp:tool:edit', '#', 103, 1, NOW(), NULL, NULL, ''); +INSERT INTO sys_menu +VALUES (2005, 'MCP工具删除', 2001, 4, '#', '', '', 1, 0, 'F', '0', '0', + 'mcp:tool:remove', '#', 103, 1, NOW(), NULL, NULL, ''); +INSERT INTO sys_menu +VALUES (2006, 'MCP工具测试', 2001, 5, '#', '', '', 1, 0, 'F', '0', '0', + 'mcp:tool:test', '#', 103, 1, NOW(), NULL, NULL, ''); +INSERT INTO sys_menu +VALUES (2007, 'MCP工具导出', 2001, 6, '#', '', '', 1, 0, 'F', '0', '0', + 'mcp:tool:export', '#', 103, 1, NOW(), NULL, NULL, ''); + +-- ---------------------------- +-- MCP 市场管理 +-- ---------------------------- +INSERT INTO sys_menu +VALUES (2010, 'MCP市场管理', 2000, 2, 'market', 'mcp/market/index', '', 1, 0, 'C', '0', + '0', 'mcp:market:list', 'mdi:storefront-outline', 103, 1, NOW(), NULL, NULL, + 'MCP市场管理菜单'); + +-- MCP 市场管理按钮权限 +INSERT INTO sys_menu +VALUES (2011, 'MCP市场查询', 2010, 1, '#', '', '', 1, 0, 'F', '0', '0', + 'mcp:market:query', '#', 103, 1, NOW(), NULL, NULL, ''); +INSERT INTO sys_menu +VALUES (2012, 'MCP市场新增', 2010, 2, '#', '', '', 1, 0, 'F', '0', '0', + 'mcp:market:add', '#', 103, 1, NOW(), NULL, NULL, ''); +INSERT INTO sys_menu +VALUES (2013, 'MCP市场修改', 2010, 3, '#', '', '', 1, 0, 'F', '0', '0', + 'mcp:market:edit', '#', 103, 1, NOW(), NULL, NULL, ''); +INSERT INTO sys_menu +VALUES (2014, 'MCP市场删除', 2010, 4, '#', '', '', 1, 0, 'F', '0', '0', + 'mcp:market:remove', '#', 103, 1, NOW(), NULL, NULL, ''); +INSERT INTO sys_menu +VALUES (2015, 'MCP市场刷新', 2010, 5, '#', '', '', 1, 0, 'F', '0', '0', + 'mcp:market:refresh', '#', 103, 1, NOW(), NULL, NULL, ''); +INSERT INTO sys_menu +VALUES (2016, 'MCP工具加载', 2010, 6, '#', '', '', 1, 0, 'F', '0', '0', + 'mcp:market:load', '#', 103, 1, NOW(), NULL, NULL, ''); +INSERT INTO sys_menu +VALUES (2017, 'MCP市场导出', 2010, 7, '#', '', '', 1, 0, 'F', '0', '0', + 'mcp:market:export', '#', 103, 1, NOW(), NULL, NULL, ''); + +-- ---------------------------- +-- MCP 配置管理 (可选,预留扩展) +-- ---------------------------- +-- INSERT INTO sys_menu VALUES (2020, 'MCP配置管理', 2000, 3, 'config', 'mcp/config/index', '', 1, 0, 'C', '0', +-- '0', 'mcp:config:list', 'ant-design:setting-outlined', 103, 1, NOW(), NULL, NULL, +-- 'MCP配置管理菜单'); + + diff --git a/pom.xml b/pom.xml index 2e5a27ab..e10bf329 100644 --- a/pom.xml +++ b/pom.xml @@ -59,6 +59,8 @@ 1.5.3 1.19.6 1.0.7 + + 1.27.1 1.1.0 @@ -402,6 +404,13 @@ ${revision} + + + org.ruoyi + ruoyi-mcp + ${revision} + + com.github.binarywang @@ -416,6 +425,13 @@ ${jackson-dataformat-xml.version} + + + org.apache.commons + commons-compress + ${commons-compress.version} + + diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml index 28ce987a..1ee16ddb 100644 --- a/ruoyi-admin/pom.xml +++ b/ruoyi-admin/pom.xml @@ -116,6 +116,12 @@ ruoyi-aiflow + + + org.ruoyi + ruoyi-mcp + + de.codecentric spring-boot-admin-starter-client diff --git a/ruoyi-common/ruoyi-common-excel/pom.xml b/ruoyi-common/ruoyi-common-excel/pom.xml index 51a34a08..e48189f9 100644 --- a/ruoyi-common/ruoyi-common-excel/pom.xml +++ b/ruoyi-common/ruoyi-common-excel/pom.xml @@ -25,6 +25,12 @@ cn.idev.excel fastexcel + + + + org.apache.commons + commons-compress + diff --git a/ruoyi-common/ruoyi-common-sse/src/main/java/org/ruoyi/common/sse/utils/SseMessageUtils.java b/ruoyi-common/ruoyi-common-sse/src/main/java/org/ruoyi/common/sse/utils/SseMessageUtils.java index ec4e95ca..b4c224d5 100644 --- a/ruoyi-common/ruoyi-common-sse/src/main/java/org/ruoyi/common/sse/utils/SseMessageUtils.java +++ b/ruoyi-common/ruoyi-common-sse/src/main/java/org/ruoyi/common/sse/utils/SseMessageUtils.java @@ -6,9 +6,6 @@ import lombok.extern.slf4j.Slf4j; import org.ruoyi.common.core.utils.SpringUtils; import org.ruoyi.common.sse.core.SseEmitterManager; import org.ruoyi.common.sse.dto.SseMessageDto; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; - -import java.io.IOException; /** * SSE工具类 diff --git a/ruoyi-modules/pom.xml b/ruoyi-modules/pom.xml index 17b9a97e..67d9ecfd 100644 --- a/ruoyi-modules/pom.xml +++ b/ruoyi-modules/pom.xml @@ -16,6 +16,7 @@ ruoyi-demo ruoyi-generator ruoyi-job + ruoyi-mcp ruoyi-system ruoyi-wechat ruoyi-workflow diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowStarter.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowStarter.java index 7865af51..4cb377a1 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowStarter.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowStarter.java @@ -17,7 +17,6 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.util.List; -import static org.ruoyi.workflow.cosntant.AdiConstant.SSE_TIMEOUT; import static org.ruoyi.workflow.enums.ErrorEnum.*; @Slf4j diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/AbstractWfNode.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/AbstractWfNode.java index d3262971..e0cef0fd 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/AbstractWfNode.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/AbstractWfNode.java @@ -6,7 +6,6 @@ import lombok.Data; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.SerializationUtils; -import org.apache.commons.lang3.StringUtils; import org.ruoyi.common.core.exception.base.BaseException; import org.ruoyi.workflow.base.NodeInputConfigTypeHandler; import org.ruoyi.workflow.entity.WorkflowComponent; diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/keywordExtractor/KeywordExtractorNode.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/keywordExtractor/KeywordExtractorNode.java index 5ad7147a..b3f85796 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/keywordExtractor/KeywordExtractorNode.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/keywordExtractor/KeywordExtractorNode.java @@ -1,7 +1,6 @@ package org.ruoyi.workflow.workflow.node.keywordExtractor; import dev.langchain4j.data.message.SystemMessage; -import dev.langchain4j.data.message.UserMessage; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.ruoyi.workflow.entity.WorkflowComponent; diff --git a/ruoyi-modules/ruoyi-chat/pom.xml b/ruoyi-modules/ruoyi-chat/pom.xml index ae85abd9..26c66ef5 100644 --- a/ruoyi-modules/ruoyi-chat/pom.xml +++ b/ruoyi-modules/ruoyi-chat/pom.xml @@ -19,6 +19,11 @@ ruoyi-common-chat + + org.ruoyi + ruoyi-mcp + + org.ruoyi ruoyi-common-sensitive diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/QueryAllTablesTool.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/QueryAllTablesTool.java index 9e490dc3..81623dca 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/QueryAllTablesTool.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/QueryAllTablesTool.java @@ -1,10 +1,7 @@ package org.ruoyi.agent.tool; -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; import dev.langchain4j.agent.tool.Tool; import lombok.extern.slf4j.Slf4j; -import org.ruoyi.agent.config.AgentMysqlProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/knowledge/KnowledgeGraphInstanceVo.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/knowledge/KnowledgeGraphInstanceVo.java index 25045920..f358b6f5 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/knowledge/KnowledgeGraphInstanceVo.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/knowledge/KnowledgeGraphInstanceVo.java @@ -10,8 +10,6 @@ import org.ruoyi.domain.entity.knowledge.KnowledgeGraphInstance; import java.io.Serial; import java.io.Serializable; -import java.util.Date; - /** diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/knowledge/KnowledgeGraphSegmentVo.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/knowledge/KnowledgeGraphSegmentVo.java index 78973e84..1d534e38 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/knowledge/KnowledgeGraphSegmentVo.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/knowledge/KnowledgeGraphSegmentVo.java @@ -10,8 +10,6 @@ import org.ruoyi.domain.entity.knowledge.KnowledgeGraphSegment; import java.io.Serial; import java.io.Serializable; -import java.util.Date; - /** diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/QianWenChatServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/QianWenChatServiceImpl.java index ab583520..26c1858c 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/QianWenChatServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/QianWenChatServiceImpl.java @@ -1,6 +1,5 @@ package org.ruoyi.service.chat.impl.provider; -import dev.langchain4j.agent.tool.ToolSpecification; import dev.langchain4j.agentic.AgenticServices; import dev.langchain4j.agentic.supervisor.SupervisorAgent; import dev.langchain4j.agentic.supervisor.SupervisorResponseStrategy; @@ -9,20 +8,15 @@ 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.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.http.StreamableHttpMcpTransport; 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.config.McpSseConfig; import org.ruoyi.enums.ChatModeType; +import org.ruoyi.mcp.service.core.ToolProviderFactory; import org.ruoyi.service.chat.impl.AbstractStreamingChatService; -import org.springframework.beans.factory.annotation.Autowired; +import org.ruoyi.common.core.utils.SpringUtils; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import org.ruoyi.common.chat.domain.dto.request.ChatRequest; @@ -41,9 +35,6 @@ import java.util.List; @Slf4j public class QianWenChatServiceImpl extends AbstractStreamingChatService { - @Autowired - private McpSseConfig mcpSseConfig; - /** * 千问开发者默认地址 */ @@ -103,50 +94,53 @@ public class QianWenChatServiceImpl extends AbstractStreamingChatService { /** * 调用MCP服务(智能体) + * 使用统一的ToolProviderFactory获取所有已配置的工具(BUILTIN + MCP) + * * @param userMessage 用户信息 * @param chatModelVo 模型信息 * @return 返回LLM信息 */ - protected String doAgent(String userMessage,ChatModelVo chatModelVo) { - // 判断是否开启MCP服务 - if (!mcpSseConfig.isEnabled()) { - return ""; - } + protected String doAgent(String userMessage, ChatModelVo chatModelVo) { + // 步骤1: 获取统一工具提供工厂 + ToolProviderFactory toolProviderFactory = SpringUtils.getBean(ToolProviderFactory.class); - // 步骤1:根据SSE对外暴露端点连接 - McpTransport httpMcpTransport = new StreamableHttpMcpTransport.Builder(). - url(mcpSseConfig.getUrl()). - logRequests(true). - build(); + // 步骤2: 获取 BUILTIN 工具对象 + List builtinTools = toolProviderFactory.getAllBuiltinToolObjects(); - // 步骤2:开启客户端连接 - McpClient mcpClient = new DefaultMcpClient.Builder() - .transport(httpMcpTransport) - .build(); + // 步骤3: 获取 MCP 工具提供者 + ToolProvider mcpToolProvider = toolProviderFactory.getAllEnabledMcpToolsProvider(); - // 获取所有mcp工具 - List toolSpecifications = mcpClient.listTools(); - System.out.println(toolSpecifications); + log.info("doAgent: BUILTIN tools count = {}, MCP tools enabled = {}", + builtinTools.size(), mcpToolProvider != null); - // 步骤3:将mcp对象包装 - ToolProvider toolProvider = McpToolProvider.builder() - .mcpClients(List.of(mcpClient)) - .build(); - - // 步骤4:加载LLM模型对话 + // 步骤4: 加载LLM模型 QwenChatModel qwenChatModel = QwenChatModel.builder() .baseUrl(QWEN_API_HOST) .apiKey(chatModelVo.getApiKey()) .modelName(chatModelVo.getModelName()) .build(); - // 步骤5:将MCP对象由智能体Agent管控 - McpAgent mcpAgent = AgenticServices.agentBuilder(McpAgent.class) - .chatModel(qwenChatModel) - .toolProvider(toolProvider) - .build(); + // 步骤5: 创建MCP Agent,使用所有已配置的工具 + // 使用 .tools() 传入 BUILTIN 工具对象(Java 对象,带 @Tool 注解的方法) + // 使用 .toolProvider() 传入 MCP 工具提供者(MCP 协议工具) + var agentBuilder = AgenticServices.agentBuilder(McpAgent.class) + .chatModel(qwenChatModel); - // 步骤6:将所有MCP对象由超级智能体管控 + // 添加 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) @@ -154,7 +148,7 @@ public class QianWenChatServiceImpl extends AbstractStreamingChatService { .responseStrategy(SupervisorResponseStrategy.LAST) .build(); - // 步骤7:调用大模型LLM + // 步骤7: 调用大模型LLM return supervisor.invoke(userMessage); } diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/util/VelocityUtils.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/util/VelocityUtils.java index 9b591bfc..cb78b33d 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/util/VelocityUtils.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/util/VelocityUtils.java @@ -12,8 +12,6 @@ import org.ruoyi.common.json.utils.JsonUtils; import org.ruoyi.common.mybatis.helper.DataBaseHelper; import org.ruoyi.generator.domain.GenTable; import org.ruoyi.generator.domain.GenTableColumn; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; import org.apache.velocity.VelocityContext; import java.util.*; diff --git a/ruoyi-modules/ruoyi-mcp/pom.xml b/ruoyi-modules/ruoyi-mcp/pom.xml new file mode 100644 index 00000000..e5dfa27a --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/pom.xml @@ -0,0 +1,77 @@ + + + 4.0.0 + + + org.ruoyi + ruoyi-modules + ${revision} + + + ruoyi-mcp + + + MCP模块 - 管理MCP工具连接、市场集成和内置工具 + + + + + + org.ruoyi + ruoyi-common-core + + + + org.ruoyi + ruoyi-common-web + + + + org.ruoyi + ruoyi-common-mybatis + + + + org.ruoyi + ruoyi-common-log + + + + org.ruoyi + ruoyi-common-tenant + + + + org.ruoyi + ruoyi-common-security + + + + org.ruoyi + ruoyi-common-excel + + + + org.ruoyi + ruoyi-common-idempotent + + + + + dev.langchain4j + langchain4j-mcp + ${langchain4j.community.version} + + + + + com.fasterxml.jackson.core + jackson-databind + + + + + diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/config/McpProperties.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/config/McpProperties.java new file mode 100644 index 00000000..e686c9a2 --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/config/McpProperties.java @@ -0,0 +1,40 @@ +package org.ruoyi.mcp.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * MCP 配置属性 + * + * @author ruoyi team + */ +@Data +@Component +@ConfigurationProperties(prefix = "app.mcp") +public class McpProperties { + + /** + * 客户端配置 + */ + private ClientConfig client = new ClientConfig(); + + + @Data + public static class ClientConfig { + /** + * 请求超时时间(秒) + */ + private int requestTimeout = 30; + + /** + * 连接超时时间(秒) + */ + private int connectionTimeout = 10; + + /** + * 最大重试次数 + */ + private int maxRetries = 3; + } +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/config/SystemToolInitializer.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/config/SystemToolInitializer.java new file mode 100644 index 00000000..30647c65 --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/config/SystemToolInitializer.java @@ -0,0 +1,93 @@ +package org.ruoyi.mcp.config; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.ruoyi.mcp.domain.entity.McpTool; +import org.ruoyi.mcp.enums.McpToolStatus; +import org.ruoyi.mcp.mapper.McpToolMapper; +import org.ruoyi.mcp.service.core.BuiltinToolDefinition; +import org.ruoyi.mcp.service.core.BuiltinToolRegistry; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +/** + * 系统工具初始化器 + * 在应用启动时,将系统内置工具同步到数据库 + * 这样可以统一管理所有工具,支持动态启用/禁用 + * + * @author ruoyi team + */ +@Slf4j +@Component +@Order(999) // 确保在其他初始化器之后执行 +@RequiredArgsConstructor +public class SystemToolInitializer implements ApplicationRunner { + + private final McpToolMapper mcpToolMapper; + private final BuiltinToolRegistry builtinToolRegistry; + + @Override + @Transactional + public void run(ApplicationArguments args) { + log.info("开始同步系统内置工具到数据库..."); + + int addedCount = 0; + int existingCount = 0; + + for (BuiltinToolDefinition tool : builtinToolRegistry.getAllBuiltinTools()) { + try { + boolean added = syncBuiltinTool(tool); + if (added) { + addedCount++; + } else { + existingCount++; + } + } catch (Exception e) { + log.error("同步内置工具失败: {}", tool.name(), e); + } + } + + log.info("系统内置工具同步完成: 新增 {} 个, 已存在 {} 个", addedCount, existingCount); + } + + /** + * 同步单个内置工具到数据库 + * + * @param tool 工具定义 + * @return 是否新增(true=新增, false=已存在) + */ + private boolean syncBuiltinTool(BuiltinToolDefinition tool) { + // 检查是否已存在 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(McpTool::getName, tool.name()) + .eq(McpTool::getType, BuiltinToolRegistry.TYPE_BUILTIN); + + McpTool existing = mcpToolMapper.selectOne(wrapper); + + if (existing != null) { + // 已存在,更新描述信息(保留状态不变) + if (!tool.description().equals(existing.getDescription())) { + existing.setDescription(tool.description()); + mcpToolMapper.updateById(existing); + log.debug("更新内置工具描述: {}", tool.name()); + } + return false; + } + + // 新增 + McpTool newTool = new McpTool(); + newTool.setName(tool.name()); + newTool.setDescription(tool.description()); + newTool.setType(BuiltinToolRegistry.TYPE_BUILTIN); + newTool.setStatus(McpToolStatus.ENABLED.getValue()); // 默认启用 + newTool.setConfigJson(null); // 内置工具不需要配置 + mcpToolMapper.insert(newTool); + + log.info("新增内置工具: {} ({})", tool.name(), tool.displayName()); + return true; + } +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/controller/McpMarketController.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/controller/McpMarketController.java new file mode 100644 index 00000000..52a0eacc --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/controller/McpMarketController.java @@ -0,0 +1,171 @@ +package org.ruoyi.mcp.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.ruoyi.common.core.domain.R; +import org.ruoyi.common.excel.utils.ExcelUtil; +import org.ruoyi.common.idempotent.annotation.RepeatSubmit; +import org.ruoyi.common.log.annotation.Log; +import org.ruoyi.common.log.enums.BusinessType; +import org.ruoyi.common.mybatis.core.page.PageQuery; +import org.ruoyi.common.mybatis.core.page.TableDataInfo; +import org.ruoyi.common.web.core.BaseController; +import org.ruoyi.mcp.domain.bo.McpMarketBo; +import org.ruoyi.mcp.domain.dto.McpMarketListResult; +import org.ruoyi.mcp.domain.dto.McpMarketToolListResult; +import org.ruoyi.mcp.domain.vo.McpMarketVo; +import org.ruoyi.mcp.service.IMcpMarketService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * MCP 市场管理 Controller + * + * @author ruoyi team + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/mcp/market") +public class McpMarketController extends BaseController { + + private final IMcpMarketService mcpMarketService; + + /** + * 查询市场列表 + */ + @SaCheckPermission("mcp:market:list") + @GetMapping("/list") + public TableDataInfo list(McpMarketBo bo, PageQuery pageQuery) { + return mcpMarketService.selectPageList(bo, pageQuery); + } + + /** + * 查询市场列表(不分页) + */ + @SaCheckPermission("mcp:market:list") + @GetMapping("/all") + public McpMarketListResult listAll( + @RequestParam(required = false) String keyword, + @RequestParam(required = false) String status) { + return mcpMarketService.listMarkets(keyword, status); + } + + /** + * 导出 MCP 市场列表 + */ + @SaCheckPermission("mcp:market:export") + @Log(title = "MCP市场管理", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(McpMarketBo bo, HttpServletResponse response) { + List list = mcpMarketService.queryList(bo); + ExcelUtil.exportExcel(list, "MCP市场", McpMarketVo.class, response); + } + + /** + * 根据市场ID获取详细信息 + * + * @param id 市场ID + */ + @SaCheckPermission("mcp:market:query") + @GetMapping("/{id}") + public R getInfo(@PathVariable Long id) { + return R.ok(mcpMarketService.selectById(id)); + } + + /** + * 新增市场 + */ + @SaCheckPermission("mcp:market:add") + @Log(title = "MCP市场管理", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping + public R add(@Validated @RequestBody McpMarketBo bo) { + mcpMarketService.insert(bo); + return R.ok(); + } + + /** + * 修改市场 + */ + @SaCheckPermission("mcp:market:edit") + @Log(title = "MCP市场管理", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping + public R edit(@Validated @RequestBody McpMarketBo bo) { + mcpMarketService.update(bo); + return R.ok(); + } + + /** + * 删除市场 + * + * @param ids 市场ID串 + */ + @SaCheckPermission("mcp:market:remove") + @Log(title = "MCP市场管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public R remove(@PathVariable Long[] ids) { + mcpMarketService.deleteByIds(List.of(ids)); + return R.ok(); + } + + /** + * 更新市场状态 + */ + @SaCheckPermission("mcp:market:edit") + @Log(title = "MCP市场管理", businessType = BusinessType.UPDATE) + @PutMapping("/{id}/status") + public R updateStatus(@PathVariable Long id, @RequestParam String status) { + mcpMarketService.updateStatus(id, status); + return R.ok(); + } + + /** + * 获取市场工具列表(分页) + */ + @SaCheckPermission("mcp:market:query") + @GetMapping("/{marketId}/tools") + public McpMarketToolListResult getMarketTools( + @PathVariable Long marketId, + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size) { + return mcpMarketService.getMarketTools(marketId, page, size); + } + + /** + * 刷新市场工具列表 + */ + @SaCheckPermission("mcp:market:edit") + @Log(title = "MCP市场管理", businessType = BusinessType.UPDATE) + @PostMapping("/{marketId}/refresh") + public R refreshMarketTools(@PathVariable Long marketId) { + return R.ok(mcpMarketService.refreshMarketTools(marketId)); + } + + /** + * 加载单个工具到本地 + */ + @SaCheckPermission("mcp:market:add") + @Log(title = "MCP市场管理", businessType = BusinessType.INSERT) + @PostMapping("/tools/{toolId}/load") + public R loadToolToLocal(@PathVariable Long toolId) { + mcpMarketService.loadToolToLocal(toolId); + return R.ok(); + } + + /** + * 批量加载工具到本地 + */ + @SaCheckPermission("mcp:market:add") + @Log(title = "MCP市场管理", businessType = BusinessType.INSERT) + @PostMapping("/tools/batch-load") + public R> batchLoadTools(@RequestBody List toolIds) { + int successCount = mcpMarketService.batchLoadTools(toolIds); + return R.ok(Map.of("successCount", successCount)); + } +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/controller/McpToolController.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/controller/McpToolController.java new file mode 100644 index 00000000..eae82f72 --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/controller/McpToolController.java @@ -0,0 +1,136 @@ +package org.ruoyi.mcp.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.ruoyi.common.core.domain.R; +import org.ruoyi.common.excel.utils.ExcelUtil; +import org.ruoyi.common.idempotent.annotation.RepeatSubmit; +import org.ruoyi.common.log.annotation.Log; +import org.ruoyi.common.log.enums.BusinessType; +import org.ruoyi.common.mybatis.core.page.PageQuery; +import org.ruoyi.common.mybatis.core.page.TableDataInfo; +import org.ruoyi.common.web.core.BaseController; +import org.ruoyi.mcp.domain.bo.McpToolBo; +import org.ruoyi.mcp.domain.dto.McpToolListResult; +import org.ruoyi.mcp.domain.dto.McpToolTestResult; +import org.ruoyi.mcp.domain.vo.McpToolVo; +import org.ruoyi.mcp.service.IMcpToolService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * MCP 工具管理 Controller + * + * @author ruoyi team + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/mcp/tool") +public class McpToolController extends BaseController { + + private final IMcpToolService mcpToolService; + + /** + * 查询 MCP 工具列表 + */ + @SaCheckPermission("mcp:tool:list") + @GetMapping("/list") + public TableDataInfo list(McpToolBo bo, PageQuery pageQuery) { + return mcpToolService.selectPageList(bo, pageQuery); + } + + /** + * 查询 MCP 工具列表(不分页) + */ + @SaCheckPermission("mcp:tool:list") + @GetMapping("/all") + public McpToolListResult listAll( + @RequestParam(required = false) String keyword, + @RequestParam(required = false) String type, + @RequestParam(required = false) String status) { + return mcpToolService.listTools(keyword, type, status); + } + + /** + * 导出 MCP 工具列表 + */ + @SaCheckPermission("mcp:tool:export") + @Log(title = "MCP工具管理", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(McpToolBo bo, HttpServletResponse response) { + List list = mcpToolService.queryList(bo); + ExcelUtil.exportExcel(list, "MCP工具", McpToolVo.class, response); + } + + /** + * 根据工具ID获取详细信息 + * + * @param id 工具ID + */ + @SaCheckPermission("mcp:tool:query") + @GetMapping("/{id}") + public R getInfo(@PathVariable Long id) { + return R.ok(mcpToolService.selectById(id)); + } + + /** + * 新增 MCP 工具 + */ + @SaCheckPermission("mcp:tool:add") + @Log(title = "MCP工具管理", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping + public R add(@Validated @RequestBody McpToolBo bo) { + mcpToolService.insert(bo); + return R.ok(); + } + + /** + * 修改 MCP 工具 + */ + @SaCheckPermission("mcp:tool:edit") + @Log(title = "MCP工具管理", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping + public R edit(@Validated @RequestBody McpToolBo bo) { + mcpToolService.update(bo); + return R.ok(); + } + + /** + * 删除 MCP 工具 + * + * @param ids 工具ID串 + */ + @SaCheckPermission("mcp:tool:remove") + @Log(title = "MCP工具管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public R remove(@PathVariable Long[] ids) { + mcpToolService.deleteByIds(List.of(ids)); + return R.ok(); + } + + /** + * 更新工具状态 + */ + @SaCheckPermission("mcp:tool:edit") + @Log(title = "MCP工具管理", businessType = BusinessType.UPDATE) + @PutMapping("/{id}/status") + public R updateStatus(@PathVariable Long id, @RequestParam String status) { + mcpToolService.updateStatus(id, status); + return R.ok(); + } + + /** + * 测试工具连接 + */ + @SaCheckPermission("mcp:tool:query") + @PostMapping("/{id}/test") + public R testTool(@PathVariable Long id) { + return R.ok(mcpToolService.testTool(id)); + } +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/bo/McpMarketBo.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/bo/McpMarketBo.java new file mode 100644 index 00000000..00493b8d --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/bo/McpMarketBo.java @@ -0,0 +1,55 @@ +package org.ruoyi.mcp.domain.bo; + +import io.github.linpeilie.annotations.AutoMapper; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.ruoyi.common.mybatis.core.domain.BaseEntity; +import org.ruoyi.mcp.domain.entity.McpMarket; + +/** + * MCP 市场业务对象 + * + * @author ruoyi team + */ +@Data +@EqualsAndHashCode(callSuper = true) +@AutoMapper(target = McpMarket.class, reverseConvertGenerate = false) +public class McpMarketBo extends BaseEntity { + + /** + * 市场ID + */ + private Long id; + + /** + * 市场名称 + */ + @NotBlank(message = "市场名称不能为空") + @Size(min = 0, max = 200, message = "市场名称不能超过{max}个字符") + private String name; + + /** + * 市场 URL + */ + @NotBlank(message = "市场URL不能为空") + @Size(min = 0, max = 500, message = "市场URL不能超过{max}个字符") + private String url; + + /** + * 市场描述 + */ + private String description; + + /** + * 认证配置(JSON格式) + */ + private String authConfig; + + /** + * 状态:ENABLED-启用, DISABLED-禁用 + */ + private String status; + +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/bo/McpToolBo.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/bo/McpToolBo.java new file mode 100644 index 00000000..7bb2005a --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/bo/McpToolBo.java @@ -0,0 +1,59 @@ +package org.ruoyi.mcp.domain.bo; + +import io.github.linpeilie.annotations.AutoMapper; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.ruoyi.common.mybatis.core.domain.BaseEntity; +import org.ruoyi.mcp.domain.entity.McpTool; + +import java.io.Serial; + +/** + * MCP 工具业务对象 + * + * @author ruoyi team + */ +@Data +@EqualsAndHashCode(callSuper = true) +@AutoMapper(target = McpTool.class, reverseConvertGenerate = false) +public class McpToolBo extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 工具ID + */ + private Long id; + + /** + * 工具名称 + */ + @NotBlank(message = "工具名称不能为空") + @Size(min = 0, max = 200, message = "工具名称不能超过{max}个字符") + private String name; + + /** + * 工具描述 + */ + private String description; + + /** + * 工具类型:LOCAL-本地, REMOTE-远程, BUILTIN-内置 + */ + @NotBlank(message = "工具类型不能为空") + private String type; + + /** + * 状态:ENABLED-启用, DISABLED-禁用 + */ + private String status; + + /** + * 配置信息(JSON格式) + */ + private String configJson; + +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketListResult.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketListResult.java new file mode 100644 index 00000000..69f0a5d9 --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketListResult.java @@ -0,0 +1,44 @@ +package org.ruoyi.mcp.domain.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.ruoyi.mcp.domain.entity.McpMarket; + +import java.util.List; + +/** + * MCP 市场列表返回结果 + * + * @author ruoyi team + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class McpMarketListResult { + + /** + * 是否成功 + */ + private boolean success; + + /** + * 市场列表 + */ + private List data; + + /** + * 总数 + */ + private int total; + + public static McpMarketListResult of(List data) { + return McpMarketListResult.builder() + .success(true) + .data(data) + .total(data != null ? data.size() : 0) + .build(); + } +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketRefreshResult.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketRefreshResult.java new file mode 100644 index 00000000..d52e91c7 --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketRefreshResult.java @@ -0,0 +1,38 @@ +package org.ruoyi.mcp.domain.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * MCP 市场工具刷新结果 + * + * @author ruoyi team + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class McpMarketRefreshResult { + + /** + * 是否成功 + */ + private boolean success; + + /** + * 消息 + */ + private String message; + + /** + * 新增工具数量 + */ + private int addedCount; + + /** + * 更新工具数量 + */ + private int updatedCount; +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketToolListResult.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketToolListResult.java new file mode 100644 index 00000000..8f2e6a81 --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketToolListResult.java @@ -0,0 +1,63 @@ +package org.ruoyi.mcp.domain.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.ruoyi.mcp.domain.entity.McpMarketTool; + +import java.util.List; + +/** + * MCP 市场工具列表返回结果(分页) + * + * @author ruoyi team + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class McpMarketToolListResult { + + /** + * 是否成功 + */ + private boolean success; + + /** + * 工具列表 + */ + private List data; + + /** + * 总数 + */ + private long total; + + /** + * 当前页 + */ + private int page; + + /** + * 每页大小 + */ + private int size; + + /** + * 总页数 + */ + private long pages; + + public static McpMarketToolListResult of(List data, long total, int page, int size) { + long pages = (total + size - 1) / size; + return McpMarketToolListResult.builder() + .success(true) + .data(data) + .total(total) + .page(page) + .size(size) + .pages(pages) + .build(); + } +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpToolListResult.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpToolListResult.java new file mode 100644 index 00000000..e330abc2 --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpToolListResult.java @@ -0,0 +1,44 @@ +package org.ruoyi.mcp.domain.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.ruoyi.mcp.domain.entity.McpTool; + +import java.util.List; + +/** + * MCP 工具列表返回结果 + * + * @author ruoyi team + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class McpToolListResult { + + /** + * 是否成功 + */ + private boolean success; + + /** + * 工具列表 + */ + private List data; + + /** + * 总数 + */ + private int total; + + public static McpToolListResult of(List data) { + return McpToolListResult.builder() + .success(true) + .data(data) + .total(data != null ? data.size() : 0) + .build(); + } +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpToolTestResult.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpToolTestResult.java new file mode 100644 index 00000000..eeb6f3dd --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpToolTestResult.java @@ -0,0 +1,56 @@ +package org.ruoyi.mcp.domain.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * MCP 工具测试结果 + * + * @author ruoyi team + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class McpToolTestResult { + + /** + * 是否成功 + */ + private boolean success; + + /** + * 消息 + */ + private String message; + + /** + * 发现的工具数量 + */ + private Integer toolCount; + + /** + * 工具名称列表 + */ + private List tools; + + public static McpToolTestResult success(String message, int toolCount, List tools) { + return McpToolTestResult.builder() + .success(true) + .message(message) + .toolCount(toolCount) + .tools(tools) + .build(); + } + + public static McpToolTestResult fail(String message) { + return McpToolTestResult.builder() + .success(false) + .message(message) + .build(); + } +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpMarket.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpMarket.java new file mode 100644 index 00000000..8d2cf21e --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpMarket.java @@ -0,0 +1,51 @@ +package org.ruoyi.mcp.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.ruoyi.common.tenant.core.TenantEntity; + +/** + * MCP 市场信息实体 + * + * @author ruoyi team + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("mcp_market_info") +public class McpMarket extends TenantEntity { + + /** + * 市场ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 市场名称 + */ + private String name; + + /** + * 市场 URL + */ + private String url; + + /** + * 市场描述 + */ + private String description; + + /** + * 认证配置(JSON格式) + */ + private String authConfig; + + /** + * 状态:ENABLED-启用, DISABLED-禁用 + */ + private String status; + +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpMarketTool.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpMarketTool.java new file mode 100644 index 00000000..69b942ab --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpMarketTool.java @@ -0,0 +1,61 @@ +package org.ruoyi.mcp.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.ruoyi.common.mybatis.core.domain.BaseEntity; + +/** + * MCP 市场工具关联实体 + * + * @author ruoyi team + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("mcp_market_tool") +public class McpMarketTool extends BaseEntity { + + /** + * ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 市场 ID + */ + private Long marketId; + + /** + * 工具名称 + */ + private String toolName; + + /** + * 工具描述 + */ + private String toolDescription; + + /** + * 工具版本 + */ + private String toolVersion; + + /** + * 工具元数据(JSON格式) + */ + private String toolMetadata; + + /** + * 是否已加载到本地 + */ + private Boolean isLoaded; + + /** + * 关联的本地工具 ID + */ + private Long localToolId; + +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpTool.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpTool.java new file mode 100644 index 00000000..0a5b3088 --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpTool.java @@ -0,0 +1,55 @@ +package org.ruoyi.mcp.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.ruoyi.common.tenant.core.TenantEntity; + + +/** + * MCP 工具信息实体 + * + * @author ruoyi team + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("mcp_tool_info") +public class McpTool extends TenantEntity { + + /** + * 工具ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 工具名称 + */ + private String name; + + /** + * 工具描述 + */ + private String description; + + /** + * 工具类型:LOCAL-本地, REMOTE-远程, BUILTIN-内置 + */ + private String type; + + /** + * 状态:ENABLED-启用, DISABLED-禁用 + */ + private String status; + + /** + * 配置信息(JSON格式) + * LOCAL: {"command": "npx", "args": ["-y", "@example/mcp-server"], "env": {...}} + * REMOTE: {"baseUrl": "http://localhost:8080/mcp"} + * BUILTIN: null + */ + private String configJson; + +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/vo/McpMarketVo.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/vo/McpMarketVo.java new file mode 100644 index 00000000..243604cf --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/vo/McpMarketVo.java @@ -0,0 +1,74 @@ +package org.ruoyi.mcp.domain.vo; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import org.ruoyi.mcp.domain.entity.McpMarket; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * MCP 市场视图对象 + * + * @author ruoyi team + */ +@Data +@ExcelIgnoreUnannotated +@AutoMapper(target = McpMarket.class) +public class McpMarketVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 市场ID + */ + @ExcelProperty(value = "市场ID") + private Long id; + + /** + * 市场名称 + */ + @ExcelProperty(value = "市场名称") + private String name; + + /** + * 市场 URL + */ + @ExcelProperty(value = "市场URL") + private String url; + + /** + * 市场描述 + */ + @ExcelProperty(value = "市场描述") + private String description; + + /** + * 认证配置 + */ + @ExcelProperty(value = "认证配置") + private String authConfig; + + /** + * 状态 + */ + @ExcelProperty(value = "状态") + private String status; + + /** + * 创建时间 + */ + @ExcelProperty(value = "创建时间") + private Date createTime; + + /** + * 更新时间 + */ + @ExcelProperty(value = "更新时间") + private Date updateTime; + +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/vo/McpToolVo.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/vo/McpToolVo.java new file mode 100644 index 00000000..88cd2494 --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/vo/McpToolVo.java @@ -0,0 +1,74 @@ +package org.ruoyi.mcp.domain.vo; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import org.ruoyi.mcp.domain.entity.McpTool; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * MCP 工具视图对象 + * + * @author ruoyi team + */ +@Data +@ExcelIgnoreUnannotated +@AutoMapper(target = McpTool.class) +public class McpToolVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 工具ID + */ + @ExcelProperty(value = "工具ID") + private Long id; + + /** + * 工具名称 + */ + @ExcelProperty(value = "工具名称") + private String name; + + /** + * 工具描述 + */ + @ExcelProperty(value = "工具描述") + private String description; + + /** + * 工具类型 + */ + @ExcelProperty(value = "工具类型") + private String type; + + /** + * 状态 + */ + @ExcelProperty(value = "状态") + private String status; + + /** + * 配置信息 + */ + @ExcelProperty(value = "配置信息") + private String configJson; + + /** + * 创建时间 + */ + @ExcelProperty(value = "创建时间") + private Date createTime; + + /** + * 更新时间 + */ + @ExcelProperty(value = "更新时间") + private Date updateTime; + +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/enums/McpToolStatus.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/enums/McpToolStatus.java new file mode 100644 index 00000000..caf8c49b --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/enums/McpToolStatus.java @@ -0,0 +1,47 @@ +package org.ruoyi.mcp.enums; + +import lombok.Getter; + +/** + * MCP 工具状态枚举 + * + * @author ruoyi team + */ +@Getter +public enum McpToolStatus { + + /** + * 启用状态 + */ + ENABLED("ENABLED", "启用"), + + /** + * 禁用状态 + */ + DISABLED("DISABLED", "禁用"); + + /** + * 状态值(存储到数据库) + */ + private final String value; + + /** + * 状态描述 + */ + private final String description; + + McpToolStatus(String value, String description) { + this.value = value; + this.description = description; + } + + /** + * 判断是否为启用状态 + * + * @param value 状态值 + * @return 是否启用 + */ + public static boolean isEnabled(String value) { + return ENABLED.value.equals(value); + } +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpMarketMapper.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpMarketMapper.java new file mode 100644 index 00000000..7d9ed31d --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpMarketMapper.java @@ -0,0 +1,15 @@ +package org.ruoyi.mcp.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.ruoyi.common.mybatis.core.mapper.BaseMapperPlus; +import org.ruoyi.mcp.domain.entity.McpMarket; +import org.ruoyi.mcp.domain.vo.McpMarketVo; + +/** + * MCP 市场信息 Mapper + * + * @author ruoyi team + */ +@Mapper +public interface McpMarketMapper extends BaseMapperPlus { +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpMarketToolMapper.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpMarketToolMapper.java new file mode 100644 index 00000000..2211906d --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpMarketToolMapper.java @@ -0,0 +1,14 @@ +package org.ruoyi.mcp.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.ruoyi.common.mybatis.core.mapper.BaseMapperPlus; +import org.ruoyi.mcp.domain.entity.McpMarketTool; + +/** + * MCP 市场工具关联 Mapper + * + * @author ruoyi team + */ +@Mapper +public interface McpMarketToolMapper extends BaseMapperPlus { +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpToolMapper.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpToolMapper.java new file mode 100644 index 00000000..5e46f399 --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpToolMapper.java @@ -0,0 +1,15 @@ +package org.ruoyi.mcp.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.ruoyi.common.mybatis.core.mapper.BaseMapperPlus; +import org.ruoyi.mcp.domain.entity.McpTool; +import org.ruoyi.mcp.domain.vo.McpToolVo; + +/** + * MCP 工具信息 Mapper + * + * @author ruoyi team + */ +@Mapper +public interface McpToolMapper extends BaseMapperPlus { +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/IMcpMarketService.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/IMcpMarketService.java new file mode 100644 index 00000000..b7a68b1f --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/IMcpMarketService.java @@ -0,0 +1,117 @@ +package org.ruoyi.mcp.service; + +import org.ruoyi.common.mybatis.core.page.PageQuery; +import org.ruoyi.common.mybatis.core.page.TableDataInfo; +import org.ruoyi.mcp.domain.bo.McpMarketBo; +import org.ruoyi.mcp.domain.dto.McpMarketListResult; +import org.ruoyi.mcp.domain.dto.McpMarketRefreshResult; +import org.ruoyi.mcp.domain.dto.McpMarketToolListResult; +import org.ruoyi.mcp.domain.vo.McpMarketVo; + +import java.util.List; + +/** + * MCP 市场服务接口 + * + * @author ruoyi team + */ +public interface IMcpMarketService { + + /** + * 分页查询市场列表 + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 市场分页列表 + */ + TableDataInfo selectPageList(McpMarketBo bo, PageQuery pageQuery); + + /** + * 查询市场列表(不分页) + * + * @param keyword 关键词 + * @param status 状态 + * @return 市场列表结果 + */ + McpMarketListResult listMarkets(String keyword, String status); + + /** + * 查询市场列表(用于导出) + * + * @param bo 查询条件 + * @return 市场列表 + */ + List 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 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 toolIds); +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/IMcpToolService.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/IMcpToolService.java new file mode 100644 index 00000000..d7cba323 --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/IMcpToolService.java @@ -0,0 +1,92 @@ +package org.ruoyi.mcp.service; + +import org.ruoyi.common.mybatis.core.page.PageQuery; +import org.ruoyi.common.mybatis.core.page.TableDataInfo; +import org.ruoyi.mcp.domain.bo.McpToolBo; +import org.ruoyi.mcp.domain.dto.McpToolListResult; +import org.ruoyi.mcp.domain.dto.McpToolTestResult; +import org.ruoyi.mcp.domain.vo.McpToolVo; + +import java.util.List; + +/** + * MCP 工具服务接口 + * + * @author ruoyi team + */ +public interface IMcpToolService { + + /** + * 分页查询工具列表 + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 工具分页列表 + */ + TableDataInfo selectPageList(McpToolBo bo, PageQuery pageQuery); + + /** + * 查询工具列表(不分页) + * + * @param keyword 关键词 + * @param type 类型 + * @param status 状态 + * @return 工具列表结果 + */ + McpToolListResult listTools(String keyword, String type, String status); + + /** + * 查询工具列表(用于导出) + * + * @param bo 查询条件 + * @return 工具列表 + */ + List 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 ids); + + /** + * 更新工具状态 + * + * @param id 工具 ID + * @param status 状态 + */ + void updateStatus(Long id, String status); + + /** + * 测试工具连接 + * + * @param id 工具 ID + * @return 测试结果 + */ + McpToolTestResult testTool(Long id); +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolDefinition.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolDefinition.java new file mode 100644 index 00000000..96890034 --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolDefinition.java @@ -0,0 +1,13 @@ +package org.ruoyi.mcp.service.core; + +/** + * 内置工具定义 + * 用于描述系统内置的工具信息 + * + * @param name 工具名称(唯一标识) + * @param displayName 显示名称 + * @param description 工具描述 + * @author ruoyi team + */ +public record BuiltinToolDefinition(String name, String displayName, String description) { +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolProvider.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolProvider.java new file mode 100644 index 00000000..d72922b0 --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolProvider.java @@ -0,0 +1,55 @@ +package org.ruoyi.mcp.service.core; + +/** + * 内置工具提供者接口 + * 所有系统内置工具都应实现此接口,以便自动注册到 BuiltinToolRegistry + * + * @author ruoyi team + * + *

使用方式: + *

+ * {@code
+ * @Component
+ * public class MyTool implements BuiltinToolProvider {
+ *     @Override
+ *     public String getToolName() {
+ *         return "my_tool";
+ *     }
+ *
+ *     @Override
+ *     public String getDisplayName() {
+ *         return "我的工具";
+ *     }
+ *
+ *     @Override
+ *     public String getDescription() {
+ *         return "工具描述...";
+ *     }
+ * }
+ * }
+ * 
+ */ +public interface BuiltinToolProvider { + + /** + * 获取工具名称(唯一标识,用于数据库存储) + * 建议使用 snake_case 格式,如:list_directory, edit_file + * + * @return 工具名称 + */ + String getToolName(); + + /** + * 获取工具显示名称(用于 UI 展示) + * + * @return 显示名称 + */ + String getDisplayName(); + + /** + * 获取工具描述(用于 AI 理解工具用途) + * + * @return 工具描述 + */ + String getDescription(); +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolRegistry.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolRegistry.java new file mode 100644 index 00000000..b3953b7f --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolRegistry.java @@ -0,0 +1,129 @@ +package org.ruoyi.mcp.service.core; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 内置工具注册表 + * 自动发现并注册所有实现 {@link BuiltinToolProvider} 接口的工具 + * + *

工具注册流程: + *

    + *
  1. Spring 自动注入所有 {@link BuiltinToolProvider} 实现
  2. + *
  3. {@link #init()} 方法在 Bean 初始化后自动调用
  4. + *
  5. 将所有工具注册到内部 Map
  6. + *
+ * + *

添加新工具只需: + *

    + *
  1. 创建一个类实现 {@link BuiltinToolProvider} 接口
  2. + *
  3. 添加 {@code @Component} 注解
  4. + *
  5. 工具会自动被发现和注册
  6. + *
+ * + * @author ruoyi team + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class BuiltinToolRegistry { + + /** + * 工具类型常量 + */ + public static final String TYPE_BUILTIN = "BUILTIN"; + + /** + * Spring 自动注入所有实现 BuiltinToolProvider 接口的 Bean + */ + private final List toolProviders; + + /** + * 内置工具定义映射表 (工具名称 -> 工具提供者) + */ + private final Map registeredTools = new ConcurrentHashMap<>(); + + /** + * 初始化方法,在 Bean 创建后自动调用 + * 将所有 BuiltinToolProvider 注册到内部 Map + */ + @PostConstruct + public void init() { + log.info("开始注册内置工具,发现 {} 个工具提供者", toolProviders.size()); + + for (BuiltinToolProvider provider : toolProviders) { + String toolName = provider.getToolName(); + + if (registeredTools.containsKey(toolName)) { + log.warn("工具名称重复: {},将覆盖原有注册", toolName); + } + + registeredTools.put(toolName, provider); + log.info("注册内置工具: {} ({})", toolName, provider.getDisplayName()); + } + + log.info("内置工具注册完成,共 {} 个工具", registeredTools.size()); + } + + /** + * 获取工具提供者 + * + * @param toolName 工具名称 + * @return 工具提供者,如果不存在则返回 null + */ + public BuiltinToolProvider getToolProvider(String toolName) { + return registeredTools.get(toolName); + } + + /** + * 检查工具是否已注册 + * + * @param toolName 工具名称 + * @return 是否已注册 + */ + public boolean hasTool(String toolName) { + return registeredTools.containsKey(toolName); + } + + /** + * 获取所有内置工具定义 + * + * @return 内置工具定义集合 + */ + public Collection getAllBuiltinTools() { + return registeredTools.values().stream() + .map(provider -> new BuiltinToolDefinition( + provider.getToolName(), + provider.getDisplayName(), + provider.getDescription() + )) + .toList(); + } + + /** + * 获取所有内置工具对象 + * 这些对象包含 @Tool 注解的方法,可直接用于 AgenticServices + * + * @return 内置工具对象列表 + */ + public List getAllBuiltinToolObjects() { + return List.copyOf(registeredTools.values()); + } + + /** + * 根据工具名称获取工具对象 + * + * @param toolName 工具名称 + * @return 工具对象,如果不存在则返回 null + */ + public Object getBuiltinToolObject(String toolName) { + return registeredTools.get(toolName); + } +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/LangChain4jMcpToolProviderService.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/LangChain4jMcpToolProviderService.java new file mode 100644 index 00000000..7abc9fd9 --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/LangChain4jMcpToolProviderService.java @@ -0,0 +1,445 @@ +package org.ruoyi.mcp.service.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +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.http.StreamableHttpMcpTransport; +import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport; +import dev.langchain4j.service.tool.ToolProvider; +import jakarta.annotation.PreDestroy; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.ruoyi.mcp.domain.entity.McpTool; +import org.ruoyi.mcp.enums.McpToolStatus; +import org.ruoyi.mcp.mapper.McpToolMapper; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * LangChain4j MCP 工具提供者服务 + * 从数据库读取 MCP 工具配置,创建 LangChain4j 的 McpToolProvider 供 Agent 使用 + * + * @author ruoyi team + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class LangChain4jMcpToolProviderService { + + /** + * 最大失败次数,超过此次数将暂时禁用工具 + */ + private static final int MAX_FAILURE_COUNT = 3; + /** + * 工具禁用时长(毫秒),默认 5 分钟 + */ + private static final long DISABLE_DURATION = 5 * 60 * 1000; + private final McpToolMapper mcpToolMapper; + private final ObjectMapper objectMapper; + /** + * 缓存活跃的 MCP Client + */ + private final Map activeClients = new ConcurrentHashMap<>(); + /** + * 工具健康状态缓存(工具ID -> 是否健康) + */ + private final Map toolHealthStatus = new ConcurrentHashMap<>(); + /** + * 工具失败次数(工具ID -> 失败次数) + */ + private final Map toolFailureCount = new ConcurrentHashMap<>(); + /** + * 工具禁用时间(工具ID -> 禁用截止时间戳) + */ + private final Map toolDisabledUntil = new ConcurrentHashMap<>(); + + /** + * 根据工具 ID 列表获取 ToolProvider + * + * @param toolIds 工具 ID 列表 + * @return ToolProvider 实例 + */ + public ToolProvider getToolProvider(List toolIds) { + if (toolIds == null || toolIds.isEmpty()) { + return McpToolProvider.builder().build(); + } + + List clients = new ArrayList<>(); + for (Long toolId : toolIds) { + try { + McpClient client = getOrCreateClient(toolId); + if (client != null) { + clients.add(client); + } + } catch (Exception e) { + log.error("Failed to create MCP client for tool {}: {}", toolId, e.getMessage()); + } + } + + if (clients.isEmpty()) { + return McpToolProvider.builder().build(); + } + + return McpToolProvider.builder() + .mcpClients(clients) + .build(); + } + + /** + * 获取所有启用的 MCP 工具的 ToolProvider + * + * @return ToolProvider 实例 + */ + public ToolProvider getAllEnabledToolsProvider() { + List enabledTools = mcpToolMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(McpTool::getStatus, McpToolStatus.ENABLED.getValue()) + ); + + if (enabledTools.isEmpty()) { + return McpToolProvider.builder().build(); + } + + List toolIds = enabledTools.stream() + .map(McpTool::getId) + .toList(); + + return getToolProvider(toolIds); + } + + /** + * 获取指定名称的 MCP 工具的 ToolProvider + * + * @param toolNames 工具名称列表 + * @return ToolProvider 实例 + */ + public ToolProvider getToolProviderByNames(List toolNames) { + if (toolNames == null || toolNames.isEmpty()) { + return McpToolProvider.builder().build(); + } + + List tools = mcpToolMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .in(McpTool::getName, toolNames) + .eq(McpTool::getStatus, McpToolStatus.ENABLED.getValue()) + ); + + if (tools.isEmpty()) { + return McpToolProvider.builder().build(); + } + + List toolIds = tools.stream() + .map(McpTool::getId) + .toList(); + + return getToolProvider(toolIds); + } + + /** + * 获取或创建 MCP Client + * 包含健康检查和失败重试逻辑 + */ + private McpClient getOrCreateClient(Long toolId) { + // 检查工具是否被禁用 + if (isToolDisabled(toolId)) { + log.warn("Tool {} is temporarily disabled due to previous failures", toolId); + return null; + } + + // 尝试从缓存获取 + McpClient cachedClient = activeClients.get(toolId); + if (cachedClient != null && isToolHealthy(toolId)) { + return cachedClient; + } + + // 创建新的客户端 + return activeClients.compute(toolId, (id, existingClient) -> { + McpTool tool = mcpToolMapper.selectById(id); + if (tool == null || !McpToolStatus.isEnabled(tool.getStatus())) { + return null; + } + + // 跳过内置工具(BUILTIN 类型) + if ("BUILTIN".equals(tool.getType())) { + log.debug("Skipping builtin tool: {}", tool.getName()); + return null; + } + + try { + McpClient client = createMcpClient(tool); + // 标记工具为健康状态 + markToolHealthy(id); + log.info("Successfully created LangChain4j MCP client for tool: {}", tool.getName()); + return client; + } catch (Exception e) { + log.error("Failed to create MCP client for tool {}: {}", tool.getName(), e.getMessage()); + // 记录失败并可能禁用工具 + handleToolFailure(id); + return null; + } + }); + } + + /** + * 检查工具是否被暂时禁用 + */ + private boolean isToolDisabled(Long toolId) { + Long disabledUntil = toolDisabledUntil.get(toolId); + if (disabledUntil == null) { + return false; + } + if (System.currentTimeMillis() > disabledUntil) { + // 禁用时间已过,重新启用 + toolDisabledUntil.remove(toolId); + toolFailureCount.put(toolId, 0); + log.info("Tool {} is re-enabled after disable period", toolId); + return false; + } + return true; + } + + /** + * 检查工具是否健康 + */ + private boolean isToolHealthy(Long toolId) { + return toolHealthStatus.getOrDefault(toolId, true); + } + + /** + * 标记工具为健康状态 + */ + private void markToolHealthy(Long toolId) { + toolHealthStatus.put(toolId, true); + toolFailureCount.put(toolId, 0); + } + + /** + * 处理工具失败 + */ + private void handleToolFailure(Long toolId) { + int failures = toolFailureCount.getOrDefault(toolId, 0) + 1; + toolFailureCount.put(toolId, failures); + toolHealthStatus.put(toolId, false); + + if (failures >= MAX_FAILURE_COUNT) { + // 禁用工具一段时间 + long disableUntil = System.currentTimeMillis() + DISABLE_DURATION; + toolDisabledUntil.put(toolId, disableUntil); + log.warn("Tool {} has failed {} times, disabling until {}", + toolId, failures, new java.util.Date(disableUntil)); + } else { + log.warn("Tool {} has failed {} times (max: {})", + toolId, failures, MAX_FAILURE_COUNT); + } + } + + /** + * 手动检查工具健康状态 + * + * @param toolId 工具 ID + * @return 工具是否健康 + */ + public boolean checkToolHealth(Long toolId) { + McpTool tool = mcpToolMapper.selectById(toolId); + if (tool == null || !McpToolStatus.isEnabled(tool.getStatus())) { + return false; + } + + try { + // 尝试创建客户端来验证连接 + McpClient client = createMcpClient(tool); + if (client != null) { + markToolHealthy(toolId); + return true; + } + return false; + } catch (Exception e) { + log.error("Health check failed for tool {}: {}", tool.getName(), e.getMessage()); + handleToolFailure(toolId); + return false; + } + } + + /** + * 获取所有工具的健康状态 + * + * @return 工具 ID -> 健康状态的映射 + */ + public Map getAllToolsHealthStatus() { + List allTools = mcpToolMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(McpTool::getStatus, McpToolStatus.ENABLED.getValue()) + ); + + Map statusMap = new ConcurrentHashMap<>(); + for (McpTool tool : allTools) { + boolean isHealthy = isToolHealthy(tool.getId()) && !isToolDisabled(tool.getId()); + statusMap.put(tool.getId(), isHealthy); + } + return statusMap; + } + + /** + * 根据工具配置创建 MCP Client + */ + private McpClient createMcpClient(McpTool tool) throws Exception { + if ("LOCAL".equals(tool.getType())) { + return createStdioClient(tool); + } else if ("REMOTE".equals(tool.getType())) { + return createRemoteClient(tool); + } + return null; + } + + /** + * 创建 STDIO Client (本地命令行工具) + */ + private McpClient createStdioClient(McpTool tool) throws Exception { + String configJson = tool.getConfigJson(); + if (configJson == null || configJson.isBlank()) { + throw new IllegalArgumentException("Config JSON is required for LOCAL type tool"); + } + + JsonNode configNode = objectMapper.readTree(configJson); + + // 解析命令 + String command = null; + List args = new ArrayList<>(); + + if (configNode.has("command")) { + command = configNode.get("command").asText(); + } + + if (configNode.has("args") && configNode.get("args").isArray()) { + for (JsonNode arg : configNode.get("args")) { + args.add(arg.asText()); + } + } + + if (command == null || command.isBlank()) { + throw new IllegalArgumentException("Command is required in config JSON"); + } + + // 处理 Windows 系统的命令 + command = resolveCommand(command); + + // 构建完整命令列表 + List fullCommand = new ArrayList<>(); + fullCommand.add(command); + fullCommand.addAll(args); + + log.info("Creating STDIO MCP client for tool: {}, command: {}", tool.getName(), fullCommand); + + // 创建传输层 + McpTransport transport = StdioMcpTransport.builder() + .command(fullCommand) + .logEvents(true) + .build(); + + // 创建客户端 + return new DefaultMcpClient.Builder() + .transport(transport) + .build(); + } + + /** + * 创建远程 HTTP/SSE Client + */ + private McpClient createRemoteClient(McpTool tool) throws Exception { + String configJson = tool.getConfigJson(); + if (configJson == null || configJson.isBlank()) { + throw new IllegalArgumentException("Config JSON is required for REMOTE type tool"); + } + + JsonNode configNode = objectMapper.readTree(configJson); + + if (!configNode.has("baseUrl")) { + throw new IllegalArgumentException("baseUrl is required in config JSON for REMOTE type tool"); + } + + String baseUrl = configNode.get("baseUrl").asText(); + log.info("Creating HTTP/SSE MCP client for tool: {}, baseUrl: {}", tool.getName(), baseUrl); + + // 创建 HTTP/SSE 传输层 + McpTransport transport = StreamableHttpMcpTransport.builder() + .url(baseUrl) + .logRequests(true) + .build(); + + // 创建客户端 + return new DefaultMcpClient.Builder() + .transport(transport) + .build(); + } + + /** + * 解析命令,处理 Windows 系统的兼容性问题 + */ + private String resolveCommand(String command) { + if (command == null || command.isBlank()) { + return command; + } + + boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win"); + + if (isWindows) { + String lowerCommand = command.toLowerCase(); + if (lowerCommand.equals("npx") || lowerCommand.equals("npm") || + lowerCommand.equals("node") || lowerCommand.equals("pnpm") || + lowerCommand.equals("yarn") || lowerCommand.equals("uvx") || + lowerCommand.equals("uv")) { + String resolvedCommand = command + ".cmd"; + log.debug("Windows detected, resolved command: {} -> {}", command, resolvedCommand); + return resolvedCommand; + } + } + + return command; + } + + /** + * 刷新指定工具的客户端连接 + */ + public void refreshClient(Long toolId) { + closeClient(toolId); + log.info("Refreshed MCP client for tool: {}", toolId); + } + + /** + * 关闭指定工具的客户端连接 + */ + private void closeClient(Long toolId) { + McpClient client = activeClients.remove(toolId); + if (client != null) { + try { + // LangChain4j McpClient 没有 close 方法,直接移除即可 + log.info("Removed MCP client for tool: {}", toolId); + } catch (Exception e) { + log.warn("Error closing MCP client for tool {}: {}", toolId, e.getMessage()); + } + } + } + + /** + * 应用关闭时清理所有连接 + */ + @PreDestroy + public void cleanup() { + log.info("Cleaning up {} MCP clients...", activeClients.size()); + activeClients.keySet().forEach(this::closeClient); + } + + /** + * 获取当前活跃的客户端数量 + */ + public int getActiveClientCount() { + return activeClients.size(); + } +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/ToolProviderFactory.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/ToolProviderFactory.java new file mode 100644 index 00000000..edf21bb3 --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/ToolProviderFactory.java @@ -0,0 +1,171 @@ +package org.ruoyi.mcp.service.core; + +import dev.langchain4j.mcp.McpToolProvider; +import dev.langchain4j.service.tool.ToolProvider; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.ruoyi.mcp.domain.entity.McpTool; +import org.ruoyi.mcp.enums.McpToolStatus; +import org.ruoyi.mcp.mapper.McpToolMapper; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * 统一工具提供工厂 + * 整合所有类型的MCP工具提供者,为Agent和Chat服务提供统一的工具获取入口 + * + *

支持的工具类型: + *

    + *
  • BUILTIN - 内置工具(如文件操作工具)
  • + *
  • LOCAL - 本地STDIO工具(通过命令行启动的MCP服务器)
  • + *
  • REMOTE - 远程HTTP/SSE工具(通过网络连接的MCP服务器)
  • + *
+ * + * @author ruoyi team + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ToolProviderFactory { + + /** + * 工具类型常量 + */ + public static final String TYPE_BUILTIN = "BUILTIN"; + public static final String TYPE_LOCAL = "LOCAL"; + public static final String TYPE_REMOTE = "REMOTE"; + private final BuiltinToolRegistry builtinToolRegistry; + private final LangChain4jMcpToolProviderService langChain4jMcpToolProviderService; + private final McpToolMapper mcpToolMapper; + + /** + * 根据工具ID列表获取LangChain4j的ToolProvider + * 用于LangChain4j Agent框架使用工具 + * + * @param toolIds 工具ID列表 + * @return ToolProvider实例 + */ + public ToolProvider getToolProvider(List toolIds) { + if (toolIds == null || toolIds.isEmpty()) { + return McpToolProvider.builder().build(); + } + + // 只获取非内置工具(LangChain4j的MCP工具) + List mcpToolIds = new ArrayList<>(); + + for (Long toolId : toolIds) { + McpTool tool = mcpToolMapper.selectById(toolId); + if (tool != null && McpToolStatus.isEnabled(tool.getStatus())) { + if (!TYPE_BUILTIN.equals(tool.getType())) { + mcpToolIds.add(toolId); + } + } + } + + // 使用LangChain4j服务获取MCP工具的ToolProvider + return langChain4jMcpToolProviderService.getToolProvider(mcpToolIds); + } + + /** + * 根据工具名称列表获取LangChain4j的ToolProvider + * + * @param toolNames 工具名称列表 + * @return ToolProvider实例 + */ + public ToolProvider getToolProviderByNames(List toolNames) { + if (toolNames == null || toolNames.isEmpty()) { + return McpToolProvider.builder().build(); + } + + // 直接使用LangChain4j服务,它已经实现了按名称查询 + return langChain4jMcpToolProviderService.getToolProviderByNames(toolNames); + } + + /** + * 获取所有已启用的MCP工具的ToolProvider + * + * @return ToolProvider实例 + */ + public ToolProvider getAllEnabledMcpToolsProvider() { + return langChain4jMcpToolProviderService.getAllEnabledToolsProvider(); + } + + /** + * 检查工具是否为内置工具 + * + * @param toolName 工具名称 + * @return 是否为内置工具 + */ + public boolean isBuiltinTool(String toolName) { + return builtinToolRegistry.hasTool(toolName); + } + + /** + * 根据工具名称获取工具ID + * + * @param toolName 工具名称 + * @return 工具ID,未找到返回null + */ + public Long getToolIdByName(String toolName) { + McpTool tool = mcpToolMapper.selectOne( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(McpTool::getName, toolName) + .last("LIMIT 1") + ); + return tool != null ? tool.getId() : null; + } + + /** + * 根据工具名称列表获取工具ID列表 + * + * @param toolNames 工具名称列表 + * @return 工具ID列表 + */ + public List getToolIdsByNames(List toolNames) { + if (toolNames == null || toolNames.isEmpty()) { + return List.of(); + } + + List tools = mcpToolMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .in(McpTool::getName, toolNames) + .eq(McpTool::getStatus, McpToolStatus.ENABLED.getValue()) + ); + + return tools.stream() + .map(McpTool::getId) + .toList(); + } + + /** + * 刷新工具连接 + * + * @param toolId 工具ID + */ + public void refreshTool(Long toolId) { + langChain4jMcpToolProviderService.refreshClient(toolId); + log.info("已刷新工具连接: toolId={}", toolId); + } + + /** + * 获取工具健康状态 + * + * @return 工具ID -> 健康状态的映射 + */ + public Map getToolsHealthStatus() { + return langChain4jMcpToolProviderService.getAllToolsHealthStatus(); + } + + /** + * 获取所有 BUILTIN 工具对象 + * 这些对象包含 @Tool 注解的方法,可直接用于 AgenticServices + * + * @return BUILTIN 工具对象列表 + */ + public List getAllBuiltinToolObjects() { + return builtinToolRegistry.getAllBuiltinToolObjects(); + } +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/impl/McpMarketServiceImpl.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/impl/McpMarketServiceImpl.java new file mode 100644 index 00000000..b3bed3ab --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/impl/McpMarketServiceImpl.java @@ -0,0 +1,328 @@ +package org.ruoyi.mcp.service.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.mcp.domain.bo.McpMarketBo; +import org.ruoyi.mcp.domain.dto.McpMarketListResult; +import org.ruoyi.mcp.domain.dto.McpMarketRefreshResult; +import org.ruoyi.mcp.domain.dto.McpMarketToolListResult; +import org.ruoyi.mcp.domain.entity.McpMarket; +import org.ruoyi.mcp.domain.entity.McpMarketTool; +import org.ruoyi.mcp.domain.entity.McpTool; +import org.ruoyi.mcp.domain.vo.McpMarketVo; +import org.ruoyi.mcp.enums.McpToolStatus; +import org.ruoyi.mcp.mapper.McpMarketMapper; +import org.ruoyi.mcp.mapper.McpMarketToolMapper; +import org.ruoyi.mcp.mapper.McpToolMapper; +import org.ruoyi.mcp.service.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 selectPageList(McpMarketBo bo, PageQuery pageQuery) { + LambdaQueryWrapper wrapper = buildQueryWrapper(bo); + Page page = baseMapper.selectVoPage(pageQuery.build(), wrapper); + return TableDataInfo.build(page); + } + + @Override + public McpMarketListResult listMarkets(String keyword, String status) { + LambdaQueryWrapper 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 list = baseMapper.selectList(wrapper); + + return McpMarketListResult.of(list); + } + + @Override + public List queryList(McpMarketBo bo) { + LambdaQueryWrapper 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 ids) { + for (Long id : ids) { + // 先删除关联的市场工具 + LambdaQueryWrapper 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 wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(McpMarketTool::getMarketId, marketId); + wrapper.orderByDesc(McpMarketTool::getCreateTime); + + Page 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 existingWrapper = new LambdaQueryWrapper<>(); + existingWrapper.eq(McpMarketTool::getMarketId, marketId); + List existingTools = mcpMarketToolMapper.selectList(existingWrapper); + + // 创建现有工具的名称到ID映射 + Map 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 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 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 buildQueryWrapper(McpMarketBo bo) { + Map params = bo.getParams(); + LambdaQueryWrapper 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; + } +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/impl/McpToolServiceImpl.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/impl/McpToolServiceImpl.java new file mode 100644 index 00000000..26c2d247 --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/impl/McpToolServiceImpl.java @@ -0,0 +1,226 @@ +package org.ruoyi.mcp.service.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.mcp.domain.bo.McpToolBo; +import org.ruoyi.mcp.domain.dto.McpToolListResult; +import org.ruoyi.mcp.domain.dto.McpToolTestResult; +import org.ruoyi.mcp.domain.entity.McpTool; +import org.ruoyi.mcp.domain.vo.McpToolVo; +import org.ruoyi.mcp.enums.McpToolStatus; +import org.ruoyi.mcp.mapper.McpToolMapper; +import org.ruoyi.mcp.service.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 selectPageList(McpToolBo bo, PageQuery pageQuery) { + LambdaQueryWrapper wrapper = buildQueryWrapper(bo); + Page page = baseMapper.selectVoPage(pageQuery.build(), wrapper); + return TableDataInfo.build(page); + } + + @Override + public McpToolListResult listTools(String keyword, String type, String status) { + LambdaQueryWrapper 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 list = baseMapper.selectList(wrapper); + + return McpToolListResult.of(list); + } + + @Override + public List queryList(McpToolBo bo) { + LambdaQueryWrapper 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 ids) { + // 过滤掉内置工具 + List 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 buildQueryWrapper(McpToolBo bo) { + Map params = bo.getParams(); + LambdaQueryWrapper 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; + } +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/EditFileTool.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/EditFileTool.java new file mode 100644 index 00000000..26473da2 --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/EditFileTool.java @@ -0,0 +1,151 @@ +package org.ruoyi.mcp.tools; + +import dev.langchain4j.agent.tool.Tool; +import org.ruoyi.mcp.service.core.BuiltinToolProvider; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.List; + +/** + * 编辑文件工具 + * 支持基于diff的文件编辑 + */ +@Component +public class EditFileTool implements BuiltinToolProvider { + + public static final String DESCRIPTION = "Edits a file by applying a diff. " + + "Use this tool when you need to make specific changes to a file. " + + "The tool will show the diff before applying changes. " + + "Use absolute paths within the workspace directory."; + + private final String rootDirectory; + private final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(getClass()); + + public EditFileTool() { + this.rootDirectory = Paths.get(System.getProperty("user.dir"), "workspace").toString(); + } + + /** + * 编辑文件 + * + * @param filePath 文件绝对路径 + * @param diff 要应用的diff内容 + * @return 操作结果 + */ + @Tool(DESCRIPTION) + public String editFile(String filePath, String diff) { + try { + // 验证参数 + if (filePath == null || filePath.trim().isEmpty()) { + return "Error: File path cannot be empty"; + } + + if (diff == null || diff.trim().isEmpty()) { + return "Error: Diff cannot be empty"; + } + + Path path = Paths.get(filePath); + + // 验证是否为绝对路径 + if (!path.isAbsolute()) { + return "Error: File path must be absolute: " + filePath; + } + + // 验证是否在工作目录内 + if (!isWithinWorkspace(path)) { + return "Error: File path must be within the workspace directory (" + rootDirectory + "): " + filePath; + } + + // 检查文件是否存在 + if (!Files.exists(path)) { + return "Error: File not found: " + filePath; + } + + // 检查是否为目录 + if (Files.isDirectory(path)) { + return "Error: Path is a directory, not a file: " + filePath; + } + + // 读取原始内容 + String originalContent = Files.readString(path, StandardCharsets.UTF_8); + List originalLines = Arrays.asList(originalContent.split("\n")); + + // 应用diff + try { + // 这里简化处理,直接用新内容替换 + // 在实际应用中,可能需要更复杂的diff解析 + String newContent = applyDiff(originalContent, diff); + + // 写入文件 + Files.writeString(path, newContent, StandardCharsets.UTF_8, + StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); + + String relativePath = getRelativePath(path); + return String.format("Successfully edited file: %s", relativePath); + + } catch (Exception e) { + return "Error: Failed to apply diff: " + e.getMessage(); + } + + } catch (IOException e) { + logger.error("Error editing file: {}", filePath, e); + return "Error: " + e.getMessage(); + } catch (Exception e) { + logger.error("Unexpected error editing file: {}", filePath, e); + return "Error: Unexpected error: " + e.getMessage(); + } + } + + /** + * 简化的diff应用逻辑 + * 实际应用中可能需要使用更复杂的diff解析器 + */ + private String applyDiff(String originalContent, String diff) { + // 这里简化处理,实际应用中需要解析diff格式 + // 目前将diff作为新内容直接替换 + // 可以考虑使用jgit等库来解析 unified diff 格式 + return diff; + } + + private boolean isWithinWorkspace(Path filePath) { + try { + Path workspaceRoot = Paths.get(rootDirectory).toRealPath(); + Path normalizedPath = filePath.normalize(); + return normalizedPath.startsWith(workspaceRoot.normalize()); + } catch (IOException e) { + logger.warn("Could not resolve workspace path", e); + return false; + } + } + + private String getRelativePath(Path filePath) { + try { + Path workspaceRoot = Paths.get(rootDirectory); + return workspaceRoot.relativize(filePath).toString(); + } catch (Exception e) { + return filePath.toString(); + } + } + + @Override + public String getToolName() { + return "edit_file"; + } + + @Override + public String getDisplayName() { + return "编辑文件"; + } + + @Override + public String getDescription() { + return DESCRIPTION; + } +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/ListDirectoryTool.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/ListDirectoryTool.java new file mode 100644 index 00000000..8f6c0cdd --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/ListDirectoryTool.java @@ -0,0 +1,285 @@ +package org.ruoyi.mcp.tools; + +import dev.langchain4j.agent.tool.Tool; +import org.ruoyi.mcp.service.core.BuiltinToolProvider; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; + +/** + * 目录列表工具 + * 列出指定目录的文件和子目录,支持递归列表 + */ +@Component +public class ListDirectoryTool implements BuiltinToolProvider { + + public static final String DESCRIPTION = "Lists files and directories in the specified path. " + + "Supports recursive listing and filtering. " + + "Shows file sizes, modification times, and types. " + + "Use absolute paths within the workspace directory."; + + private final String rootDirectory; + private final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(getClass()); + + public ListDirectoryTool() { + this.rootDirectory = Paths.get(System.getProperty("user.dir"), "workspace").toString(); + } + + /** + * 列出目录内容 + * + * @param filePath 目录绝对路径 + * @param recursive 是否递归列出子目录(可选,默认 false) + * @param maxDepth 最大递归深度(可选,默认 3,范围 1-10) + * @return 目录列表结果 + */ + @Tool(DESCRIPTION) + public String listDirectory(String filePath, Boolean recursive, Integer maxDepth) { + // 创建参数对象 + ListDirectoryParams params = new ListDirectoryParams(); + params.filePath = filePath; + params.recursive = recursive != null ? recursive : false; + params.maxDepth = maxDepth != null ? maxDepth : 3; + + return execute(params); + } + + public String execute(ListDirectoryParams params) { + try { + // 验证参数 + String validationError = validateParams(params); + if (validationError != null) { + return "Error: " + validationError; + } + + Path dirPath = Paths.get(params.filePath); + + // 检查目录是否存在 + if (!Files.exists(dirPath)) { + return "Error: Directory not found: " + params.filePath; + } + + // 检查是否为目录 + if (!Files.isDirectory(dirPath)) { + return "Error: Path is not a directory: " + params.filePath; + } + + // 列出文件和目录 + List fileInfos = listFiles(dirPath, params); + + // 生成输出 + return formatFileList(fileInfos, params); + + } catch (IOException e) { + logger.error("Error listing directory: {}", params.filePath, e); + return "Error: " + e.getMessage(); + } catch (Exception e) { + logger.error("Unexpected error listing directory: {}", params.filePath, e); + return "Error: Unexpected error: " + e.getMessage(); + } + } + + private String validateParams(ListDirectoryParams params) { + // 验证路径 + if (params.filePath == null || params.filePath.trim().isEmpty()) { + return "Directory path cannot be empty"; + } + + Path dirPath = Paths.get(params.filePath); + + // 验证是否为绝对路径 + if (!dirPath.isAbsolute()) { + return "Directory path must be absolute: " + params.filePath; + } + + // 验证是否在工作目录内 + if (!isWithinWorkspace(dirPath)) { + return "Directory path must be within the workspace directory (" + rootDirectory + "): " + params.filePath; + } + + // 验证最大深度 + if (params.maxDepth != null && (params.maxDepth < 1 || params.maxDepth > 10)) { + return "Max depth must be between 1 and 10"; + } + + return null; + } + + private List listFiles(Path dirPath, ListDirectoryParams params) throws IOException { + List fileInfos = new ArrayList<>(); + + if (params.recursive != null && params.recursive) { + int maxDepth = params.maxDepth != null ? params.maxDepth : 3; + listFilesRecursive(dirPath, fileInfos, 0, maxDepth, params); + } else { + listFilesInDirectory(dirPath, fileInfos, params); + } + + // 排序:目录在前,然后按名称排序 + fileInfos.sort(Comparator + .comparing((FileInfo f) -> !f.isDirectory()) + .thenComparing(FileInfo::name)); + + return fileInfos; + } + + private void listFilesInDirectory(Path dirPath, List fileInfos, ListDirectoryParams params) throws IOException { + try (Stream stream = Files.list(dirPath)) { + stream.forEach(path -> { + try { + FileInfo fileInfo = createFileInfo(path, dirPath); + fileInfos.add(fileInfo); + } catch (IOException e) { + logger.warn("Could not get info for file: " + path, e); + } + }); + } + } + + private void listFilesRecursive(Path dirPath, List fileInfos, int currentDepth, int maxDepth, ListDirectoryParams params) throws IOException { + if (currentDepth >= maxDepth) { + return; + } + + try (Stream stream = Files.list(dirPath)) { + List paths = stream.toList(); + + for (Path path : paths) { + try { + FileInfo fileInfo = createFileInfo(path, Paths.get(params.filePath)); + fileInfos.add(fileInfo); + + // 如果是目录,递归列出 + if (Files.isDirectory(path)) { + listFilesRecursive(path, fileInfos, currentDepth + 1, maxDepth, params); + } + } catch (IOException e) { + logger.warn("Could not get info for file: " + path, e); + } + } + } + } + + private FileInfo createFileInfo(Path path, Path basePath) throws IOException { + String name = path.getFileName().toString(); + boolean isDirectory = Files.isDirectory(path); + long size = isDirectory ? 0 : Files.size(path); + + LocalDateTime lastModified = LocalDateTime.ofInstant( + Files.getLastModifiedTime(path).toInstant(), + ZoneId.systemDefault() + ); + + String relativePath = basePath.relativize(path).toString(); + + return new FileInfo(name, relativePath, isDirectory, size, lastModified); + } + + private String formatFileList(List fileInfos, ListDirectoryParams params) { + if (fileInfos.isEmpty()) { + return "Directory is empty."; + } + + StringBuilder sb = new StringBuilder(); + sb.append(String.format("Directory listing for: %s\n", getRelativePath(Paths.get(params.filePath)))); + sb.append(String.format("Total items: %d\n\n", fileInfos.size())); + + // 表头 + sb.append(String.format("%-4s %-40s %-12s %-20s %s\n", + "Type", "Name", "Size", "Modified", "Path")); + sb.append("-".repeat(80)).append("\n"); + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + for (FileInfo fileInfo : fileInfos) { + String type = fileInfo.isDirectory() ? "DIR" : "FILE"; + String sizeStr = fileInfo.isDirectory() ? "-" : formatFileSize(fileInfo.size()); + String modifiedStr = fileInfo.lastModified().format(formatter); + + sb.append(String.format("%-4s %-40s %-12s %-20s %s\n", + type, + truncate(fileInfo.name()), + sizeStr, + modifiedStr, + fileInfo.relativePath() + )); + } + + return sb.toString(); + } + + private String formatFileSize(long bytes) { + if (bytes < 1024) return bytes + " B"; + if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0); + if (bytes < 1024 * 1024 * 1024) return String.format("%.1f MB", bytes / (1024.0 * 1024)); + return String.format("%.1f GB", bytes / (1024.0 * 1024 * 1024)); + } + + private String truncate(String str) { + if (str.length() <= 40) { + return str; + } + return str.substring(0, 40 - 3) + "..."; + } + + private boolean isWithinWorkspace(Path dirPath) { + try { + Path workspaceRoot = Paths.get(rootDirectory).toRealPath(); + Path normalizedPath = dirPath.normalize(); + return normalizedPath.startsWith(workspaceRoot.normalize()); + } catch (IOException e) { + logger.warn("Could not resolve workspace path", e); + return false; + } + } + + private String getRelativePath(Path dirPath) { + try { + Path workspaceRoot = Paths.get(rootDirectory); + return workspaceRoot.relativize(dirPath).toString(); + } catch (Exception e) { + return dirPath.toString(); + } + } + + @Override + public String getToolName() { + return "list_directory"; + } + + @Override + public String getDisplayName() { + return "列出目录"; + } + + @Override + public String getDescription() { + return DESCRIPTION; + } + + /** + * 文件信息 + */ + public record FileInfo(String name, String relativePath, boolean isDirectory, long size, + LocalDateTime lastModified) { + } + + /** + * 列表目录参数 + */ + public static class ListDirectoryParams { + public String filePath; + public Boolean recursive; + public Integer maxDepth; + } +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/ReadFileTool.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/ReadFileTool.java new file mode 100644 index 00000000..7b4886e7 --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/ReadFileTool.java @@ -0,0 +1,121 @@ +package org.ruoyi.mcp.tools; + +import dev.langchain4j.agent.tool.Tool; +import org.ruoyi.mcp.service.core.BuiltinToolProvider; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * 读取文件工具 + * 读取指定路径的文件内容 + */ +@Component +public class ReadFileTool implements BuiltinToolProvider { + + public static final String DESCRIPTION = "Reads the contents of a file. " + + "Use absolute paths within the workspace directory. " + + "Returns the complete file content as a string."; + + private final String rootDirectory; + private final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(getClass()); + + public ReadFileTool() { + this.rootDirectory = Paths.get(System.getProperty("user.dir"), "workspace").toString(); + } + + /** + * 读取文件内容 + * + * @param filePath 文件绝对路径 + * @return 文件内容 + */ + @Tool(DESCRIPTION) + public String readFile(String filePath) { + try { + // 验证参数 + if (filePath == null || filePath.trim().isEmpty()) { + return "Error: File path cannot be empty"; + } + + Path path = Paths.get(filePath); + + // 验证是否为绝对路径 + if (!path.isAbsolute()) { + return "Error: File path must be absolute: " + filePath; + } + + // 验证是否在工作目录内 + if (!isWithinWorkspace(path)) { + return "Error: File path must be within the workspace directory (" + rootDirectory + "): " + filePath; + } + + // 检查文件是否存在 + if (!Files.exists(path)) { + return "Error: File not found: " + filePath; + } + + // 检查是否为目录 + if (Files.isDirectory(path)) { + return "Error: Path is a directory, not a file: " + filePath; + } + + // 读取文件内容 + String content = Files.readString(path, StandardCharsets.UTF_8); + + // 获取相对路径 + String relativePath = getRelativePath(path); + long sizeBytes = content.getBytes(StandardCharsets.UTF_8).length; + long lineCount = content.lines().count(); + + return String.format("File: %s (%d lines, %d bytes)\n\n%s", + relativePath, lineCount, sizeBytes, content); + + } catch (IOException e) { + logger.error("Error reading file: {}", filePath, e); + return "Error: " + e.getMessage(); + } catch (Exception e) { + logger.error("Unexpected error reading file: {}", filePath, e); + return "Error: Unexpected error: " + e.getMessage(); + } + } + + private boolean isWithinWorkspace(Path filePath) { + try { + Path workspaceRoot = Paths.get(rootDirectory).toRealPath(); + Path normalizedPath = filePath.normalize(); + return normalizedPath.startsWith(workspaceRoot.normalize()); + } catch (IOException e) { + logger.warn("Could not resolve workspace path", e); + return false; + } + } + + private String getRelativePath(Path filePath) { + try { + Path workspaceRoot = Paths.get(rootDirectory); + return workspaceRoot.relativize(filePath).toString(); + } catch (Exception e) { + return filePath.toString(); + } + } + + @Override + public String getToolName() { + return "read_file"; + } + + @Override + public String getDisplayName() { + return "读取文件"; + } + + @Override + public String getDescription() { + return DESCRIPTION; + } +} From 1e6e236d3c5d5d878db7bf7e69935c98a1b6a849 Mon Sep 17 00:00:00 2001 From: evo <446796145@qq.com> Date: Mon, 23 Feb 2026 16:07:38 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E6=8A=A5=E9=94=99=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/handler/GlobalExceptionHandler.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/ruoyi/common/web/handler/GlobalExceptionHandler.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/ruoyi/common/web/handler/GlobalExceptionHandler.java index 03a682db..3d81e0d0 100644 --- a/ruoyi-common/ruoyi-common-web/src/main/java/org/ruoyi/common/web/handler/GlobalExceptionHandler.java +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/ruoyi/common/web/handler/GlobalExceptionHandler.java @@ -121,16 +121,23 @@ public class GlobalExceptionHandler { /** * 拦截未知的运行时异常 + * 注意:对于文件下载/导出等场景,IOException 可能是正常流程的一部分, + * 需要排除 export/download 等路径,避免干扰文件导出 */ - @ResponseStatus(org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler(IOException.class) - public void handleIoException(IOException e, HttpServletRequest request) { + public R handleIoException(IOException e, HttpServletRequest request) { String requestURI = request.getRequestURI(); if (requestURI.contains("sse")) { // sse 经常性连接中断 例如关闭浏览器 直接屏蔽 - return; + return null; + } + // 排除文件下载/导出相关的 IOException,让异常正常传播以便上层处理 + if (requestURI.contains("/export") || requestURI.contains("/download")) { + // 重新抛出,让调用方处理 + throw new RuntimeException("文件导出/下载IO异常: " + e.getMessage(), e); } log.error("请求地址'{}',连接中断", requestURI, e); + return R.fail(e.getMessage()); } /** @@ -146,6 +153,13 @@ public class GlobalExceptionHandler { @ExceptionHandler(RuntimeException.class) public R handleRuntimeException(RuntimeException e, HttpServletRequest request) { String requestURI = request.getRequestURI(); + // 对于文件导出相关异常,不进行封装处理,让原始异常信息传播 + Throwable cause = e.getCause(); + if (requestURI.contains("/export") || requestURI.contains("/download")) { + log.error("请求地址'{}',文件导出/下载异常.", requestURI, e); + // 对于文件导出,直接返回异常信息,不进行额外封装 + return R.fail(cause != null ? cause.getMessage() : e.getMessage()); + } log.error("请求地址'{}',发生未知异常.", requestURI, e); return R.fail(e.getMessage()); } From ee477213e0e596fb7eb6e700f9b708b3183b9098 Mon Sep 17 00:00:00 2001 From: evo <446796145@qq.com> Date: Mon, 23 Feb 2026 18:21:31 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E4=BC=98=E5=8C=96mcp=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application.yml | 13 +++++ .../main/java/org/ruoyi/agent/McpAgent.java | 45 +++++++-------- .../LangChain4jMcpToolProviderService.java | 57 ++++++++++--------- 3 files changed, 63 insertions(+), 52 deletions(-) diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 2a17ad30..ca371e3d 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -216,6 +216,8 @@ springdoc: packages-to-scan: org.ruoyi.generator - group: 5.工作流模块 packages-to-scan: org.ruoyi.workflow + - group: 6.MCP模块 + packages-to-scan: org.ruoyi.mcp # 防止XSS攻击 xss: @@ -357,3 +359,14 @@ knowledge: cache-enabled: true # 缓存过期时间(分钟) cache-expire-minutes: 60 + +--- # MCP 模块配置 +app: + mcp: + client: + # 请求超时时间(秒) + request-timeout: 30 + # 连接超时时间(秒) + connection-timeout: 10 + # 最大重试次数 + max-retries: 3 diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/McpAgent.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/McpAgent.java index 1f5d0c6a..6fc613f2 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/McpAgent.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/McpAgent.java @@ -7,40 +7,35 @@ import dev.langchain4j.service.V; public interface McpAgent { /** - * 系统提示词:定义智能体身份、核心职责、强制遵守的规则 - * 适配SSE流式特性,明确工具全来自远端MCP服务,仅做代理调用和结果整理 + * 系统提示词:通用工具调用智能体 + * 不限定具体工具类型,让 LangChain4j 自动传递工具描述给 LLM */ @SystemMessage(""" - 你是专业的MCP服务工具代理智能体,核心能力是通过HTTP SSE流式传输协议,调用本地http://localhost:8085/sse地址上MCP服务端注册的所有工具。 - 你的核心工作职责: - 1. 准确理解用户的自然语言请求,判断需要调用MCP服务端的哪一个/哪些工具; - 2. 通过绑定的工具提供者,向MCP服务端发起工具调用请求,传递完整的工具执行参数; - 3. 实时接收MCP服务端通过SSE流式返回的工具执行结果,保证结果片段的完整性; - 4. 将流式结果按原始顺序整理为清晰、易懂的自然语言答案,返回给用户。 + 你是一个AI助手,可以通过调用各种工具来帮助用户完成不同的任务。 - 【强制遵守的核心规则 - 无例外】 - 1. 所有工具调用必须通过远端MCP服务执行,严禁尝试本地执行任何业务逻辑; - 2. 处理SSE流式结果时,严格保留结果片段的返回顺序,不得打乱或遗漏; - 3. 若MCP服务返回错误(如工具未找到、参数错误、执行失败),直接将错误信息友好反馈给用户,无需额外推理; - 4. 工具执行结果若为结构化数据(如JSON、表格),需格式化后返回,提升可读性。 + 【工具使用规则】 + 1. 根据用户的请求,判断需要使用哪些工具 + 2. 仔细阅读每个工具的描述,确保理解工具的功能和参数要求 + 3. 使用正确的参数调用工具 + 4. 如果工具执行失败,向用户友好地说明错误原因,并尝试提供替代方案 + 5. 对于复杂任务,可以分步骤使用多个工具完成 + 6. 将工具执行结果以清晰易懂的方式呈现给用户 + + 【响应格式】 + - 直接回答用户的问题 + - 如果使用了工具,说明使用了什么工具以及结果 + - 如果遇到错误,提供友好的错误信息和解决建议 """) - /** - * 用户消息模板:{{query}}为参数占位符,与方法入参的@V("query")绑定 - */ @UserMessage(""" - 请通过调用MCP服务端的工具,处理用户的以下请求: {{query}} """) + + @Agent("通用工具调用智能体") /** - * 智能体标识:用于日志打印、监控追踪、多智能体协作时的身份识别 - */ - @Agent("MCP服务SSE流式代理智能体-连接本地8085端口") - /** - * 智能体对外调用入口方法 - * @param query 用户的自然语言请求(如:生成订单数据柱状图、查询今日天气) - * @V("query") 将方法入参值绑定到@UserMessage的{{query}}占位符中 - * @return 整理后的MCP工具执行结果(流式结果会自动拼接为完整字符串) + * 智能体对外调用入口 + * @param query 用户的自然语言请求 + * @return 处理结果 */ String callMcpTool(@V("query") String query); } diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/LangChain4jMcpToolProviderService.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/LangChain4jMcpToolProviderService.java index 7abc9fd9..71a853f3 100644 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/LangChain4jMcpToolProviderService.java +++ b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/LangChain4jMcpToolProviderService.java @@ -100,6 +100,7 @@ public class LangChain4jMcpToolProviderService { public ToolProvider getAllEnabledToolsProvider() { List enabledTools = mcpToolMapper.selectList( new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .ne(McpTool::getType,"BUILTIN") .eq(McpTool::getStatus, McpToolStatus.ENABLED.getValue()) ); @@ -144,47 +145,49 @@ public class LangChain4jMcpToolProviderService { /** * 获取或创建 MCP Client - * 包含健康检查和失败重试逻辑 */ private McpClient getOrCreateClient(Long toolId) { // 检查工具是否被禁用 if (isToolDisabled(toolId)) { - log.warn("Tool {} is temporarily disabled due to previous failures", toolId); + log.warn("Tool {} is temporarily disabled", toolId); return null; } - // 尝试从缓存获取 + // 返回缓存的客户端 McpClient cachedClient = activeClients.get(toolId); if (cachedClient != null && isToolHealthy(toolId)) { return cachedClient; } - // 创建新的客户端 - return activeClients.compute(toolId, (id, existingClient) -> { - McpTool tool = mcpToolMapper.selectById(id); - if (tool == null || !McpToolStatus.isEnabled(tool.getStatus())) { - return null; - } + // 查询工具配置 + McpTool tool = mcpToolMapper.selectById(toolId); + if (tool == null || !McpToolStatus.isEnabled(tool.getStatus())) { + return null; + } - // 跳过内置工具(BUILTIN 类型) - if ("BUILTIN".equals(tool.getType())) { - log.debug("Skipping builtin tool: {}", tool.getName()); - return null; - } + String toolType = tool.getType(); + // 只支持 LOCAL 和 REMOTE 类型 + if (!ToolProviderFactory.TYPE_LOCAL.equals(toolType) && !ToolProviderFactory.TYPE_REMOTE.equals(toolType)) { + log.warn("Unsupported tool type: {} for tool: {}", toolType, tool.getName()); + return null; + } - try { - McpClient client = createMcpClient(tool); - // 标记工具为健康状态 - markToolHealthy(id); - log.info("Successfully created LangChain4j MCP client for tool: {}", tool.getName()); - return client; - } catch (Exception e) { - log.error("Failed to create MCP client for tool {}: {}", tool.getName(), e.getMessage()); - // 记录失败并可能禁用工具 - handleToolFailure(id); + // 创建并缓存新客户端 + try { + McpClient client = createMcpClient(tool); + if (client == null) { + log.warn("Failed to create MCP client for tool: {}", tool.getName()); return null; } - }); + activeClients.put(toolId, client); + markToolHealthy(toolId); + log.info("Created MCP client for tool: {}", tool.getName()); + return client; + } catch (Exception e) { + log.error("Failed to create MCP client for tool {}: {}", tool.getName(), e.getMessage(), e); + handleToolFailure(toolId); + return null; + } } /** @@ -360,11 +363,11 @@ public class LangChain4jMcpToolProviderService { JsonNode configNode = objectMapper.readTree(configJson); - if (!configNode.has("baseUrl")) { + if (!configNode.has("url")) { throw new IllegalArgumentException("baseUrl is required in config JSON for REMOTE type tool"); } - String baseUrl = configNode.get("baseUrl").asText(); + String baseUrl = configNode.get("url").asText(); log.info("Creating HTTP/SSE MCP client for tool: {}, baseUrl: {}", tool.getName(), baseUrl); // 创建 HTTP/SSE 传输层 From e768c4b3566d9732ba808a532009a5ff93115bf7 Mon Sep 17 00:00:00 2001 From: ageerle Date: Tue, 24 Feb 2026 22:46:47 +0800 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4=E9=BB=98?= =?UTF-8?q?=E8=AE=A4sql=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/script/sql/ruoyi-ai-v3_mysql8.sql | 325 ++++++++---------- docs/script/sql/workFlow-v3.0.sql | 6 - .../src/main/resources/application-dev.yml | 6 +- 3 files changed, 145 insertions(+), 192 deletions(-) delete mode 100644 docs/script/sql/workFlow-v3.0.sql diff --git a/docs/script/sql/ruoyi-ai-v3_mysql8.sql b/docs/script/sql/ruoyi-ai-v3_mysql8.sql index b1528ce2..505bb0b9 100644 --- a/docs/script/sql/ruoyi-ai-v3_mysql8.sql +++ b/docs/script/sql/ruoyi-ai-v3_mysql8.sql @@ -1,3 +1,18 @@ +/* + Navicat MySQL Dump SQL + + Source Server : 本地 + Source Server Type : MySQL + Source Server Version : 80045 (8.0.45) + Source Host : localhost:3306 + Source Schema : ruoyi-ai + + Target Server Type : MySQL + Target Server Version : 80045 (8.0.45) + File Encoding : 65001 + + Date: 24/02/2026 22:44:53 +*/ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; @@ -1448,6 +1463,90 @@ CREATE TABLE `knowledge_info` ( -- Records of knowledge_info -- ---------------------------- +-- ---------------------------- +-- Table structure for mcp_market_info +-- ---------------------------- +DROP TABLE IF EXISTS `mcp_market_info`; +CREATE TABLE `mcp_market_info` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '市场ID', + `name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '市场名称', + `url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '市场URL', + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '市场描述', + `auth_config` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '认证配置(JSON格式)', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'ENABLED' COMMENT '状态:ENABLED-启用, DISABLED-禁用', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '000000' COMMENT '租户编号', + `create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门', + `create_by` bigint NULL DEFAULT NULL COMMENT '创建者', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', + `update_by` bigint NULL DEFAULT NULL COMMENT '更新者', + `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', + `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_name`(`name` ASC) USING BTREE, + INDEX `idx_status`(`status` ASC) USING BTREE, + INDEX `idx_tenant_id`(`tenant_id` ASC) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'MCP市场表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of mcp_market_info +-- ---------------------------- + +-- ---------------------------- +-- Table structure for mcp_market_tool +-- ---------------------------- +DROP TABLE IF EXISTS `mcp_market_tool`; +CREATE TABLE `mcp_market_tool` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID', + `market_id` bigint NOT NULL COMMENT '市场ID', + `tool_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '工具名称', + `tool_description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '工具描述', + `tool_version` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '工具版本', + `tool_metadata` json NULL COMMENT '工具元数据(JSON格式)', + `is_loaded` tinyint(1) NULL DEFAULT 0 COMMENT '是否已加载到本地', + `local_tool_id` bigint NULL DEFAULT NULL COMMENT '关联的本地工具ID', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_market_id`(`market_id` ASC) USING BTREE, + INDEX `idx_tool_name`(`tool_name` ASC) USING BTREE, + INDEX `idx_is_loaded`(`is_loaded` ASC) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'MCP市场工具关联表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of mcp_market_tool +-- ---------------------------- + +-- ---------------------------- +-- Table structure for mcp_tool_info +-- ---------------------------- +DROP TABLE IF EXISTS `mcp_tool_info`; +CREATE TABLE `mcp_tool_info` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '工具ID', + `name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '工具名称', + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '工具描述', + `type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'LOCAL' COMMENT '工具类型:LOCAL-本地, REMOTE-远程, BUILTIN-内置', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'ENABLED' COMMENT '状态:ENABLED-启用, DISABLED-禁用', + `config_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '配置信息(JSON格式)', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '000000' COMMENT '租户编号', + `create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门', + `create_by` bigint NULL DEFAULT NULL COMMENT '创建者', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', + `update_by` bigint NULL DEFAULT NULL COMMENT '更新者', + `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', + `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_name`(`name` ASC) USING BTREE, + INDEX `idx_type`(`type` ASC) USING BTREE, + INDEX `idx_status`(`status` ASC) USING BTREE, + INDEX `idx_tenant_id`(`tenant_id` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'MCP工具表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of mcp_tool_info +-- ---------------------------- +INSERT INTO `mcp_tool_info` VALUES (1, 'edit_file', 'Edits a file by applying a diff. Use this tool when you need to make specific changes to a file. The tool will show the diff before applying changes. Use absolute paths within the workspace directory.', 'BUILTIN', 'ENABLED', NULL, '000000', -1, -1, '2026-02-24 20:19:41', -1, '2026-02-24 20:19:41', '0'); +INSERT INTO `mcp_tool_info` VALUES (2, 'list_directory', 'Lists files and directories in the specified path. Supports recursive listing and filtering. Shows file sizes, modification times, and types. Use absolute paths within the workspace directory.', 'BUILTIN', 'ENABLED', NULL, '000000', -1, -1, '2026-02-24 20:19:41', -1, '2026-02-24 20:19:41', '0'); +INSERT INTO `mcp_tool_info` VALUES (3, 'read_file', 'Reads the contents of a file. Use absolute paths within the workspace directory. Returns the complete file content as a string.', 'BUILTIN', 'ENABLED', NULL, '000000', -1, -1, '2026-02-24 20:19:41', -1, '2026-02-24 20:19:41', '0'); + -- ---------------------------- -- Table structure for sj_distributed_lock -- ---------------------------- @@ -1460,7 +1559,7 @@ CREATE TABLE `sj_distributed_lock` ( `create_dt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_dt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', PRIMARY KEY (`name`) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '锁定表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '锁定表' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sj_distributed_lock @@ -1485,7 +1584,7 @@ CREATE TABLE `sj_group_config` ( `update_dt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uk_namespace_id_group_name`(`namespace_id` ASC, `group_name` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '组配置' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '组配置' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sj_group_config @@ -1531,7 +1630,7 @@ CREATE TABLE `sj_job` ( INDEX `idx_namespace_id_group_name`(`namespace_id` ASC, `group_name` ASC) USING BTREE, INDEX `idx_job_status_bucket_index`(`job_status` ASC, `bucket_index` ASC) USING BTREE, INDEX `idx_create_dt`(`create_dt` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '任务信息' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '任务信息' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sj_job @@ -1553,7 +1652,7 @@ CREATE TABLE `sj_job_executor` ( PRIMARY KEY (`id`) USING BTREE, INDEX `idx_namespace_id_group_name`(`namespace_id` ASC, `group_name` ASC) USING BTREE, INDEX `idx_create_dt`(`create_dt` ASC) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '任务执行器信息' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '任务执行器信息' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sj_job_executor @@ -1579,7 +1678,7 @@ CREATE TABLE `sj_job_log_message` ( INDEX `idx_task_batch_id_task_id`(`task_batch_id` ASC, `task_id` ASC) USING BTREE, INDEX `idx_create_dt`(`create_dt` ASC) USING BTREE, INDEX `idx_namespace_id_group_name`(`namespace_id` ASC, `group_name` ASC) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '调度日志' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '调度日志' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sj_job_log_message @@ -1608,7 +1707,7 @@ CREATE TABLE `sj_job_summary` ( PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uk_trigger_at_system_task_type_business_id`(`trigger_at` ASC, `system_task_type` ASC, `business_id` ASC) USING BTREE, INDEX `idx_namespace_id_group_name_business_id`(`namespace_id` ASC, `group_name` ASC, `business_id` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'DashBoard_Job' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'DashBoard_Job' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sj_job_summary @@ -1642,7 +1741,7 @@ CREATE TABLE `sj_job_task` ( INDEX `idx_task_batch_id_task_status`(`task_batch_id` ASC, `task_status` ASC) USING BTREE, INDEX `idx_create_dt`(`create_dt` ASC) USING BTREE, INDEX `idx_namespace_id_group_name`(`namespace_id` ASC, `group_name` ASC) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '任务实例' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '任务实例' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sj_job_task @@ -1674,7 +1773,7 @@ CREATE TABLE `sj_job_task_batch` ( INDEX `idx_create_dt`(`create_dt` ASC) USING BTREE, INDEX `idx_namespace_id_group_name`(`namespace_id` ASC, `group_name` ASC) USING BTREE, INDEX `idx_workflow_task_batch_id_workflow_node_id`(`workflow_task_batch_id` ASC, `workflow_node_id` ASC) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '任务批次' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '任务批次' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sj_job_task_batch @@ -1695,7 +1794,7 @@ CREATE TABLE `sj_namespace` ( PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uk_unique_id`(`unique_id` ASC) USING BTREE, INDEX `idx_name`(`name` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '命名空间' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '命名空间' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sj_namespace @@ -1724,7 +1823,7 @@ CREATE TABLE `sj_notify_config` ( `update_dt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_namespace_id_group_name_scene_name`(`namespace_id` ASC, `group_name` ASC) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '通知配置' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '通知配置' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sj_notify_config @@ -1745,7 +1844,7 @@ CREATE TABLE `sj_notify_recipient` ( `update_dt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_namespace_id`(`namespace_id` ASC) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '告警通知接收人' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '告警通知接收人' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sj_notify_recipient @@ -1784,7 +1883,7 @@ CREATE TABLE `sj_retry` ( INDEX `idx_retry_status_bucket_index`(`retry_status` ASC, `bucket_index` ASC) USING BTREE, INDEX `idx_parent_id`(`parent_id` ASC) USING BTREE, INDEX `idx_create_dt`(`create_dt` ASC) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '重试信息表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '重试信息表' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sj_retry @@ -1813,7 +1912,7 @@ CREATE TABLE `sj_retry_dead_letter` ( INDEX `idx_idempotent_id`(`idempotent_id` ASC) USING BTREE, INDEX `idx_biz_no`(`biz_no` ASC) USING BTREE, INDEX `idx_create_dt`(`create_dt` ASC) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '死信队列表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '死信队列表' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sj_retry_dead_letter @@ -1848,7 +1947,7 @@ CREATE TABLE `sj_retry_scene_config` ( `update_dt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uk_namespace_id_group_name_scene_name`(`namespace_id` ASC, `group_name` ASC, `scene_name` ASC) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '场景配置' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '场景配置' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sj_retry_scene_config @@ -1873,7 +1972,7 @@ CREATE TABLE `sj_retry_summary` ( PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uk_scene_name_trigger_at`(`namespace_id` ASC, `group_name` ASC, `scene_name` ASC, `trigger_at` ASC) USING BTREE, INDEX `idx_trigger_at`(`trigger_at` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'DashBoard_Retry' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'DashBoard_Retry' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sj_retry_summary @@ -1901,7 +2000,7 @@ CREATE TABLE `sj_retry_task` ( INDEX `task_status`(`task_status` ASC) USING BTREE, INDEX `idx_create_dt`(`create_dt` ASC) USING BTREE, INDEX `idx_retry_id`(`retry_id` ASC) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '重试任务表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '重试任务表' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sj_retry_task @@ -1924,7 +2023,7 @@ CREATE TABLE `sj_retry_task_log_message` ( PRIMARY KEY (`id`) USING BTREE, INDEX `idx_namespace_id_group_name_retry_task_id`(`namespace_id` ASC, `group_name` ASC, `retry_task_id` ASC) USING BTREE, INDEX `idx_create_dt`(`create_dt` ASC) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '任务调度日志信息记录表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '任务调度日志信息记录表' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sj_retry_task_log_message @@ -1951,7 +2050,7 @@ CREATE TABLE `sj_server_node` ( UNIQUE INDEX `uk_host_id_host_ip`(`host_id` ASC, `host_ip` ASC) USING BTREE, INDEX `idx_namespace_id_group_name`(`namespace_id` ASC, `group_name` ASC) USING BTREE, INDEX `idx_expire_at_node_type`(`expire_at` ASC, `node_type` ASC) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '服务器节点' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '服务器节点' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sj_server_node @@ -1970,7 +2069,7 @@ CREATE TABLE `sj_system_user` ( `update_dt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uk_username`(`username` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '系统用户表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '系统用户表' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sj_system_user @@ -1990,7 +2089,7 @@ CREATE TABLE `sj_system_user_permission` ( `update_dt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uk_namespace_id_group_name_system_user_id`(`namespace_id` ASC, `group_name` ASC, `system_user_id` ASC) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '系统用户权限表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '系统用户权限表' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sj_system_user_permission @@ -2025,7 +2124,7 @@ CREATE TABLE `sj_workflow` ( PRIMARY KEY (`id`) USING BTREE, INDEX `idx_create_dt`(`create_dt` ASC) USING BTREE, INDEX `idx_namespace_id_group_name`(`namespace_id` ASC, `group_name` ASC) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '工作流' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '工作流' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sj_workflow @@ -2056,7 +2155,7 @@ CREATE TABLE `sj_workflow_node` ( PRIMARY KEY (`id`) USING BTREE, INDEX `idx_create_dt`(`create_dt` ASC) USING BTREE, INDEX `idx_namespace_id_group_name`(`namespace_id` ASC, `group_name` ASC) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '工作流节点' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '工作流节点' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sj_workflow_node @@ -2085,7 +2184,7 @@ CREATE TABLE `sj_workflow_task_batch` ( INDEX `idx_job_id_task_batch_status`(`workflow_id` ASC, `task_batch_status` ASC) USING BTREE, INDEX `idx_create_dt`(`create_dt` ASC) USING BTREE, INDEX `idx_namespace_id_group_name`(`namespace_id` ASC, `group_name` ASC) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '工作流批次' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '工作流批次' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sj_workflow_task_batch @@ -2708,6 +2807,22 @@ INSERT INTO `sys_menu` VALUES (1620, '配置列表', 118, 5, '#', '', '', 1, 0, INSERT INTO `sys_menu` VALUES (1621, '配置添加', 118, 6, '#', '', '', 1, 0, 'F', '0', '0', 'system:ossConfig:add', '#', 103, 1, '2025-12-14 16:11:49', NULL, NULL, ''); INSERT INTO `sys_menu` VALUES (1622, '配置编辑', 118, 6, '#', '', '', 1, 0, 'F', '0', '0', 'system:ossConfig:edit', '#', 103, 1, '2025-12-14 16:11:49', NULL, NULL, ''); INSERT INTO `sys_menu` VALUES (1623, '配置删除', 118, 6, '#', '', '', 1, 0, 'F', '0', '0', 'system:ossConfig:remove', '#', 103, 1, '2025-12-14 16:11:49', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (2000, 'MCP管理', 0, 5, 'mcp', '', '', 1, 0, 'M', '0', '0', '', 'mdi:robot-industrial', 103, 1, '2026-02-24 20:02:47', NULL, NULL, 'MCP模块管理菜单'); +INSERT INTO `sys_menu` VALUES (2001, 'MCP工具管理', 2000, 1, 'tool', 'mcp/tool/index', '', 1, 0, 'C', '0', '0', 'mcp:tool:list', 'material-symbols:tools-hammer-outline', 103, 1, '2026-02-24 20:02:47', NULL, NULL, 'MCP工具管理菜单'); +INSERT INTO `sys_menu` VALUES (2002, 'MCP工具查询', 2001, 1, '#', '', '', 1, 0, 'F', '0', '0', 'mcp:tool:query', '#', 103, 1, '2026-02-24 20:02:47', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (2003, 'MCP工具新增', 2001, 2, '#', '', '', 1, 0, 'F', '0', '0', 'mcp:tool:add', '#', 103, 1, '2026-02-24 20:02:47', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (2004, 'MCP工具修改', 2001, 3, '#', '', '', 1, 0, 'F', '0', '0', 'mcp:tool:edit', '#', 103, 1, '2026-02-24 20:02:47', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (2005, 'MCP工具删除', 2001, 4, '#', '', '', 1, 0, 'F', '0', '0', 'mcp:tool:remove', '#', 103, 1, '2026-02-24 20:02:47', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (2006, 'MCP工具测试', 2001, 5, '#', '', '', 1, 0, 'F', '0', '0', 'mcp:tool:test', '#', 103, 1, '2026-02-24 20:02:47', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (2007, 'MCP工具导出', 2001, 6, '#', '', '', 1, 0, 'F', '0', '0', 'mcp:tool:export', '#', 103, 1, '2026-02-24 20:02:47', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (2010, 'MCP市场管理', 2000, 2, 'market', 'mcp/market/index', '', 1, 0, 'C', '0', '0', 'mcp:market:list', 'mdi:storefront-outline', 103, 1, '2026-02-24 20:02:47', NULL, NULL, 'MCP市场管理菜单'); +INSERT INTO `sys_menu` VALUES (2011, 'MCP市场查询', 2010, 1, '#', '', '', 1, 0, 'F', '0', '0', 'mcp:market:query', '#', 103, 1, '2026-02-24 20:02:47', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (2012, 'MCP市场新增', 2010, 2, '#', '', '', 1, 0, 'F', '0', '0', 'mcp:market:add', '#', 103, 1, '2026-02-24 20:02:47', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (2013, 'MCP市场修改', 2010, 3, '#', '', '', 1, 0, 'F', '0', '0', 'mcp:market:edit', '#', 103, 1, '2026-02-24 20:02:47', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (2014, 'MCP市场删除', 2010, 4, '#', '', '', 1, 0, 'F', '0', '0', 'mcp:market:remove', '#', 103, 1, '2026-02-24 20:02:47', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (2015, 'MCP市场刷新', 2010, 5, '#', '', '', 1, 0, 'F', '0', '0', 'mcp:market:refresh', '#', 103, 1, '2026-02-24 20:02:47', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (2016, 'MCP工具加载', 2010, 6, '#', '', '', 1, 0, 'F', '0', '0', 'mcp:market:load', '#', 103, 1, '2026-02-24 20:02:47', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (2017, 'MCP市场导出', 2010, 7, '#', '', '', 1, 0, 'F', '0', '0', 'mcp:market:export', '#', 103, 1, '2026-02-24 20:02:47', NULL, NULL, ''); INSERT INTO `sys_menu` VALUES (11616, '工作流', 0, 6, 'workflow', '', '', 1, 0, 'M', '0', '0', '', 'mdi:workflow-outline', 103, 1, '2026-01-05 14:39:33', 1, '2026-01-05 14:56:07', ''); INSERT INTO `sys_menu` VALUES (11618, '我的任务', 0, 7, 'task', '', '', 1, 0, 'M', '0', '0', '', 'carbon:task-approved', 103, 1, '2026-01-05 14:39:33', NULL, NULL, ''); INSERT INTO `sys_menu` VALUES (11619, '我的待办', 11618, 2, 'taskWaiting', 'workflow/task/taskWaiting', '', 1, 1, 'C', '0', '0', '', 'ri:todo-line', 103, 1, '2026-01-05 14:39:33', NULL, NULL, ''); @@ -3421,7 +3536,7 @@ CREATE TABLE `t_workflow_component` ( `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '000000' COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_display_order`(`display_order` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 20 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '工作流组件库 | Workflow Component' ROW_FORMAT = DYNAMIC; +) ENGINE = InnoDB AUTO_INCREMENT = 37 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '工作流组件库 | Workflow Component' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of t_workflow_component @@ -3429,6 +3544,9 @@ CREATE TABLE `t_workflow_component` ( INSERT INTO `t_workflow_component` VALUES (17, '5cd68dccbbb411f0bb7840c2ba9a7fbc', 'Start', '开始', '流程由此开始', 0, 1, '2025-11-07 16:32:49', '2025-11-07 16:32:49', 0, '000000'); INSERT INTO `t_workflow_component` VALUES (18, '5cd6ac69bbb411f0bb7840c2ba9a7fbc', 'End', '结束', '流程由此结束', 0, 1, '2025-11-07 16:32:49', '2025-11-07 16:32:49', 0, '000000'); INSERT INTO `t_workflow_component` VALUES (19, '5cd6c8eabbb411f0bb7840c2ba9a7fbc', 'Answer', '生成回答', '调用大语言模型回答问题', 0, 1, '2025-11-07 16:32:49', '2025-11-07 16:32:49', 0, '000000'); +INSERT INTO `t_workflow_component` VALUES (25, '0b4369bb60dc46d6bd84ceb4e36184dc', 'KeywordExtractor', '关键词提取', '从文本中提取关键词', 0, 1, '2025-12-26 16:30:05', '2025-12-26 16:30:05', 0, '000000'); +INSERT INTO `t_workflow_component` VALUES (26, 'bb00fc2f52c74fec82ee3f99725b56bb', 'Switcher', '条件分支', '根据条件执行不同分支', 0, 1, '2025-12-26 16:30:46', '2025-12-26 16:30:46', 0, '000000'); +INSERT INTO `t_workflow_component` VALUES (36, 'f37dbcb8f0d5464d90fbb22774490a56', 'HumanFeedback', '人类', '人机沟通', 0, 1, '2025-12-30 17:37:14', '2025-12-30 17:37:14', 0, '000000'); -- ---------------------------- -- Table structure for t_workflow_edge @@ -3647,162 +3765,3 @@ INSERT INTO `test_tree` VALUES (12, '000000', 10, 108, 3, '子节点88', 0, 103, INSERT INTO `test_tree` VALUES (13, '000000', 10, 108, 3, '子节点99', 0, 103, '2026-02-03 05:14:54', 1, NULL, NULL, 0); SET FOREIGN_KEY_CHECKS = 1; - - --- MCP 模块数据库表结构 --- 版本: V3.0.0 --- 描述: MCP 工具管理和 MCP 市场管理表 - --- ---------------------------- --- MCP 工具表 --- ---------------------------- -DROP TABLE IF EXISTS `mcp_tool_info`; -CREATE TABLE `mcp_tool_info` -( - `id` bigint NOT NULL AUTO_INCREMENT COMMENT '工具ID', - `name` varchar(200) NOT NULL COMMENT '工具名称', - `description` text COMMENT '工具描述', - `type` varchar(20) DEFAULT 'LOCAL' COMMENT '工具类型:LOCAL-本地, REMOTE-远程, BUILTIN-内置', - `status` varchar(20) DEFAULT 'ENABLED' COMMENT '状态:ENABLED-启用, DISABLED-禁用', - `config_json` text COMMENT '配置信息(JSON格式)', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', - `create_dept` bigint DEFAULT NULL COMMENT '创建部门', - `create_by` bigint DEFAULT NULL COMMENT '创建者', - `create_time` datetime DEFAULT NULL COMMENT '创建时间', - `update_by` bigint DEFAULT NULL COMMENT '更新者', - `update_time` datetime DEFAULT NULL COMMENT '更新时间', - `del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)', - PRIMARY KEY (`id`), - KEY `idx_name` (`name`), - KEY `idx_type` (`type`), - KEY `idx_status` (`status`), - KEY `idx_tenant_id` (`tenant_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='MCP工具表'; - --- ---------------------------- --- MCP 市场表 --- ---------------------------- -DROP TABLE IF EXISTS `mcp_market_info`; -CREATE TABLE `mcp_market_info` -( - `id` bigint NOT NULL AUTO_INCREMENT COMMENT '市场ID', - `name` varchar(200) NOT NULL COMMENT '市场名称', - `url` varchar(500) NOT NULL COMMENT '市场URL', - `description` text COMMENT '市场描述', - `auth_config` text COMMENT '认证配置(JSON格式)', - `status` varchar(20) DEFAULT 'ENABLED' COMMENT '状态:ENABLED-启用, DISABLED-禁用', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', - `create_dept` bigint DEFAULT NULL COMMENT '创建部门', - `create_by` bigint DEFAULT NULL COMMENT '创建者', - `create_time` datetime DEFAULT NULL COMMENT '创建时间', - `update_by` bigint DEFAULT NULL COMMENT '更新者', - `update_time` datetime DEFAULT NULL COMMENT '更新时间', - `del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)', - PRIMARY KEY (`id`), - KEY `idx_name` (`name`), - KEY `idx_status` (`status`), - KEY `idx_tenant_id` (`tenant_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='MCP市场表'; - --- ---------------------------- --- MCP 市场工具关联表 --- ---------------------------- -DROP TABLE IF EXISTS `mcp_market_tool`; -CREATE TABLE `mcp_market_tool` -( - `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID', - `market_id` bigint NOT NULL COMMENT '市场ID', - `tool_name` varchar(200) NOT NULL COMMENT '工具名称', - `tool_description` text COMMENT '工具描述', - `tool_version` varchar(50) COMMENT '工具版本', - `tool_metadata` json COMMENT '工具元数据(JSON格式)', - `is_loaded` tinyint(1) DEFAULT 0 COMMENT '是否已加载到本地', - `local_tool_id` bigint COMMENT '关联的本地工具ID', - `create_time` datetime DEFAULT NULL COMMENT '创建时间', - PRIMARY KEY (`id`), - KEY `idx_market_id` (`market_id`), - KEY `idx_tool_name` (`tool_name`), - KEY `idx_is_loaded` (`is_loaded`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='MCP市场工具关联表'; - - - --- MCP 模块菜单权限 SQL --- 版本: V3.0.1 --- 描述: MCP 工具管理和 MCP 市场管理菜单权限 --- 菜单 ID 规划: 2000-2199 - --- ---------------------------- --- MCP 主菜单 --- ---------------------------- -INSERT INTO sys_menu -VALUES (2000, 'MCP管理', 0, 5, 'mcp', '', '', 1, 0, 'M', '0', '0', '', - 'mdi:robot-industrial', 103, 1, NOW(), NULL, NULL, 'MCP模块管理菜单'); - --- ---------------------------- --- MCP 工具管理 --- ---------------------------- -INSERT INTO sys_menu -VALUES (2001, 'MCP工具管理', 2000, 1, 'tool', 'mcp/tool/index', '', 1, 0, 'C', '0', - '0', 'mcp:tool:list', 'material-symbols:tools-hammer-outline', 103, 1, NOW(), NULL, - NULL, 'MCP工具管理菜单'); - --- MCP 工具管理按钮权限 -INSERT INTO sys_menu -VALUES (2002, 'MCP工具查询', 2001, 1, '#', '', '', 1, 0, 'F', '0', '0', - 'mcp:tool:query', '#', 103, 1, NOW(), NULL, NULL, ''); -INSERT INTO sys_menu -VALUES (2003, 'MCP工具新增', 2001, 2, '#', '', '', 1, 0, 'F', '0', '0', - 'mcp:tool:add', '#', 103, 1, NOW(), NULL, NULL, ''); -INSERT INTO sys_menu -VALUES (2004, 'MCP工具修改', 2001, 3, '#', '', '', 1, 0, 'F', '0', '0', - 'mcp:tool:edit', '#', 103, 1, NOW(), NULL, NULL, ''); -INSERT INTO sys_menu -VALUES (2005, 'MCP工具删除', 2001, 4, '#', '', '', 1, 0, 'F', '0', '0', - 'mcp:tool:remove', '#', 103, 1, NOW(), NULL, NULL, ''); -INSERT INTO sys_menu -VALUES (2006, 'MCP工具测试', 2001, 5, '#', '', '', 1, 0, 'F', '0', '0', - 'mcp:tool:test', '#', 103, 1, NOW(), NULL, NULL, ''); -INSERT INTO sys_menu -VALUES (2007, 'MCP工具导出', 2001, 6, '#', '', '', 1, 0, 'F', '0', '0', - 'mcp:tool:export', '#', 103, 1, NOW(), NULL, NULL, ''); - --- ---------------------------- --- MCP 市场管理 --- ---------------------------- -INSERT INTO sys_menu -VALUES (2010, 'MCP市场管理', 2000, 2, 'market', 'mcp/market/index', '', 1, 0, 'C', '0', - '0', 'mcp:market:list', 'mdi:storefront-outline', 103, 1, NOW(), NULL, NULL, - 'MCP市场管理菜单'); - --- MCP 市场管理按钮权限 -INSERT INTO sys_menu -VALUES (2011, 'MCP市场查询', 2010, 1, '#', '', '', 1, 0, 'F', '0', '0', - 'mcp:market:query', '#', 103, 1, NOW(), NULL, NULL, ''); -INSERT INTO sys_menu -VALUES (2012, 'MCP市场新增', 2010, 2, '#', '', '', 1, 0, 'F', '0', '0', - 'mcp:market:add', '#', 103, 1, NOW(), NULL, NULL, ''); -INSERT INTO sys_menu -VALUES (2013, 'MCP市场修改', 2010, 3, '#', '', '', 1, 0, 'F', '0', '0', - 'mcp:market:edit', '#', 103, 1, NOW(), NULL, NULL, ''); -INSERT INTO sys_menu -VALUES (2014, 'MCP市场删除', 2010, 4, '#', '', '', 1, 0, 'F', '0', '0', - 'mcp:market:remove', '#', 103, 1, NOW(), NULL, NULL, ''); -INSERT INTO sys_menu -VALUES (2015, 'MCP市场刷新', 2010, 5, '#', '', '', 1, 0, 'F', '0', '0', - 'mcp:market:refresh', '#', 103, 1, NOW(), NULL, NULL, ''); -INSERT INTO sys_menu -VALUES (2016, 'MCP工具加载', 2010, 6, '#', '', '', 1, 0, 'F', '0', '0', - 'mcp:market:load', '#', 103, 1, NOW(), NULL, NULL, ''); -INSERT INTO sys_menu -VALUES (2017, 'MCP市场导出', 2010, 7, '#', '', '', 1, 0, 'F', '0', '0', - 'mcp:market:export', '#', 103, 1, NOW(), NULL, NULL, ''); - --- ---------------------------- --- MCP 配置管理 (可选,预留扩展) --- ---------------------------- --- INSERT INTO sys_menu VALUES (2020, 'MCP配置管理', 2000, 3, 'config', 'mcp/config/index', '', 1, 0, 'C', '0', --- '0', 'mcp:config:list', 'ant-design:setting-outlined', 103, 1, NOW(), NULL, NULL, --- 'MCP配置管理菜单'); - - diff --git a/docs/script/sql/workFlow-v3.0.sql b/docs/script/sql/workFlow-v3.0.sql deleted file mode 100644 index b97366f2..00000000 --- a/docs/script/sql/workFlow-v3.0.sql +++ /dev/null @@ -1,6 +0,0 @@ -INSERT INTO `mysql`.`t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (17, '5cd68dccbbb411f0bb7840c2ba9a7fbc', 'Start', '开始', '流程由此开始', 0, 1, '2025-11-07 16:32:49', '2025-11-07 16:32:49', 0, '000000'); -INSERT INTO `mysql`.`t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (18, '5cd6ac69bbb411f0bb7840c2ba9a7fbc', 'End', '结束', '流程由此结束', 0, 1, '2025-11-07 16:32:49', '2025-11-07 16:32:49', 0, '000000'); -INSERT INTO `mysql`.`t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (19, '5cd6c8eabbb411f0bb7840c2ba9a7fbc', 'Answer', '生成回答', '调用大语言模型回答问题', 0, 1, '2025-11-07 16:32:49', '2025-11-07 16:32:49', 0, '000000'); -INSERT INTO `mysql`.`t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (25, '0b4369bb60dc46d6bd84ceb4e36184dc', 'KeywordExtractor', '关键词提取', '从文本中提取关键词', 0, 1, '2025-12-26 16:30:05', '2025-12-26 16:30:05', 0, '000000'); -INSERT INTO `mysql`.`t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (26, 'bb00fc2f52c74fec82ee3f99725b56bb', 'Switcher', '条件分支', '根据条件执行不同分支', 0, 1, '2025-12-26 16:30:46', '2025-12-26 16:30:46', 0, '000000'); -INSERT INTO `mysql`.`t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (36, 'f37dbcb8f0d5464d90fbb22774490a56', 'HumanFeedback', '人类', '人机沟通', 0, 1, '2025-12-30 17:37:14', '2025-12-30 17:37:14', 0, '000000'); diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index 8bcc34bb..20e1ee87 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -58,9 +58,9 @@ spring: driverClassName: com.mysql.cj.jdbc.Driver # jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562 # rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题) - url: jdbc:mysql://127.0.0.1:3306/mysql?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true + url: jdbc:mysql://127.0.0.1:3306/ruoyi-ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true username: root - password: 123456 + password: root hikari: # 最大连接池数量 maxPoolSize: 20 @@ -87,7 +87,7 @@ agent: sys: upload: path: D:\\DownLoad - + --- # redis 单机配置(单机与集群只能开启一个另一个需要注释掉) spring.data: redis: From c78b1a14a9fdaacecfddb27f49a0c8f0e091a32a Mon Sep 17 00:00:00 2001 From: ageerle Date: Wed, 25 Feb 2026 21:20:22 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0ppio=E5=8E=82?= =?UTF-8?q?=E5=95=86=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/ruoyi/enums/ChatModeType.java | 1 + .../chat/impl/provider/PPIOServiceImpl.java | 50 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/PPIOServiceImpl.java diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/ChatModeType.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/ChatModeType.java index 3312388b..a36438fe 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/ChatModeType.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/ChatModeType.java @@ -14,6 +14,7 @@ public enum ChatModeType { ZHI_PU("zhipu", "智谱清言"), DEEP_SEEK("deepseek", "深度求索"), QIAN_WEN("qianwen", "通义千问"), + PPIO("ppio", "PPIO派欧云"), OPEN_AI("openai", "openai"); private final String code; private final String description; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/PPIOServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/PPIOServiceImpl.java new file mode 100644 index 00000000..d6c0053b --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/PPIOServiceImpl.java @@ -0,0 +1,50 @@ +package org.ruoyi.service.chat.impl.provider; + + +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.model.chat.StreamingChatModel; +import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; +import dev.langchain4j.model.openai.OpenAiStreamingChatModel; +import lombok.extern.slf4j.Slf4j; +import org.ruoyi.common.chat.domain.dto.request.ChatRequest; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; +import org.ruoyi.enums.ChatModeType; +import org.ruoyi.service.chat.impl.AbstractStreamingChatService; +import org.springframework.stereotype.Service; + +import java.util.List; + + +/** + * PPIO服务调用 + * + * @author ageerle@163.com + * @date 2025/12/13 + */ +@Service +@Slf4j +public class PPIOServiceImpl extends AbstractStreamingChatService { + + @Override + protected StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) { + return OpenAiStreamingChatModel.builder() + .baseUrl(chatModelVo.getApiHost()) + .apiKey(chatModelVo.getApiKey()) + .modelName(chatModelVo.getModelName()) + .returnThinking(chatRequest.getEnableThinking()) + .build(); + } + + @Override + protected void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List messagesWithMemory, StreamingChatResponseHandler handler) { + StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo, chatRequest); + streamingChatModel.chat(messagesWithMemory, handler); + } + + + @Override + public String getProviderName() { + return ChatModeType.PPIO.getCode(); + } + +}