From e3fb25fba6be264bfb30fde9eee248a6f0a6ca82 Mon Sep 17 00:00:00 2001 From: evo <446796145@qq.com> Date: Wed, 4 Mar 2026 14:32:41 +0800 Subject: [PATCH 01/11] =?UTF-8?q?=E5=A2=9E=E5=8A=A0McpAgent=E7=A9=BA?= =?UTF-8?q?=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ruoyi-chat/src/main/java/org/ruoyi/agent/McpAgent.java | 1 + 1 file changed, 1 insertion(+) 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 6fc613f2..fda33dce 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 @@ -6,6 +6,7 @@ import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.V; public interface McpAgent { + /** * 系统提示词:通用工具调用智能体 * 不限定具体工具类型,让 LangChain4j 自动传递工具描述给 LLM From 0130028952f0b597eb2c709bb6f75702f0d72760 Mon Sep 17 00:00:00 2001 From: evo <446796145@qq.com> Date: Wed, 4 Mar 2026 16:34:56 +0800 Subject: [PATCH 02/11] =?UTF-8?q?fix:=20=E8=BF=98=E5=8E=9F=20McpAgent=20?= =?UTF-8?q?=E4=B8=BA=E9=80=9A=E7=94=A8=E5=B7=A5=E5=85=B7=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/ruoyi/agent/McpAgent.java | 45 +++++++++---------- 1 file changed, 20 insertions(+), 25 deletions(-) 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 28d64c4f..fda33dce 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 @@ -8,40 +8,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); } From 27211b67f968f48a1837d627cd6b13a9a79c1cf5 Mon Sep 17 00:00:00 2001 From: ageerle Date: Fri, 6 Mar 2026 10:20:15 +0800 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 55 ++++++++++++++++-------------------------- README_EN.md | 67 +++++++++++++++++++++++----------------------------- 2 files changed, 51 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 90ebdeed..3670c342 100644 --- a/README.md +++ b/README.md @@ -17,12 +17,9 @@ RuoYi AI Logo -## 功能建议&bug提交:【腾讯文档】 -https://docs.qq.com/sheet/DR3hoR3FVVkpJcnVm - ### 企业级AI助手平台 -*开箱即用的全栈AI平台,集成Coze、DIFY等主流AI平台,提供先进的RAG技术、知识图谱、数字人和AI流程编排能力* +*开箱即用的全栈AI平台,支持多智能体协同、Supervisor模式编排、多种决策模型,提供先进的RAG技术和可视化流程编排能力* **[English](README_EN.md)** | **[📖 使用文档](https://doc.pandarobot.chat)** | **[🚀 在线体验](https://web.pandarobot.chat)** | **[🐛 问题反馈](https://github.com/ageerle/ruoyi-ai/issues)** | **[💡 功能建议](https://github.com/ageerle/ruoyi-ai/issues)** @@ -34,34 +31,23 @@ https://docs.qq.com/sheet/DR3hoR3FVVkpJcnVm ## ✨ 核心亮点 -### 智能AI引擎 -- **多模型接入**:支持 OpenAI、DeepSeek、通义千问、智谱AI 等主流厂商的模型 -- **多模态理解**:支持文本、图片、文档等多种格式的智能处理 -- **AI平台集成**:集成了 **扣子(Coze)**、**DIFY**、**FastGPT** 等主流AI应用平台 -- **MCP能力集成**:基于模型上下文协议,打造可扩展的AI工具生态系统 -- **AI编程助手**:内置智能代码分析和项目脚手架生成能力 - -### 本地化RAG方案 -- **私有知识库**:基于 Langchain4j 框架 + BGE-large-zh-v1.5 中文向量模型实现本地私有知识库 -- **多种向量库**:支持 Milvus、Weaviate、Qdrant 等主流向量数据库 -- **数据安全可控**:支持完全本地部署,保护企业数据隐私 -- **灵活模型部署**:兼容 Ollama、vLLM 等本地推理框架 - -### AI创作工具 -- **AI 绘画创作**: 集成 MidJourney、GPT-4o-image -- **智能PPT生成**:一键将文本内容转换为精美演示文稿 - -### 知识图谱与智能编排 -- **知识图谱构建**:自动从文档和对话中提取实体关系,构建可视化知识网络 -- **AI 流程编排**:可视化工作流设计器,支持复杂AI任务的编排和自动化执行 -- **数字人交互**:集成数字人形象,提供更自然的人机交互体验 +| 模块 | 现有能力 | 扩展方向 | +|:--------:|----------------------------------------------------------|-----------------------| +| **模型管理** | 多模型接入(OpenAI/DeepSeek/通义/智谱)、多模态理解、Coze/DIFY/FastGPT平台集成 | 自动模式、容错机制 | +| **知识库** | 本地RAG + 向量库(Milvus/Weaviate) + 知识图谱 + 文档解析 +重排序 | 音频视频解析、知识出处 | +| **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态 | 工具插件市场、toolAgent自动加载工具 | +| **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型(支持rag)调用,邮件发送,人工审核等节点 | 更多节点类型 | +| **多智能体** | 基于Langchain4j的Agent框架、Supervisor模式编排,支持多种决策模型 | 智能体可配置 | +| **AI编程** | 智能代码分析、项目脚手架生成、Copilot助手 | 代码生成优化 | ## 🚀 快速体验 ### 在线演示 -- **用户端体验**:[web.pandarobot.chat](https://web.pandarobot.chat) (账号:admin 密码:admin123) -- **管理后台**:[admin.pandarobot.chat](https://admin.pandarobot.chat) (账号:admin 密码:admin123) +| 平台 | 地址 | 账号 | +|:------:|---|---| +| 用户端 | [web.pandarobot.chat](https://web.pandarobot.chat) | admin / admin123 | +| 管理后台 | [admin.pandarobot.chat](https://admin.pandarobot.chat) | admin / admin123 | ### 项目源码 @@ -71,18 +57,19 @@ https://docs.qq.com/sheet/DR3hoR3FVVkpJcnVm | 🎨 用户前端 | [ruoyi-web](https://github.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitee.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitcode.com/ageerle/ruoyi-web) | | 🛠️ 管理后台 | [ruoyi-admin](https://github.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitee.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitcode.com/ageerle/ruoyi-admin) | +### 合作项目 +| 项目名称 | GitHub 仓库 | Gitee 仓库 +|----------------|-------------------------------------------------------|------------------------------------------------------| +| element-plus-x | [element-plus-x](https://github.com/element-plus-x/Element-Plus-X) | [element-plus-x](https://gitee.com/he-jiayue/element-plus-x) | + ## 🛠️ 技术架构 ### 核心框架 -- **后端架构**:Spring Boot 3.5 + Langchain4j -- **数据存储**:MySQL 8.0 + Redis + 向量数据库(Milvus/Weaviate/Qdrant) -- **前端技术**:Vue 3 + Vben Admin + Element UI +- **后端架构**:Spring Boot 4.0 + Spring ai 2.0 + Langchain4j +- **数据存储**:MySQL 8.0 + Redis + 向量数据库(Milvus/Weaviate) +- **前端技术**:Vue 3 + Vben Admin + element-plus-x - **安全认证**:Sa-Token + JWT 双重保障 -### 系统组件 -- **文档处理**:PDF、Word、Excel 解析,图像智能分析 -- **实时通信**:WebSocket 实时通信,SSE 流式响应 -- **系统监控**:完善的日志体系、性能监控、服务健康检查 ## 📚 使用文档 diff --git a/README_EN.md b/README_EN.md index 16e3dcd4..a98746a1 100644 --- a/README_EN.md +++ b/README_EN.md @@ -19,64 +19,57 @@ ### Enterprise-Grade AI Assistant Platform -*An out-of-the-box intelligent AI platform that integrates mainstream AI platforms such as Coze and DIFY, providing advanced RAG technology, knowledge graphs, digital humans, and AI workflow orchestration capabilities* +*An out-of-the-box full-stack AI platform supporting multi-agent collaboration, Supervisor mode orchestration, and multiple decision models, with advanced RAG technology and visual workflow orchestration capabilities* **[中文](README.md)** | **[📖 Documentation](https://doc.pandarobot.chat)** | **[🚀 Live Demo](https://web.pandarobot.chat)** | **[🐛 Report Issues](https://github.com/ageerle/ruoyi-ai/issues)** | **[💡 Feature Requests](https://github.com/ageerle/ruoyi-ai/issues)** + + + ## ✨ Core Features -### Intelligent AI Engine -- **Multi-Model Integration**: Supports mainstream LLM providers including OpenAI, DeepSeek, Alibaba's Tongyi Qianwen, and Zhipu AI -- **Multi-Modal Understanding**: Intelligently processes multiple formats including text, images, and documents -- **AI Platform Integration**: Integrates mainstream AI application platforms like **Coze**, **DIFY**, and **FastGPT** -- **MCP Capability Integration**: Build an extensible AI toolkit ecosystem based on the Model Context Protocol -- **AI Coding Assistant**: Built-in intelligent code analysis and project scaffolding generation capabilities - -### Local RAG Solution -- **Private Knowledge Base**: Implements local private knowledge base based on Langchain4j framework + BGE-large-zh-v1.5 Chinese vector model -- **Multiple Vector Databases**: Supports mainstream vector databases including Milvus, Weaviate, and Qdrant -- **Data Security & Privacy**: Supports fully local deployment to protect enterprise data privacy -- **Flexible Model Deployment**: Compatible with local inference frameworks like Ollama and vLLM - -### AI Creative Tools -- **AI Image Generation**: Integrates MidJourney and GPT-4o-image -- **Intelligent PPT Generation**: Convert text content to beautiful presentations with one click - -### Knowledge Graph & Intelligent Orchestration -- **Knowledge Graph Construction**: Automatically extract entity relationships from documents and conversations, build visualized knowledge networks -- **AI Workflow Orchestration**: Visual workflow designer supporting complex AI task orchestration and automated execution -- **Digital Human Interaction**: Integrated digital avatars providing more natural human-machine interaction experience +| Module | Current Capabilities | Extension Direction | +|:--------:|---|---| +| **Model Management** | Multi-model integration (OpenAI/DeepSeek/Tongyi/Zhipu), multi-modal understanding, Coze/DIFY/FastGPT platform integration | Auto mode, fault tolerance | +| **Knowledge Base** | Local RAG + Vector DB (Milvus/Weaviate) + Knowledge Graph + Document parsing + Reranking | Audio/video parsing, knowledge source | +| **Tool Management** | MCP protocol integration, Skills capability + Extensible tool ecosystem | Tool plugin marketplace, toolAgent auto-loading | +| **Workflow Orchestration** | Visual workflow designer, drag-and-drop node orchestration, SSE streaming execution, currently supports model (with RAG) calls, email sending, manual review nodes | More node types | +| **Multi-Agent** | Agent framework based on Langchain4j, Supervisor mode orchestration, supports multiple decision models | Configurable agents | +| **AI Coding** | Intelligent code analysis, project scaffolding generation, Copilot assistant | Code generation optimization | ## 🚀 Quick Start ### Live Demo -- **User Experience**: [web.pandarobot.chat](https://web.pandarobot.chat) (Username: admin, Password: admin123) -- **Admin Dashboard**: [admin.pandarobot.chat](https://admin.pandarobot.chat) (Username: admin, Password: admin123) +| Platform | URL | Account | +|:------:|---|---| +| User Frontend | [web.pandarobot.chat](https://web.pandarobot.chat) | admin / admin123 | +| Admin Panel | [admin.pandarobot.chat](https://admin.pandarobot.chat) | admin / admin123 | ### Project Repositories -| Module | GitHub Repository | Gitee Repository | GitCode Repository | -|------------------|-------------------------------------------------------|------------------------------------------------------|--------------------------------------------------------| -| 🔧 Backend | [ruoyi-ai](https://github.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitee.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitcode.com/ageerle/ruoyi-ai) | -| 🎨 User Frontend | [ruoyi-web](https://github.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitee.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitcode.com/ageerle/ruoyi-web) | -| 🛠️ Admin Panel | [ruoyi-admin](https://github.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitee.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitcode.com/ageerle/ruoyi-admin) | +| Module | GitHub Repository | Gitee Repository | GitCode Repository | +|----------|-------------------------------------------------------|------------------------------------------------------|--------------------------------------------------------| +| 🔧 Backend | [ruoyi-ai](https://github.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitee.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitcode.com/ageerle/ruoyi-ai) | +| 🎨 User Frontend | [ruoyi-web](https://github.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitee.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitcode.com/ageerle/ruoyi-web) | +| 🛠️ Admin Panel | [ruoyi-admin](https://github.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitee.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitcode.com/ageerle/ruoyi-admin) | + +### Partner Projects +| Project Name | GitHub Repository | Gitee Repository +|----------------|-------------------------------------------------------|------------------------------------------------------| +| element-plus-x | [element-plus-x](https://github.com/element-plus-x/Element-Plus-X) | [element-plus-x](https://gitee.com/he-jiayue/element-plus-x) | ## 🛠️ Technical Architecture ### Core Framework -- **Backend**: Spring Boot 3.5 + Langchain4j -- **Data Storage**: MySQL 8.0 + Redis + Vector Databases (Milvus/Weaviate/Qdrant) -- **Frontend**: Vue 3 + Vben Admin + Element UI +- **Backend**: Spring Boot 4.0 + Spring AI 2.0 + Langchain4j +- **Data Storage**: MySQL 8.0 + Redis + Vector Databases (Milvus/Weaviate) +- **Frontend**: Vue 3 + Vben Admin + element-plus-x - **Security**: Sa-Token + JWT dual-layer security -### System Components -- **Document Processing**: PDF, Word, and Excel parsing with intelligent image analysis -- **Real-Time Communication**: WebSocket real-time communication with SSE streaming responses -- **System Monitoring**: Comprehensive logging system, performance monitoring, and service health checks ## 📚 Documentation @@ -170,4 +163,4 @@ Thanks to the following excellent open-source projects for their support: [license-shield]: https://img.shields.io/github/license/ageerle/ruoyi-ai.svg?style=flat-square -[license-url]: https://github.com/ageerle/ruoyi-ai/blob/main/LICENSE \ No newline at end of file +[license-url]: https://github.com/ageerle/ruoyi-ai/blob/main/LICENSE From 4977df0ba8d627b412a2c77e7154a876547171ce Mon Sep 17 00:00:00 2001 From: ageerle Date: Fri, 6 Mar 2026 10:27:45 +0800 Subject: [PATCH 04/11] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +++--- README_EN.md | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3670c342..003e6971 100644 --- a/README.md +++ b/README.md @@ -31,12 +31,12 @@ ## ✨ 核心亮点 -| 模块 | 现有能力 | 扩展方向 | -|:--------:|----------------------------------------------------------|-----------------------| +| 模块 | 现有能力 | 扩展方向 | +|:---:|---|---| | **模型管理** | 多模型接入(OpenAI/DeepSeek/通义/智谱)、多模态理解、Coze/DIFY/FastGPT平台集成 | 自动模式、容错机制 | | **知识库** | 本地RAG + 向量库(Milvus/Weaviate) + 知识图谱 + 文档解析 +重排序 | 音频视频解析、知识出处 | | **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态 | 工具插件市场、toolAgent自动加载工具 | -| **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型(支持rag)调用,邮件发送,人工审核等节点 | 更多节点类型 | +| **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点 | 更多节点类型 | | **多智能体** | 基于Langchain4j的Agent框架、Supervisor模式编排,支持多种决策模型 | 智能体可配置 | | **AI编程** | 智能代码分析、项目脚手架生成、Copilot助手 | 代码生成优化 | diff --git a/README_EN.md b/README_EN.md index a98746a1..3ae559e1 100644 --- a/README_EN.md +++ b/README_EN.md @@ -31,8 +31,8 @@ ## ✨ Core Features -| Module | Current Capabilities | Extension Direction | -|:--------:|---|---| +| Module | Current Capabilities | Extension Direction | +|:---:|---|---| | **Model Management** | Multi-model integration (OpenAI/DeepSeek/Tongyi/Zhipu), multi-modal understanding, Coze/DIFY/FastGPT platform integration | Auto mode, fault tolerance | | **Knowledge Base** | Local RAG + Vector DB (Milvus/Weaviate) + Knowledge Graph + Document parsing + Reranking | Audio/video parsing, knowledge source | | **Tool Management** | MCP protocol integration, Skills capability + Extensible tool ecosystem | Tool plugin marketplace, toolAgent auto-loading | From dc1e84bc528d217a66d35bb9105ac3afb977b53c Mon Sep 17 00:00:00 2001 From: ageerle Date: Fri, 6 Mar 2026 10:57:53 +0800 Subject: [PATCH 05/11] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=BC=81?= =?UTF-8?q?=E4=B8=9AAI=E5=BA=94=E7=94=A8=E5=90=88=E4=BD=9C=E7=99=BB?= =?UTF-8?q?=E8=AE=B0Issue=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增企业合作登记模板,用于收集企业AI应用需求 - 包含基本信息、AI应用需求、联系方式三个模块 - 预设筛选字段便于评估合作匹配度 Co-Authored-By: Claude Opus 4.6 --- .github/ISSUE_TEMPLATE/config.yml | 8 +++ .../enterprise-collaboration.md | 57 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/enterprise-collaboration.md diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..2c53ddd7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: 📖 项目文档 + url: https://solon.noear.org/article/cases + about: 查看项目文档和案例展示 + - name: 💬 技术交流 + url: https://github.com/NAWWWI/ruoyi-ai/discussions + about: 参与技术讨论和经验分享 diff --git a/.github/ISSUE_TEMPLATE/enterprise-collaboration.md b/.github/ISSUE_TEMPLATE/enterprise-collaboration.md new file mode 100644 index 00000000..325bc0a8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enterprise-collaboration.md @@ -0,0 +1,57 @@ +--- +name: 企业AI应用合作登记 +about: 登记企业信息与合作意向,获取免费技术支持 +title: '[合作登记] ' +labels: '合作登记' +assignees: '' +--- + +## 登记说明 + +感谢您关注 RuoYi AI!我们团队今年的重点是帮助企业落地AI应用,如果贵公司符合要求,我们可以提供**免费的技术支持**。请填写以下信息,我们会尽快与您联系。 + +--- + +## 基本信息 + +| 字段 | 内容 | +|------|------| +| 公司名称 | | +| 所属行业 | | +| 公司规模 | □ 1-50人 □ 51-200人 □ 201-500人 □ 500人以上 | +| 公司所在地 | | +| 公司简介(可选) | | +| 公司官网(可选) | | + +--- + +## AI应用需求 + +| 字段 | 内容 | +|------|------| +| 当前AI应用状态 | □ 尚未开始 □ 规划中 □ 已有初步应用 □ 已有成熟应用 | +| 计划落地时间 | □ 1个月内 □ 1-3个月 □ 3-6个月 □ 6个月以上 | +| 期望的AI应用场景 | (如:智能客服、文档处理、知识库问答、智能助手等) | +| 当前痛点或挑战 | | +| 期望获得的支持 | □ 技术咨询 □ 架构设计 □ 代码支持 □ 部署指导 □ 其他 | + +--- + +## 联系方式 + +| 字段 | 内容 | +|------|------| +| 联系人姓名 | | +| 联系人职位 | | +| 联系方式 | (手机/微信/邮箱,至少填一项) | +| 最佳联系时间(可选) | | + +--- + +## 补充说明 + +如有其他补充信息,请在此填写: + +--- + +> **温馨提示**:提交的信息仅用于合作沟通,不会对外公开。 From 0690156362b508f725a3455008a1c842dba779b4 Mon Sep 17 00:00:00 2001 From: ageerle Date: Fri, 6 Mar 2026 11:19:27 +0800 Subject: [PATCH 06/11] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96=E4=BC=81?= =?UTF-8?q?=E4=B8=9A=E5=90=88=E4=BD=9C=E7=99=BB=E8=AE=B0=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 改为评论登记模式,用户在Issue下评论填写 - 提供格式预览和可复制模板 - 新增公司Logo和项目Logo字段 - 移除联系方式模块,保护隐私 Co-Authored-By: Claude Opus 4.6 --- .../enterprise-collaboration.md | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/enterprise-collaboration.md b/.github/ISSUE_TEMPLATE/enterprise-collaboration.md index 325bc0a8..21e8e335 100644 --- a/.github/ISSUE_TEMPLATE/enterprise-collaboration.md +++ b/.github/ISSUE_TEMPLATE/enterprise-collaboration.md @@ -1,56 +1,56 @@ --- name: 企业AI应用合作登记 about: 登记企业信息与合作意向,获取免费技术支持 -title: '[合作登记] ' +title: '[合作登记] 企业AI应用需求登记' labels: '合作登记' assignees: '' --- ## 登记说明 -感谢您关注 RuoYi AI!我们团队今年的重点是帮助企业落地AI应用,如果贵公司符合要求,我们可以提供**免费的技术支持**。请填写以下信息,我们会尽快与您联系。 +感谢您关注 RuoYi AI!我们团队今年的重点是帮助企业落地AI应用,如果贵公司符合要求,我们可以提供**免费的技术支持**。 + +请在下方评论中填写登记信息,格式如下: --- -## 基本信息 +## 登记格式预览 | 字段 | 内容 | -|------|------| -| 公司名称 | | -| 所属行业 | | -| 公司规模 | □ 1-50人 □ 51-200人 □ 201-500人 □ 500人以上 | -| 公司所在地 | | -| 公司简介(可选) | | -| 公司官网(可选) | | - ---- - -## AI应用需求 - -| 字段 | 内容 | -|------|------| +|:---|:---| +| 公司名称 | (必填) | +| 公司Logo地址 | (可选) | +| 所属行业 | (必填) | +| 公司所在地 | (必填) | +| 项目名称 | (必填) | +| 项目Logo地址 | (可选) | +| 项目简介 | (必填) | | 当前AI应用状态 | □ 尚未开始 □ 规划中 □ 已有初步应用 □ 已有成熟应用 | | 计划落地时间 | □ 1个月内 □ 1-3个月 □ 3-6个月 □ 6个月以上 | -| 期望的AI应用场景 | (如:智能客服、文档处理、知识库问答、智能助手等) | -| 当前痛点或挑战 | | -| 期望获得的支持 | □ 技术咨询 □ 架构设计 □ 代码支持 □ 部署指导 □ 其他 | +| 当前痛点或挑战 | (可选) | +| 公司简介 | (可选) | --- -## 联系方式 +## 可复制格式 +复制下方内容并在评论中填写: + +``` | 字段 | 内容 | -|------|------| -| 联系人姓名 | | -| 联系人职位 | | -| 联系方式 | (手机/微信/邮箱,至少填一项) | -| 最佳联系时间(可选) | | - ---- - -## 补充说明 - -如有其他补充信息,请在此填写: +|:---|:---| +| 公司名称 | | +| 公司Logo地址 | | +| 所属行业 | | +| 公司所在地 | | +| 项目名称 | | +| 项目Logo地址 | | +| 项目简介 | | +| 当前AI应用状态 | | +| 计划落地时间 | | +| 当前痛点或挑战 | | +| 公司简介 | | +``` --- From b85c17a126923511eeddaf2682de97acdc65fdf4 Mon Sep 17 00:00:00 2001 From: ageerle Date: Fri, 6 Mar 2026 11:39:19 +0800 Subject: [PATCH 07/11] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84Issue?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E4=B8=BA=E9=80=9A=E7=94=A8=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除企业合作登记模板 - 新增漏洞报告模板 - 新增想法建议模板 - 新增自定义模板 Co-Authored-By: Claude Opus 4.6 --- .github/ISSUE_TEMPLATE/bug_report.md | 41 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/custom.md | 15 +++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 31 +++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/custom.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..251341d2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,41 @@ +--- +name: 漏洞报告 +about: 报告项目中的Bug或安全问题 +title: '[Bug] ' +labels: 'bug' +assignees: '' +--- + +## 问题描述 + +简要描述遇到的问题: + +## 复现步骤 + +1. +2. +3. + +## 期望行为 + +描述你期望发生的情况: + +## 实际行为 + +描述实际发生的情况: + +## 环境信息 + +| 项目 | 信息 | +|:---|:---| +| 操作系统 | | +| JDK版本 | | +| 项目版本 | | + +## 截图/日志 + +如有相关信息,请在此粘贴: + +## 补充说明 + +其他补充信息: diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 00000000..af6ecc3e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,15 @@ +--- +name: 自定义 +about: 其他问题或讨论 +title: '' +labels: '' +assignees: '' +--- + +## 描述 + +请详细描述你的问题或需求: + +## 补充信息 + +如有其他补充,请在此填写: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..403ca8c9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,31 @@ +--- +name: 想法建议 +about: 提出新功能建议或改进想法 +title: '[Feature] ' +labels: 'enhancement' +assignees: '' +--- + +## 建议类型 + +□ 新功能 □ 功能改进 □ 文档完善 □ 其他 + +## 建议描述 + +清晰描述你的建议内容: + +## 使用场景 + +描述这个功能在什么场景下会用到: + +## 期望效果 + +描述你期望的效果: + +## 参考示例 + +如有类似的参考实现或产品,请提供链接: + +## 补充说明 + +其他补充信息: From 6c4c661516896e5caf27d6d505d448a23595b2e5 Mon Sep 17 00:00:00 2001 From: ageerle Date: Fri, 6 Mar 2026 11:43:54 +0800 Subject: [PATCH 08/11] =?UTF-8?q?chore:=20=E7=A7=BB=E9=99=A4=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E6=96=87=E6=A1=A3=E5=92=8C=E6=8A=80=E6=9C=AF=E4=BA=A4?= =?UTF-8?q?=E6=B5=81=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- .github/ISSUE_TEMPLATE/config.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 2c53ddd7..0086358d 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1 @@ blank_issues_enabled: true -contact_links: - - name: 📖 项目文档 - url: https://solon.noear.org/article/cases - about: 查看项目文档和案例展示 - - name: 💬 技术交流 - url: https://github.com/NAWWWI/ruoyi-ai/discussions - about: 参与技术讨论和经验分享 From f160ec714b1c33a2264b6bf00cb21ec39c781128 Mon Sep 17 00:00:00 2001 From: evo <446796145@qq.com> Date: Sat, 7 Mar 2026 15:53:06 +0800 Subject: [PATCH 09/11] =?UTF-8?q?feat-=E6=81=A2=E5=A4=8Dmcp=E6=A8=A1?= =?UTF-8?q?=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 | 379 ++++++++------- pom.xml | 17 +- ruoyi-admin/pom.xml | 6 + .../src/main/resources/application.yml | 13 + ruoyi-common/ruoyi-common-excel/pom.xml | 6 + .../common/sse/utils/SseMessageUtils.java | 3 - .../web/handler/GlobalExceptionHandler.java | 20 +- ruoyi-modules/pom.xml | 1 + ruoyi-modules/ruoyi-chat/pom.xml | 13 +- .../impl/provider/QianWenChatServiceImpl.java | 120 ++--- 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 +++++ 43 files changed, 3718 insertions(+), 275 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 805c932d..589d8780 100644 --- a/docs/script/sql/ruoyi-ai-v3_mysql8.sql +++ b/docs/script/sql/ruoyi-ai-v3_mysql8.sql @@ -103,8 +103,8 @@ CREATE TABLE `chat_model` ( -- ---------------------------- -- Records of chat_model -- ---------------------------- -INSERT INTO `chat_model` VALUES (2000585866022060033, 'chat', 'deepseek/deepseek-v3.2', 'ppio', 'deepseek', 1, '1', 'Y', 'Y', 1, 'https://api.ppinfra.com/openai', 'sk_xx', 103, 1, '2025-12-15 23:16:54', 1, '2026-02-25 21:46:08', 'DeepSeek-V3.2 是一款在高效推理、复杂推理能力与智能体场景中表现突出的领先模型。其基于 DeepSeek Sparse Attention(DSA)稀疏注意力机制,在显著降低计算开销的同时优化长上下文性能;通过可扩展强化学习框架,整体能力达到 GPT-5 同级,高算力版本 V3.2-Speciale 更在推理表现上接近 Gemini-3.0-Pro;同时,模型依托大型智能体任务合成管线,具备更强的工具调用与多步骤决策能力,并在 2025 年 IMO 与 IOI 中取得金牌级表现。作为 MaaS 平台,我们已对 DeepSeek-V3.2 完成深度适配,通过动态调度、批处理加速、低延迟推理与企业级 SLA 保障,进一步增强其在企业生产环境中的稳定性、性价比与可控性,适用于搜索、问答、智能体、代码、数据处理等多类高价值场景。', 0); -INSERT INTO `chat_model` VALUES (2007528268536287233, 'vector', 'baai/bge-m3', 'ppio', 'bge-m3', 0, '1', 'N', 'Y', 1, 'https://api.ppinfra.com/openai', 'sk_xx', 103, 1, '2026-01-04 03:03:32', 1, '2026-02-25 21:15:14', 'bge-large-zh-v1.5', 0); +INSERT INTO `chat_model` VALUES (2000585866022060033, 'chat', 'deepseek/deepseek-v3.2', 'openai', 'deepseek', 1, '1', 'Y', 'Y', 1, 'https://api.ppinfra.com/openai', 'sk_xx', 103, 1, '2025-12-15 23:16:54', 1, '2026-02-06 01:02:31', 'DeepSeek-V3.2 是一款在高效推理、复杂推理能力与智能体场景中表现突出的领先模型。其基于 DeepSeek Sparse Attention(DSA)稀疏注意力机制,在显著降低计算开销的同时优化长上下文性能;通过可扩展强化学习框架,整体能力达到 GPT-5 同级,高算力版本 V3.2-Speciale 更在推理表现上接近 Gemini-3.0-Pro;同时,模型依托大型智能体任务合成管线,具备更强的工具调用与多步骤决策能力,并在 2025 年 IMO 与 IOI 中取得金牌级表现。作为 MaaS 平台,我们已对 DeepSeek-V3.2 完成深度适配,通过动态调度、批处理加速、低延迟推理与企业级 SLA 保障,进一步增强其在企业生产环境中的稳定性、性价比与可控性,适用于搜索、问答、智能体、代码、数据处理等多类高价值场景。', 0); +INSERT INTO `chat_model` VALUES (2007528268536287233, 'vector', 'baai/bge-m3', 'openai', 'bge-m3', 0, '1', 'N', 'Y', 1, 'https://api.ppinfra.com/openai', 'sk_xx', 103, 1, '2026-01-04 03:03:32', 1, '2026-02-06 01:02:35', 'bge-large-zh-v1.5', 0); -- ---------------------------- -- Table structure for chat_provider @@ -137,11 +137,11 @@ CREATE TABLE `chat_provider` ( -- ---------------------------- -- Records of chat_provider -- ---------------------------- -INSERT INTO `chat_provider` VALUES (1, 'OpenAI', 'openai', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/01091be272334383a1efd9bc22b73ee6.png', 'OpenAI官方API服务商', 'https://api.openai.com', '0', 1, NULL, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:46:59', 'OpenAI厂商', NULL, '0', NULL, 0); -INSERT INTO `chat_provider` VALUES (2, '阿里云百炼', 'qianwen', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/de2aa7e649de44f3ba5c6380ac6acd04.png', '阿里云百炼大模型服务', 'https://dashscope.aliyuncs.com', '0', 2, NULL, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:49:13', '阿里云厂商', NULL, '0', NULL, 0); +INSERT INTO `chat_provider` VALUES (1, 'OpenAI', 'openai', 'https://ruoyi-ai-1254149996.cos.ap-guangzhou.myqcloud.com/2025/12/15/9d944a6abfcd46e2bd6e364f07202589.png', 'OpenAI官方API服务商', 'https://api.openai.com', '0', 1, NULL, '2025-12-14 21:48:11', '1', '1', '2026-01-22 15:05:55', 'OpenAI厂商', NULL, '0', NULL, 0); +INSERT INTO `chat_provider` VALUES (2, '阿里云百炼', 'qianwen', 'https://ruoyi-ai-1254149996.cos.ap-guangzhou.myqcloud.com/2025/12/15/039ad13f690649f0ade139f8c803727b.png', '阿里云百炼大模型服务', 'https://dashscope.aliyuncs.com', '0', 2, NULL, '2025-12-14 21:48:11', '1', '1', '2026-02-06 00:58:22', '阿里云厂商', NULL, '0', NULL, 0); INSERT INTO `chat_provider` VALUES (3, '智谱AI', 'zhipu', 'https://ruoyi-ai-1254149996.cos.ap-guangzhou.myqcloud.com/2025/12/15/a43e98fb7b3b4861b8caa6184e6fa40a.png', '智谱AI大模型服务', 'https://open.bigmodel.cn', '0', 3, NULL, '2025-12-14 21:48:11', '1', '1', '2026-02-06 00:49:14', '智谱AI厂商', NULL, '1', NULL, 0); -INSERT INTO `chat_provider` VALUES (5, 'ollama', 'ollama', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/afecabebc8014d80b0f06b4796a74c5d.png', 'ollama大模型', 'http://127.0.0.1:11434', '0', 5, NULL, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:48:48', 'ollama厂商', NULL, '0', NULL, 0); -INSERT INTO `chat_provider` VALUES (2000585060904435714, 'PPIO', 'ppio', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/049bb6a507174f73bba4b8d8b9e55b8a.png', 'api聚合厂商', 'https://api.ppinfra.com/openai', '0', 5, 103, '2025-12-15 23:13:42', '1', '1', '2026-02-25 20:49:01', 'api聚合厂商', NULL, '0', NULL, 0); +INSERT INTO `chat_provider` VALUES (5, 'ollama', 'ollama', 'https://ruoyi-ai-1254149996.cos.ap-guangzhou.myqcloud.com/2025/12/15/2ff984bc9e4249df992733b31959056b.png', 'ollama大模型', 'http://127.0.0.1:11434', '0', 5, NULL, '2025-12-14 21:48:11', '1', '1', '2025-12-15 00:49:05', 'ollama厂商', NULL, '0', NULL, 0); +INSERT INTO `chat_provider` VALUES (2000585060904435714, 'PPIO', 'ppio', 'https://ruoyi-ai-1254149996.cos.ap-guangzhou.myqcloud.com/2025/12/15/c4f8e304ce7740029b0024934d4625bc.png', 'api聚合厂商', 'https://api.ppinfra.com/openai', '0', 5, 103, '2025-12-15 23:13:42', '1', '1', '2026-01-02 00:54:45', 'api聚合厂商', NULL, '0', NULL, 0); -- ---------------------------- -- Table structure for chat_session @@ -1412,90 +1412,6 @@ 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 AUTO_INCREMENT = 1 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 AUTO_INCREMENT = 1 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 -- ---------------------------- @@ -1508,7 +1424,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 @@ -1533,7 +1449,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 @@ -1579,7 +1495,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 @@ -1601,7 +1517,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 AUTO_INCREMENT = 1 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 @@ -1627,7 +1543,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 AUTO_INCREMENT = 1 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 @@ -1656,7 +1572,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 @@ -1690,7 +1606,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 AUTO_INCREMENT = 1 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 @@ -1722,7 +1638,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 AUTO_INCREMENT = 1 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 @@ -1743,7 +1659,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 @@ -1772,7 +1688,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 AUTO_INCREMENT = 1 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 @@ -1793,7 +1709,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 AUTO_INCREMENT = 1 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 @@ -1832,7 +1748,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 AUTO_INCREMENT = 1 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 @@ -1861,7 +1777,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 AUTO_INCREMENT = 1 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 @@ -1896,7 +1812,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 AUTO_INCREMENT = 1 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 @@ -1921,7 +1837,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 @@ -1949,7 +1865,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 AUTO_INCREMENT = 1 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 @@ -1972,7 +1888,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 AUTO_INCREMENT = 1 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 @@ -1999,7 +1915,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 AUTO_INCREMENT = 1 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 @@ -2018,7 +1934,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 @@ -2038,7 +1954,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 AUTO_INCREMENT = 1 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 @@ -2073,7 +1989,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 AUTO_INCREMENT = 1 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 @@ -2104,7 +2020,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 AUTO_INCREMENT = 1 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 @@ -2133,7 +2049,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 AUTO_INCREMENT = 1 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 @@ -2336,11 +2252,6 @@ INSERT INTO `sys_dict_data` VALUES (2018858143757504522, '154726', 0, 'PC', 'pc' INSERT INTO `sys_dict_data` VALUES (2018858143761698817, '154726', 0, '安卓', 'android', 'sys_device_type', '', 'default', 'N', 103, 1, '2026-02-04 09:24:25', 1, '2026-02-04 09:24:25', '安卓'); INSERT INTO `sys_dict_data` VALUES (2018858143761698818, '154726', 0, 'iOS', 'ios', 'sys_device_type', '', 'default', 'N', 103, 1, '2026-02-04 09:24:25', 1, '2026-02-04 09:24:25', 'iOS'); INSERT INTO `sys_dict_data` VALUES (2018858143761698819, '154726', 0, '小程序', 'xcx', 'sys_device_type', '', 'default', 'N', 103, 1, '2026-02-04 09:24:25', 1, '2026-02-04 09:24:25', '小程序'); -INSERT INTO `sys_dict_data` VALUES (2026642472673288194, '000000', 0, '对话', 'chat', 'chat_model_category', NULL, 'cyan', 'N', 103, 1, '2026-02-25 20:56:33', 1, '2026-02-25 21:01:42', NULL); -INSERT INTO `sys_dict_data` VALUES (2026642525081116674, '000000', 1, '图像', 'image', 'chat_model_category', NULL, 'success', 'N', 103, 1, '2026-02-25 20:56:46', 1, '2026-02-25 21:01:37', NULL); -INSERT INTO `sys_dict_data` VALUES (2026643983713247233, '000000', 1, '次数计费', '1', 'sys_model_billing', NULL, 'green', 'N', 103, 1, '2026-02-25 21:02:34', 1, '2026-02-25 21:02:56', NULL); -INSERT INTO `sys_dict_data` VALUES (2026644058522853378, '000000', 2, 'token计费', '2', 'sys_model_billing', NULL, 'primary', 'N', 103, 1, '2026-02-25 21:02:51', 1, '2026-02-25 21:02:51', NULL); -INSERT INTO `sys_dict_data` VALUES (2027261114955931650, '000000', 2, '向量', 'vector', 'chat_model_category', NULL, 'default', 'N', 103, 1, '2026-02-27 13:54:49', 1, '2026-02-27 13:54:54', NULL); -- ---------------------------- -- Table structure for sys_dict_type @@ -2384,8 +2295,6 @@ INSERT INTO `sys_dict_type` VALUES (2018858143723950085, '154726', '操作类型 INSERT INTO `sys_dict_type` VALUES (2018858143723950086, '154726', '系统状态', 'sys_common_status', 103, 1, '2026-02-04 09:24:25', 1, '2026-02-04 09:24:25', '登录状态列表'); INSERT INTO `sys_dict_type` VALUES (2018858143723950087, '154726', '授权类型', 'sys_grant_type', 103, 1, '2026-02-04 09:24:25', 1, '2026-02-04 09:24:25', '认证授权类型'); INSERT INTO `sys_dict_type` VALUES (2018858143723950088, '154726', '设备类型', 'sys_device_type', 103, 1, '2026-02-04 09:24:25', 1, '2026-02-04 09:24:25', '客户端设备类型'); -INSERT INTO `sys_dict_type` VALUES (2026642112982360066, '000000', '模型分类', 'chat_model_category', 103, 1, '2026-02-25 20:55:08', 1, '2026-02-25 20:55:08', '模型分类'); -INSERT INTO `sys_dict_type` VALUES (2026642183606050817, '000000', '计费方式', 'sys_model_billing', 103, 1, '2026-02-25 20:55:24', 1, '2026-02-25 20:55:24', '计费方式'); -- ---------------------------- -- Table structure for sys_logininfor @@ -2624,15 +2533,6 @@ INSERT INTO `sys_logininfor` VALUES (2019224998575153153, '000000', 'admin', 'pc INSERT INTO `sys_logininfor` VALUES (2019225059417726977, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-05 09:42:24'); INSERT INTO `sys_logininfor` VALUES (2019240817392693249, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-05 10:45:01'); INSERT INTO `sys_logininfor` VALUES (2019447979716972545, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-06 00:28:13'); -INSERT INTO `sys_logininfor` VALUES (2026536636865163265, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-25 13:56:00'); -INSERT INTO `sys_logininfor` VALUES (2026556949535502337, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-25 15:16:43'); -INSERT INTO `sys_logininfor` VALUES (2026578433112911874, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-25 16:42:05'); -INSERT INTO `sys_logininfor` VALUES (2026638437400518657, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-25 20:40:31'); -INSERT INTO `sys_logininfor` VALUES (2026647463072952321, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-25 21:16:23'); -INSERT INTO `sys_logininfor` VALUES (2026653919016968194, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '退出成功', '2026-02-25 21:42:02'); -INSERT INTO `sys_logininfor` VALUES (2026654082020204546, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-25 21:42:41'); -INSERT INTO `sys_logininfor` VALUES (2026654455514587138, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-25 21:44:10'); -INSERT INTO `sys_logininfor` VALUES (2027260957187186689, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-27 13:54:12'); -- ---------------------------- -- Table structure for sys_menu @@ -2668,7 +2568,7 @@ CREATE TABLE `sys_menu` ( INSERT INTO `sys_menu` VALUES (1, '系统管理', 0, 3, 'system', '', '', 1, 0, 'M', '0', '0', '', 'eos-icons:system-group', 103, 1, '2025-12-14 16:11:49', 1, '2026-01-01 19:06:19', '系统管理目录'); INSERT INTO `sys_menu` VALUES (2, '系统监控', 0, 3, 'monitor', '', '', 1, 0, 'M', '0', '0', '', 'solar:monitor-camera-outline', 103, 1, '2025-12-14 16:11:49', 1, '2025-12-14 17:56:44', '系统监控目录'); INSERT INTO `sys_menu` VALUES (3, '系统工具', 0, 4, 'tool', NULL, '', 1, 0, 'M', '0', '0', '', 'ant-design:tool-outlined', 103, 1, '2025-12-14 16:11:49', NULL, NULL, '系统工具目录'); -INSERT INTO `sys_menu` VALUES (6, '租户管理', 0, 8, 'tenant', '', '', 1, 0, 'M', '0', '0', '', 'ph:users-light', 103, 1, '2025-12-14 16:11:49', 1, '2026-02-25 20:42:14', '租户管理目录'); +INSERT INTO `sys_menu` VALUES (6, '租户管理', 0, 2, 'tenant', NULL, '', 1, 0, 'M', '0', '0', '', 'ph:users-light', 103, 1, '2025-12-14 16:11:49', NULL, NULL, '租户管理目录'); INSERT INTO `sys_menu` VALUES (100, '用户管理', 1, 1, 'user', 'system/user/index', '', 1, 0, 'C', '0', '0', 'system:user:list', 'ant-design:user-outlined', 103, 1, '2025-12-14 16:11:49', NULL, NULL, '用户管理菜单'); INSERT INTO `sys_menu` VALUES (101, '角色管理', 1, 2, 'role', 'system/role/index', '', 1, 0, 'C', '0', '0', 'system:role:list', 'eos-icons:role-binding-outlined', 103, 1, '2025-12-14 16:11:49', NULL, NULL, '角色管理菜单'); INSERT INTO `sys_menu` VALUES (102, '菜单管理', 1, 3, 'menu', 'system/menu/index', '', 1, 0, 'C', '0', '0', 'system:menu:list', 'ic:sharp-menu', 103, 1, '2025-12-14 16:11:49', NULL, NULL, '菜单管理菜单'); @@ -2772,22 +2672,6 @@ 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, 2, 'mcp', '', '', 1, 0, 'M', '0', '0', '', 'mdi:robot-industrial', 103, 1, '2026-02-24 20:02:47', 1, '2026-02-25 20:41:54', 'MCP模块管理菜单'); -INSERT INTO `sys_menu` VALUES (2001, 'MCP工具管理', 2000, 1, 'tool', 'mcp/tool/index', '', 1, 0, 'C', '0', '0', 'mcp:tool:list', 'octicon:tools-24', 103, 1, '2026-02-24 20:02:47', 1, '2026-02-25 20:41:27', '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, ''); @@ -2812,6 +2696,13 @@ INSERT INTO `sys_menu` VALUES (11803, '流程达式定义新增', 11801, 2, '#', INSERT INTO `sys_menu` VALUES (11804, '流程达式定义修改', 11801, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'workflow:spel:edit', '#', 103, 1, '2026-01-05 14:39:33', NULL, NULL, ''); INSERT INTO `sys_menu` VALUES (11805, '流程达式定义删除', 11801, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'workflow:spel:remove', '#', 103, 1, '2026-01-05 14:39:33', NULL, NULL, ''); INSERT INTO `sys_menu` VALUES (11806, '流程达式定义导出', 11801, 5, '#', '', NULL, 1, 0, 'F', '0', '0', 'workflow:spel:export', '#', 103, 1, '2026-01-05 14:39:33', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1971546066781597696, '数字人体验', 2019459914910994434, 10, 'aihumanPublish', 'aihuman/aihumanPublish/index', NULL, 1, 0, 'C', '0', '0', '', 'mdi:human-child', 103, 1, '2026-02-06 01:13:22', 1, '2026-02-06 01:29:34', '数字人信息管理菜单'); +INSERT INTO `sys_menu` VALUES (1971546066781597697, '数字人信息管理查询', 1971546066781597696, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'aihuman:aihumanInfo:query', '#', 103, 1, '2026-02-06 01:13:22', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1971546066781597698, '数字人信息管理新增', 1971546066781597696, 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'aihuman:aihumanInfo:add', '#', 103, 1, '2026-02-06 01:13:22', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1971546066781597699, '数字人信息管理修改', 1971546066781597696, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'aihuman:aihumanInfo:edit', '#', 103, 1, '2026-02-06 01:13:22', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1971546066781597700, '数字人信息管理删除', 1971546066781597696, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'aihuman:aihumanInfo:remove', '#', 103, 1, '2026-02-06 01:13:22', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1971546066781597701, '数字人信息管理导出', 1971546066781597696, 5, '#', '', NULL, 1, 0, 'F', '0', '0', 'aihuman:aihumanInfo:export', '#', 103, 1, '2026-02-06 01:13:22', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1980480880138051584, '数字人配置', 2019459914910994434, 1, 'aihumanConfig', 'aihuman/aihumanConfig/index', NULL, 1, 0, 'C', '0', '0', 'aihuman:aihumanConfig:list', 'mdi:human-child', 103, 1, '2026-02-06 01:13:35', 1, '2026-02-06 01:28:12', ''); INSERT INTO `sys_menu` VALUES (2000209300188356609, '对话管理', 0, 0, 'chat', '', NULL, 1, 0, 'M', '0', '0', NULL, 'material-symbols:chat-outline', 103, 1, '2025-12-14 22:20:34', 1, '2025-12-14 22:21:24', ''); INSERT INTO `sys_menu` VALUES (2000210913451892738, '厂商管理', 2000209300188356609, 1, 'provider', 'chat/provider/index', NULL, 1, 0, 'C', '0', '0', 'system:provider:list', 'tabler:cube-spark', 103, 1, '2025-12-14 22:28:05', 1, '2025-12-14 23:42:55', '厂商管理菜单'); INSERT INTO `sys_menu` VALUES (2000210913451892739, '厂商管理查询', 2000210913451892738, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:provider:query', '#', 103, 1, '2025-12-14 22:28:05', NULL, NULL, ''); @@ -2844,6 +2735,7 @@ INSERT INTO `sys_menu` VALUES (2006681261898813444, '知识库修改', 200668126 INSERT INTO `sys_menu` VALUES (2006681261898813445, '知识库删除', 2006681261898813441, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:info:remove', '#', 103, 1, '2026-01-01 18:59:06', NULL, NULL, ''); INSERT INTO `sys_menu` VALUES (2006681261898813446, '知识库导出', 2006681261898813441, 5, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:info:export', '#', 103, 1, '2026-01-01 18:59:06', NULL, NULL, ''); INSERT INTO `sys_menu` VALUES (2006683336984580098, '知识管理', 0, 2, 'knowledge', '', NULL, 1, 0, 'M', '0', '0', NULL, 'bx:book', 103, 1, '2026-01-01 19:06:05', 1, '2026-01-01 19:06:05', ''); +INSERT INTO `sys_menu` VALUES (2019459914910994434, '数字人管理', 0, 2, 'human', '', NULL, 1, 0, 'M', '0', '0', NULL, 'tdesign:user', 103, 1, '2026-02-06 01:15:38', 1, '2026-02-06 01:16:58', ''); INSERT INTO `sys_menu` VALUES (2019464280262905857, '图谱实例', 2019464531388469250, 1, 'graphInstance', 'graph/graphInstance/index', NULL, 1, 0, 'C', '0', '0', 'operator:graph:list', 'ant-design:node-index-outlined', 103, 1, '2026-02-06 01:32:59', 1, '2026-02-06 01:40:06', ''); INSERT INTO `sys_menu` VALUES (2019464531388469250, '知识图谱', 2006683336984580098, 15, 'graph', '', NULL, 1, 0, 'M', '0', '0', NULL, 'carbon:chart-relationship', 103, 1, '2026-02-06 01:33:59', 1, '2026-02-06 01:33:59', ''); INSERT INTO `sys_menu` VALUES (2019464779217309697, '图谱可视化', 2019464531388469250, 2, 'graphVisualization', 'graph/graphVisualization/index', NULL, 1, 0, 'C', '0', '0', 'operator:graph:view', 'carbon:chart-network', 103, 1, '2026-02-06 01:34:58', 1, '2026-02-06 01:40:14', ''); @@ -2932,12 +2824,6 @@ CREATE TABLE `sys_oss` ( -- ---------------------------- -- Records of sys_oss -- ---------------------------- -INSERT INTO `sys_oss` VALUES (2026580908423340033, '000000', '2026/02/25/9219d6d71a6d45e19192014609d92dc9.png', 'logo.png', '.png', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/9219d6d71a6d45e19192014609d92dc9.png', '{\"fileSize\":\"183613\",\"contentType\":\"image/png\"}', 103, '2026-02-25 16:51:55', 1, '2026-02-25 16:51:55', 1, 'qcloud'); -INSERT INTO `sys_oss` VALUES (2026640059920883713, '000000', '2026/02/25/01091be272334383a1efd9bc22b73ee6.png', 'openai.png', '.png', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/01091be272334383a1efd9bc22b73ee6.png', '{\"fileSize\":\"11297\",\"contentType\":\"image/png\"}', 103, '2026-02-25 20:46:58', 1, '2026-02-25 20:46:58', 1, 'qcloud'); -INSERT INTO `sys_oss` VALUES (2026640515967557633, '000000', '2026/02/25/afecabebc8014d80b0f06b4796a74c5d.png', 'ollama.png', '.png', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/afecabebc8014d80b0f06b4796a74c5d.png', '{\"fileSize\":\"8746\",\"contentType\":\"image/png\"}', 103, '2026-02-25 20:48:47', 1, '2026-02-25 20:48:47', 1, 'qcloud'); -INSERT INTO `sys_oss` VALUES (2026640548213366785, '000000', '2026/02/25/e16429a462e54e14a1d36673146b9e3c.png', 'ppio-color.png', '.png', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/e16429a462e54e14a1d36673146b9e3c.png', '{\"fileSize\":\"7382\",\"contentType\":\"image/png\"}', 103, '2026-02-25 20:48:55', 1, '2026-02-25 20:48:55', 1, 'qcloud'); -INSERT INTO `sys_oss` VALUES (2026640572443860993, '000000', '2026/02/25/049bb6a507174f73bba4b8d8b9e55b8a.png', 'ppio-color.png', '.png', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/049bb6a507174f73bba4b8d8b9e55b8a.png', '{\"fileSize\":\"7382\",\"contentType\":\"image/png\"}', 103, '2026-02-25 20:49:00', 1, '2026-02-25 20:49:00', 1, 'qcloud'); -INSERT INTO `sys_oss` VALUES (2026640621945036802, '000000', '2026/02/25/de2aa7e649de44f3ba5c6380ac6acd04.png', 'bailian-color.png', '.png', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/de2aa7e649de44f3ba5c6380ac6acd04.png', '{\"fileSize\":\"5901\",\"contentType\":\"image/png\"}', 103, '2026-02-25 20:49:12', 1, '2026-02-25 20:49:12', 1, 'qcloud'); -- ---------------------------- -- Table structure for sys_oss_config @@ -2970,10 +2856,10 @@ CREATE TABLE `sys_oss_config` ( -- ---------------------------- -- Records of sys_oss_config -- ---------------------------- -INSERT INTO `sys_oss_config` VALUES (1, '000000', 'minio', 'ruoyi', 'ruoyi123', 'ruoyi', '', '127.0.0.1:9000', '', 'N', '', '1', '1', '', 103, 1, '2026-02-03 05:14:52', 1, '2026-02-25 15:44:13', NULL); +INSERT INTO `sys_oss_config` VALUES (1, '000000', 'minio', 'ruoyi', 'ruoyi123', 'ruoyi', '', '127.0.0.1:9000', '', 'N', '', '1', '0', '', 103, 1, '2026-02-03 05:14:52', 1, '2026-02-03 05:14:52', NULL); INSERT INTO `sys_oss_config` VALUES (2, '000000', 'qiniu', 'XXXXXXXXXXXXXXX', 'XXXXXXXXXXXXXXX', 'ruoyi', '', 's3-cn-north-1.qiniucs.com', '', 'N', '', '1', '1', '', 103, 1, '2026-02-03 05:14:52', 1, '2026-02-03 05:14:52', NULL); INSERT INTO `sys_oss_config` VALUES (3, '000000', 'aliyun', 'XXXXXXXXXXXXXXX', 'XXXXXXXXXXXXXXX', 'ruoyi', '', 'oss-cn-beijing.aliyuncs.com', '', 'N', '', '1', '1', '', 103, 1, '2026-02-03 05:14:52', 1, '2026-02-03 05:14:52', NULL); -INSERT INTO `sys_oss_config` VALUES (4, '000000', 'qcloud', 'xx', 'xx', 'ruoyiai-1254149996', '', 'cos.ap-guangzhou.myqcloud.com', '', 'Y', 'ap-guangzhou', '1', '0', '', 103, 1, '2026-02-03 05:14:52', 1, '2026-02-25 16:51:41', ''); +INSERT INTO `sys_oss_config` VALUES (4, '000000', 'qcloud', 'XXXXXXXXXXXXXXX', 'XXXXXXXXXXXXXXX', 'ruoyi-1240000000', '', 'cos.ap-beijing.myqcloud.com', '', 'N', 'ap-beijing', '1', '1', '', 103, 1, '2026-02-03 05:14:52', 1, '2026-02-03 05:14:52', NULL); INSERT INTO `sys_oss_config` VALUES (5, '000000', 'image', 'ruoyi', 'ruoyi123', 'ruoyi', 'image', '127.0.0.1:9000', '', 'N', '', '1', '1', '', 103, 1, '2026-02-03 05:14:53', 1, '2026-02-03 05:14:53', NULL); -- ---------------------------- @@ -3420,7 +3306,7 @@ CREATE TABLE `sys_user` ( -- ---------------------------- -- Records of sys_user -- ---------------------------- -INSERT INTO `sys_user` VALUES (1, '000000', 103, 'admin', 'admin', 'sys_user', 'ageerle@163.com', '15888888888', '1', NULL, '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', '2026-02-27 13:54:12', 103, 1, '2026-02-05 09:22:12', -1, '2026-02-27 13:54:12', '管理员', NULL, 0.00); +INSERT INTO `sys_user` VALUES (1, '000000', 103, 'admin', 'admin', 'sys_user', 'ageerle@163.com', '15888888888', '1', NULL, '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', '2026-02-06 00:28:13', 103, 1, '2026-02-05 09:22:12', -1, '2026-02-06 00:28:13', '管理员', NULL, 0.00); INSERT INTO `sys_user` VALUES (3, '000000', 108, 'test', '本部门及以下 密码666666', 'sys_user', '', '', '0', NULL, '$2a$10$b8yUzN0C71sbz.PhNOCgJe.Tu1yWC3RNrTyjSQ8p1W0.aaUXUJ.Ne', '0', '0', '127.0.0.1', '2026-02-05 09:22:12', 103, 1, '2026-02-05 09:22:12', 3, '2026-02-05 09:22:12', NULL, NULL, 0.00); INSERT INTO `sys_user` VALUES (4, '000000', 102, 'test1', '仅本人 密码666666', 'sys_user', '', '', '0', NULL, '$2a$10$b8yUzN0C71sbz.PhNOCgJe.Tu1yWC3RNrTyjSQ8p1W0.aaUXUJ.Ne', '0', '0', '127.0.0.1', '2026-02-05 09:22:12', 103, 1, '2026-02-05 09:22:12', 4, '2026-02-05 09:22:12', NULL, NULL, 0.00); @@ -3499,7 +3385,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 = 37 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '工作流组件库 | Workflow Component' ROW_FORMAT = DYNAMIC; +) ENGINE = InnoDB AUTO_INCREMENT = 20 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '工作流组件库 | Workflow Component' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of t_workflow_component @@ -3507,9 +3393,6 @@ 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 @@ -3729,25 +3612,161 @@ INSERT INTO `test_tree` VALUES (13, '000000', 10, 108, 3, '子节点99', 0, 103, SET FOREIGN_KEY_CHECKS = 1; -INSERT INTO `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 `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 `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 `t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (20, '0b4369bb60dc46d6bd84ceb4e36184dc', 'KeywordExtractor', '关键词提取', '从文本中提取关键词', 0, 1, '2025-12-26 16:30:05', '2025-12-26 16:30:05', 0, '000000'); -INSERT INTO `t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (21, 'bb00fc2f52c74fec82ee3f99725b56bb', 'Switcher', '条件分支', '根据条件执行不同分支', 0, 1, '2025-12-26 16:30:46', '2025-12-26 16:30:46', 0, '000000'); -INSERT INTO `t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (22, 'f37dbcb8f0d5464d90fbb22774490a56', 'HumanFeedback', '人类', '人机沟通', 0, 1, '2025-12-30 17:37:14', '2025-12-30 17:37:14', 0, '000000'); -INSERT INTO `t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (23, 'af9d6d7b9c9b47f990ad25ec84912b73', 'Tongyiwanx', '阿里图像生成', '使用通义万相生成图像', 0, 1, '2025-12-26 16:32:25', '2025-12-26 16:32:25', 0, '000000'); -INSERT INTO `t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (24, 'a1e2c9d4b8f04e1a9c3d6f8e2a7b1c9d', 'MailSend', '发送邮箱', '发送邮箱', 0, 1, '2025-12-30 17:37:14', '2025-12-30 17:37:14', 0, '000000'); -INSERT INTO `t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (25, 'f1e2d3c4b5a67890f1e2d3c4b5a6f1e2', 'HttpRequest', '请求节点', '请求节点', 0, 1, '2025-12-30 17:37:14', '2025-12-30 17:37:14', 0, '000000'); -INSERT INTO `chat_model` (`id`, `category`, `model_name`, `provider_code`, `model_describe`, `model_price`, `model_type`, `model_show`, `model_free`, `priority`, `api_host`, `api_key`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `tenant_id`) VALUES (2022565766560468994, 'image', 'wan2.5-t2i-preview', 'Tongyiwanx', 'wan2.5-t2i-preview', 1, '1', 'Y', 'Y', 1, 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation', 'skxxxx', 103, 1, '2026-02-14 14:57:11', 1, '2026-02-14 14:57:11', '通义万相文生图', 0); -INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query_param`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2021046920636690433, '流程管理', 0, 0, 'flow', '', NULL, 1, 0, 'M', '0', '0', NULL, 'ph:user-fill', 103, 1, '2026-02-10 10:21:50', 1, '2026-02-10 15:59:28', ''); -INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query_param`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2021047050391678978, '工作流编排', 2021046920636690433, 0, 'aiflowengine', 'aiflow/index', NULL, 1, 0, 'C', '0', '0', '', 'ph:user-fill', 103, 1, '2026-02-10 10:22:21', 1, '2026-02-10 16:04:41', ''); -INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2027192921483309058, '000000', 'HTTP请求节点响应模板', 'node.httpRequest.template', '✅ HTTP请求节点:结束响应 - ', 'Y', 103, 1, '2026-02-27 09:23:51', 1, '2026-02-27 09:31:41', NULL); -INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2027193296990957569, '000000', '文生图节点响应模板', 'node.image.template', '🎨 文生图节点:结束响应 - 图片URL: ', 'Y', 103, 1, '2026-02-27 09:25:20', 1, '2026-02-27 09:31:52', NULL); -INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2027193820393959425, '000000', '发送邮箱节点响应模板', 'node.mailsend.template', '📧 发送邮箱节点:结束响应 - ', 'Y', 103, 1, '2026-02-27 09:27:25', 1, '2026-02-27 09:32:05', NULL); -INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2027194134438277122, '000000', '结束节点响应模板', 'node.end.template', '🔚 流程已执行完毕,如果您有其他需求,请随时重新发起请求。', 'Y', 103, 1, '2026-02-27 09:28:40', 1, '2026-02-27 09:32:53', NULL); -INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2027206492573335554, '000000', '人机交互节点响应模板', 'node.humanFeedback.template', '👤 人机交互节点:等待用户操作 - ', 'Y', 103, 1, '2026-02-27 10:17:46', 1, '2026-02-27 10:17:46', NULL); -INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2027208880369647617, '000000', '条件分支节点响应模板', 'node.switch.template', '🔀 条件分支节点:触发 -> 跳转到节点 ', 'Y', 103, 1, '2026-02-27 10:27:15', 1, '2026-02-27 10:35:54', NULL); -INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2027213914603995137, '000000', '大模型回答节点响应模板', 'node.llmAnswer.template', '🤖 LLM 节点 生成回答:', 'Y', 103, 1, '2026-02-27 10:47:16', 1, '2026-02-27 10:52:40', NULL); -INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2027214387000066050, '000000', '关键词提取响应模板', 'node.keywordExtractor.template', '🔑 关键词提取节点 处理完成 : ', 'Y', 103, 1, '2026-02-27 10:49:08', 1, '2026-02-27 10:52:08', NULL); -INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2027217577397391361, '000000', '工作流异常响应模板', 'node.exception.template', '🛑 工作流发生异常:', 'N', 103, 1, '2026-02-27 11:01:49', 1, '2026-02-27 11:02:01', NULL); + +-- 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 372c2770..dbd40c94 100644 --- a/pom.xml +++ b/pom.xml @@ -56,10 +56,11 @@ 1.11.0 1.11.0-beta19 - 1.1.0-beta7 1.5.3 1.19.6 1.0.7 + + 1.27.1 1.1.0 @@ -396,6 +397,13 @@ ${revision} + + + org.ruoyi + ruoyi-mcp + ${revision} + + com.github.binarywang @@ -410,6 +418,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 4675462c..153e82da 100644 --- a/ruoyi-admin/pom.xml +++ b/ruoyi-admin/pom.xml @@ -110,6 +110,12 @@ ruoyi-aiflow + + + org.ruoyi + ruoyi-mcp + + de.codecentric spring-boot-admin-starter-client 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-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-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()); } diff --git a/ruoyi-modules/pom.xml b/ruoyi-modules/pom.xml index 5db99df1..73fe7257 100644 --- a/ruoyi-modules/pom.xml +++ b/ruoyi-modules/pom.xml @@ -15,6 +15,7 @@ ruoyi-demo ruoyi-generator ruoyi-job + ruoyi-mcp ruoyi-system ruoyi-wechat ruoyi-workflow diff --git a/ruoyi-modules/ruoyi-chat/pom.xml b/ruoyi-modules/ruoyi-chat/pom.xml index 35a3535f..e88a1d87 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 @@ -51,15 +56,9 @@ dev.langchain4j langchain4j-community-zhipu-ai - ${langchain4j.community.zhipu.ai.version} + ${langchain4j.community.version} - - - - - - com.zaxxer 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 e962e2f7..0b65a1f9 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 @@ -8,28 +8,22 @@ 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.service.chat.impl.AbstractStreamingChatService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; import org.ruoyi.common.chat.domain.dto.request.ChatRequest; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; +import org.ruoyi.common.core.utils.SpringUtils; +import org.ruoyi.enums.ChatModeType; +import org.ruoyi.mcp.service.core.ToolProviderFactory; +import org.ruoyi.service.chat.impl.AbstractStreamingChatService; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantLock; /** @@ -42,20 +36,9 @@ import java.util.concurrent.locks.ReentrantLock; @Slf4j public class QianWenChatServiceImpl extends AbstractStreamingChatService { - @Autowired - private McpSseConfig mcpSseConfig; - // 添加文档解析的前缀字段 private static final String UPLOAD_FILE_API_PREFIX = "fileid"; - // 缓存不同API Key和模型的MCP智能体实例 - private final ConcurrentHashMap supervisorCache = new ConcurrentHashMap<>(); - - // 缓存不同API Key和模型的MCP客户端实例 - private final ConcurrentHashMap mcpClientCache = new ConcurrentHashMap<>(); - - // 缓存不同API Key和模型的MCP工具提供者实例 - private final ConcurrentHashMap toolProviderCache = new ConcurrentHashMap<>(); // 用于线程安全的锁 private final ReentrantLock cacheLock = new ReentrantLock(); @@ -108,77 +91,55 @@ public class QianWenChatServiceImpl extends AbstractStreamingChatService { }).orElse(messagesWithMemory); } - /** - * 获取缓存键 - */ - private String getCacheKey(ChatModelVo chatModelVo) { - return chatModelVo.getApiKey() + ":" + chatModelVo.getModelName(); - } - - /** - * 初始化MCP客户端连接 - */ - private McpClient initializeMcpClient() { - // 步骤1:根据SSE对外暴露端点连接 - McpTransport httpMcpTransport = new StreamableHttpMcpTransport.Builder(). - url(mcpSseConfig.getUrl()). - logRequests(true). - build(); - - // 步骤2:开启客户端连接 - return new DefaultMcpClient.Builder() - .transport(httpMcpTransport) - .build(); - } - /** * 调用MCP服务(智能体) + * 使用统一的ToolProviderFactory获取所有已配置的工具(BUILTIN + MCP) + * * @param userMessage 用户信息 * @param chatModelVo 模型信息 * @return 返回LLM信息 */ protected String doAgent(String userMessage, ChatModelVo chatModelVo) { - // 判断是否开启MCP服务 - if (!mcpSseConfig.isEnabled()) { - return ""; - } - // 生成缓存键 - String cacheKey = getCacheKey(chatModelVo); - // 尝试从缓存获取监督智能体 - SupervisorAgent cachedSupervisor = supervisorCache.get(cacheKey); - if (cachedSupervisor != null) { - // 如果已存在缓存的监督智能体,直接使用 - return cachedSupervisor.invoke(userMessage); - } - cacheLock.lock(); try { - // 双重检查,防止并发情况下的重复初始化 - cachedSupervisor = supervisorCache.get(cacheKey); - if (cachedSupervisor != null) { - return cachedSupervisor.invoke(userMessage); - } + // 步骤1: 获取统一工具提供工厂 + ToolProviderFactory toolProviderFactory = SpringUtils.getBean(ToolProviderFactory.class); - // 获取或初始化MCP客户端 - McpClient mcpClient = mcpClientCache.computeIfAbsent(cacheKey, k -> initializeMcpClient()); + // 步骤2: 获取 BUILTIN 工具对象 + List builtinTools = toolProviderFactory.getAllBuiltinToolObjects(); - // 步骤3:将mcp对象包装 - ToolProvider toolProvider = toolProviderCache.computeIfAbsent(cacheKey, k -> McpToolProvider.builder() - .mcpClients(List.of(mcpClient)) - .build()); + // 步骤3: 获取 MCP 工具提供者 + ToolProvider mcpToolProvider = toolProviderFactory.getAllEnabledMcpToolsProvider(); - // 步骤4:加载LLM模型对话 + log.info("doAgent: BUILTIN tools count = {}, MCP tools enabled = {}", + builtinTools.size(), mcpToolProvider != null); + + // 步骤4: 加载LLM模型 QwenChatModel qwenChatModel = QwenChatModel.builder() .apiKey(chatModelVo.getApiKey()) .modelName(chatModelVo.getModelName()) .build(); - // 步骤5:将MCP对象由智能体Agent管控 - 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) @@ -186,10 +147,7 @@ public class QianWenChatServiceImpl extends AbstractStreamingChatService { .responseStrategy(SupervisorResponseStrategy.LAST) .build(); - // 缓存监督智能体 - supervisorCache.put(cacheKey, supervisor); - - // 步骤7:调用大模型LLM + // 步骤7: 调用大模型LLM return supervisor.invoke(userMessage); } finally { cacheLock.unlock(); 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 84dbc2cfbfc0221008efecee3dab66625663fec0 Mon Sep 17 00:00:00 2001 From: evo <446796145@qq.com> Date: Sun, 8 Mar 2026 22:41:24 +0800 Subject: [PATCH 10/11] =?UTF-8?q?feat=EF=BC=9A=E6=81=A2=E5=A4=8Dmcp?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=20=E5=8A=A8=E6=80=81agent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + pom.xml | 7 - ruoyi-admin/pom.xml | 5 - .../ruoyi/config/MapperConflictResolver.java | 48 +++ ruoyi-modules/pom.xml | 1 - .../ruoyi-chat/docs/frontend-guide.md | 244 +++++++++++++ ruoyi-modules/ruoyi-chat/docs/mcp-api-spec.md | 336 ++++++++++++++++++ ruoyi-modules/ruoyi-chat/pom.xml | 5 - .../org/ruoyi/agent/ChartGenerationAgent.java | 2 +- .../main/java/org/ruoyi/agent/McpAgent.java | 2 +- .../main/java/org/ruoyi/agent/SqlAgent.java | 3 +- .../ruoyi/agent/StreamingCreativeWriter.java | 18 - .../java/org/ruoyi/agent/WebSearchAgent.java | 2 +- .../agent/manager/TableSchemaManager.java | 11 +- .../ruoyi/agent/tool/ExecuteSqlQueryTool.java | 27 +- .../ruoyi/agent/tool/QueryAllTablesTool.java | 86 +++-- .../agent/tool/QueryTableSchemaTool.java | 28 +- .../config/mcp}/SystemToolInitializer.java | 8 +- .../controller/mcp}/McpMarketController.java | 15 +- .../controller/mcp}/McpToolController.java | 12 +- .../org/ruoyi/domain/bo/mcp}/McpMarketBo.java | 4 +- .../org/ruoyi/domain/bo/mcp}/McpToolBo.java | 4 +- .../domain/dto/mcp}/McpMarketListResult.java | 4 +- .../dto/mcp}/McpMarketRefreshResult.java | 2 +- .../dto/mcp}/McpMarketToolListResult.java | 4 +- .../domain/dto/mcp}/McpToolListResult.java | 4 +- .../domain/dto/mcp}/McpToolTestResult.java | 2 +- .../ruoyi/domain/entity/mcp}/McpMarket.java | 2 +- .../domain/entity/mcp}/McpMarketTool.java | 2 +- .../org/ruoyi/domain/entity/mcp}/McpTool.java | 2 +- .../org/ruoyi/domain/vo/mcp}/McpMarketVo.java | 4 +- .../org/ruoyi/domain/vo/mcp}/McpToolVo.java | 8 +- .../java/org/ruoyi}/enums/McpToolStatus.java | 2 +- .../ruoyi/mapper/mcp}/McpMarketMapper.java | 6 +- .../mapper/mcp}/McpMarketToolMapper.java | 4 +- .../org/ruoyi/mapper/mcp}/McpToolMapper.java | 6 +- .../service/core/BuiltinToolDefinition.java | 0 .../mcp/service/core/BuiltinToolProvider.java | 0 .../mcp/service/core/BuiltinToolRegistry.java | 174 +++++++++ .../LangChain4jMcpToolProviderService.java | 37 +- .../mcp/service/core/ToolProviderFactory.java | 49 +++ .../org/ruoyi/mcp/tools/EditFileTool.java | 0 .../ruoyi/mcp/tools/ListDirectoryTool.java | 0 .../org/ruoyi/mcp/tools/ReadFileTool.java | 0 .../impl/AbstractStreamingChatService.java | 202 +++++------ .../impl/provider/QianWenChatServiceImpl.java | 75 ---- .../ruoyi/service/mcp}/IMcpMarketService.java | 12 +- .../ruoyi/service/mcp}/IMcpToolService.java | 10 +- .../mcp}/impl/McpMarketServiceImpl.java | 28 +- .../service/mcp}/impl/McpToolServiceImpl.java | 18 +- ruoyi-modules/ruoyi-mcp/pom.xml | 77 ---- .../org/ruoyi/mcp/config/McpProperties.java | 40 --- .../mcp/service/core/BuiltinToolRegistry.java | 129 ------- .../mcp/service/core/ToolProviderFactory.java | 171 --------- 54 files changed, 1150 insertions(+), 793 deletions(-) create mode 100644 ruoyi-admin/src/main/java/org/ruoyi/config/MapperConflictResolver.java create mode 100644 ruoyi-modules/ruoyi-chat/docs/frontend-guide.md create mode 100644 ruoyi-modules/ruoyi-chat/docs/mcp-api-spec.md delete mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/StreamingCreativeWriter.java rename ruoyi-modules/{ruoyi-mcp/src/main/java/org/ruoyi/mcp/config => ruoyi-chat/src/main/java/org/ruoyi/config/mcp}/SystemToolInitializer.java (95%) rename ruoyi-modules/{ruoyi-mcp/src/main/java/org/ruoyi/mcp/controller => ruoyi-chat/src/main/java/org/ruoyi/controller/mcp}/McpMarketController.java (92%) rename ruoyi-modules/{ruoyi-mcp/src/main/java/org/ruoyi/mcp/controller => ruoyi-chat/src/main/java/org/ruoyi/controller/mcp}/McpToolController.java (93%) rename ruoyi-modules/{ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/bo => ruoyi-chat/src/main/java/org/ruoyi/domain/bo/mcp}/McpMarketBo.java (93%) rename ruoyi-modules/{ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/bo => ruoyi-chat/src/main/java/org/ruoyi/domain/bo/mcp}/McpToolBo.java (94%) rename ruoyi-modules/{ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto => ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp}/McpMarketListResult.java (90%) rename ruoyi-modules/{ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto => ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp}/McpMarketRefreshResult.java (94%) rename ruoyi-modules/{ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto => ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp}/McpMarketToolListResult.java (92%) rename ruoyi-modules/{ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto => ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp}/McpToolListResult.java (90%) rename ruoyi-modules/{ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto => ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp}/McpToolTestResult.java (96%) rename ruoyi-modules/{ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity => ruoyi-chat/src/main/java/org/ruoyi/domain/entity/mcp}/McpMarket.java (96%) rename ruoyi-modules/{ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity => ruoyi-chat/src/main/java/org/ruoyi/domain/entity/mcp}/McpMarketTool.java (96%) rename ruoyi-modules/{ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity => ruoyi-chat/src/main/java/org/ruoyi/domain/entity/mcp}/McpTool.java (96%) rename ruoyi-modules/{ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/vo => ruoyi-chat/src/main/java/org/ruoyi/domain/vo/mcp}/McpMarketVo.java (94%) rename ruoyi-modules/{ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/vo => ruoyi-chat/src/main/java/org/ruoyi/domain/vo/mcp}/McpToolVo.java (88%) rename ruoyi-modules/{ruoyi-mcp/src/main/java/org/ruoyi/mcp => ruoyi-chat/src/main/java/org/ruoyi}/enums/McpToolStatus.java (96%) rename ruoyi-modules/{ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper => ruoyi-chat/src/main/java/org/ruoyi/mapper/mcp}/McpMarketMapper.java (68%) rename ruoyi-modules/{ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper => ruoyi-chat/src/main/java/org/ruoyi/mapper/mcp}/McpMarketToolMapper.java (76%) rename ruoyi-modules/{ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper => ruoyi-chat/src/main/java/org/ruoyi/mapper/mcp}/McpToolMapper.java (68%) rename ruoyi-modules/{ruoyi-mcp => ruoyi-chat}/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolDefinition.java (100%) rename ruoyi-modules/{ruoyi-mcp => ruoyi-chat}/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolProvider.java (100%) create mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolRegistry.java rename ruoyi-modules/{ruoyi-mcp => ruoyi-chat}/src/main/java/org/ruoyi/mcp/service/core/LangChain4jMcpToolProviderService.java (90%) create mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/core/ToolProviderFactory.java rename ruoyi-modules/{ruoyi-mcp => ruoyi-chat}/src/main/java/org/ruoyi/mcp/tools/EditFileTool.java (100%) rename ruoyi-modules/{ruoyi-mcp => ruoyi-chat}/src/main/java/org/ruoyi/mcp/tools/ListDirectoryTool.java (100%) rename ruoyi-modules/{ruoyi-mcp => ruoyi-chat}/src/main/java/org/ruoyi/mcp/tools/ReadFileTool.java (100%) rename ruoyi-modules/{ruoyi-mcp/src/main/java/org/ruoyi/mcp/service => ruoyi-chat/src/main/java/org/ruoyi/service/mcp}/IMcpMarketService.java (89%) rename ruoyi-modules/{ruoyi-mcp/src/main/java/org/ruoyi/mcp/service => ruoyi-chat/src/main/java/org/ruoyi/service/mcp}/IMcpToolService.java (88%) rename ruoyi-modules/{ruoyi-mcp/src/main/java/org/ruoyi/mcp/service => ruoyi-chat/src/main/java/org/ruoyi/service/mcp}/impl/McpMarketServiceImpl.java (94%) rename ruoyi-modules/{ruoyi-mcp/src/main/java/org/ruoyi/mcp/service => ruoyi-chat/src/main/java/org/ruoyi/service/mcp}/impl/McpToolServiceImpl.java (95%) delete mode 100644 ruoyi-modules/ruoyi-mcp/pom.xml delete mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/config/McpProperties.java delete mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolRegistry.java delete mode 100644 ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/ToolProviderFactory.java diff --git a/.gitignore b/.gitignore index 03567532..d9cc6d06 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ nbdist/ !*/build/*.xml .flattened-pom.xml +/.claude/settings.local.json diff --git a/pom.xml b/pom.xml index dbd40c94..36bf6d39 100644 --- a/pom.xml +++ b/pom.xml @@ -397,13 +397,6 @@ ${revision} - - - org.ruoyi - ruoyi-mcp - ${revision} - - com.github.binarywang diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml index 153e82da..49750f04 100644 --- a/ruoyi-admin/pom.xml +++ b/ruoyi-admin/pom.xml @@ -110,11 +110,6 @@ ruoyi-aiflow - - - org.ruoyi - ruoyi-mcp - de.codecentric diff --git a/ruoyi-admin/src/main/java/org/ruoyi/config/MapperConflictResolver.java b/ruoyi-admin/src/main/java/org/ruoyi/config/MapperConflictResolver.java new file mode 100644 index 00000000..467c96db --- /dev/null +++ b/ruoyi-admin/src/main/java/org/ruoyi/config/MapperConflictResolver.java @@ -0,0 +1,48 @@ +package org.ruoyi.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.context.annotation.Configuration; + +/** + * BeanDefinitionRegistry后置处理器 + * 解决 MapStruct Plus 生成的 mapper 冲突问题 + * + * @author ruoyi team + */ +@Configuration +public class MapperConflictResolver implements BeanDefinitionRegistryPostProcessor { + + private static final Logger log = LoggerFactory.getLogger(MapperConflictResolver.class); + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + String[] beanNames = registry.getBeanDefinitionNames(); + + // 查找冲突的 mapper bean + for (String beanName : beanNames) { + if (beanName.equals("chatMessageBoToChatMessageMapperImpl")) { + BeanDefinition beanDefinition = registry.getBeanDefinition(beanName); + String beanClassName = beanDefinition.getBeanClassName(); + + log.info("Found mapper bean: {} -> {}", beanName, beanClassName); + + // 如果是 org.ruoyi.domain.bo.chat 包下的(冲突的),移除它 + if (beanClassName != null && beanClassName.startsWith("org.ruoyi.domain.bo.chat")) { + log.warn("Removing conflicting bean definition: {} ({})", beanName, beanClassName); + registry.removeBeanDefinition(beanName); + } + } + } + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + // 不需要实现 + } +} diff --git a/ruoyi-modules/pom.xml b/ruoyi-modules/pom.xml index 73fe7257..5db99df1 100644 --- a/ruoyi-modules/pom.xml +++ b/ruoyi-modules/pom.xml @@ -15,7 +15,6 @@ ruoyi-demo ruoyi-generator ruoyi-job - ruoyi-mcp ruoyi-system ruoyi-wechat ruoyi-workflow diff --git a/ruoyi-modules/ruoyi-chat/docs/frontend-guide.md b/ruoyi-modules/ruoyi-chat/docs/frontend-guide.md new file mode 100644 index 00000000..05cb5b9f --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/docs/frontend-guide.md @@ -0,0 +1,244 @@ +# MCP工具管理前端开发指南 + +## 前置条件 + +- Node.js >= 16.0 +- Vue 3 +- Element Plus +- ECharts (用于图表展示) +- Axios (用于HTTP请求) + +## 安装依赖 + +```bash +npm install element-plus echarts axios +``` + +## 项目结构 + +``` +ruoyi-ui/ +├── src/ +│ ├── api/ +│ │ └── mcp/ +│ │ └── tool.js # API接口 +│ ├── views/ +│ │ └── mcp/ +│ │ ├── tool/ +│ │ │ └── index.vue # 工具管理页面 +│ │ ├── market/ +│ │ │ └── index.vue # 市场管理页面 +│ │ └── log/ +│ │ └── index.vue # 调用日志页面 +│ └── utils/ +│ └── request.js # Axios封装 +``` + +## 菜单配置 + +在系统菜单管理中添加以下菜单: + +### 1. MCP工具管理 + +| 字段 | 值 | +|------|-----| +| 菜单名称 | MCP工具管理 | +| 菜单类型 | 目录 | +| 显示顺序 | 1 | +| 路由地址 | mcp | +| 组件路径 | | + +#### 子菜单:工具列表 + +| 字段 | 值 | +|------|-----| +| 菜单名称 | 工具列表 | +| 菜单类型 | 菜单 | +| 显示顺序 | 1 | +| 路由地址 | tool | +| 组件路径 | mcp/tool/index | +| 权限标识 | mcp:tool:list | + +#### 子菜单:市场管理 + +| 字段 | 值 | +|------|-----| +| 菜单名称 | 市场管理 | +| 菜单类型 | 菜单 | +| 显示顺序 | 2 | +| 路由地址 | market | +| 组件路径 | mcp/market/index | +| 权限标识 | mcp:market:list | + +#### 子菜单:调用日志 + +| 字段 | 值 | +|------|-----| +| 菜单名称 | 调用日志 | +| 菜单类型 | 菜单 | +| 显示顺序 | 3 | +| 路由地址 | log | +| 组件路径 | mcp/log/index | +| 权限标识 | mcp:tool:query | + +## 权限配置 + +| 权限标识 | 权限名称 | 说明 | +|----------|----------|------| +| mcp:tool:list | 工具列表 | 查看工具列表 | +| mcp:tool:query | 工具查询 | 查看工具详情 | +| mcp:tool:add | 工具新增 | 新增工具 | +| mcp:tool:edit | 工具修改 | 修改工具 | +| mcp:tool:remove | 工具删除 | 删除工具 | +| mcp:tool:export | 工具导出 | 导出工具数据 | +| mcp:market:list | 市场列表 | 查看市场列表 | +| mcp:market:query | 市场查询 | 查看市场详情 | +| mcp:market:add | 市场新增 | 新增市场 | +| mcp:market:edit | 市场修改 | 修改市场 | +| mcp:market:remove | 市场删除 | 删除市场 | + +## 路由配置 + +在路由配置文件中添加: + +```javascript +{ + path: '/mcp', + component: Layout, + redirect: '/mcp/tool', + name: 'Mcp', + meta: { title: 'MCP工具管理', icon: 'tools' }, + children: [ + { + path: 'tool', + name: 'McpTool', + component: () => import('@/views/mcp/tool/index'), + meta: { title: '工具列表', icon: 'tool' } + }, + { + path: 'market', + name: 'McpMarket', + component: () => import('@/views/mcp/market/index'), + meta: { title: '市场管理', icon: 'shop' } + }, + { + path: 'log', + name: 'McpLog', + component: () => import('@/views/mcp/log/index'), + meta: { title: '调用日志', icon: 'document' } + } + ] +} +``` + +## API请求配置 + +确保Axios请求拦截器正确配置: + +```javascript +// src/utils/request.js +import axios from 'axios' +import { ElMessage } from 'element-plus' + +const service = axios.create({ + baseURL: process.env.VUE_APP_BASE_API, + timeout: 30000 +}) + +// 请求拦截器 +service.interceptors.request.use( + config => { + // 添加token + const token = localStorage.getItem('Admin-Token') + if (token) { + config.headers['Authorization'] = 'Bearer ' + token + } + return config + }, + error => { + return Promise.reject(error) + } +) + +// 响应拦截器 +service.interceptors.response.use( + response => { + const res = response.data + if (res.code !== 200) { + ElMessage.error(res.msg || '请求失败') + return Promise.reject(new Error(res.msg || '请求失败')) + } + return res + }, + error => { + ElMessage.error(error.message) + return Promise.reject(error) + } +) + +export default service +``` + +## 开发步骤 + +1. **复制代码文件** + - 将 `tool.js` 复制到 `src/api/mcp/` 目录 + - 将 `*.vue` 文件复制到对应的视图目录 + +2. **安装依赖** + ```bash + npm install element-plus echarts + ``` + +3. **配置路由** + - 在路由配置中添加MCP相关路由 + +4. **配置菜单** + - 在系统管理中添加菜单 + +5. **配置权限** + - 在系统管理中添加权限标识 + +6. **测试功能** + - 启动开发服务器 + - 测试各项功能 + +## 注意事项 + +1. **工具类型说明** + - BUILTIN: 内置工具(系统自带,不可编辑) + - LOCAL: 本地STDIO工具(通过命令行启动) + - REMOTE: 远程HTTP工具(通过网络连接) + +2. **配置JSON格式** + - LOCAL类型: `{"command": "npx", "args": ["-y", "@example/tool"], "env": {}}` + - REMOTE类型: `{"baseUrl": "http://localhost:8080/mcp"}` + +3. **错误处理** + - 工具连接测试可能超时,请合理设置超时时间 + - 删除工具前请确认没有正在运行的Agent使用该工具 + +4. **性能优化** + - 调用日志数据量大时,建议使用分页加载 + - 图表数据建议缓存处理,避免频繁请求 + +## 常见问题 + +### 1. 跨域问题 +在 `vue.config.js` 中配置代理: +```javascript +devServer: { + proxy: { + '/api': { + target: 'http://localhost:8080', + changeOrigin: true + } + } +} +``` + +### 2. 图表不显示 +确保ECharts容器有固定高度,并在数据加载后初始化图表。 + +### 3. 权限不生效 +检查菜单权限配置和后端接口权限注解是否一致。 diff --git a/ruoyi-modules/ruoyi-chat/docs/mcp-api-spec.md b/ruoyi-modules/ruoyi-chat/docs/mcp-api-spec.md new file mode 100644 index 00000000..9d8460b7 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/docs/mcp-api-spec.md @@ -0,0 +1,336 @@ +# MCP工具管理模块 - API接口文档 + +## 概述 + +本文档描述了MCP工具管理模块的REST API接口,供前端开发人员参考。 + +## 基础信息 + +- **Base URL**: `/api/mcp` +- **认证方式**: Bearer Token (SaToken) +- **响应格式**: JSON + +--- + +## 1. MCP工具管理 + +### 1.1 查询工具列表(分页) + +**接口**: `GET /tool/list` + +**权限**: `mcp:tool:list` + +**请求参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| name | String | 否 | 工具名称(模糊查询) | +| description | String | 否 | 工具描述(模糊查询) | +| type | String | 否 | 工具类型:LOCAL/REMOTE/BUILTIN | +| status | String | 否 | 状态:0-启用, 1-禁用 | +| pageNum | Integer | 是 | 页码,默认1 | +| pageSize | Integer | 是 | 每页数量,默认10 | + +**响应示例**: +```json +{ + "rows": [ + { + "id": 1, + "name": "ReadFileTool", + "description": "读取文件内容工具", + "type": "BUILTIN", + "status": "0", + "configJson": null, + "createTime": "2026-03-08 10:00:00", + "updateTime": "2026-03-08 10:00:00" + } + ], + "total": 1 +} +``` + +### 1.2 查询工具列表(不分页) + +**接口**: `GET /tool/all` + +**权限**: `mcp:tool:list` + +**请求参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| keyword | String | 否 | 关键词 | +| type | String | 否 | 工具类型 | +| status | String | 否 | 状态 | + +**响应示例**: +```json +{ + "tools": [ + { + "id": 1, + "name": "ReadFileTool", + "description": "读取文件内容工具", + "type": "BUILTIN", + "status": "0" + } + ], + "total": 1 +} +``` + +### 1.3 获取工具详情 + +**接口**: `GET /tool/{id}` + +**权限**: `mcp:tool:query` + +**路径参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 工具ID | + +### 1.4 新增工具 + +**接口**: `POST /tool` + +**权限**: `mcp:tool:add` + +**请求体**: +```json +{ + "name": "MyMcpTool", + "description": "我的MCP工具", + "type": "REMOTE", + "status": "0", + "configJson": "{\"baseUrl\": \"http://localhost:8080/mcp\"}" +} +``` + +### 1.5 修改工具 + +**接口**: `PUT /tool` + +**权限**: `mcp:tool:edit` + +**请求体**: 同新增工具 + +### 1.6 删除工具 + +**接口**: `DELETE /tool/{ids}` + +**权限**: `mcp:tool:remove` + +**路径参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| ids | String | 是 | 工具ID,多个用逗号分隔 | + +### 1.7 更新工具状态 + +**接口**: `PUT /tool/{id}/status` + +**权限**: `mcp:tool:edit` + +**路径参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 工具ID | + +**请求参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| status | String | 是 | 状态:0-启用, 1-禁用 | + +### 1.8 测试工具连接 + +**接口**: `POST /tool/{id}/test` + +**权限**: `mcp:tool:query` + +**响应示例**: +```json +{ + "success": true, + "message": "连接测试成功", + "toolCount": 5, + "tools": ["tool1", "tool2", "tool3", "tool4", "tool5"] +} +``` + +--- + +## 2. MCP市场管理 + +### 2.1 查询市场列表 + +**接口**: `GET /market/list` + +**权限**: `mcp:market:list` + +**请求参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| name | String | 否 | 市场名称 | +| description | String | 否 | 市场描述 | +| status | String | 否 | 状态 | +| pageNum | Integer | 是 | 页码 | +| pageSize | Integer | 是 | 每页数量 | + +### 2.2 获取市场工具列表 + +**接口**: `GET /market/{marketId}/tools` + +**权限**: `mcp:market:query` + +**路径参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| marketId | Long | 是 | 市场ID | + +**请求参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| page | Integer | 否 | 页码,默认1 | +| size | Integer | 否 | 每页数量,默认10 | + +### 2.3 刷新市场工具 + +**接口**: `POST /market/{marketId}/refresh` + +**权限**: `mcp:market:edit` + +**响应示例**: +```json +{ + "success": true, + "message": "刷新成功", + "addedCount": 3, + "updatedCount": 5 +} +``` + +### 2.4 加载工具到本地 + +**接口**: `POST /market/tool/{toolId}/load` + +**权限**: `mcp:market:edit` + +**路径参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| toolId | Long | 是 | 市场工具ID | + +### 2.5 批量加载工具 + +**接口**: `POST /market/tools/batchLoad` + +**权限**: `mcp:market:edit` + +**请求体**: +```json +{ + "toolIds": [1, 2, 3] +} +``` + +--- + +## 3. 工具调用日志 + +### 3.1 查询调用日志 + +**接口**: `GET /tool/callLog` + +**权限**: `mcp:tool:query` + +**请求参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| toolId | Long | 否 | 工具ID | +| sessionId | Long | 否 | 会话ID | +| startDate | Date | 否 | 开始日期 | +| endDate | Date | 否 | 结束日期 | +| pageNum | Integer | 是 | 页码 | +| pageSize | Integer | 是 | 每页数量 | + +### 3.2 获取工具统计 + +**接口**: `GET /tool/{toolId}/metrics` + +**权限**: `mcp:tool:query` + +**响应示例**: +```json +{ + "toolId": 1, + "toolName": "ReadFileTool", + "today": { + "callCount": 100, + "successCount": 95, + "failureCount": 5, + "avgDurationMs": 150, + "successRate": 95.0 + }, + "week": { + "callCount": 500, + "successCount": 475, + "failureCount": 25, + "avgDurationMs": 160, + "successRate": 95.0 + } +} +``` + +--- + +## 4. 状态码说明 + +| 状态码 | 说明 | +|--------|------| +| 200 | 请求成功 | +| 401 | 未认证 | +| 403 | 无权限 | +| 404 | 资源不存在 | +| 500 | 服务器错误 | + +--- + +## 5. 前端页面需求 + +### 5.1 MCP工具管理页面 (`/mcp/tool`) + +**功能**: +- 工具列表展示(分页) +- 工具搜索和筛选 +- 新增/编辑/删除工具 +- 工具状态切换 +- 工具连接测试 + +**表格列**: +- 工具名称 +- 工具描述 +- 工具类型(标签显示) +- 状态(开关) +- 创建时间 +- 操作(编辑、删除、测试) + +### 5.2 MCP市场管理页面 (`/mcp/market`) + +**功能**: +- 市场列表展示 +- 市场工具浏览 +- 刷新市场工具 +- 加载工具到本地 + +### 5.3 工具调用日志页面 (`/mcp/log`) + +**功能**: +- 调用日志列表 +- 按工具/日期筛选 +- 成功率统计 +- 响应时间统计 + +**图表**: +- 每日调用次数趋势图 +- 工具调用成功率饼图 +- 平均响应时间柱状图 diff --git a/ruoyi-modules/ruoyi-chat/pom.xml b/ruoyi-modules/ruoyi-chat/pom.xml index e88a1d87..f0a95fe5 100644 --- a/ruoyi-modules/ruoyi-chat/pom.xml +++ b/ruoyi-modules/ruoyi-chat/pom.xml @@ -19,11 +19,6 @@ 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/ChartGenerationAgent.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/ChartGenerationAgent.java index 2de1fcea..ab61ee62 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/ChartGenerationAgent.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/ChartGenerationAgent.java @@ -6,7 +6,7 @@ import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.V; -public interface ChartGenerationAgent { +public interface ChartGenerationAgent extends Agent { @SystemMessage(""" You are a chart generation specialist. Your only task is to generate Apache ECharts 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 fda33dce..7bef6b4a 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 @@ -5,7 +5,7 @@ import dev.langchain4j.service.SystemMessage; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.V; -public interface McpAgent { +public interface McpAgent extends Agent { /** * 系统提示词:通用工具调用智能体 diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/SqlAgent.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/SqlAgent.java index 99e1ff05..77b8e1ab 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/SqlAgent.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/SqlAgent.java @@ -2,7 +2,6 @@ package org.ruoyi.agent; import dev.langchain4j.agentic.Agent; import dev.langchain4j.service.SystemMessage; -import dev.langchain4j.service.TokenStream; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.V; @@ -12,7 +11,7 @@ import dev.langchain4j.service.V; * and returning relevant data and analysis results. * */ -public interface SqlAgent { +public interface SqlAgent extends Agent { @SystemMessage(""" This agent is designed for MySQL 5.7 diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/StreamingCreativeWriter.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/StreamingCreativeWriter.java deleted file mode 100644 index 9486b853..00000000 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/StreamingCreativeWriter.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.ruoyi.agent; - -import dev.langchain4j.agentic.Agent; -import dev.langchain4j.service.TokenStream; -import dev.langchain4j.service.UserMessage; -import dev.langchain4j.service.V; - -public interface StreamingCreativeWriter { - @UserMessage(""" - You are a creative writer. - Generate a draft of a story no more than - 3 sentences long around the given topic. - Return only the story and nothing else. - The topic is {{topic}}. - """) - @Agent("Generates a story based on the given topic") - TokenStream generateStory(@V("topic") String topic); -} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/WebSearchAgent.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/WebSearchAgent.java index 33f8737e..970e3a2b 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/WebSearchAgent.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/WebSearchAgent.java @@ -10,7 +10,7 @@ import dev.langchain4j.service.V; * A web search assistant that answers natural language questions by searching the internet * and returning relevant information from web pages. */ -public interface WebSearchAgent { +public interface WebSearchAgent extends Agent { @SystemMessage(""" You are a web search assistant. Answer questions by searching and retrieving web content. diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/manager/TableSchemaManager.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/manager/TableSchemaManager.java index a02eb78d..7366acc8 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/manager/TableSchemaManager.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/manager/TableSchemaManager.java @@ -40,7 +40,7 @@ import lombok.extern.slf4j.Slf4j; @Component @DS("agent") public class TableSchemaManager { - + @Autowired(required = false) private DataSource agentDataSource; @@ -77,7 +77,7 @@ public class TableSchemaManager { loadAllowedTableSchemas(); initialized = true; log.info("Schema cache initialized with {} tables", schemaCache.size()); - + } catch (Exception e) { log.error("Failed to initialize schema cache", e); } @@ -87,7 +87,7 @@ public class TableSchemaManager { /** * 加载所有允许的表的结构信息 */ - private void loadAllowedTableSchemas() throws SQLException { + private void loadAllowedTableSchemas() { List allowedTables = getAllowedTableNames(); for (String tableName : allowedTables) { try { @@ -191,10 +191,7 @@ public class TableSchemaManager { } List allowedTables = getAllowedTableNames(); - return allowedTables.stream() - .map(schemaCache::get) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + return allowedTables.stream().map(schemaCache::get).filter(Objects::nonNull).collect(Collectors.toList()); } /** diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/ExecuteSqlQueryTool.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/ExecuteSqlQueryTool.java index 2cc33a67..7fb56ad6 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/ExecuteSqlQueryTool.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/ExecuteSqlQueryTool.java @@ -11,13 +11,14 @@ import java.util.Map; import javax.sql.DataSource; -import org.springframework.beans.factory.annotation.Autowired; +import org.ruoyi.common.core.utils.SpringUtils; import org.springframework.stereotype.Component; import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder; import dev.langchain4j.agent.tool.Tool; import lombok.extern.slf4j.Slf4j; +import org.ruoyi.mcp.service.core.BuiltinToolProvider; /** * 执行 SQL 查询的 Tool @@ -25,10 +26,12 @@ import lombok.extern.slf4j.Slf4j; */ @Slf4j @Component -public class ExecuteSqlQueryTool { +public class ExecuteSqlQueryTool implements BuiltinToolProvider { - @Autowired(required = false) - private DataSource dataSource; + // 使用延迟初始化,避免在构造函数中调用 SpringUtils.getBean() + private DataSource getDataSource() { + return SpringUtils.getBean(DataSource.class); + } /** * 执行 SELECT SQL 查询 @@ -52,6 +55,7 @@ public class ExecuteSqlQueryTool { } try { + DataSource dataSource = getDataSource(); if (dataSource == null) { return "Error: Database datasource not configured"; } @@ -177,4 +181,19 @@ public class ExecuteSqlQueryTool { } return str; } + + @Override + public String getToolName() { + return "execute_sql_query"; + } + + @Override + public String getDisplayName() { + return "执行SQL查询"; + } + + @Override + public String getDescription() { + return "Execute a SELECT SQL query and return the results. Example: SELECT * FROM sys_user"; + } } 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 d9ed0666..9fd6637c 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 @@ -4,11 +4,12 @@ import java.util.List; import org.ruoyi.agent.domain.TableStructure; import org.ruoyi.agent.manager.TableSchemaManager; -import org.springframework.beans.factory.annotation.Autowired; +import org.ruoyi.common.core.utils.SpringUtils; import org.springframework.stereotype.Component; import dev.langchain4j.agent.tool.Tool; import lombok.extern.slf4j.Slf4j; +import org.ruoyi.mcp.service.core.BuiltinToolProvider; /** * 查询数据库所有表的 Tool @@ -16,11 +17,13 @@ import lombok.extern.slf4j.Slf4j; */ @Slf4j @Component -public class QueryAllTablesTool { +public class QueryAllTablesTool implements BuiltinToolProvider { + + // 使用延迟初始化,避免在构造函数中调用 SpringUtils.getBean() + private TableSchemaManager getTableSchemaManager() { + return SpringUtils.getBean(TableSchemaManager.class); + } - - @Autowired - private TableSchemaManager tableSchemaManager; // 注入管理器 /** * 查询数据库中所有表 * 返回数据库中存在的所有表的列表 @@ -30,36 +33,49 @@ public class QueryAllTablesTool { @Tool("Query all tables in the database and return table names and basic information") public String queryAllTables() { try { - // 1. 从管理器获取所有允许的表结构信息(内部已包含初始化/缓存逻辑) - List tableSchemas = tableSchemaManager.getAllowedTableSchemas(); - - if (tableSchemas == null || tableSchemas.isEmpty()) { - return "No tables found in database or cache is empty."; - } - - // 2. 格式化结果 - StringBuilder result = new StringBuilder(); - result.append("Found ").append(tableSchemas.size()).append(" tables in cache:\n"); - - for (TableStructure schema : tableSchemas) { - String tableName = schema.getTableName(); - String tableType = schema.getTableType() != null ? schema.getTableType() : "TABLE"; - String tableComment = schema.getTableComment(); - - result.append(String.format("- %s (%s) - %s\n", - tableName, - tableType, - tableComment != null ? tableComment : "No comment")); - } - - log.info("Successfully retrieved {} tables from schema cache", tableSchemas.size()); - return result.toString(); - - } catch (Exception e) { - log.error("Error retrieving tables from cache", e); - return "Error: " + e.getMessage(); + // 1. 从管理器获取所有允许的表结构信息(内部已包含初始化/缓存逻辑) + List tableSchemas = getTableSchemaManager().getAllowedTableSchemas(); + + if (tableSchemas == null || tableSchemas.isEmpty()) { + return "No tables found in database or cache is empty."; } - - + + // 2. 格式化结果 + StringBuilder result = new StringBuilder(); + result.append("Found ").append(tableSchemas.size()).append(" tables in cache:\n"); + + for (TableStructure schema : tableSchemas) { + String tableName = schema.getTableName(); + String tableType = schema.getTableType() != null ? schema.getTableType() : "TABLE"; + String tableComment = schema.getTableComment(); + + result.append(String.format("- %s (%s) - %s\n", + tableName, + tableType, + tableComment != null ? tableComment : "No comment")); + } + + log.info("Successfully retrieved {} tables from schema cache", tableSchemas.size()); + return result.toString(); + + } catch (Exception e) { + log.error("Error retrieving tables from cache", e); + return "Error: " + e.getMessage(); + } + } + + @Override + public String getToolName() { + return "query_all_tables"; + } + + @Override + public String getDisplayName() { + return "查询所有表"; + } + + @Override + public String getDescription() { + return "Query all tables in the database and return table names and basic information"; } } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/QueryTableSchemaTool.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/QueryTableSchemaTool.java index 33d06b27..73a62670 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/QueryTableSchemaTool.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/QueryTableSchemaTool.java @@ -6,20 +6,23 @@ import java.sql.ResultSet; import javax.sql.DataSource; -import org.springframework.beans.factory.annotation.Autowired; +import org.ruoyi.common.core.utils.SpringUtils; import org.springframework.stereotype.Component; import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder; import dev.langchain4j.agent.tool.Tool; import lombok.extern.slf4j.Slf4j; +import org.ruoyi.mcp.service.core.BuiltinToolProvider; @Component @Slf4j -public class QueryTableSchemaTool { +public class QueryTableSchemaTool implements BuiltinToolProvider { - @Autowired(required = false) - private DataSource dataSource; + // 使用延迟初始化,避免在构造函数中调用 SpringUtils.getBean() + private DataSource getDataSource() { + return SpringUtils.getBean(DataSource.class); + } @Tool("Query the CREATE TABLE statement (DDL) for a specific table by table name") public String queryTableSchema(String tableName) { @@ -35,7 +38,7 @@ public class QueryTableSchemaTool { String sql = "SHOW CREATE TABLE `" + tableName + "`"; - try (Connection connection = dataSource.getConnection(); + try (Connection connection = getDataSource().getConnection(); PreparedStatement ps = connection.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { @@ -54,4 +57,19 @@ public class QueryTableSchemaTool { DynamicDataSourceContextHolder.clear(); } } + + @Override + public String getToolName() { + return "query_table_schema"; + } + + @Override + public String getDisplayName() { + return "查询表结构"; + } + + @Override + public String getDescription() { + return "Query the CREATE TABLE statement (DDL) for a specific table by table name"; + } } diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/config/SystemToolInitializer.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/config/mcp/SystemToolInitializer.java similarity index 95% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/config/SystemToolInitializer.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/config/mcp/SystemToolInitializer.java index 30647c65..8426db52 100644 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/config/SystemToolInitializer.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/config/mcp/SystemToolInitializer.java @@ -1,11 +1,11 @@ -package org.ruoyi.mcp.config; +package org.ruoyi.config.mcp; 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.domain.entity.mcp.McpTool; +import org.ruoyi.enums.McpToolStatus; +import org.ruoyi.mapper.mcp.McpToolMapper; import org.ruoyi.mcp.service.core.BuiltinToolDefinition; import org.ruoyi.mcp.service.core.BuiltinToolRegistry; import org.springframework.boot.ApplicationArguments; diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/controller/McpMarketController.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/mcp/McpMarketController.java similarity index 92% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/controller/McpMarketController.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/mcp/McpMarketController.java index 52a0eacc..3f840425 100644 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/controller/McpMarketController.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/mcp/McpMarketController.java @@ -1,4 +1,4 @@ -package org.ruoyi.mcp.controller; +package org.ruoyi.controller.mcp; import cn.dev33.satoken.annotation.SaCheckPermission; import jakarta.servlet.http.HttpServletResponse; @@ -11,11 +11,12 @@ 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.ruoyi.domain.bo.mcp.McpMarketBo; +import org.ruoyi.domain.dto.mcp.McpMarketListResult; +import org.ruoyi.domain.dto.mcp.McpMarketRefreshResult; +import org.ruoyi.domain.dto.mcp.McpMarketToolListResult; +import org.ruoyi.domain.vo.mcp.McpMarketVo; +import org.ruoyi.service.mcp.IMcpMarketService; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -143,7 +144,7 @@ public class McpMarketController extends BaseController { @SaCheckPermission("mcp:market:edit") @Log(title = "MCP市场管理", businessType = BusinessType.UPDATE) @PostMapping("/{marketId}/refresh") - public R refreshMarketTools(@PathVariable Long marketId) { + public R refreshMarketTools(@PathVariable Long marketId) { return R.ok(mcpMarketService.refreshMarketTools(marketId)); } diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/controller/McpToolController.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/mcp/McpToolController.java similarity index 93% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/controller/McpToolController.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/mcp/McpToolController.java index eae82f72..96d51298 100644 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/controller/McpToolController.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/mcp/McpToolController.java @@ -1,4 +1,4 @@ -package org.ruoyi.mcp.controller; +package org.ruoyi.controller.mcp; import cn.dev33.satoken.annotation.SaCheckPermission; import jakarta.servlet.http.HttpServletResponse; @@ -11,11 +11,11 @@ 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.ruoyi.domain.bo.mcp.McpToolBo; +import org.ruoyi.domain.dto.mcp.McpToolListResult; +import org.ruoyi.domain.dto.mcp.McpToolTestResult; +import org.ruoyi.domain.vo.mcp.McpToolVo; +import org.ruoyi.service.mcp.IMcpToolService; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/bo/McpMarketBo.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/bo/mcp/McpMarketBo.java similarity index 93% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/bo/McpMarketBo.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/bo/mcp/McpMarketBo.java index 00493b8d..e0306c0d 100644 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/bo/McpMarketBo.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/bo/mcp/McpMarketBo.java @@ -1,4 +1,4 @@ -package org.ruoyi.mcp.domain.bo; +package org.ruoyi.domain.bo.mcp; import io.github.linpeilie.annotations.AutoMapper; import jakarta.validation.constraints.NotBlank; @@ -6,7 +6,7 @@ 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; +import org.ruoyi.domain.entity.mcp.McpMarket; /** * MCP 市场业务对象 diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/bo/McpToolBo.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/bo/mcp/McpToolBo.java similarity index 94% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/bo/McpToolBo.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/bo/mcp/McpToolBo.java index 7bb2005a..74d699e6 100644 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/bo/McpToolBo.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/bo/mcp/McpToolBo.java @@ -1,4 +1,4 @@ -package org.ruoyi.mcp.domain.bo; +package org.ruoyi.domain.bo.mcp; import io.github.linpeilie.annotations.AutoMapper; import jakarta.validation.constraints.NotBlank; @@ -6,7 +6,7 @@ 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 org.ruoyi.domain.entity.mcp.McpTool; import java.io.Serial; diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketListResult.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpMarketListResult.java similarity index 90% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketListResult.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpMarketListResult.java index 69f0a5d9..eba96d0a 100644 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketListResult.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpMarketListResult.java @@ -1,10 +1,10 @@ -package org.ruoyi.mcp.domain.dto; +package org.ruoyi.domain.dto.mcp; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import org.ruoyi.mcp.domain.entity.McpMarket; +import org.ruoyi.domain.entity.mcp.McpMarket; import java.util.List; diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketRefreshResult.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpMarketRefreshResult.java similarity index 94% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketRefreshResult.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpMarketRefreshResult.java index d52e91c7..e9c1002d 100644 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketRefreshResult.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpMarketRefreshResult.java @@ -1,4 +1,4 @@ -package org.ruoyi.mcp.domain.dto; +package org.ruoyi.domain.dto.mcp; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketToolListResult.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpMarketToolListResult.java similarity index 92% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketToolListResult.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpMarketToolListResult.java index 8f2e6a81..a66cfd9a 100644 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketToolListResult.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpMarketToolListResult.java @@ -1,10 +1,10 @@ -package org.ruoyi.mcp.domain.dto; +package org.ruoyi.domain.dto.mcp; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import org.ruoyi.mcp.domain.entity.McpMarketTool; +import org.ruoyi.domain.entity.mcp.McpMarketTool; import java.util.List; diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpToolListResult.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpToolListResult.java similarity index 90% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpToolListResult.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpToolListResult.java index e330abc2..385edcae 100644 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpToolListResult.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpToolListResult.java @@ -1,10 +1,10 @@ -package org.ruoyi.mcp.domain.dto; +package org.ruoyi.domain.dto.mcp; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import org.ruoyi.mcp.domain.entity.McpTool; +import org.ruoyi.domain.entity.mcp.McpTool; import java.util.List; diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpToolTestResult.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpToolTestResult.java similarity index 96% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpToolTestResult.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpToolTestResult.java index eeb6f3dd..71b873da 100644 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpToolTestResult.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpToolTestResult.java @@ -1,4 +1,4 @@ -package org.ruoyi.mcp.domain.dto; +package org.ruoyi.domain.dto.mcp; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpMarket.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/mcp/McpMarket.java similarity index 96% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpMarket.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/mcp/McpMarket.java index 8d2cf21e..1f805fe2 100644 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpMarket.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/mcp/McpMarket.java @@ -1,4 +1,4 @@ -package org.ruoyi.mcp.domain.entity; +package org.ruoyi.domain.entity.mcp; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpMarketTool.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/mcp/McpMarketTool.java similarity index 96% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpMarketTool.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/mcp/McpMarketTool.java index 69b942ab..ac7e9c02 100644 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpMarketTool.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/mcp/McpMarketTool.java @@ -1,4 +1,4 @@ -package org.ruoyi.mcp.domain.entity; +package org.ruoyi.domain.entity.mcp; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpTool.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/mcp/McpTool.java similarity index 96% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpTool.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/mcp/McpTool.java index 0a5b3088..8ce47880 100644 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpTool.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/mcp/McpTool.java @@ -1,4 +1,4 @@ -package org.ruoyi.mcp.domain.entity; +package org.ruoyi.domain.entity.mcp; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/vo/McpMarketVo.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/mcp/McpMarketVo.java similarity index 94% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/vo/McpMarketVo.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/mcp/McpMarketVo.java index 243604cf..0abbea71 100644 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/vo/McpMarketVo.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/mcp/McpMarketVo.java @@ -1,10 +1,10 @@ -package org.ruoyi.mcp.domain.vo; +package org.ruoyi.domain.vo.mcp; 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 org.ruoyi.domain.entity.mcp.McpMarket; import java.io.Serial; import java.io.Serializable; diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/vo/McpToolVo.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/mcp/McpToolVo.java similarity index 88% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/vo/McpToolVo.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/mcp/McpToolVo.java index 88cd2494..a5fbd748 100644 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/vo/McpToolVo.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/mcp/McpToolVo.java @@ -1,12 +1,11 @@ -package org.ruoyi.mcp.domain.vo; +package org.ruoyi.domain.vo.mcp; 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 org.ruoyi.domain.entity.mcp.McpTool; -import java.io.Serial; import java.io.Serializable; import java.util.Date; @@ -20,9 +19,6 @@ import java.util.Date; @AutoMapper(target = McpTool.class) public class McpToolVo implements Serializable { - @Serial - private static final long serialVersionUID = 1L; - /** * 工具ID */ diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/enums/McpToolStatus.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/McpToolStatus.java similarity index 96% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/enums/McpToolStatus.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/McpToolStatus.java index caf8c49b..7facd02a 100644 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/enums/McpToolStatus.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/McpToolStatus.java @@ -1,4 +1,4 @@ -package org.ruoyi.mcp.enums; +package org.ruoyi.enums; import lombok.Getter; diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpMarketMapper.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/mcp/McpMarketMapper.java similarity index 68% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpMarketMapper.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/mcp/McpMarketMapper.java index 7d9ed31d..6864f2c4 100644 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpMarketMapper.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/mcp/McpMarketMapper.java @@ -1,9 +1,9 @@ -package org.ruoyi.mcp.mapper; +package org.ruoyi.mapper.mcp; 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; +import org.ruoyi.domain.entity.mcp.McpMarket; +import org.ruoyi.domain.vo.mcp.McpMarketVo; /** * MCP 市场信息 Mapper diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpMarketToolMapper.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/mcp/McpMarketToolMapper.java similarity index 76% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpMarketToolMapper.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/mcp/McpMarketToolMapper.java index 2211906d..4580f33c 100644 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpMarketToolMapper.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/mcp/McpMarketToolMapper.java @@ -1,8 +1,8 @@ -package org.ruoyi.mcp.mapper; +package org.ruoyi.mapper.mcp; import org.apache.ibatis.annotations.Mapper; import org.ruoyi.common.mybatis.core.mapper.BaseMapperPlus; -import org.ruoyi.mcp.domain.entity.McpMarketTool; +import org.ruoyi.domain.entity.mcp.McpMarketTool; /** * MCP 市场工具关联 Mapper diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpToolMapper.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/mcp/McpToolMapper.java similarity index 68% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpToolMapper.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/mcp/McpToolMapper.java index 5e46f399..a3ce4d33 100644 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpToolMapper.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/mcp/McpToolMapper.java @@ -1,9 +1,9 @@ -package org.ruoyi.mcp.mapper; +package org.ruoyi.mapper.mcp; 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; +import org.ruoyi.domain.entity.mcp.McpTool; +import org.ruoyi.domain.vo.mcp.McpToolVo; /** * MCP 工具信息 Mapper diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolDefinition.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolDefinition.java similarity index 100% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolDefinition.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolDefinition.java diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolProvider.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolProvider.java similarity index 100% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolProvider.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolProvider.java diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolRegistry.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolRegistry.java new file mode 100644 index 00000000..48e1034f --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolRegistry.java @@ -0,0 +1,174 @@ +package org.ruoyi.mcp.service.core; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.util.ClassUtils; + +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 + * 注意:这些是 Spring 代理,不能直接用于 LangChain4j + * 我们需要提取 Class 信息以便创建新实例 + */ + private final List toolProviders; + + /** + * 内置工具类映射表 (工具名称 -> 工具类) + * 存储 Class 对象而不是实例,以便创建不带 Spring 代理的新实例 + */ + private final Map> registeredToolClasses = new ConcurrentHashMap<>(); + + /** + * 内置工具显示名称映射表 (工具名称 -> 显示名称) + */ + private final Map displayNames = new ConcurrentHashMap<>(); + + /** + * 初始化方法,在 Bean 创建后自动调用 + * 提取工具类信息而不是存储 Spring 代理实例 + */ + @PostConstruct + public void init() { + log.info("开始注册内置工具,发现 {} 个工具提供者", toolProviders.size()); + + // 1. 注册通过 Spring 自动发现工具 + for (BuiltinToolProvider provider : toolProviders) { + String toolName = provider.getToolName(); + + if (registeredToolClasses.containsKey(toolName)) { + log.warn("工具名称重复: {},将覆盖原有注册", toolName); + } + + // 使用 ClassUtils.getUserClass 获取原始类,避免 Spring CGLIB 代理类 + Class targetClass = ClassUtils.getUserClass(provider); + registeredToolClasses.put(toolName, targetClass); + displayNames.put(toolName, provider.getDisplayName()); + log.info("注册内置工具: {} ({}) - 原始类: {}", toolName, provider.getDisplayName(), targetClass.getName()); + } + + log.info("内置工具注册完成,共 {} 个工具", registeredToolClasses.size()); + } + + /** + * 获取工具提供者(返回 Spring 代理,仅用于元数据查询) + * + * @param toolName 工具名称 + * @return 工具提供者,如果不存在则返回 null + */ + public BuiltinToolProvider getToolProvider(String toolName) { + // 这个方法返回 Spring 代理,仅用于获取元数据 + for (BuiltinToolProvider provider : toolProviders) { + if (provider.getToolName().equals(toolName)) { + return provider; + } + } + return null; + } + + /** + * 检查工具是否已注册 + * + * @param toolName 工具名称 + * @return 是否已注册 + */ + public boolean hasTool(String toolName) { + return registeredToolClasses.containsKey(toolName); + } + + /** + * 获取所有内置工具定义 + * + * @return 内置工具定义集合 + */ + public Collection getAllBuiltinTools() { + return displayNames.entrySet().stream() + .map(entry -> new BuiltinToolDefinition( + entry.getKey(), + entry.getValue(), + "" // Description can be added later if needed + )) + .toList(); + } + + /** + * 获取所有内置工具对象 + * 这些对象包含 @Tool 注解的方法,可直接用于 AgenticServices + * 注意:每次调用都创建新实例,以避免 Spring CGLIB 代理问题 + * + * @return 内置工具对象列表 + */ + public List getAllBuiltinToolObjects() { + List toolInstances = new java.util.ArrayList<>(); + + for (java.util.Map.Entry> entry : registeredToolClasses.entrySet()) { + try { + // 使用无参构造函数创建新实例,保留 @Tool 注解 + Object instance = entry.getValue().getDeclaredConstructor().newInstance(); + toolInstances.add(instance); + log.debug("创建工具实例: {}", entry.getKey()); + } catch (Exception e) { + log.error("创建工具实例失败: {} - {}", entry.getKey(), e.getMessage()); + } + } + + return toolInstances; + } + + /** + * 根据工具名称获取工具对象 + * 注意:每次调用都创建新实例,以避免 Spring CGLIB 代理问题 + * + * @param toolName 工具名称 + * @return 工具对象,如果不存在则返回 null + */ + public Object getBuiltinToolObject(String toolName) { + Class toolClass = registeredToolClasses.get(toolName); + if (toolClass == null) { + return null; + } + + try { + // 使用无参构造函数创建新实例,保留 @Tool 注解 + return toolClass.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + log.error("创建工具实例失败: {} - {}", toolName, e.getMessage()); + return null; + } + } +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/LangChain4jMcpToolProviderService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/core/LangChain4jMcpToolProviderService.java similarity index 90% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/LangChain4jMcpToolProviderService.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/core/LangChain4jMcpToolProviderService.java index 7abc9fd9..1c09793e 100644 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/LangChain4jMcpToolProviderService.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/core/LangChain4jMcpToolProviderService.java @@ -1,5 +1,6 @@ package org.ruoyi.mcp.service.core; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import dev.langchain4j.mcp.McpToolProvider; @@ -12,9 +13,9 @@ 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.ruoyi.domain.entity.mcp.McpTool; +import org.ruoyi.enums.McpToolStatus; +import org.ruoyi.mapper.mcp.McpToolMapper; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -99,7 +100,7 @@ public class LangChain4jMcpToolProviderService { */ public ToolProvider getAllEnabledToolsProvider() { List enabledTools = mcpToolMapper.selectList( - new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + new LambdaQueryWrapper() .eq(McpTool::getStatus, McpToolStatus.ENABLED.getValue()) ); @@ -330,6 +331,11 @@ public class LangChain4jMcpToolProviderService { // 处理 Windows 系统的命令 command = resolveCommand(command); + // 检查命令是否可用 + if (!isCommandAvailable(command)) { + throw new IllegalArgumentException("Command '" + command + "' is not available on this system. Please install the required package or use a different tool."); + } + // 构建完整命令列表 List fullCommand = new ArrayList<>(); fullCommand.add(command); @@ -349,6 +355,29 @@ public class LangChain4jMcpToolProviderService { .build(); } + /** + * 检查命令是否在系统上可用 + */ + private boolean isCommandAvailable(String command) { + try { + ProcessBuilder pb = new ProcessBuilder(command, "--version"); + pb.redirectErrorStream(true); + Process process = pb.start(); + boolean finished = process.waitFor(5, java.util.concurrent.TimeUnit.SECONDS); + if (!finished) { + process.destroyForcibly(); + return false; + } + int exitCode = process.exitValue(); + // 对于某些命令,--version 可能返回非零退出码,所以我们只检查进程是否能启动 + // 如果进程能启动并退出(无论退出码是什么),我们认为命令可用 + return true; + } catch (Exception e) { + log.debug("Command '{}' is not available: {}", command, e.getMessage()); + return false; + } + } + /** * 创建远程 HTTP/SSE Client */ diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/core/ToolProviderFactory.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/core/ToolProviderFactory.java new file mode 100644 index 00000000..189593e0 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/core/ToolProviderFactory.java @@ -0,0 +1,49 @@ +package org.ruoyi.mcp.service.core; + +import dev.langchain4j.service.tool.ToolProvider; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 统一工具提供工厂 + * 整合所有类型的MCP工具提供者,为Agent和Chat服务提供统一的工具获取入口 + * + *

支持的工具类型: + *

    + *
  • BUILTIN - 内置工具(如文件操作工具)
  • + *
  • LOCAL - 本地STDIO工具(通过命令行启动的MCP服务器)
  • + *
  • REMOTE - 远程HTTP/SSE工具(通过网络连接的MCP服务器)
  • + *
+ * + * @author ruoyi team + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ToolProviderFactory { + + private final BuiltinToolRegistry builtinToolRegistry; + private final LangChain4jMcpToolProviderService langChain4jMcpToolProviderService; + + /** + * 获取所有已启用的MCP工具的ToolProvider + * + * @return ToolProvider实例 + */ + public ToolProvider getAllEnabledMcpToolsProvider() { + return langChain4jMcpToolProviderService.getAllEnabledToolsProvider(); + } + + /** + * 获取所有 BUILTIN 工具对象 + * 这些对象包含 @Tool 注解的方法,可直接用于 AgenticServices + * + * @return BUILTIN 工具对象列表 + */ + public List getAllBuiltinToolObjects() { + return builtinToolRegistry.getAllBuiltinToolObjects(); + } +} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/EditFileTool.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/tools/EditFileTool.java similarity index 100% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/EditFileTool.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/tools/EditFileTool.java diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/ListDirectoryTool.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/tools/ListDirectoryTool.java similarity index 100% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/ListDirectoryTool.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/tools/ListDirectoryTool.java diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/ReadFileTool.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/tools/ReadFileTool.java similarity index 100% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/ReadFileTool.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/tools/ReadFileTool.java diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/AbstractStreamingChatService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/AbstractStreamingChatService.java index 2e81a9a1..958aacfc 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/AbstractStreamingChatService.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/AbstractStreamingChatService.java @@ -1,50 +1,41 @@ package org.ruoyi.service.chat.impl; import dev.langchain4j.agentic.AgenticServices; -import dev.langchain4j.agentic.supervisor.SupervisorAgent; -import dev.langchain4j.agentic.supervisor.SupervisorResponseStrategy; import dev.langchain4j.community.model.dashscope.QwenChatModel; import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.data.message.UserMessage; -import dev.langchain4j.mcp.McpToolProvider; -import dev.langchain4j.mcp.client.DefaultMcpClient; -import dev.langchain4j.mcp.client.McpClient; -import dev.langchain4j.mcp.client.transport.McpTransport; -import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport; import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.model.chat.StreamingChatModel; import dev.langchain4j.model.chat.response.ChatResponse; import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; -import dev.langchain4j.model.openai.OpenAiChatModel; import dev.langchain4j.service.tool.ToolProvider; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.ruoyi.agent.ChartGenerationAgent; -import org.ruoyi.agent.SqlAgent; -import org.ruoyi.agent.WebSearchAgent; -import org.ruoyi.agent.tool.ExecuteSqlQueryTool; -import org.ruoyi.agent.tool.QueryAllTablesTool; -import org.ruoyi.agent.tool.QueryTableSchemaTool; +import org.ruoyi.agent.McpAgent; import org.ruoyi.common.chat.base.ThreadContext; +import org.ruoyi.common.chat.domain.dto.request.ChatRequest; import org.ruoyi.common.chat.domain.dto.request.ReSumeRunner; import org.ruoyi.common.chat.domain.dto.request.WorkFlowRunner; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; +import org.ruoyi.common.chat.entity.chat.ChatContext; import org.ruoyi.common.chat.enums.RoleType; import org.ruoyi.common.chat.service.chat.IChatService; -import org.ruoyi.common.chat.domain.dto.request.ChatRequest; -import org.ruoyi.common.chat.entity.chat.ChatContext; -import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.common.chat.service.chatMessage.AbstractChatMessageService; import org.ruoyi.common.chat.service.workFlow.IWorkFlowStarterService; import org.ruoyi.common.core.utils.ObjectUtils; import org.ruoyi.common.core.utils.SpringUtils; import org.ruoyi.common.core.utils.StringUtils; import org.ruoyi.common.sse.utils.SseMessageUtils; +import org.ruoyi.mcp.service.core.ToolProviderFactory; import org.ruoyi.service.chat.impl.memory.PersistentChatMemoryStore; import org.springframework.util.CollectionUtils; import org.springframework.validation.annotation.Validated; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; /** @@ -213,17 +204,6 @@ public abstract class AbstractStreamingChatService extends AbstractChatMessageSe }); } - /** - * 清理指定会话的内存缓存(可选) - * 在会话结束时调用,释放内存资源 - * - * @param sessionId 会话ID - */ - public static void clearChatMemory(Object sessionId) { - memoryCache.remove(sessionId); - log.debug("已清理会话 {} 的内存缓存", sessionId); - } - /** * 执行聊天(钩子方法 - 子类必须实现) * 注意:messages 已包含完整的历史上下文和当前消息 @@ -232,7 +212,8 @@ public abstract class AbstractStreamingChatService extends AbstractChatMessageSe * @param chatRequest 聊天请求 * @param handler 响应处理器 */ - protected abstract void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List messagesWithMemory, StreamingChatResponseHandler handler); + protected abstract void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, + List messagesWithMemory, StreamingChatResponseHandler handler); /** * 创建标准的响应处理器 @@ -302,103 +283,80 @@ public abstract class AbstractStreamingChatService extends AbstractChatMessageSe }; } - /** - * 构建具体厂商的 StreamingChatModel - * 子类必须实现此方法,返回对应厂商的模型实例 - */ - protected abstract StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest); - - /** * 获取提供者名称(子类必须实现) */ public abstract String getProviderName(); protected String doAgent(String userMessage, ChatModelVo chatModelVo) { - // 步骤1: 配置MCP传输层 - 连接到bing-cn-mcp服务器 - // 该服务提供两个工具: bing_search (必应搜索) 和 crawl_webpage (网页抓取) - // McpTransport transport = new StdioMcpTransport.Builder() - // .command(List.of("C:\\Program Files\\nodejs\\npx.cmd", "-y", - // "bing-cn-mcp" - // )) - // .logEvents(true) - // .build(); - - // // 步骤2: 创建MCP客户端 - // McpClient mcpClient = new DefaultMcpClient.Builder() - // .transport(transport) - // .build(); - - // // 步骤3: 配置工具提供者 - // ToolProvider toolProvider = McpToolProvider.builder() - // .mcpClients(List.of(mcpClient)) - // .build(); - - - McpTransport transport1 = new StdioMcpTransport.Builder() - .command(List.of("npx", "-y", - "mcp-echarts" - )) - .logEvents(true) - .build(); - - // 步骤2: 创建MCP客户端 - McpClient mcpClient1 = new DefaultMcpClient.Builder() - .transport(transport1) - .build(); - - // 步骤3: 配置工具提供者 - ToolProvider toolProvider1 = McpToolProvider.builder() - .mcpClients(List.of(mcpClient1)) - .build(); - - // 步骤4: 配置OpenAI模型 - // OpenAiChatModel PLANNER_MODEL = OpenAiChatModel.builder() - // .baseUrl(chatModelVo.getApiHost()) - // .apiKey(chatModelVo.getApiKey()) - // .modelName(chatModelVo.getModelName()) - // .build(); - - - QwenChatModel qwenChatModel = QwenChatModel.builder() - // .baseUrl(chatModelVo.getApiHost()) - .apiKey(chatModelVo.getApiKey()) - .modelName(chatModelVo.getModelName()) - .build(); - - SqlAgent sqlAgent = AgenticServices.agentBuilder(SqlAgent.class) - .chatModel( - qwenChatModel) - .tools( - SpringUtils.getBean(QueryAllTablesTool.class), // 必须通过 getBean 获取 - SpringUtils.getBean(QueryTableSchemaTool.class), - SpringUtils.getBean(ExecuteSqlQueryTool.class) - ) - .build(); - - // WebSearchAgent searchAgent = AgenticServices.agentBuilder(WebSearchAgent.class) - // .chatModel(PLANNER_MODEL) - // .toolProvider(toolProvider) - // .build(); - - ChartGenerationAgent chartGenerationAgent = AgenticServices.agentBuilder(ChartGenerationAgent.class) - .chatModel( - qwenChatModel) - .toolProvider(toolProvider1) - .build(); - String res = sqlAgent.getData(userMessage); - String res1 = chartGenerationAgent.generateChart(res); - System.out.println(res1); - System.out.println(res); - SupervisorAgent supervisor = AgenticServices - .supervisorBuilder() - .chatModel(qwenChatModel) - .subAgents(sqlAgent, chartGenerationAgent) - .responseStrategy(SupervisorResponseStrategy.LAST) - .build(); - - String invoke = supervisor.invoke(userMessage); - System.out.println(invoke); - return res1; + log.info("执行Agent任务,消息: {}", userMessage); + // 加载所有可用的 Agent,让 Supervisor 根据任务类型自动选择 + return doAgentWithAllAgents(userMessage, chatModelVo); } + + /** + * 使用单一 Agent 处理所有任务 + * 不使用 Supervisor 模式,而是使用 MCP Agent 来处理所有任务 + * + * @param userMessage 用户消息 + * @param chatModelVo 聊天模型配置 + * @return Agent 响应结果 + */ + protected String doAgentWithAllAgents(String userMessage, ChatModelVo chatModelVo) { + + try { + // 1. 加载 LLM 模型 + QwenChatModel qwenChatModel = QwenChatModel.builder() + .apiKey(chatModelVo.getApiKey()) + .modelName(chatModelVo.getModelName()) + .build(); + + // 2. 获取统一工具提供工厂 + ToolProviderFactory toolProviderFactory = SpringUtils.getBean(ToolProviderFactory.class); + + // 3. 获取所有可用的工具 + + // 3.1 添加 BUILTIN 工具对象(包括 SQL 工具) + List builtinTools = toolProviderFactory.getAllBuiltinToolObjects(); + + List allTools = new ArrayList<>(builtinTools); + + log.debug("Loaded {} builtin tools (including SQL tools)", builtinTools.size()); + + log.debug("Total tools: {}", allTools.size()); + + // 4. 获取 MCP 工具提供者 + ToolProvider mcpToolProvider = toolProviderFactory.getAllEnabledMcpToolsProvider(); + + // 5. 创建 MCP Agent(包含所有工具) + var agentBuilder = AgenticServices.agentBuilder(McpAgent.class).chatModel(qwenChatModel); + + // 添加所有工具 + if (!allTools.isEmpty()) { + agentBuilder.tools(allTools.toArray(new Object[0])); + } + + // 添加 MCP 工具 + if (mcpToolProvider != null) { + agentBuilder.toolProvider(mcpToolProvider); + } + + McpAgent mcpAgent = agentBuilder.build(); + + // 6. 调用大模型LLM + String result = mcpAgent.callMcpTool(userMessage); + log.info("Agent 执行完成,结果长度: {}", result.length()); + return result; + + } catch (Exception e) { + log.error("Agent 模式执行失败: {}", e.getMessage(), e); + } + return null; + } + + /** + * 创建流式聊天模型 + * 子类必须实现此方法,返回对应厂商的模型实例 + */ + protected abstract StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest); } 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 0b65a1f9..22a9ae0e 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,30 +1,21 @@ package org.ruoyi.service.chat.impl.provider; -import dev.langchain4j.agentic.AgenticServices; -import dev.langchain4j.agentic.supervisor.SupervisorAgent; -import dev.langchain4j.agentic.supervisor.SupervisorResponseStrategy; -import dev.langchain4j.community.model.dashscope.QwenChatModel; import dev.langchain4j.community.model.dashscope.QwenStreamingChatModel; import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.data.message.SystemMessage; import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.model.chat.StreamingChatModel; import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; -import dev.langchain4j.service.tool.ToolProvider; import lombok.extern.slf4j.Slf4j; -import org.ruoyi.agent.McpAgent; import org.ruoyi.common.chat.domain.dto.request.ChatRequest; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; -import org.ruoyi.common.core.utils.SpringUtils; import org.ruoyi.enums.ChatModeType; -import org.ruoyi.mcp.service.core.ToolProviderFactory; import org.ruoyi.service.chat.impl.AbstractStreamingChatService; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.locks.ReentrantLock; /** * qianWenAI服务调用 @@ -39,9 +30,6 @@ public class QianWenChatServiceImpl extends AbstractStreamingChatService { // 添加文档解析的前缀字段 private static final String UPLOAD_FILE_API_PREFIX = "fileid"; - // 用于线程安全的锁 - private final ReentrantLock cacheLock = new ReentrantLock(); - @Override protected StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo,ChatRequest chatRequest) { return QwenStreamingChatModel.builder() @@ -91,69 +79,6 @@ public class QianWenChatServiceImpl extends AbstractStreamingChatService { }).orElse(messagesWithMemory); } - /** - * 调用MCP服务(智能体) - * 使用统一的ToolProviderFactory获取所有已配置的工具(BUILTIN + MCP) - * - * @param userMessage 用户信息 - * @param chatModelVo 模型信息 - * @return 返回LLM信息 - */ - protected String doAgent(String userMessage, ChatModelVo chatModelVo) { - try { - // 步骤1: 获取统一工具提供工厂 - ToolProviderFactory toolProviderFactory = SpringUtils.getBean(ToolProviderFactory.class); - - // 步骤2: 获取 BUILTIN 工具对象 - List builtinTools = toolProviderFactory.getAllBuiltinToolObjects(); - - // 步骤3: 获取 MCP 工具提供者 - ToolProvider mcpToolProvider = toolProviderFactory.getAllEnabledMcpToolsProvider(); - - log.info("doAgent: BUILTIN tools count = {}, MCP tools enabled = {}", - builtinTools.size(), mcpToolProvider != null); - - // 步骤4: 加载LLM模型 - QwenChatModel qwenChatModel = QwenChatModel.builder() - .apiKey(chatModelVo.getApiKey()) - .modelName(chatModelVo.getModelName()) - .build(); - - // 步骤5: 创建MCP Agent,使用所有已配置的工具 - // 使用 .tools() 传入 BUILTIN 工具对象(Java 对象,带 @Tool 注解的方法) - // 使用 .toolProvider() 传入 MCP 工具提供者(MCP 协议工具) - var agentBuilder = AgenticServices.agentBuilder(McpAgent.class) - .chatModel(qwenChatModel); - - // 添加 BUILTIN 工具(如果有) - if (!builtinTools.isEmpty()) { - agentBuilder.tools(builtinTools.toArray(new Object[0])); - log.debug("Added {} BUILTIN tools to agent", builtinTools.size()); - } - - // 添加 MCP 工具(如果有) - if (mcpToolProvider != null) { - agentBuilder.toolProvider(mcpToolProvider); - log.debug("Added MCP tool provider to agent"); - } - - McpAgent mcpAgent = agentBuilder.build(); - - // 步骤6: 创建超级智能体协调MCP Agent - SupervisorAgent supervisor = AgenticServices - .supervisorBuilder() - .chatModel(qwenChatModel) - .subAgents(mcpAgent) - .responseStrategy(SupervisorResponseStrategy.LAST) - .build(); - - // 步骤7: 调用大模型LLM - return supervisor.invoke(userMessage); - } finally { - cacheLock.unlock(); - } - } - @Override public String getProviderName() { return ChatModeType.QIAN_WEN.getCode(); diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/IMcpMarketService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/mcp/IMcpMarketService.java similarity index 89% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/IMcpMarketService.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/mcp/IMcpMarketService.java index b7a68b1f..e359100b 100644 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/IMcpMarketService.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/mcp/IMcpMarketService.java @@ -1,12 +1,12 @@ -package org.ruoyi.mcp.service; +package org.ruoyi.service.mcp; 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 org.ruoyi.domain.bo.mcp.McpMarketBo; +import org.ruoyi.domain.dto.mcp.McpMarketListResult; +import org.ruoyi.domain.dto.mcp.McpMarketRefreshResult; +import org.ruoyi.domain.dto.mcp.McpMarketToolListResult; +import org.ruoyi.domain.vo.mcp.McpMarketVo; import java.util.List; diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/IMcpToolService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/mcp/IMcpToolService.java similarity index 88% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/IMcpToolService.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/mcp/IMcpToolService.java index d7cba323..d61ef823 100644 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/IMcpToolService.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/mcp/IMcpToolService.java @@ -1,11 +1,11 @@ -package org.ruoyi.mcp.service; +package org.ruoyi.service.mcp; 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 org.ruoyi.domain.bo.mcp.McpToolBo; +import org.ruoyi.domain.dto.mcp.McpToolListResult; +import org.ruoyi.domain.dto.mcp.McpToolTestResult; +import org.ruoyi.domain.vo.mcp.McpToolVo; import java.util.List; diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/impl/McpMarketServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/mcp/impl/McpMarketServiceImpl.java similarity index 94% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/impl/McpMarketServiceImpl.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/mcp/impl/McpMarketServiceImpl.java index b3bed3ab..46eaad7b 100644 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/impl/McpMarketServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/mcp/impl/McpMarketServiceImpl.java @@ -1,4 +1,4 @@ -package org.ruoyi.mcp.service.impl; +package org.ruoyi.service.mcp.impl; import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpResponse; @@ -13,19 +13,19 @@ 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.ruoyi.domain.bo.mcp.McpMarketBo; +import org.ruoyi.domain.dto.mcp.McpMarketListResult; +import org.ruoyi.domain.dto.mcp.McpMarketRefreshResult; +import org.ruoyi.domain.dto.mcp.McpMarketToolListResult; +import org.ruoyi.domain.entity.mcp.McpMarket; +import org.ruoyi.domain.entity.mcp.McpMarketTool; +import org.ruoyi.domain.entity.mcp.McpTool; +import org.ruoyi.domain.vo.mcp.McpMarketVo; +import org.ruoyi.enums.McpToolStatus; +import org.ruoyi.mapper.mcp.McpMarketMapper; +import org.ruoyi.mapper.mcp.McpMarketToolMapper; +import org.ruoyi.mapper.mcp.McpToolMapper; +import org.ruoyi.service.mcp.IMcpMarketService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/impl/McpToolServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/mcp/impl/McpToolServiceImpl.java similarity index 95% rename from ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/impl/McpToolServiceImpl.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/mcp/impl/McpToolServiceImpl.java index 26c2d247..ce7c684d 100644 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/impl/McpToolServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/mcp/impl/McpToolServiceImpl.java @@ -1,4 +1,4 @@ -package org.ruoyi.mcp.service.impl; +package org.ruoyi.service.mcp.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; @@ -9,14 +9,14 @@ 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.domain.bo.mcp.McpToolBo; +import org.ruoyi.domain.dto.mcp.McpToolListResult; +import org.ruoyi.domain.dto.mcp.McpToolTestResult; +import org.ruoyi.domain.entity.mcp.McpTool; +import org.ruoyi.domain.vo.mcp.McpToolVo; +import org.ruoyi.enums.McpToolStatus; +import org.ruoyi.mapper.mcp.McpToolMapper; +import org.ruoyi.service.mcp.IMcpToolService; import org.ruoyi.mcp.service.core.BuiltinToolRegistry; import org.ruoyi.mcp.service.core.LangChain4jMcpToolProviderService; import org.springframework.stereotype.Service; diff --git a/ruoyi-modules/ruoyi-mcp/pom.xml b/ruoyi-modules/ruoyi-mcp/pom.xml deleted file mode 100644 index e5dfa27a..00000000 --- a/ruoyi-modules/ruoyi-mcp/pom.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - 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 deleted file mode 100644 index e686c9a2..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/config/McpProperties.java +++ /dev/null @@ -1,40 +0,0 @@ -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/service/core/BuiltinToolRegistry.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolRegistry.java deleted file mode 100644 index b3953b7f..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolRegistry.java +++ /dev/null @@ -1,129 +0,0 @@ -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/ToolProviderFactory.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/ToolProviderFactory.java deleted file mode 100644 index edf21bb3..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/ToolProviderFactory.java +++ /dev/null @@ -1,171 +0,0 @@ -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(); - } -} From e601eb6db56db2dabaeda56bda5d3fe72cfcabba Mon Sep 17 00:00:00 2001 From: ageerle Date: Mon, 9 Mar 2026 10:27:53 +0800 Subject: [PATCH 11/11] =?UTF-8?q?feat(wechat):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + README.md | 23 ++++++++ README_EN.md | 17 +----- docs/image/bibi.png | Bin 0 -> 72069 bytes docs/image/dy.png | Bin 0 -> 159863 bytes pom.xml | 4 +- .../src/main/resources/application-dev.yml | 4 +- .../src/main/resources/application.yml | 5 +- .../common/core/service/ConfigService.java | 9 +++ .../system}/ChatConfigController.java | 52 ++++++++++++------ .../org/ruoyi/system/domain}/ChatConfig.java | 9 ++- .../ruoyi/system/domain/bo}/ChatConfigBo.java | 13 +++-- .../ruoyi/system/domain/vo}/ChatConfigVo.java | 5 +- .../system/mapper}/ChatConfigMapper.java | 6 +- .../system/service}/IChatConfigService.java | 6 +- .../service}/impl/ChatConfigServiceImpl.java | 24 ++++---- .../service/impl/SysConfigServiceImpl.java | 13 +++++ .../service/impl/UserLoginServiceImpl.java | 18 ++++-- .../java/org/ruoyi/util/WeixinApiUtil.java | 2 +- 19 files changed, 142 insertions(+), 71 deletions(-) create mode 100644 docs/image/bibi.png create mode 100644 docs/image/dy.png rename ruoyi-modules/{ruoyi-chat/src/main/java/org/ruoyi/controller/chat => ruoyi-system/src/main/java/org/ruoyi/system/controller/system}/ChatConfigController.java (78%) rename ruoyi-modules/{ruoyi-chat/src/main/java/org/ruoyi/domain/entity/chat => ruoyi-system/src/main/java/org/ruoyi/system/domain}/ChatConfig.java (80%) rename ruoyi-modules/{ruoyi-chat/src/main/java/org/ruoyi/domain/bo/chat => ruoyi-system/src/main/java/org/ruoyi/system/domain/bo}/ChatConfigBo.java (88%) rename ruoyi-modules/{ruoyi-chat/src/main/java/org/ruoyi/domain/vo/chat => ruoyi-system/src/main/java/org/ruoyi/system/domain/vo}/ChatConfigVo.java (93%) rename ruoyi-modules/{ruoyi-chat/src/main/java/org/ruoyi/mapper/chat => ruoyi-system/src/main/java/org/ruoyi/system/mapper}/ChatConfigMapper.java (64%) rename ruoyi-modules/{ruoyi-chat/src/main/java/org/ruoyi/service/chat => ruoyi-system/src/main/java/org/ruoyi/system/service}/IChatConfigService.java (91%) rename ruoyi-modules/{ruoyi-chat/src/main/java/org/ruoyi/service/chat => ruoyi-system/src/main/java/org/ruoyi/system/service}/impl/ChatConfigServiceImpl.java (93%) diff --git a/.gitignore b/.gitignore index d9cc6d06..f4016dc4 100644 --- a/.gitignore +++ b/.gitignore @@ -21,10 +21,13 @@ target/ ### IntelliJ IDEA ### .idea +.claude +.github *.iws *.iml *.ipr + ### JRebel ### rebel.xml diff --git a/README.md b/README.md index 003e6971..bb8c4859 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,29 @@ --- + +## 📺 视频教程 + +
+ + + + + + + +
+微信二维码
+打开抖音扫一扫
+获取免费视频教程 +
+QQ群二维码
+打开B站扫一扫
+获取免费视频教程 +
+ +
+
**[⭐ 点个Star支持一下](https://github.com/ageerle/ruoyi-ai)** • **[ Fork 开始贡献](https://github.com/ageerle/ruoyi-ai/fork)** • **[📚 English](README_EN.md)** • **[📖 查看完整文档](https://doc.pandarobot.chat)** diff --git a/README_EN.md b/README_EN.md index 3ae559e1..20e4a54e 100644 --- a/README_EN.md +++ b/README_EN.md @@ -1,3 +1,4 @@ + # RuoYi AI
@@ -115,21 +116,7 @@ Thanks to the following excellent open-source projects for their support:
- - - - - - -
-WeChat QR Code
-Scan to Add Author's WeChat
-Invitation to join the group -
-QQ Group QR Code
-QQ Technical Discussion Group
-Technical discussions -
+**[📱 Join Telegram Group](https://t.me/+LqooQAc5HxRmYmE1)**
diff --git a/docs/image/bibi.png b/docs/image/bibi.png new file mode 100644 index 0000000000000000000000000000000000000000..bafba054fa6eef9e5422a6fa7f7a3f0132ad0fe8 GIT binary patch literal 72069 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

Z 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

!VwI&`MPRWcjUk^{s|7{jq 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 S)i$>-u|Ko^ai}O|_x}NIg|deL literal 0 HcmV?d00001 diff --git a/pom.xml b/pom.xml index 36bf6d39..8771fb18 100644 --- a/pom.xml +++ b/pom.xml @@ -50,9 +50,9 @@ 1.8.2 - 4.4.0 + 4.6.0 - 2.20.1 + 2.18.2 1.11.0 1.11.0-beta19 diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index 03b7bc24..42ef9fff 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -90,7 +90,7 @@ spring: sys: upload: path: D:\\DownLoad - + --- # redis 单机配置(单机与集群只能开启一个另一个需要注释掉) spring.data: redis: @@ -268,4 +268,4 @@ justauth: client-secret: 1f7d08**********5b7**********29e redirect-uri: ${justauth.address}/social-callback?source=gitea -AGENT_ALLOWED_TABLES: "abtest_rule,abtest_project,agent_ban_log,agent_ban_logs,agent_install_sub_task,agent_install_sum_task,agent_install_task" \ No newline at end of file +AGENT_ALLOWED_TABLES: "abtest_rule,abtest_project,agent_ban_log,agent_ban_logs,agent_install_sub_task,agent_install_sum_task,agent_install_task" diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index ca371e3d..5743c299 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -125,7 +125,9 @@ security: - /*/api-docs/** - /warm-flow-ui/config - /workflow/run - + - /user/qrcode + - /user/login/qrcode + - /weixin/check # 多租户配置 tenant: # 是否开启 @@ -309,6 +311,7 @@ wechat: secret: '' token: '' aesKey: '' + --- # Neo4j 知识图谱配置 neo4j: uri: bolt://117.72.192.162:7687 diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/ruoyi/common/core/service/ConfigService.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/ruoyi/common/core/service/ConfigService.java index 7a5cf5b5..c2ebc98b 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/ruoyi/common/core/service/ConfigService.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/ruoyi/common/core/service/ConfigService.java @@ -15,4 +15,13 @@ public interface ConfigService { */ String getConfigValue(String configKey); + /** + * 根据配置类型和配置key获取值 + * + * @param category 配置类型 + * @param configKey 配置key + * @return 配置属性 + */ + String getConfigValue(String category, String configKey); + } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatConfigController.java b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/controller/system/ChatConfigController.java similarity index 78% rename from ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatConfigController.java rename to ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/controller/system/ChatConfigController.java index 994334d2..7c15462c 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatConfigController.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/controller/system/ChatConfigController.java @@ -1,26 +1,27 @@ -package org.ruoyi.controller.chat; +package org.ruoyi.system.controller.system; -import java.util.List; - -import lombok.RequiredArgsConstructor; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.validation.constraints.*; import cn.dev33.satoken.annotation.SaCheckPermission; -import org.ruoyi.common.web.core.BaseController; -import org.ruoyi.service.chat.IChatConfigService; -import org.springframework.web.bind.annotation.*; -import org.springframework.validation.annotation.Validated; -import org.ruoyi.common.idempotent.annotation.RepeatSubmit; -import org.ruoyi.common.log.annotation.Log; -import org.ruoyi.common.mybatis.core.page.PageQuery; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; import org.ruoyi.common.core.domain.R; import org.ruoyi.common.core.validate.AddGroup; import org.ruoyi.common.core.validate.EditGroup; -import org.ruoyi.common.log.enums.BusinessType; import org.ruoyi.common.excel.utils.ExcelUtil; -import org.ruoyi.domain.vo.chat.ChatConfigVo; -import org.ruoyi.domain.bo.chat.ChatConfigBo; +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.system.domain.bo.ChatConfigBo; +import org.ruoyi.system.domain.vo.ChatConfigVo; +import org.ruoyi.system.service.IChatConfigService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; /** * 配置信息 @@ -79,6 +80,25 @@ public class ChatConfigController extends BaseController { return toAjax(chatConfigService.insertByBo(bo)); } + /** + * 新增或者修改配置信息 + */ + @SaCheckPermission("system:config:add") + @Log(title = "新增或者修改配置信息", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PostMapping("/saveOrUpdate") + public R saveOrUpdate(@RequestBody List boList) { + for (ChatConfigBo chatConfigBo : boList) { + if (chatConfigBo.getId() == null) { + chatConfigService.insertByBo(chatConfigBo); + } else { + chatConfigService.updateByBo(chatConfigBo); + } + } + return toAjax(true); + } + + /** * 修改配置信息 */ diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/chat/ChatConfig.java b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/domain/ChatConfig.java similarity index 80% rename from ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/chat/ChatConfig.java rename to ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/domain/ChatConfig.java index eaa8020b..a9f0e510 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/chat/ChatConfig.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/domain/ChatConfig.java @@ -1,9 +1,12 @@ -package org.ruoyi.domain.entity.chat; +package org.ruoyi.system.domain; -import org.ruoyi.common.tenant.core.TenantEntity; -import com.baomidou.mybatisplus.annotation.*; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.Version; import lombok.Data; import lombok.EqualsAndHashCode; +import org.ruoyi.common.tenant.core.TenantEntity; import java.io.Serial; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/bo/chat/ChatConfigBo.java b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/domain/bo/ChatConfigBo.java similarity index 88% rename from ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/bo/chat/ChatConfigBo.java rename to ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/domain/bo/ChatConfigBo.java index 5167fb58..8f38bb7c 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/bo/chat/ChatConfigBo.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/domain/bo/ChatConfigBo.java @@ -1,13 +1,14 @@ -package org.ruoyi.domain.bo.chat; +package org.ruoyi.system.domain.bo; -import org.ruoyi.common.core.validate.AddGroup; -import org.ruoyi.common.core.validate.EditGroup; -import org.ruoyi.domain.entity.chat.ChatConfig; -import org.ruoyi.common.mybatis.core.domain.BaseEntity; import io.github.linpeilie.annotations.AutoMapper; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.Data; import lombok.EqualsAndHashCode; -import jakarta.validation.constraints.*; +import org.ruoyi.common.core.validate.AddGroup; +import org.ruoyi.common.core.validate.EditGroup; +import org.ruoyi.common.mybatis.core.domain.BaseEntity; +import org.ruoyi.system.domain.ChatConfig; /** * 配置信息业务对象 chat_config diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/chat/ChatConfigVo.java b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/domain/vo/ChatConfigVo.java similarity index 93% rename from ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/chat/ChatConfigVo.java rename to ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/domain/vo/ChatConfigVo.java index b5188404..7365ef08 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/chat/ChatConfigVo.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/domain/vo/ChatConfigVo.java @@ -1,16 +1,15 @@ -package org.ruoyi.domain.vo.chat; +package org.ruoyi.system.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.domain.entity.chat.ChatConfig; +import org.ruoyi.system.domain.ChatConfig; import java.io.Serial; import java.io.Serializable; - /** * 配置信息视图对象 chat_config * diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/chat/ChatConfigMapper.java b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/mapper/ChatConfigMapper.java similarity index 64% rename from ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/chat/ChatConfigMapper.java rename to ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/mapper/ChatConfigMapper.java index 11ca95bb..d740a9f2 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/chat/ChatConfigMapper.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/mapper/ChatConfigMapper.java @@ -1,8 +1,8 @@ -package org.ruoyi.mapper.chat; +package org.ruoyi.system.mapper; -import org.ruoyi.domain.entity.chat.ChatConfig; -import org.ruoyi.domain.vo.chat.ChatConfigVo; import org.ruoyi.common.mybatis.core.mapper.BaseMapperPlus; +import org.ruoyi.system.domain.ChatConfig; +import org.ruoyi.system.domain.vo.ChatConfigVo; /** * 配置信息Mapper接口 diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/IChatConfigService.java b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/IChatConfigService.java similarity index 91% rename from ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/IChatConfigService.java rename to ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/IChatConfigService.java index 3c7f25c5..13a917b9 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/IChatConfigService.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/IChatConfigService.java @@ -1,9 +1,9 @@ -package org.ruoyi.service.chat; +package org.ruoyi.system.service; import org.ruoyi.common.mybatis.core.page.PageQuery; import org.ruoyi.common.mybatis.core.page.TableDataInfo; -import org.ruoyi.domain.bo.chat.ChatConfigBo; -import org.ruoyi.domain.vo.chat.ChatConfigVo; +import org.ruoyi.system.domain.bo.ChatConfigBo; +import org.ruoyi.system.domain.vo.ChatConfigVo; import java.util.Collection; import java.util.List; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatConfigServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/impl/ChatConfigServiceImpl.java similarity index 93% rename from ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatConfigServiceImpl.java rename to ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/impl/ChatConfigServiceImpl.java index ee1954a0..b79c256f 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatConfigServiceImpl.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/impl/ChatConfigServiceImpl.java @@ -1,24 +1,24 @@ -package org.ruoyi.service.chat.impl; +package org.ruoyi.system.service.impl; -import org.ruoyi.common.core.utils.MapstructUtils; -import org.ruoyi.common.core.utils.StringUtils; -import org.ruoyi.common.mybatis.core.page.TableDataInfo; -import org.ruoyi.common.mybatis.core.page.PageQuery; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 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.service.chat.IChatConfigService; +import org.ruoyi.common.core.utils.MapstructUtils; +import org.ruoyi.common.core.utils.StringUtils; +import org.ruoyi.common.mybatis.core.page.PageQuery; +import org.ruoyi.common.mybatis.core.page.TableDataInfo; +import org.ruoyi.system.domain.ChatConfig; +import org.ruoyi.system.domain.bo.ChatConfigBo; +import org.ruoyi.system.domain.vo.ChatConfigVo; +import org.ruoyi.system.mapper.ChatConfigMapper; +import org.ruoyi.system.service.IChatConfigService; import org.springframework.stereotype.Service; -import org.ruoyi.domain.bo.chat.ChatConfigBo; -import org.ruoyi.domain.vo.chat.ChatConfigVo; -import org.ruoyi.domain.entity.chat.ChatConfig; -import org.ruoyi.mapper.chat.ChatConfigMapper; +import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.Collection; /** * 配置信息Service业务层处理 diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/impl/SysConfigServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/impl/SysConfigServiceImpl.java index 7f285089..b3b77a97 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/impl/SysConfigServiceImpl.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/impl/SysConfigServiceImpl.java @@ -18,9 +18,11 @@ import org.ruoyi.common.mybatis.core.page.PageQuery; import org.ruoyi.common.mybatis.core.page.TableDataInfo; import org.ruoyi.common.redis.utils.CacheUtils; import org.ruoyi.common.tenant.helper.TenantHelper; +import org.ruoyi.system.domain.ChatConfig; import org.ruoyi.system.domain.SysConfig; import org.ruoyi.system.domain.bo.SysConfigBo; import org.ruoyi.system.domain.vo.SysConfigVo; +import org.ruoyi.system.mapper.ChatConfigMapper; import org.ruoyi.system.mapper.SysConfigMapper; import org.ruoyi.system.service.ISysConfigService; import org.springframework.cache.annotation.CachePut; @@ -41,6 +43,9 @@ public class SysConfigServiceImpl implements ISysConfigService, ConfigService { private final SysConfigMapper baseMapper; + + private final ChatConfigMapper configMapper; + /** * 分页查询参数配置列表 * @@ -212,4 +217,12 @@ public class SysConfigServiceImpl implements ISysConfigService, ConfigService { return SpringUtils.getAopProxy(this).selectConfigByKey(configKey); } + @Override + public String getConfigValue(String category, String configKey) { + ChatConfig config = configMapper.selectOne(new LambdaQueryWrapper() + .eq(ChatConfig::getCategory, category) + .eq(ChatConfig::getConfigName, configKey)); + return ObjectUtils.notNullGetter(config, ChatConfig::getConfigValue, StringUtils.EMPTY); + } + } diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/impl/UserLoginServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/impl/UserLoginServiceImpl.java index 4fa230cb..fb06e69b 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/impl/UserLoginServiceImpl.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/impl/UserLoginServiceImpl.java @@ -7,8 +7,10 @@ import cn.hutool.crypto.digest.BCrypt; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.math.NumberUtils; import org.ruoyi.common.core.constant.Constants; +import org.ruoyi.common.core.constant.SystemConstants; import org.ruoyi.common.core.domain.dto.VisitorLoginUserDto; import org.ruoyi.common.core.enums.UserType; +import org.ruoyi.common.core.exception.ServiceException; import org.ruoyi.common.core.service.UserLoginService; import org.ruoyi.common.core.utils.MessageUtils; import org.ruoyi.common.satoken.utils.LoginHelper; @@ -33,12 +35,19 @@ public class UserLoginServiceImpl implements UserLoginService { public VisitorLoginUserDto mpLogin(String openid, String clientId) { + // 校验客户端 + SysClientVo client = clientService.queryByClientId(clientId); + if (ObjectUtil.isNull(client)) { + throw new ServiceException(MessageUtils.message("auth.grant.type.error")); + } else if (!SystemConstants.NORMAL.equals(client.getStatus())) { + throw new ServiceException(MessageUtils.message("auth.grant.type.blocked")); + } // 使用 openid 查询绑定用户 如未绑定用户 则根据业务自行处理 例如 创建默认用户 SysUserVo user = userService.selectUserByOpenId(openid); VisitorLoginUserDto loginUser = new VisitorLoginUserDto(); if (ObjectUtil.isNull(user)) { SysUserBo sysUser = new SysUserBo(); - String name = "用户" + UUID.randomUUID().toString().replace("-", ""); + String name = "用户" + UUID.randomUUID().toString().replace("-", "").substring(0, 7); // 设置默认用户名 sysUser.setUserName(name); // 设置默认昵称 @@ -47,6 +56,8 @@ public class UserLoginServiceImpl implements UserLoginService { sysUser.setPassword(BCrypt.hashpw("123456")); // 设置微信openId sysUser.setOpenId(openid); + // 设置用户类型 + sysUser.setUserType(UserType.SYS_USER.getUserType()); // 设置默认余额 sysUser.setUserBalance(NumberUtils.toDouble("1")); // 注册用户,设置默认租户为0 @@ -54,16 +65,15 @@ public class UserLoginServiceImpl implements UserLoginService { // 构建登录用户信息 loginUser.setUserId(sysUser.getUserId()); loginUser.setUsername(sysUser.getUserName()); - loginUser.setUserType(UserType.APP_USER.getUserType()); + loginUser.setUserType(UserType.SYS_USER.getUserType()); loginUser.setOpenid(openid); } else { // 此处可根据登录用户的数据不同 自行创建 loginUser loginUser.setUserId(user.getUserId()); loginUser.setUsername(user.getUserName()); - loginUser.setUserType(user.getUserType()); + loginUser.setUserType(UserType.SYS_USER.getUserType()); loginUser.setOpenid(openid); } - SysClientVo client = clientService.queryByClientId(clientId); SaLoginParameter model = new SaLoginParameter(); model.setDeviceType(client.getDeviceType()); // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置 diff --git a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinApiUtil.java b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinApiUtil.java index 97fd19e4..ceef7554 100644 --- a/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinApiUtil.java +++ b/ruoyi-modules/ruoyi-wechat/src/main/java/org/ruoyi/util/WeixinApiUtil.java @@ -74,6 +74,6 @@ public class WeixinApiUtil { } public String getKey(String key) { - return configService.getConfigValue("weixin"); + return configService.getConfigValue("wechat",key); } }