mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-09-12 16:05:05 +00:00
feat: streamline workflow orchestration and chat routing
Add Zhipu web search integration, remove obsolete workflow nodes and resume handling, and separate model, agent, and workflow chat execution.
This commit is contained in:
@@ -1,124 +0,0 @@
|
||||
# FastJson 安全漏洞修复报告
|
||||
|
||||
## 修复概述
|
||||
- **修复日期**: 2026-07-29
|
||||
- **漏洞等级**: 🔴 Critical (严重)
|
||||
- **修复状态**: ✅ 已完成
|
||||
|
||||
## 漏洞描述
|
||||
项目使用的 FastJson 1.2.83 版本存在严重的反序列化远程代码执行(RCE)漏洞,包括:
|
||||
- CVE-2022-25845
|
||||
- CVE-2023-21931
|
||||
- 多个未公开的反序列化漏洞
|
||||
|
||||
攻击者可通过构造恶意JSON实现远程代码执行,具有极高的安全风险。
|
||||
|
||||
## 修复方案
|
||||
完全移除 FastJson 依赖,替换为 Spring Boot 内置的 Jackson 库。
|
||||
|
||||
## 修复详情
|
||||
|
||||
### 1. POM 依赖修改
|
||||
|
||||
#### 根 pom.xml
|
||||
- 删除: fastjson.version 属性定义
|
||||
- 删除: fastjson 依赖声明
|
||||
- 状态: ✅ 已完成
|
||||
|
||||
#### ruoyi-common-chat/pom.xml
|
||||
- 删除: fastjson 依赖
|
||||
- 新增: jackson-databind 依赖
|
||||
- 状态: ✅ 已完成
|
||||
|
||||
### 2. Java 代码修改
|
||||
|
||||
共修改了 **6个Java文件**,替换所有 FastJson API 为 Jackson API。
|
||||
|
||||
#### 修改文件列表:
|
||||
1. ✅ QwenFileUploadUtils.java - 千问文件上传工具
|
||||
2. ✅ ChatRequest.java - 聊天请求对象
|
||||
3. ✅ MailSendNode.java - 邮件发送节点
|
||||
4. ✅ SwitcherNode.java - 条件分支节点
|
||||
5. ✅ AbstractAuthWeChatEnterpriseRequest.java - 企业微信登录
|
||||
6. ✅ AuthDingTalkV2Request.java - 钉钉登录
|
||||
|
||||
### 3. API 替换对照表
|
||||
|
||||
| 操作 | FastJson | Jackson |
|
||||
|------|----------|---------|
|
||||
| 解析JSON | JSONObject.parseObject(str) | objectMapper.readTree(str) |
|
||||
| 获取字符串 | json.getString("key") | json.get("key").asText() |
|
||||
| 获取整数 | json.getIntValue("key") | json.get("key").asInt() |
|
||||
| 判断包含 | json.containsKey("key") | json.has("key") |
|
||||
| 对象转JSON | JSON.toJSONString(obj) | objectMapper.writeValueAsString(obj) |
|
||||
|
||||
## 特殊说明 - JustAuth库兼容
|
||||
|
||||
由于第三方 JustAuth 库的 AuthUser.rawUserInfo 字段需要 FastJson 的 JSONObject 类型,
|
||||
在两个社交登录文件中保留了最小化的 FastJson 使用:
|
||||
|
||||
- AbstractAuthWeChatEnterpriseRequest.java
|
||||
- AuthDingTalkV2Request.java
|
||||
|
||||
**使用方式**: 仅用于格式转换(Jackson JsonNode → FastJson JSONObject)
|
||||
**安全性**: ✅ 不涉及反序列化,仅数据转换,安全可控
|
||||
|
||||
## 验证结果
|
||||
|
||||
### 编译验证
|
||||
```
|
||||
mvn clean compile -DskipTests
|
||||
```
|
||||
**结果**: ✅ BUILD SUCCESS (所有38个模块编译通过)
|
||||
**耗时**: 01:26 min
|
||||
|
||||
### 代码检查
|
||||
- FastJson 导入残留: 0个(除兼容性转换)
|
||||
- POM 依赖残留: 0个
|
||||
|
||||
## 安全提升对比
|
||||
|
||||
### 修复前
|
||||
- ❌ FastJson 1.2.83 (严重RCE漏洞)
|
||||
- ❌ 全局攻击面暴露
|
||||
- ❌ 可被恶意JSON远程执行代码
|
||||
|
||||
### 修复后
|
||||
- ✅ Jackson 2.18.2 (Spring Boot内置,安全稳定)
|
||||
- ✅ 移除反序列化RCE攻击面
|
||||
- ✅ 显著提升系统安全性
|
||||
- ⚠️ 保留最小化FastJson使用(仅格式转换)
|
||||
|
||||
## 受影响的功能模块
|
||||
|
||||
1. ✅ 千问文件上传
|
||||
2. ✅ 聊天请求处理
|
||||
3. ✅ 工作流邮件发送
|
||||
4. ✅ 工作流条件分支
|
||||
5. ✅ 企业微信登录
|
||||
6. ✅ 钉钉登录
|
||||
|
||||
**测试建议**: 重点测试以上功能模块的JSON处理和社交登录功能
|
||||
|
||||
## 后续优化建议
|
||||
|
||||
1. **监控JustAuth更新**: 等待其支持Jackson后完全移除FastJson
|
||||
2. **功能测试**: 进行完整的回归测试
|
||||
3. **安全监控**: 关注Jackson的安全更新
|
||||
|
||||
## 总结
|
||||
|
||||
✅ **修复完成度**: 95%
|
||||
- 主要业务代码: 100% 完成
|
||||
- 第三方库兼容: 保留最小化使用
|
||||
|
||||
🎯 **安全成果**:
|
||||
- 消除了 FastJson 1.2.83 的严重RCE漏洞
|
||||
- 提升了整体系统安全防护能力
|
||||
- 所有修改已通过编译验证
|
||||
|
||||
---
|
||||
|
||||
**修复人员**: Claude Code AI
|
||||
**审核状态**: ✅ 待人工审核
|
||||
**建议操作**: 合并前进行完整功能测试
|
||||
@@ -1,64 +0,0 @@
|
||||
# RAG 完整修复与全量验收报告
|
||||
|
||||
验收时间:2026-07-21(Asia/Shanghai)
|
||||
验收对象:当前未提交工作区(保留原有改动)
|
||||
|
||||
## 结论
|
||||
|
||||
计划内的 1–15 项工程缺陷已完成代码修复,默认 Maven 构建已从“跳过测试”改为真实执行测试。全仓 37 个 reactor 模块测试成功,`ruoyi-chat` 49/49 通过,两个前端生产构建通过,`git diff --check` 通过。
|
||||
|
||||
本机已运行 MySQL、Redis、MinIO 和 Weaviate 1.30.0;Milvus/Qdrant 容器以及有效的 embedding/chat/rerank provider 凭证不存在,因此这三项真实 provider/存储引擎冒烟被标记为环境限制,不影响确定性代码验收。
|
||||
|
||||
## 1–15 项验收
|
||||
|
||||
| # | 状态 | 修复/证据 |
|
||||
|---|---|---|
|
||||
| 1 | 通过 | Markdown/Java/字符分片的边界、空文档、超长块回归通过。 |
|
||||
| 2 | 通过 | Supervisor 每轮仅保留一个 RAG 入口,不再用已含 RAG 的 prompt 重复检索。 |
|
||||
| 3 | 通过 | 历史消息进入 Supervisor prompt,检索 query 与最终 prompt 分离。 |
|
||||
| 4 | 通过 | `fid` 稳定 ID 贯穿 DB/三种向量库/RRF,融合去重回归通过。 |
|
||||
| 5 | 通过 | aiflow vector/hybrid 复用统一检索服务;graph 明确返回不支持,不再伪装为 vector。 |
|
||||
| 6 | 通过 | 重解析改为先写新 fid、再清旧向量、最后替换 DB;失败补偿新向量;删片段/附件/库遇向量删除失败即中止。 |
|
||||
| 7 | 通过 | embedding/rerank provider 使用 prototype 实例,工厂缓存可按模型刷新,避免跨配置污染。 |
|
||||
| 8 | 通过 | `similarityThreshold` 仅用于粗召回;`rerankScoreThreshold` 仅在 rerank 真实成功后生效,回归测试通过。 |
|
||||
| 9 | 通过 | 默认配置和 Compose 统一为 Weaviate 1.30.0、`28080:8080`。 |
|
||||
| 10 | 通过 | 三种策略均使用 `embedAll`;Weaviate batch objects、Milvus `addAll`、Qdrant `addAll`。 |
|
||||
| 11 | 通过 | upload/parse/retrieval 权限保留,parse/retrieval 增加分布式防重复提交,upload 由现有知识库+文件名唯一约束兜底。 |
|
||||
| 12 | 通过 | 分隔符使用字面量语义,`|`/`.`/`*` 回归通过。 |
|
||||
| 13 | 通过 | hybrid 通道失败可降级到 vector;所有可用通道都失败时抛出明确业务异常。 |
|
||||
| 14 | 通过 | Weaviate client 稳定懒加载单例;schema 仅在已存在或创建成功后进入缓存。 |
|
||||
| 15 | 通过 | 工厂新增严格 `getStrategy(type)`,知识库 `vectorModel` 优先,空值才回退全局,非法值直接报错。 |
|
||||
|
||||
## 其他完成项
|
||||
|
||||
- 多知识库并行检索,按 `kid + docId + fid` 去重,统一上限和字符预算。
|
||||
- 5 分钟短 TTL 检索缓存,key 覆盖检索参数,知识数据变更主动失效。
|
||||
- rerank 仅保留 provider 实际返回的文档。
|
||||
- 知识库文档数改为 group-by 查询,消除该 N+1。
|
||||
- Milvus/Qdrant/Weaviate 的删 collection/doc/fid 语义对齐;Milvus 删库改为 drop collection。
|
||||
- MCP `npx` 根据操作系统解析,支持系统属性/环境变量覆盖。
|
||||
- `fid` 非空唯一、`doc_id varchar(32)`、租户/用户索引与可重复执行迁移脚本已提供。
|
||||
- 用户端聊天页已接入知识库列表和最小选择器。
|
||||
|
||||
## 测试记录
|
||||
|
||||
| 检查 | 结果 |
|
||||
|---|---|
|
||||
| `mvn -Pdev test` | 37/37 reactor 模块 SUCCESS;`ruoyi-chat` 49/49 |
|
||||
| `mvn -Pdev -pl ruoyi-modules/ruoyi-aiflow -am -DskipTests compile` | 21/21 SUCCESS |
|
||||
| `ruoyi-web: pnpm build` | SUCCESS,2621 modules transformed |
|
||||
| `ruoyi-admin: pnpm build` | SUCCESS,10/10 build tasks |
|
||||
| `git diff --check` | SUCCESS,无空白错误 |
|
||||
| Weaviate/MySQL/Redis/MinIO | Docker 服务运行,Weaviate 1.30.0 映射 28080 |
|
||||
| 三向量库 Docker 集成 | SUCCESS;Weaviate 1.30.0、Milvus 2.5.7、Qdrant 1.17.0 真实写入/检索/删除测试 3/3 通过 |
|
||||
| 真实 embedding/chat/rerank | 环境限制:当前配置为无效/占位凭证 |
|
||||
|
||||
本轮未创建新的 `codex_rag_verify_` 持久化数据;上一轮验收数据已清理,未动现有非测试数据。
|
||||
|
||||
## 2026-07-21 三向量库 Docker 补充验收
|
||||
|
||||
- 启动并保留 `ruoyi-rag-milvus`、`ruoyi-rag-milvus-etcd`、`ruoyi-rag-milvus-minio`、`ruoyi-rag-qdrant`,四个容器健康检查均为 `healthy`。
|
||||
- Milvus 专用 MinIO 仅在 Docker 内网可达,没有占用宿主机 9000/9001;Milvus 映射 19530/9091,Qdrant 映射 6333/6334。
|
||||
- `ThreeVectorStoresDockerIT` 使用 32 维确定性 embedding,对三库逐一验证 batch write、vector search、fid delete、docId delete 和 drop collection,3/3 通过。
|
||||
- 首轮测试发现 Milvus `autoFlush=false` 导致批量写入后不可立即检索、元数据删除不可立即见;改为写入和删除返回前 flush 后通过。
|
||||
- 清理后 Weaviate/Qdrant 的 `CodexRagVerify*` collection 计数均为 0,Milvus collection 也由测试 finally 成功 drop;本轮未写入 MySQL 或 OSS 测试数据。
|
||||
252
README.md
252
README.md
@@ -17,236 +17,235 @@
|
||||
|
||||
<img src="docs/image/logo.png" alt="RuoYi AI Logo" width="120" height="120">
|
||||
|
||||
### 企业级AI助手平台
|
||||
### Enterprise-Grade AI Assistant Platform
|
||||
|
||||
*开箱即用的全栈AI平台,支持多智能体协同、Supervisor模式编排、多种决策模式、RAG技术和流程编排能力*
|
||||
*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*
|
||||
|
||||
**[English](README_EN.md)** | **[📖 使用文档](https://doc.ruoyiai.chat/)** |
|
||||
**[🚀 在线体验](https://web.ruoyiai.chat/)** | **[🐛 问题反馈](https://github.com/ageerle/ruoyi-ai/issues)** | **[💡 功能建议](https://github.com/ageerle/ruoyi-ai/issues)**
|
||||
**[中文](README_ZH.md)** | **[📖 Documentation](https://doc.ruoyiai.chat/)** |
|
||||
**[🚀 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>
|
||||
|
||||
|
||||
## ✨ 核心亮点
|
||||
|
||||
| 模块 | 现有能力
|
||||
|:---------:|---
|
||||
| **模型管理** | 多模型接入(DeepSeek/智谱/MIMO/百炼/OpenAI)、多模态理解、Coze/DIFY/FastGPT/RAGFlow平台集成
|
||||
| **知识管理** | 本地RAG + 向量库(Milvus/Weaviate/Qdrant) + 文档解析
|
||||
| **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态
|
||||
| **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点
|
||||
| **智能体管理** | 基于Langchain4j的Agent框架、Supervisor模式编排,支持多种决策模型,可以灵活搭配工具,skills
|
||||
|
||||
|
||||
### 项目源码
|
||||
## ✨ Core Features
|
||||
|
||||
| 项目模块 | GitHub 仓库 | Gitee 仓库 | GitCode 仓库 |
|
||||
| Module | Current Capabilities |
|
||||
|:---:|---|
|
||||
| **Model Management** | Multi-model integration (DeepSeek/Zhipu/MIMO/Bailian/OpenAI), multi-modal understanding, Coze/DIFY/FastGPT/RAGFlow platform integration |
|
||||
| **Knowledge Management** | Local RAG + Vector DB (Milvus/Weaviate/Qdrant) + Document parsing |
|
||||
| **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, and other nodes |
|
||||
| **Multi-Agent** | Agent framework based on Langchain4j, Supervisor mode orchestration, supports multiple decision models, can flexibly combine tools and skills |
|
||||
|
||||
### Project Repositories
|
||||
|
||||
| Module | GitHub Repository | Gitee Repository | GitCode Repository |
|
||||
|----------|-------------------------------------------------------|------------------------------------------------------|--------------------------------------------------------|
|
||||
| 🔧 后端服务 | [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-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) |
|
||||
| 🤖 编程助手 | [ruoyi-copilot](https://github.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitee.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitcode.com/ageerle/ruoyi-copilot) |
|
||||
| 📱 小程序端 | [ruoyi-uniapp](https://github.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitee.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitcode.com/ageerle/ruoyi-uniapp) |
|
||||
| 🔧 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) |
|
||||
| 🎬 Drama | [ruoyi-drama](https://github.com/ageerle/ruoyi-drama) | [ruoyi-drama](https://gitee.com/ageerle/ruoyi-drama) | [ruoyi-drama](https://gitcode.com/ageerle/ruoyi-drama) |
|
||||
| 🤖 Copilot | [ruoyi-copilot](https://github.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitee.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitcode.com/ageerle/ruoyi-copilot) |
|
||||
| 📱 Mini-App | [ruoyi-uniapp](https://github.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitee.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitcode.com/ageerle/ruoyi-uniapp) |
|
||||
|
||||
### 合作项目
|
||||
| 项目名称 | GitHub 仓库 | Gitee 仓库
|
||||
### 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) |
|
||||
| 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
|
||||
|
||||
### 核心框架
|
||||
- **后端架构**:Spring Boot 3.5.8 + Langchain4j
|
||||
- **数据存储**:MySQL 8.0 + Redis + 向量数据库(Milvus/Weaviate/Qdrant)
|
||||
- **前端技术**:Vue 3 + Vben Admin + element-plus-x
|
||||
- **安全认证**:Sa-Token + JWT 双重保障
|
||||
- **文档处理**:PDF、Word、Excel 解析,图像智能分析
|
||||
- **实时通信**:WebSocket 实时通信,SSE 流式响应
|
||||
- **系统监控**:完善的日志体系、性能监控、服务健康检查
|
||||
### Core Framework
|
||||
- **Backend**: Spring Boot 3.5.8 + Langchain4j
|
||||
- **Data Storage**: MySQL 8.0 + Redis + Vector Databases (Milvus/Weaviate/Qdrant)
|
||||
- **Frontend**: Vue 3 + Vben Admin + element-plus-x
|
||||
- **Security**: Sa-Token + JWT dual-layer security
|
||||
- **Document Processing**: PDF, Word, Excel parsing, intelligent image analysis
|
||||
- **Real-time Communication**: WebSocket real-time communication, SSE streaming response
|
||||
- **System Monitoring**: Comprehensive logging system, performance monitoring, service health checks
|
||||
|
||||
## 🐳 Docker 部署
|
||||
## 🐳 Docker Deployment
|
||||
|
||||
本项目提供两种 Docker 部署方式:
|
||||
This project provides two Docker deployment methods:
|
||||
|
||||
### 方式一:一键启动所有服务(推荐)
|
||||
### Method 1: One-click Start All Services (Recommended)
|
||||
|
||||
使用 `docker-compose-all.yaml` 可以一键启动所有服务(包括后端、管理端、用户端及依赖服务):
|
||||
Use `docker-compose-all.yaml` to start all services at once (including backend, admin panel, user frontend, and dependencies):
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
# Clone the repository
|
||||
git clone https://github.com/ageerle/ruoyi-ai.git
|
||||
cd ruoyi-ai
|
||||
|
||||
# 启动所有服务(从镜像仓库拉取预构建镜像)
|
||||
# Start all services (pull pre-built images from registry)
|
||||
docker-compose -f docker-compose-all.yaml up -d
|
||||
|
||||
# 查看服务状态
|
||||
# Check service status
|
||||
docker-compose -f docker-compose-all.yaml ps
|
||||
|
||||
# 访问服务
|
||||
# 管理端: http://localhost:25666 (admin / admin123)
|
||||
# 用户端: http://localhost:25137
|
||||
# 后端API: http://localhost:26039
|
||||
# Access services
|
||||
# Admin Panel: http://localhost:25666 (admin / admin123)
|
||||
# User Frontend: http://localhost:25137
|
||||
# Backend API: http://localhost:26039
|
||||
```
|
||||
|
||||
### 方式二:分步部署(源码编译)
|
||||
### Method 2: Step-by-step Deployment (Source Build)
|
||||
|
||||
如果您需要从源码构建后端服务,请按照以下步骤操作:
|
||||
If you need to build backend services from source, follow these steps:
|
||||
|
||||
#### 第一步:部署后端服务
|
||||
#### Step 1: Deploy Backend Service
|
||||
|
||||
```bash
|
||||
# 进入后端项目目录
|
||||
# Enter backend project directory
|
||||
cd ruoyi-ai
|
||||
|
||||
# 启动后端服务(源码编译构建)
|
||||
# Start backend service (build from source)
|
||||
docker-compose up -d --build
|
||||
|
||||
# 等待后端服务启动完成
|
||||
# Wait for backend service to start
|
||||
docker-compose logs -f backend
|
||||
```
|
||||
|
||||
#### 第二步:部署管理端
|
||||
#### Step 2: Deploy Admin Panel
|
||||
|
||||
```bash
|
||||
# 进入管理端项目目录
|
||||
# Enter admin panel project directory
|
||||
cd ruoyi-admin
|
||||
|
||||
# 构建并启动管理端
|
||||
# Build and start admin panel
|
||||
docker-compose up -d --build
|
||||
|
||||
# 访问管理端
|
||||
# 地址: http://localhost:5666
|
||||
# Access admin panel
|
||||
# URL: http://localhost:5666
|
||||
```
|
||||
|
||||
#### 第三步:部署用户端(可选)
|
||||
#### Step 3: Deploy User Frontend (Optional)
|
||||
|
||||
```bash
|
||||
# 进入用户端项目目录
|
||||
# Enter user frontend project directory
|
||||
cd ruoyi-web
|
||||
|
||||
# 构建并启动用户端
|
||||
# Build and start user frontend
|
||||
docker-compose up -d --build
|
||||
|
||||
# 访问用户端
|
||||
# 地址: http://localhost:5137
|
||||
# Access user frontend
|
||||
# URL: http://localhost:5137
|
||||
```
|
||||
|
||||
### 服务端口说明
|
||||
### Service Ports
|
||||
|
||||
| 服务 | 一键启动端口 | 分步部署端口 | 说明 |
|
||||
| Service | One-click Port | Step-by-step Port | Description |
|
||||
|------|-------------|-------------|------|
|
||||
| 管理端 | 25666 | 5666 | 管理后台访问地址 |
|
||||
| 用户端 | 25137 | 5137 | 用户前端访问地址 |
|
||||
| 后端服务 | 26039 | 6039 | 后端 API 服务 |
|
||||
| MySQL | 23306 | 23306 | 数据库服务 |
|
||||
| Redis | 26379 | 6379 | 缓存服务 |
|
||||
| Weaviate | 28080 | 28080 | 向量数据库 |
|
||||
| MinIO API | 29000 | 9000 | 对象存储 API |
|
||||
| MinIO Console | 29090 | 9090 | 对象存储控制台 |
|
||||
| Admin Panel | 25666 | 5666 | Admin backend access |
|
||||
| User Frontend | 25137 | 5137 | User frontend access |
|
||||
| Backend Service | 26039 | 6039 | Backend API service |
|
||||
| MySQL | 23306 | 23306 | Database service |
|
||||
| Redis | 26379 | 6379 | Cache service |
|
||||
| Weaviate | 28080 | 28080 | Vector database |
|
||||
| MinIO API | 29000 | 9000 | Object storage API |
|
||||
| MinIO Console | 29090 | 9090 | Object storage console |
|
||||
|
||||
### 镜像仓库
|
||||
### Image Registry
|
||||
|
||||
所有镜像托管在阿里云容器镜像服务:
|
||||
All images are hosted on Alibaba Cloud Container Registry:
|
||||
|
||||
```
|
||||
crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai
|
||||
```
|
||||
|
||||
可用镜像:
|
||||
- `mysql:v3` - MySQL 数据库(包含初始化 SQL)
|
||||
- `redis:6.2` - Redis 缓存
|
||||
- `weaviate:1.30.0` - 向量数据库
|
||||
- `minio:latest` - 对象存储
|
||||
- `ruoyi-ai-backend:latest` - 后端服务
|
||||
- `ruoyi-ai-admin:latest` - 管理端前端
|
||||
- `ruoyi-ai-web:latest` - 用户端前端
|
||||
Available images:
|
||||
- `mysql:v3` - MySQL database (includes initialization SQL)
|
||||
- `redis:6.2` - Redis cache
|
||||
- `weaviate:1.30.0` - Vector database
|
||||
- `minio:latest` - Object storage
|
||||
- `ruoyi-ai-backend:latest` - Backend service
|
||||
- `ruoyi-ai-admin:latest` - Admin frontend
|
||||
- `ruoyi-ai-web:latest` - User frontend
|
||||
|
||||
### 常用命令
|
||||
### Common Commands
|
||||
|
||||
```bash
|
||||
# 停止所有服务
|
||||
# Stop all services
|
||||
docker-compose -f docker-compose-all.yaml down
|
||||
|
||||
# 查看服务日志
|
||||
docker-compose -f docker-compose-all.yaml logs -f [服务名]
|
||||
# View service logs
|
||||
docker-compose -f docker-compose-all.yaml logs -f [service-name]
|
||||
|
||||
# 重启某个服务
|
||||
docker-compose -f docker-compose-all.yaml restart [服务名]
|
||||
# Restart a service
|
||||
docker-compose -f docker-compose-all.yaml restart [service-name]
|
||||
```
|
||||
|
||||
## 📚 使用文档
|
||||
## 📚 Documentation
|
||||
|
||||
想要深入了解安装部署、功能配置和二次开发?
|
||||
Want to learn more about installation, deployment, configuration, and secondary development?
|
||||
|
||||
**👉 [完整使用文档](https://doc.ruoyiai.chat/)**
|
||||
**👉 [Complete Documentation](https://doc.ruoyiai.chat/)**
|
||||
|
||||
## 🤝 参与贡献
|
||||
## 🤝 Contributing
|
||||
|
||||
我们热烈欢迎社区贡献!无论您是资深开发者还是初学者,都可以为项目贡献力量 💪
|
||||
We warmly welcome community contributions! Whether you are a seasoned developer or just getting started, you can contribute to the project 💪
|
||||
|
||||
### 贡献方式
|
||||
### How to Contribute
|
||||
|
||||
1. **Fork** 项目到您的账户
|
||||
2. **创建分支** (`git checkout -b feature/新功能名称`)
|
||||
3. **提交代码** (`git commit -m '添加某某功能'`)
|
||||
4. **推送分支** (`git push origin feature/新功能名称`)
|
||||
5. **发起 Pull Request**
|
||||
1. **Fork** the project to your account
|
||||
2. **Create a branch** (`git checkout -b feature/new-feature-name`)
|
||||
3. **Commit your changes** (`git commit -m 'Add new feature'`)
|
||||
4. **Push to the branch** (`git push origin feature/new-feature-name`)
|
||||
5. **Create a Pull Request**
|
||||
|
||||
> 💡 **小贴士**:建议将 PR 提交到 GitHub,我们会自动同步到其他代码托管平台
|
||||
> 💡 **Tip**: We recommend submitting PRs to GitHub, we will automatically sync to other code hosting platforms
|
||||
|
||||
## 📄 开源协议
|
||||
## 📄 License
|
||||
|
||||
本项目采用 **MIT 开源协议**,详情请查看 [LICENSE](LICENSE) 文件。
|
||||
This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## 🙏 特别鸣谢
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
感谢以下优秀的开源项目为本项目提供支持:
|
||||
- [Langchain4j](https://github.com/langchain4j/langchain4j) - 强大的 Java LLM 开发框架
|
||||
- [RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus) - 成熟的企业级快速开发框架
|
||||
- [Vben Admin](https://github.com/vbenjs/vue-vben-admin) - 现代化的 Vue 后台管理模板
|
||||
Thanks to the following excellent open-source projects for their support:
|
||||
- [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
|
||||
- [Vben Admin](https://github.com/vbenjs/vue-vben-admin) - Modern Vue admin template
|
||||
|
||||
## 💎 Sponsors
|
||||
|
||||
## 💎 赞助商
|
||||
|
||||
**感谢以下赞助商对本项目的支持:**
|
||||
**Thanks to the following sponsors for supporting this project:**
|
||||
|
||||
<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+ 精选模型**。
|
||||
[Visit Atlas Cloud](https://www.atlascloud.ai?ref=89F97E&utm_source=github&utm_campaign=ruoyi-drama) · [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="火山引擎 CodingPlan" width="160" height="80">
|
||||
<img src="docs/image/sponsor/huoshan.png" alt="Volcengine 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 复杂长程任务!
|
||||
[Sign up to claim 25 million tokens — go now](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)
|
||||
Enjoy ByteDance's in-house Doubao models plus full-power open-source SOTA models, covering text, VLM, and image generation — all modalities in one stop: Seed-2.1, Seedream-5.0, GLM-5.2, DeepSeek, and more. Not just for coding — it can also tackle complex long-horizon Agent tasks!
|
||||
|
||||
|
||||
## 💬 社区交流
|
||||
## 💬 Community Chat
|
||||
|
||||
<div align="center">
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="docs/image/wx.png" alt="微信二维码" width="200" height="200"><br>
|
||||
<strong>扫码添加作者微信</strong><br>
|
||||
<em>邀请进群学习</em>
|
||||
<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/wx06.png" alt="微信二维码" width="200" height="200"><br>
|
||||
<strong>微信技术交流群</strong><br>
|
||||
<em>技术讨论</em>
|
||||
<img src="docs/image/wx06.png" alt="WeChat QR Code" width="200" height="200"><br>
|
||||
<strong>WeChat Tech Exchange Group</strong><br>
|
||||
<em>Technical discussion</em>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="docs/image/qq.png" alt="QQ群二维码" width="200" height="200"><br>
|
||||
<strong>QQ技术交流群</strong><br>
|
||||
<em>技术讨论</em>
|
||||
<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>
|
||||
@@ -255,11 +254,12 @@ docker-compose -f docker-compose-all.yaml restart [服务名]
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
**[⭐ 点个Star支持一下](https://github.com/ageerle/ruoyi-ai)** • **[ Fork 开始贡献](https://github.com/ageerle/ruoyi-ai/fork)** • **[📚 English](README_EN.md)** • **[📖 查看完整文档](https://doc.ruoyiai.chat/)**
|
||||
**[⭐ Star to Support](https://github.com/ageerle/ruoyi-ai)** • **[Fork to Contribute](https://github.com/ageerle/ruoyi-ai/fork)** • **[📚 中文](README_ZH.md)** • **[📖 Complete Documentation](https://doc.ruoyiai.chat/)**
|
||||
|
||||
*用 ❤️ 打造,由 RuoYi AI 开源社区维护*
|
||||
*Built with ❤️, maintained by the RuoYi AI open-source community*
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
310
README_EN.md
310
README_EN.md
@@ -1,310 +0,0 @@
|
||||
|
||||
# RuoYi AI
|
||||
|
||||
<div align="center">
|
||||
|
||||
[![Contributors][contributors-shield]][contributors-url]
|
||||
[![Forks][forks-shield]][forks-url]
|
||||
[![Stargazers][stars-shield]][stars-url]
|
||||
[![Issues][issues-shield]][issues-url]
|
||||
[![MIT License][license-shield]][license-url]
|
||||
|
||||
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/13209">
|
||||
<img src="https://trendshift.io/api/badge/repositories/13209" alt="GitHub Trending">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<img src="docs/image/logo.png" alt="RuoYi AI Logo" width="120" height="120">
|
||||
|
||||
### Enterprise-Grade AI Assistant Platform
|
||||
|
||||
*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.ruoyiai.chat/)** |
|
||||
**[🚀 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>
|
||||
|
||||
|
||||
|
||||
|
||||
## ✨ Core Features
|
||||
|
||||
| Module | Current Capabilities |
|
||||
|:---:|---|
|
||||
| **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 |
|
||||
| **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 |
|
||||
| **Multi-Agent** | Agent framework based on Langchain4j, Supervisor mode orchestration, supports multiple decision models |
|
||||
|
||||
### 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) |
|
||||
| 🎬 Drama | [ruoyi-drama](https://github.com/ageerle/ruoyi-drama) | [ruoyi-drama](https://gitee.com/ageerle/ruoyi-drama) | |
|
||||
| 🤖 Copilot | [ruoyi-copilot](https://github.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitee.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitcode.com/ageerle/ruoyi-copilot) |
|
||||
| 📱 Mini-App | [ruoyi-uniapp](https://github.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitee.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitcode.com/ageerle/ruoyi-uniapp) |
|
||||
|
||||
### 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.8 + Langchain4j
|
||||
- **Data Storage**: MySQL 8.0 + Redis + Vector Databases (Milvus/Weaviate/Qdrant)
|
||||
- **Frontend**: Vue 3 + Vben Admin + element-plus-x
|
||||
- **Security**: Sa-Token + JWT dual-layer security
|
||||
- **Document Processing**: PDF, Word, Excel parsing, intelligent image analysis
|
||||
- **Real-time Communication**: WebSocket real-time communication, SSE streaming response
|
||||
- **System Monitoring**: Comprehensive logging system, performance monitoring, service health checks
|
||||
|
||||
## 🐳 Docker Deployment
|
||||
|
||||
This project provides two Docker deployment methods:
|
||||
|
||||
### Method 1: One-click Start All Services (Recommended)
|
||||
|
||||
Use `docker-compose-all.yaml` to start all services at once (including backend, admin panel, user frontend, and dependencies):
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/ageerle/ruoyi-ai.git
|
||||
cd ruoyi-ai
|
||||
|
||||
# Start all services (pull pre-built images from registry)
|
||||
docker-compose -f docker-compose-all.yaml up -d
|
||||
|
||||
# Check service status
|
||||
docker-compose -f docker-compose-all.yaml ps
|
||||
|
||||
# Access services
|
||||
# Admin Panel: http://localhost:25666 (admin / admin123)
|
||||
# User Frontend: http://localhost:25137
|
||||
# Backend API: http://localhost:26039
|
||||
```
|
||||
|
||||
### Method 2: Step-by-step Deployment (Source Build)
|
||||
|
||||
If you need to build backend services from source, follow these steps:
|
||||
|
||||
#### Step 1: Deploy Backend Service
|
||||
|
||||
```bash
|
||||
# Enter backend project directory
|
||||
cd ruoyi-ai
|
||||
|
||||
# Start backend service (build from source)
|
||||
docker-compose up -d --build
|
||||
|
||||
# Wait for backend service to start
|
||||
docker-compose logs -f backend
|
||||
```
|
||||
|
||||
#### Step 2: Deploy Admin Panel
|
||||
|
||||
```bash
|
||||
# Enter admin panel project directory
|
||||
cd ruoyi-admin
|
||||
|
||||
# Build and start admin panel
|
||||
docker-compose up -d --build
|
||||
|
||||
# Access admin panel
|
||||
# URL: http://localhost:5666
|
||||
```
|
||||
|
||||
#### Step 3: Deploy User Frontend (Optional)
|
||||
|
||||
```bash
|
||||
# Enter user frontend project directory
|
||||
cd ruoyi-web
|
||||
|
||||
# Build and start user frontend
|
||||
docker-compose up -d --build
|
||||
|
||||
# Access user frontend
|
||||
# URL: http://localhost:5137
|
||||
```
|
||||
|
||||
### Service Ports
|
||||
|
||||
| Service | One-click Port | Step-by-step Port | Description |
|
||||
|------|-------------|-------------|------|
|
||||
| Admin Panel | 25666 | 5666 | Admin backend access |
|
||||
| User Frontend | 25137 | 5137 | User frontend access |
|
||||
| Backend Service | 26039 | 6039 | Backend API service |
|
||||
| MySQL | 23306 | 23306 | Database service |
|
||||
| Redis | 26379 | 6379 | Cache service |
|
||||
| Weaviate | 28080 | 28080 | Vector database |
|
||||
| MinIO API | 29000 | 9000 | Object storage API |
|
||||
| MinIO Console | 29090 | 9090 | Object storage console |
|
||||
|
||||
### Image Registry
|
||||
|
||||
All images are hosted on Alibaba Cloud Container Registry:
|
||||
|
||||
```
|
||||
crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai
|
||||
```
|
||||
|
||||
Available images:
|
||||
- `mysql:v3` - MySQL database (includes initialization SQL)
|
||||
- `redis:6.2` - Redis cache
|
||||
- `weaviate:1.30.0` - Vector database
|
||||
- `minio:latest` - Object storage
|
||||
- `ruoyi-ai-backend:latest` - Backend service
|
||||
- `ruoyi-ai-admin:latest` - Admin frontend
|
||||
- `ruoyi-ai-web:latest` - User frontend
|
||||
|
||||
### Common Commands
|
||||
|
||||
```bash
|
||||
# Stop all services
|
||||
docker-compose -f docker-compose-all.yaml down
|
||||
|
||||
# View service logs
|
||||
docker-compose -f docker-compose-all.yaml logs -f [service-name]
|
||||
|
||||
# Restart a service
|
||||
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
|
||||
|
||||
Want to learn more about installation, deployment, configuration, and secondary development?
|
||||
|
||||
**👉 [Complete Documentation](https://doc.ruoyiai.chat/)**
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We warmly welcome community contributions! Whether you are a seasoned developer or just getting started, you can contribute to the project 💪
|
||||
|
||||
### How to Contribute
|
||||
|
||||
1. **Fork** the project to your account
|
||||
2. **Create a branch** (`git checkout -b feature/new-feature-name`)
|
||||
3. **Commit your changes** (`git commit -m 'Add new feature'`)
|
||||
4. **Push to the branch** (`git push origin feature/new-feature-name`)
|
||||
5. **Create a Pull Request**
|
||||
|
||||
> 💡 **Tip**: We recommend submitting PRs to GitHub, we will automatically sync to other code hosting platforms
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
Thanks to the following excellent open-source projects for their support:
|
||||
- [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
|
||||
- [Vben Admin](https://github.com/vbenjs/vue-vben-admin) - Modern Vue admin template
|
||||
|
||||
## 💎 Sponsors
|
||||
|
||||
**Thanks to the following sponsors for supporting this project:**
|
||||
|
||||
<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
|
||||
|
||||
<div align="center">
|
||||
|
||||
<table>
|
||||
<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 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.ruoyiai.chat/)**
|
||||
|
||||
*Built with ❤️, maintained by the RuoYi AI open-source community*
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Badge Links -->
|
||||
|
||||
[contributors-shield]: https://img.shields.io/github/contributors/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[contributors-url]: https://github.com/ageerle/ruoyi-ai/graphs/contributors
|
||||
|
||||
[forks-shield]: https://img.shields.io/github/forks/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[forks-url]: https://github.com/ageerle/ruoyi-ai/network/members
|
||||
|
||||
[stars-shield]: https://img.shields.io/github/stars/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[stars-url]: https://github.com/ageerle/ruoyi-ai/stargazers
|
||||
|
||||
[issues-shield]: https://img.shields.io/github/issues/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[issues-url]: https://github.com/ageerle/ruoyi-ai/issues
|
||||
|
||||
[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
|
||||
286
README_ZH.md
Normal file
286
README_ZH.md
Normal file
@@ -0,0 +1,286 @@
|
||||
# RuoYi AI
|
||||
|
||||
<div align="center">
|
||||
|
||||
[![Contributors][contributors-shield]][contributors-url]
|
||||
[![Forks][forks-shield]][forks-url]
|
||||
[![Stargazers][stars-shield]][stars-url]
|
||||
[![Issues][issues-shield]][issues-url]
|
||||
[![MIT License][license-shield]][license-url]
|
||||
|
||||
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/13209">
|
||||
<img src="https://trendshift.io/api/badge/repositories/13209" alt="GitHub Trending">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<img src="docs/image/logo.png" alt="RuoYi AI Logo" width="120" height="120">
|
||||
|
||||
### 企业级AI助手平台
|
||||
|
||||
*开箱即用的全栈AI平台,支持多智能体协同、Supervisor模式编排、多种决策模式、RAG技术和流程编排能力*
|
||||
|
||||
**[English](README.md)** | **[📖 使用文档](https://doc.ruoyiai.chat/)** |
|
||||
**[🚀 在线体验](https://web.ruoyiai.chat/)** | **[🐛 问题反馈](https://github.com/ageerle/ruoyi-ai/issues)** | **[💡 功能建议](https://github.com/ageerle/ruoyi-ai/issues)**
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
## ✨ 核心亮点
|
||||
|
||||
| 模块 | 现有能力
|
||||
|:---------:|---
|
||||
| **模型管理** | 多模型接入(DeepSeek/智谱/MIMO/百炼/OpenAI)、多模态理解、Coze/DIFY/FastGPT/RAGFlow平台集成
|
||||
| **知识管理** | 本地RAG + 向量库(Milvus/Weaviate/Qdrant) + 文档解析
|
||||
| **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态
|
||||
| **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点
|
||||
| **智能体管理** | 基于Langchain4j的Agent框架、Supervisor模式编排,支持多种决策模型,可以灵活搭配工具,skills
|
||||
|
||||
|
||||
### 项目源码
|
||||
|
||||
| 项目模块 | GitHub 仓库 | Gitee 仓库 | GitCode 仓库 |
|
||||
|----------|-------------------------------------------------------|------------------------------------------------------|--------------------------------------------------------|
|
||||
| 🔧 后端服务 | [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-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) |
|
||||
| 🤖 编程助手 | [ruoyi-copilot](https://github.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitee.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitcode.com/ageerle/ruoyi-copilot) |
|
||||
| 📱 小程序端 | [ruoyi-uniapp](https://github.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitee.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitcode.com/ageerle/ruoyi-uniapp) |
|
||||
|
||||
### 合作项目
|
||||
| 项目名称 | 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.8 + Langchain4j
|
||||
- **数据存储**:MySQL 8.0 + Redis + 向量数据库(Milvus/Weaviate/Qdrant)
|
||||
- **前端技术**:Vue 3 + Vben Admin + element-plus-x
|
||||
- **安全认证**:Sa-Token + JWT 双重保障
|
||||
- **文档处理**:PDF、Word、Excel 解析,图像智能分析
|
||||
- **实时通信**:WebSocket 实时通信,SSE 流式响应
|
||||
- **系统监控**:完善的日志体系、性能监控、服务健康检查
|
||||
|
||||
## 🐳 Docker 部署
|
||||
|
||||
本项目提供两种 Docker 部署方式:
|
||||
|
||||
### 方式一:一键启动所有服务(推荐)
|
||||
|
||||
使用 `docker-compose-all.yaml` 可以一键启动所有服务(包括后端、管理端、用户端及依赖服务):
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
git clone https://github.com/ageerle/ruoyi-ai.git
|
||||
cd ruoyi-ai
|
||||
|
||||
# 启动所有服务(从镜像仓库拉取预构建镜像)
|
||||
docker-compose -f docker-compose-all.yaml up -d
|
||||
|
||||
# 查看服务状态
|
||||
docker-compose -f docker-compose-all.yaml ps
|
||||
|
||||
# 访问服务
|
||||
# 管理端: http://localhost:25666 (admin / admin123)
|
||||
# 用户端: http://localhost:25137
|
||||
# 后端API: http://localhost:26039
|
||||
```
|
||||
|
||||
### 方式二:分步部署(源码编译)
|
||||
|
||||
如果您需要从源码构建后端服务,请按照以下步骤操作:
|
||||
|
||||
#### 第一步:部署后端服务
|
||||
|
||||
```bash
|
||||
# 进入后端项目目录
|
||||
cd ruoyi-ai
|
||||
|
||||
# 启动后端服务(源码编译构建)
|
||||
docker-compose up -d --build
|
||||
|
||||
# 等待后端服务启动完成
|
||||
docker-compose logs -f backend
|
||||
```
|
||||
|
||||
#### 第二步:部署管理端
|
||||
|
||||
```bash
|
||||
# 进入管理端项目目录
|
||||
cd ruoyi-admin
|
||||
|
||||
# 构建并启动管理端
|
||||
docker-compose up -d --build
|
||||
|
||||
# 访问管理端
|
||||
# 地址: http://localhost:5666
|
||||
```
|
||||
|
||||
#### 第三步:部署用户端(可选)
|
||||
|
||||
```bash
|
||||
# 进入用户端项目目录
|
||||
cd ruoyi-web
|
||||
|
||||
# 构建并启动用户端
|
||||
docker-compose up -d --build
|
||||
|
||||
# 访问用户端
|
||||
# 地址: http://localhost:5137
|
||||
```
|
||||
|
||||
### 服务端口说明
|
||||
|
||||
| 服务 | 一键启动端口 | 分步部署端口 | 说明 |
|
||||
|------|-------------|-------------|------|
|
||||
| 管理端 | 25666 | 5666 | 管理后台访问地址 |
|
||||
| 用户端 | 25137 | 5137 | 用户前端访问地址 |
|
||||
| 后端服务 | 26039 | 6039 | 后端 API 服务 |
|
||||
| MySQL | 23306 | 23306 | 数据库服务 |
|
||||
| Redis | 26379 | 6379 | 缓存服务 |
|
||||
| Weaviate | 28080 | 28080 | 向量数据库 |
|
||||
| MinIO API | 29000 | 9000 | 对象存储 API |
|
||||
| MinIO Console | 29090 | 9090 | 对象存储控制台 |
|
||||
|
||||
### 镜像仓库
|
||||
|
||||
所有镜像托管在阿里云容器镜像服务:
|
||||
|
||||
```
|
||||
crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai
|
||||
```
|
||||
|
||||
可用镜像:
|
||||
- `mysql:v3` - MySQL 数据库(包含初始化 SQL)
|
||||
- `redis:6.2` - Redis 缓存
|
||||
- `weaviate:1.30.0` - 向量数据库
|
||||
- `minio:latest` - 对象存储
|
||||
- `ruoyi-ai-backend:latest` - 后端服务
|
||||
- `ruoyi-ai-admin:latest` - 管理端前端
|
||||
- `ruoyi-ai-web:latest` - 用户端前端
|
||||
|
||||
### 常用命令
|
||||
|
||||
```bash
|
||||
# 停止所有服务
|
||||
docker-compose -f docker-compose-all.yaml down
|
||||
|
||||
# 查看服务日志
|
||||
docker-compose -f docker-compose-all.yaml logs -f [服务名]
|
||||
|
||||
# 重启某个服务
|
||||
docker-compose -f docker-compose-all.yaml restart [服务名]
|
||||
```
|
||||
|
||||
## 📚 使用文档
|
||||
|
||||
想要深入了解安装部署、功能配置和二次开发?
|
||||
|
||||
**👉 [完整使用文档](https://doc.ruoyiai.chat/)**
|
||||
|
||||
## 🤝 参与贡献
|
||||
|
||||
我们热烈欢迎社区贡献!无论您是资深开发者还是初学者,都可以为项目贡献力量 💪
|
||||
|
||||
### 贡献方式
|
||||
|
||||
1. **Fork** 项目到您的账户
|
||||
2. **创建分支** (`git checkout -b feature/新功能名称`)
|
||||
3. **提交代码** (`git commit -m '添加某某功能'`)
|
||||
4. **推送分支** (`git push origin feature/新功能名称`)
|
||||
5. **发起 Pull Request**
|
||||
|
||||
> 💡 **小贴士**:建议将 PR 提交到 GitHub,我们会自动同步到其他代码托管平台
|
||||
|
||||
## 📄 开源协议
|
||||
|
||||
本项目采用 **MIT 开源协议**,详情请查看 [LICENSE](LICENSE) 文件。
|
||||
|
||||
## 🙏 特别鸣谢
|
||||
|
||||
感谢以下优秀的开源项目为本项目提供支持:
|
||||
- [Langchain4j](https://github.com/langchain4j/langchain4j) - 强大的 Java LLM 开发框架
|
||||
- [RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus) - 成熟的企业级快速开发框架
|
||||
- [Vben Admin](https://github.com/vbenjs/vue-vben-admin) - 现代化的 Vue 后台管理模板
|
||||
|
||||
|
||||
## 💎 赞助商
|
||||
|
||||
**感谢以下赞助商对本项目的支持:**
|
||||
|
||||
<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 复杂长程任务!
|
||||
|
||||
|
||||
## 💬 社区交流
|
||||
|
||||
<div align="center">
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="docs/image/wx.png" alt="微信二维码" width="200" height="200"><br>
|
||||
<strong>扫码添加作者微信</strong><br>
|
||||
<em>邀请进群学习</em>
|
||||
</td>
|
||||
<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>
|
||||
<strong>QQ技术交流群</strong><br>
|
||||
<em>技术讨论</em>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
<div align="center">
|
||||
|
||||
**[⭐ 点个Star支持一下](https://github.com/ageerle/ruoyi-ai)** • **[ Fork 开始贡献](https://github.com/ageerle/ruoyi-ai/fork)** • **[📚 English](README.md)** • **[📖 查看完整文档](https://doc.ruoyiai.chat/)**
|
||||
|
||||
*用 ❤️ 打造,由 RuoYi AI 开源社区维护*
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Badge Links -->
|
||||
|
||||
[contributors-shield]: https://img.shields.io/github/contributors/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[contributors-url]: https://github.com/ageerle/ruoyi-ai/graphs/contributors
|
||||
|
||||
[forks-shield]: https://img.shields.io/github/forks/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[forks-url]: https://github.com/ageerle/ruoyi-ai/network/members
|
||||
|
||||
[stars-shield]: https://img.shields.io/github/stars/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[stars-url]: https://github.com/ageerle/ruoyi-ai/stargazers
|
||||
|
||||
[issues-shield]: https://img.shields.io/github/issues/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[issues-url]: https://github.com/ageerle/ruoyi-ai/issues
|
||||
|
||||
[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
|
||||
@@ -2291,11 +2291,10 @@ INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`,
|
||||
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);
|
||||
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 (2084157200000000003, '000000', '网络搜索节点响应模板', 'node.googleSearch.template', '🔍 网络搜索节点处理完成:', 'Y', 103, 1, '2026-07-29 19:40:00', 1, '2026-07-29 19:40:00', NULL);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sys_dept
|
||||
@@ -3417,7 +3416,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 = 38 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '工作流组件库 | Workflow Component' ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of t_workflow_component
|
||||
@@ -3425,9 +3424,8 @@ 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');
|
||||
INSERT INTO `t_workflow_component` VALUES (37, 'a7f8c2d44e5b4c83a9d6f103c2b47e18', 'Google', '网络搜索', '调用智谱 Web Search 检索互联网信息', 40, 1, '2026-07-29 20:30:00', '2026-07-29 20:30:00', 0, '000000');
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for t_workflow_edge
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-- 补充工作流节点消息模板配置 (对应 issue IJX5VV)
|
||||
-- 背景:NodeMessageTemplateEnum 依赖以下 9 个 sys_config 键,缺失时
|
||||
-- 背景:NodeMessageTemplateEnum 依赖以下 7 个 sys_config 键,缺失时
|
||||
-- WorkflowMessageUtil.getNodeMessageTemplate 会抛出「请先配置该节点的响应模板」。
|
||||
-- 这批配置在历史提交 20d531c0 中存在,SQL 脚本合并重命名时遗失,此处恢复。
|
||||
-- 幂等:按 config_key + tenant_id 判重,可重复执行。
|
||||
@@ -20,10 +20,6 @@ INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`,
|
||||
SELECT 2027194134438277122, '000000', '结束节点响应模板', 'node.end.template', '🔚 流程已执行完毕,如果您有其他需求,请随时重新发起请求。', 'Y', 103, 1, '2026-02-27 09:28:40', 1, '2026-02-27 09:32:53', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.end.template' AND `tenant_id` = '000000');
|
||||
|
||||
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`)
|
||||
SELECT 2027206492573335554, '000000', '人机交互节点响应模板', 'node.humanFeedback.template', '👤 人机交互节点:等待用户操作 - ', 'Y', 103, 1, '2026-02-27 10:17:46', 1, '2026-02-27 10:17:46', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.humanFeedback.template' AND `tenant_id` = '000000');
|
||||
|
||||
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`)
|
||||
SELECT 2027208880369647617, '000000', '条件分支节点响应模板', 'node.switch.template', '🔀 条件分支节点:触发 -> 跳转到节点 ', 'Y', 103, 1, '2026-02-27 10:27:15', 1, '2026-02-27 10:35:54', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.switch.template' AND `tenant_id` = '000000');
|
||||
@@ -32,10 +28,6 @@ INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`,
|
||||
SELECT 2027213914603995137, '000000', '大模型回答节点响应模板', 'node.llmAnswer.template', '🤖 LLM 节点 生成回答:', 'Y', 103, 1, '2026-02-27 10:47:16', 1, '2026-02-27 10:52:40', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.llmAnswer.template' AND `tenant_id` = '000000');
|
||||
|
||||
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`)
|
||||
SELECT 2027214387000066050, '000000', '关键词提取响应模板', 'node.keywordExtractor.template', '🔑 关键词提取节点 处理完成 : ', 'Y', 103, 1, '2026-02-27 10:49:08', 1, '2026-02-27 10:52:08', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.keywordExtractor.template' AND `tenant_id` = '000000');
|
||||
|
||||
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`)
|
||||
SELECT 2027217577397391361, '000000', '工作流异常响应模板', 'node.exception.template', '🛑 工作流发生异常:', 'N', 103, 1, '2026-02-27 11:01:49', 1, '2026-02-27 11:02:01', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.exception.template' AND `tenant_id` = '000000');
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
-- 一次性补全工作流节点消息模板配置 (NodeMessageTemplateEnum 全部 8 个键)
|
||||
-- 背景:节点执行时 WorkflowMessageUtil.getNodeMessageTemplate 从 sys_config 读取展示模板,
|
||||
-- 历史库中这批配置缺失, 导致运行工作流抛「请先配置该节点的响应模板」。
|
||||
-- 其中前 7 个见 2026-07-21-sys-config-node-template.sql,
|
||||
-- Google Search 为此前从未入库的节点模板。
|
||||
-- 说明:代码已增加内置默认模板兜底, 本脚本为可选, 执行后模板可在 系统管理-配置管理 中自定义。
|
||||
-- 幂等:按 config_key + tenant_id 判重, 可重复执行。
|
||||
|
||||
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`)
|
||||
SELECT 2027192921483309058, '000000', 'HTTP请求节点响应模板', 'node.httpRequest.template', '✅ HTTP请求节点:结束响应 - ', 'Y', 103, 1, '2026-02-27 09:23:51', 1, '2026-02-27 09:31:41', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.httpRequest.template' AND `tenant_id` = '000000');
|
||||
|
||||
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`)
|
||||
SELECT 2027193296990957569, '000000', '文生图节点响应模板', 'node.image.template', '🎨 文生图节点:结束响应 - 图片URL: ', 'Y', 103, 1, '2026-02-27 09:25:20', 1, '2026-02-27 09:31:52', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.image.template' AND `tenant_id` = '000000');
|
||||
|
||||
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`)
|
||||
SELECT 2027193820393959425, '000000', '发送邮箱节点响应模板', 'node.mailsend.template', '📧 发送邮箱节点:结束响应 - ', 'Y', 103, 1, '2026-02-27 09:27:25', 1, '2026-02-27 09:32:05', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.mailsend.template' AND `tenant_id` = '000000');
|
||||
|
||||
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`)
|
||||
SELECT 2027194134438277122, '000000', '结束节点响应模板', 'node.end.template', '🔚 流程已执行完毕,如果您有其他需求,请随时重新发起请求。', 'Y', 103, 1, '2026-02-27 09:28:40', 1, '2026-02-27 09:32:53', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.end.template' AND `tenant_id` = '000000');
|
||||
|
||||
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`)
|
||||
SELECT 2027208880369647617, '000000', '条件分支节点响应模板', 'node.switch.template', '🔀 条件分支节点:触发 -> 跳转到节点 ', 'Y', 103, 1, '2026-02-27 10:27:15', 1, '2026-02-27 10:35:54', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.switch.template' AND `tenant_id` = '000000');
|
||||
|
||||
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`)
|
||||
SELECT 2027213914603995137, '000000', '大模型回答节点响应模板', 'node.llmAnswer.template', '🤖 LLM 节点 生成回答:', 'Y', 103, 1, '2026-02-27 10:47:16', 1, '2026-02-27 10:52:40', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.llmAnswer.template' AND `tenant_id` = '000000');
|
||||
|
||||
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`)
|
||||
SELECT 2027217577397391361, '000000', '工作流异常响应模板', 'node.exception.template', '🛑 工作流发生异常:', 'N', 103, 1, '2026-02-27 11:01:49', 1, '2026-02-27 11:02:01', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.exception.template' AND `tenant_id` = '000000');
|
||||
|
||||
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`)
|
||||
SELECT 2084157200000000003, '000000', '网络搜索节点响应模板', 'node.googleSearch.template', '🔍 网络搜索节点处理完成:', 'Y', 103, 1, '2026-07-29 19:40:00', 1, '2026-07-29 19:40:00', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.googleSearch.template' AND `tenant_id` = '000000');
|
||||
66
docs/script/sql/update/2026-07-29-zhipu-web-search-node.sql
Normal file
66
docs/script/sql/update/2026-07-29-zhipu-web-search-node.sql
Normal file
@@ -0,0 +1,66 @@
|
||||
-- 智谱 Web Search 工作流扩展节点
|
||||
-- 内部组件名继续使用 Google,以兼容现有前端组件和已保存流程。
|
||||
|
||||
UPDATE `t_workflow_component`
|
||||
SET `title` = '网络搜索',
|
||||
`remark` = '调用智谱 Web Search 检索互联网信息',
|
||||
`display_order` = 40,
|
||||
`is_enable` = 1,
|
||||
`is_deleted` = 0,
|
||||
`update_time` = NOW()
|
||||
WHERE `name` = 'Google'
|
||||
AND `tenant_id` = '000000';
|
||||
|
||||
INSERT INTO `t_workflow_component`
|
||||
(`uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`,
|
||||
`create_time`, `update_time`, `is_deleted`, `tenant_id`)
|
||||
SELECT
|
||||
'a7f8c2d44e5b4c83a9d6f103c2b47e18',
|
||||
'Google',
|
||||
'网络搜索',
|
||||
'调用智谱 Web Search 检索互联网信息',
|
||||
40,
|
||||
1,
|
||||
NOW(),
|
||||
NOW(),
|
||||
0,
|
||||
'000000'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM `t_workflow_component`
|
||||
WHERE `name` = 'Google'
|
||||
AND `tenant_id` = '000000'
|
||||
);
|
||||
|
||||
UPDATE `sys_config`
|
||||
SET `config_name` = '网络搜索节点响应模板',
|
||||
`config_value` = '🔍 网络搜索节点处理完成:',
|
||||
`update_time` = NOW()
|
||||
WHERE `config_key` = 'node.googleSearch.template'
|
||||
AND `tenant_id` = '000000';
|
||||
|
||||
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`)
|
||||
SELECT
|
||||
2084157200000000003,
|
||||
'000000',
|
||||
'网络搜索节点响应模板',
|
||||
'node.googleSearch.template',
|
||||
'🔍 网络搜索节点处理完成:',
|
||||
'Y',
|
||||
103,
|
||||
1,
|
||||
NOW(),
|
||||
1,
|
||||
NOW(),
|
||||
'智谱 Web Search 工作流扩展节点'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM `sys_config`
|
||||
WHERE `config_key` = 'node.googleSearch.template'
|
||||
AND `tenant_id` = '000000'
|
||||
);
|
||||
11
pom.xml
11
pom.xml
@@ -342,10 +342,18 @@
|
||||
</dependency>
|
||||
|
||||
<!-- JustAuth 的依赖配置-->
|
||||
<!-- 排除 fastjson(存在 RCE 漏洞且已停止维护), 项目内自定义的登录类(钉钉/企业微信/gitea/maxkey/topiam)已改用 Jackson -->
|
||||
<!-- 注意: JustAuth 内置平台类(github/gitee/qq 等)内部仍使用 fastjson, 排除后如需使用这些平台请自行覆写对应请求类 -->
|
||||
<dependency>
|
||||
<groupId>me.zhyd.oauth</groupId>
|
||||
<artifactId>JustAuth</artifactId>
|
||||
<version>${justauth.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- 离线IP地址定位库 ip2region -->
|
||||
@@ -355,9 +363,6 @@
|
||||
<version>${ip2region.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- FastJson已完全移除,项目统一使用Jackson -->
|
||||
<!-- Jackson相关依赖已由Spring Boot统一管理 -->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-system</artifactId>
|
||||
|
||||
@@ -60,7 +60,7 @@ spring:
|
||||
# rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
|
||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
username: root
|
||||
password: 123456
|
||||
password: root
|
||||
# agent:
|
||||
# url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
# # url: jdbc:mysql://localhost:3306/agent_db
|
||||
|
||||
@@ -332,6 +332,16 @@ vector-store:
|
||||
api-key:
|
||||
use-tls: false
|
||||
|
||||
# 流程编排扩展节点
|
||||
workflow:
|
||||
web-search:
|
||||
zhipu:
|
||||
# 推荐通过环境变量注入;为空时回退到模型管理中的 zhipu 厂商密钥
|
||||
api-key: ${ZAI_API_KEY:}
|
||||
base-url: ${ZHIPU_WEB_SEARCH_BASE_URL:https://open.bigmodel.cn/api/paas/v4/}
|
||||
connect-timeout: ${ZHIPU_WEB_SEARCH_CONNECT_TIMEOUT:10}
|
||||
read-timeout: ${ZHIPU_WEB_SEARCH_READ_TIMEOUT:30}
|
||||
|
||||
# 短剧成片合成
|
||||
short-drama:
|
||||
composition:
|
||||
|
||||
@@ -54,7 +54,6 @@ public enum ErrorEnum {
|
||||
A_WF_RUNTIME_NOT_FOUND("A00045", "工作流运行时数据找不到"),
|
||||
A_SEARCH_QUERY_IS_EMPTY("A00046", "搜索内容不能为空"),
|
||||
A_WF_COMPONENT_NOT_FOUND("A00047", "工作流基础组件找不到"),
|
||||
A_WF_RESUME_FAIL("A00048", "工作流恢复执行时失败"),
|
||||
A_MAIL_SENDER_EMPTY("A00049", "邮件发送人不能为空"),
|
||||
A_MAIL_SENDER_CONFIG_ERROR("A00050", "邮件发送人配置错误"),
|
||||
A_MAIL_RECEIVER_EMPTY("A00051", "邮件接收人不能为空"),
|
||||
|
||||
@@ -22,12 +22,4 @@ public interface IWorkFlowStarterService {
|
||||
* @return 流式输出结果
|
||||
*/
|
||||
SseEmitter streaming(User user, String workflowUuid, List<ObjectNode> userInputs, Long sessionId);
|
||||
|
||||
/**
|
||||
* 恢复工作流
|
||||
* @param runtimeUuid 运行时UUID
|
||||
* @param userInput 用户输入
|
||||
* @param sseEmitter SSE连接对象
|
||||
*/
|
||||
void resumeFlow(String runtimeUuid, String userInput, SseEmitter sseEmitter);
|
||||
}
|
||||
|
||||
@@ -15,9 +15,6 @@ import me.zhyd.oauth.utils.HttpUtils;
|
||||
import me.zhyd.oauth.utils.StringUtils;
|
||||
import me.zhyd.oauth.utils.UrlBuilder;
|
||||
|
||||
// 临时保留FastJson用于JustAuth库兼容
|
||||
import com.alibaba.fastjson.JSON;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 企业微信登录父类
|
||||
@@ -64,11 +61,8 @@ public abstract class AbstractAuthWeChatEnterpriseRequest extends AuthDefaultReq
|
||||
String userTicket = object.has("user_ticket") ? object.get("user_ticket").asText() : null;
|
||||
JsonNode userDetail = getUserDetail(authToken.getAccessToken(), userId, userTicket);
|
||||
|
||||
// 将JsonNode转换为JSONObject以兼容JustAuth库
|
||||
com.alibaba.fastjson.JSONObject rawUserInfo = com.alibaba.fastjson.JSON.parseObject(userDetail.toString());
|
||||
|
||||
// rawUserInfo 为 JustAuth 的 fastjson 类型字段, 项目内无消费方, 不再设置
|
||||
return AuthUser.builder()
|
||||
.rawUserInfo(rawUserInfo)
|
||||
.username(userDetail.has("name") ? userDetail.get("name").asText() : null)
|
||||
.nickname(userDetail.has("alias") ? userDetail.get("alias").asText() : null)
|
||||
.avatar(userDetail.has("avatar") ? userDetail.get("avatar").asText() : null)
|
||||
|
||||
@@ -19,9 +19,6 @@ import me.zhyd.oauth.utils.UrlBuilder;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
// 临时保留FastJson用于JustAuth库兼容
|
||||
import com.alibaba.fastjson.JSON;
|
||||
|
||||
/**
|
||||
* 新版钉钉二维码登录
|
||||
*
|
||||
@@ -92,13 +89,10 @@ public class AuthDingTalkV2Request extends AuthDefaultRequest {
|
||||
String response = new HttpUtils(config.getHttpConfig()).get(this.source.userInfo(), null, header, false).getBody();
|
||||
JsonNode object = objectMapper.readTree(response);
|
||||
|
||||
// 将JsonNode转换为JSONObject以兼容JustAuth库
|
||||
com.alibaba.fastjson.JSONObject rawUserInfo = com.alibaba.fastjson.JSON.parseObject(object.toString());
|
||||
|
||||
authToken.setOpenId(object.has("openId") ? object.get("openId").asText() : null);
|
||||
authToken.setUnionId(object.has("unionId") ? object.get("unionId").asText() : null);
|
||||
// rawUserInfo 为 JustAuth 的 fastjson 类型字段, 项目内无消费方, 不再设置
|
||||
return AuthUser.builder()
|
||||
.rawUserInfo(rawUserInfo)
|
||||
.uuid(object.has("unionId") ? object.get("unionId").asText() : null)
|
||||
.username(object.has("nick") ? object.get("nick").asText() : null)
|
||||
.nickname(object.has("nick") ? object.get("nick").asText() : null)
|
||||
|
||||
@@ -24,6 +24,13 @@
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- 智谱官方 Java SDK:流程编排 Web Search 扩展节点 -->
|
||||
<dependency>
|
||||
<groupId>ai.z.openapi</groupId>
|
||||
<artifactId>zai-sdk</artifactId>
|
||||
<version>0.3.5</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-chat</artifactId>
|
||||
@@ -45,6 +52,11 @@
|
||||
<artifactId>ruoyi-common-satoken</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-tenant</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-mail</artifactId>
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
package org.ruoyi.workflow.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import org.ruoyi.common.core.domain.R;
|
||||
import org.ruoyi.workflow.dto.workflow.WfRuntimeNodeDto;
|
||||
import org.ruoyi.workflow.dto.workflow.WfRuntimeResp;
|
||||
import org.ruoyi.workflow.dto.workflow.WorkflowResumeReq;
|
||||
import org.ruoyi.workflow.service.WorkflowRuntimeService;
|
||||
import org.ruoyi.workflow.workflow.WorkflowStarter;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -24,16 +21,6 @@ public class WorkflowRuntimeController {
|
||||
@Resource
|
||||
private WorkflowRuntimeService workflowRuntimeService;
|
||||
|
||||
@Resource
|
||||
private WorkflowStarter workflowStarter;
|
||||
|
||||
@Operation(summary = "接收用户输入以继续执行剩余流程")
|
||||
@PostMapping(value = "/resume/{runtimeUuid}")
|
||||
public R resume(@PathVariable String runtimeUuid, @RequestBody WorkflowResumeReq resumeReq) {
|
||||
workflowStarter.resumeFlow(runtimeUuid, resumeReq.getFeedbackContent(), resumeReq.getSseEmitter());
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
public R<Page<WfRuntimeResp>> search(@RequestParam String wfUuid,
|
||||
@NotNull @Min(1) Integer currentPage,
|
||||
|
||||
@@ -337,7 +337,6 @@ public class AdiConstant {
|
||||
public static final String DEFAULT_INPUT_PARAM_NAME = "input";
|
||||
public static final String DEFAULT_OUTPUT_PARAM_NAME = "output";
|
||||
public static final String DEFAULT_ERROR_OUTPUT_PARAM_NAME = "error_msg";
|
||||
public static final String HUMAN_FEEDBACK_KEY = "human_feedback";
|
||||
public static final int NODE_PROCESS_STATUS_READY = 1;
|
||||
public static final int NODE_PROCESS_STATUS_DOING = 2;
|
||||
public static final int NODE_PROCESS_STATUS_SUCCESS = 3;
|
||||
@@ -347,7 +346,6 @@ public class AdiConstant {
|
||||
public static final int WORKFLOW_PROCESS_STATUS_DOING = 2;
|
||||
public static final int WORKFLOW_PROCESS_STATUS_SUCCESS = 3;
|
||||
public static final int WORKFLOW_PROCESS_STATUS_FAIL = 4;
|
||||
public static final int WORKFLOW_PROCESS_STATUS_WAITING_INPUT = 5;
|
||||
|
||||
public static final int WORKFLOW_NODE_PROCESS_TYPE_NORMAL = 1;
|
||||
public static final int WORKFLOW_NODE_PROCESS_TYPE_CONDITIONAL = 2;
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package org.ruoyi.workflow.dto.workflow;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
@Data
|
||||
public class WorkflowResumeReq {
|
||||
private String feedbackContent;
|
||||
private SseEmitter sseEmitter;
|
||||
}
|
||||
@@ -55,8 +55,13 @@ public class SSEEmitterHelper {
|
||||
} else {
|
||||
sseEmitter.send(msg);
|
||||
}
|
||||
} catch (IllegalStateException ise) {
|
||||
// SSE连接已关闭(用户刷新页面、关闭标签页或重新提交)
|
||||
log.warn("SSE emitter already completed for event [{}], ignoring", name);
|
||||
COMPLETED_SSE.put(sseEmitter, Boolean.TRUE);
|
||||
} catch (IOException ioException) {
|
||||
log.error("stream onNext error", ioException);
|
||||
COMPLETED_SSE.put(sseEmitter, Boolean.TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.common.chat.enums.RoleType;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.core.exception.ServiceException;
|
||||
import org.ruoyi.common.core.service.ConfigService;
|
||||
import org.ruoyi.common.core.utils.SpringUtils;
|
||||
import org.ruoyi.common.core.utils.StringUtils;
|
||||
@@ -12,6 +11,7 @@ import org.ruoyi.workflow.entity.WorkflowNode;
|
||||
import org.ruoyi.workflow.helper.SSEEmitterHelper;
|
||||
import org.ruoyi.workflow.workflow.WfState;
|
||||
import org.ruoyi.workflow.workflow.WorkflowUtil;
|
||||
import org.ruoyi.workflow.workflow.node.enmus.NodeMessageTemplateEnum;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
/**
|
||||
@@ -38,17 +38,20 @@ public class WorkflowMessageUtil {
|
||||
|
||||
|
||||
/**
|
||||
* 获取节点的响应模板
|
||||
* 获取节点的响应模板 <br/>
|
||||
* 优先读取 sys_config 配置(可在系统管理-配置管理中自定义),
|
||||
* 未配置时回退到枚举内置默认模板, 模板仅为展示文案, 缺失不应中断工作流执行
|
||||
* @param configKey 参数Key
|
||||
* @return 返回模板样式
|
||||
*/
|
||||
public static String getNodeMessageTemplate(String configKey){
|
||||
ConfigService configService = SpringUtil.getBean(ConfigService.class);
|
||||
String configValue = configService.getConfigValue(configKey);
|
||||
if (StringUtils.isEmpty(configValue)) {
|
||||
throw new ServiceException("请先配置该节点的响应模板");
|
||||
if (StringUtils.isNotEmpty(configValue)) {
|
||||
return configValue;
|
||||
}
|
||||
return configValue;
|
||||
log.warn("sys_config 未配置节点响应模板 [{}], 已回退使用内置默认模板", configKey);
|
||||
return NodeMessageTemplateEnum.getDefaultTemplate(configKey);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package org.ruoyi.workflow.workflow;
|
||||
|
||||
import org.apache.commons.collections4.map.PassiveExpiringMap;
|
||||
|
||||
/**
|
||||
* 已中断正在等待用户输入的流程 <br/>
|
||||
* TODO 需要考虑项目多节点部署的情况
|
||||
*/
|
||||
public class InterruptedFlow {
|
||||
|
||||
/**
|
||||
* 10分钟超时
|
||||
*/
|
||||
private static final PassiveExpiringMap.ExpirationPolicy<String, WorkflowEngine> ep = new PassiveExpiringMap.ConstantTimeToLiveExpirationPolicy<>(60 * 1000 * 10);
|
||||
public static PassiveExpiringMap<String, WorkflowEngine> RUNTIME_TO_GRAPH = new PassiveExpiringMap<>(ep);
|
||||
|
||||
}
|
||||
@@ -16,24 +16,14 @@ public enum WfComponentNameEnum {
|
||||
|
||||
TONGYI_WANX("Tongyiwanx"),
|
||||
|
||||
DOCUMENT_EXTRACTOR("DocumentExtractor"),
|
||||
|
||||
KEYWORD_EXTRACTOR("KeywordExtractor"),
|
||||
|
||||
FAQ_EXTRACTOR("FaqExtractor"),
|
||||
|
||||
KNOWLEDGE_RETRIEVER("KnowledgeRetrieval"),
|
||||
|
||||
SWITCHER("Switcher"),
|
||||
|
||||
CLASSIFIER("Classifier"),
|
||||
|
||||
TEMPLATE("Template"),
|
||||
|
||||
GOOGLE_SEARCH("Google"),
|
||||
|
||||
HUMAN_FEEDBACK("HumanFeedback"),
|
||||
|
||||
MAIL_SEND("MailSend"),
|
||||
|
||||
HTTP_REQUEST("HttpRequest");
|
||||
|
||||
@@ -4,15 +4,14 @@ import org.ruoyi.workflow.entity.WorkflowComponent;
|
||||
import org.ruoyi.workflow.entity.WorkflowNode;
|
||||
import org.ruoyi.workflow.workflow.node.AbstractWfNode;
|
||||
import org.ruoyi.workflow.workflow.node.EndNode;
|
||||
import org.ruoyi.workflow.workflow.node.humanFeedBack.HumanFeedbackNode;
|
||||
import org.ruoyi.workflow.workflow.node.answer.LLMAnswerNode;
|
||||
import org.ruoyi.workflow.workflow.node.httpRequest.HttpRequestNode;
|
||||
import org.ruoyi.workflow.workflow.node.image.ImageNode;
|
||||
import org.ruoyi.workflow.workflow.node.keywordExtractor.KeywordExtractorNode;
|
||||
import org.ruoyi.workflow.workflow.node.knowledgeRetrieval.KnowledgeRetrievalNode;
|
||||
import org.ruoyi.workflow.workflow.node.mailSend.MailSendNode;
|
||||
import org.ruoyi.workflow.workflow.node.start.StartNode;
|
||||
import org.ruoyi.workflow.workflow.node.switcher.SwitcherNode;
|
||||
import org.ruoyi.workflow.workflow.node.googleSearch.GoogleSearchNode;
|
||||
|
||||
public class WfNodeFactory {
|
||||
public static AbstractWfNode create(WorkflowComponent wfComponent, WorkflowNode nodeDefinition,
|
||||
@@ -21,14 +20,13 @@ public class WfNodeFactory {
|
||||
switch (WfComponentNameEnum.getByName(wfComponent.getName())) {
|
||||
case START -> wfNode = new StartNode(wfComponent, nodeDefinition, wfState, nodeState);
|
||||
case LLM_ANSWER -> wfNode = new LLMAnswerNode(wfComponent, nodeDefinition, wfState, nodeState);
|
||||
case KEYWORD_EXTRACTOR -> wfNode = new KeywordExtractorNode(wfComponent, nodeDefinition, wfState, nodeState);
|
||||
case TONGYI_WANX -> wfNode = new ImageNode(wfComponent, nodeDefinition, wfState, nodeState);
|
||||
case KNOWLEDGE_RETRIEVER -> wfNode = new KnowledgeRetrievalNode(wfComponent, nodeDefinition, wfState, nodeState);
|
||||
case END -> wfNode = new EndNode(wfComponent, nodeDefinition, wfState, nodeState);
|
||||
case MAIL_SEND -> wfNode = new MailSendNode(wfComponent, nodeDefinition, wfState, nodeState);
|
||||
case HTTP_REQUEST -> wfNode = new HttpRequestNode(wfComponent, nodeDefinition, wfState, nodeState);
|
||||
case SWITCHER -> wfNode = new SwitcherNode(wfComponent, nodeDefinition, wfState, nodeState);
|
||||
case HUMAN_FEEDBACK -> wfNode = new HumanFeedbackNode(wfComponent, nodeDefinition, wfState, nodeState);
|
||||
case GOOGLE_SEARCH -> wfNode = new GoogleSearchNode(wfComponent, nodeDefinition, wfState, nodeState);
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,11 +55,6 @@ public class WfState {
|
||||
private List<NodeIOData> output = new ArrayList<>();
|
||||
private Integer processStatus = WORKFLOW_PROCESS_STATUS_READY;
|
||||
|
||||
/**
|
||||
* 人机交互节点
|
||||
*/
|
||||
private Set<String> interruptNodes = new HashSet<>();
|
||||
|
||||
public WfState(User user, List<NodeIOData> input, String uuid, Long userId, String tokenValue, SseEmitter sseEmitter, Long sessionId) {
|
||||
this.input = input;
|
||||
this.user = user;
|
||||
@@ -133,8 +128,4 @@ public class WfState {
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public void addInterruptNode(String nodeUuid) {
|
||||
this.interruptNodes.add(nodeUuid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,82 +108,39 @@ public class WorkflowEngine {
|
||||
|
||||
MemorySaver saver = new MemorySaver();
|
||||
CompileConfig compileConfig = CompileConfig.builder().checkpointSaver(saver)
|
||||
.interruptBefore(wfState.getInterruptNodes().toArray(String[]::new))
|
||||
.build();
|
||||
app = mainStateGraph.compile(compileConfig);
|
||||
RunnableConfig invokeConfig = RunnableConfig.builder().build();
|
||||
exe(invokeConfig, false);
|
||||
exe(invokeConfig);
|
||||
} catch (Exception e) {
|
||||
errorWhenExe(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void exe(RunnableConfig invokeConfig, boolean resume) {
|
||||
private void exe(RunnableConfig invokeConfig) {
|
||||
//不使用langgraph4j state的update相关方法,无需传入input
|
||||
AsyncGenerator<NodeOutput<WfNodeState>> outputs = app.stream(resume ? null : Map.of(), invokeConfig);
|
||||
AsyncGenerator<NodeOutput<WfNodeState>> outputs = app.stream(Map.of(), invokeConfig);
|
||||
streamingResult(wfState, outputs, sseEmitter);
|
||||
|
||||
StateSnapshot<WfNodeState> stateSnapshot = app.getState(invokeConfig);
|
||||
String nextNode = stateSnapshot.config().nextNode().orElse("");
|
||||
//还有下个节点,表示进入中断状态,等待用户输入后继续执<E7BBAD>?
|
||||
if (StringUtils.isNotBlank(nextNode) && !nextNode.equalsIgnoreCase(END)) {
|
||||
// 获取提示模板
|
||||
String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.HUMAN_FEED_BACK.getValue());
|
||||
// 获取人机交互提示信息
|
||||
String intTip = nodeMessageTemplate + WorkflowUtil.getHumanFeedbackTip(nextNode, wfNodes);
|
||||
//将等待输入信息[事件与提示词]发送到到客户端
|
||||
SSEEmitterHelper.parseAndSendPartialMsg(sseEmitter, "[NODE_WAIT_FEEDBACK_BY_" + nextNode + "]", intTip);
|
||||
// 保存提示信息到Chat信息记录中(对话使用)
|
||||
WorkflowMessageUtil.saveWorkflowMessage(wfState, intTip);
|
||||
InterruptedFlow.RUNTIME_TO_GRAPH.put(wfState.getUuid(), this);
|
||||
//更新状<E696B0>?
|
||||
wfState.setProcessStatus(WORKFLOW_PROCESS_STATUS_WAITING_INPUT);
|
||||
workflowRuntimeService.updateOutput(wfRuntimeResp.getId(), wfState);
|
||||
} else {
|
||||
WorkflowRuntime updatedRuntime = workflowRuntimeService.updateOutput(wfRuntimeResp.getId(), wfState);
|
||||
// 保存成功会话信息
|
||||
wfNodes.stream().filter(item -> stateSnapshot.node().equals(item.getUuid()))
|
||||
.findFirst().ifPresent(wfNode -> {
|
||||
// 获取节点模板提示词信息
|
||||
String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.END.getValue());
|
||||
// 发送SSE消息驱动事件和保存会话
|
||||
WorkflowMessageUtil.notifyAndStoreMessage(wfState, sseEmitter, wfNode, nodeMessageTemplate);
|
||||
});
|
||||
// 发送结束消息
|
||||
sseEmitterHelper.sendComplete(user.getId(), sseEmitter, updatedRuntime.getOutput());
|
||||
// 发送驱动消息事件
|
||||
InterruptedFlow.RUNTIME_TO_GRAPH.remove(wfState.getUuid());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 中断流程等待用户输入时,会进行暂停状态,用户输入后调用本方法执行流程剩余部分
|
||||
*
|
||||
* @param userInput 用户输入
|
||||
*/
|
||||
public void resume(String userInput) {
|
||||
RunnableConfig invokeConfig = RunnableConfig.builder().build();
|
||||
try {
|
||||
app.updateState(invokeConfig, Map.of(HUMAN_FEEDBACK_KEY, userInput), null);
|
||||
exe(invokeConfig, true);
|
||||
} catch (Exception e) {
|
||||
errorWhenExe(e);
|
||||
} finally {
|
||||
//有可能多次接收人机交互,待整个流程完全执行后才能删除
|
||||
if (wfState.getProcessStatus() != WORKFLOW_PROCESS_STATUS_WAITING_INPUT) {
|
||||
InterruptedFlow.RUNTIME_TO_GRAPH.remove(wfState.getUuid());
|
||||
}
|
||||
}
|
||||
wfState.setProcessStatus(WORKFLOW_PROCESS_STATUS_SUCCESS);
|
||||
WorkflowRuntime updatedRuntime = workflowRuntimeService.updateOutput(wfRuntimeResp.getId(), wfState);
|
||||
wfNodes.stream().filter(item -> stateSnapshot.node().equals(item.getUuid()))
|
||||
.findFirst().ifPresent(wfNode -> {
|
||||
String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.END.getValue());
|
||||
WorkflowMessageUtil.notifyAndStoreMessage(wfState, sseEmitter, wfNode, nodeMessageTemplate);
|
||||
});
|
||||
sseEmitterHelper.sendComplete(user.getId(), sseEmitter, updatedRuntime.getOutput());
|
||||
}
|
||||
|
||||
private void errorWhenExe(Exception e) {
|
||||
log.error("error", e);
|
||||
String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.EXCEPTION.getValue());
|
||||
String errorMsg = e.getMessage();
|
||||
if (errorMsg.contains("parallel node doesn't support conditional branch")) {
|
||||
if (errorMsg != null && errorMsg.contains("parallel node doesn't support conditional branch")) {
|
||||
errorMsg = "并行节点中不能包含条件分<EFBFBD>?";
|
||||
}
|
||||
errorMsg = nodeMessageTemplate + errorMsg;
|
||||
errorMsg = nodeMessageTemplate + (errorMsg != null ? errorMsg : e.getClass().getSimpleName());
|
||||
// 保存会话信息且发送驱动消息事件
|
||||
WorkflowMessageUtil.saveWorkflowMessage(wfState, errorMsg);
|
||||
sseEmitterHelper.sendErrorAndComplete(user.getId(), sseEmitter, errorMsg);
|
||||
@@ -267,6 +224,10 @@ public class WorkflowEngine {
|
||||
log.info("node:{},chunk:{}", node, chunk);
|
||||
SSEEmitterHelper.parseAndSendPartialMsg(sseEmitter, "[NODE_CHUNK_" + node + "]", chunk);
|
||||
} else {
|
||||
// __END__ 是 langgraph4j 的终止伪节点, 无对应业务节点状态, 跳过
|
||||
if (END.equals(out.node())) {
|
||||
continue;
|
||||
}
|
||||
AbstractWfNode abstractWfNode = wfState.getCompletedNodes().stream()
|
||||
.filter(item -> item.getNode().getUuid().endsWith(out.node())).findFirst().orElse(null);
|
||||
if (null != abstractWfNode) {
|
||||
|
||||
@@ -19,7 +19,6 @@ import static org.bsc.langgraph4j.StateGraph.END;
|
||||
import static org.bsc.langgraph4j.StateGraph.START;
|
||||
import static org.bsc.langgraph4j.action.AsyncEdgeAction.edge_async;
|
||||
import static org.bsc.langgraph4j.action.AsyncNodeAction.node_async;
|
||||
import static org.ruoyi.workflow.workflow.WfComponentNameEnum.HUMAN_FEEDBACK;
|
||||
|
||||
/**
|
||||
* 负责构建工作流运行所依赖的状态图<E68081>?
|
||||
@@ -27,7 +26,6 @@ import static org.ruoyi.workflow.workflow.WfComponentNameEnum.HUMAN_FEEDBACK;
|
||||
@Slf4j
|
||||
public class WorkflowGraphBuilder {
|
||||
|
||||
private final Map<Long, WorkflowComponent> componentIndex;
|
||||
private final Map<String, WorkflowNode> nodeIndex;
|
||||
private final Map<String, List<WorkflowEdge>> edgesBySource;
|
||||
private final Map<String, List<WorkflowEdge>> edgesByTarget;
|
||||
@@ -46,8 +44,6 @@ public class WorkflowGraphBuilder {
|
||||
List<WorkflowEdge> edges,
|
||||
WorkflowNodeRunner nodeRunner,
|
||||
WfState wfState) {
|
||||
this.componentIndex = components.stream()
|
||||
.collect(Collectors.toMap(WorkflowComponent::getId, Function.identity(), (origin, ignore) -> origin));
|
||||
this.nodeIndex = nodes.stream()
|
||||
.collect(Collectors.toMap(WorkflowNode::getUuid, Function.identity(), (origin, ignore) -> origin));
|
||||
this.edgesBySource = edges.stream().collect(Collectors.groupingBy(WorkflowEdge::getSourceNodeUuid));
|
||||
@@ -217,14 +213,6 @@ public class WorkflowGraphBuilder {
|
||||
WorkflowNode wfNode = getNodeByUuid(stateGraphNodeUuid);
|
||||
stateGraph.addNode(stateGraphNodeUuid, node_async(state -> nodeRunner.run(wfNode, state)));
|
||||
stateGraphList.add(stateGraph);
|
||||
|
||||
WorkflowComponent component = componentIndex.get(wfNode.getWorkflowComponentId());
|
||||
if (component == null) {
|
||||
throw new BaseException(ErrorEnum.A_PARAMS_ERROR.getInfo());
|
||||
}
|
||||
if (HUMAN_FEEDBACK.getName().equals(component.getName())) {
|
||||
wfState.addInterruptNode(stateGraphNodeUuid);
|
||||
}
|
||||
}
|
||||
|
||||
private void addEdgeToStateGraph(StateGraph<WfNodeState> stateGraph, String source, String target) throws GraphStateException {
|
||||
|
||||
@@ -6,9 +6,9 @@ import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.entity.User;
|
||||
import org.ruoyi.common.chat.service.workFlow.IWorkFlowStarterService;
|
||||
import org.ruoyi.common.core.exception.base.BaseException;
|
||||
import org.ruoyi.common.satoken.utils.LoginHelper;
|
||||
import org.ruoyi.common.sse.core.SseEmitterManager;
|
||||
import org.ruoyi.common.tenant.helper.TenantHelper;
|
||||
import org.ruoyi.workflow.entity.*;
|
||||
import org.ruoyi.workflow.helper.SSEEmitterHelper;
|
||||
import org.ruoyi.workflow.service.*;
|
||||
@@ -58,6 +58,8 @@ public class WorkflowStarter implements IWorkFlowStarterService {
|
||||
Long userId = LoginHelper.getUserId();
|
||||
// 获取登录Token(仅透传给 WfState,工作流 SSE 通过 emitter 直发,不串台)
|
||||
String tokenValue = StpUtil.getTokenValue();
|
||||
// 获取当前租户ID(@Async 线程不继承请求线程的租户上下文,需显式透传)
|
||||
String tenantId = TenantHelper.getTenantId();
|
||||
// 根据会话ID连接SSE对象(每会话一个连接,避免同用户多会话串台)
|
||||
SseEmitter sseEmitter = sseEmitterManager.connect(String.valueOf(sessionId));
|
||||
if (!sseEmitterHelper.checkOrComplete(user, sseEmitter)) {
|
||||
@@ -71,41 +73,33 @@ public class WorkflowStarter implements IWorkFlowStarterService {
|
||||
sseEmitterHelper.sendErrorAndComplete(user.getId(), sseEmitter, A_WF_DISABLED.getInfo());
|
||||
return sseEmitter;
|
||||
}
|
||||
self.asyncRun(user, workflow, userInputs, sseEmitter, userId, tokenValue, sessionId);
|
||||
self.asyncRun(user, workflow, userInputs, sseEmitter, userId, tokenValue, sessionId, tenantId);
|
||||
return sseEmitter;
|
||||
}
|
||||
|
||||
@Async
|
||||
public void asyncRun(User user, Workflow workflow, List<ObjectNode> userInputs, SseEmitter sseEmitter, Long userId, String tokenValue, Long sessionId) {
|
||||
log.info("WorkflowEngine run,userId:{},workflowUuid:{},userInputs:{}", user.getId(), workflow.getUuid(), userInputs);
|
||||
List<WorkflowComponent> components = workflowComponentService.getAllEnable();
|
||||
List<WorkflowNode> nodes = workflowNodeService.lambdaQuery()
|
||||
.eq(WorkflowNode::getWorkflowId, workflow.getId())
|
||||
.eq(WorkflowNode::getIsDeleted, false)
|
||||
.list();
|
||||
List<WorkflowEdge> edges = workflowEdgeService.lambdaQuery()
|
||||
.eq(WorkflowEdge::getWorkflowId, workflow.getId())
|
||||
.eq(WorkflowEdge::getIsDeleted, false)
|
||||
.list();
|
||||
WorkflowEngine workflowEngine = new WorkflowEngine(workflow,
|
||||
sseEmitterHelper, components, nodes, edges,
|
||||
workflowRuntimeService, workflowRuntimeNodeService);
|
||||
workflowEngine.run(user, userInputs, sseEmitter, userId, tokenValue, sessionId);
|
||||
}
|
||||
|
||||
@Async
|
||||
public void resumeFlow(String runtimeUuid, String userInput, SseEmitter sseEmitter) {
|
||||
WorkflowEngine workflowEngine = InterruptedFlow.RUNTIME_TO_GRAPH.get(runtimeUuid);
|
||||
if (null == workflowEngine) {
|
||||
log.error("工作流恢复执行时失败,runtime:{}", runtimeUuid);
|
||||
throw new BaseException(A_WF_RESUME_FAIL.getInfo());
|
||||
public void asyncRun(User user, Workflow workflow, List<ObjectNode> userInputs, SseEmitter sseEmitter, Long userId, String tokenValue, Long sessionId, String tenantId) {
|
||||
// @Async 线程不继承请求线程的租户上下文, 显式设置, 避免租户缓存/隔离逻辑异常
|
||||
if (tenantId != null) {
|
||||
TenantHelper.setDynamic(tenantId);
|
||||
}
|
||||
// 如果SSE连接对象不为空传入该对象(Chat调用工作流对话使用)
|
||||
if (null != sseEmitter){
|
||||
workflowEngine.setSseEmitter(sseEmitter);
|
||||
// 为了让每个节点都可以发送模板消息 保持SSE对象一致(以防出现向已关闭的SSE对象发送消息)
|
||||
workflowEngine.getWfState().setSseEmitter(sseEmitter);
|
||||
try {
|
||||
log.info("WorkflowEngine run,userId:{},workflowUuid:{},userInputs:{}", user.getId(), workflow.getUuid(), userInputs);
|
||||
List<WorkflowComponent> components = workflowComponentService.getAllEnable();
|
||||
List<WorkflowNode> nodes = workflowNodeService.lambdaQuery()
|
||||
.eq(WorkflowNode::getWorkflowId, workflow.getId())
|
||||
.eq(WorkflowNode::getIsDeleted, false)
|
||||
.list();
|
||||
List<WorkflowEdge> edges = workflowEdgeService.lambdaQuery()
|
||||
.eq(WorkflowEdge::getWorkflowId, workflow.getId())
|
||||
.eq(WorkflowEdge::getIsDeleted, false)
|
||||
.list();
|
||||
WorkflowEngine workflowEngine = new WorkflowEngine(workflow,
|
||||
sseEmitterHelper, components, nodes, edges,
|
||||
workflowRuntimeService, workflowRuntimeNodeService);
|
||||
workflowEngine.run(user, userInputs, sseEmitter, userId, tokenValue, sessionId);
|
||||
} finally {
|
||||
TenantHelper.clearDynamic();
|
||||
}
|
||||
workflowEngine.resume(userInput);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.ruoyi.workflow.workflow;
|
||||
|
||||
import cn.hutool.core.collection.CollStreamUtil;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.data.message.UserMessage;
|
||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||
@@ -20,7 +19,6 @@ import org.ruoyi.common.chat.factory.ImageServiceFactory;
|
||||
import org.ruoyi.workflow.base.NodeInputConfigTypeHandler;
|
||||
import org.ruoyi.workflow.entity.WorkflowNode;
|
||||
import org.ruoyi.workflow.enums.WfIODataTypeEnum;
|
||||
import org.ruoyi.workflow.util.JsonUtil;
|
||||
import org.ruoyi.workflow.workflow.data.NodeIOData;
|
||||
import org.ruoyi.workflow.workflow.data.NodeIODataContent;
|
||||
import org.ruoyi.workflow.workflow.def.WfNodeParamRef;
|
||||
@@ -88,22 +86,6 @@ public class WorkflowUtil{
|
||||
return result;
|
||||
}
|
||||
|
||||
public static String getHumanFeedbackTip(String nodeUuid, List<WorkflowNode> wfNodes) {
|
||||
WorkflowNode wfNode = wfNodes.stream()
|
||||
.filter(item -> item.getUuid().equals(nodeUuid))
|
||||
.findFirst().orElse(null);
|
||||
if (null == wfNode) {
|
||||
return "";
|
||||
}
|
||||
String wfNodeNodeConfig = wfNode.getNodeConfig();
|
||||
if (StrUtil.isBlank(wfNodeNodeConfig)) {
|
||||
return "";
|
||||
}
|
||||
Map<String, Object> map = JsonUtil.toMap(wfNodeNodeConfig);
|
||||
Object tip = map.getOrDefault("tip", "");
|
||||
return String.valueOf(tip);
|
||||
}
|
||||
|
||||
public void streamingInvokeLLM(WfState wfState, WfNodeState state, WorkflowNode node, String modelName,
|
||||
String prompt, String nodeMessageTemplate) {
|
||||
log.info("stream invoke, modelName: {}", modelName);
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package org.ruoyi.workflow.workflow.node.classifier;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class ClassifierNodeConfig {
|
||||
private List categories = new ArrayList<>();
|
||||
@JsonProperty("model_platform")
|
||||
private String modelPlatform;
|
||||
@JsonProperty("model_name")
|
||||
private String modelName;
|
||||
}
|
||||
@@ -3,23 +3,44 @@ package org.ruoyi.workflow.workflow.node.enmus;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 节点消息模板ConfigKey枚举
|
||||
* 节点消息模板ConfigKey枚举 <br/>
|
||||
* 模板优先从 sys_config 读取(可在系统管理-配置管理中自定义), 未配置时回退到 defaultTemplate 内置默认值
|
||||
*/
|
||||
@Getter
|
||||
public enum NodeMessageTemplateEnum {
|
||||
HTTP_REQUEST("node.httpRequest.template"),
|
||||
MAIL_SEND("node.mailsend.template"),
|
||||
IMAGE("node.image.template"),
|
||||
HUMAN_FEED_BACK("node.humanFeedback.template"),
|
||||
SWITCH("node.switch.template"),
|
||||
LLM_RESPONSE("node.llmAnswer.template"),
|
||||
KEYWORD_EXTRACTOR("node.keywordExtractor.template"),
|
||||
EXCEPTION("node.exception.template"),
|
||||
END("node.end.template");
|
||||
HTTP_REQUEST("node.httpRequest.template", "✅ HTTP请求节点:结束响应 - "),
|
||||
MAIL_SEND("node.mailsend.template", "📧 发送邮箱节点:结束响应 - "),
|
||||
IMAGE("node.image.template", "🎨 文生图节点:结束响应 - 图片URL: "),
|
||||
SWITCH("node.switch.template", "🔀 条件分支节点:触发 -> 跳转到节点 "),
|
||||
LLM_RESPONSE("node.llmAnswer.template", "🤖 LLM 节点 生成回答:"),
|
||||
GOOGLE_SEARCH("node.googleSearch.template", "🔍 网络搜索节点处理完成:"),
|
||||
EXCEPTION("node.exception.template", "🛑 工作流发生异常:"),
|
||||
END("node.end.template", "🔚 流程已执行完毕,如果您有其他需求,请随时重新发起请求。");
|
||||
|
||||
private final String value;
|
||||
|
||||
NodeMessageTemplateEnum(String value) {
|
||||
/**
|
||||
* 内置默认模板, sys_config 未配置对应键时使用
|
||||
*/
|
||||
private final String defaultTemplate;
|
||||
|
||||
NodeMessageTemplateEnum(String value, String defaultTemplate) {
|
||||
this.value = value;
|
||||
this.defaultTemplate = defaultTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 configKey 获取内置默认模板, 未知键返回空串
|
||||
*
|
||||
* @param configKey sys_config 配置键
|
||||
* @return 内置默认模板
|
||||
*/
|
||||
public static String getDefaultTemplate(String configKey) {
|
||||
for (NodeMessageTemplateEnum item : values()) {
|
||||
if (item.value.equals(configKey)) {
|
||||
return item.defaultTemplate;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package org.ruoyi.workflow.workflow.node.googleSearch;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.ruoyi.workflow.entity.WorkflowComponent;
|
||||
import org.ruoyi.workflow.entity.WorkflowNode;
|
||||
import org.ruoyi.workflow.util.JsonUtil;
|
||||
import org.ruoyi.workflow.util.SpringUtil;
|
||||
import org.ruoyi.workflow.workflow.NodeProcessResult;
|
||||
import org.ruoyi.workflow.workflow.WfNodeState;
|
||||
import org.ruoyi.workflow.workflow.WfState;
|
||||
import org.ruoyi.workflow.workflow.WorkflowUtil;
|
||||
import org.ruoyi.workflow.workflow.data.NodeIOData;
|
||||
import org.ruoyi.workflow.workflow.node.AbstractWfNode;
|
||||
import org.ruoyi.workflow.workflow.node.enmus.NodeMessageTemplateEnum;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.DEFAULT_OUTPUT_PARAM_NAME;
|
||||
|
||||
/**
|
||||
* 【扩展节点】网络搜索
|
||||
* 通过智谱 Web Search API 返回适合大模型消费的结构化网页结果。
|
||||
*/
|
||||
@Slf4j
|
||||
public class GoogleSearchNode extends AbstractWfNode {
|
||||
|
||||
public GoogleSearchNode(WorkflowComponent wfComponent, WorkflowNode nodeDef, WfState wfState, WfNodeState nodeState) {
|
||||
super(wfComponent, nodeDef, wfState, nodeState);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理搜索请求
|
||||
* nodeConfig 格式:
|
||||
* {
|
||||
* "query": "搜索关键词",
|
||||
* "search_engine": "search_std",
|
||||
* "result_count": 10,
|
||||
* "search_domain_filter": "",
|
||||
* "search_recency_filter": "noLimit",
|
||||
* "content_size": "medium",
|
||||
* "include_image": false
|
||||
* }
|
||||
*
|
||||
* @return 搜索结果
|
||||
*/
|
||||
@Override
|
||||
public NodeProcessResult onProcess() {
|
||||
GoogleSearchNodeConfig config = checkAndGetConfig(GoogleSearchNodeConfig.class);
|
||||
|
||||
// 获取搜索关键词
|
||||
String searchQuery = WorkflowUtil.renderTemplate(config.getQuery(), state.getInputs());
|
||||
if (StringUtils.isBlank(searchQuery)) {
|
||||
searchQuery = getFirstInputText();
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(searchQuery)) {
|
||||
throw new IllegalArgumentException("未提供搜索关键词");
|
||||
}
|
||||
searchQuery = searchQuery.trim();
|
||||
if (searchQuery.length() > 70) {
|
||||
throw new IllegalArgumentException("搜索关键词不能超过 70 个字符");
|
||||
}
|
||||
|
||||
log.info("Web search node processing, engine: {}, result_count: {}",
|
||||
config.getSearchEngine(), config.getResultCount());
|
||||
|
||||
String nodeMessageTemplate = getNodeMessageTemplate(NodeMessageTemplateEnum.GOOGLE_SEARCH.getValue());
|
||||
notifyAndStoreMessage(wfState, nodeMessageTemplate);
|
||||
|
||||
ZhipuWebSearchClient searchClient = SpringUtil.getBean(ZhipuWebSearchClient.class);
|
||||
ZhipuWebSearchClient.SearchResponse response = searchClient.search(
|
||||
searchQuery,
|
||||
config,
|
||||
UUID.randomUUID().toString()
|
||||
);
|
||||
String searchResult = JsonUtil.toJson(response);
|
||||
if (searchResult == null) {
|
||||
throw new IllegalStateException("搜索结果序列化失败");
|
||||
}
|
||||
|
||||
log.info("Web search completed, result count: {}", response.count());
|
||||
notifyAndStoreMessage(wfState, nodeMessageTemplate + "返回 " + response.count() + " 条结果");
|
||||
|
||||
List<NodeIOData> outputs = List.of(
|
||||
NodeIOData.createByText(DEFAULT_OUTPUT_PARAM_NAME, "智谱网络搜索结果", searchResult)
|
||||
);
|
||||
return NodeProcessResult.builder().content(outputs).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.ruoyi.workflow.workflow.node.googleSearch;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class GoogleSearchNodeConfig {
|
||||
/**
|
||||
* 搜索查询关键词
|
||||
*/
|
||||
private String query;
|
||||
|
||||
@JsonProperty("search_engine")
|
||||
@Pattern(
|
||||
regexp = "search_std|search_pro|search_pro_sogou|search_pro_quark",
|
||||
message = "搜索引擎参数无效"
|
||||
)
|
||||
private String searchEngine = "search_std";
|
||||
|
||||
@JsonProperty("result_count")
|
||||
@Min(value = 1, message = "搜索结果数量不能小于 1")
|
||||
@Max(value = 50, message = "搜索结果数量不能大于 50")
|
||||
private Integer resultCount = 10;
|
||||
|
||||
@JsonProperty("search_domain_filter")
|
||||
private String searchDomainFilter;
|
||||
|
||||
@JsonProperty("search_recency_filter")
|
||||
@Pattern(
|
||||
regexp = "oneDay|oneWeek|oneMonth|oneYear|noLimit",
|
||||
message = "搜索时间范围参数无效"
|
||||
)
|
||||
private String searchRecencyFilter = "noLimit";
|
||||
|
||||
@JsonProperty("content_size")
|
||||
@Pattern(regexp = "medium|high", message = "网页摘要长度参数无效")
|
||||
private String contentSize = "medium";
|
||||
|
||||
@JsonProperty("include_image")
|
||||
private Boolean includeImage = false;
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package org.ruoyi.workflow.workflow.node.googleSearch;
|
||||
|
||||
import ai.z.openapi.ZhipuAiClient;
|
||||
import ai.z.openapi.service.web_search.WebSearchRequest;
|
||||
import ai.z.openapi.service.web_search.WebSearchResp;
|
||||
import ai.z.openapi.service.web_search.WebSearchResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.ruoyi.common.chat.domain.bo.chat.ChatModelBo;
|
||||
import org.ruoyi.common.chat.service.chat.IChatModelService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 智谱 Web Search 官方 SDK 适配器。
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ZhipuWebSearchClient {
|
||||
|
||||
private static final String ZHIPU_PROVIDER_CODE = "zhipu";
|
||||
private static final String DEFAULT_BASE_URL = "https://open.bigmodel.cn/api/paas/v4/";
|
||||
|
||||
private final ZhipuWebSearchProperties properties;
|
||||
private final IChatModelService chatModelService;
|
||||
|
||||
public SearchResponse search(String query, GoogleSearchNodeConfig config, String requestId) {
|
||||
Credential credential = resolveCredential();
|
||||
ZhipuAiClient client = createClient(credential);
|
||||
try {
|
||||
WebSearchRequest request = WebSearchRequest.builder()
|
||||
.searchQuery(query)
|
||||
.searchEngine(config.getSearchEngine())
|
||||
.count(config.getResultCount())
|
||||
.searchDomainFilter(blankToNull(config.getSearchDomainFilter()))
|
||||
.searchRecencyFilter(config.getSearchRecencyFilter())
|
||||
.contentSize(config.getContentSize())
|
||||
.includeImage(config.getIncludeImage())
|
||||
.requestId(requestId)
|
||||
.build();
|
||||
|
||||
WebSearchResponse response = client.webSearch().createWebSearch(request);
|
||||
if (response == null || !response.isSuccess() || response.getData() == null) {
|
||||
String message = response == null ? "接口未返回响应" : StringUtils.defaultIfBlank(response.getMsg(), "未知错误");
|
||||
throw new IllegalStateException("智谱 Web Search 调用失败:" + message);
|
||||
}
|
||||
|
||||
List<SearchResult> results = response.getData().getWebSearchResp() == null
|
||||
? List.of()
|
||||
: response.getData().getWebSearchResp().stream()
|
||||
.map(this::toSearchResult)
|
||||
.toList();
|
||||
|
||||
return new SearchResponse(
|
||||
query,
|
||||
config.getSearchEngine(),
|
||||
response.getData().getRequestId(),
|
||||
results.size(),
|
||||
results
|
||||
);
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
private ZhipuAiClient createClient(Credential credential) {
|
||||
int connectTimeout = positiveOrDefault(properties.getConnectTimeout(), 10);
|
||||
int readTimeout = positiveOrDefault(properties.getReadTimeout(), 30);
|
||||
return ZhipuAiClient.builder()
|
||||
.ofZHIPU()
|
||||
.apiKey(credential.apiKey())
|
||||
.baseUrl(credential.baseUrl())
|
||||
.networkConfig(connectTimeout, readTimeout, readTimeout, readTimeout, TimeUnit.SECONDS)
|
||||
.enableTokenCache()
|
||||
.build();
|
||||
}
|
||||
|
||||
private Credential resolveCredential() {
|
||||
if (isUsableApiKey(properties.getApiKey())) {
|
||||
return new Credential(normalizeBaseUrl(properties.getBaseUrl()), properties.getApiKey().trim());
|
||||
}
|
||||
|
||||
ChatModelBo query = new ChatModelBo();
|
||||
query.setProviderCode(ZHIPU_PROVIDER_CODE);
|
||||
return chatModelService.queryList(query).stream()
|
||||
.filter(model -> isUsableApiKey(model.getApiKey()))
|
||||
.findFirst()
|
||||
.map(model -> new Credential(normalizeBaseUrl(model.getApiHost()), model.getApiKey().trim()))
|
||||
.orElseThrow(() -> new IllegalStateException(
|
||||
"未配置智谱 API Key,请设置环境变量 ZAI_API_KEY,或在模型管理中配置 zhipu 厂商密钥"
|
||||
));
|
||||
}
|
||||
|
||||
private boolean isUsableApiKey(String apiKey) {
|
||||
return StringUtils.isNotBlank(apiKey)
|
||||
&& !"sk_xx".equalsIgnoreCase(apiKey.trim())
|
||||
&& !"your_api_key".equalsIgnoreCase(apiKey.trim());
|
||||
}
|
||||
|
||||
private String normalizeBaseUrl(String baseUrl) {
|
||||
String normalized = StringUtils.defaultIfBlank(baseUrl, DEFAULT_BASE_URL).trim();
|
||||
normalized = StringUtils.removeEnd(normalized, "/");
|
||||
if (!normalized.endsWith("/api/paas/v4")) {
|
||||
normalized += "/api/paas/v4";
|
||||
}
|
||||
return normalized + "/";
|
||||
}
|
||||
|
||||
private int positiveOrDefault(Integer value, int defaultValue) {
|
||||
return value != null && value > 0 ? value : defaultValue;
|
||||
}
|
||||
|
||||
private String blankToNull(String value) {
|
||||
return StringUtils.isBlank(value) ? null : value.trim();
|
||||
}
|
||||
|
||||
private SearchResult toSearchResult(WebSearchResp result) {
|
||||
return new SearchResult(
|
||||
result.getTitle(),
|
||||
result.getContent(),
|
||||
result.getLink(),
|
||||
result.getMedia(),
|
||||
result.getIcon(),
|
||||
result.getRefer(),
|
||||
result.getPublishDate(),
|
||||
result.getImages()
|
||||
);
|
||||
}
|
||||
|
||||
private record Credential(String baseUrl, String apiKey) {
|
||||
}
|
||||
|
||||
public record SearchResponse(
|
||||
String query,
|
||||
String searchEngine,
|
||||
String requestId,
|
||||
int count,
|
||||
List<SearchResult> results
|
||||
) {
|
||||
}
|
||||
|
||||
public record SearchResult(
|
||||
String title,
|
||||
String content,
|
||||
String link,
|
||||
String media,
|
||||
String icon,
|
||||
String refer,
|
||||
String publishDate,
|
||||
List<String> images
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.ruoyi.workflow.workflow.node.googleSearch;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 智谱 Web Search 配置。
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "workflow.web-search.zhipu")
|
||||
public class ZhipuWebSearchProperties {
|
||||
|
||||
/**
|
||||
* 智谱国内开放平台 API 根地址。
|
||||
*/
|
||||
private String baseUrl = "https://open.bigmodel.cn/api/paas/v4/";
|
||||
|
||||
/**
|
||||
* 智谱 API Key。建议通过环境变量 ZAI_API_KEY 注入。
|
||||
*/
|
||||
private String apiKey;
|
||||
|
||||
/**
|
||||
* 连接超时秒数。
|
||||
*/
|
||||
private Integer connectTimeout = 10;
|
||||
|
||||
/**
|
||||
* 读取超时秒数。
|
||||
*/
|
||||
private Integer readTimeout = 30;
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package org.ruoyi.workflow.workflow.node.humanFeedBack;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.ruoyi.workflow.entity.WorkflowComponent;
|
||||
import org.ruoyi.workflow.entity.WorkflowNode;
|
||||
import org.ruoyi.workflow.workflow.NodeProcessResult;
|
||||
import org.ruoyi.workflow.workflow.WfNodeState;
|
||||
import org.ruoyi.workflow.workflow.WfState;
|
||||
import org.ruoyi.workflow.workflow.WorkflowUtil;
|
||||
import org.ruoyi.workflow.workflow.data.NodeIOData;
|
||||
import org.ruoyi.workflow.workflow.node.AbstractWfNode;
|
||||
|
||||
import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.*;
|
||||
|
||||
/**
|
||||
* 人机交互节点实现类
|
||||
*/
|
||||
@Slf4j
|
||||
public class HumanFeedbackNode extends AbstractWfNode {
|
||||
|
||||
public HumanFeedbackNode(WorkflowComponent component, WorkflowNode nodeDefinition, WfState wfState, WfNodeState nodeState) {
|
||||
super(component, nodeDefinition, wfState, nodeState);
|
||||
}
|
||||
|
||||
// 人机交互节点的处理逻辑
|
||||
@Override
|
||||
public NodeProcessResult onProcess() {
|
||||
log.info("Processing HumanFeedback node: {}", node.getTitle());
|
||||
// 从状态中获取用户输入数据
|
||||
Object humanFeedbackState = state.data().get(HUMAN_FEEDBACK_KEY);
|
||||
if (null != humanFeedbackState) {
|
||||
String userInput = humanFeedbackState.toString();
|
||||
if (StringUtils.isNotBlank(userInput)) {
|
||||
// 用户已提供输入,将用户输入添加到节点输入和输出中
|
||||
NodeIOData feedbackData = NodeIOData.createByText("output", "default", userInput);
|
||||
// 添加到输出列表,这样后续节点可以使用
|
||||
state.getOutputs().add(feedbackData);
|
||||
// 设置为成功状态
|
||||
state.setProcessStatus(NODE_PROCESS_STATUS_SUCCESS);
|
||||
log.info("Human feedback processed for node: {}, content: {}", node.getTitle(), userInput);
|
||||
} else {
|
||||
// 用户输入为空,设置等待状态
|
||||
state.setProcessStatus(NODE_PROCESS_STATUS_DOING);
|
||||
log.info("Human feedback is empty for node: {}", node.getTitle());
|
||||
}
|
||||
} else {
|
||||
// 没有用户输入,这可能是正常情况(等待用户输入)
|
||||
// 但为了确保流程可以继续,我们仍然标记为成功
|
||||
state.setProcessStatus(NODE_PROCESS_STATUS_SUCCESS);
|
||||
log.info("No human feedback found for node: {}, continuing workflow", node.getTitle());
|
||||
}
|
||||
return new NodeProcessResult();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
package org.ruoyi.workflow.workflow.node.keywordExtractor;
|
||||
|
||||
import dev.langchain4j.data.message.SystemMessage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.ruoyi.workflow.entity.WorkflowComponent;
|
||||
import org.ruoyi.workflow.entity.WorkflowNode;
|
||||
import org.ruoyi.workflow.util.SpringUtil;
|
||||
import org.ruoyi.workflow.util.WorkflowMessageUtil;
|
||||
import org.ruoyi.workflow.workflow.NodeProcessResult;
|
||||
import org.ruoyi.workflow.workflow.WfNodeState;
|
||||
import org.ruoyi.workflow.workflow.WfState;
|
||||
import org.ruoyi.workflow.workflow.WorkflowUtil;
|
||||
import org.ruoyi.workflow.workflow.data.NodeIOData;
|
||||
import org.ruoyi.workflow.workflow.node.AbstractWfNode;
|
||||
import org.ruoyi.workflow.workflow.node.enmus.NodeMessageTemplateEnum;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.DEFAULT_OUTPUT_PARAM_NAME;
|
||||
|
||||
/**
|
||||
* 【节点】关键词提取节点
|
||||
* 使用 LLM 从文本中提取关键词
|
||||
*/
|
||||
@Slf4j
|
||||
public class KeywordExtractorNode extends AbstractWfNode {
|
||||
|
||||
public KeywordExtractorNode(WorkflowComponent wfComponent, WorkflowNode nodeDef, WfState wfState, WfNodeState nodeState) {
|
||||
super(wfComponent, nodeDef, wfState, nodeState);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理关键词提取
|
||||
* nodeConfig 格式:
|
||||
* {
|
||||
* "model_name": "deepseek-chat",
|
||||
* "category": "llm",
|
||||
* "top_n": 5,
|
||||
* "prompt": "额外的提示词"
|
||||
* }
|
||||
*
|
||||
* @return 提取的关键词列表
|
||||
*/
|
||||
@Override
|
||||
public NodeProcessResult onProcess() {
|
||||
KeywordExtractorNodeConfig config = checkAndGetConfig(KeywordExtractorNodeConfig.class);
|
||||
|
||||
// 获取输入文本
|
||||
String inputText = getFirstInputText();
|
||||
if (StringUtils.isBlank(inputText)) {
|
||||
log.warn("Keyword extractor node has no input text, node: {}", state.getUuid());
|
||||
// 返回空结果
|
||||
List<NodeIOData> outputs = new ArrayList<>();
|
||||
outputs.add(NodeIOData.createByText(DEFAULT_OUTPUT_PARAM_NAME, "", ""));
|
||||
return NodeProcessResult.builder().content(outputs).build();
|
||||
}
|
||||
|
||||
log.info("Keyword extractor node config: {}", config);
|
||||
log.info("Input text length: {}", inputText.length());
|
||||
|
||||
// 构建提示词
|
||||
String prompt = buildPrompt(config, inputText);
|
||||
log.info("Keyword extraction prompt: {}", prompt);
|
||||
|
||||
// 调用 LLM 进行关键词提取
|
||||
WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class);
|
||||
String modelName = config.getModelName();
|
||||
// 获取节点模板提示词信息
|
||||
String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.KEYWORD_EXTRACTOR.getValue());
|
||||
// 发送SSE事件消息
|
||||
WorkflowMessageUtil.sendEmitterMessage(wfState.getSseEmitter(), node, nodeMessageTemplate);
|
||||
// 使用流式调用
|
||||
workflowUtil.streamingInvokeLLM(wfState, state, node, modelName, prompt, nodeMessageTemplate);
|
||||
return new NodeProcessResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建关键词提取的提示词
|
||||
*/
|
||||
private String buildPrompt(KeywordExtractorNodeConfig config, String inputText) {
|
||||
StringBuilder promptBuilder = new StringBuilder();
|
||||
|
||||
// 基础提示词
|
||||
promptBuilder.append("请从以下文本中提取 ").append(config.getTopN()).append(" 个最重要的关键词。\n\n");
|
||||
|
||||
// 添加自定义提示词(如果有)
|
||||
if (StringUtils.isNotBlank(config.getPrompt())) {
|
||||
promptBuilder.append(config.getPrompt()).append("\n\n");
|
||||
}
|
||||
|
||||
// 输出格式要求
|
||||
promptBuilder.append("要求:\n");
|
||||
promptBuilder.append("1. 只返回关键词,每个关键词用逗号分隔\n");
|
||||
promptBuilder.append("2. 关键词应该是名词或名词短语\n");
|
||||
promptBuilder.append("3. 按重要性从高到低排序\n");
|
||||
promptBuilder.append("4. 不要添加任何解释或额外的文字\n\n");
|
||||
|
||||
// 原始文本
|
||||
promptBuilder.append("文本内容:\n");
|
||||
promptBuilder.append(inputText);
|
||||
|
||||
return promptBuilder.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package org.ruoyi.workflow.workflow.node.keywordExtractor;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 关键词提取节点配置
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
@Data
|
||||
public class KeywordExtractorNodeConfig {
|
||||
|
||||
/**
|
||||
* 模型分类(如:llm, embedding 等)
|
||||
*/
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
@NotNull
|
||||
@JsonProperty("model_name")
|
||||
private String modelName;
|
||||
|
||||
/**
|
||||
* 提取的关键词数量
|
||||
*/
|
||||
@Min(1)
|
||||
@Max(50)
|
||||
@JsonProperty("top_n")
|
||||
private Integer topN = 5;
|
||||
|
||||
/**
|
||||
* 提示词(可选)
|
||||
* 用于指导关键词提取的额外说明
|
||||
*/
|
||||
private String prompt;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.ruoyi.workflow.entity.WorkflowComponent;
|
||||
import org.ruoyi.workflow.entity.WorkflowNode;
|
||||
import org.ruoyi.workflow.util.JsonUtil;
|
||||
import org.ruoyi.workflow.workflow.NodeProcessResult;
|
||||
import org.ruoyi.workflow.workflow.WfNodeState;
|
||||
import org.ruoyi.workflow.workflow.WfState;
|
||||
@@ -40,15 +41,25 @@ public class MailSendNode extends AbstractWfNode {
|
||||
String input = getDataFromInput(inputs);
|
||||
// 判断是否为JSON格式(LLM输出转换 由LLM生成格式)
|
||||
if (StringUtils.isNotBlank(input) && isJson(input)) {
|
||||
// 使用Jackson解析和合并配置
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
JsonNode inputJson = objectMapper.readTree(input);
|
||||
// 将config转换为JsonNode
|
||||
JsonNode configJson = objectMapper.valueToTree(config);
|
||||
// 合并两个JSON节点
|
||||
JsonNode mergedJson = objectMapper.readerForUpdating(configJson).readValue(inputJson);
|
||||
// 转换回config对象
|
||||
config = objectMapper.treeToValue(mergedJson, MailSendNodeConfig.class);
|
||||
try {
|
||||
// 使用统一的 JsonUtil 进行解析
|
||||
JsonNode inputJson = JsonUtil.toJsonNode(input);
|
||||
if (inputJson != null) {
|
||||
// 使用 JsonUtil 内部的 ObjectMapper 进行合并
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
// 将config转换为JsonNode
|
||||
JsonNode configJson = objectMapper.valueToTree(config);
|
||||
// 合并两个JSON节点
|
||||
JsonNode mergedJson = objectMapper.readerForUpdating(configJson).readValue(inputJson);
|
||||
// 转换回config对象
|
||||
config = objectMapper.treeToValue(mergedJson, MailSendNodeConfig.class);
|
||||
} else {
|
||||
log.warn("输入 JSON 解析结果为 null,使用原始配置");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("合并邮件配置失败,使用原始配置: {}", e.getMessage(), e);
|
||||
// 继续使用原始 config,不中断流程
|
||||
}
|
||||
}
|
||||
|
||||
// 安全获取模板(使用 defaultString 避免 null)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.ruoyi.workflow.workflow.node.switcher;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -9,6 +8,7 @@ import org.ruoyi.common.core.utils.SpringUtils;
|
||||
import org.ruoyi.workflow.entity.WorkflowComponent;
|
||||
import org.ruoyi.workflow.entity.WorkflowNode;
|
||||
import org.ruoyi.workflow.service.WorkflowNodeService;
|
||||
import org.ruoyi.workflow.util.JsonUtil;
|
||||
import org.ruoyi.workflow.workflow.NodeProcessResult;
|
||||
import org.ruoyi.workflow.workflow.WfNodeState;
|
||||
import org.ruoyi.workflow.workflow.WfState;
|
||||
@@ -18,8 +18,6 @@ import org.ruoyi.workflow.workflow.node.enmus.NodeMessageTemplateEnum;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 条件分支节点
|
||||
@@ -339,9 +337,12 @@ public class SwitcherNode extends AbstractWfNode {
|
||||
log.info("节点 '{}' 的输入配置: {}", nodeUuid, inputConfig);
|
||||
if (StringUtils.isNotBlank(inputConfig)){
|
||||
try {
|
||||
// 使用Jackson解析输入配置
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
JsonNode configJson = objectMapper.readTree(inputConfig);
|
||||
// 使用统一的 JsonUtil 而不是每次创建新的 ObjectMapper
|
||||
JsonNode configJson = JsonUtil.toJsonNode(inputConfig);
|
||||
if (configJson == null) {
|
||||
log.warn("节点 '{}' 的输入配置 JSON 解析结果为 null", nodeUuid);
|
||||
return result;
|
||||
}
|
||||
// 获取 user_inputs 数组
|
||||
JsonNode userInputs = configJson.get("user_inputs");
|
||||
if (userInputs != null && userInputs.isArray()) {
|
||||
@@ -357,7 +358,8 @@ public class SwitcherNode extends AbstractWfNode {
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("解析节点输入配置失败: {}", nodeUuid, e);
|
||||
log.error("解析节点 '{}' 输入配置失败,参数名: {}, 配置内容: {}", nodeUuid, paramName, inputConfig, e);
|
||||
// 不抛出异常,返回默认结果,避免中断整个流程
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
-- =============================================
|
||||
-- 流程编排搜索节点配置脚本
|
||||
-- =============================================
|
||||
-- 说明:本脚本用于添加搜索节点的系统配置
|
||||
-- 执行前请确保 sys_config 表存在
|
||||
-- =============================================
|
||||
|
||||
-- 搜索节点模板配置
|
||||
INSERT INTO sys_config (config_name, config_key, config_value, config_type, remark, create_by, create_time, update_by, update_time)
|
||||
VALUES ('搜索节点模板', 'node.googleSearch.template', '正在搜索相关内容...', 'Y', '搜索节点的响应模板,用于网络搜索功能', 'admin', NOW(), 'admin', NOW())
|
||||
ON DUPLICATE KEY UPDATE config_value = '正在搜索相关内容...', update_time = NOW();
|
||||
|
||||
-- =============================================
|
||||
-- 验证配置是否添加成功
|
||||
-- =============================================
|
||||
SELECT config_id, config_name, config_key, config_value, config_type, remark
|
||||
FROM sys_config
|
||||
WHERE config_key IN (
|
||||
'node.googleSearch.template'
|
||||
)
|
||||
ORDER BY config_id;
|
||||
@@ -1,425 +0,0 @@
|
||||
# Ruoyi-AI 流程编排模块详细说明文档
|
||||
|
||||
## 概述
|
||||
|
||||
Ruoyi-AI 工作流模块是一个基于 LangGraph4j 的智能工作流引擎,支持可视化工作流设计、AI 模型集成、条件分支、人机交互等高级功能。该模块采用微服务架构,提供完整的
|
||||
RESTful API 和流式响应支持。
|
||||
|
||||
## 模块架构
|
||||
|
||||
### 1. 核心依赖
|
||||
|
||||
- **LangGraph4j**: 1.5.3 - 工作流图执行引擎
|
||||
- **LangChain4j**: 1.11.0 - AI 模型集成框架
|
||||
- **Spring Boot**: 3.5.8 - 应用框架
|
||||
- **MyBatis Plus**: 数据访问层
|
||||
- **Redis**: 缓存和状态管理
|
||||
- **OpenAPI**: API 文档
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 1. 工作流管理
|
||||
|
||||
#### 1.1 工作流定义
|
||||
|
||||
- **创建工作流**: 支持自定义标题、描述、公开性设置
|
||||
- **编辑工作流**: 可视化节点编辑、连接线配置
|
||||
- **版本控制**: 支持工作流的版本管理和回滚
|
||||
- **权限管理**: 支持公开/私有工作流设置
|
||||
|
||||
#### 1.2 工作流执行
|
||||
|
||||
- **流式执行**: 基于 SSE 的实时流式响应
|
||||
- **状态管理**: 完整的执行状态跟踪
|
||||
- **错误处理**: 详细的错误信息和异常处理
|
||||
- **中断恢复**: 支持工作流中断和恢复执行
|
||||
|
||||
### 2. 节点类型
|
||||
|
||||
#### 2.1 基础节点
|
||||
|
||||
- **Start**: 开始节点,定义工作流入口
|
||||
- **End**: 结束节点,定义工作流出口
|
||||
|
||||
|
||||
#### 2.2 AI 模型节点
|
||||
|
||||
- **Answer**: 大语言模型问答节点
|
||||
- **Dalle3**: DALL-E 3 图像生成
|
||||
- **Tongyiwanx**: 通义万相图像生成
|
||||
- **Classifier**: 内容分类节点
|
||||
|
||||
#### 2.3 数据处理节点
|
||||
|
||||
- **DocumentExtractor**: 文档信息提取
|
||||
- **KeywordExtractor**: 关键词提取
|
||||
- **FaqExtractor**: 常见问题提取
|
||||
- **KnowledgeRetrieval**: 知识库检索
|
||||
|
||||
#### 2.4 控制流节点
|
||||
|
||||
- **Switcher**: 条件分支节点
|
||||
- **HumanFeedback**: 人机交互节点
|
||||
|
||||
#### 2.5 外部集成节点
|
||||
|
||||
- **Google**: Google 搜索集成
|
||||
- **MailSend**: 邮件发送
|
||||
- **HttpRequest**: HTTP 请求
|
||||
- **Template**: 模板转换
|
||||
|
||||
### 3. 数据流管理
|
||||
|
||||
#### 3.1 输入输出定义
|
||||
|
||||
```java
|
||||
// 节点输入输出数据结构
|
||||
public class NodeIOData {
|
||||
private String name; // 参数名称
|
||||
private NodeIODataContent content; // 参数内容
|
||||
}
|
||||
|
||||
// 支持的数据类型
|
||||
public enum WfIODataTypeEnum {
|
||||
TEXT, // 文本
|
||||
NUMBER, // 数字
|
||||
BOOLEAN, // 布尔值
|
||||
FILES, // 文件
|
||||
OPTIONS // 选项
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2 参数引用
|
||||
|
||||
- **节点间引用**: 支持上游节点输出作为下游节点输入
|
||||
- **参数映射**: 自动处理参数名称映射
|
||||
- **类型转换**: 自动进行数据类型转换
|
||||
|
||||
## 数据库设计
|
||||
|
||||
### 1. 核心表结构
|
||||
|
||||
#### 1.1 工作流定义表 (t_workflow)
|
||||
|
||||
```sql
|
||||
CREATE TABLE t_workflow (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
uuid VARCHAR(32) NOT NULL DEFAULT '',
|
||||
title VARCHAR(100) NOT NULL DEFAULT '',
|
||||
remark TEXT NOT NULL DEFAULT '',
|
||||
user_id BIGINT NOT NULL DEFAULT 0,
|
||||
is_public TINYINT(1) NOT NULL DEFAULT 0,
|
||||
is_enable TINYINT(1) NOT NULL DEFAULT 1,
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
is_deleted TINYINT(1) NOT NULL DEFAULT 0
|
||||
);
|
||||
```
|
||||
|
||||
#### 1.2 工作流节点表 (t_workflow_node)
|
||||
|
||||
```sql
|
||||
CREATE TABLE t_workflow_node (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
uuid VARCHAR(32) NOT NULL DEFAULT '',
|
||||
workflow_id BIGINT NOT NULL DEFAULT 0,
|
||||
workflow_component_id BIGINT NOT NULL DEFAULT 0,
|
||||
user_id BIGINT NOT NULL DEFAULT 0,
|
||||
title VARCHAR(100) NOT NULL DEFAULT '',
|
||||
remark VARCHAR(500) NOT NULL DEFAULT '',
|
||||
input_config JSON NOT NULL DEFAULT ('{}'),
|
||||
node_config JSON NOT NULL DEFAULT ('{}'),
|
||||
position_x DOUBLE NOT NULL DEFAULT 0,
|
||||
position_y DOUBLE NOT NULL DEFAULT 0,
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
is_deleted TINYINT(1) NOT NULL DEFAULT 0
|
||||
);
|
||||
```
|
||||
|
||||
#### 1.3 工作流边表 (t_workflow_edge)
|
||||
|
||||
```sql
|
||||
CREATE TABLE t_workflow_edge (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
uuid VARCHAR(32) NOT NULL DEFAULT '',
|
||||
workflow_id BIGINT NOT NULL DEFAULT 0,
|
||||
source_node_uuid VARCHAR(32) NOT NULL DEFAULT '',
|
||||
source_handle VARCHAR(32) NOT NULL DEFAULT '',
|
||||
target_node_uuid VARCHAR(32) NOT NULL DEFAULT '',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
is_deleted TINYINT(1) NOT NULL DEFAULT 0
|
||||
);
|
||||
```
|
||||
|
||||
#### 1.4 工作流运行时表 (t_workflow_runtime)
|
||||
|
||||
```sql
|
||||
CREATE TABLE t_workflow_runtime (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
uuid VARCHAR(32) NOT NULL DEFAULT '',
|
||||
user_id BIGINT NOT NULL DEFAULT 0,
|
||||
workflow_id BIGINT NOT NULL DEFAULT 0,
|
||||
input JSON NOT NULL DEFAULT ('{}'),
|
||||
output JSON NOT NULL DEFAULT ('{}'),
|
||||
status SMALLINT NOT NULL DEFAULT 1,
|
||||
status_remark VARCHAR(250) NOT NULL DEFAULT '',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
is_deleted TINYINT(1) NOT NULL DEFAULT 0
|
||||
);
|
||||
```
|
||||
|
||||
#### 1.5 工作流组件表 (t_workflow_component)
|
||||
|
||||
```sql
|
||||
CREATE TABLE t_workflow_component (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
uuid VARCHAR(32) DEFAULT '' NOT NULL,
|
||||
name VARCHAR(32) DEFAULT '' NOT NULL,
|
||||
title VARCHAR(100) DEFAULT '' NOT NULL,
|
||||
remark TEXT NOT NULL,
|
||||
display_order INT DEFAULT 0 NOT NULL,
|
||||
is_enable TINYINT(1) DEFAULT 0 NOT NULL,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
is_deleted TINYINT(1) DEFAULT 0 NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
## API 接口
|
||||
|
||||
### 1. 工作流管理接口
|
||||
|
||||
#### 1.1 基础操作
|
||||
|
||||
```http
|
||||
# 创建工作流
|
||||
POST /workflow/add
|
||||
Content-Type: application/json
|
||||
{
|
||||
"title": "工作流标题",
|
||||
"remark": "工作流描述",
|
||||
"isPublic": false
|
||||
}
|
||||
|
||||
# 更新工作流
|
||||
POST /workflow/update
|
||||
Content-Type: application/json
|
||||
{
|
||||
"uuid": "工作流UUID",
|
||||
"title": "新标题",
|
||||
"remark": "新描述"
|
||||
}
|
||||
|
||||
# 删除工作流
|
||||
POST /workflow/del/{uuid}
|
||||
|
||||
# 启用/禁用工作流
|
||||
POST /workflow/enable/{uuid}?enable=true
|
||||
```
|
||||
|
||||
#### 1.2 搜索和查询
|
||||
|
||||
```http
|
||||
# 搜索我的工作流
|
||||
GET /workflow/mine/search?keyword=关键词&isPublic=true¤tPage=1&pageSize=10
|
||||
|
||||
# 搜索公开工作流
|
||||
GET /workflow/public/search?keyword=关键词¤tPage=1&pageSize=10
|
||||
|
||||
# 获取工作流组件列表
|
||||
GET /workflow/public/component/list
|
||||
```
|
||||
|
||||
### 2. 工作流执行接口
|
||||
|
||||
#### 2.1 流式执行
|
||||
|
||||
```http
|
||||
# 流式执行工作流
|
||||
POST /workflow/run
|
||||
Content-Type: application/json
|
||||
Accept: text/event-stream
|
||||
{
|
||||
"uuid": "工作流UUID",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "input",
|
||||
"content": {
|
||||
"type": 1,
|
||||
"textContent": "用户输入内容"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 运行时管理
|
||||
|
||||
```http
|
||||
# 恢复中断的工作流
|
||||
POST /workflow/runtime/resume/{runtimeUuid}
|
||||
Content-Type: application/json
|
||||
{
|
||||
"feedbackContent": "用户反馈内容"
|
||||
}
|
||||
|
||||
# 查询工作流执行历史
|
||||
GET /workflow/runtime/page?wfUuid=工作流UUID¤tPage=1&pageSize=10
|
||||
|
||||
# 查询运行时节点详情
|
||||
GET /workflow/runtime/nodes/{runtimeUuid}
|
||||
|
||||
# 清理运行时数据
|
||||
POST /workflow/runtime/clear?wfUuid=工作流UUID
|
||||
```
|
||||
|
||||
### 3. 管理端接口
|
||||
|
||||
#### 3.1 工作流管理
|
||||
|
||||
```http
|
||||
# 搜索所有工作流
|
||||
POST /admin/workflow/search
|
||||
Content-Type: application/json
|
||||
{
|
||||
"title": "搜索关键词",
|
||||
"isPublic": true,
|
||||
"isEnable": true
|
||||
}
|
||||
|
||||
# 启用/禁用工作流
|
||||
POST /admin/workflow/enable?uuid=工作流UUID&isEnable=true
|
||||
```
|
||||
|
||||
## 核心实现
|
||||
|
||||
### 1. 工作流引擎 (WorkflowEngine)
|
||||
|
||||
工作流引擎是整个模块的核心,负责:
|
||||
|
||||
- 工作流图的构建和编译
|
||||
- 节点执行调度
|
||||
- 状态管理和持久化
|
||||
- 流式输出处理
|
||||
|
||||
```java
|
||||
public class WorkflowEngine {
|
||||
// 核心执行方法
|
||||
public void run(User user, List<ObjectNode> userInputs, SseEmitter sseEmitter) {
|
||||
// 1. 验证工作流状态
|
||||
// 2. 创建运行时实例
|
||||
// 3. 构建状态图
|
||||
// 4. 执行工作流
|
||||
// 5. 处理流式输出
|
||||
}
|
||||
|
||||
// 恢复执行方法
|
||||
public void resume(String userInput) {
|
||||
// 1. 更新状态
|
||||
// 2. 继续执行
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 节点工厂 (WfNodeFactory)
|
||||
|
||||
节点工厂负责根据组件类型创建对应的节点实例:
|
||||
|
||||
```java
|
||||
public class WfNodeFactory {
|
||||
public static AbstractWfNode create(WorkflowComponent component,
|
||||
WorkflowNode node,
|
||||
WfState wfState,
|
||||
WfNodeState nodeState) {
|
||||
// 根据组件类型创建对应的节点实例
|
||||
switch (component.getName()) {
|
||||
case "Answer":
|
||||
return new LLMAnswerNode(component, node, wfState, nodeState);
|
||||
case "Switcher":
|
||||
return new SwitcherNode(component, node, wfState, nodeState);
|
||||
// ... 其他节点类型
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 图构建器 (WorkflowGraphBuilder)
|
||||
|
||||
图构建器负责将工作流定义转换为可执行的状态图:
|
||||
|
||||
```java
|
||||
public class WorkflowGraphBuilder {
|
||||
public StateGraph<WfNodeState> build(WorkflowNode startNode) {
|
||||
// 1. 构建编译节点树
|
||||
// 2. 转换为状态图
|
||||
// 3. 添加节点和边
|
||||
// 4. 处理条件分支
|
||||
// 5. 处理并行执行
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 流式响应机制
|
||||
|
||||
### 1. SSE 事件类型
|
||||
|
||||
工作流执行过程中会发送多种类型的 SSE 事件:
|
||||
|
||||
```javascript
|
||||
// 节点开始执行
|
||||
[NODE_RUN_节点UUID] - 节点执行开始事件
|
||||
|
||||
// 节点输入数据
|
||||
[NODE_INPUT_节点UUID] - 节点输入数据事件
|
||||
|
||||
// 节点输出数据
|
||||
[NODE_OUTPUT_节点UUID] - 节点输出数据事件
|
||||
|
||||
// 流式内容块
|
||||
[NODE_CHUNK_节点UUID] - 流式内容块事件
|
||||
|
||||
// 等待用户输入
|
||||
[NODE_WAIT_FEEDBACK_BY_节点UUID] - 等待用户输入事件
|
||||
```
|
||||
|
||||
### 2. 流式处理流程
|
||||
|
||||
1. **初始化**: 创建工作流运行时实例
|
||||
2. **节点执行**: 逐个执行工作流节点
|
||||
3. **实时输出**: 通过 SSE 实时推送执行结果
|
||||
4. **状态更新**: 实时更新节点和工作流状态
|
||||
5. **错误处理**: 捕获并处理执行过程中的错误
|
||||
|
||||
## 扩展开发
|
||||
|
||||
### 1. 自定义节点开发
|
||||
|
||||
要开发自定义工作流节点,需要:
|
||||
|
||||
1. **创建节点类**:继承 `AbstractWfNode`
|
||||
2. **实现处理逻辑**:重写 `onProcess()` 方法
|
||||
3. **定义配置类**:创建节点配置类
|
||||
4. **注册组件**:在组件表中注册新组件
|
||||
|
||||
```java
|
||||
public class CustomNode extends AbstractWfNode {
|
||||
@Override
|
||||
protected NodeProcessResult onProcess() {
|
||||
// 实现自定义处理逻辑
|
||||
List<NodeIOData> outputs = new ArrayList<>();
|
||||
// ... 处理逻辑
|
||||
return NodeProcessResult.success(outputs);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 自定义组件注册
|
||||
|
||||
```sql
|
||||
-- 在 t_workflow_component 表中添加新组件
|
||||
INSERT INTO t_workflow_component (uuid, name, title, remark, is_enable)
|
||||
VALUES (REPLACE(UUID(), '-', ''), 'CustomNode', '自定义节点', '自定义节点描述', true);
|
||||
```
|
||||
@@ -1,10 +1,8 @@
|
||||
package org.ruoyi.controller.chat;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.domain.dto.request.AgentChatRequest;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.service.chat.impl.ChatServiceFacade;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
@@ -150,35 +150,59 @@ public class ChatServiceFacade implements IChatService {
|
||||
* @return SseEmitter
|
||||
*/
|
||||
public SseEmitter sseChat(ChatRequest chatRequest) {
|
||||
|
||||
// 具体的服务实现
|
||||
Long userId = LoginHelper.getUserId();
|
||||
String tokenValue = StpUtil.getTokenValue();
|
||||
// 每个会话一个 SSE 连接,避免同用户多会话串台
|
||||
SseEmitter emitter = sseEmitterManager.connect(String.valueOf(chatRequest.getSessionId()));
|
||||
|
||||
boolean workflowMode = Boolean.TRUE.equals(chatRequest.getEnableWorkFlow());
|
||||
boolean agentMode = chatRequest.getAgentId() != null;
|
||||
if (workflowMode && agentMode) {
|
||||
throw new IllegalArgumentException("对话模式参数冲突:工作流和智能体不能同时启用");
|
||||
}
|
||||
|
||||
// 工作流模式。工作流引擎负责创建并持有自己的 SSE,必须在普通聊天连接创建前路由。
|
||||
if (workflowMode) {
|
||||
chatMessageService.saveChatMessage(
|
||||
userId,
|
||||
chatRequest.getSessionId(),
|
||||
chatRequest.getContent(),
|
||||
RoleType.USER.getName(),
|
||||
chatRequest.getModel()
|
||||
);
|
||||
return handleWorkflowChat(chatRequest);
|
||||
}
|
||||
|
||||
// 智能体解析:传入 agentId 时按智能体绑定的模型覆盖 model 字段
|
||||
AgentVo agentVo = null;
|
||||
if (chatRequest.getAgentId() != null) {
|
||||
if (agentMode) {
|
||||
agentVo = agentService.queryById(chatRequest.getAgentId());
|
||||
if (agentVo == null) {
|
||||
throw new IllegalArgumentException("智能体不存在: " + chatRequest.getAgentId());
|
||||
}
|
||||
if (agentVo != null && agentVo.getModelId() != null) {
|
||||
ChatModelVo agentModel = chatModelService.queryById(agentVo.getModelId());
|
||||
if (agentModel != null) {
|
||||
chatRequest.setModel(agentModel.getModelName());
|
||||
if (agentModel == null) {
|
||||
throw new IllegalArgumentException("智能体绑定的模型不存在: " + agentVo.getModelId());
|
||||
}
|
||||
} else {
|
||||
log.warn("智能体不存在或未配置模型,回退到 model 字段: agentId={}", chatRequest.getAgentId());
|
||||
chatRequest.setModel(agentModel.getModelName());
|
||||
}
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(chatRequest.getModel())) {
|
||||
throw new IllegalArgumentException(
|
||||
agentVo == null ? "对话模式必须指定模型" : "智能体未绑定模型,且请求未提供回退模型"
|
||||
);
|
||||
}
|
||||
|
||||
// 根据模型名称查询完整配置
|
||||
ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
|
||||
if (chatModelVo == null) {
|
||||
throw new IllegalArgumentException("模型不存在: " + chatRequest.getModel());
|
||||
}
|
||||
|
||||
// 对话和智能体模式共用按会话隔离的 SSE。
|
||||
SseEmitter emitter = sseEmitterManager.connect(String.valueOf(chatRequest.getSessionId()));
|
||||
|
||||
// 构建上下文消息列表(系统提示词 + 历史消息 + 当前用户消息)
|
||||
// 注意:RAG 检索增强统一在 handleAgentChat 中执行一次,此处不再重复检索
|
||||
List<ChatMessage> contextMessages = buildContextMessages(chatRequest, agentVo);
|
||||
|
||||
chatRequest.setEmitter(emitter);
|
||||
@@ -190,43 +214,68 @@ public class ChatServiceFacade implements IChatService {
|
||||
// 保存用户消息
|
||||
chatMessageService.saveChatMessage(userId, chatRequest.getSessionId(), chatRequest.getContent(), RoleType.USER.getName(), chatRequest.getModel());
|
||||
|
||||
TraceRunHandle traceRun = Boolean.TRUE.equals(chatRequest.getEnableWorkFlow())
|
||||
? null : startRagTraceRun(chatRequest, userId);
|
||||
// 3. 路由对话模式:工作流对话 / 智能体对话(两者均返回各自的 SseEmitter)
|
||||
return handleSpecialChatModes(chatRequest, agentVo, traceRun);
|
||||
}
|
||||
TraceRunHandle traceRun = startRagTraceRun(chatRequest, userId);
|
||||
|
||||
/**
|
||||
* 路由对话模式:仅两种情况——工作流对话 / 智能体对话。
|
||||
*
|
||||
* @param chatRequest 聊天请求
|
||||
* @param agentVo 智能体配置(可为 null)
|
||||
* @return 对应模式的 SseEmitter
|
||||
*/
|
||||
private SseEmitter handleSpecialChatModes(ChatRequest chatRequest, AgentVo agentVo, TraceRunHandle traceRun) {
|
||||
// 模式1:工作流对话(前端应用市场选工作流后携带 workFlowRunner)
|
||||
if (Boolean.TRUE.equals(chatRequest.getEnableWorkFlow())) {
|
||||
log.info("处理工作流对话,会话: {}", chatRequest.getSessionId());
|
||||
WorkFlowRunner runner = chatRequest.getWorkFlowRunner();
|
||||
if (ObjectUtils.isEmpty(runner)) {
|
||||
log.warn("工作流参数为空");
|
||||
}
|
||||
return workFlowStarterService.streaming(
|
||||
ThreadContext.getCurrentUser(),
|
||||
runner.getUuid(),
|
||||
runner.getInputs(),
|
||||
chatRequest.getSessionId()
|
||||
);
|
||||
// 智能体和普通对话互斥:有 agentId 为智能体,否则为普通模型对话。
|
||||
if (agentVo != null) {
|
||||
log.info("处理智能体对话,会话:{},agentId:{}", chatRequest.getSessionId(), chatRequest.getAgentId());
|
||||
return handleAgentChat(chatRequest, agentVo, traceRun);
|
||||
}
|
||||
// 模式2:智能体对话(默认走 Supervisor 多 Agent 编排)
|
||||
return handleAgentChat(chatRequest, agentVo, traceRun);
|
||||
log.info("处理普通对话,会话:{},模型:{}", chatRequest.getSessionId(), chatRequest.getModel());
|
||||
return handleModelChat(chatRequest, traceRun);
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能体对话模式(默认):构建 Supervisor 多 Agent 编排并异步执行,结果通过 SSE 推送。
|
||||
* 工作流模式。工作流运行时负责 SSE、节点执行和结束事件。
|
||||
*/
|
||||
private SseEmitter handleWorkflowChat(ChatRequest chatRequest) {
|
||||
WorkFlowRunner runner = chatRequest.getWorkFlowRunner();
|
||||
if (ObjectUtils.isEmpty(runner) || StringUtils.isBlank(runner.getUuid())) {
|
||||
throw new IllegalArgumentException("工作流模式必须提供 workFlowRunner.uuid");
|
||||
}
|
||||
log.info("处理工作流对话,会话:{},workflowUuid:{}", chatRequest.getSessionId(), runner.getUuid());
|
||||
return workFlowStarterService.streaming(
|
||||
ThreadContext.getCurrentUser(),
|
||||
runner.getUuid(),
|
||||
runner.getInputs() == null ? List.of() : runner.getInputs(),
|
||||
chatRequest.getSessionId()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 普通对话模式:直接调用选定模型,不装配 Supervisor、MCP、Skills 或专业子 Agent。
|
||||
*/
|
||||
private SseEmitter handleModelChat(ChatRequest chatRequest, TraceRunHandle traceRun) {
|
||||
ChatModelVo chatModelVo = chatRequest.getChatModelVo();
|
||||
AbstractChatService chatService = chatServiceFactory.getOriginalService(chatModelVo.getProviderCode());
|
||||
StreamingChatModel streamingChatModel = chatService.buildStreamingChatModel(chatModelVo, chatRequest);
|
||||
List<ChatMessage> messages = buildModelChatMessages(chatRequest);
|
||||
|
||||
TraceStreamSpan llmSpan = null;
|
||||
try (TraceScope ignored = openTraceScope(traceRun, chatRequest.getUserId())) {
|
||||
llmSpan = startLlmCallSpan(traceRun, chatRequest, "handleModelChat");
|
||||
streamingChatModel.chat(
|
||||
messages,
|
||||
createModelChatResponseHandler(chatRequest, traceRun, llmSpan)
|
||||
);
|
||||
} catch (Exception e) {
|
||||
if (llmSpan != null) {
|
||||
llmSpan.finishError(e);
|
||||
llmSpan.detach();
|
||||
}
|
||||
finishTraceRun(traceRun, TraceConstants.STATUS_ERROR, e);
|
||||
SseMessageUtils.sendError(String.valueOf(chatRequest.getSessionId()), e.getMessage());
|
||||
SseMessageUtils.completeConnection(String.valueOf(chatRequest.getSessionId()));
|
||||
log.error("普通对话执行失败", e);
|
||||
}
|
||||
return chatRequest.getEmitter();
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能体对话模式:构建 Supervisor 多 Agent 编排并异步执行,结果通过 SSE 推送。
|
||||
*
|
||||
* @param chatRequest 聊天请求
|
||||
* @param agentVo 智能体配置(可为 null,无智能体时用请求 model 兜底)
|
||||
* @param agentVo 智能体配置
|
||||
*/
|
||||
private SseEmitter handleAgentChat(ChatRequest chatRequest, AgentVo agentVo, TraceRunHandle traceRun) {
|
||||
ChatModelVo chatModelVo = chatRequest.getChatModelVo();
|
||||
@@ -321,7 +370,7 @@ public class ChatServiceFacade implements IChatService {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
TraceStreamSpan llmSpan = null;
|
||||
try (TraceScope ignored = openTraceScope(traceRun, userId)) {
|
||||
llmSpan = startLlmCallSpan(traceRun, chatRequest);
|
||||
llmSpan = startLlmCallSpan(traceRun, chatRequest, "handleAgentChat");
|
||||
String result = supervisor.invoke(prompt);
|
||||
SseMessageUtils.sendContent(sessionId, result);
|
||||
SseMessageUtils.sendDone(sessionId);
|
||||
@@ -386,7 +435,8 @@ public class ChatServiceFacade implements IChatService {
|
||||
traceRun.businessId, userId, traceRun.tenantId);
|
||||
}
|
||||
|
||||
private TraceStreamSpan startLlmCallSpan(TraceRunHandle traceRun, ChatRequest chatRequest) {
|
||||
private TraceStreamSpan startLlmCallSpan(TraceRunHandle traceRun, ChatRequest chatRequest,
|
||||
String methodName) {
|
||||
if (traceRun == null || StringUtils.isBlank(TraceContext.getTraceId())) {
|
||||
return null;
|
||||
}
|
||||
@@ -401,7 +451,7 @@ public class ChatServiceFacade implements IChatService {
|
||||
node.setNodeName("llm-call");
|
||||
node.setNodeType(RagTraceNodeTypes.NODE_LLM_CALL);
|
||||
node.setClassName(ChatServiceFacade.class.getName());
|
||||
node.setMethodName("handleAgentChat");
|
||||
node.setMethodName(methodName);
|
||||
node.setStatus(TraceConstants.STATUS_RUNNING);
|
||||
node.setStartTime(new Date(startMillis));
|
||||
node.setInputPayload(RagTracePayloadBuilder.streamInputSummary(chatRequest));
|
||||
@@ -564,7 +614,11 @@ public class ChatServiceFacade implements IChatService {
|
||||
Long userId = LoginHelper.getUserId();
|
||||
|
||||
// 5. 建立 SSE 连接(用于前端监听,按会话隔离)
|
||||
sseEmitterManager.connect(String.valueOf(chatRequest.getSessionId()));
|
||||
// 工作流调用时(externalHandler 非空), SSE 连接由工作流引擎创建并持有(WorkflowStarter#streaming),
|
||||
// connect 为替换语义(关闭同键旧连接), 此处重连会掐断工作流连接, 必须跳过
|
||||
if (externalHandler == null) {
|
||||
sseEmitterManager.connect(String.valueOf(chatRequest.getSessionId()));
|
||||
}
|
||||
|
||||
// 保存用户消息
|
||||
chatMessageService.saveChatMessage(userId, chatRequest.getSessionId(), chatRequest.getContent(), RoleType.USER.getName(), chatRequest.getModel());
|
||||
@@ -645,6 +699,19 @@ public class ChatServiceFacade implements IChatService {
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建普通对话消息。保留历史上下文,并在请求指定知识库时仅增强当前用户消息。
|
||||
*/
|
||||
private List<ChatMessage> buildModelChatMessages(ChatRequest chatRequest) {
|
||||
List<ChatMessage> messages = new ArrayList<>(chatRequest.getContextMessages());
|
||||
String augmentedInput = augmentAgentInput(chatRequest, null);
|
||||
int lastIndex = messages.size() - 1;
|
||||
if (lastIndex >= 0 && messages.get(lastIndex) instanceof UserMessage) {
|
||||
messages.set(lastIndex, UserMessage.userMessage(augmentedInput));
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将上下文消息格式化为多轮对话文本(供只接受 String 输入的 Supervisor 使用)。
|
||||
* 跳过 SystemMessage(系统提示词单独前置)与最后一条当前用户消息(单独做 RAG 增强后拼接)。
|
||||
@@ -791,6 +858,77 @@ public class ChatServiceFacade implements IChatService {
|
||||
return queryVectorBo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 普通对话响应处理器:推送流式内容、保存助手消息并结束链路追踪。
|
||||
*/
|
||||
private StreamingChatResponseHandler createModelChatResponseHandler(ChatRequest chatRequest,
|
||||
TraceRunHandle traceRun,
|
||||
TraceStreamSpan llmSpan) {
|
||||
String sessionId = String.valueOf(chatRequest.getSessionId());
|
||||
return new StreamingChatResponseHandler() {
|
||||
|
||||
private final StringBuilder messageBuffer = new StringBuilder();
|
||||
|
||||
@Override
|
||||
public void onPartialResponse(String partialResponse) {
|
||||
messageBuffer.append(partialResponse);
|
||||
SseMessageUtils.sendContent(sessionId, partialResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPartialThinking(PartialThinking partialThinking) {
|
||||
SseMessageUtils.sendReasoning(sessionId, partialThinking.text());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleteResponse(ChatResponse completeResponse) {
|
||||
try {
|
||||
String fullMessage = messageBuffer.toString();
|
||||
if (StringUtils.isNotBlank(fullMessage)) {
|
||||
chatMessageService.saveChatMessage(
|
||||
chatRequest.getUserId(),
|
||||
chatRequest.getSessionId(),
|
||||
fullMessage,
|
||||
RoleType.ASSISTANT.getName(),
|
||||
chatRequest.getModel()
|
||||
);
|
||||
} else {
|
||||
log.warn("普通对话返回空消息,会话:{}", chatRequest.getSessionId());
|
||||
}
|
||||
if (llmSpan != null) {
|
||||
llmSpan.finishSuccess(RagTracePayloadBuilder.streamOutputSummary(fullMessage.length()));
|
||||
}
|
||||
finishTraceRun(traceRun, TraceConstants.STATUS_SUCCESS, null);
|
||||
SseMessageUtils.sendDone(sessionId);
|
||||
} catch (Exception e) {
|
||||
if (llmSpan != null) {
|
||||
llmSpan.finishError(e);
|
||||
}
|
||||
finishTraceRun(traceRun, TraceConstants.STATUS_ERROR, e);
|
||||
SseMessageUtils.sendError(sessionId, e.getMessage());
|
||||
log.error("普通对话完成处理失败", e);
|
||||
} finally {
|
||||
if (llmSpan != null) {
|
||||
llmSpan.detach();
|
||||
}
|
||||
SseMessageUtils.completeConnection(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
if (llmSpan != null) {
|
||||
llmSpan.finishError(error);
|
||||
llmSpan.detach();
|
||||
}
|
||||
finishTraceRun(traceRun, TraceConstants.STATUS_ERROR, error);
|
||||
SseMessageUtils.sendError(sessionId, error.getMessage());
|
||||
SseMessageUtils.completeConnection(sessionId);
|
||||
log.error("普通对话流式响应失败", error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建组合响应处理器 - 同时发送到 SSE 和外部 handler
|
||||
*
|
||||
@@ -811,7 +949,10 @@ public class ChatServiceFacade implements IChatService {
|
||||
messageBuffer.append(partialResponse);
|
||||
|
||||
// 2. 发送内容事件到 SSE(前端可通过 SSE 监听)
|
||||
SseMessageUtils.sendContent(sessionId, partialResponse);
|
||||
// 工作流调用时连接归工作流引擎所有, token 由引擎以 [NODE_CHUNK_] 事件推送, 不走聊天协议
|
||||
if (externalHandler == null) {
|
||||
SseMessageUtils.sendContent(sessionId, partialResponse);
|
||||
}
|
||||
|
||||
// 3. 转发给外部 handler(Workflow 等模块可处理)
|
||||
if (externalHandler != null) {
|
||||
@@ -821,8 +962,10 @@ public class ChatServiceFacade implements IChatService {
|
||||
|
||||
@Override
|
||||
public void onPartialThinking(PartialThinking partialThinking) {
|
||||
// 发送推理内容到 SSE(前端通过 reasoning 事件监听)
|
||||
SseMessageUtils.sendReasoning(sessionId, partialThinking.text());
|
||||
// 发送推理内容到 SSE(前端通过 reasoning 事件监听), 工作流调用时不发送
|
||||
if (externalHandler == null) {
|
||||
SseMessageUtils.sendReasoning(sessionId, partialThinking.text());
|
||||
}
|
||||
|
||||
// 转发给外部 handler
|
||||
if (externalHandler != null) {
|
||||
@@ -833,11 +976,12 @@ public class ChatServiceFacade implements IChatService {
|
||||
@Override
|
||||
public void onCompleteResponse(ChatResponse completeResponse) {
|
||||
try {
|
||||
// 1. 发送完成事件
|
||||
SseMessageUtils.sendDone(sessionId);
|
||||
|
||||
// 2. 关闭 SSE 连接
|
||||
SseMessageUtils.completeConnection(sessionId);
|
||||
// 1&2. 发送完成事件并关闭 SSE 连接
|
||||
// 工作流调用时流程可能还有后续节点, 连接关闭由工作流引擎统一负责, 此处不能关闭
|
||||
if (externalHandler == null) {
|
||||
SseMessageUtils.sendDone(sessionId);
|
||||
SseMessageUtils.completeConnection(sessionId);
|
||||
}
|
||||
|
||||
// 3. 转发给外部 handler
|
||||
if (externalHandler != null) {
|
||||
@@ -850,8 +994,10 @@ public class ChatServiceFacade implements IChatService {
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
// 发送错误事件
|
||||
SseMessageUtils.sendError(sessionId, error.getMessage());
|
||||
// 发送错误事件(工作流调用时由工作流引擎统一上报)
|
||||
if (externalHandler == null) {
|
||||
SseMessageUtils.sendError(sessionId, error.getMessage());
|
||||
}
|
||||
log.error("流式响应错误: {}", error.getMessage(), error);
|
||||
|
||||
// 转发给外部 handler
|
||||
|
||||
Reference in New Issue
Block a user