41 Commits

Author SHA1 Message Date
ageerle@163.com
06d110ed16 @docs: 修正 snailjob 注释里失效的 ry_job.sql 引用
实际建表脚本在 docs/script/sql/ruoyi-ai-v3_mysql8.sql(sj_ 前缀表),仓库中并无 ry_job.sql。
关联 issue #311。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-15 18:25:55 +08:00
ageerle@163.com
dfcadda2bb @style: 修正 QdrantVectorStoreStrategy 缩进为空格
createSchema 调用与文件其余代码保持 4 空格缩进统一,避免 checkstyle/spotless 报错。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-15 18:00:07 +08:00
Jack Jiang
3c9546ded9 fix: 修复 Qdrant 初始化创建集合问题 (#298) 2026-07-15 17:58:12 +08:00
Octopus
a3d82e092e Refresh MiniMax models and protocol support (#313)
Co-authored-by: octo-patch <266937838+octo-patch@users.noreply.github.com>
2026-07-15 17:43:51 +08:00
ageerle@163.com
96a1b741df fix: 兼容 Atlas 预测结果查询 404,避免前端轮询刷屏
Atlas /model/prediction/{id} 对已过期或不存在的任务返回 404
(常见于更换 API Key 后残留的旧任务)。原实现直接抛异常,前端
轮询反复触发错误提示。改为 404 时返回 status=failed 的响应,
让上层走正常失败分支落库,前端识别后终止轮询。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-15 11:17:20 +08:00
ageerle@163.com
e7b7d1d084 更新README.md
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-15 10:52:15 +08:00
ageerle@163.com
efc30c3ac8 docs: README 新增短剧服务 ruoyi-drama 源码链接
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-14 22:30:10 +08:00
ageerle@163.com
2ae13aafd4 feat: 新增短剧/媒体生成能力并脱敏数据库脚本
- 新增音频/视频/图片生成服务与 Atlas、OpenAI 实现
- 新增短剧脚本生成、分镜与 ffmpeg 合成模块
- 新增 Coze/Dify 聊天 provider 及 Agent 配置
- 脱敏 SQL 脚本:移除真实 DeepSeek API Key,掩码 COS 桶名
- 清理过期的 SQL update 脚本与 chat 文档

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-14 21:49:03 +08:00
ageerle@163.com
7aa67cfc2f docs: 移除赞助商区块引用样式及多余换行
- 去掉 blockquote 引用块样式消除左侧竖线
- 去掉多余 <br> 避免链接文字断行

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 18:19:04 +08:00
ageerle@163.com
6d38e2bf25 docs: 赞助商图片改用本地路径并新增火山引擎 CodingPlan
- Atlas Cloud 图片从远程链接改为 docs/image/sponsor/ 本地路径
- 新增火山引擎 CodingPlan 开发者计划赞助商(中英文 README 同步)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 18:14:46 +08:00
ageerle@163.com
8ed5e61f63 docs: 添加微信技术交流群二维码
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 10:25:18 +08:00
ageerle@163.com
c3c89f8cc8 docs: 调整中文版赞助商图标大小
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 11:44:42 +08:00
ageerle@163.com
a3a6bdbc55 docs: 调整英文版赞助商图标大小与中文版一致
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 11:43:37 +08:00
ageerle@163.com
07d9fc1f45 docs: 同步英文版 README 与中文版保持一致
- 移除英文版 Quick Start/Live Demo 章节
- 移除 Ecosystem Partners 章节,合并到 Sponsors
- 更新赞助商格式为图片横幅样式
- 社区交流改为微信/QQ 二维码(替换 Telegram)
- 移除 Acknowledgments 中的 Spring AI Alibaba Copilot
- 修复 Technical Architecture 多余空行
- 更新文档链接域名(pandarobot -> ruoyiai)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 11:38:48 +08:00
ageerle@163.com
73a60e3591 docs: 更新 Atlas Cloud 赞助链接添加推荐参数
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 14:37:14 +08:00
ageerle@163.com
c79848c654 feat: 替换 PPIO 为 Atlas Cloud 并增强知识库功能
- 将 PPIO 服务提供商替换为 Atlas Cloud
- 更新 README 赞助商信息
- 删除 PPIOEmbeddingProvider 和相关测试
- 知识库新增重排序、混合检索、相似度阈值等字段
- 更新 SQL 初始化脚本

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 11:18:59 +08:00
ageerle@163.com
ec092a11c3 docs: 添加 web coding 快速部署说明
在 README 中新增使用 web coding 急速部署的章节,包含在线体验链接和部署截图展示

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 17:45:16 +08:00
ageerle
9a7b727413 Merge pull request #293 from RobustH/main
升级RAG模块
2026-04-23 09:15:02 +08:00
RobustH
b8d16b7669 feat(rag): 对接用户端用户知识库对话,集成知识库配置应用 2026-04-23 00:52:53 +08:00
RobustH
058a4aee2a feat(rag): 新增测试配置应用的功能 2026-04-21 22:54:11 +08:00
RobustH
1b50c7f9f1 fix(rag): 修复合并重复,重排模型新增硅基流动供应商 2026-04-21 22:41:00 +08:00
RobustH
e7f53fd55f Merge remote-tracking branch 'origin/main'
# Conflicts:
#	ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/bo/knowledge/KnowledgeInfoBo.java
#	ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/knowledge/KnowledgeInfo.java
#	ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/knowledge/KnowledgeInfoVo.java
#	ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatServiceFacade.java
2026-04-21 21:41:51 +08:00
ageerle
07bdc5e585 Merge pull request #292 from yangzhen233/feature/rerank-model
Feature/rerank model
2026-04-20 21:06:27 +08:00
yangzhen
e1b8a5f011 新增千问3重排序模型,并附带新增sql文件 2026-04-20 16:07:02 +08:00
杨振
80ca76ea37 添加重排序功能 2026-04-20 01:02:09 +08:00
wangle
2c6ff66830 fix: 修正application.yml演示模式message缩进
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 13:42:39 +08:00
wangle
4f79a66559 feat: 添加小米MiMo、DeepSeek、自定义厂商等provider支持
- 新增小米MiMo服务实现类(MiMoServiceImpl)
- ChatModeType添加XIAOMI枚举
- 更新SQL初始化脚本,新增多家厂商(provider)和模型数据
- 添加2026-04-19数据库更新脚本
- application.yml演示模式排除路径增加attach/fragment/info接口
- 删除独立的minimax_provider.sql(数据已合并到主SQL)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 13:42:05 +08:00
wangle
22883b4334 Merge branch pr-280: 添加MiniMax作为LLM提供商
解决冲突:
- README: 保留Qdrant向量库信息 + 合并MiniMax模型接入
- pom.xml: 保留spring-boot-starter-test + 添加MiniMax测试依赖
- ChatModeType: 保留CUSTOM_API + 新增MINIMAX枚举
- MinimaxServiceImpl: 保留MyChatModelListener监听器

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 11:47:04 +08:00
wangle
081da6d18d feat: 添加MiniMax作为LLM提供商,合并PR#280并补充监听
合并PR#280的MiniMax provider实现,解决与main分支的冲突,
并在MinimaxServiceImpl中补充MyChatModelListener监听,
与其他provider保持一致。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-17 18:31:53 +08:00
ageerle
74eb5b2530 Merge pull request #290 from yangzhen233/feature/zhipu-embedding
添加智谱向量模型实现
2026-04-17 18:24:24 +08:00
ageerle
b0328fe0ef Merge pull request #291 from xiaonieli7/main
fate:增加自定义模型,调整前端模型选择下拉框
2026-04-17 18:16:59 +08:00
Administrator
2ee0aae57e fate:增加自定义模型,调整前端模型选择下拉框 2026-04-17 08:31:40 +08:00
杨振
d9c3de660a 添加智谱向量模型实现 2026-04-16 21:18:11 +08:00
RobustH
ccbf5c9520 feat(rag): 知识库检索测试新增混合检索 2026-04-14 23:18:29 +08:00
ageerle
c4f7c1f5d0 Merge pull request #283 from MrWws/fix/docker-compose-all.yaml
fix: 修复运行docker-compose-all.yaml报错的问题
2026-04-14 14:36:14 +08:00
RobustH
1208c46cca feat(rag): 集成硅基流动、阿里百炼重排模型并全方位增强检索测试体验 2026-04-14 01:40:28 +08:00
RobustH
06a63c377e feat: 新增检索测试相关接口
- 实现向量 L2 归一化,统一 Milvus/Qdrant/Weaviate 检索评分为 [0, 1] 空间
2026-04-13 23:33:56 +08:00
RobustH
0fa25032a3 feat(knowledge): 优化知识库文件状态枚举为"未解析,解析中,解析成功,解析失败",支持异步线程池解析文档 2026-04-13 00:15:01 +08:00
RobustH
28ad29d6ed feat(knowledge): 完善知识库及附件统计功能并修复分块数统计问题 2026-04-12 18:38:32 +08:00
MrWws
b9097b4989 fix: 修复运行docker-compose-all.yaml报错的问题 2026-03-31 22:59:26 +08:00
octopus
5d14eb20af feat: add MiniMax as first-class LLM provider
Add MiniMax AI as the 7th LLM provider, supporting chat (M2.7, M2.5,
M2.5-highspeed) and embedding (embo-01) models via OpenAI-compatible API.

Changes:
- Add MINIMAX enum to ChatModeType
- Add MinimaxServiceImpl chat provider (OpenAI-compat streaming)
- Add MinimaxEmbeddingProvider for vector embeddings
- Add SQL migration for provider and model registration
- Add 14 unit tests + 3 integration tests
- Update README/README_EN with MiniMax in provider list
2026-03-21 16:14:19 +08:00
203 changed files with 15376 additions and 1790 deletions

View File

@@ -21,8 +21,8 @@
*开箱即用的全栈AI平台支持多智能体协同、Supervisor模式编排、多种决策模式、RAG技术和流程编排能力* *开箱即用的全栈AI平台支持多智能体协同、Supervisor模式编排、多种决策模式、RAG技术和流程编排能力*
**[English](README_EN.md)** | **[📖 使用文档](https://doc.pandarobot.chat)** | **[English](README_EN.md)** | **[📖 使用文档](https://doc.ruoyiai.chat/)** |
**[🚀 在线体验](https://web.pandarobot.chat)** | **[🐛 问题反馈](https://github.com/ageerle/ruoyi-ai/issues)** | **[💡 功能建议](https://github.com/ageerle/ruoyi-ai/issues)** **[🚀 在线体验](https://web.ruoyiai.chat/)** | **[🐛 问题反馈](https://github.com/ageerle/ruoyi-ai/issues)** | **[💡 功能建议](https://github.com/ageerle/ruoyi-ai/issues)**
</div> </div>
@@ -30,21 +30,13 @@
## ✨ 核心亮点 ## ✨ 核心亮点
| 模块 | 现有能力 | 模块 | 现有能力
|:----------:|--- |:---------:|---
| **模型管理** | 多模型接入(OpenAI/DeepSeek/通义/智谱)、多模态理解、Coze/DIFY/FastGPT平台集成 | **模型管理** | 多模型接入(DeepSeek/智谱/MIMO/百炼/OpenAI)、多模态理解、Coze/DIFY/FastGPT/RAGFlow平台集成
| **知识管理** | 本地RAG + 向量库(Milvus/Weaviate/Qdrant) + 文档解析 | **知识管理** | 本地RAG + 向量库(Milvus/Weaviate/Qdrant) + 文档解析
| **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态 | **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态
| **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点 | **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点
| **智能体** | 基于Langchain4j的Agent框架、Supervisor模式编排,支持多种决策模型 | **智能体管理** | 基于Langchain4j的Agent框架、Supervisor模式编排,支持多种决策模型,可以灵活搭配工具,skills
## 🚀 快速体验
### 在线演示
| 平台 | 地址 | 账号 |
|:------:|---|---|
| 用户端 | [web.pandarobot.chat](https://web.pandarobot.chat) | admin / admin123 |
| 管理后台 | [admin.pandarobot.chat](https://admin.pandarobot.chat) | admin / admin123 |
### 项目源码 ### 项目源码
@@ -53,6 +45,7 @@
| 🔧 后端服务 | [ruoyi-ai](https://github.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitee.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitcode.com/ageerle/ruoyi-ai) | | 🔧 后端服务 | [ruoyi-ai](https://github.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitee.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitcode.com/ageerle/ruoyi-ai) |
| 🎨 用户前端 | [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-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) | | 🛠️ 管理后台 | [ruoyi-admin](https://github.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitee.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitcode.com/ageerle/ruoyi-admin) |
| 🎬 短剧平台 | [ruoyi-drama](https://github.com/ageerle/ruoyi-drama) | [ruoyi-drama](https://gitee.com/ageerle/ruoyi-drama) | [ruoyi-drama](https://gitcode.com/ageerle/ruoyi-drama) |
### 合作项目 ### 合作项目
| 项目名称 | GitHub 仓库 | Gitee 仓库 | 项目名称 | GitHub 仓库 | Gitee 仓库
@@ -66,8 +59,6 @@
- **数据存储**MySQL 8.0 + Redis + 向量数据库Milvus/Weaviate/Qdrant - **数据存储**MySQL 8.0 + Redis + 向量数据库Milvus/Weaviate/Qdrant
- **前端技术**Vue 3 + Vben Admin + element-plus-x - **前端技术**Vue 3 + Vben Admin + element-plus-x
- **安全认证**Sa-Token + JWT 双重保障 - **安全认证**Sa-Token + JWT 双重保障
- **文档处理**PDF、Word、Excel 解析,图像智能分析 - **文档处理**PDF、Word、Excel 解析,图像智能分析
- **实时通信**WebSocket 实时通信SSE 流式响应 - **实时通信**WebSocket 实时通信SSE 流式响应
- **系统监控**:完善的日志体系、性能监控、服务健康检查 - **系统监控**:完善的日志体系、性能监控、服务健康检查
@@ -187,7 +178,7 @@ docker-compose -f docker-compose-all.yaml restart [服务名]
想要深入了解安装部署、功能配置和二次开发? 想要深入了解安装部署、功能配置和二次开发?
**👉 [完整使用文档](https://doc.pandarobot.chat)** **👉 [完整使用文档](https://doc.ruoyiai.chat/)**
## 🤝 参与贡献 ## 🤝 参与贡献
@@ -210,17 +201,29 @@ docker-compose -f docker-compose-all.yaml restart [服务名]
## 🙏 特别鸣谢 ## 🙏 特别鸣谢
感谢以下优秀的开源项目为本项目提供支持: 感谢以下优秀的开源项目为本项目提供支持:
- [Spring AI Alibaba Copilot](https://github.com/spring-ai-alibaba/copilot) - 基于spring-ai-alibaba
的智能编码助手
- [Langchain4j](https://github.com/langchain4j/langchain4j) - 强大的 Java LLM 开发框架 - [Langchain4j](https://github.com/langchain4j/langchain4j) - 强大的 Java LLM 开发框架
- [RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus) - 成熟的企业级快速开发框架 - [RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus) - 成熟的企业级快速开发框架
- [Vben Admin](https://github.com/vbenjs/vue-vben-admin) - 现代化的 Vue 后台管理模板 - [Vben Admin](https://github.com/vbenjs/vue-vben-admin) - 现代化的 Vue 后台管理模板
## 🌐 生态伙伴
- [PPIO 派欧云](https://ppinfra.com/user/register?invited_by=P8QTUY&utm_source=github_ruoyi-ai) - 提供高性价比的 GPU ## 💎 赞助商
算力和模型 API 服务
- [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_ruoyi) - 万卡RTX40系GPU+海内外主流模型API服务秒级响应按量计费新客免费用。 **感谢以下赞助商对本项目的支持:**
<a href="https://www.atlascloud.ai?ref=89F97E">
<img src="docs/image/sponsor/atlascloud_banner.png" alt="Atlas Cloud" width="160" height="80">
</a>
[访问Atlas Cloud官网](https://www.atlascloud.ai?ref=89F97E&utm_source=github&utm_campaign=ruoyi-drama) · [编程计划优惠](https://www.atlascloud.ai/console/coding-plan)
全模态 AI 推理平台,为开发者提供统一的 AI API支持视频生成、图像生成和大语言模型。一次接入即可访问 **300+ 精选模型**
<a href="https://www.volcengine.com/activity/codingplan?utm_campaign=hw&utm_content=hw&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ageerle-ruoyi-ai">
<img src="docs/image/sponsor/huoshan.png" alt="火山引擎 CodingPlan" width="160" height="80">
</a>
[注册即领2500万Tokens立即前往](https://www.volcengine.com/activity/ai618?utm_campaign=hw&utm_content=hw&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ageerle-ruoyi-ai)
享字节自研豆包模型+满血版开源 SOTA模型覆盖文本、VLM、图像生成全模态一站配齐Seed-2.1、Seedream-5.0、GLM-5.2、DeepSeek等。不止编程、更能解决 Agent 复杂长程任务!
## 💬 社区交流 ## 💬 社区交流
@@ -234,6 +237,11 @@ docker-compose -f docker-compose-all.yaml restart [服务名]
<em>邀请进群学习</em> <em>邀请进群学习</em>
</td> </td>
<td align="center"> <td align="center">
<img src="docs/image/wx06.png" alt="微信二维码" width="200" height="200"><br>
<strong>微信技术交流群</strong><br>
<em>技术讨论</em>
</td>
<td align="center">
<img src="docs/image/qq.png" alt="QQ群二维码" width="200" height="200"><br> <img src="docs/image/qq.png" alt="QQ群二维码" width="200" height="200"><br>
<strong>QQ技术交流群</strong><br> <strong>QQ技术交流群</strong><br>
<em>技术讨论</em> <em>技术讨论</em>
@@ -247,7 +255,7 @@ docker-compose -f docker-compose-all.yaml restart [服务名]
--- ---
<div align="center"> <div align="center">
**[⭐ 点个Star支持一下](https://github.com/ageerle/ruoyi-ai)** • **[ Fork 开始贡献](https://github.com/ageerle/ruoyi-ai/fork)** • **[📚 English](README_EN.md)** • **[📖 查看完整文档](https://doc.pandarobot.chat)** **[⭐ 点个Star支持一下](https://github.com/ageerle/ruoyi-ai)** • **[ Fork 开始贡献](https://github.com/ageerle/ruoyi-ai/fork)** • **[📚 English](README_EN.md)** • **[📖 查看完整文档](https://doc.ruoyiai.chat/)**
*用 ❤️ 打造,由 RuoYi AI 开源社区维护* *用 ❤️ 打造,由 RuoYi AI 开源社区维护*

View File

@@ -22,8 +22,8 @@
*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* *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)** | **[中文](README.md)** | **[📖 Documentation](https://doc.ruoyiai.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)** **[🚀 Live Demo](https://web.ruoyiai.chat/)** | **[🐛 Report Issues](https://github.com/ageerle/ruoyi-ai/issues)** | **[💡 Feature Requests](https://github.com/ageerle/ruoyi-ai/issues)**
</div> </div>
@@ -34,21 +34,12 @@
| Module | Current Capabilities | | Module | Current Capabilities |
|:---:|---| |:---:|---|
| **Model Management** | Multi-model integration (OpenAI/DeepSeek/Tongyi/Zhipu), multi-modal understanding, Coze/DIFY/FastGPT platform integration | | **Model Management** | Multi-model integration (OpenAI/DeepSeek/Tongyi/Zhipu/MiniMax), multi-modal understanding, Coze/DIFY/FastGPT platform integration |
| **Knowledge Base** | Local RAG + Vector DB (Milvus/Weaviate/Qdrant) + Document parsing | | **Knowledge Base** | Local RAG + Vector DB (Milvus/Weaviate/Qdrant) + Document parsing |
| **Tool Management** | MCP protocol integration, Skills capability + Extensible tool ecosystem | | **Tool Management** | MCP protocol integration, Skills capability + Extensible tool ecosystem |
| **Workflow Orchestration** | Visual workflow designer, drag-and-drop node orchestration, SSE streaming execution, currently supports model calls, email sending, manual review nodes | | **Workflow Orchestration** | Visual workflow designer, drag-and-drop node orchestration, SSE streaming execution, currently supports model calls, email sending, manual review nodes |
| **Multi-Agent** | Agent framework based on Langchain4j, Supervisor mode orchestration, supports multiple decision models | | **Multi-Agent** | Agent framework based on Langchain4j, Supervisor mode orchestration, supports multiple decision models |
## 🚀 Quick Start
### Live Demo
| 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 ### Project Repositories
| Module | GitHub Repository | Gitee Repository | GitCode Repository | | Module | GitHub Repository | Gitee Repository | GitCode Repository |
@@ -56,6 +47,7 @@
| 🔧 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) | | 🔧 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) | | 🎨 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) | | 🛠️ 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) |
| 🎬 Drama | [ruoyi-drama](https://github.com/ageerle/ruoyi-drama) | [ruoyi-drama](https://gitee.com/ageerle/ruoyi-drama) | |
### Partner Projects ### Partner Projects
| Project Name | GitHub Repository | Gitee Repository | | Project Name | GitHub Repository | Gitee Repository |
@@ -69,8 +61,6 @@
- **Data Storage**: MySQL 8.0 + Redis + Vector Databases (Milvus/Weaviate/Qdrant) - **Data Storage**: MySQL 8.0 + Redis + Vector Databases (Milvus/Weaviate/Qdrant)
- **Frontend**: Vue 3 + Vben Admin + element-plus-x - **Frontend**: Vue 3 + Vben Admin + element-plus-x
- **Security**: Sa-Token + JWT dual-layer security - **Security**: Sa-Token + JWT dual-layer security
- **Document Processing**: PDF, Word, Excel parsing, intelligent image analysis - **Document Processing**: PDF, Word, Excel parsing, intelligent image analysis
- **Real-time Communication**: WebSocket real-time communication, SSE streaming response - **Real-time Communication**: WebSocket real-time communication, SSE streaming response
- **System Monitoring**: Comprehensive logging system, performance monitoring, service health checks - **System Monitoring**: Comprehensive logging system, performance monitoring, service health checks
@@ -186,11 +176,39 @@ docker-compose -f docker-compose-all.yaml logs -f [service-name]
docker-compose -f docker-compose-all.yaml restart [service-name] docker-compose -f docker-compose-all.yaml restart [service-name]
``` ```
### MiniMax Configuration
The built-in MiniMax provider accepts one API Host value and selects the matching protocol adapter. Use a Base URL from this table:
| Region | OpenAI-compatible Base URL | Anthropic-compatible Base URL |
| --- | --- | --- |
| Global | `https://api.minimax.io/v1` | `https://api.minimax.io/anthropic` |
| China | `https://api.minimaxi.com/v1` | `https://api.minimaxi.com/anthropic` |
For Anthropic-compatible requests, configure the Base URL ending in `/anthropic`. Do not append `/v1` or `/v1/messages`; the provider adapter derives the request path internally.
| Model ID | Total context | Input modalities | Thinking |
| --- | ---: | --- | --- |
| `MiniMax-M3` | 1,000,000 tokens | Text, image, video | Adaptive or disabled |
| `MiniMax-M2.7` | 204,800 tokens | Text | Always on |
Current pay-as-you-go prices are in USD per million tokens:
| Model | Service tier and input range | Input | Output | Cache read | Cache write |
| --- | --- | ---: | ---: | ---: | ---: |
| `MiniMax-M3` | Standard, up to 512,000 input tokens | $0.30 | $1.20 | $0.06 | Not listed |
| `MiniMax-M3` | Standard, over 512,000 input tokens | $0.60 | $2.40 | $0.12 | Not listed |
| `MiniMax-M3` | Priority, up to 512,000 input tokens | $0.45 | $1.80 | $0.09 | Not listed |
| `MiniMax-M3` | Priority, over 512,000 input tokens | $0.90 | $3.60 | $0.18 | Not listed |
| `MiniMax-M2.7` | Standard | $0.30 | $1.20 | $0.06 | $0.375 |
See the [official API overview](https://platform.minimax.io/docs/api-reference/api-overview) and [pay-as-you-go pricing](https://platform.minimax.io/docs/guides/pricing-paygo) for current details.
## 📚 Documentation ## 📚 Documentation
Want to learn more about installation, deployment, configuration, and secondary development? Want to learn more about installation, deployment, configuration, and secondary development?
**👉 [Complete Documentation](https://doc.pandarobot.chat)** **👉 [Complete Documentation](https://doc.ruoyiai.chat/)**
## 🤝 Contributing ## 🤝 Contributing
@@ -213,23 +231,47 @@ This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) f
## 🙏 Acknowledgments ## 🙏 Acknowledgments
Thanks to the following excellent open-source projects for their support: Thanks to the following excellent open-source projects for their support:
- [Spring AI Alibaba Copilot](https://github.com/spring-ai-alibaba/copilot) - Intelligent coding assistant based on spring-ai-alibaba
- [Langchain4j](https://github.com/langchain4j/langchain4j) - Powerful Java LLM development framework - [Langchain4j](https://github.com/langchain4j/langchain4j) - Powerful Java LLM development framework
- [RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus) - Mature enterprise-level rapid development framework - [RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus) - Mature enterprise-level rapid development framework
- [Vben Admin](https://github.com/vbenjs/vue-vben-admin) - Modern Vue admin template - [Vben Admin](https://github.com/vbenjs/vue-vben-admin) - Modern Vue admin template
## 🌐 Ecosystem Partners ## 💎 Sponsors
- [PPIO Cloud](https://ppinfra.com/user/register?invited_by=P8QTUY&utm_source=github_ruoyi-ai) - Provides cost-effective GPU computing and model API services **Thanks to the following sponsors for supporting this project:**
- [Youyun Intelligent Computing](https://www.compshare.cn/?ytag=GPU_YY-gh_ruoyi) - Thousands of RTX40 series GPUs + mainstream models API services, second-level response, pay-per-use, free for new customers.
<a href="https://www.atlascloud.ai?ref=89F97E">
<img src="docs/image/sponsor/atlascloud_banner.png" alt="Atlas Cloud" width="160" height="80">
</a>
[Visit Atlas Cloud](https://www.atlascloud.ai?ref=89F97E) · [Coding Plan Promotion](https://www.atlascloud.ai/console/coding-plan)
A full-modal AI inference platform that gives developers a unified AI API, supporting video generation, image generation, and LLMs. Connect once to access **300+ curated models**.
<a href="https://www.volcengine.com/activity/codingplan?utm_campaign=hw&utm_content=hw&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ageerle-ruoyi-ai">
<img src="docs/image/sponsor/huoshan.png" alt="Volcengine CodingPlan" width="160" height="80">
</a>
[Volcengine CodingPlan Developer Program](https://www.volcengine.com/activity/codingplan?utm_campaign=hw&utm_content=hw&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ageerle-ruoyi-ai)
Volcengine is ByteDance's cloud and AI service platform. Volcengine Ark provides API access to Doubao LLM, DeepSeek, and more — a one-stop AI development and inference platform for developers.
## 💬 Community Chat ## 💬 Community Chat
<div align="center"> <div align="center">
**[📱 Join Telegram Group]( <table>
https://t.me/+LqooQAc5HxRmYmE1)** <tr>
<td align="center">
<img src="docs/image/wx.png" alt="WeChat QR Code" width="200" height="200"><br>
<strong>Scan to add author on WeChat</strong><br>
<em>Join group for learning</em>
</td>
<td align="center">
<img src="docs/image/qq.png" alt="QQ Group QR Code" width="200" height="200"><br>
<strong>QQ Tech Exchange Group</strong><br>
<em>Technical discussion</em>
</td>
</tr>
</table>
</div> </div>
@@ -237,7 +279,7 @@ https://t.me/+LqooQAc5HxRmYmE1)**
<div align="center"> <div align="center">
**[⭐ Star to Support](https://github.com/ageerle/ruoyi-ai)** • **[Fork to Contribute](https://github.com/ageerle/ruoyi-ai/fork)** • **[📚 中文](README.md)** • **[📖 Complete Documentation](https://doc.pandarobot.chat)** **[⭐ Star to Support](https://github.com/ageerle/ruoyi-ai)** • **[Fork to Contribute](https://github.com/ageerle/ruoyi-ai/fork)** • **[📚 中文](README.md)** • **[📖 Complete Documentation](https://doc.ruoyiai.chat/)**
*Built with ❤️, maintained by the RuoYi AI open-source community* *Built with ❤️, maintained by the RuoYi AI open-source community*

View File

@@ -20,6 +20,22 @@ RUN mvn clean package -Pprod -DskipTests
# 最终运行镜像 # 最终运行镜像
FROM eclipse-temurin:17-jre-alpine FROM eclipse-temurin:17-jre-alpine
RUN set -eux; \
apk add --no-cache ffmpeg; \
for filter in scale pad fps setsar format tpad trim settb setpts xfade \
aresample aformat apad atrim asetpts anullsrc concat acrossfade; do \
ffmpeg -hide_banner -filters 2>/dev/null | grep -Eq "[[:space:]]${filter}[[:space:]]"; \
done; \
ffmpeg -hide_banner -h filter=xfade 2>&1 | grep -q dissolve; \
ffmpeg -hide_banner -h filter=xfade 2>&1 | grep -q fadeblack; \
ffmpeg -hide_banner -h filter=xfade 2>&1 | grep -q slideleft; \
ffmpeg -hide_banner -encoders 2>/dev/null | grep -Eq '[[:space:]]libx264[[:space:]]'; \
ffmpeg -hide_banner -encoders 2>/dev/null | grep -Eq '[[:space:]]aac[[:space:]]'; \
ffprobe -hide_banner -version >/dev/null
ENV FFMPEG_PATH=/usr/bin/ffmpeg \
FFPROBE_PATH=/usr/bin/ffprobe
# 设置工作目录 # 设置工作目录
WORKDIR /app WORKDIR /app
@@ -27,10 +43,10 @@ WORKDIR /app
COPY --from=builder /build/ruoyi-admin/target/ruoyi-admin.jar ./ruoyi-admin.jar COPY --from=builder /build/ruoyi-admin/target/ruoyi-admin.jar ./ruoyi-admin.jar
# 创建日志目录 # 创建日志目录
RUN mkdir -p /ruoyi/server/logs RUN mkdir -p /ruoyi/server/logs /ruoyi/server/temp
# 暴露端口 # 暴露端口
EXPOSE 6039 EXPOSE 6039
# 启动命令 # 启动命令
ENTRYPOINT ["java", "-jar", "ruoyi-admin.jar", "--spring.profiles.active=prod"] ENTRYPOINT ["java", "-Djava.io.tmpdir=/ruoyi/server/temp", "-jar", "ruoyi-admin.jar", "--spring.profiles.active=prod"]

View File

@@ -65,7 +65,7 @@ services:
- "28080:8080" - "28080:8080"
environment: environment:
QUERY_DEFAULTS_LIMIT: 25 QUERY_DEFAULTS_LIMIT: 25
AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: true AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: "true"
PERSISTENCE_DATA_PATH: /var/lib/weaviate PERSISTENCE_DATA_PATH: /var/lib/weaviate
DEFAULT_VECTORIZER_MODULE: none DEFAULT_VECTORIZER_MODULE: none
ENABLE_MODULES: text2vec-cohere,text2vec-huggingface,text2vec-palm,text2vec-openai,generative-openai,generative-cohere,generative-palm,ref2vec-centroid,reranker-cohere,qna-openai ENABLE_MODULES: text2vec-cohere,text2vec-huggingface,text2vec-palm,text2vec-openai,generative-openai,generative-cohere,generative-palm,ref2vec-centroid,reranker-cohere,qna-openai

BIN
docs/image/01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

BIN
docs/image/02.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

BIN
docs/image/03.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

BIN
docs/image/wx06.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -1,10 +0,0 @@
#!/bin/bash
# 数据库初始化脚本
# 使用 --force 参数确保即使出错也继续执行
echo "开始初始化数据库..."
# 使用 --force 参数忽略错误继续执行
mysql -uroot -proot ruoyi-ai-agent --force < /docker-entrypoint-initdb.d/ruoyi-ai-v3_mysql8.sql
echo "数据库初始化完成"

View File

@@ -0,0 +1,86 @@
[CmdletBinding()]
param(
[switch]$Force
)
$ErrorActionPreference = 'Stop'
function Find-Executable([string]$Name) {
$command = Get-Command $Name -ErrorAction SilentlyContinue
if ($command) {
return $command.Source
}
$roots = @(
(Join-Path $env:LOCALAPPDATA 'Microsoft\WinGet\Packages'),
(Join-Path $env:USERPROFILE 'scoop\apps\ffmpeg'),
'C:\ProgramData\chocolatey\bin',
'C:\ffmpeg'
)
foreach ($root in $roots) {
if (-not (Test-Path $root)) { continue }
$match = Get-ChildItem -Path $root -Filter "$Name.exe" -Recurse -ErrorAction SilentlyContinue |
Select-Object -First 1 -ExpandProperty FullName
if ($match) { return $match }
}
return $null
}
$ffmpeg = Find-Executable 'ffmpeg'
$ffprobe = Find-Executable 'ffprobe'
if ($Force -or -not $ffmpeg -or -not $ffprobe) {
$winget = Get-Command winget -ErrorAction SilentlyContinue
if (-not $winget) {
throw '未找到 winget。请先从 Microsoft Store 安装“应用安装程序”,或使用项目 Dockerfile 运行后端。'
}
Write-Host '正在通过 winget 安装 FFmpeg...' -ForegroundColor Cyan
& winget install --id Gyan.FFmpeg --exact --source winget `
--accept-package-agreements --accept-source-agreements
if ($LASTEXITCODE -ne 0) {
throw "winget 安装失败,退出码: $LASTEXITCODE"
}
$ffmpeg = Find-Executable 'ffmpeg'
$ffprobe = Find-Executable 'ffprobe'
}
if (-not $ffmpeg -or -not $ffprobe) {
throw 'FFmpeg 已安装但未能定位 ffmpeg.exe 或 ffprobe.exe。请重新打开 PowerShell 后再次运行脚本。'
}
$binDirectory = Split-Path -Parent $ffmpeg
[Environment]::SetEnvironmentVariable('FFMPEG_PATH', $ffmpeg, 'User')
[Environment]::SetEnvironmentVariable('FFPROBE_PATH', $ffprobe, 'User')
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
$pathParts = @($userPath -split ';' | Where-Object { $_ })
if ($pathParts -notcontains $binDirectory) {
$newPath = (@($pathParts) + $binDirectory) -join ';'
[Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
}
$env:FFMPEG_PATH = $ffmpeg
$env:FFPROBE_PATH = $ffprobe
if (($env:Path -split ';') -notcontains $binDirectory) {
$env:Path = "$binDirectory;$env:Path"
}
Write-Host "ffmpeg: $ffmpeg" -ForegroundColor Green
Write-Host "ffprobe: $ffprobe" -ForegroundColor Green
& $ffmpeg -hide_banner -version | Select-Object -First 1
& $ffprobe -hide_banner -version | Select-Object -First 1
$encoders = & $ffmpeg -hide_banner -encoders 2>&1 | Out-String
if ($encoders -notmatch '\blibx264\b') {
throw '当前 FFmpeg 不包含 libx264 编码器,无法满足短剧合成配置。'
}
if ($encoders -notmatch '\baac\b') {
throw '当前 FFmpeg 不包含 AAC 编码器,无法满足短剧合成配置。'
}
Write-Host ''
Write-Host '安装与配置完成。请完全重启 IntelliJ IDEA 和 ruoyi-ai 后端服务。' -ForegroundColor Yellow
Write-Host '重启后Spring 将从 FFMPEG_PATH 和 FFPROBE_PATH 读取绝对路径。'

File diff suppressed because one or more lines are too long

16
pom.xml
View File

@@ -13,7 +13,7 @@
<description>全栈式AI开发平台</description> <description>全栈式AI开发平台</description>
<properties> <properties>
<revision>3.0.0</revision> <revision>3.1.0</revision>
<spring-boot.version>3.5.8</spring-boot.version> <spring-boot.version>3.5.8</spring-boot.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
@@ -54,17 +54,21 @@
<!-- Jackson XML --> <!-- Jackson XML -->
<jackson-dataformat-xml.version>2.18.2</jackson-dataformat-xml.version> <jackson-dataformat-xml.version>2.18.2</jackson-dataformat-xml.version>
<!-- AI 相关依赖 --> <!-- AI 相关依赖 -->
<langchain4j.version>1.13.0</langchain4j.version>
<langchain4j.community.version>1.13.0-beta23</langchain4j.community.version> <langchain4j.version>1.17.2</langchain4j.version>
<langgraph4j.version>1.5.3</langgraph4j.version> <langchain4j.beta.version>1.17.2-beta27</langchain4j.beta.version>
<langchain4j.community.version>1.17.0-beta27</langchain4j.community.version>
<langgraph4j.version>1.8.20</langgraph4j.version>
<weaviate.version>1.19.6</weaviate.version> <weaviate.version>1.19.6</weaviate.version>
<dify.version>1.0.7</dify.version> <dify.version>1.2.7</dify.version>
<coze.version>0.4.2</coze.version>
<!-- gRPC 版本 - 解决 Milvus SDK 依赖冲突 --> <!-- gRPC 版本 - 解决 Milvus SDK 依赖冲突 -->
<grpc.version>1.62.2</grpc.version> <grpc.version>1.62.2</grpc.version>
<!-- Apache Commons Compress - 用于POI处理ZIP格式 --> <!-- Apache Commons Compress - 用于POI处理ZIP格式 -->
<commons-compress.version>1.27.1</commons-compress.version> <commons-compress.version>1.27.1</commons-compress.version>
<avatar-generator.version>1.1.0</avatar-generator.version> <avatar-generator.version>1.1.0</avatar-generator.version>
<jsoup.version>1.21.2</jsoup.version> <jsoup.version>1.21.2</jsoup.version>
<knife4j.version>4.4.0</knife4j.version> <knife4j.version>4.4.0</knife4j.version>

View File

@@ -1,31 +1,59 @@
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/ # FFmpeg-enabled variant of ruoyi-admin/Dockerfile.
FROM bellsoft/liberica-openjdk-rocky:17.0.16-cds FROM bellsoft/liberica-openjdk-rocky:17.0.16-cds
#FROM bellsoft/liberica-openjdk-rocky:21.0.8-cds
#FROM findepi/graalvm:java17-native
LABEL maintainer="Lion Li" LABEL maintainer="Lion Li"
# Rocky base repositories do not provide the full codec build. RPM Fusion Free
# supplies ffmpeg with libx264, while the checks below prevent a reduced build
# from reaching production unnoticed.
RUN set -eux; \
dnf -y install dnf-plugins-core epel-release; \
. /etc/os-release; \
rocky_major="${VERSION_ID%%.*}"; \
if [ "${rocky_major}" -ge 9 ]; then \
dnf config-manager --set-enabled crb; \
else \
dnf config-manager --set-enabled powertools; \
fi; \
dnf -y install "https://mirrors.rpmfusion.org/free/el/rpmfusion-free-release-${rocky_major}.noarch.rpm"; \
dnf -y install ffmpeg; \
for filter in scale pad fps setsar format tpad trim settb setpts xfade \
aresample aformat apad atrim asetpts anullsrc concat acrossfade; do \
ffmpeg -hide_banner -filters 2>/dev/null | grep -Eq "[[:space:]]${filter}[[:space:]]"; \
done; \
ffmpeg -hide_banner -h filter=xfade 2>&1 | grep -q dissolve; \
ffmpeg -hide_banner -h filter=xfade 2>&1 | grep -q fadeblack; \
ffmpeg -hide_banner -h filter=xfade 2>&1 | grep -q slideleft; \
ffmpeg -hide_banner -encoders 2>/dev/null | grep -Eq '[[:space:]]libx264[[:space:]]'; \
ffmpeg -hide_banner -encoders 2>/dev/null | grep -Eq '[[:space:]]aac[[:space:]]'; \
ffprobe -hide_banner -version >/dev/null; \
dnf clean all; \
rm -rf /var/cache/dnf
RUN mkdir -p /ruoyi/server/logs \ RUN mkdir -p /ruoyi/server/logs \
/ruoyi/server/temp \ /ruoyi/server/temp \
/ruoyi/skywalking/agent /ruoyi/skywalking/agent
WORKDIR /ruoyi/server WORKDIR /ruoyi/server
ENV SERVER_PORT=8080 SNAIL_PORT=28080 LANG=C.UTF-8 LC_ALL=C.UTF-8 JAVA_OPTS="" ENV SERVER_PORT=8080 \
SNAIL_PORT=28080 \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8 \
JAVA_OPTS="" \
FFMPEG_PATH=/usr/bin/ffmpeg \
FFPROBE_PATH=/usr/bin/ffprobe
EXPOSE ${SERVER_PORT} EXPOSE ${SERVER_PORT}
# 暴露 snail job 客户端端口 用于定时任务调度中心通信
EXPOSE ${SNAIL_PORT} EXPOSE ${SNAIL_PORT}
ADD ./target/ruoyi-admin.jar ./app.jar ADD ./target/ruoyi-admin.jar ./app.jar
SHELL ["/bin/bash", "-c"] SHELL ["/bin/bash", "-c"]
ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -Dserver.port=${SERVER_PORT} \ ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom \
-Djava.io.tmpdir=/ruoyi/server/temp \
-Dserver.port=${SERVER_PORT} \
-Dsnail-job.port=${SNAIL_PORT} \ -Dsnail-job.port=${SNAIL_PORT} \
# 应用名称 如果想区分集群节点监控 改成不同的名称即可
#-Dskywalking.agent.service_name=ruoyi-server \
#-javaagent:/ruoyi/skywalking/agent/skywalking-agent.jar \
-XX:+HeapDumpOnOutOfMemoryError -XX:+UseZGC ${JAVA_OPTS} \ -XX:+HeapDumpOnOutOfMemoryError -XX:+UseZGC ${JAVA_OPTS} \
-jar app.jar -jar app.jar

View File

@@ -27,12 +27,12 @@ snail-job:
enabled: false enabled: false
# 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务 # 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务
group: "ruoyi_group" group: "ruoyi_group"
# SnailJob 接入验证令牌 详见 script/sql/ry_job.sql `sj_group_config` 表 # SnailJob 接入验证令牌 详见 docs/script/sql/ruoyi-ai-v3_mysql8.sql `sj_group_config` 表
token: "SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT" token: "SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT"
server: server:
host: 127.0.0.1 host: 127.0.0.1
port: 17888 port: 17888
# 命名空间UUID 详见 script/sql/ry_job.sql `sj_namespace`表`unique_id`字段 # 命名空间UUID 详见 docs/script/sql/ruoyi-ai-v3_mysql8.sql `sj_namespace`表`unique_id`字段
namespace: ${spring.profiles.active} namespace: ${spring.profiles.active}
# 随主应用端口漂移 # 随主应用端口漂移
port: 2${server.port} port: 2${server.port}

View File

@@ -28,12 +28,12 @@ snail-job:
enabled: false enabled: false
# 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务 # 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务
group: "ruoyi_group" group: "ruoyi_group"
# SnailJob 接入验证令牌 详见 script/sql/ry_job.sql `sj_group_config` 表 # SnailJob 接入验证令牌 详见 docs/script/sql/ruoyi-ai-v3_mysql8.sql `sj_group_config` 表
token: "SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT" token: "SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT"
server: server:
host: 127.0.0.1 host: 127.0.0.1
port: 17888 port: 17888
# 命名空间UUID 详见 script/sql/ry_job.sql `sj_namespace`表`unique_id`字段 # 命名空间UUID 详见 docs/script/sql/ruoyi-ai-v3_mysql8.sql `sj_namespace`表`unique_id`字段
namespace: ${spring.profiles.active} namespace: ${spring.profiles.active}
# 随主应用端口漂移 # 随主应用端口漂移
port: 2${server.port} port: 2${server.port}

View File

@@ -265,7 +265,7 @@ demo:
# 是否开启演示模式(开启后所有写操作将被拦截) # 是否开启演示模式(开启后所有写操作将被拦截)
enabled: false enabled: false
# 提示消息 # 提示消息
message: "演示模式,不允许进行写操作" message: "演示模式,不允许操作"
# 排除的路径(这些路径不受演示模式限制) # 排除的路径(这些路径不受演示模式限制)
excludes: excludes:
- /login - /login
@@ -276,7 +276,9 @@ demo:
- /chat/send - /chat/send
- /system/session/** - /system/session/**
- /system/message/** - /system/message/**
- /system/attach/**
- /system/fragment/**
- /system/info/**
--- # warm-flow工作流配置 --- # warm-flow工作流配置
warm-flow: warm-flow:
# 是否开启工作流默认true # 是否开启工作流默认true
@@ -311,3 +313,37 @@ vector-store:
collectionname: LocalKnowledge collectionname: LocalKnowledge
api-key: api-key:
use-tls: false use-tls: false
# 短剧成片合成
short-drama:
composition:
ffmpeg-path: ${FFMPEG_PATH:ffmpeg}
ffprobe-path: ${FFPROBE_PATH:ffprobe}
fps: ${SHORT_DRAMA_COMPOSITION_FPS:30}
audio-sample-rate: ${SHORT_DRAMA_COMPOSITION_AUDIO_SAMPLE_RATE:48000}
video-codec: ${SHORT_DRAMA_COMPOSITION_VIDEO_CODEC:libx264}
audio-codec: ${SHORT_DRAMA_COMPOSITION_AUDIO_CODEC:aac}
preset: ${SHORT_DRAMA_COMPOSITION_PRESET:medium}
crf: ${SHORT_DRAMA_COMPOSITION_CRF:20}
audio-bitrate: ${SHORT_DRAMA_COMPOSITION_AUDIO_BITRATE:192k}
max-clips: ${SHORT_DRAMA_COMPOSITION_MAX_CLIPS:100}
max-transition-seconds: ${SHORT_DRAMA_COMPOSITION_MAX_TRANSITION_SECONDS:2.0}
minimum-output-bytes: ${SHORT_DRAMA_COMPOSITION_MINIMUM_OUTPUT_BYTES:1024}
max-process-output-bytes: ${SHORT_DRAMA_COMPOSITION_MAX_PROCESS_OUTPUT_BYTES:1048576}
max-source-bytes: ${SHORT_DRAMA_COMPOSITION_MAX_SOURCE_BYTES:536870912}
max-total-source-bytes: ${SHORT_DRAMA_COMPOSITION_MAX_TOTAL_SOURCE_BYTES:2147483648}
probe-timeout: ${SHORT_DRAMA_COMPOSITION_PROBE_TIMEOUT:30s}
process-timeout: ${SHORT_DRAMA_COMPOSITION_PROCESS_TIMEOUT:30m}
job-stale-after: ${SHORT_DRAMA_COMPOSITION_JOB_STALE_AFTER:45m}
worker-core-size: ${SHORT_DRAMA_COMPOSITION_WORKER_CORE_SIZE:1}
worker-max-size: ${SHORT_DRAMA_COMPOSITION_WORKER_MAX_SIZE:2}
worker-queue-capacity: ${SHORT_DRAMA_COMPOSITION_WORKER_QUEUE_CAPACITY:8}
storage-mode: ${SHORT_DRAMA_COMPOSITION_STORAGE_MODE:local}
local-output-directory: ${SHORT_DRAMA_COMPOSITION_LOCAL_OUTPUT_DIRECTORY:logs/short-drama-compositions}
download:
connect-timeout: ${SHORT_DRAMA_DOWNLOAD_CONNECT_TIMEOUT:30s}
call-timeout: ${SHORT_DRAMA_DOWNLOAD_CALL_TIMEOUT:10m}
max-redirects: ${SHORT_DRAMA_DOWNLOAD_MAX_REDIRECTS:5}
# 生产环境建议配置为视频供应商或 OSS/CDN 域名,多个值用逗号分隔。
allowed-hosts: ${SHORT_DRAMA_DOWNLOAD_ALLOWED_HOSTS:}
fake-ip-allowed-hosts: ${SHORT_DRAMA_DOWNLOAD_FAKE_IP_ALLOWED_HOSTS:atlas-media.oss-us-west-1.aliyuncs.com}

View File

@@ -14,7 +14,7 @@
</description> </description>
<properties> <properties>
<revision>3.0.0</revision> <revision>3.1.0</revision>
</properties> </properties>
<dependencyManagement> <dependencyManagement>

View File

@@ -0,0 +1,30 @@
package org.ruoyi.common.chat.domain.bo.chat;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.ruoyi.common.mybatis.core.domain.BaseEntity;
/**
* 按厂商批量更新密钥业务对象
*
* @author ageerle
* @date 2025-12-14
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ModelBatchKeyBo extends BaseEntity {
/**
* 模型供应商
*/
@NotBlank(message = "厂商编码不能为空")
private String providerCode;
/**
* 密钥
*/
@NotBlank(message = "密钥不能为空")
private String apiKey;
}

View File

@@ -21,9 +21,19 @@ import java.util.List;
@Data @Data
public class ChatRequest { public class ChatRequest {
@NotEmpty(message = "传入的模型不能为空") /**
* 模型名称。
* 智能体模式下可缺省:传 agentId 时后端按智能体绑定的模型解析,此字段仅作回退。
*/
private String model; private String model;
/**
* 智能体ID。传入时后端按智能体配置解析模型/工具/技能/知识库/提示词/是否深度思考。
*/
@JsonSerialize(using = ToStringSerializer.class)
@JSONField(serializeUsing = String.class)
private Long agentId;
/** /**
* 对话消息 * 对话消息
*/ */
@@ -35,16 +45,6 @@ public class ChatRequest {
*/ */
private WorkFlowRunner workFlowRunner; private WorkFlowRunner workFlowRunner;
/**
* 人机交互信息体
*/
private ReSumeRunner reSumeRunner;
/**
* 是否为人机交互用户继续输入
*/
private Boolean isResume = false;
/** /**
* 是否启用工作流 * 是否启用工作流
*/ */

View File

@@ -1,19 +0,0 @@
package org.ruoyi.common.chat.domain.dto.request;
import lombok.Data;
/**
* 人机交互输入信息
*/
@Data
public class ReSumeRunner {
/**
* 运行节点UUID
*/
private String runtimeUuid;
/**
* 人机交互用户输入信息
*/
private String feedbackContent;
}

View File

@@ -0,0 +1,29 @@
package org.ruoyi.common.chat.entity.audio;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Data;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
/**
* Text-to-speech context.
*/
@Data
@Builder
public class AudioContext {
@NotNull(message = "模型配置不能为空")
private ChatModelVo chatModelVo;
@NotBlank(message = "输入文本不能为空")
private String input;
private String voice;
private String responseFormat;
private Double speed;
private String instructions;
}

View File

@@ -42,4 +42,9 @@ public class ImageContext {
@Min(value = 0, message = "随机数种子不能小于0") @Min(value = 0, message = "随机数种子不能小于0")
@Max(value = 2147483647, message = "随机数种子不能大于2147483647") @Max(value = 2147483647, message = "随机数种子不能大于2147483647")
private Integer seed; private Integer seed;
/**
* 参考图 URL图生图用
*/
private String image;
} }

View File

@@ -0,0 +1,28 @@
package org.ruoyi.common.chat.entity.media;
import lombok.Builder;
import lombok.Data;
/**
* Unified media generation response.
*/
@Data
@Builder
public class MediaGenerationResponse {
private String type;
private String mimeType;
private String url;
private String b64Json;
private String dataUrl;
private String id;
private String status;
private String rawResponse;
}

View File

@@ -0,0 +1,35 @@
package org.ruoyi.common.chat.entity.video;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Data;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
/**
* Text-to-video context.
*/
@Data
@Builder
public class VideoContext {
@NotNull(message = "模型配置不能为空")
private ChatModelVo chatModelVo;
@NotBlank(message = "提示词不能为空")
private String prompt;
private String size;
private Integer seconds;
private String quality;
/** 参考图 URL图生视频模式单图 */
private String imageUrl;
/** 多参考图 URL多参考图生视频模式配合 @imageN prompt 使用) */
private java.util.List<String> referenceImages;
private String videoId;
}

View File

@@ -0,0 +1,40 @@
package org.ruoyi.common.chat.factory;
import org.ruoyi.common.chat.service.audio.IAudioGenerationService;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Text-to-speech service factory.
*/
@Component
public class AudioServiceFactory implements ApplicationContextAware {
private final Map<String, IAudioGenerationService> audioServiceMap = new ConcurrentHashMap<>();
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, IAudioGenerationService> serviceMap = applicationContext.getBeansOfType(IAudioGenerationService.class);
for (IAudioGenerationService service : serviceMap.values()) {
if (service != null) {
audioServiceMap.put(service.getProviderName(), service);
}
}
}
public IAudioGenerationService getOriginalService(String providerCode) {
IAudioGenerationService service = audioServiceMap.get(providerCode);
if (service == null && "custom_api".equals(providerCode)) {
service = audioServiceMap.get("openai");
}
if (service == null) {
throw new IllegalArgumentException("不支持的语音模型供应商: " + providerCode);
}
return service;
}
}

View File

@@ -37,6 +37,9 @@ public class ImageServiceFactory implements ApplicationContextAware {
*/ */
public IImageGenerationService getOriginalService(String category) { public IImageGenerationService getOriginalService(String category) {
IImageGenerationService service = imageSerivceMap.get(category); IImageGenerationService service = imageSerivceMap.get(category);
if (service == null && "custom_api".equals(category)) {
service = imageSerivceMap.get("openai");
}
if (service == null) { if (service == null) {
throw new IllegalArgumentException("不支持的模型类别: " + category); throw new IllegalArgumentException("不支持的模型类别: " + category);
} }

View File

@@ -0,0 +1,40 @@
package org.ruoyi.common.chat.factory;
import org.ruoyi.common.chat.service.video.IVideoGenerationService;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Text-to-video service factory.
*/
@Component
public class VideoServiceFactory implements ApplicationContextAware {
private final Map<String, IVideoGenerationService> videoServiceMap = new ConcurrentHashMap<>();
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, IVideoGenerationService> serviceMap = applicationContext.getBeansOfType(IVideoGenerationService.class);
for (IVideoGenerationService service : serviceMap.values()) {
if (service != null) {
videoServiceMap.put(service.getProviderName(), service);
}
}
}
public IVideoGenerationService getOriginalService(String providerCode) {
IVideoGenerationService service = videoServiceMap.get(providerCode);
if (service == null && "custom_api".equals(providerCode)) {
service = videoServiceMap.get("openai");
}
if (service == null) {
throw new IllegalArgumentException("不支持的视频模型供应商: " + providerCode);
}
return service;
}
}

View File

@@ -0,0 +1,15 @@
package org.ruoyi.common.chat.service.audio;
import jakarta.validation.Valid;
import org.ruoyi.common.chat.entity.audio.AudioContext;
import org.ruoyi.common.chat.entity.media.MediaGenerationResponse;
/**
* Public text-to-speech service.
*/
public interface IAudioGenerationService {
MediaGenerationResponse generateSpeech(@Valid AudioContext audioContext);
String getProviderName();
}

View File

@@ -73,4 +73,13 @@ public interface IChatModelService {
* @return 是否删除成功 * @return 是否删除成功
*/ */
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid); Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 按厂商批量更新密钥
*
* @param providerCode 厂商编码
* @param apiKey 密钥
* @return 是否更新成功
*/
Boolean updateApiKeyByProvider(String providerCode, String apiKey);
} }

View File

@@ -2,6 +2,8 @@ package org.ruoyi.common.chat.service.image;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import org.ruoyi.common.chat.entity.image.ImageContext; import org.ruoyi.common.chat.entity.image.ImageContext;
import org.ruoyi.common.chat.entity.media.MediaGenerationResponse;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
/** /**
* 公共文生图接口 * 公共文生图接口
@@ -9,10 +11,31 @@ import org.ruoyi.common.chat.entity.image.ImageContext;
public interface IImageGenerationService { public interface IImageGenerationService {
/** /**
* 根据文字生成图片 * 根据文字生成图片(同步阻塞,返回 URL
*/ */
String generateImage(@Valid ImageContext imageContext); String generateImage(@Valid ImageContext imageContext);
/**
* 异步启动图片生成(返回 prediction 信息,用于轮询进度)
* 默认回退到同步模式
*/
default MediaGenerationResponse startImageGeneration(@Valid ImageContext imageContext) {
String url = generateImage(imageContext);
return MediaGenerationResponse.builder()
.type("image")
.url(url)
.status("completed")
.build();
}
/**
* 上传图生图/图生视频使用的临时媒体文件。
* 不支持上传的供应商保持默认异常,避免静默回退到错误存储。
*/
default String uploadMedia(ChatModelVo model, byte[] content, String fileName, String contentType) {
throw new UnsupportedOperationException("当前图片供应商不支持临时媒体上传");
}
/** /**
* 获取服务提供商名称 * 获取服务提供商名称
*/ */

View File

@@ -0,0 +1,17 @@
package org.ruoyi.common.chat.service.video;
import jakarta.validation.Valid;
import org.ruoyi.common.chat.entity.media.MediaGenerationResponse;
import org.ruoyi.common.chat.entity.video.VideoContext;
/**
* Public text-to-video service.
*/
public interface IVideoGenerationService {
MediaGenerationResponse generateVideo(@Valid VideoContext videoContext);
MediaGenerationResponse retrieveVideo(@Valid VideoContext videoContext);
String getProviderName();
}

View File

@@ -10,6 +10,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.core.task.VirtualThreadTaskExecutor; import org.springframework.core.task.VirtualThreadTaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.*; import java.util.concurrent.*;
/** /**
@@ -22,6 +23,12 @@ import java.util.concurrent.*;
@EnableConfigurationProperties(ThreadPoolProperties.class) @EnableConfigurationProperties(ThreadPoolProperties.class)
public class ThreadPoolConfig { public class ThreadPoolConfig {
private final ThreadPoolProperties properties;
public ThreadPoolConfig(ThreadPoolProperties properties) {
this.properties = properties;
}
/** /**
* 核心线程数 = cpu 核心数 + 1 * 核心线程数 = cpu 核心数 + 1
*/ */
@@ -54,6 +61,22 @@ public class ThreadPoolConfig {
return scheduledThreadPoolExecutor; return scheduledThreadPoolExecutor;
} }
/**
* 知识库解析专用异步线程池
*/
@Bean(name = "knowledgeParseExecutor")
public ThreadPoolTaskExecutor knowledgeParseExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(core);
executor.setMaxPoolSize(core * 2);
executor.setQueueCapacity(properties.getQueueCapacity());
executor.setKeepAliveSeconds(properties.getKeepAliveSeconds());
executor.setThreadNamePrefix("knowledge-parse-pool-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
/** /**
* 销毁事件 * 销毁事件
* 停止线程池 * 停止线程池

View File

@@ -1,8 +1,11 @@
package org.ruoyi.common.core.service; package org.ruoyi.common.core.service;
import jakarta.servlet.http.HttpServletResponse;
import org.ruoyi.common.core.domain.dto.OssDTO; import org.ruoyi.common.core.domain.dto.OssDTO;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.List; import java.util.List;
/** /**
@@ -33,5 +36,19 @@ public interface OssService {
*/ */
OssDTO uploadFile(MultipartFile file); OssDTO uploadFile(MultipartFile file);
/**
* Upload a server-side file without buffering the complete payload in memory.
*/
OssDTO uploadFile(File file);
/**
* Stream an object as an authenticated attachment response.
*/
void downloadFile(Long ossId, HttpServletResponse response) throws IOException;
/**
* Delete one object and its sys_oss metadata.
*/
Boolean deleteFile(Long ossId);
} }

View File

@@ -310,7 +310,7 @@ public class OssClient {
client.deleteObject( client.deleteObject(
x -> x.bucket(properties.getBucketName()) x -> x.bucket(properties.getBucketName())
.key(removeBaseUrl(path)) .key(removeBaseUrl(path))
.build()); .build()).join();
} catch (Exception e) { } catch (Exception e) {
throw new OssException("删除文件失败,请检查配置信息:[" + e.getMessage() + "]"); throw new OssException("删除文件失败,请检查配置信息:[" + e.getMessage() + "]");
} }

View File

@@ -265,10 +265,6 @@ public class WorkflowEngine {
String node = streamingOutput.node(); String node = streamingOutput.node();
String chunk = streamingOutput.chunk(); String chunk = streamingOutput.chunk();
log.info("node:{},chunk:{}", node, chunk); log.info("node:{},chunk:{}", node, chunk);
Map<String, String> strMap = new HashMap<>();
strMap.put("ck", chunk);
// SSEEmitterHelper.parseAndSendPartialMsg(sseEmitter, "[NODE_CHUNK_" + node + "]", strMap.toString());
SSEEmitterHelper.parseAndSendPartialMsg(sseEmitter, "[NODE_CHUNK_" + node + "]", chunk); SSEEmitterHelper.parseAndSendPartialMsg(sseEmitter, "[NODE_CHUNK_" + node + "]", chunk);
} else { } else {
AbstractWfNode abstractWfNode = wfState.getCompletedNodes().stream() AbstractWfNode abstractWfNode = wfState.getCompletedNodes().stream()

View File

@@ -1,336 +0,0 @@
# 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`)
**功能**:
- 调用日志列表
- 按工具/日期筛选
- 成功率统计
- 响应时间统计
**图表**:
- 每日调用次数趋势图
- 工具调用成功率饼图
- 平均响应时间柱状图

View File

@@ -1,152 +0,0 @@
# 数据库操作智能体实现总结
## 概述
基于 LangChain4j 的 **Pure agentic AI** 模式,完成了一个智能数据库查询系统。该系统能够根据用户的自然语言问题,自动分析数据库结构、生成查询计划并执行相应的数据库操作。
## 架构设计
### 1. 整体框架
```
用户请求 → SupervisorAgent(协调器) → SqlAgent(数据库专家) → 数据库工具 → 数据库
结果处理与响应
```
### 2. 核心组件
#### A. SqlAgent (数据库查询专家)
- **文件**: `org.ruoyi.agent.SqlAgent`
- **职责**: 根据用户的自然语言问题,调用相应的工具查询数据库
- **使用的工具**:
- `QueryAllTablesTool`: 查询所有表名和注释
- `QueryTableSchemaTool`: 查询表的DDLCREATE TABLE语句
- `ExecuteSqlQueryTool`: 执行SELECT查询
```java
public interface SqlAgent {
@SystemMessage("...") // 详细的系统提示
@UserMessage("请回答以下问题:{{query}}")
@Agent("一个智能数据库查询助手...")
String getData(@V("query") String query);
}
```
#### B. SupervisorAgent (总体协调器)
-`OpenAIServiceImpl.doAgent()` 中创建
- 作用:协调 SqlAgent 的执行,管理任务流程
- 响应策略:`SUMMARY` - 返回所有操作的摘要
```java
SupervisorAgent supervisor = AgenticServices
.supervisorBuilder()
.chatModel(PLANNER_MODEL)
.subAgents(sqlAgent)
.responseStrategy(SupervisorResponseStrategy.SUMMARY)
.build();
```
#### C. 数据库工具 (Tools)
##### 1. QueryAllTablesTool
```java
@Tool("Query all tables in the database and return table names and basic information")
public String queryAllTables()
```
- 返回数据库中所有表的名称和注释
- 使用注入的 `agentDataSource` DataSource
##### 2. QueryTableSchemaTool
```java
@Tool("Query the CREATE TABLE statement (DDL) for a specific table by table name")
public String queryTableSchema(String tableName)
```
- 返回指定表的建表SQL语句
- 包含SQL注入防护表名有效性验证
##### 3. ExecuteSqlQueryTool
```java
@Tool("Execute a SELECT SQL query and return the results. Example: SELECT * FROM sys_user")
public String executeSql(String sql)
```
- 执行SELECT查询安全性考虑不允许执行其他操作
- 格式化查询结果最多显示前20行
### 3. 配置体系
#### AgentMysqlProperties
配置文件前缀:`agent.mysql`
```yaml
agent:
mysql:
enabled: true
url: jdbc:mysql://localhost:3306/your_database
username: your_username
password: your_password
max-pool-size: 10
min-idle: 2
```
#### AgentMysqlConfig
- 创建独立的 DataSource Bean (`agentDataSource`)
- 使用 HikariCP 连接池管理
- 与项目主数据源隔离
#### TableSchemaManager
- 在应用启动时初始化表结构缓存
- 使用 `ConcurrentHashMap` 存储结构信息
- 支持按需刷新单个表的结构
## 工作流程示例
### 用户查询: "数据库有哪些表?"
```
1. SupervisorAgent 接收请求
2. SupervisorAgent 分析请求,决定调用 SqlAgent
3. SqlAgent 理解需求,调用 QueryAllTablesTool
4. QueryAllTablesTool 连接数据库,获取所有表
5. 结果返回给 SqlAgent
6. SqlAgent 格式化结果
7. SupervisorAgent 生成最终摘要
8. 结果通过流式处理器返回给用户
```
### 用户查询: "查询 sys_user 表中有多少条记录"
```
1. SqlAgent 接收请求
2. SqlAgent 分析需求,可能先调用 QueryTableSchemaTool 了解表结构
3. 然后调用 ExecuteSqlQueryTool 执行 "SELECT COUNT(*) FROM sys_user"
4. 获取查询结果并返回
```
## Agentic AI 特性
### 自适应决策
- Agent 能根据上下文和之前的结果决定下一步操作
- 不是预定义的固定流程,而是动态适应
### 例子
当询问"查询部门表的字段信息时"
- SqlAgent 可能先调用 `QueryTableSchemaTool` 获取建表SQL
- 如果发现需要具体的数据示例,会继续调用 `ExecuteSqlQueryTool`
- 整个决策过程由 LLM 驱动,非硬编码
## 安全考虑
1. **数据源隔离**: Agent 使用独立的数据源agentDataSource与主应用隔离
2. **SQL验证**: 只允许执行 SELECT 查询
3. **表名验证**: 表名必须通过正则表达式验证防止SQL注入
4. **权限限制**: 可通过 AGENT_ALLOWED_TABLES 环境变量限制可访问的表
5. **凭证管理**: 数据库凭证通过配置文件管理,不硬编码

View File

@@ -35,6 +35,12 @@
<version>${langchain4j.version}</version> <version>${langchain4j.version}</version>
</dependency> </dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-anthropic</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<dependency> <dependency>
<groupId>dev.langchain4j</groupId> <groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId> <artifactId>langchain4j-ollama</artifactId>
@@ -44,7 +50,7 @@
<dependency> <dependency>
<groupId>dev.langchain4j</groupId> <groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-agentic</artifactId> <artifactId>langchain4j-agentic</artifactId>
<version>${langchain4j.community.version}</version> <version>${langchain4j.beta.version}</version>
</dependency> </dependency>
<dependency> <dependency>
@@ -69,7 +75,7 @@
<dependency> <dependency>
<groupId>dev.langchain4j</groupId> <groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-document-parser-apache-tika</artifactId> <artifactId>langchain4j-document-parser-apache-tika</artifactId>
<version>${langchain4j.community.version}</version> <version>${langchain4j.beta.version}</version>
<exclusions> <exclusions>
<exclusion> <exclusion>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
@@ -82,39 +88,39 @@
<dependency> <dependency>
<groupId>dev.langchain4j</groupId> <groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-milvus</artifactId> <artifactId>langchain4j-milvus</artifactId>
<version>${langchain4j.community.version}</version> <version>${langchain4j.beta.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>dev.langchain4j</groupId> <groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-weaviate</artifactId> <artifactId>langchain4j-weaviate</artifactId>
<version>${langchain4j.community.version}</version> <version>${langchain4j.beta.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>dev.langchain4j</groupId> <groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-qdrant</artifactId> <artifactId>langchain4j-qdrant</artifactId>
<version>${langchain4j.community.version}</version> <version>${langchain4j.beta.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>dev.langchain4j</groupId> <groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId> <artifactId>langchain4j-mcp</artifactId>
<version>${langchain4j.community.version}</version> <version>${langchain4j.beta.version}</version>
</dependency> </dependency>
<!-- LangChain4j Skills - 技能模块 --> <!-- LangChain4j Skills - 技能模块 -->
<dependency> <dependency>
<groupId>dev.langchain4j</groupId> <groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-skills</artifactId> <artifactId>langchain4j-skills</artifactId>
<version>${langchain4j.community.version}</version> <version>${langchain4j.beta.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>dev.langchain4j</groupId> <groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-experimental-skills-shell</artifactId> <artifactId>langchain4j-experimental-skills-shell</artifactId>
<version>${langchain4j.community.version}</version> <version>${langchain4j.beta.version}</version>
</dependency> </dependency>
@@ -155,10 +161,24 @@
<version>${dify.version}</version> <version>${dify.version}</version>
</dependency> </dependency>
<!-- HikariCP 数据库连接池 -->
<dependency> <dependency>
<groupId>com.zaxxer</groupId> <groupId>com.coze</groupId>
<artifactId>HikariCP</artifactId> <artifactId>coze-api</artifactId>
<version>${coze.version}</version>
<exclusions>
<exclusion>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</exclusion>
<exclusion>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
</exclusion>
<exclusion>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<!-- MySQL JDBC 驱动 --> <!-- MySQL JDBC 驱动 -->
@@ -174,6 +194,23 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -0,0 +1,51 @@
package org.ruoyi.agent;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
import org.ruoyi.domain.bo.shortdrama.ShortDramaScriptResult;
/**
* 短剧剧本打磨 Agent —— 使用 langchain4j AiServices 结构化输出
* <p>
* 框架自动生成 JSON Schema 并强制 LLM 返回符合结构的数据,无需手工 parse。
*
* @author ageerle
*/
public interface ShortDramaScriptAgent {
@SystemMessage("""
你是顶级短剧编剧和创意总监。请根据用户的一个创意想法,创作完整的短剧剧本。
【核心原则 - 最高优先级】
1. 剧本必须完整、有张力、有画面感
2. 角色要有鲜明性格,不是工具人
3. 情节紧凑,每句台词都推动剧情
4. 场景描写具体,让分镜师能直接画出画面
5. 对话自然有力,避免废话
【剧本格式】
使用标准剧本格式,包含以下元素:
1. 场景头(Scene Heading):内景/外景+地点+时间,如"内景 客厅 清晨"
2. 场景描述(Scene Description):简洁描述场景环境、布局、关键道具
3. 动作描述(Action):描述角色的动作、表情、行为,连续段落形式
4. 对话(Dialogue):角色名: 台词内容
5. 画外音(Voiceover):旁白、独白、回忆中的声音
【剧本长度要求】
- scriptText1000-3000字含完整的开场、冲突发展、高潮、结尾
- outlineText400-800字概述完整故事线
【角色塑造要求】
- 每个角色要有明确的性格标签(如:霸道总裁、温柔女医、腹黑谋士)
- 角色之间要有清晰的关系和冲突
- 对话要符合角色性格
【情节要求】
- 必须有清晰的冲突和反转
- 情绪节奏要有起伏(紧张→舒缓→爆发)
- 结尾要有记忆点(反转/留白/情感升华)
""")
@UserMessage("用户期望项目名:{{projectName}}\n用户创意{{idea}}")
ShortDramaScriptResult polish(@V("projectName") String projectName, @V("idea") String idea);
}

View File

@@ -0,0 +1,32 @@
package org.ruoyi.config.agent;
import java.nio.file.Path;
/**
* 磁盘 Skills 目录路径解析器
* <p>
* langchain4j 的 ShellSkills 通过 FileSystemSkillLoader 从磁盘加载 SKILL.md
* 路径硬编码在 ChatServiceFacade 中。抽到此工具类供智能体管理端与聊天流程共用,
* 避免两处路径漂移。
*
* @author ruoyi team
*/
public final class SkillsPathResolver {
private SkillsPathResolver() {
}
/**
* skills 目录相对项目根目录的路径
*/
private static final String SKILLS_RELATIVE_PATH = "ruoyi-admin/src/main/resources/skills";
/**
* 返回磁盘 skills 目录的绝对路径
*/
public static Path resolveSkillsPath() {
String userDir = System.getProperty("user.dir");
return Path.of(userDir, SKILLS_RELATIVE_PATH);
}
}

View File

@@ -0,0 +1,72 @@
package org.ruoyi.constant;
/**
* 短剧图片资产常量 — 三视图 prompt 工程
*
* @author ruoyi
*/
public final class ShortDramaImageConstants {
private ShortDramaImageConstants() {}
/**
* 角色三视图 prompt 前缀,生图时自动拼到用户 prompt 之前。
* 把风格和构图指令放最前面,避免被角色描述稀释导致画风漂移。
*/
public static final String CHARACTER_PROMPT_PREFIX =
"character design sheet, multiple views reference sheet, " +
"front view / side view / back view full body, " +
"clean white background, no props, no text. ";
/**
* 角色三视图 prompt 后缀,生图时自动追加到用户 prompt 之后。
* 左侧1/3正面特写 + 右侧2/3三视图横向排列正面全身、侧面全身、背面全身
*/
public static final String CHARACTER_PROMPT_SUFFIX =
"。角色设定图,画面分为左右两个区域:" +
"【左侧区域】占约1/3宽度是角色的正面特写" +
"(完整正脸,最具辨识度的正面形态);" +
"【右侧区域】占约2/3宽度是角色三视图横向排列" +
"(从左到右依次为:正面全身、侧面全身、背面全身)," +
"三视图高度一致。纯白色背景,无其他元素。";
/** 场景图 prompt 前缀 */
public static final String LOCATION_PROMPT_PREFIX = "宽广空间全景,";
/** 场景图 prompt 后缀 */
public static final String LOCATION_PROMPT_SUFFIX = ",禁止出现任何角色,纯背景板";
/** 每个资产最多保留的图片变体数量 */
public static final int MAX_IMAGE_VARIANTS = 20;
// ==================== 视觉风格 ====================
/** 项目视觉风格 → 生图 prompt 后缀映射,确保同项目所有图片风格一致 */
public static final java.util.Map<String, String> ART_STYLE_PROMPTS = java.util.Map.of(
"american-comic", "美式漫画风格,粗线条,高饱和度色彩,强烈光影对比",
"chinese-comic", "现代国漫动画风格Chinese donghua 2D comic style赛璐璐平涂上色干净锐利的黑色线稿平面化光影无真实景深动漫人物比例略放大双眼、修长身形皮肤平滑无毛孔无写实肤质国风服饰剪裁与材质细节清晰色彩饱满通透画面精致干净禁止真人写实、摄影实拍、3D渲染、CGI、厚涂油画、写实皮肤纹理、景深虚化",
"japanese-anime", "现代日系动漫风格赛璐璐上色清晰干净的线条视觉小说CG感高质量2D风格",
"realistic", "真实电影级画面质感,真实现实场景,色彩饱满通透,画面干净精致,真实感"
);
/** 默认视觉风格 */
public static final String DEFAULT_ART_STYLE = "realistic";
/** 查找 artStyle 对应的 prompt 后缀,找不到返回空字符串 */
public static String artStylePrompt(String artStyle) {
if (artStyle == null) return "";
return ART_STYLE_PROMPTS.getOrDefault(artStyle, "");
}
/**
* 由项目 id 派生一个稳定的生图随机种子。
* 同项目内所有角色/形象共用同一颗种子,渲染基调(光影、配色、笔触)更趋一致;
* 不同项目派生不同种子,避免跨项目撞图。返回值落在 [0, 2_000_000_000),兼容各供应商。
*/
public static Integer styleSeed(Long projectId) {
if (projectId == null) return null;
long h = projectId;
h ^= (h >>> 32);
return (int) Math.floorMod(h * 2654435761L, 2_000_000_000L);
}
}

View File

@@ -0,0 +1,130 @@
package org.ruoyi.controller.agent;
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.agent.AgentBo;
import org.ruoyi.domain.vo.agent.AgentVo;
import org.ruoyi.domain.vo.agent.SkillOptionVo;
import org.ruoyi.service.agent.IAgentService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 智能体管理 Controller
*
* @author ruoyi team
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/agent/agent")
public class AgentController extends BaseController {
private final IAgentService agentService;
/**
* 分页查询智能体列表
*/
@SaCheckPermission("agent:agent:list")
@GetMapping("/list")
public TableDataInfo<AgentVo> list(AgentBo bo, PageQuery pageQuery) {
return agentService.queryPageList(bo, pageQuery);
}
/**
* 查询智能体列表(不分页,用于导出)
*/
@SaCheckPermission("agent:agent:list")
@GetMapping("/queryList")
public R<List<AgentVo>> queryList(AgentBo bo) {
return R.ok(agentService.queryList(bo));
}
/**
* 导出智能体列表
*/
@SaCheckPermission("agent:agent:export")
@Log(title = "智能体管理", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(AgentBo bo, HttpServletResponse response) {
List<AgentVo> list = agentService.queryList(bo);
ExcelUtil.exportExcel(list, "智能体", AgentVo.class, response);
}
/**
* 根据ID获取智能体详情
*/
@SaCheckPermission("agent:agent:query")
@GetMapping("/{id}")
public R<AgentVo> getInfo(@PathVariable Long id) {
return R.ok(agentService.queryById(id));
}
/**
* 新增智能体
*/
@SaCheckPermission("agent:agent:add")
@Log(title = "智能体管理", businessType = BusinessType.INSERT)
@RepeatSubmit
@PostMapping
public R<Void> add(@Validated @RequestBody AgentBo bo) {
return toAjax(agentService.insertByBo(bo));
}
/**
* 修改智能体
*/
@SaCheckPermission("agent:agent:edit")
@Log(title = "智能体管理", businessType = BusinessType.UPDATE)
@RepeatSubmit
@PutMapping
public R<Void> edit(@Validated @RequestBody AgentBo bo) {
return toAjax(agentService.updateByBo(bo));
}
/**
* 删除智能体
*/
@SaCheckPermission("agent:agent:remove")
@Log(title = "智能体管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@PathVariable Long[] ids) {
return toAjax(agentService.deleteByIds(List.of(ids)));
}
/**
* 用户端聊天页智能体下拉选项(启用状态,不需权限校验)
*/
@GetMapping("/agentOptions")
public R<List<AgentVo>> agentOptions() {
return R.ok(agentService.queryEnabledOptions());
}
/**
* 列出磁盘上可用的 Skills供管理端表单勾选
*/
@SaCheckPermission("agent:agent:list")
@GetMapping("/skillOptions")
public R<List<SkillOptionVo>> skillOptions() {
return R.ok(agentService.listSkillOptions());
}
}

View File

@@ -8,7 +8,10 @@ import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import org.ruoyi.common.chat.service.chat.IChatModelService; import org.ruoyi.common.chat.service.chat.IChatModelService;
import org.ruoyi.common.chat.domain.bo.chat.ChatModelBo; import org.ruoyi.common.chat.domain.bo.chat.ChatModelBo;
import org.ruoyi.common.chat.domain.bo.chat.ModelBatchKeyBo;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.common.core.utils.StringUtils;
import org.ruoyi.enums.ChatModeType;
import org.ruoyi.enums.ModelType; import org.ruoyi.enums.ModelType;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
@@ -23,6 +26,8 @@ import org.ruoyi.common.log.enums.BusinessType;
import org.ruoyi.common.excel.utils.ExcelUtil; import org.ruoyi.common.excel.utils.ExcelUtil;
import org.ruoyi.common.mybatis.core.page.TableDataInfo; import org.ruoyi.common.mybatis.core.page.TableDataInfo;
import java.util.LinkedHashMap;
/** /**
* 模型管理 * 模型管理
* *
@@ -51,10 +56,27 @@ public class ChatModelController extends BaseController {
*/ */
@GetMapping("/modelList") @GetMapping("/modelList")
public R<List<ChatModelVo>> modelList(ChatModelBo bo) { public R<List<ChatModelVo>> modelList(ChatModelBo bo) {
if (StringUtils.isBlank(bo.getCategory())) {
bo.setCategory(ModelType.CHAT.getKey()); bo.setCategory(ModelType.CHAT.getKey());
}
return R.ok(chatModelService.queryList(bo)); return R.ok(chatModelService.queryList(bo));
} }
/**
* 获取模型供应商枚举
*/
@GetMapping("/providerOptions")
public R<List<LinkedHashMap<String, String>>> providerOptions() {
List<LinkedHashMap<String, String>> options = new java.util.ArrayList<>();
for (ChatModeType type : ChatModeType.values()) {
LinkedHashMap<String, String> item = new LinkedHashMap<>();
item.put("label", type.getDescription());
item.put("value", type.getCode());
options.add(item);
}
return R.ok(options);
}
/** /**
* 导出模型管理列表 * 导出模型管理列表
*/ */
@@ -100,6 +122,17 @@ public class ChatModelController extends BaseController {
return toAjax(chatModelService.updateByBo(bo)); return toAjax(chatModelService.updateByBo(bo));
} }
/**
* 按厂商批量更新密钥
*/
@SaCheckPermission("system:model:edit")
@Log(title = "模型管理", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping("/batchKeyByProvider")
public R<Void> batchKeyByProvider(@Validated @RequestBody ModelBatchKeyBo bo) {
return toAjax(chatModelService.updateApiKeyByProvider(bo.getProviderCode(), bo.getApiKey()));
}
/** /**
* 删除模型管理 * 删除模型管理
* *

View File

@@ -0,0 +1,141 @@
package org.ruoyi.controller.chat;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.common.chat.entity.audio.AudioContext;
import org.ruoyi.common.chat.entity.image.ImageContext;
import org.ruoyi.common.chat.entity.media.MediaGenerationResponse;
import org.ruoyi.common.chat.entity.video.VideoContext;
import org.ruoyi.common.chat.factory.AudioServiceFactory;
import org.ruoyi.common.chat.factory.ImageServiceFactory;
import org.ruoyi.common.chat.factory.VideoServiceFactory;
import org.ruoyi.common.chat.service.chat.IChatModelService;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.domain.bo.media.ImageGenerationRequest;
import org.ruoyi.domain.bo.media.SpeechGenerationRequest;
import org.ruoyi.domain.bo.media.VideoGenerationRequest;
import org.ruoyi.enums.ModelType;
import org.ruoyi.service.media.AtlasPredictionService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Validated
@RestController
@RequiredArgsConstructor
@RequestMapping("/media")
public class MediaGenerationController {
private final IChatModelService chatModelService;
private final AudioServiceFactory audioServiceFactory;
private final ImageServiceFactory imageServiceFactory;
private final VideoServiceFactory videoServiceFactory;
private final AtlasPredictionService atlasPredictionService;
@PostMapping("/speech")
public R<MediaGenerationResponse> speech(@Valid @RequestBody SpeechGenerationRequest request) {
ChatModelVo model = loadModel(request.getModel(), ModelType.AUDIO.getKey());
MediaGenerationResponse response = audioServiceFactory.getOriginalService(model.getProviderCode())
.generateSpeech(AudioContext.builder()
.chatModelVo(model)
.input(request.getInput())
.voice(request.getVoice())
.responseFormat(request.getResponseFormat())
.speed(request.getSpeed())
.instructions(request.getInstructions())
.build());
return R.ok(response);
}
@PostMapping("/image")
public R<MediaGenerationResponse> image(@Valid @RequestBody ImageGenerationRequest request) {
ChatModelVo model = loadModel(request.getModel(), ModelType.IMAGE.getKey());
String result = imageServiceFactory.getOriginalService(model.getProviderCode())
.generateImage(ImageContext.builder()
.chatModelVo(model)
.prompt(request.getPrompt())
.size(request.getSize())
.seed(request.getSeed())
.build());
return R.ok(toImageResponse(result));
}
@PostMapping("/video")
public R<MediaGenerationResponse> video(@Valid @RequestBody VideoGenerationRequest request) {
ChatModelVo model = loadModel(request.getModel(), ModelType.VIDEO.getKey());
MediaGenerationResponse response = videoServiceFactory.getOriginalService(model.getProviderCode())
.generateVideo(VideoContext.builder()
.chatModelVo(model)
.prompt(request.getPrompt())
.size(request.getSize())
.seconds(request.getSeconds())
.quality(request.getQuality())
.build());
return R.ok(response);
}
@GetMapping("/video")
public R<MediaGenerationResponse> videoResult(@NotBlank(message = "模型不能为空") @RequestParam String model,
@NotBlank(message = "videoId不能为空") @RequestParam String videoId) {
ChatModelVo chatModelVo = loadModel(model, ModelType.VIDEO.getKey());
MediaGenerationResponse response = videoServiceFactory.getOriginalService(chatModelVo.getProviderCode())
.retrieveVideo(VideoContext.builder()
.chatModelVo(chatModelVo)
.prompt("retrieve")
.videoId(videoId)
.build());
return R.ok(response);
}
@GetMapping("/prediction")
public R<MediaGenerationResponse> prediction(@NotBlank(message = "模型不能为空") @RequestParam String model,
@NotBlank(message = "predictionId不能为空") @RequestParam String predictionId) {
ChatModelVo chatModelVo = chatModelService.selectModelByName(model);
if (chatModelVo == null) {
throw new IllegalArgumentException("未找到模型配置: " + model);
}
return R.ok(atlasPredictionService.retrieve(chatModelVo, predictionId));
}
private ChatModelVo loadModel(String modelName, String category) {
ChatModelVo model = chatModelService.selectModelByName(modelName);
if (model == null) {
throw new IllegalArgumentException("未找到模型配置: " + modelName);
}
if (!category.equals(model.getCategory())) {
throw new IllegalArgumentException("模型分类不匹配,期望: " + category + ", 实际: " + model.getCategory());
}
return model;
}
private MediaGenerationResponse toImageResponse(String result) {
if (result != null && result.startsWith("{")) {
try {
return atlasPredictionService.toResponse(result, "image");
} catch (Exception e) {
throw new RuntimeException("图片生成响应解析失败: " + e.getMessage(), e);
}
}
if (result != null && result.startsWith("data:")) {
String mimeType = result.substring("data:".length(), result.indexOf(";base64,"));
String b64 = result.substring(result.indexOf(";base64,") + ";base64,".length());
return MediaGenerationResponse.builder()
.type("image")
.mimeType(mimeType)
.b64Json(b64)
.dataUrl(result)
.build();
}
return MediaGenerationResponse.builder()
.type("image")
.mimeType("image/png")
.url(result)
.build();
}
}

View File

@@ -110,6 +110,17 @@ public class KnowledgeAttachController extends BaseController {
@PostMapping(value = "/upload") @PostMapping(value = "/upload")
public R<String> upload(KnowledgeInfoUploadBo bo){ public R<String> upload(KnowledgeInfoUploadBo bo){
knowledgeAttachService.upload(bo); knowledgeAttachService.upload(bo);
return R.ok("上传知识库附件成功!"); return R.ok("上传成功!");
}
/**
* 手动解析附件内容
*
* @param id 附件ID
*/
@PostMapping("/parse/{id}")
public R<Void> parse(@PathVariable Long id) {
knowledgeAttachService.parse(id);
return R.ok();
} }
} }

View File

@@ -8,6 +8,7 @@ import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import org.ruoyi.domain.bo.knowledge.KnowledgeFragmentBo; import org.ruoyi.domain.bo.knowledge.KnowledgeFragmentBo;
import org.ruoyi.domain.vo.knowledge.KnowledgeFragmentVo; import org.ruoyi.domain.vo.knowledge.KnowledgeFragmentVo;
import org.ruoyi.domain.vo.knowledge.KnowledgeRetrievalVo;
import org.ruoyi.service.knowledge.IKnowledgeFragmentService; import org.ruoyi.service.knowledge.IKnowledgeFragmentService;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
@@ -102,4 +103,12 @@ public class KnowledgeFragmentController extends BaseController {
@PathVariable Long[] ids) { @PathVariable Long[] ids) {
return toAjax(knowledgeFragmentService.deleteWithValidByIds(List.of(ids), true)); return toAjax(knowledgeFragmentService.deleteWithValidByIds(List.of(ids), true));
} }
/**
* 检索测试
*/
@PostMapping("/retrieval")
public R<List<KnowledgeRetrievalVo>> retrieval(@RequestBody KnowledgeFragmentBo bo) {
return R.ok(knowledgeFragmentService.retrieval(bo));
}
} }

View File

@@ -1,105 +0,0 @@
package org.ruoyi.controller.knowledge;
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.domain.bo.knowledge.KnowledgeGraphInstanceBo;
import org.ruoyi.domain.vo.knowledge.KnowledgeGraphInstanceVo;
import org.ruoyi.service.knowledge.IKnowledgeGraphInstanceService;
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.web.core.BaseController;
import org.ruoyi.common.mybatis.core.page.PageQuery;
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.common.mybatis.core.page.TableDataInfo;
/**
* 知识图谱实例
*
* @author ageerle
* @date 2025-12-17
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/system/graphInstance")
public class KnowledgeGraphInstanceController extends BaseController {
private final IKnowledgeGraphInstanceService knowledgeGraphInstanceService;
/**
* 查询知识图谱实例列表
*/
@SaCheckPermission("system:graphInstance:list")
@GetMapping("/list")
public TableDataInfo<KnowledgeGraphInstanceVo> list(KnowledgeGraphInstanceBo bo, PageQuery pageQuery) {
return knowledgeGraphInstanceService.queryPageList(bo, pageQuery);
}
/**
* 导出知识图谱实例列表
*/
@SaCheckPermission("system:graphInstance:export")
@Log(title = "知识图谱实例", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(KnowledgeGraphInstanceBo bo, HttpServletResponse response) {
List<KnowledgeGraphInstanceVo> list = knowledgeGraphInstanceService.queryList(bo);
ExcelUtil.exportExcel(list, "知识图谱实例", KnowledgeGraphInstanceVo.class, response);
}
/**
* 获取知识图谱实例详细信息
*
* @param id 主键
*/
@SaCheckPermission("system:graphInstance:query")
@GetMapping("/{id}")
public R<KnowledgeGraphInstanceVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(knowledgeGraphInstanceService.queryById(id));
}
/**
* 新增知识图谱实例
*/
@SaCheckPermission("system:graphInstance:add")
@Log(title = "知识图谱实例", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody KnowledgeGraphInstanceBo bo) {
return toAjax(knowledgeGraphInstanceService.insertByBo(bo));
}
/**
* 修改知识图谱实例
*/
@SaCheckPermission("system:graphInstance:edit")
@Log(title = "知识图谱实例", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody KnowledgeGraphInstanceBo bo) {
return toAjax(knowledgeGraphInstanceService.updateByBo(bo));
}
/**
* 删除知识图谱实例
*
* @param ids 主键串
*/
@SaCheckPermission("system:graphInstance:remove")
@Log(title = "知识图谱实例", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(knowledgeGraphInstanceService.deleteWithValidByIds(List.of(ids), true));
}
}

View File

@@ -1,105 +0,0 @@
package org.ruoyi.controller.knowledge;
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.domain.bo.knowledge.KnowledgeGraphSegmentBo;
import org.ruoyi.domain.vo.knowledge.KnowledgeGraphSegmentVo;
import org.ruoyi.service.knowledge.IKnowledgeGraphSegmentService;
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.web.core.BaseController;
import org.ruoyi.common.mybatis.core.page.PageQuery;
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.common.mybatis.core.page.TableDataInfo;
/**
* 知识图谱片段
*
* @author ageerle
* @date 2025-12-17
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/system/graphSegment")
public class KnowledgeGraphSegmentController extends BaseController {
private final IKnowledgeGraphSegmentService knowledgeGraphSegmentService;
/**
* 查询知识图谱片段列表
*/
@SaCheckPermission("system:graphSegment:list")
@GetMapping("/list")
public TableDataInfo<KnowledgeGraphSegmentVo> list(KnowledgeGraphSegmentBo bo, PageQuery pageQuery) {
return knowledgeGraphSegmentService.queryPageList(bo, pageQuery);
}
/**
* 导出知识图谱片段列表
*/
@SaCheckPermission("system:graphSegment:export")
@Log(title = "知识图谱片段", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(KnowledgeGraphSegmentBo bo, HttpServletResponse response) {
List<KnowledgeGraphSegmentVo> list = knowledgeGraphSegmentService.queryList(bo);
ExcelUtil.exportExcel(list, "知识图谱片段", KnowledgeGraphSegmentVo.class, response);
}
/**
* 获取知识图谱片段详细信息
*
* @param id 主键
*/
@SaCheckPermission("system:graphSegment:query")
@GetMapping("/{id}")
public R<KnowledgeGraphSegmentVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(knowledgeGraphSegmentService.queryById(id));
}
/**
* 新增知识图谱片段
*/
@SaCheckPermission("system:graphSegment:add")
@Log(title = "知识图谱片段", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody KnowledgeGraphSegmentBo bo) {
return toAjax(knowledgeGraphSegmentService.insertByBo(bo));
}
/**
* 修改知识图谱片段
*/
@SaCheckPermission("system:graphSegment:edit")
@Log(title = "知识图谱片段", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody KnowledgeGraphSegmentBo bo) {
return toAjax(knowledgeGraphSegmentService.updateByBo(bo));
}
/**
* 删除知识图谱片段
*
* @param ids 主键串
*/
@SaCheckPermission("system:graphSegment:remove")
@Log(title = "知识图谱片段", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(knowledgeGraphSegmentService.deleteWithValidByIds(List.of(ids), true));
}
}

Some files were not shown because too many files have changed in this diff Show More