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/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..0086358d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: true 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/enterprise-collaboration.md b/.github/ISSUE_TEMPLATE/enterprise-collaboration.md new file mode 100644 index 00000000..21e8e335 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enterprise-collaboration.md @@ -0,0 +1,57 @@ +--- +name: 企业AI应用合作登记 +about: 登记企业信息与合作意向,获取免费技术支持 +title: '[合作登记] 企业AI应用需求登记' +labels: '合作登记' +assignees: '' +--- + +## 登记说明 + +感谢您关注 RuoYi AI!我们团队今年的重点是帮助企业落地AI应用,如果贵公司符合要求,我们可以提供**免费的技术支持**。 + +请在下方评论中填写登记信息,格式如下: + +--- + +## 登记格式预览 + +| 字段 | 内容 | +|:---|:---| +| 公司名称 | (必填) | +| 公司Logo地址 | (可选) | +| 所属行业 | (必填) | +| 公司所在地 | (必填) | +| 项目名称 | (必填) | +| 项目Logo地址 | (可选) | +| 项目简介 | (必填) | +| 当前AI应用状态 | □ 尚未开始 □ 规划中 □ 已有初步应用 □ 已有成熟应用 | +| 计划落地时间 | □ 1个月内 □ 1-3个月 □ 3-6个月 □ 6个月以上 | +| 当前痛点或挑战 | (可选) | +| 公司简介 | (可选) | + +--- + +## 可复制格式 + +复制下方内容并在评论中填写: + +``` +| 字段 | 内容 | +|:---|:---| +| 公司名称 | | +| 公司Logo地址 | | +| 所属行业 | | +| 公司所在地 | | +| 项目名称 | | +| 项目Logo地址 | | +| 项目简介 | | +| 当前AI应用状态 | | +| 计划落地时间 | | +| 当前痛点或挑战 | | +| 公司简介 | | +``` + +--- + +> **温馨提示**:提交的信息仅用于合作沟通,不会对外公开。 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: '' +--- + +## 建议类型 + +□ 新功能 □ 功能改进 □ 文档完善 □ 其他 + +## 建议描述 + +清晰描述你的建议内容: + +## 使用场景 + +描述这个功能在什么场景下会用到: + +## 期望效果 + +描述你期望的效果: + +## 参考示例 + +如有类似的参考实现或产品,请提供链接: + +## 补充说明 + +其他补充信息: diff --git a/.gitignore b/.gitignore index c23b47ce..e204fe12 100644 --- a/.gitignore +++ b/.gitignore @@ -21,10 +21,13 @@ target/ ### IntelliJ IDEA ### .idea +.claude +.github *.iws *.iml *.ipr + ### JRebel ### rebel.xml @@ -48,3 +51,4 @@ data/ !*/build/*.xml .flattened-pom.xml +/.claude/settings.local.json diff --git a/README.md b/README.md index 90ebdeed..bb8c4859 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流式执行,目前已经支持模型调用,邮件发送,人工审核等节点 | 更多节点类型 | +| **多智能体** | 基于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 流式响应 -- **系统监控**:完善的日志体系、性能监控、服务健康检查 ## 📚 使用文档 @@ -150,6 +137,29 @@ https://docs.qq.com/sheet/DR3hoR3FVVkpJcnVm --- + +## 📺 视频教程 + +
+ + + + + + + +
+微信二维码
+打开抖音扫一扫
+获取免费视频教程 +
+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 16e3dcd4..20e4a54e 100644 --- a/README_EN.md +++ b/README_EN.md @@ -1,3 +1,4 @@ + # RuoYi AI
@@ -19,64 +20,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 @@ -122,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)**
@@ -170,4 +150,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 diff --git a/docs/image/bibi.png b/docs/image/bibi.png new file mode 100644 index 00000000..bafba054 Binary files /dev/null and b/docs/image/bibi.png differ diff --git a/docs/image/dy.png b/docs/image/dy.png new file mode 100644 index 00000000..c303e76a Binary files /dev/null and b/docs/image/dy.png differ diff --git a/docs/script/sql/ruoyi-ai-v3_mysql8.sql b/docs/script/sql/ruoyi-ai-v3_mysql8.sql index 0f957597..87ef9ecc 100644 --- a/docs/script/sql/ruoyi-ai-v3_mysql8.sql +++ b/docs/script/sql/ruoyi-ai-v3_mysql8.sql @@ -105,8 +105,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 @@ -139,11 +139,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 @@ -1414,90 +1414,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 -- ---------------------------- @@ -1510,7 +1426,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 @@ -1535,7 +1451,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 @@ -1581,7 +1497,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 @@ -1603,7 +1519,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 @@ -1629,7 +1545,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 @@ -1658,7 +1574,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 @@ -1692,7 +1608,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 @@ -1724,7 +1640,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 @@ -1745,7 +1661,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 @@ -1774,7 +1690,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 @@ -1795,7 +1711,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 @@ -1834,7 +1750,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 @@ -1863,7 +1779,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 @@ -1898,7 +1814,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 @@ -1923,7 +1839,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 @@ -1951,7 +1867,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 @@ -1974,7 +1890,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 @@ -2001,7 +1917,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 @@ -2020,7 +1936,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 @@ -2040,7 +1956,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 @@ -2075,7 +1991,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 @@ -2106,7 +2022,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 @@ -2135,7 +2051,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 @@ -2338,11 +2254,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 @@ -2386,8 +2297,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 @@ -2626,15 +2535,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 @@ -2670,7 +2570,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, '菜单管理菜单'); @@ -2774,22 +2674,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, ''); @@ -2814,6 +2698,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, ''); @@ -2846,6 +2737,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', ''); @@ -2934,12 +2826,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 @@ -2972,10 +2858,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); -- ---------------------------- @@ -3422,7 +3308,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); @@ -3501,7 +3387,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 @@ -3509,9 +3395,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 @@ -3731,25 +3614,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..8771fb18 100644 --- a/pom.xml +++ b/pom.xml @@ -50,16 +50,17 @@ 1.8.2 - 4.4.0 + 4.6.0 - 2.20.1 + 2.18.2 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 @@ -410,6 +411,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..49750f04 100644 --- a/ruoyi-admin/pom.xml +++ b/ruoyi-admin/pom.xml @@ -110,6 +110,7 @@ ruoyi-aiflow + de.codecentric spring-boot-admin-starter-client 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-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 2a17ad30..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: # 是否开启 @@ -216,6 +218,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: @@ -307,6 +311,7 @@ wechat: secret: '' token: '' aesKey: '' + --- # Neo4j 知识图谱配置 neo4j: uri: bolt://117.72.192.162:7687 @@ -357,3 +362,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-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-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/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 35a3535f..f0a95fe5 100644 --- a/ruoyi-modules/ruoyi-chat/pom.xml +++ b/ruoyi-modules/ruoyi-chat/pom.xml @@ -51,15 +51,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/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 1f5d0c6a..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,42 +5,38 @@ import dev.langchain4j.service.SystemMessage; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.V; -public interface McpAgent { +public interface McpAgent extends Agent { + /** - * 系统提示词:定义智能体身份、核心职责、强制遵守的规则 - * 适配SSE流式特性,明确工具全来自远端MCP服务,仅做代理调用和结果整理 + * 系统提示词:通用工具调用智能体 + * 不限定具体工具类型,让 LangChain4j 自动传递工具描述给 LLM */ @SystemMessage(""" - 你是专业的MCP服务工具代理智能体,核心能力是通过HTTP SSE流式传输协议,调用本地http://localhost:8085/sse地址上MCP服务端注册的所有工具。 - 你的核心工作职责: - 1. 准确理解用户的自然语言请求,判断需要调用MCP服务端的哪一个/哪些工具; - 2. 通过绑定的工具提供者,向MCP服务端发起工具调用请求,传递完整的工具执行参数; - 3. 实时接收MCP服务端通过SSE流式返回的工具执行结果,保证结果片段的完整性; - 4. 将流式结果按原始顺序整理为清晰、易懂的自然语言答案,返回给用户。 + 你是一个AI助手,可以通过调用各种工具来帮助用户完成不同的任务。 - 【强制遵守的核心规则 - 无例外】 - 1. 所有工具调用必须通过远端MCP服务执行,严禁尝试本地执行任何业务逻辑; - 2. 处理SSE流式结果时,严格保留结果片段的返回顺序,不得打乱或遗漏; - 3. 若MCP服务返回错误(如工具未找到、参数错误、执行失败),直接将错误信息友好反馈给用户,无需额外推理; - 4. 工具执行结果若为结构化数据(如JSON、表格),需格式化后返回,提升可读性。 + 【工具使用规则】 + 1. 根据用户的请求,判断需要使用哪些工具 + 2. 仔细阅读每个工具的描述,确保理解工具的功能和参数要求 + 3. 使用正确的参数调用工具 + 4. 如果工具执行失败,向用户友好地说明错误原因,并尝试提供替代方案 + 5. 对于复杂任务,可以分步骤使用多个工具完成 + 6. 将工具执行结果以清晰易懂的方式呈现给用户 + + 【响应格式】 + - 直接回答用户的问题 + - 如果使用了工具,说明使用了什么工具以及结果 + - 如果遇到错误,提供友好的错误信息和解决建议 """) - /** - * 用户消息模板:{{query}}为参数占位符,与方法入参的@V("query")绑定 - */ @UserMessage(""" - 请通过调用MCP服务端的工具,处理用户的以下请求: {{query}} """) + + @Agent("通用工具调用智能体") /** - * 智能体标识:用于日志打印、监控追踪、多智能体协作时的身份识别 - */ - @Agent("MCP服务SSE流式代理智能体-连接本地8085端口") - /** - * 智能体对外调用入口方法 - * @param query 用户的自然语言请求(如:生成订单数据柱状图、查询今日天气) - * @V("query") 将方法入参值绑定到@UserMessage的{{query}}占位符中 - * @return 整理后的MCP工具执行结果(流式结果会自动拼接为完整字符串) + * 智能体对外调用入口 + * @param query 用户的自然语言请求 + * @return 处理结果 */ String callMcpTool(@V("query") String query); } diff --git a/ruoyi-modules/ruoyi-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-chat/src/main/java/org/ruoyi/config/mcp/SystemToolInitializer.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/config/mcp/SystemToolInitializer.java new file mode 100644 index 00000000..8426db52 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/config/mcp/SystemToolInitializer.java @@ -0,0 +1,93 @@ +package org.ruoyi.config.mcp; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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; +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-chat/src/main/java/org/ruoyi/controller/mcp/McpMarketController.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/mcp/McpMarketController.java new file mode 100644 index 00000000..3f840425 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/mcp/McpMarketController.java @@ -0,0 +1,172 @@ +package org.ruoyi.controller.mcp; + +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.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.*; + +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-chat/src/main/java/org/ruoyi/controller/mcp/McpToolController.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/mcp/McpToolController.java new file mode 100644 index 00000000..96d51298 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/mcp/McpToolController.java @@ -0,0 +1,136 @@ +package org.ruoyi.controller.mcp; + +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.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.*; + +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-chat/src/main/java/org/ruoyi/domain/bo/mcp/McpMarketBo.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/bo/mcp/McpMarketBo.java new file mode 100644 index 00000000..e0306c0d --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/bo/mcp/McpMarketBo.java @@ -0,0 +1,55 @@ +package org.ruoyi.domain.bo.mcp; + +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.domain.entity.mcp.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-chat/src/main/java/org/ruoyi/domain/bo/mcp/McpToolBo.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/bo/mcp/McpToolBo.java new file mode 100644 index 00000000..74d699e6 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/bo/mcp/McpToolBo.java @@ -0,0 +1,59 @@ +package org.ruoyi.domain.bo.mcp; + +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.domain.entity.mcp.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-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpMarketListResult.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpMarketListResult.java new file mode 100644 index 00000000..eba96d0a --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpMarketListResult.java @@ -0,0 +1,44 @@ +package org.ruoyi.domain.dto.mcp; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.ruoyi.domain.entity.mcp.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-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpMarketRefreshResult.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpMarketRefreshResult.java new file mode 100644 index 00000000..e9c1002d --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpMarketRefreshResult.java @@ -0,0 +1,38 @@ +package org.ruoyi.domain.dto.mcp; + +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-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpMarketToolListResult.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpMarketToolListResult.java new file mode 100644 index 00000000..a66cfd9a --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpMarketToolListResult.java @@ -0,0 +1,63 @@ +package org.ruoyi.domain.dto.mcp; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.ruoyi.domain.entity.mcp.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-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpToolListResult.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpToolListResult.java new file mode 100644 index 00000000..385edcae --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpToolListResult.java @@ -0,0 +1,44 @@ +package org.ruoyi.domain.dto.mcp; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.ruoyi.domain.entity.mcp.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-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpToolTestResult.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpToolTestResult.java new file mode 100644 index 00000000..71b873da --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/mcp/McpToolTestResult.java @@ -0,0 +1,56 @@ +package org.ruoyi.domain.dto.mcp; + +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-chat/src/main/java/org/ruoyi/domain/entity/mcp/McpMarket.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/mcp/McpMarket.java new file mode 100644 index 00000000..1f805fe2 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/mcp/McpMarket.java @@ -0,0 +1,51 @@ +package org.ruoyi.domain.entity.mcp; + +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-chat/src/main/java/org/ruoyi/domain/entity/mcp/McpMarketTool.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/mcp/McpMarketTool.java new file mode 100644 index 00000000..ac7e9c02 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/mcp/McpMarketTool.java @@ -0,0 +1,61 @@ +package org.ruoyi.domain.entity.mcp; + +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-chat/src/main/java/org/ruoyi/domain/entity/mcp/McpTool.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/mcp/McpTool.java new file mode 100644 index 00000000..8ce47880 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/mcp/McpTool.java @@ -0,0 +1,55 @@ +package org.ruoyi.domain.entity.mcp; + +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-chat/src/main/java/org/ruoyi/domain/vo/mcp/McpMarketVo.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/mcp/McpMarketVo.java new file mode 100644 index 00000000..0abbea71 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/mcp/McpMarketVo.java @@ -0,0 +1,74 @@ +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.domain.entity.mcp.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-chat/src/main/java/org/ruoyi/domain/vo/mcp/McpToolVo.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/mcp/McpToolVo.java new file mode 100644 index 00000000..a5fbd748 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/mcp/McpToolVo.java @@ -0,0 +1,70 @@ +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.domain.entity.mcp.McpTool; + +import java.io.Serializable; +import java.util.Date; + +/** + * MCP 工具视图对象 + * + * @author ruoyi team + */ +@Data +@ExcelIgnoreUnannotated +@AutoMapper(target = McpTool.class) +public class McpToolVo implements Serializable { + + /** + * 工具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-chat/src/main/java/org/ruoyi/enums/McpToolStatus.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/McpToolStatus.java new file mode 100644 index 00000000..7facd02a --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/McpToolStatus.java @@ -0,0 +1,47 @@ +package org.ruoyi.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-chat/src/main/java/org/ruoyi/mapper/mcp/McpMarketMapper.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/mcp/McpMarketMapper.java new file mode 100644 index 00000000..6864f2c4 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/mcp/McpMarketMapper.java @@ -0,0 +1,15 @@ +package org.ruoyi.mapper.mcp; + +import org.apache.ibatis.annotations.Mapper; +import org.ruoyi.common.mybatis.core.mapper.BaseMapperPlus; +import org.ruoyi.domain.entity.mcp.McpMarket; +import org.ruoyi.domain.vo.mcp.McpMarketVo; + +/** + * MCP 市场信息 Mapper + * + * @author ruoyi team + */ +@Mapper +public interface McpMarketMapper extends BaseMapperPlus { +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/mcp/McpMarketToolMapper.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/mcp/McpMarketToolMapper.java new file mode 100644 index 00000000..4580f33c --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/mcp/McpMarketToolMapper.java @@ -0,0 +1,14 @@ +package org.ruoyi.mapper.mcp; + +import org.apache.ibatis.annotations.Mapper; +import org.ruoyi.common.mybatis.core.mapper.BaseMapperPlus; +import org.ruoyi.domain.entity.mcp.McpMarketTool; + +/** + * MCP 市场工具关联 Mapper + * + * @author ruoyi team + */ +@Mapper +public interface McpMarketToolMapper extends BaseMapperPlus { +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/mcp/McpToolMapper.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/mcp/McpToolMapper.java new file mode 100644 index 00000000..a3ce4d33 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/mcp/McpToolMapper.java @@ -0,0 +1,15 @@ +package org.ruoyi.mapper.mcp; + +import org.apache.ibatis.annotations.Mapper; +import org.ruoyi.common.mybatis.core.mapper.BaseMapperPlus; +import org.ruoyi.domain.entity.mcp.McpTool; +import org.ruoyi.domain.vo.mcp.McpToolVo; + +/** + * MCP 工具信息 Mapper + * + * @author ruoyi team + */ +@Mapper +public interface McpToolMapper extends BaseMapperPlus { +} diff --git a/ruoyi-modules/ruoyi-chat/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 new file mode 100644 index 00000000..96890034 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/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-chat/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 new file mode 100644 index 00000000..d72922b0 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/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-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-chat/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 new file mode 100644 index 00000000..1c09793e --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/core/LangChain4jMcpToolProviderService.java @@ -0,0 +1,474 @@ +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; +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.domain.entity.mcp.McpTool; +import org.ruoyi.enums.McpToolStatus; +import org.ruoyi.mapper.mcp.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 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); + + // 检查命令是否可用 + 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); + 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(); + } + + /** + * 检查命令是否在系统上可用 + */ + 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 + */ + 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-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-chat/src/main/java/org/ruoyi/mcp/tools/EditFileTool.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/tools/EditFileTool.java new file mode 100644 index 00000000..26473da2 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/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-chat/src/main/java/org/ruoyi/mcp/tools/ListDirectoryTool.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/tools/ListDirectoryTool.java new file mode 100644 index 00000000..8f6c0cdd --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/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-chat/src/main/java/org/ruoyi/mcp/tools/ReadFileTool.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/tools/ReadFileTool.java new file mode 100644 index 00000000..7b4886e7 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/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; + } +} 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 e962e2f7..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,36 +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.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.enums.ChatModeType; +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; /** * qianWenAI服务调用 @@ -42,23 +27,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(); - @Override protected StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo,ChatRequest chatRequest) { return QwenStreamingChatModel.builder() @@ -108,94 +79,6 @@ 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服务(智能体) - * @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); - } - - // 获取或初始化MCP客户端 - McpClient mcpClient = mcpClientCache.computeIfAbsent(cacheKey, k -> initializeMcpClient()); - - // 步骤3:将mcp对象包装 - ToolProvider toolProvider = toolProviderCache.computeIfAbsent(cacheKey, k -> McpToolProvider.builder() - .mcpClients(List.of(mcpClient)) - .build()); - - // 步骤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(); - - // 步骤6:将所有MCP对象由超级智能体管控 - SupervisorAgent supervisor = AgenticServices - .supervisorBuilder() - .chatModel(qwenChatModel) - .subAgents(mcpAgent) - .responseStrategy(SupervisorResponseStrategy.LAST) - .build(); - - // 缓存监督智能体 - supervisorCache.put(cacheKey, supervisor); - - // 步骤7:调用大模型LLM - return supervisor.invoke(userMessage); - } finally { - cacheLock.unlock(); - } - } - @Override public String getProviderName() { return ChatModeType.QIAN_WEN.getCode(); diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/mcp/IMcpMarketService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/mcp/IMcpMarketService.java new file mode 100644 index 00000000..e359100b --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/mcp/IMcpMarketService.java @@ -0,0 +1,117 @@ +package org.ruoyi.service.mcp; + +import org.ruoyi.common.mybatis.core.page.PageQuery; +import org.ruoyi.common.mybatis.core.page.TableDataInfo; +import org.ruoyi.domain.bo.mcp.McpMarketBo; +import org.ruoyi.domain.dto.mcp.McpMarketListResult; +import org.ruoyi.domain.dto.mcp.McpMarketRefreshResult; +import org.ruoyi.domain.dto.mcp.McpMarketToolListResult; +import org.ruoyi.domain.vo.mcp.McpMarketVo; + +import java.util.List; + +/** + * MCP 市场服务接口 + * + * @author ruoyi team + */ +public interface IMcpMarketService { + + /** + * 分页查询市场列表 + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 市场分页列表 + */ + TableDataInfo 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-chat/src/main/java/org/ruoyi/service/mcp/IMcpToolService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/mcp/IMcpToolService.java new file mode 100644 index 00000000..d61ef823 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/mcp/IMcpToolService.java @@ -0,0 +1,92 @@ +package org.ruoyi.service.mcp; + +import org.ruoyi.common.mybatis.core.page.PageQuery; +import org.ruoyi.common.mybatis.core.page.TableDataInfo; +import org.ruoyi.domain.bo.mcp.McpToolBo; +import org.ruoyi.domain.dto.mcp.McpToolListResult; +import org.ruoyi.domain.dto.mcp.McpToolTestResult; +import org.ruoyi.domain.vo.mcp.McpToolVo; + +import java.util.List; + +/** + * MCP 工具服务接口 + * + * @author ruoyi team + */ +public interface IMcpToolService { + + /** + * 分页查询工具列表 + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 工具分页列表 + */ + TableDataInfo 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-chat/src/main/java/org/ruoyi/service/mcp/impl/McpMarketServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/mcp/impl/McpMarketServiceImpl.java new file mode 100644 index 00000000..46eaad7b --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/mcp/impl/McpMarketServiceImpl.java @@ -0,0 +1,328 @@ +package org.ruoyi.service.mcp.impl; + +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.ruoyi.common.core.exception.ServiceException; +import org.ruoyi.common.core.utils.MapstructUtils; +import org.ruoyi.common.mybatis.core.page.PageQuery; +import org.ruoyi.common.mybatis.core.page.TableDataInfo; +import org.ruoyi.domain.bo.mcp.McpMarketBo; +import org.ruoyi.domain.dto.mcp.McpMarketListResult; +import org.ruoyi.domain.dto.mcp.McpMarketRefreshResult; +import org.ruoyi.domain.dto.mcp.McpMarketToolListResult; +import org.ruoyi.domain.entity.mcp.McpMarket; +import org.ruoyi.domain.entity.mcp.McpMarketTool; +import org.ruoyi.domain.entity.mcp.McpTool; +import org.ruoyi.domain.vo.mcp.McpMarketVo; +import org.ruoyi.enums.McpToolStatus; +import org.ruoyi.mapper.mcp.McpMarketMapper; +import org.ruoyi.mapper.mcp.McpMarketToolMapper; +import org.ruoyi.mapper.mcp.McpToolMapper; +import org.ruoyi.service.mcp.IMcpMarketService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * MCP 市场服务实现 + * + * @author ruoyi team + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class McpMarketServiceImpl implements IMcpMarketService { + + private final McpMarketMapper baseMapper; + private final McpMarketToolMapper mcpMarketToolMapper; + private final McpToolMapper mcpToolMapper; + private final ObjectMapper objectMapper; + + @Override + public TableDataInfo 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-chat/src/main/java/org/ruoyi/service/mcp/impl/McpToolServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/mcp/impl/McpToolServiceImpl.java new file mode 100644 index 00000000..ce7c684d --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/mcp/impl/McpToolServiceImpl.java @@ -0,0 +1,226 @@ +package org.ruoyi.service.mcp.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.ruoyi.common.core.exception.ServiceException; +import org.ruoyi.common.core.utils.MapstructUtils; +import org.ruoyi.common.mybatis.core.page.PageQuery; +import org.ruoyi.common.mybatis.core.page.TableDataInfo; +import org.ruoyi.domain.bo.mcp.McpToolBo; +import org.ruoyi.domain.dto.mcp.McpToolListResult; +import org.ruoyi.domain.dto.mcp.McpToolTestResult; +import org.ruoyi.domain.entity.mcp.McpTool; +import org.ruoyi.domain.vo.mcp.McpToolVo; +import org.ruoyi.enums.McpToolStatus; +import org.ruoyi.mapper.mcp.McpToolMapper; +import org.ruoyi.service.mcp.IMcpToolService; +import org.ruoyi.mcp.service.core.BuiltinToolRegistry; +import org.ruoyi.mcp.service.core.LangChain4jMcpToolProviderService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.List; +import java.util.Map; + +/** + * MCP 工具服务实现 + * + * @author ruoyi team + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class McpToolServiceImpl implements IMcpToolService { + + private final McpToolMapper baseMapper; + private final LangChain4jMcpToolProviderService langChain4jMcpToolProviderService; + private final BuiltinToolRegistry builtinToolRegistry; + + @Override + public TableDataInfo 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-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); } }