12 Commits

Author SHA1 Message Date
ageerle
9d439d1dcf refactor: move RAG trace classes to argtrace package and update SQL schema
- Rename org.ruoyi.trace package to org.ruoyi.argtrace for RagTraceNodeTypes
  and RagTracePayloadBuilder; fix package declarations and update imports in
  ChatServiceFacade and KnowledgeRetrievalServiceImpl
- Remove obsolete RagTracePayloadBuilderTest
- Switch supervisor response strategy from SUMMARY to LAST in ChatServiceFacade
- Refresh docs/script/sql/ruoyi-ai.sql with the latest full schema dump
- Add live demo links (admin panel, user frontend, commercial edition) to
  README.md and README_ZH.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 17:44:39 +08:00
ageerle
0a08c489be docs: remove redundant registry commands 2026-08-04 15:57:32 +08:00
ageerle
038e7725f9 docs: add prebuilt image deployment guide 2026-08-04 15:55:17 +08:00
ageerle
4ccf249663 docs: remove obsolete WeChat image 2026-08-04 15:50:51 +08:00
ageerle
1d5b01f77f docs: remove obsolete Douyin image 2026-08-04 15:50:36 +08:00
ageerle
538dac3a46 docs: add Douyin tutorial QR code 2026-08-04 15:49:40 +08:00
ageerle
6c383dd9f5 fix: 修复智能体编辑时关联工具加载失败 (#320) 2026-08-04 15:38:25 +08:00
ageerle
9ae49f8797 fix: allow frontend dependency builds in image workflow 2026-08-04 15:30:34 +08:00
ageerle
3c175ba28d chore: sync pending project changes 2026-08-04 15:23:19 +08:00
ageerle
e34592c3d4 release: prepare v3.1.0 2026-08-04 15:18:57 +08:00
ageerle
fd044a4752 docs: align README badges in one row 2026-07-30 00:22:22 +08:00
ageerle
6e264ad500 feat: streamline workflow orchestration and chat routing
Add Zhipu web search integration, remove obsolete workflow nodes and resume handling, and separate model, agent, and workflow chat execution.
2026-07-30 00:15:44 +08:00
58 changed files with 2169 additions and 1707 deletions

133
.github/workflows/publish-images.yml vendored Normal file
View File

@@ -0,0 +1,133 @@
name: Publish Docker Images
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
release_tag:
description: Release tag to rebuild
required: true
type: string
concurrency:
group: publish-images-${{ inputs.release_tag || github.ref_name }}
cancel-in-progress: false
env:
REGISTRY: ghcr.io
IMAGE_OWNER: ${{ github.repository_owner }}
RELEASE_TAG: ${{ inputs.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: Configure frontend dependency builds
if: matrix.source == 'admin' || matrix.source == 'web'
shell: bash
run: |
python3 - "${{ matrix.dockerfile }}" <<'PY'
from pathlib import Path
import sys
dockerfile = Path(sys.argv[1])
content = dockerfile.read_text()
marker = "pnpm install --frozen-lockfile"
replacement = (
"pnpm install --frozen-lockfile "
"--config.dangerously-allow-all-builds=true"
)
if marker not in content:
raise SystemExit(f"pnpm install command not found in {dockerfile}")
dockerfile.write_text(content.replace(marker, replacement, 1))
PY
- 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
View File

@@ -23,6 +23,9 @@ target/
.idea
.claude
.github
!.github/
!.github/workflows/
!.github/workflows/**
*.iws
*.iml
*.ipr

View File

@@ -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
**审核状态**: ✅ 待人工审核
**建议操作**: 合并前进行完整功能测试

View File

@@ -1,64 +0,0 @@
# RAG 完整修复与全量验收报告
验收时间2026-07-21Asia/Shanghai
验收对象:当前未提交工作区(保留原有改动)
## 结论
计划内的 115 项工程缺陷已完成代码修复,默认 Maven 构建已从“跳过测试”改为真实执行测试。全仓 37 个 reactor 模块测试成功,`ruoyi-chat` 49/49 通过,两个前端生产构建通过,`git diff --check` 通过。
本机已运行 MySQL、Redis、MinIO 和 Weaviate 1.30.0Milvus/Qdrant 容器以及有效的 embedding/chat/rerank provider 凭证不存在,因此这三项真实 provider/存储引擎冒烟被标记为环境限制,不影响确定性代码验收。
## 115 项验收
| # | 状态 | 修复/证据 |
|---|---|---|
| 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` | SUCCESS2621 modules transformed |
| `ruoyi-admin: pnpm build` | SUCCESS10/10 build tasks |
| `git diff --check` | SUCCESS无空白错误 |
| Weaviate/MySQL/Redis/MinIO | Docker 服务运行Weaviate 1.30.0 映射 28080 |
| 三向量库 Docker 集成 | SUCCESSWeaviate 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/9001Milvus 映射 19530/9091Qdrant 映射 6333/6334。
- `ThreeVectorStoresDockerIT` 使用 32 维确定性 embedding对三库逐一验证 batch write、vector search、fid delete、docId delete 和 drop collection3/3 通过。
- 首轮测试发现 Milvus `autoFlush=false` 导致批量写入后不可立即检索、元数据删除不可立即见;改为写入和删除返回前 flush 后通过。
- 清理后 Weaviate/Qdrant 的 `CodexRagVerify*` collection 计数均为 0Milvus collection 也由测试 finally 成功 drop本轮未写入 MySQL 或 OSS 测试数据。

286
README.md
View File

@@ -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,231 @@
<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>
## ✨ 核心亮点
## 🚀 Live Demo
| 模块 | 现有能力
|:---------:|---
| **模型管理** | 多模型接入(DeepSeek/智谱/MIMO/百炼/OpenAI)、多模态理解、Coze/DIFY/FastGPT/RAGFlow平台集成
| **知识管理** | 本地RAG + 向量库(Milvus/Weaviate/Qdrant) + 文档解析
| **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态
| **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点
| **智能体管理** | 基于Langchain4j的Agent框架、Supervisor模式编排,支持多种决策模型,可以灵活搭配工具,skills
| Service | URL | Default Account |
|---|---|---|
| Admin Panel | http://129.226.199.247:25666 | admin / admin123 |
| User Frontend | http://129.226.199.247:25137 | admin / admin123 |
| Commercial Edition | https://web.ruoyiai.chat | WeChat QR code login |
## ✨ Core Features
### 项目源码
| 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 |
| 项目模块 | GitHub 仓库 | Gitee 仓库 | GitCode 仓库 |
### Project Repositories
| Module | GitHub Repository | Gitee Repository | GitCode Repository |
|----------|-------------------------------------------------------|------------------------------------------------------|--------------------------------------------------------|
| 🔧 后端服务 | [ruoyi-ai](https://github.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitee.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitcode.com/ageerle/ruoyi-ai) |
| 🎨 用户前端 | [ruoyi-web](https://github.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitee.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitcode.com/ageerle/ruoyi-web) |
| 🛠️ 管理后台 | [ruoyi-admin](https://github.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitee.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitcode.com/ageerle/ruoyi-admin) |
| 🎬 短剧平台 | [ruoyi-drama](https://github.com/ageerle/ruoyi-drama) | [ruoyi-drama](https://gitee.com/ageerle/ruoyi-drama) | [ruoyi-drama](https://gitcode.com/ageerle/ruoyi-drama) |
| 🤖 编程助手 | [ruoyi-copilot](https://github.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitee.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitcode.com/ageerle/ruoyi-copilot) |
| 📱 小程序端 | [ruoyi-uniapp](https://github.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitee.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitcode.com/ageerle/ruoyi-uniapp) |
| 🔧 Backend | [ruoyi-ai](https://github.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitee.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitcode.com/ageerle/ruoyi-ai) |
| 🎨 User Frontend | [ruoyi-web](https://github.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitee.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitcode.com/ageerle/ruoyi-web) |
| 🛠️ Admin Panel | [ruoyi-admin](https://github.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitee.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitcode.com/ageerle/ruoyi-admin) |
| 🎬 Drama | [ruoyi-drama](https://github.com/ageerle/ruoyi-drama) | [ruoyi-drama](https://gitee.com/ageerle/ruoyi-drama) | [ruoyi-drama](https://gitcode.com/ageerle/ruoyi-drama) |
| 🤖 Copilot | [ruoyi-copilot](https://github.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitee.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitcode.com/ageerle/ruoyi-copilot) |
| 📱 Mini-App | [ruoyi-uniapp](https://github.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitee.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitcode.com/ageerle/ruoyi-uniapp) |
### 合作项目
| 项目名称 | GitHub 仓库 | Gitee 仓库
### Partner Projects
| Project Name | GitHub Repository | Gitee Repository |
|----------------|-------------------------------------------------------|------------------------------------------------------|
| element-plus-x | [element-plus-x](https://github.com/element-plus-x/Element-Plus-X) | [element-plus-x](https://gitee.com/he-jiayue/element-plus-x) |
| element-plus-x | [element-plus-x](https://github.com/element-plus-x/Element-Plus-X) | [element-plus-x](https://gitee.com/he-jiayue/element-plus-x) |
## 🛠️ 技术架构
## 🛠️ Technical Architecture
### 核心框架
- **后端架构**Spring Boot 3.5.8 + Langchain4j
- **数据存储**MySQL 8.0 + Redis + 向量数据库(Milvus/Weaviate/Qdrant
- **前端技术**Vue 3 + Vben Admin + element-plus-x
- **安全认证**Sa-Token + JWT 双重保障
- **文档处理**PDFWordExcel 解析,图像智能分析
- **实时通信**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
# 克隆仓库
git clone https://github.com/ageerle/ruoyi-ai.git
# Requirements: Docker Engine and Docker Compose V2
# Clone the v3.1.0 release
git clone --depth 1 --branch v3.1.0 https://github.com/ageerle/ruoyi-ai.git
cd ruoyi-ai
# 启动所有服务(从镜像仓库拉取预构建镜像)
docker-compose -f docker-compose-all.yaml up -d
# Pin the image version. Public GHCR images do not require docker login.
cp docs/docker/ruoyi-ai/.env.example docs/docker/ruoyi-ai/.env
sed -i 's/^RUIYI_VERSION=.*/RUIYI_VERSION=v3.1.0/' docs/docker/ruoyi-ai/.env
# 查看服务状态
docker-compose -f docker-compose-all.yaml ps
# Pull pre-built images from GHCR and start all services
docker compose --env-file docs/docker/ruoyi-ai/.env \
-f docs/docker/ruoyi-ai/docker-compose-all.yaml pull
docker compose --env-file docs/docker/ruoyi-ai/.env \
-f docs/docker/ruoyi-ai/docker-compose-all.yaml up -d
# 访问服务
# 管理端: http://localhost:25666 (admin / admin123)
# 用户端: http://localhost:25137
# 后端API: http://localhost:26039
# Check service status
docker compose --env-file docs/docker/ruoyi-ai/.env \
-f docs/docker/ruoyi-ai/docker-compose-all.yaml ps
# Access services (replace SERVER_IP with the server address)
# Admin Panel: http://SERVER_IP:25666 (admin / admin123)
# User Frontend: http://SERVER_IP:25137
# Backend API: http://SERVER_IP:26039
```
### 方式二:分步部署(源码编译)
The default Compose file also publishes MySQL (`23306`), Redis (`26379`),
Weaviate (`28080`), and MinIO (`29000`/`29090`). For production deployments,
change the default MySQL and MinIO passwords and expose only the application
ports through the firewall or a reverse proxy.
如果您需要从源码构建后端服务,请按照以下步骤操作:
To upgrade to another published release, update `RUIYI_VERSION` in
`docs/docker/ruoyi-ai/.env`, then run `docker compose pull` and
`docker compose up -d` with the same `--env-file` and `-f` options. Do not use
`docker compose down -v` unless you intend to delete persistent data volumes.
#### 第一步:部署后端服务
### 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 |
### 镜像仓库
## 📚 Documentation
所有镜像托管在阿里云容器镜像服务:
Want to learn more about installation, deployment, configuration, and secondary development?
```
crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai
```
**👉 [Complete Documentation](https://doc.ruoyiai.chat/)**
可用镜像:
- `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` - 用户端前端
## 🤝 Contributing
### 常用命令
We warmly welcome community contributions! Whether you are a seasoned developer or just getting started, you can contribute to the project 💪
```bash
# 停止所有服务
docker-compose -f docker-compose-all.yaml down
### How to Contribute
# 查看服务日志
docker-compose -f docker-compose-all.yaml logs -f [服务名]
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**
# 重启某个服务
docker-compose -f docker-compose-all.yaml restart [服务名]
```
> 💡 **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.
**👉 [完整使用文档](https://doc.ruoyiai.chat/)**
## 🙏 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
### 贡献方式
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 后台管理模板
## 💎 赞助商
**感谢以下赞助商对本项目的支持:**
**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.1Seedream-5.0GLM-5.2DeepSeek等。不止编程、更能解决 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/douyin.png" alt="Douyin QR Code" width="200" height="200"><br>
<strong>Douyin Video Tutorials</strong><br>
<em>Open Douyin, scan & follow to watch video tutorials</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 +246,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>

View File

@@ -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

277
README_ZH.md Normal file
View File

@@ -0,0 +1,277 @@
# 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>
## 🚀 演示地址
| 服务 | 访问地址 | 默认账号 |
|---|---|---|
| 管理端 | http://129.226.199.247:25666 | admin / admin123 |
| 用户端 | http://129.226.199.247:25137 | admin / admin123 |
| 商业版 | https://web.ruoyiai.chat | 微信扫码登录 |
## ✨ 核心亮点
| 模块 | 现有能力
|:---------:|---
| **模型管理** | 多模型接入(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
# 环境要求Docker Engine 和 Docker Compose V2
# 克隆 v3.1.0 版本
git clone --depth 1 --branch v3.1.0 https://github.com/ageerle/ruoyi-ai.git
cd ruoyi-ai
# 固定镜像版本。GHCR 镜像已公开,无需 docker login
cp docs/docker/ruoyi-ai/.env.example docs/docker/ruoyi-ai/.env
sed -i 's/^RUIYI_VERSION=.*/RUIYI_VERSION=v3.1.0/' docs/docker/ruoyi-ai/.env
# 从 GHCR 拉取预构建镜像并启动全部服务
docker compose --env-file docs/docker/ruoyi-ai/.env \
-f docs/docker/ruoyi-ai/docker-compose-all.yaml pull
docker compose --env-file docs/docker/ruoyi-ai/.env \
-f docs/docker/ruoyi-ai/docker-compose-all.yaml up -d
# 查看服务状态
docker compose --env-file docs/docker/ruoyi-ai/.env \
-f docs/docker/ruoyi-ai/docker-compose-all.yaml ps
# 访问服务(将 SERVER_IP 替换为服务器地址)
# 管理端: http://SERVER_IP:25666 (admin / admin123)
# 用户端: http://SERVER_IP:25137
# 后端API: http://SERVER_IP:26039
```
默认 Compose 还会发布 MySQL`23306`、Redis`26379`、Weaviate`28080`)和
MinIO`29000`/`29090`)端口。生产环境请修改 MySQL 和 MinIO 默认密码,并通过防火墙或反向代理只开放应用端口。
升级到其他已发布版本时,修改 `docs/docker/ruoyi-ai/.env` 中的 `RUIYI_VERSION`,然后使用相同的
`--env-file``-f` 参数执行 `docker compose pull``docker compose up -d`。除非确定要删除持久化数据卷,
不要执行 `docker compose down -v`
### 方式二:分步部署(源码编译)
如果您需要从源码构建后端服务,请按照以下步骤操作:
#### 第一步:部署后端服务
```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 | 对象存储控制台 |
## 📚 使用文档
想要深入了解安装部署、功能配置和二次开发?
**👉 [完整使用文档](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/douyin.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

View 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

View File

@@ -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:

BIN
docs/image/douyin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -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');

View File

@@ -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');

View 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
View File

@@ -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>

View File

@@ -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

View File

@@ -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:

View File

@@ -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>

View File

@@ -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", "邮件接收人不能为空"),

View File

@@ -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);
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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>

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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);
}
/**

View File

@@ -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);
}

View File

@@ -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");

View File

@@ -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 -> {
}
}

View File

@@ -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);
}
}

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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 "";
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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
) {
}
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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);
// 不抛出异常,返回默认结果,避免中断整个流程
}
}
}

View File

@@ -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;

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