mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-09-13 00:14:59 +00:00
Compare commits
4 Commits
83fd1ee983
...
v3.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c175ba28d | ||
|
|
e34592c3d4 | ||
|
|
fd044a4752 | ||
|
|
6e264ad500 |
107
.github/workflows/publish-images.yml
vendored
Normal file
107
.github/workflows/publish-images.yml
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
name: Publish Docker Images
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
concurrency:
|
||||
group: publish-images-${{ github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_OWNER: ${{ github.repository_owner }}
|
||||
RELEASE_TAG: ${{ github.ref_name }}
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish ${{ matrix.image }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- image: ruoyi-ai-backend
|
||||
source: local
|
||||
context: .
|
||||
dockerfile: docs/docker/ruoyi-ai/Dockerfile.backend
|
||||
- image: ruoyi-ai-mysql
|
||||
source: local
|
||||
context: .
|
||||
dockerfile: docs/docker/ruoyi-ai/Dockerfile.mysql
|
||||
- image: ruoyi-ai-admin
|
||||
source: admin
|
||||
context: build/ruoyi-admin
|
||||
dockerfile: build/ruoyi-admin/apps/web-antd/Dockerfile
|
||||
- image: ruoyi-ai-web
|
||||
source: web
|
||||
context: build/ruoyi-web
|
||||
dockerfile: build/ruoyi-web/Dockerfile.frontend
|
||||
|
||||
steps:
|
||||
- name: Checkout backend repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ env.RELEASE_TAG }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Checkout admin repository
|
||||
if: matrix.source == 'admin'
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ github.repository_owner }}/ruoyi-admin
|
||||
ref: ${{ env.RELEASE_TAG }}
|
||||
path: build/ruoyi-admin
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Checkout web repository
|
||||
if: matrix.source == 'web'
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ github.repository_owner }}/ruoyi-web
|
||||
ref: ${{ env.RELEASE_TAG }}
|
||||
path: build/ruoyi-web
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ matrix.image }}
|
||||
tags: |
|
||||
type=raw,value=${{ env.RELEASE_TAG }}
|
||||
type=raw,value=latest
|
||||
labels: |
|
||||
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
||||
org.opencontainers.image.version=${{ env.RELEASE_TAG }}
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ${{ matrix.context }}
|
||||
file: ${{ matrix.dockerfile }}
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha,scope=${{ matrix.image }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.image }}
|
||||
provenance: true
|
||||
sbom: true
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -23,6 +23,9 @@ target/
|
||||
.idea
|
||||
.claude
|
||||
.github
|
||||
!.github/
|
||||
!.github/workflows/
|
||||
!.github/workflows/**
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
@@ -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 测试数据。
|
||||
264
README.md
264
README.md
@@ -2,11 +2,7 @@
|
||||
|
||||
<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]
|
||||
[![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">
|
||||
@@ -17,236 +13,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) |
|
||||
|
||||
## 🛠️ 技术架构
|
||||
## 🛠️ 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
|
||||
|
||||
# 启动所有服务(从镜像仓库拉取预构建镜像)
|
||||
docker-compose -f docker-compose-all.yaml up -d
|
||||
# Start all services (pull pre-built images from GHCR)
|
||||
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml up -d
|
||||
|
||||
# 查看服务状态
|
||||
docker-compose -f docker-compose-all.yaml ps
|
||||
# Check service status
|
||||
docker compose -f docs/docker/ruoyi-ai/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
|
||||
|
||||
所有镜像托管在阿里云容器镜像服务:
|
||||
Application images are published to GitHub Container Registry (GHCR) by GitHub Actions when a release tag such as `v3.1.0` is pushed:
|
||||
|
||||
```
|
||||
crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai
|
||||
ghcr.io/ageerle/ruoyi-ai-backend:v3.1.0
|
||||
ghcr.io/ageerle/ruoyi-ai-mysql:v3.1.0
|
||||
ghcr.io/ageerle/ruoyi-ai-admin:v3.1.0
|
||||
ghcr.io/ageerle/ruoyi-ai-web:v3.1.0
|
||||
```
|
||||
|
||||
可用镜像:
|
||||
- `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` - 用户端前端
|
||||
The Compose file defaults to `latest`. To pin a release, create `docs/docker/ruoyi-ai/.env` with `RUIYI_VERSION=v3.1.0`.
|
||||
|
||||
### 常用命令
|
||||
After the first workflow run, set the four GHCR packages to `Public` in GitHub so deployment servers can pull them without logging in.
|
||||
|
||||
The same release tag must exist in `ageerle/ruoyi-admin` and `ageerle/ruoyi-web`; the publish workflow checks out those repositories at the backend release tag before building their images.
|
||||
|
||||
### Common Commands
|
||||
|
||||
```bash
|
||||
# 停止所有服务
|
||||
docker-compose -f docker-compose-all.yaml down
|
||||
# Stop all services
|
||||
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml down
|
||||
|
||||
# 查看服务日志
|
||||
docker-compose -f docker-compose-all.yaml logs -f [服务名]
|
||||
# View service logs
|
||||
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml logs -f [service-name]
|
||||
|
||||
# 重启某个服务
|
||||
docker-compose -f docker-compose-all.yaml restart [服务名]
|
||||
# Restart a service
|
||||
docker compose -f docs/docker/ruoyi-ai/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 +250,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
|
||||
282
README_ZH.md
Normal file
282
README_ZH.md
Normal file
@@ -0,0 +1,282 @@
|
||||
# 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
|
||||
|
||||
# 启动所有服务(从 GHCR 拉取预构建镜像)
|
||||
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml up -d
|
||||
|
||||
# 查看服务状态
|
||||
docker compose -f docs/docker/ruoyi-ai/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 | 对象存储控制台 |
|
||||
|
||||
### 镜像仓库
|
||||
|
||||
每次推送类似 `v3.1.0` 的版本标签后,GitHub Actions 会自动构建并发布应用镜像到 GitHub Container Registry(GHCR):
|
||||
|
||||
```
|
||||
ghcr.io/ageerle/ruoyi-ai-backend:v3.1.0
|
||||
ghcr.io/ageerle/ruoyi-ai-mysql:v3.1.0
|
||||
ghcr.io/ageerle/ruoyi-ai-admin:v3.1.0
|
||||
ghcr.io/ageerle/ruoyi-ai-web:v3.1.0
|
||||
```
|
||||
|
||||
Compose 默认使用 `latest`。如需固定版本,可在 `docs/docker/ruoyi-ai/.env` 中设置 `RUIYI_VERSION=v3.1.0`。
|
||||
|
||||
首次工作流运行完成后,请在 GitHub 的 Packages 设置中将这四个 GHCR 镜像设为 `Public`,用户服务器才能免登录拉取。
|
||||
|
||||
管理端 `ageerle/ruoyi-admin` 和用户端 `ageerle/ruoyi-web` 也必须存在同名版本标签;发布工作流会使用后端发布标签检出这两个仓库后再构建镜像。
|
||||
|
||||
### 常用命令
|
||||
|
||||
```bash
|
||||
# 停止所有服务
|
||||
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml down
|
||||
|
||||
# 查看服务日志
|
||||
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml logs -f [服务名]
|
||||
|
||||
# 重启某个服务
|
||||
docker compose -f docs/docker/ruoyi-ai/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
|
||||
6
docs/docker/ruoyi-ai/.env.example
Normal file
6
docs/docker/ruoyi-ai/.env.example
Normal file
@@ -0,0 +1,6 @@
|
||||
# GitHub Container Registry namespace owner.
|
||||
# For the upstream project this is ageerle; change it when using a fork.
|
||||
IMAGE_OWNER=ageerle
|
||||
|
||||
# Use latest for convenience, or pin a release such as v3.1.0.
|
||||
RUIYI_VERSION=latest
|
||||
@@ -10,13 +10,13 @@
|
||||
# - RuoYi-Admin (管理端前端)
|
||||
# - RuoYi-Web (用户端前端)
|
||||
#
|
||||
# 镜像仓库地址: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai
|
||||
# 镜像仓库地址: ghcr.io/ageerle
|
||||
|
||||
services:
|
||||
# ==================== MySQL 数据库 ====================
|
||||
mysql:
|
||||
# 阿里云镜像地址(包含初始化SQL)
|
||||
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/mysql:v3
|
||||
# GHCR 镜像(包含初始化SQL)
|
||||
image: ghcr.io/${IMAGE_OWNER:-ageerle}/ruoyi-ai-mysql:${RUIYI_VERSION:-latest}
|
||||
container_name: ruoyi-ai-mysql
|
||||
restart: always
|
||||
ports:
|
||||
@@ -41,7 +41,7 @@ services:
|
||||
|
||||
# ==================== Redis 缓存 ====================
|
||||
redis:
|
||||
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/redis:6.2
|
||||
image: redis:6.2
|
||||
container_name: ruoyi-ai-redis
|
||||
restart: always
|
||||
ports:
|
||||
@@ -59,7 +59,7 @@ services:
|
||||
|
||||
# ==================== Weaviate 向量数据库 ====================
|
||||
weaviate:
|
||||
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/weaviate:1.30.0
|
||||
image: semitechnologies/weaviate:1.30.0
|
||||
container_name: ruoyi-ai-weaviate
|
||||
restart: always
|
||||
ports:
|
||||
@@ -78,7 +78,7 @@ services:
|
||||
|
||||
# ==================== MinIO 对象存储 ====================
|
||||
minio:
|
||||
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/minio:latest
|
||||
image: minio/minio:latest
|
||||
container_name: ruoyi-ai-minio
|
||||
restart: always
|
||||
ports:
|
||||
@@ -95,7 +95,7 @@ services:
|
||||
|
||||
# ==================== RuoYi-AI 后端服务 ====================
|
||||
backend:
|
||||
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/ruoyi-ai-backend:latest
|
||||
image: ghcr.io/${IMAGE_OWNER:-ageerle}/ruoyi-ai-backend:${RUIYI_VERSION:-latest}
|
||||
container_name: ruoyi-ai-backend
|
||||
restart: always
|
||||
ports:
|
||||
@@ -129,7 +129,7 @@ services:
|
||||
|
||||
# ==================== RuoYi-AI 管理端前端 ====================
|
||||
admin-frontend:
|
||||
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/ruoyi-ai-admin:latest
|
||||
image: ghcr.io/${IMAGE_OWNER:-ageerle}/ruoyi-ai-admin:${RUIYI_VERSION:-latest}
|
||||
container_name: ruoyi-ai-admin
|
||||
restart: always
|
||||
ports:
|
||||
@@ -154,7 +154,7 @@ services:
|
||||
|
||||
# ==================== RuoYi-AI 用户端前端 ====================
|
||||
web-frontend:
|
||||
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/ruoyi-ai-web:latest
|
||||
image: ghcr.io/${IMAGE_OWNER:-ageerle}/ruoyi-ai-web:${RUIYI_VERSION:-latest}
|
||||
container_name: ruoyi-ai-web
|
||||
restart: always
|
||||
ports:
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
12
pom.xml
12
pom.xml
@@ -43,7 +43,6 @@
|
||||
<aws.sdk.version>2.28.22</aws.sdk.version>
|
||||
<!-- SMS 配置 -->
|
||||
<sms4j.version>3.3.5</sms4j.version>
|
||||
<!-- FastJson已移除,使用Jackson替代 -->
|
||||
<!-- 面向运行时的D-ORM依赖 -->
|
||||
<anyline.version>8.7.2-20250603</anyline.version>
|
||||
<!-- 工作流配置 -->
|
||||
@@ -62,6 +61,8 @@
|
||||
<weaviate.version>1.19.6</weaviate.version>
|
||||
<dify.version>1.2.7</dify.version>
|
||||
<coze.version>0.4.2</coze.version>
|
||||
<!-- 智谱官方 Java SDK -->
|
||||
<zai-sdk.version>0.3.5</zai-sdk.version>
|
||||
|
||||
<!-- gRPC 版本 - 解决 Milvus SDK 依赖冲突 -->
|
||||
<grpc.version>1.62.2</grpc.version>
|
||||
@@ -346,6 +347,12 @@
|
||||
<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 +362,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:
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
<artifactId>ruoyi-common-sse</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- FastJson已移除,使用Spring Boot自带的Jackson -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
|
||||
@@ -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,7 @@ 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());
|
||||
|
||||
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,9 @@ 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);
|
||||
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>${zai-sdk.version}</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>
|
||||
@@ -114,6 +126,7 @@
|
||||
<version>${langchain4j.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
@@ -133,8 +146,6 @@
|
||||
<version>${langgraph4j.version}</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</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