mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-09-13 00:14:59 +00:00
Compare commits
17 Commits
afaa86ef6e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d439d1dcf | ||
|
|
0a08c489be | ||
|
|
038e7725f9 | ||
|
|
4ccf249663 | ||
|
|
1d5b01f77f | ||
|
|
538dac3a46 | ||
|
|
6c383dd9f5 | ||
|
|
9ae49f8797 | ||
|
|
3c175ba28d | ||
|
|
e34592c3d4 | ||
|
|
fd044a4752 | ||
|
|
6e264ad500 | ||
|
|
83fd1ee983 | ||
|
|
7640de34e5 | ||
|
|
77846601ee | ||
|
|
b80dc6e23f | ||
|
|
176cc67a5c |
133
.github/workflows/publish-images.yml
vendored
Normal file
133
.github/workflows/publish-images.yml
vendored
Normal 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
3
.gitignore
vendored
@@ -23,6 +23,9 @@ target/
|
|||||||
.idea
|
.idea
|
||||||
.claude
|
.claude
|
||||||
.github
|
.github
|
||||||
|
!.github/
|
||||||
|
!.github/workflows/
|
||||||
|
!.github/workflows/**
|
||||||
*.iws
|
*.iws
|
||||||
*.iml
|
*.iml
|
||||||
*.ipr
|
*.ipr
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
# RAG 完整修复与全量验收报告
|
|
||||||
|
|
||||||
验收时间:2026-07-21(Asia/Shanghai)
|
|
||||||
验收对象:当前未提交工作区(保留原有改动)
|
|
||||||
|
|
||||||
## 结论
|
|
||||||
|
|
||||||
计划内的 1–15 项工程缺陷已完成代码修复,默认 Maven 构建已从“跳过测试”改为真实执行测试。全仓 37 个 reactor 模块测试成功,`ruoyi-chat` 49/49 通过,两个前端生产构建通过,`git diff --check` 通过。
|
|
||||||
|
|
||||||
本机已运行 MySQL、Redis、MinIO 和 Weaviate 1.30.0;Milvus/Qdrant 容器以及有效的 embedding/chat/rerank provider 凭证不存在,因此这三项真实 provider/存储引擎冒烟被标记为环境限制,不影响确定性代码验收。
|
|
||||||
|
|
||||||
## 1–15 项验收
|
|
||||||
|
|
||||||
| # | 状态 | 修复/证据 |
|
|
||||||
|---|---|---|
|
|
||||||
| 1 | 通过 | Markdown/Java/字符分片的边界、空文档、超长块回归通过。 |
|
|
||||||
| 2 | 通过 | Supervisor 每轮仅保留一个 RAG 入口,不再用已含 RAG 的 prompt 重复检索。 |
|
|
||||||
| 3 | 通过 | 历史消息进入 Supervisor prompt,检索 query 与最终 prompt 分离。 |
|
|
||||||
| 4 | 通过 | `fid` 稳定 ID 贯穿 DB/三种向量库/RRF,融合去重回归通过。 |
|
|
||||||
| 5 | 通过 | aiflow vector/hybrid 复用统一检索服务;graph 明确返回不支持,不再伪装为 vector。 |
|
|
||||||
| 6 | 通过 | 重解析改为先写新 fid、再清旧向量、最后替换 DB;失败补偿新向量;删片段/附件/库遇向量删除失败即中止。 |
|
|
||||||
| 7 | 通过 | embedding/rerank provider 使用 prototype 实例,工厂缓存可按模型刷新,避免跨配置污染。 |
|
|
||||||
| 8 | 通过 | `similarityThreshold` 仅用于粗召回;`rerankScoreThreshold` 仅在 rerank 真实成功后生效,回归测试通过。 |
|
|
||||||
| 9 | 通过 | 默认配置和 Compose 统一为 Weaviate 1.30.0、`28080:8080`。 |
|
|
||||||
| 10 | 通过 | 三种策略均使用 `embedAll`;Weaviate batch objects、Milvus `addAll`、Qdrant `addAll`。 |
|
|
||||||
| 11 | 通过 | upload/parse/retrieval 权限保留,parse/retrieval 增加分布式防重复提交,upload 由现有知识库+文件名唯一约束兜底。 |
|
|
||||||
| 12 | 通过 | 分隔符使用字面量语义,`|`/`.`/`*` 回归通过。 |
|
|
||||||
| 13 | 通过 | hybrid 通道失败可降级到 vector;所有可用通道都失败时抛出明确业务异常。 |
|
|
||||||
| 14 | 通过 | Weaviate client 稳定懒加载单例;schema 仅在已存在或创建成功后进入缓存。 |
|
|
||||||
| 15 | 通过 | 工厂新增严格 `getStrategy(type)`,知识库 `vectorModel` 优先,空值才回退全局,非法值直接报错。 |
|
|
||||||
|
|
||||||
## 其他完成项
|
|
||||||
|
|
||||||
- 多知识库并行检索,按 `kid + docId + fid` 去重,统一上限和字符预算。
|
|
||||||
- 5 分钟短 TTL 检索缓存,key 覆盖检索参数,知识数据变更主动失效。
|
|
||||||
- rerank 仅保留 provider 实际返回的文档。
|
|
||||||
- 知识库文档数改为 group-by 查询,消除该 N+1。
|
|
||||||
- Milvus/Qdrant/Weaviate 的删 collection/doc/fid 语义对齐;Milvus 删库改为 drop collection。
|
|
||||||
- MCP `npx` 根据操作系统解析,支持系统属性/环境变量覆盖。
|
|
||||||
- `fid` 非空唯一、`doc_id varchar(32)`、租户/用户索引与可重复执行迁移脚本已提供。
|
|
||||||
- 用户端聊天页已接入知识库列表和最小选择器。
|
|
||||||
|
|
||||||
## 测试记录
|
|
||||||
|
|
||||||
| 检查 | 结果 |
|
|
||||||
|---|---|
|
|
||||||
| `mvn -Pdev test` | 37/37 reactor 模块 SUCCESS;`ruoyi-chat` 49/49 |
|
|
||||||
| `mvn -Pdev -pl ruoyi-modules/ruoyi-aiflow -am -DskipTests compile` | 21/21 SUCCESS |
|
|
||||||
| `ruoyi-web: pnpm build` | SUCCESS,2621 modules transformed |
|
|
||||||
| `ruoyi-admin: pnpm build` | SUCCESS,10/10 build tasks |
|
|
||||||
| `git diff --check` | SUCCESS,无空白错误 |
|
|
||||||
| Weaviate/MySQL/Redis/MinIO | Docker 服务运行,Weaviate 1.30.0 映射 28080 |
|
|
||||||
| 三向量库 Docker 集成 | SUCCESS;Weaviate 1.30.0、Milvus 2.5.7、Qdrant 1.17.0 真实写入/检索/删除测试 3/3 通过 |
|
|
||||||
| 真实 embedding/chat/rerank | 环境限制:当前配置为无效/占位凭证 |
|
|
||||||
|
|
||||||
本轮未创建新的 `codex_rag_verify_` 持久化数据;上一轮验收数据已清理,未动现有非测试数据。
|
|
||||||
|
|
||||||
## 2026-07-21 三向量库 Docker 补充验收
|
|
||||||
|
|
||||||
- 启动并保留 `ruoyi-rag-milvus`、`ruoyi-rag-milvus-etcd`、`ruoyi-rag-milvus-minio`、`ruoyi-rag-qdrant`,四个容器健康检查均为 `healthy`。
|
|
||||||
- Milvus 专用 MinIO 仅在 Docker 内网可达,没有占用宿主机 9000/9001;Milvus 映射 19530/9091,Qdrant 映射 6333/6334。
|
|
||||||
- `ThreeVectorStoresDockerIT` 使用 32 维确定性 embedding,对三库逐一验证 batch write、vector search、fid delete、docId delete 和 drop collection,3/3 通过。
|
|
||||||
- 首轮测试发现 Milvus `autoFlush=false` 导致批量写入后不可立即检索、元数据删除不可立即见;改为写入和删除返回前 flush 后通过。
|
|
||||||
- 清理后 Weaviate/Qdrant 的 `CodexRagVerify*` collection 计数均为 0,Milvus collection 也由测试 finally 成功 drop;本轮未写入 MySQL 或 OSS 测试数据。
|
|
||||||
286
README.md
286
README.md
@@ -2,11 +2,7 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[![Contributors][contributors-shield]][contributors-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]
|
||||||
[![Forks][forks-shield]][forks-url]
|
|
||||||
[![Stargazers][stars-shield]][stars-url]
|
|
||||||
[![Issues][issues-shield]][issues-url]
|
|
||||||
[![MIT License][license-shield]][license-url]
|
|
||||||
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -17,236 +13,231 @@
|
|||||||
|
|
||||||
<img src="docs/image/logo.png" alt="RuoYi AI Logo" width="120" height="120">
|
<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/)** |
|
**[中文](README_ZH.md)** | **[📖 Documentation](https://doc.ruoyiai.chat/)** |
|
||||||
**[🚀 在线体验](https://web.ruoyiai.chat/)** | **[🐛 问题反馈](https://github.com/ageerle/ruoyi-ai/issues)** | **[💡 功能建议](https://github.com/ageerle/ruoyi-ai/issues)**
|
**[🚀 Live Demo](https://web.ruoyiai.chat/)** | **[🐛 Report Issues](https://github.com/ageerle/ruoyi-ai/issues)** | **[💡 Feature Requests](https://github.com/ageerle/ruoyi-ai/issues)**
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
## ✨ 核心亮点
|
## 🚀 Live Demo
|
||||||
|
|
||||||
| 模块 | 现有能力
|
| Service | URL | Default Account |
|
||||||
|:---------:|---
|
|---|---|---|
|
||||||
| **模型管理** | 多模型接入(DeepSeek/智谱/MIMO/百炼/OpenAI)、多模态理解、Coze/DIFY/FastGPT/RAGFlow平台集成
|
| Admin Panel | http://129.226.199.247:25666 | admin / admin123 |
|
||||||
| **知识管理** | 本地RAG + 向量库(Milvus/Weaviate/Qdrant) + 文档解析
|
| User Frontend | http://129.226.199.247:25137 | admin / admin123 |
|
||||||
| **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态
|
| Commercial Edition | https://web.ruoyiai.chat | WeChat QR code login |
|
||||||
| **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点
|
|
||||||
| **智能体管理** | 基于Langchain4j的Agent框架、Supervisor模式编排,支持多种决策模型,可以灵活搭配工具,skills
|
|
||||||
|
|
||||||
|
## ✨ 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) |
|
| 🔧 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) |
|
||||||
| 🎨 用户前端 | [ruoyi-web](https://github.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitee.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitcode.com/ageerle/ruoyi-web) |
|
| 🎨 User Frontend | [ruoyi-web](https://github.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitee.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitcode.com/ageerle/ruoyi-web) |
|
||||||
| 🛠️ 管理后台 | [ruoyi-admin](https://github.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitee.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitcode.com/ageerle/ruoyi-admin) |
|
| 🛠️ Admin Panel | [ruoyi-admin](https://github.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitee.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitcode.com/ageerle/ruoyi-admin) |
|
||||||
| 🎬 短剧平台 | [ruoyi-drama](https://github.com/ageerle/ruoyi-drama) | [ruoyi-drama](https://gitee.com/ageerle/ruoyi-drama) | [ruoyi-drama](https://gitcode.com/ageerle/ruoyi-drama) |
|
| 🎬 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) |
|
||||||
| 🤖 编程助手 | [ruoyi-copilot](https://github.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitee.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitcode.com/ageerle/ruoyi-copilot) |
|
| 🤖 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) |
|
||||||
| 📱 小程序端 | [ruoyi-uniapp](https://github.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitee.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitcode.com/ageerle/ruoyi-uniapp) |
|
| 📱 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
|
||||||
| 项目名称 | GitHub 仓库 | Gitee 仓库
|
| 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
|
||||||
|
|
||||||
### 核心框架
|
### Core Framework
|
||||||
- **后端架构**:Spring Boot 3.5.8 + Langchain4j
|
- **Backend**: Spring Boot 3.5.8 + Langchain4j
|
||||||
- **数据存储**:MySQL 8.0 + Redis + 向量数据库(Milvus/Weaviate/Qdrant)
|
- **Data Storage**: MySQL 8.0 + Redis + Vector Databases (Milvus/Weaviate/Qdrant)
|
||||||
- **前端技术**:Vue 3 + Vben Admin + element-plus-x
|
- **Frontend**: Vue 3 + Vben Admin + element-plus-x
|
||||||
- **安全认证**:Sa-Token + JWT 双重保障
|
- **Security**: Sa-Token + JWT dual-layer security
|
||||||
- **文档处理**:PDF、Word、Excel 解析,图像智能分析
|
- **Document Processing**: PDF, Word, Excel parsing, intelligent image analysis
|
||||||
- **实时通信**:WebSocket 实时通信,SSE 流式响应
|
- **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
|
```bash
|
||||||
# 克隆仓库
|
# Requirements: Docker Engine and Docker Compose V2
|
||||||
git clone https://github.com/ageerle/ruoyi-ai.git
|
|
||||||
|
# Clone the v3.1.0 release
|
||||||
|
git clone --depth 1 --branch v3.1.0 https://github.com/ageerle/ruoyi-ai.git
|
||||||
cd ruoyi-ai
|
cd ruoyi-ai
|
||||||
|
|
||||||
# 启动所有服务(从镜像仓库拉取预构建镜像)
|
# Pin the image version. Public GHCR images do not require docker login.
|
||||||
docker-compose -f docker-compose-all.yaml up -d
|
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
|
||||||
|
|
||||||
# 查看服务状态
|
# Pull pre-built images from GHCR and start all services
|
||||||
docker-compose -f docker-compose-all.yaml ps
|
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
|
||||||
|
|
||||||
# 访问服务
|
# Check service status
|
||||||
# 管理端: http://localhost:25666 (admin / admin123)
|
docker compose --env-file docs/docker/ruoyi-ai/.env \
|
||||||
# 用户端: http://localhost:25137
|
-f docs/docker/ruoyi-ai/docker-compose-all.yaml ps
|
||||||
# 后端API: http://localhost:26039
|
|
||||||
|
# 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
|
```bash
|
||||||
# 进入后端项目目录
|
# Enter backend project directory
|
||||||
cd ruoyi-ai
|
cd ruoyi-ai
|
||||||
|
|
||||||
# 启动后端服务(源码编译构建)
|
# Start backend service (build from source)
|
||||||
docker-compose up -d --build
|
docker-compose up -d --build
|
||||||
|
|
||||||
# 等待后端服务启动完成
|
# Wait for backend service to start
|
||||||
docker-compose logs -f backend
|
docker-compose logs -f backend
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 第二步:部署管理端
|
#### Step 2: Deploy Admin Panel
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 进入管理端项目目录
|
# Enter admin panel project directory
|
||||||
cd ruoyi-admin
|
cd ruoyi-admin
|
||||||
|
|
||||||
# 构建并启动管理端
|
# Build and start admin panel
|
||||||
docker-compose up -d --build
|
docker-compose up -d --build
|
||||||
|
|
||||||
# 访问管理端
|
# Access admin panel
|
||||||
# 地址: http://localhost:5666
|
# URL: http://localhost:5666
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 第三步:部署用户端(可选)
|
#### Step 3: Deploy User Frontend (Optional)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 进入用户端项目目录
|
# Enter user frontend project directory
|
||||||
cd ruoyi-web
|
cd ruoyi-web
|
||||||
|
|
||||||
# 构建并启动用户端
|
# Build and start user frontend
|
||||||
docker-compose up -d --build
|
docker-compose up -d --build
|
||||||
|
|
||||||
# 访问用户端
|
# Access user frontend
|
||||||
# 地址: http://localhost:5137
|
# URL: http://localhost:5137
|
||||||
```
|
```
|
||||||
|
|
||||||
### 服务端口说明
|
### Service Ports
|
||||||
|
|
||||||
| 服务 | 一键启动端口 | 分步部署端口 | 说明 |
|
| Service | One-click Port | Step-by-step Port | Description |
|
||||||
|------|-------------|-------------|------|
|
|------|-------------|-------------|------|
|
||||||
| 管理端 | 25666 | 5666 | 管理后台访问地址 |
|
| Admin Panel | 25666 | 5666 | Admin backend access |
|
||||||
| 用户端 | 25137 | 5137 | 用户前端访问地址 |
|
| User Frontend | 25137 | 5137 | User frontend access |
|
||||||
| 后端服务 | 26039 | 6039 | 后端 API 服务 |
|
| Backend Service | 26039 | 6039 | Backend API service |
|
||||||
| MySQL | 23306 | 23306 | 数据库服务 |
|
| MySQL | 23306 | 23306 | Database service |
|
||||||
| Redis | 26379 | 6379 | 缓存服务 |
|
| Redis | 26379 | 6379 | Cache service |
|
||||||
| Weaviate | 28080 | 28080 | 向量数据库 |
|
| Weaviate | 28080 | 28080 | Vector database |
|
||||||
| MinIO API | 29000 | 9000 | 对象存储 API |
|
| MinIO API | 29000 | 9000 | Object storage API |
|
||||||
| MinIO Console | 29090 | 9090 | 对象存储控制台 |
|
| MinIO Console | 29090 | 9090 | Object storage console |
|
||||||
|
|
||||||
### 镜像仓库
|
## 📚 Documentation
|
||||||
|
|
||||||
所有镜像托管在阿里云容器镜像服务:
|
Want to learn more about installation, deployment, configuration, and secondary development?
|
||||||
|
|
||||||
```
|
**👉 [Complete Documentation](https://doc.ruoyiai.chat/)**
|
||||||
crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai
|
|
||||||
```
|
|
||||||
|
|
||||||
可用镜像:
|
## 🤝 Contributing
|
||||||
- `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` - 用户端前端
|
|
||||||
|
|
||||||
### 常用命令
|
We warmly welcome community contributions! Whether you are a seasoned developer or just getting started, you can contribute to the project 💪
|
||||||
|
|
||||||
```bash
|
### How to Contribute
|
||||||
# 停止所有服务
|
|
||||||
docker-compose -f docker-compose-all.yaml down
|
|
||||||
|
|
||||||
# 查看服务日志
|
1. **Fork** the project to your account
|
||||||
docker-compose -f docker-compose-all.yaml logs -f [服务名]
|
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
|
||||||
docker-compose -f docker-compose-all.yaml restart [服务名]
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📚 使用文档
|
## 📄 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
|
||||||
|
|
||||||
### 贡献方式
|
**Thanks to the following sponsors for supporting this project:**
|
||||||
|
|
||||||
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">
|
<a href="https://www.atlascloud.ai?ref=89F97E">
|
||||||
<img src="docs/image/sponsor/atlascloud_banner.png" alt="Atlas Cloud" width="160" height="80">
|
<img src="docs/image/sponsor/atlascloud_banner.png" alt="Atlas Cloud" width="160" height="80">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
[访问Atlas Cloud官网](https://www.atlascloud.ai?ref=89F97E&utm_source=github&utm_campaign=ruoyi-drama) · [编程计划优惠](https://www.atlascloud.ai/console/coding-plan)
|
[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)
|
||||||
全模态 AI 推理平台,为开发者提供统一的 AI API,支持视频生成、图像生成和大语言模型。一次接入,即可访问 **300+ 精选模型**。
|
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">
|
<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>
|
</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)
|
[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)
|
||||||
享字节自研豆包模型+满血版开源 SOTA模型,覆盖文本、VLM、图像生成,全模态一站配齐:Seed-2.1、Seedream-5.0、GLM-5.2、DeepSeek等。不止编程、更能解决 Agent 复杂长程任务!
|
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">
|
<div align="center">
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<img src="docs/image/wx.png" alt="微信二维码" width="200" height="200"><br>
|
<img src="docs/image/wx.png" alt="WeChat QR Code" width="200" height="200"><br>
|
||||||
<strong>扫码添加作者微信</strong><br>
|
<strong>Scan to add author on WeChat</strong><br>
|
||||||
<em>邀请进群学习</em>
|
<em>Join group for learning</em>
|
||||||
</td>
|
</td>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<img src="docs/image/wx06.png" alt="微信二维码" width="200" height="200"><br>
|
<img src="docs/image/douyin.png" alt="Douyin QR Code" width="200" height="200"><br>
|
||||||
<strong>微信技术交流群</strong><br>
|
<strong>Douyin Video Tutorials</strong><br>
|
||||||
<em>技术讨论</em>
|
<em>Open Douyin, scan & follow to watch video tutorials</em>
|
||||||
</td>
|
</td>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<img src="docs/image/qq.png" alt="QQ群二维码" width="200" height="200"><br>
|
<img src="docs/image/qq.png" alt="QQ Group QR Code" width="200" height="200"><br>
|
||||||
<strong>QQ技术交流群</strong><br>
|
<strong>QQ Tech Exchange Group</strong><br>
|
||||||
<em>技术讨论</em>
|
<em>Technical discussion</em>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
@@ -255,11 +246,12 @@ docker-compose -f docker-compose-all.yaml restart [服务名]
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
**[⭐ 点个Star支持一下](https://github.com/ageerle/ruoyi-ai)** • **[ Fork 开始贡献](https://github.com/ageerle/ruoyi-ai/fork)** • **[📚 English](README_EN.md)** • **[📖 查看完整文档](https://doc.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>
|
</div>
|
||||||
|
|
||||||
|
|||||||
310
README_EN.md
310
README_EN.md
@@ -1,310 +0,0 @@
|
|||||||
|
|
||||||
# RuoYi AI
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
[![Contributors][contributors-shield]][contributors-url]
|
|
||||||
[![Forks][forks-shield]][forks-url]
|
|
||||||
[![Stargazers][stars-shield]][stars-url]
|
|
||||||
[![Issues][issues-shield]][issues-url]
|
|
||||||
[![MIT License][license-shield]][license-url]
|
|
||||||
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://trendshift.io/repositories/13209">
|
|
||||||
<img src="https://trendshift.io/api/badge/repositories/13209" alt="GitHub Trending">
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<img src="docs/image/logo.png" alt="RuoYi AI Logo" width="120" height="120">
|
|
||||||
|
|
||||||
### Enterprise-Grade AI Assistant Platform
|
|
||||||
|
|
||||||
*An out-of-the-box full-stack AI platform supporting multi-agent collaboration, Supervisor mode orchestration, and multiple decision models, with advanced RAG technology and visual workflow orchestration capabilities*
|
|
||||||
|
|
||||||
**[中文](README.md)** | **[📖 Documentation](https://doc.ruoyiai.chat/)** |
|
|
||||||
**[🚀 Live Demo](https://web.ruoyiai.chat/)** | **[🐛 Report Issues](https://github.com/ageerle/ruoyi-ai/issues)** | **[💡 Feature Requests](https://github.com/ageerle/ruoyi-ai/issues)**
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## ✨ Core Features
|
|
||||||
|
|
||||||
| Module | Current Capabilities |
|
|
||||||
|:---:|---|
|
|
||||||
| **Model Management** | Multi-model integration (OpenAI/DeepSeek/Tongyi/Zhipu/MiniMax), multi-modal understanding, Coze/DIFY/FastGPT platform integration |
|
|
||||||
| **Knowledge Base** | Local RAG + Vector DB (Milvus/Weaviate/Qdrant) + Document parsing |
|
|
||||||
| **Tool Management** | MCP protocol integration, Skills capability + Extensible tool ecosystem |
|
|
||||||
| **Workflow Orchestration** | Visual workflow designer, drag-and-drop node orchestration, SSE streaming execution, currently supports model calls, email sending, manual review nodes |
|
|
||||||
| **Multi-Agent** | Agent framework based on Langchain4j, Supervisor mode orchestration, supports multiple decision models |
|
|
||||||
|
|
||||||
### Project Repositories
|
|
||||||
|
|
||||||
| Module | GitHub Repository | Gitee Repository | GitCode Repository |
|
|
||||||
|----------|-------------------------------------------------------|------------------------------------------------------|--------------------------------------------------------|
|
|
||||||
| 🔧 Backend | [ruoyi-ai](https://github.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitee.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitcode.com/ageerle/ruoyi-ai) |
|
|
||||||
| 🎨 User Frontend | [ruoyi-web](https://github.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitee.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitcode.com/ageerle/ruoyi-web) |
|
|
||||||
| 🛠️ Admin Panel | [ruoyi-admin](https://github.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitee.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitcode.com/ageerle/ruoyi-admin) |
|
|
||||||
| 🎬 Drama | [ruoyi-drama](https://github.com/ageerle/ruoyi-drama) | [ruoyi-drama](https://gitee.com/ageerle/ruoyi-drama) | |
|
|
||||||
| 🤖 Copilot | [ruoyi-copilot](https://github.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitee.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitcode.com/ageerle/ruoyi-copilot) |
|
|
||||||
| 📱 Mini-App | [ruoyi-uniapp](https://github.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitee.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitcode.com/ageerle/ruoyi-uniapp) |
|
|
||||||
|
|
||||||
### Partner Projects
|
|
||||||
| Project Name | GitHub Repository | Gitee Repository |
|
|
||||||
|----------------|-------------------------------------------------------|------------------------------------------------------|
|
|
||||||
| element-plus-x | [element-plus-x](https://github.com/element-plus-x/Element-Plus-X) | [element-plus-x](https://gitee.com/he-jiayue/element-plus-x) |
|
|
||||||
|
|
||||||
## 🛠️ Technical Architecture
|
|
||||||
|
|
||||||
### Core Framework
|
|
||||||
- **Backend**: Spring Boot 3.5.8 + Langchain4j
|
|
||||||
- **Data Storage**: MySQL 8.0 + Redis + Vector Databases (Milvus/Weaviate/Qdrant)
|
|
||||||
- **Frontend**: Vue 3 + Vben Admin + element-plus-x
|
|
||||||
- **Security**: Sa-Token + JWT dual-layer security
|
|
||||||
- **Document Processing**: PDF, Word, Excel parsing, intelligent image analysis
|
|
||||||
- **Real-time Communication**: WebSocket real-time communication, SSE streaming response
|
|
||||||
- **System Monitoring**: Comprehensive logging system, performance monitoring, service health checks
|
|
||||||
|
|
||||||
## 🐳 Docker Deployment
|
|
||||||
|
|
||||||
This project provides two Docker deployment methods:
|
|
||||||
|
|
||||||
### Method 1: One-click Start All Services (Recommended)
|
|
||||||
|
|
||||||
Use `docker-compose-all.yaml` to start all services at once (including backend, admin panel, user frontend, and dependencies):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clone the repository
|
|
||||||
git clone https://github.com/ageerle/ruoyi-ai.git
|
|
||||||
cd ruoyi-ai
|
|
||||||
|
|
||||||
# Start all services (pull pre-built images from registry)
|
|
||||||
docker-compose -f docker-compose-all.yaml up -d
|
|
||||||
|
|
||||||
# Check service status
|
|
||||||
docker-compose -f docker-compose-all.yaml ps
|
|
||||||
|
|
||||||
# Access services
|
|
||||||
# Admin Panel: http://localhost:25666 (admin / admin123)
|
|
||||||
# User Frontend: http://localhost:25137
|
|
||||||
# Backend API: http://localhost:26039
|
|
||||||
```
|
|
||||||
|
|
||||||
### Method 2: Step-by-step Deployment (Source Build)
|
|
||||||
|
|
||||||
If you need to build backend services from source, follow these steps:
|
|
||||||
|
|
||||||
#### Step 1: Deploy Backend Service
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Enter backend project directory
|
|
||||||
cd ruoyi-ai
|
|
||||||
|
|
||||||
# Start backend service (build from source)
|
|
||||||
docker-compose up -d --build
|
|
||||||
|
|
||||||
# Wait for backend service to start
|
|
||||||
docker-compose logs -f backend
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Step 2: Deploy Admin Panel
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Enter admin panel project directory
|
|
||||||
cd ruoyi-admin
|
|
||||||
|
|
||||||
# Build and start admin panel
|
|
||||||
docker-compose up -d --build
|
|
||||||
|
|
||||||
# Access admin panel
|
|
||||||
# URL: http://localhost:5666
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Step 3: Deploy User Frontend (Optional)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Enter user frontend project directory
|
|
||||||
cd ruoyi-web
|
|
||||||
|
|
||||||
# Build and start user frontend
|
|
||||||
docker-compose up -d --build
|
|
||||||
|
|
||||||
# Access user frontend
|
|
||||||
# URL: http://localhost:5137
|
|
||||||
```
|
|
||||||
|
|
||||||
### Service Ports
|
|
||||||
|
|
||||||
| Service | One-click Port | Step-by-step Port | Description |
|
|
||||||
|------|-------------|-------------|------|
|
|
||||||
| Admin Panel | 25666 | 5666 | Admin backend access |
|
|
||||||
| User Frontend | 25137 | 5137 | User frontend access |
|
|
||||||
| Backend Service | 26039 | 6039 | Backend API service |
|
|
||||||
| MySQL | 23306 | 23306 | Database service |
|
|
||||||
| Redis | 26379 | 6379 | Cache service |
|
|
||||||
| Weaviate | 28080 | 28080 | Vector database |
|
|
||||||
| MinIO API | 29000 | 9000 | Object storage API |
|
|
||||||
| MinIO Console | 29090 | 9090 | Object storage console |
|
|
||||||
|
|
||||||
### Image Registry
|
|
||||||
|
|
||||||
All images are hosted on Alibaba Cloud Container Registry:
|
|
||||||
|
|
||||||
```
|
|
||||||
crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai
|
|
||||||
```
|
|
||||||
|
|
||||||
Available images:
|
|
||||||
- `mysql:v3` - MySQL database (includes initialization SQL)
|
|
||||||
- `redis:6.2` - Redis cache
|
|
||||||
- `weaviate:1.30.0` - Vector database
|
|
||||||
- `minio:latest` - Object storage
|
|
||||||
- `ruoyi-ai-backend:latest` - Backend service
|
|
||||||
- `ruoyi-ai-admin:latest` - Admin frontend
|
|
||||||
- `ruoyi-ai-web:latest` - User frontend
|
|
||||||
|
|
||||||
### Common Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Stop all services
|
|
||||||
docker-compose -f docker-compose-all.yaml down
|
|
||||||
|
|
||||||
# View service logs
|
|
||||||
docker-compose -f docker-compose-all.yaml logs -f [service-name]
|
|
||||||
|
|
||||||
# Restart a service
|
|
||||||
docker-compose -f docker-compose-all.yaml restart [service-name]
|
|
||||||
```
|
|
||||||
|
|
||||||
### MiniMax Configuration
|
|
||||||
|
|
||||||
The built-in MiniMax provider accepts one API Host value and selects the matching protocol adapter. Use a Base URL from this table:
|
|
||||||
|
|
||||||
| Region | OpenAI-compatible Base URL | Anthropic-compatible Base URL |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Global | `https://api.minimax.io/v1` | `https://api.minimax.io/anthropic` |
|
|
||||||
| China | `https://api.minimaxi.com/v1` | `https://api.minimaxi.com/anthropic` |
|
|
||||||
|
|
||||||
For Anthropic-compatible requests, configure the Base URL ending in `/anthropic`. Do not append `/v1` or `/v1/messages`; the provider adapter derives the request path internally.
|
|
||||||
|
|
||||||
| Model ID | Total context | Input modalities | Thinking |
|
|
||||||
| --- | ---: | --- | --- |
|
|
||||||
| `MiniMax-M3` | 1,000,000 tokens | Text, image, video | Adaptive or disabled |
|
|
||||||
| `MiniMax-M2.7` | 204,800 tokens | Text | Always on |
|
|
||||||
|
|
||||||
Current pay-as-you-go prices are in USD per million tokens:
|
|
||||||
|
|
||||||
| Model | Service tier and input range | Input | Output | Cache read | Cache write |
|
|
||||||
| --- | --- | ---: | ---: | ---: | ---: |
|
|
||||||
| `MiniMax-M3` | Standard, up to 512,000 input tokens | $0.30 | $1.20 | $0.06 | Not listed |
|
|
||||||
| `MiniMax-M3` | Standard, over 512,000 input tokens | $0.60 | $2.40 | $0.12 | Not listed |
|
|
||||||
| `MiniMax-M3` | Priority, up to 512,000 input tokens | $0.45 | $1.80 | $0.09 | Not listed |
|
|
||||||
| `MiniMax-M3` | Priority, over 512,000 input tokens | $0.90 | $3.60 | $0.18 | Not listed |
|
|
||||||
| `MiniMax-M2.7` | Standard | $0.30 | $1.20 | $0.06 | $0.375 |
|
|
||||||
|
|
||||||
See the [official API overview](https://platform.minimax.io/docs/api-reference/api-overview) and [pay-as-you-go pricing](https://platform.minimax.io/docs/guides/pricing-paygo) for current details.
|
|
||||||
|
|
||||||
## 📚 Documentation
|
|
||||||
|
|
||||||
Want to learn more about installation, deployment, configuration, and secondary development?
|
|
||||||
|
|
||||||
**👉 [Complete Documentation](https://doc.ruoyiai.chat/)**
|
|
||||||
|
|
||||||
## 🤝 Contributing
|
|
||||||
|
|
||||||
We warmly welcome community contributions! Whether you are a seasoned developer or just getting started, you can contribute to the project 💪
|
|
||||||
|
|
||||||
### How to Contribute
|
|
||||||
|
|
||||||
1. **Fork** the project to your account
|
|
||||||
2. **Create a branch** (`git checkout -b feature/new-feature-name`)
|
|
||||||
3. **Commit your changes** (`git commit -m 'Add new feature'`)
|
|
||||||
4. **Push to the branch** (`git push origin feature/new-feature-name`)
|
|
||||||
5. **Create a Pull Request**
|
|
||||||
|
|
||||||
> 💡 **Tip**: We recommend submitting PRs to GitHub, we will automatically sync to other code hosting platforms
|
|
||||||
|
|
||||||
## 📄 License
|
|
||||||
|
|
||||||
This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details.
|
|
||||||
|
|
||||||
## 🙏 Acknowledgments
|
|
||||||
|
|
||||||
Thanks to the following excellent open-source projects for their support:
|
|
||||||
- [Langchain4j](https://github.com/langchain4j/langchain4j) - Powerful Java LLM development framework
|
|
||||||
- [RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus) - Mature enterprise-level rapid development framework
|
|
||||||
- [Vben Admin](https://github.com/vbenjs/vue-vben-admin) - Modern Vue admin template
|
|
||||||
|
|
||||||
## 💎 Sponsors
|
|
||||||
|
|
||||||
**Thanks to the following sponsors for supporting this project:**
|
|
||||||
|
|
||||||
<a href="https://www.atlascloud.ai?ref=89F97E">
|
|
||||||
<img src="docs/image/sponsor/atlascloud_banner.png" alt="Atlas Cloud" width="160" height="80">
|
|
||||||
</a>
|
|
||||||
|
|
||||||
[Visit Atlas Cloud](https://www.atlascloud.ai?ref=89F97E) · [Coding Plan Promotion](https://www.atlascloud.ai/console/coding-plan)
|
|
||||||
A full-modal AI inference platform that gives developers a unified AI API, supporting video generation, image generation, and LLMs. Connect once to access **300+ curated models**.
|
|
||||||
|
|
||||||
<a href="https://www.volcengine.com/activity/codingplan?utm_campaign=hw&utm_content=hw&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ageerle-ruoyi-ai">
|
|
||||||
<img src="docs/image/sponsor/huoshan.png" alt="Volcengine CodingPlan" width="160" height="80">
|
|
||||||
</a>
|
|
||||||
|
|
||||||
[Volcengine CodingPlan Developer Program](https://www.volcengine.com/activity/codingplan?utm_campaign=hw&utm_content=hw&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ageerle-ruoyi-ai)
|
|
||||||
Volcengine is ByteDance's cloud and AI service platform. Volcengine Ark provides API access to Doubao LLM, DeepSeek, and more — a one-stop AI development and inference platform for developers.
|
|
||||||
|
|
||||||
## 💬 Community Chat
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td align="center">
|
|
||||||
<img src="docs/image/wx.png" alt="WeChat QR Code" width="200" height="200"><br>
|
|
||||||
<strong>Scan to add author on WeChat</strong><br>
|
|
||||||
<em>Join group for learning</em>
|
|
||||||
</td>
|
|
||||||
<td align="center">
|
|
||||||
<img src="docs/image/qq.png" alt="QQ Group QR Code" width="200" height="200"><br>
|
|
||||||
<strong>QQ Tech Exchange Group</strong><br>
|
|
||||||
<em>Technical discussion</em>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
**[⭐ Star to Support](https://github.com/ageerle/ruoyi-ai)** • **[Fork to Contribute](https://github.com/ageerle/ruoyi-ai/fork)** • **[📚 中文](README.md)** • **[📖 Complete Documentation](https://doc.ruoyiai.chat/)**
|
|
||||||
|
|
||||||
*Built with ❤️, maintained by the RuoYi AI open-source community*
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Badge Links -->
|
|
||||||
|
|
||||||
[contributors-shield]: https://img.shields.io/github/contributors/ageerle/ruoyi-ai.svg?style=flat-square
|
|
||||||
|
|
||||||
[contributors-url]: https://github.com/ageerle/ruoyi-ai/graphs/contributors
|
|
||||||
|
|
||||||
[forks-shield]: https://img.shields.io/github/forks/ageerle/ruoyi-ai.svg?style=flat-square
|
|
||||||
|
|
||||||
[forks-url]: https://github.com/ageerle/ruoyi-ai/network/members
|
|
||||||
|
|
||||||
[stars-shield]: https://img.shields.io/github/stars/ageerle/ruoyi-ai.svg?style=flat-square
|
|
||||||
|
|
||||||
[stars-url]: https://github.com/ageerle/ruoyi-ai/stargazers
|
|
||||||
|
|
||||||
[issues-shield]: https://img.shields.io/github/issues/ageerle/ruoyi-ai.svg?style=flat-square
|
|
||||||
|
|
||||||
[issues-url]: https://github.com/ageerle/ruoyi-ai/issues
|
|
||||||
|
|
||||||
[license-shield]: https://img.shields.io/github/license/ageerle/ruoyi-ai.svg?style=flat-square
|
|
||||||
|
|
||||||
[license-url]: https://github.com/ageerle/ruoyi-ai/blob/main/LICENSE
|
|
||||||
277
README_ZH.md
Normal file
277
README_ZH.md
Normal 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
|
||||||
6
docs/docker/ruoyi-ai/.env.example
Normal file
6
docs/docker/ruoyi-ai/.env.example
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# GitHub Container Registry namespace owner.
|
||||||
|
# For the upstream project this is ageerle; change it when using a fork.
|
||||||
|
IMAGE_OWNER=ageerle
|
||||||
|
|
||||||
|
# Use latest for convenience, or pin a release such as v3.1.0.
|
||||||
|
RUIYI_VERSION=latest
|
||||||
@@ -10,13 +10,13 @@
|
|||||||
# - RuoYi-Admin (管理端前端)
|
# - RuoYi-Admin (管理端前端)
|
||||||
# - RuoYi-Web (用户端前端)
|
# - RuoYi-Web (用户端前端)
|
||||||
#
|
#
|
||||||
# 镜像仓库地址: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai
|
# 镜像仓库地址: ghcr.io/ageerle
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# ==================== MySQL 数据库 ====================
|
# ==================== MySQL 数据库 ====================
|
||||||
mysql:
|
mysql:
|
||||||
# 阿里云镜像地址(包含初始化SQL)
|
# GHCR 镜像(包含初始化SQL)
|
||||||
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/mysql:v3
|
image: ghcr.io/${IMAGE_OWNER:-ageerle}/ruoyi-ai-mysql:${RUIYI_VERSION:-latest}
|
||||||
container_name: ruoyi-ai-mysql
|
container_name: ruoyi-ai-mysql
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
@@ -41,7 +41,7 @@ services:
|
|||||||
|
|
||||||
# ==================== Redis 缓存 ====================
|
# ==================== Redis 缓存 ====================
|
||||||
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
|
container_name: ruoyi-ai-redis
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
@@ -59,7 +59,7 @@ services:
|
|||||||
|
|
||||||
# ==================== Weaviate 向量数据库 ====================
|
# ==================== Weaviate 向量数据库 ====================
|
||||||
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
|
container_name: ruoyi-ai-weaviate
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
@@ -78,7 +78,7 @@ services:
|
|||||||
|
|
||||||
# ==================== MinIO 对象存储 ====================
|
# ==================== MinIO 对象存储 ====================
|
||||||
minio:
|
minio:
|
||||||
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/minio:latest
|
image: minio/minio:latest
|
||||||
container_name: ruoyi-ai-minio
|
container_name: ruoyi-ai-minio
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
@@ -95,7 +95,7 @@ services:
|
|||||||
|
|
||||||
# ==================== RuoYi-AI 后端服务 ====================
|
# ==================== RuoYi-AI 后端服务 ====================
|
||||||
backend:
|
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
|
container_name: ruoyi-ai-backend
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
@@ -129,7 +129,7 @@ services:
|
|||||||
|
|
||||||
# ==================== RuoYi-AI 管理端前端 ====================
|
# ==================== RuoYi-AI 管理端前端 ====================
|
||||||
admin-frontend:
|
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
|
container_name: ruoyi-ai-admin
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
@@ -154,7 +154,7 @@ services:
|
|||||||
|
|
||||||
# ==================== RuoYi-AI 用户端前端 ====================
|
# ==================== RuoYi-AI 用户端前端 ====================
|
||||||
web-frontend:
|
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
|
container_name: ruoyi-ai-web
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
BIN
docs/image/douyin.png
Normal file
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
@@ -1,5 +1,5 @@
|
|||||||
-- 补充工作流节点消息模板配置 (对应 issue IJX5VV)
|
-- 补充工作流节点消息模板配置 (对应 issue IJX5VV)
|
||||||
-- 背景:NodeMessageTemplateEnum 依赖以下 9 个 sys_config 键,缺失时
|
-- 背景:NodeMessageTemplateEnum 依赖以下 7 个 sys_config 键,缺失时
|
||||||
-- WorkflowMessageUtil.getNodeMessageTemplate 会抛出「请先配置该节点的响应模板」。
|
-- WorkflowMessageUtil.getNodeMessageTemplate 会抛出「请先配置该节点的响应模板」。
|
||||||
-- 这批配置在历史提交 20d531c0 中存在,SQL 脚本合并重命名时遗失,此处恢复。
|
-- 这批配置在历史提交 20d531c0 中存在,SQL 脚本合并重命名时遗失,此处恢复。
|
||||||
-- 幂等:按 config_key + tenant_id 判重,可重复执行。
|
-- 幂等:按 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
|
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');
|
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`)
|
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
|
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');
|
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
|
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');
|
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`)
|
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
|
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');
|
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.exception.template' AND `tenant_id` = '000000');
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
-- 注册链路修复:补齐缺失的菜单权限标识 + 新建注册默认角色
|
||||||
|
-- 关联问题:
|
||||||
|
-- #3 注册用户无默认角色 -> 无权限访问 AI 对话界面
|
||||||
|
-- #4 sys_menu 缺 system:session:* / system:attach:* / system:fragment:*,
|
||||||
|
-- 且 知识管理 父菜单 perms 为 knowledge:info:list,与控制器
|
||||||
|
-- KnowledgeInfoController 用的 system:info:list 对不上,:list 无法授权。
|
||||||
|
-- 本脚本幂等,可重复执行(INSERT IGNORE + UPDATE 天然幂等)。
|
||||||
|
-- 新增 menu_id / role_id / config_id 统一使用 2099010100000000xxx 段,
|
||||||
|
-- 与现有 snowflake id(2xxxxxxxxxxxxxxxxx)不冲突。
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. 修正 知识管理 父菜单权限标识:knowledge:info:list -> system:info:list
|
||||||
|
-- ============================================================
|
||||||
|
UPDATE `sys_menu`
|
||||||
|
SET `perms` = 'system:info:list'
|
||||||
|
WHERE `menu_id` = 2006681261898813441 AND `perms` = 'knowledge:info:list';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. 新增 会话管理 菜单组(system:session:*)
|
||||||
|
-- 父菜单=对话管理(2000209300188356609)。
|
||||||
|
-- 管理端暂无 chat/session 页面,故 C 菜单设为隐藏(visible=1),
|
||||||
|
-- 权限标识仍可在角色管理中授权给用户端使用。
|
||||||
|
-- ============================================================
|
||||||
|
INSERT IGNORE INTO `sys_menu` VALUES
|
||||||
|
(2099010100000000001, '会话管理', 2000209300188356609, 6, 'session', NULL, NULL, 1, 0, 'C', '1', '0', 'system:session:list', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, '会话管理菜单'),
|
||||||
|
(2099010100000000002, '会话管理查询', 2099010100000000001, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:session:query', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''),
|
||||||
|
(2099010100000000003, '会话管理新增', 2099010100000000001, 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:session:add', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''),
|
||||||
|
(2099010100000000004, '会话管理修改', 2099010100000000001, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:session:edit', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''),
|
||||||
|
(2099010100000000005, '会话管理删除', 2099010100000000001, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:session:remove', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''),
|
||||||
|
(2099010100000000006, '会话管理导出', 2099010100000000001, 5, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:session:export', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, '');
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. 新增 知识附件 菜单组(system:attach:*)
|
||||||
|
-- 父菜单=对话管理(2000209300188356609),组件对应管理端 knowledge/attach/index。
|
||||||
|
-- ============================================================
|
||||||
|
INSERT IGNORE INTO `sys_menu` VALUES
|
||||||
|
(2099010100000000010, '知识附件', 2000209300188356609, 7, 'attach', 'knowledge/attach/index', NULL, 1, 0, 'C', '0', '0', 'system:attach:list', 'ant-design:paper-clip-outlined', 103, 1, '2026-07-24 00:00:00', NULL, NULL, '知识附件菜单'),
|
||||||
|
(2099010100000000011, '知识附件查询', 2099010100000000010, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:attach:query', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''),
|
||||||
|
(2099010100000000012, '知识附件新增', 2099010100000000010, 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:attach:add', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''),
|
||||||
|
(2099010100000000013, '知识附件修改', 2099010100000000010, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:attach:edit', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''),
|
||||||
|
(2099010100000000014, '知识附件删除', 2099010100000000010, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:attach:remove', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''),
|
||||||
|
(2099010100000000015, '知识附件导出', 2099010100000000010, 5, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:attach:export', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, '');
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 4. 新增 知识片段 菜单组(system:fragment:*)
|
||||||
|
-- 父菜单=对话管理(2000209300188356609),组件对应管理端 knowledge/fragment/index。
|
||||||
|
-- ============================================================
|
||||||
|
INSERT IGNORE INTO `sys_menu` VALUES
|
||||||
|
(2099010100000000020, '知识片段', 2000209300188356609, 8, 'fragment', 'knowledge/fragment/index', NULL, 1, 0, 'C', '0', '0', 'system:fragment:list', 'ant-design:file-text-outlined', 103, 1, '2026-07-24 00:00:00', NULL, NULL, '知识片段菜单'),
|
||||||
|
(2099010100000000021, '知识片段查询', 2099010100000000020, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:fragment:query', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''),
|
||||||
|
(2099010100000000022, '知识片段新增', 2099010100000000020, 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:fragment:add', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''),
|
||||||
|
(2099010100000000023, '知识片段修改', 2099010100000000020, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:fragment:edit', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''),
|
||||||
|
(2099010100000000024, '知识片段删除', 2099010100000000020, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:fragment:remove', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''),
|
||||||
|
(2099010100000000025, '知识片段导出', 2099010100000000020, 5, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:fragment:export', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, '');
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 5. 新建 普通用户 角色(租户 000000,data_scope=5 仅本人)
|
||||||
|
-- ============================================================
|
||||||
|
INSERT IGNORE INTO `sys_role` VALUES
|
||||||
|
(2099010100000000030, '000000', '普通用户', 'user', 2, '5', 1, 1, '0', '0', 103, 1, '2026-07-24 00:00:00', NULL, NULL, '注册用户默认角色');
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 6. 角色-菜单关联:普通用户 拥有用户端 AI 对话所需权限
|
||||||
|
-- 会话管理(001-006) + 聊天消息(2000210914680823809 及 810-814)
|
||||||
|
-- + 知识管理(2006681261898813441 及 442-446)
|
||||||
|
-- + 知识附件(010-015) + 知识片段(020-025)
|
||||||
|
-- ============================================================
|
||||||
|
INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES
|
||||||
|
-- 会话管理
|
||||||
|
(2099010100000000030, 2099010100000000001),
|
||||||
|
(2099010100000000030, 2099010100000000002),
|
||||||
|
(2099010100000000030, 2099010100000000003),
|
||||||
|
(2099010100000000030, 2099010100000000004),
|
||||||
|
(2099010100000000030, 2099010100000000005),
|
||||||
|
(2099010100000000030, 2099010100000000006),
|
||||||
|
-- 聊天消息
|
||||||
|
(2099010100000000030, 2000210914680823809),
|
||||||
|
(2099010100000000030, 2000210914680823810),
|
||||||
|
(2099010100000000030, 2000210914680823811),
|
||||||
|
(2099010100000000030, 2000210914680823812),
|
||||||
|
(2099010100000000030, 2000210914680823813),
|
||||||
|
(2099010100000000030, 2000210914680823814),
|
||||||
|
-- 知识管理
|
||||||
|
(2099010100000000030, 2006681261898813441),
|
||||||
|
(2099010100000000030, 2006681261898813442),
|
||||||
|
(2099010100000000030, 2006681261898813443),
|
||||||
|
(2099010100000000030, 2006681261898813444),
|
||||||
|
(2099010100000000030, 2006681261898813445),
|
||||||
|
(2099010100000000030, 2006681261898813446),
|
||||||
|
-- 知识附件
|
||||||
|
(2099010100000000030, 2099010100000000010),
|
||||||
|
(2099010100000000030, 2099010100000000011),
|
||||||
|
(2099010100000000030, 2099010100000000012),
|
||||||
|
(2099010100000000030, 2099010100000000013),
|
||||||
|
(2099010100000000030, 2099010100000000014),
|
||||||
|
(2099010100000000030, 2099010100000000015),
|
||||||
|
-- 知识片段
|
||||||
|
(2099010100000000030, 2099010100000000020),
|
||||||
|
(2099010100000000030, 2099010100000000021),
|
||||||
|
(2099010100000000030, 2099010100000000022),
|
||||||
|
(2099010100000000030, 2099010100000000023),
|
||||||
|
(2099010100000000030, 2099010100000000024),
|
||||||
|
(2099010100000000030, 2099010100000000025);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 7. 注册默认角色配置项 sys.register.defaultRoleId
|
||||||
|
-- 后端 SysRegisterService 注册成功后读取此配置为新用户绑定角色;
|
||||||
|
-- 值为空字符串时不绑定(保留旧行为)。
|
||||||
|
-- ============================================================
|
||||||
|
INSERT IGNORE INTO `sys_config` VALUES
|
||||||
|
(2099010100000000031, '000000', '注册-默认角色ID', 'sys.register.defaultRoleId', '2099010100000000030', 'Y', 103, 1, '2026-07-24 00:00:00', NULL, NULL, '新注册用户绑定的默认角色ID,留空则不绑定');
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 8. 前置条件(运营项,默认不执行):开启用户注册
|
||||||
|
-- sys.account.registerUser 默认 false,后端 AuthController.register
|
||||||
|
-- 会据此返回「当前系统没有开启注册功能」。若要开放注册,取消注释执行。
|
||||||
|
-- ============================================================
|
||||||
|
-- UPDATE `sys_config` SET `config_value` = 'true'
|
||||||
|
-- WHERE `config_key` = 'sys.account.registerUser' AND `tenant_id` = '000000';
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
-- 一次性补全工作流节点消息模板配置 (NodeMessageTemplateEnum 全部 8 个键)
|
||||||
|
-- 背景:节点执行时 WorkflowMessageUtil.getNodeMessageTemplate 从 sys_config 读取展示模板,
|
||||||
|
-- 历史库中这批配置缺失, 导致运行工作流抛「请先配置该节点的响应模板」。
|
||||||
|
-- 其中前 7 个见 2026-07-21-sys-config-node-template.sql,
|
||||||
|
-- Google Search 为此前从未入库的节点模板。
|
||||||
|
-- 说明:代码已增加内置默认模板兜底, 本脚本为可选, 执行后模板可在 系统管理-配置管理 中自定义。
|
||||||
|
-- 幂等:按 config_key + tenant_id 判重, 可重复执行。
|
||||||
|
|
||||||
|
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||||
|
SELECT 2027192921483309058, '000000', 'HTTP请求节点响应模板', 'node.httpRequest.template', '✅ HTTP请求节点:结束响应 - ', 'Y', 103, 1, '2026-02-27 09:23:51', 1, '2026-02-27 09:31:41', NULL
|
||||||
|
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.httpRequest.template' AND `tenant_id` = '000000');
|
||||||
|
|
||||||
|
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||||
|
SELECT 2027193296990957569, '000000', '文生图节点响应模板', 'node.image.template', '🎨 文生图节点:结束响应 - 图片URL: ', 'Y', 103, 1, '2026-02-27 09:25:20', 1, '2026-02-27 09:31:52', NULL
|
||||||
|
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.image.template' AND `tenant_id` = '000000');
|
||||||
|
|
||||||
|
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||||
|
SELECT 2027193820393959425, '000000', '发送邮箱节点响应模板', 'node.mailsend.template', '📧 发送邮箱节点:结束响应 - ', 'Y', 103, 1, '2026-02-27 09:27:25', 1, '2026-02-27 09:32:05', NULL
|
||||||
|
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.mailsend.template' AND `tenant_id` = '000000');
|
||||||
|
|
||||||
|
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||||
|
SELECT 2027194134438277122, '000000', '结束节点响应模板', 'node.end.template', '🔚 流程已执行完毕,如果您有其他需求,请随时重新发起请求。', 'Y', 103, 1, '2026-02-27 09:28:40', 1, '2026-02-27 09:32:53', NULL
|
||||||
|
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.end.template' AND `tenant_id` = '000000');
|
||||||
|
|
||||||
|
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||||
|
SELECT 2027208880369647617, '000000', '条件分支节点响应模板', 'node.switch.template', '🔀 条件分支节点:触发 -> 跳转到节点 ', 'Y', 103, 1, '2026-02-27 10:27:15', 1, '2026-02-27 10:35:54', NULL
|
||||||
|
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.switch.template' AND `tenant_id` = '000000');
|
||||||
|
|
||||||
|
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||||
|
SELECT 2027213914603995137, '000000', '大模型回答节点响应模板', 'node.llmAnswer.template', '🤖 LLM 节点 生成回答:', 'Y', 103, 1, '2026-02-27 10:47:16', 1, '2026-02-27 10:52:40', NULL
|
||||||
|
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.llmAnswer.template' AND `tenant_id` = '000000');
|
||||||
|
|
||||||
|
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||||
|
SELECT 2027217577397391361, '000000', '工作流异常响应模板', 'node.exception.template', '🛑 工作流发生异常:', 'N', 103, 1, '2026-02-27 11:01:49', 1, '2026-02-27 11:02:01', NULL
|
||||||
|
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.exception.template' AND `tenant_id` = '000000');
|
||||||
|
|
||||||
|
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||||
|
SELECT 2084157200000000003, '000000', '网络搜索节点响应模板', 'node.googleSearch.template', '🔍 网络搜索节点处理完成:', 'Y', 103, 1, '2026-07-29 19:40:00', 1, '2026-07-29 19:40:00', NULL
|
||||||
|
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.googleSearch.template' AND `tenant_id` = '000000');
|
||||||
66
docs/script/sql/update/2026-07-29-zhipu-web-search-node.sql
Normal file
66
docs/script/sql/update/2026-07-29-zhipu-web-search-node.sql
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
-- 智谱 Web Search 工作流扩展节点
|
||||||
|
-- 内部组件名继续使用 Google,以兼容现有前端组件和已保存流程。
|
||||||
|
|
||||||
|
UPDATE `t_workflow_component`
|
||||||
|
SET `title` = '网络搜索',
|
||||||
|
`remark` = '调用智谱 Web Search 检索互联网信息',
|
||||||
|
`display_order` = 40,
|
||||||
|
`is_enable` = 1,
|
||||||
|
`is_deleted` = 0,
|
||||||
|
`update_time` = NOW()
|
||||||
|
WHERE `name` = 'Google'
|
||||||
|
AND `tenant_id` = '000000';
|
||||||
|
|
||||||
|
INSERT INTO `t_workflow_component`
|
||||||
|
(`uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`,
|
||||||
|
`create_time`, `update_time`, `is_deleted`, `tenant_id`)
|
||||||
|
SELECT
|
||||||
|
'a7f8c2d44e5b4c83a9d6f103c2b47e18',
|
||||||
|
'Google',
|
||||||
|
'网络搜索',
|
||||||
|
'调用智谱 Web Search 检索互联网信息',
|
||||||
|
40,
|
||||||
|
1,
|
||||||
|
NOW(),
|
||||||
|
NOW(),
|
||||||
|
0,
|
||||||
|
'000000'
|
||||||
|
FROM DUAL
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM `t_workflow_component`
|
||||||
|
WHERE `name` = 'Google'
|
||||||
|
AND `tenant_id` = '000000'
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE `sys_config`
|
||||||
|
SET `config_name` = '网络搜索节点响应模板',
|
||||||
|
`config_value` = '🔍 网络搜索节点处理完成:',
|
||||||
|
`update_time` = NOW()
|
||||||
|
WHERE `config_key` = 'node.googleSearch.template'
|
||||||
|
AND `tenant_id` = '000000';
|
||||||
|
|
||||||
|
INSERT INTO `sys_config`
|
||||||
|
(`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`,
|
||||||
|
`config_type`, `create_dept`, `create_by`, `create_time`, `update_by`,
|
||||||
|
`update_time`, `remark`)
|
||||||
|
SELECT
|
||||||
|
2084157200000000003,
|
||||||
|
'000000',
|
||||||
|
'网络搜索节点响应模板',
|
||||||
|
'node.googleSearch.template',
|
||||||
|
'🔍 网络搜索节点处理完成:',
|
||||||
|
'Y',
|
||||||
|
103,
|
||||||
|
1,
|
||||||
|
NOW(),
|
||||||
|
1,
|
||||||
|
NOW(),
|
||||||
|
'智谱 Web Search 工作流扩展节点'
|
||||||
|
FROM DUAL
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM `sys_config`
|
||||||
|
WHERE `config_key` = 'node.googleSearch.template'
|
||||||
|
AND `tenant_id` = '000000'
|
||||||
|
);
|
||||||
16
pom.xml
16
pom.xml
@@ -43,8 +43,6 @@
|
|||||||
<aws.sdk.version>2.28.22</aws.sdk.version>
|
<aws.sdk.version>2.28.22</aws.sdk.version>
|
||||||
<!-- SMS 配置 -->
|
<!-- SMS 配置 -->
|
||||||
<sms4j.version>3.3.5</sms4j.version>
|
<sms4j.version>3.3.5</sms4j.version>
|
||||||
<!-- 限制框架中的fastjson版本 -->
|
|
||||||
<fastjson.version>1.2.83</fastjson.version>
|
|
||||||
<!-- 面向运行时的D-ORM依赖 -->
|
<!-- 面向运行时的D-ORM依赖 -->
|
||||||
<anyline.version>8.7.2-20250603</anyline.version>
|
<anyline.version>8.7.2-20250603</anyline.version>
|
||||||
<!-- 工作流配置 -->
|
<!-- 工作流配置 -->
|
||||||
@@ -63,6 +61,8 @@
|
|||||||
<weaviate.version>1.19.6</weaviate.version>
|
<weaviate.version>1.19.6</weaviate.version>
|
||||||
<dify.version>1.2.7</dify.version>
|
<dify.version>1.2.7</dify.version>
|
||||||
<coze.version>0.4.2</coze.version>
|
<coze.version>0.4.2</coze.version>
|
||||||
|
<!-- 智谱官方 Java SDK -->
|
||||||
|
<zai-sdk.version>0.3.5</zai-sdk.version>
|
||||||
|
|
||||||
<!-- gRPC 版本 - 解决 Milvus SDK 依赖冲突 -->
|
<!-- gRPC 版本 - 解决 Milvus SDK 依赖冲突 -->
|
||||||
<grpc.version>1.62.2</grpc.version>
|
<grpc.version>1.62.2</grpc.version>
|
||||||
@@ -347,6 +347,12 @@
|
|||||||
<groupId>me.zhyd.oauth</groupId>
|
<groupId>me.zhyd.oauth</groupId>
|
||||||
<artifactId>JustAuth</artifactId>
|
<artifactId>JustAuth</artifactId>
|
||||||
<version>${justauth.version}</version>
|
<version>${justauth.version}</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>com.alibaba</groupId>
|
||||||
|
<artifactId>fastjson</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 离线IP地址定位库 ip2region -->
|
<!-- 离线IP地址定位库 ip2region -->
|
||||||
@@ -356,12 +362,6 @@
|
|||||||
<version>${ip2region.version}</version>
|
<version>${ip2region.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.alibaba</groupId>
|
|
||||||
<artifactId>fastjson</artifactId>
|
|
||||||
<version>${fastjson.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.ruoyi</groupId>
|
<groupId>org.ruoyi</groupId>
|
||||||
<artifactId>ruoyi-system</artifactId>
|
<artifactId>ruoyi-system</artifactId>
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ spring:
|
|||||||
# rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
|
# 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
|
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
|
username: root
|
||||||
password: 123456
|
password: root
|
||||||
# agent:
|
# 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://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
|
# # url: jdbc:mysql://localhost:3306/agent_db
|
||||||
|
|||||||
@@ -332,6 +332,16 @@ vector-store:
|
|||||||
api-key:
|
api-key:
|
||||||
use-tls: false
|
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:
|
short-drama:
|
||||||
composition:
|
composition:
|
||||||
|
|||||||
@@ -38,8 +38,8 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.alibaba</groupId>
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
<artifactId>fastjson</artifactId>
|
<artifactId>jackson-databind</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package org.ruoyi.common.chat.domain.dto.request;
|
package org.ruoyi.common.chat.domain.dto.request;
|
||||||
|
|
||||||
import com.alibaba.fastjson.annotation.JSONField;
|
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||||
import dev.langchain4j.data.message.ChatMessage;
|
import dev.langchain4j.data.message.ChatMessage;
|
||||||
@@ -31,7 +30,6 @@ public class ChatRequest {
|
|||||||
* 智能体ID。传入时后端按智能体配置解析模型/工具/技能/知识库/提示词/是否深度思考。
|
* 智能体ID。传入时后端按智能体配置解析模型/工具/技能/知识库/提示词/是否深度思考。
|
||||||
*/
|
*/
|
||||||
@JsonSerialize(using = ToStringSerializer.class)
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
@JSONField(serializeUsing = String.class)
|
|
||||||
private Long agentId;
|
private Long agentId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,7 +52,6 @@ public class ChatRequest {
|
|||||||
* 会话id
|
* 会话id
|
||||||
*/
|
*/
|
||||||
@JsonSerialize(using = ToStringSerializer.class)
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
@JSONField(serializeUsing = String.class)
|
|
||||||
private Long sessionId;
|
private Long sessionId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,7 +68,6 @@ public class ChatRequest {
|
|||||||
* 对话id(每个聊天窗口都不一样)
|
* 对话id(每个聊天窗口都不一样)
|
||||||
*/
|
*/
|
||||||
@JsonSerialize(using = ToStringSerializer.class)
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
@JSONField(serializeUsing = String.class)
|
|
||||||
private Long uuid;
|
private Long uuid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ public enum ErrorEnum {
|
|||||||
A_WF_RUNTIME_NOT_FOUND("A00045", "工作流运行时数据找不到"),
|
A_WF_RUNTIME_NOT_FOUND("A00045", "工作流运行时数据找不到"),
|
||||||
A_SEARCH_QUERY_IS_EMPTY("A00046", "搜索内容不能为空"),
|
A_SEARCH_QUERY_IS_EMPTY("A00046", "搜索内容不能为空"),
|
||||||
A_WF_COMPONENT_NOT_FOUND("A00047", "工作流基础组件找不到"),
|
A_WF_COMPONENT_NOT_FOUND("A00047", "工作流基础组件找不到"),
|
||||||
A_WF_RESUME_FAIL("A00048", "工作流恢复执行时失败"),
|
|
||||||
A_MAIL_SENDER_EMPTY("A00049", "邮件发送人不能为空"),
|
A_MAIL_SENDER_EMPTY("A00049", "邮件发送人不能为空"),
|
||||||
A_MAIL_SENDER_CONFIG_ERROR("A00050", "邮件发送人配置错误"),
|
A_MAIL_SENDER_CONFIG_ERROR("A00050", "邮件发送人配置错误"),
|
||||||
A_MAIL_RECEIVER_EMPTY("A00051", "邮件接收人不能为空"),
|
A_MAIL_RECEIVER_EMPTY("A00051", "邮件接收人不能为空"),
|
||||||
|
|||||||
@@ -22,12 +22,4 @@ public interface IWorkFlowStarterService {
|
|||||||
* @return 流式输出结果
|
* @return 流式输出结果
|
||||||
*/
|
*/
|
||||||
SseEmitter streaming(User user, String workflowUuid, List<ObjectNode> userInputs, Long sessionId);
|
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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package me.zhyd.oauth.request;
|
package me.zhyd.oauth.request;
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSONObject;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import me.zhyd.oauth.cache.AuthStateCache;
|
import me.zhyd.oauth.cache.AuthStateCache;
|
||||||
import me.zhyd.oauth.config.AuthConfig;
|
import me.zhyd.oauth.config.AuthConfig;
|
||||||
import me.zhyd.oauth.config.AuthSource;
|
import me.zhyd.oauth.config.AuthSource;
|
||||||
@@ -37,11 +38,11 @@ public abstract class AbstractAuthWeChatEnterpriseRequest extends AuthDefaultReq
|
|||||||
public AuthToken getAccessToken(AuthCallback authCallback) {
|
public AuthToken getAccessToken(AuthCallback authCallback) {
|
||||||
String response = doGetAuthorizationCode(accessTokenUrl(null));
|
String response = doGetAuthorizationCode(accessTokenUrl(null));
|
||||||
|
|
||||||
JSONObject object = this.checkResponse(response);
|
JsonNode object = this.checkResponse(response);
|
||||||
|
|
||||||
return AuthToken.builder()
|
return AuthToken.builder()
|
||||||
.accessToken(object.getString("access_token"))
|
.accessToken(object.get("access_token").asText())
|
||||||
.expireIn(object.getIntValue("expires_in"))
|
.expireIn(object.get("expires_in").asInt())
|
||||||
.code(authCallback.getCode())
|
.code(authCallback.getCode())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
@@ -49,26 +50,25 @@ public abstract class AbstractAuthWeChatEnterpriseRequest extends AuthDefaultReq
|
|||||||
@Override
|
@Override
|
||||||
public AuthUser getUserInfo(AuthToken authToken) {
|
public AuthUser getUserInfo(AuthToken authToken) {
|
||||||
String response = doGetUserInfo(authToken);
|
String response = doGetUserInfo(authToken);
|
||||||
JSONObject object = this.checkResponse(response);
|
JsonNode object = this.checkResponse(response);
|
||||||
|
|
||||||
// 返回 OpenId 或其他,均代表非当前企业用户,不支持
|
// 返回 OpenId 或其他,均代表非当前企业用户,不支持
|
||||||
// https://github.com/justauth/JustAuth/issues/227 修复bug
|
// https://github.com/justauth/JustAuth/issues/227 修复bug
|
||||||
if (!object.containsKey("userid")) {
|
if (!object.has("userid")) {
|
||||||
throw new AuthException(AuthResponseStatus.UNIDENTIFIED_PLATFORM, source);
|
throw new AuthException(AuthResponseStatus.UNIDENTIFIED_PLATFORM, source);
|
||||||
}
|
}
|
||||||
String userId = object.getString("userid");
|
String userId = object.get("userid").asText();
|
||||||
String userTicket = object.getString("user_ticket");
|
String userTicket = object.has("user_ticket") ? object.get("user_ticket").asText() : null;
|
||||||
JSONObject userDetail = getUserDetail(authToken.getAccessToken(), userId, userTicket);
|
JsonNode userDetail = getUserDetail(authToken.getAccessToken(), userId, userTicket);
|
||||||
|
|
||||||
return AuthUser.builder()
|
return AuthUser.builder()
|
||||||
.rawUserInfo(userDetail)
|
.username(userDetail.has("name") ? userDetail.get("name").asText() : null)
|
||||||
.username(userDetail.getString("name"))
|
.nickname(userDetail.has("alias") ? userDetail.get("alias").asText() : null)
|
||||||
.nickname(userDetail.getString("alias"))
|
.avatar(userDetail.has("avatar") ? userDetail.get("avatar").asText() : null)
|
||||||
.avatar(userDetail.getString("avatar"))
|
.location(userDetail.has("address") ? userDetail.get("address").asText() : null)
|
||||||
.location(userDetail.getString("address"))
|
.email(userDetail.has("email") ? userDetail.get("email").asText() : null)
|
||||||
.email(userDetail.getString("email"))
|
|
||||||
.uuid(userId)
|
.uuid(userId)
|
||||||
.gender(AuthUserGender.getWechatRealGender(userDetail.getString("gender")))
|
.gender(AuthUserGender.getWechatRealGender(userDetail.has("gender") ? userDetail.get("gender").asText() : null))
|
||||||
.token(authToken)
|
.token(authToken)
|
||||||
.source(source.toString())
|
.source(source.toString())
|
||||||
.build();
|
.build();
|
||||||
@@ -78,16 +78,21 @@ public abstract class AbstractAuthWeChatEnterpriseRequest extends AuthDefaultReq
|
|||||||
* 校验请求结果
|
* 校验请求结果
|
||||||
*
|
*
|
||||||
* @param response 请求结果
|
* @param response 请求结果
|
||||||
* @return 如果请求结果正常,则返回JSONObject
|
* @return 如果请求结果正常,则返回JsonNode
|
||||||
*/
|
*/
|
||||||
private JSONObject checkResponse(String response) {
|
private JsonNode checkResponse(String response) {
|
||||||
JSONObject object = JSONObject.parseObject(response);
|
try {
|
||||||
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
JsonNode object = objectMapper.readTree(response);
|
||||||
|
|
||||||
if (object.containsKey("errcode") && object.getIntValue("errcode") != 0) {
|
if (object.has("errcode") && object.get("errcode").asInt() != 0) {
|
||||||
throw new AuthException(object.getString("errmsg"), source);
|
throw new AuthException(object.get("errmsg").asText(), source);
|
||||||
|
}
|
||||||
|
|
||||||
|
return object;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AuthException("解析响应失败: " + e.getMessage(), source);
|
||||||
}
|
}
|
||||||
|
|
||||||
return object;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -127,28 +132,39 @@ public abstract class AbstractAuthWeChatEnterpriseRequest extends AuthDefaultReq
|
|||||||
* @param userTicket 成员票据,用于获取用户信息或敏感信息
|
* @param userTicket 成员票据,用于获取用户信息或敏感信息
|
||||||
* @return 用户详情
|
* @return 用户详情
|
||||||
*/
|
*/
|
||||||
private JSONObject getUserDetail(String accessToken, String userId, String userTicket) {
|
private JsonNode getUserDetail(String accessToken, String userId, String userTicket) {
|
||||||
// 用户基础信息
|
try {
|
||||||
String userInfoUrl = UrlBuilder.fromBaseUrl("https://qyapi.weixin.qq.com/cgi-bin/user/get")
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
.queryParam("access_token", accessToken)
|
|
||||||
.queryParam("userid", userId)
|
|
||||||
.build();
|
|
||||||
String userInfoResponse = new HttpUtils(config.getHttpConfig()).get(userInfoUrl).getBody();
|
|
||||||
JSONObject userInfo = checkResponse(userInfoResponse);
|
|
||||||
|
|
||||||
// 用户敏感信息
|
// 用户基础信息
|
||||||
if (StringUtils.isNotEmpty(userTicket)) {
|
String userInfoUrl = UrlBuilder.fromBaseUrl("https://qyapi.weixin.qq.com/cgi-bin/user/get")
|
||||||
String userDetailUrl = UrlBuilder.fromBaseUrl("https://qyapi.weixin.qq.com/cgi-bin/auth/getuserdetail")
|
|
||||||
.queryParam("access_token", accessToken)
|
.queryParam("access_token", accessToken)
|
||||||
|
.queryParam("userid", userId)
|
||||||
.build();
|
.build();
|
||||||
JSONObject param = new JSONObject();
|
String userInfoResponse = new HttpUtils(config.getHttpConfig()).get(userInfoUrl).getBody();
|
||||||
param.put("user_ticket", userTicket);
|
JsonNode userInfo = checkResponse(userInfoResponse);
|
||||||
String userDetailResponse = new HttpUtils(config.getHttpConfig()).post(userDetailUrl, param.toJSONString()).getBody();
|
|
||||||
JSONObject userDetail = checkResponse(userDetailResponse);
|
|
||||||
|
|
||||||
userInfo.putAll(userDetail);
|
// 用户敏感信息
|
||||||
|
if (StringUtils.isNotEmpty(userTicket)) {
|
||||||
|
String userDetailUrl = UrlBuilder.fromBaseUrl("https://qyapi.weixin.qq.com/cgi-bin/auth/getuserdetail")
|
||||||
|
.queryParam("access_token", accessToken)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 构建请求参数
|
||||||
|
String paramJson = objectMapper.createObjectNode()
|
||||||
|
.put("user_ticket", userTicket)
|
||||||
|
.toString();
|
||||||
|
|
||||||
|
String userDetailResponse = new HttpUtils(config.getHttpConfig()).post(userDetailUrl, paramJson).getBody();
|
||||||
|
JsonNode userDetail = checkResponse(userDetailResponse);
|
||||||
|
|
||||||
|
// 合并两个JsonNode
|
||||||
|
((com.fasterxml.jackson.databind.node.ObjectNode) userInfo).setAll((com.fasterxml.jackson.databind.node.ObjectNode) userDetail);
|
||||||
|
}
|
||||||
|
return userInfo;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AuthException("获取用户详情失败: " + e.getMessage(), source);
|
||||||
}
|
}
|
||||||
return userInfo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package me.zhyd.oauth.request;
|
package me.zhyd.oauth.request;
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSONObject;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.xkcoding.http.support.HttpHeader;
|
import com.xkcoding.http.support.HttpHeader;
|
||||||
import me.zhyd.oauth.cache.AuthStateCache;
|
import me.zhyd.oauth.cache.AuthStateCache;
|
||||||
import me.zhyd.oauth.config.AuthConfig;
|
import me.zhyd.oauth.config.AuthConfig;
|
||||||
@@ -52,44 +53,56 @@ public class AuthDingTalkV2Request extends AuthDefaultRequest {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AuthToken getAccessToken(AuthCallback authCallback) {
|
public AuthToken getAccessToken(AuthCallback authCallback) {
|
||||||
Map<String, String> params = new HashMap<>();
|
try {
|
||||||
params.put("grantType", "authorization_code");
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
params.put("clientId", config.getClientId());
|
Map<String, String> params = new HashMap<>();
|
||||||
params.put("clientSecret", config.getClientSecret());
|
params.put("grantType", "authorization_code");
|
||||||
params.put("code", authCallback.getCode());
|
params.put("clientId", config.getClientId());
|
||||||
String response = new HttpUtils(config.getHttpConfig()).post(this.source.accessToken(), JSONObject.toJSONString(params)).getBody();
|
params.put("clientSecret", config.getClientSecret());
|
||||||
JSONObject accessTokenObject = JSONObject.parseObject(response);
|
params.put("code", authCallback.getCode());
|
||||||
if (!accessTokenObject.containsKey("accessToken")) {
|
|
||||||
throw new AuthException(JSONObject.toJSONString(response), source);
|
String paramsJson = objectMapper.writeValueAsString(params);
|
||||||
|
String response = new HttpUtils(config.getHttpConfig()).post(this.source.accessToken(), paramsJson).getBody();
|
||||||
|
JsonNode accessTokenObject = objectMapper.readTree(response);
|
||||||
|
|
||||||
|
if (!accessTokenObject.has("accessToken")) {
|
||||||
|
throw new AuthException(response, source);
|
||||||
|
}
|
||||||
|
return AuthToken.builder()
|
||||||
|
.accessToken(accessTokenObject.get("accessToken").asText())
|
||||||
|
.refreshToken(accessTokenObject.has("refreshToken") ? accessTokenObject.get("refreshToken").asText() : null)
|
||||||
|
.expireIn(accessTokenObject.has("expireIn") ? accessTokenObject.get("expireIn").asInt() : 0)
|
||||||
|
.corpId(accessTokenObject.has("corpId") ? accessTokenObject.get("corpId").asText() : null)
|
||||||
|
.build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AuthException("获取AccessToken失败: " + e.getMessage(), source);
|
||||||
}
|
}
|
||||||
return AuthToken.builder()
|
|
||||||
.accessToken(accessTokenObject.getString("accessToken"))
|
|
||||||
.refreshToken(accessTokenObject.getString("refreshToken"))
|
|
||||||
.expireIn(accessTokenObject.getIntValue("expireIn"))
|
|
||||||
.corpId(accessTokenObject.getString("corpId"))
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AuthUser getUserInfo(AuthToken authToken) {
|
public AuthUser getUserInfo(AuthToken authToken) {
|
||||||
HttpHeader header = new HttpHeader();
|
try {
|
||||||
header.add("x-acs-dingtalk-access-token", authToken.getAccessToken());
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
HttpHeader header = new HttpHeader();
|
||||||
|
header.add("x-acs-dingtalk-access-token", authToken.getAccessToken());
|
||||||
|
|
||||||
String response = new HttpUtils(config.getHttpConfig()).get(this.source.userInfo(), null, header, false).getBody();
|
String response = new HttpUtils(config.getHttpConfig()).get(this.source.userInfo(), null, header, false).getBody();
|
||||||
JSONObject object = JSONObject.parseObject(response);
|
JsonNode object = objectMapper.readTree(response);
|
||||||
|
|
||||||
authToken.setOpenId(object.getString("openId"));
|
authToken.setOpenId(object.has("openId") ? object.get("openId").asText() : null);
|
||||||
authToken.setUnionId(object.getString("unionId"));
|
authToken.setUnionId(object.has("unionId") ? object.get("unionId").asText() : null);
|
||||||
return AuthUser.builder()
|
return AuthUser.builder()
|
||||||
.rawUserInfo(object)
|
.uuid(object.has("unionId") ? object.get("unionId").asText() : null)
|
||||||
.uuid(object.getString("unionId"))
|
.username(object.has("nick") ? object.get("nick").asText() : null)
|
||||||
.username(object.getString("nick"))
|
.nickname(object.has("nick") ? object.get("nick").asText() : null)
|
||||||
.nickname(object.getString("nick"))
|
.avatar(object.has("avatarUrl") ? object.get("avatarUrl").asText() : null)
|
||||||
.avatar(object.getString("avatarUrl"))
|
.snapshotUser(object.has("visitor") && object.get("visitor").asBoolean())
|
||||||
.snapshotUser(object.getBooleanValue("visitor"))
|
.token(authToken)
|
||||||
.token(authToken)
|
.source(source.toString())
|
||||||
.source(source.toString())
|
.build();
|
||||||
.build();
|
} catch (Exception e) {
|
||||||
|
throw new AuthException("获取用户信息失败: " + e.getMessage(), source);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -24,6 +24,13 @@
|
|||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|
||||||
|
<!-- 智谱官方 Java SDK:流程编排 Web Search 扩展节点 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>ai.z.openapi</groupId>
|
||||||
|
<artifactId>zai-sdk</artifactId>
|
||||||
|
<version>${zai-sdk.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.ruoyi</groupId>
|
<groupId>org.ruoyi</groupId>
|
||||||
<artifactId>ruoyi-common-chat</artifactId>
|
<artifactId>ruoyi-common-chat</artifactId>
|
||||||
@@ -45,6 +52,11 @@
|
|||||||
<artifactId>ruoyi-common-satoken</artifactId>
|
<artifactId>ruoyi-common-satoken</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ruoyi</groupId>
|
||||||
|
<artifactId>ruoyi-common-tenant</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.ruoyi</groupId>
|
<groupId>org.ruoyi</groupId>
|
||||||
<artifactId>ruoyi-common-mail</artifactId>
|
<artifactId>ruoyi-common-mail</artifactId>
|
||||||
@@ -114,6 +126,7 @@
|
|||||||
<version>${langchain4j.version}</version>
|
<version>${langchain4j.version}</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jsoup</groupId>
|
<groupId>org.jsoup</groupId>
|
||||||
<artifactId>jsoup</artifactId>
|
<artifactId>jsoup</artifactId>
|
||||||
@@ -133,8 +146,6 @@
|
|||||||
<version>${langgraph4j.version}</version>
|
<version>${langgraph4j.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
<artifactId>commons-collections4</artifactId>
|
<artifactId>commons-collections4</artifactId>
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
package org.ruoyi.workflow.controller;
|
package org.ruoyi.workflow.controller;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import jakarta.validation.constraints.Min;
|
import jakarta.validation.constraints.Min;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import org.ruoyi.common.core.domain.R;
|
import org.ruoyi.common.core.domain.R;
|
||||||
import org.ruoyi.workflow.dto.workflow.WfRuntimeNodeDto;
|
import org.ruoyi.workflow.dto.workflow.WfRuntimeNodeDto;
|
||||||
import org.ruoyi.workflow.dto.workflow.WfRuntimeResp;
|
import org.ruoyi.workflow.dto.workflow.WfRuntimeResp;
|
||||||
import org.ruoyi.workflow.dto.workflow.WorkflowResumeReq;
|
|
||||||
import org.ruoyi.workflow.service.WorkflowRuntimeService;
|
import org.ruoyi.workflow.service.WorkflowRuntimeService;
|
||||||
import org.ruoyi.workflow.workflow.WorkflowStarter;
|
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@@ -24,16 +21,6 @@ public class WorkflowRuntimeController {
|
|||||||
@Resource
|
@Resource
|
||||||
private WorkflowRuntimeService workflowRuntimeService;
|
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")
|
@GetMapping("/page")
|
||||||
public R<Page<WfRuntimeResp>> search(@RequestParam String wfUuid,
|
public R<Page<WfRuntimeResp>> search(@RequestParam String wfUuid,
|
||||||
@NotNull @Min(1) Integer currentPage,
|
@NotNull @Min(1) Integer currentPage,
|
||||||
|
|||||||
@@ -337,7 +337,6 @@ public class AdiConstant {
|
|||||||
public static final String DEFAULT_INPUT_PARAM_NAME = "input";
|
public static final String DEFAULT_INPUT_PARAM_NAME = "input";
|
||||||
public static final String DEFAULT_OUTPUT_PARAM_NAME = "output";
|
public static final String DEFAULT_OUTPUT_PARAM_NAME = "output";
|
||||||
public static final String DEFAULT_ERROR_OUTPUT_PARAM_NAME = "error_msg";
|
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_READY = 1;
|
||||||
public static final int NODE_PROCESS_STATUS_DOING = 2;
|
public static final int NODE_PROCESS_STATUS_DOING = 2;
|
||||||
public static final int NODE_PROCESS_STATUS_SUCCESS = 3;
|
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_DOING = 2;
|
||||||
public static final int WORKFLOW_PROCESS_STATUS_SUCCESS = 3;
|
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_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_NORMAL = 1;
|
||||||
public static final int WORKFLOW_NODE_PROCESS_TYPE_CONDITIONAL = 2;
|
public static final int WORKFLOW_NODE_PROCESS_TYPE_CONDITIONAL = 2;
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
package org.ruoyi.workflow.dto.workflow;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class WorkflowResumeReq {
|
|
||||||
private String feedbackContent;
|
|
||||||
private SseEmitter sseEmitter;
|
|
||||||
}
|
|
||||||
@@ -55,8 +55,13 @@ public class SSEEmitterHelper {
|
|||||||
} else {
|
} else {
|
||||||
sseEmitter.send(msg);
|
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) {
|
} catch (IOException ioException) {
|
||||||
log.error("stream onNext error", ioException);
|
log.error("stream onNext error", ioException);
|
||||||
|
COMPLETED_SSE.put(sseEmitter, Boolean.TRUE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
|||||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||||
import org.ruoyi.common.chat.enums.RoleType;
|
import org.ruoyi.common.chat.enums.RoleType;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ruoyi.common.core.exception.ServiceException;
|
|
||||||
import org.ruoyi.common.core.service.ConfigService;
|
import org.ruoyi.common.core.service.ConfigService;
|
||||||
import org.ruoyi.common.core.utils.SpringUtils;
|
import org.ruoyi.common.core.utils.SpringUtils;
|
||||||
import org.ruoyi.common.core.utils.StringUtils;
|
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.helper.SSEEmitterHelper;
|
||||||
import org.ruoyi.workflow.workflow.WfState;
|
import org.ruoyi.workflow.workflow.WfState;
|
||||||
import org.ruoyi.workflow.workflow.WorkflowUtil;
|
import org.ruoyi.workflow.workflow.WorkflowUtil;
|
||||||
|
import org.ruoyi.workflow.workflow.node.enmus.NodeMessageTemplateEnum;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,17 +38,20 @@ public class WorkflowMessageUtil {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取节点的响应模板
|
* 获取节点的响应模板 <br/>
|
||||||
|
* 优先读取 sys_config 配置(可在系统管理-配置管理中自定义),
|
||||||
|
* 未配置时回退到枚举内置默认模板, 模板仅为展示文案, 缺失不应中断工作流执行
|
||||||
* @param configKey 参数Key
|
* @param configKey 参数Key
|
||||||
* @return 返回模板样式
|
* @return 返回模板样式
|
||||||
*/
|
*/
|
||||||
public static String getNodeMessageTemplate(String configKey){
|
public static String getNodeMessageTemplate(String configKey){
|
||||||
ConfigService configService = SpringUtil.getBean(ConfigService.class);
|
ConfigService configService = SpringUtil.getBean(ConfigService.class);
|
||||||
String configValue = configService.getConfigValue(configKey);
|
String configValue = configService.getConfigValue(configKey);
|
||||||
if (StringUtils.isEmpty(configValue)) {
|
if (StringUtils.isNotEmpty(configValue)) {
|
||||||
throw new ServiceException("请先配置该节点的响应模板");
|
return configValue;
|
||||||
}
|
}
|
||||||
return configValue;
|
log.warn("sys_config 未配置节点响应模板 [{}], 已回退使用内置默认模板", configKey);
|
||||||
|
return NodeMessageTemplateEnum.getDefaultTemplate(configKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
package org.ruoyi.workflow.workflow;
|
|
||||||
|
|
||||||
import org.apache.commons.collections4.map.PassiveExpiringMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 已中断正在等待用户输入的流程 <br/>
|
|
||||||
* TODO 需要考虑项目多节点部署的情况
|
|
||||||
*/
|
|
||||||
public class InterruptedFlow {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 10分钟超时
|
|
||||||
*/
|
|
||||||
private static final PassiveExpiringMap.ExpirationPolicy<String, WorkflowEngine> ep = new PassiveExpiringMap.ConstantTimeToLiveExpirationPolicy<>(60 * 1000 * 10);
|
|
||||||
public static PassiveExpiringMap<String, WorkflowEngine> RUNTIME_TO_GRAPH = new PassiveExpiringMap<>(ep);
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -16,24 +16,14 @@ public enum WfComponentNameEnum {
|
|||||||
|
|
||||||
TONGYI_WANX("Tongyiwanx"),
|
TONGYI_WANX("Tongyiwanx"),
|
||||||
|
|
||||||
DOCUMENT_EXTRACTOR("DocumentExtractor"),
|
|
||||||
|
|
||||||
KEYWORD_EXTRACTOR("KeywordExtractor"),
|
|
||||||
|
|
||||||
FAQ_EXTRACTOR("FaqExtractor"),
|
FAQ_EXTRACTOR("FaqExtractor"),
|
||||||
|
|
||||||
KNOWLEDGE_RETRIEVER("KnowledgeRetrieval"),
|
KNOWLEDGE_RETRIEVER("KnowledgeRetrieval"),
|
||||||
|
|
||||||
SWITCHER("Switcher"),
|
SWITCHER("Switcher"),
|
||||||
|
|
||||||
CLASSIFIER("Classifier"),
|
|
||||||
|
|
||||||
TEMPLATE("Template"),
|
|
||||||
|
|
||||||
GOOGLE_SEARCH("Google"),
|
GOOGLE_SEARCH("Google"),
|
||||||
|
|
||||||
HUMAN_FEEDBACK("HumanFeedback"),
|
|
||||||
|
|
||||||
MAIL_SEND("MailSend"),
|
MAIL_SEND("MailSend"),
|
||||||
|
|
||||||
HTTP_REQUEST("HttpRequest");
|
HTTP_REQUEST("HttpRequest");
|
||||||
|
|||||||
@@ -4,15 +4,14 @@ import org.ruoyi.workflow.entity.WorkflowComponent;
|
|||||||
import org.ruoyi.workflow.entity.WorkflowNode;
|
import org.ruoyi.workflow.entity.WorkflowNode;
|
||||||
import org.ruoyi.workflow.workflow.node.AbstractWfNode;
|
import org.ruoyi.workflow.workflow.node.AbstractWfNode;
|
||||||
import org.ruoyi.workflow.workflow.node.EndNode;
|
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.answer.LLMAnswerNode;
|
||||||
import org.ruoyi.workflow.workflow.node.httpRequest.HttpRequestNode;
|
import org.ruoyi.workflow.workflow.node.httpRequest.HttpRequestNode;
|
||||||
import org.ruoyi.workflow.workflow.node.image.ImageNode;
|
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.knowledgeRetrieval.KnowledgeRetrievalNode;
|
||||||
import org.ruoyi.workflow.workflow.node.mailSend.MailSendNode;
|
import org.ruoyi.workflow.workflow.node.mailSend.MailSendNode;
|
||||||
import org.ruoyi.workflow.workflow.node.start.StartNode;
|
import org.ruoyi.workflow.workflow.node.start.StartNode;
|
||||||
import org.ruoyi.workflow.workflow.node.switcher.SwitcherNode;
|
import org.ruoyi.workflow.workflow.node.switcher.SwitcherNode;
|
||||||
|
import org.ruoyi.workflow.workflow.node.googleSearch.GoogleSearchNode;
|
||||||
|
|
||||||
public class WfNodeFactory {
|
public class WfNodeFactory {
|
||||||
public static AbstractWfNode create(WorkflowComponent wfComponent, WorkflowNode nodeDefinition,
|
public static AbstractWfNode create(WorkflowComponent wfComponent, WorkflowNode nodeDefinition,
|
||||||
@@ -21,14 +20,13 @@ public class WfNodeFactory {
|
|||||||
switch (WfComponentNameEnum.getByName(wfComponent.getName())) {
|
switch (WfComponentNameEnum.getByName(wfComponent.getName())) {
|
||||||
case START -> wfNode = new StartNode(wfComponent, nodeDefinition, wfState, nodeState);
|
case START -> wfNode = new StartNode(wfComponent, nodeDefinition, wfState, nodeState);
|
||||||
case LLM_ANSWER -> wfNode = new LLMAnswerNode(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 TONGYI_WANX -> wfNode = new ImageNode(wfComponent, nodeDefinition, wfState, nodeState);
|
||||||
case KNOWLEDGE_RETRIEVER -> wfNode = new KnowledgeRetrievalNode(wfComponent, nodeDefinition, wfState, nodeState);
|
case KNOWLEDGE_RETRIEVER -> wfNode = new KnowledgeRetrievalNode(wfComponent, nodeDefinition, wfState, nodeState);
|
||||||
case END -> wfNode = new EndNode(wfComponent, nodeDefinition, wfState, nodeState);
|
case END -> wfNode = new EndNode(wfComponent, nodeDefinition, wfState, nodeState);
|
||||||
case MAIL_SEND -> wfNode = new MailSendNode(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 HTTP_REQUEST -> wfNode = new HttpRequestNode(wfComponent, nodeDefinition, wfState, nodeState);
|
||||||
case SWITCHER -> wfNode = new SwitcherNode(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 -> {
|
default -> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,11 +55,6 @@ public class WfState {
|
|||||||
private List<NodeIOData> output = new ArrayList<>();
|
private List<NodeIOData> output = new ArrayList<>();
|
||||||
private Integer processStatus = WORKFLOW_PROCESS_STATUS_READY;
|
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) {
|
public WfState(User user, List<NodeIOData> input, String uuid, Long userId, String tokenValue, SseEmitter sseEmitter, Long sessionId) {
|
||||||
this.input = input;
|
this.input = input;
|
||||||
this.user = user;
|
this.user = user;
|
||||||
@@ -133,8 +128,4 @@ public class WfState {
|
|||||||
.findFirst()
|
.findFirst()
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addInterruptNode(String nodeUuid) {
|
|
||||||
this.interruptNodes.add(nodeUuid);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,82 +108,39 @@ public class WorkflowEngine {
|
|||||||
|
|
||||||
MemorySaver saver = new MemorySaver();
|
MemorySaver saver = new MemorySaver();
|
||||||
CompileConfig compileConfig = CompileConfig.builder().checkpointSaver(saver)
|
CompileConfig compileConfig = CompileConfig.builder().checkpointSaver(saver)
|
||||||
.interruptBefore(wfState.getInterruptNodes().toArray(String[]::new))
|
|
||||||
.build();
|
.build();
|
||||||
app = mainStateGraph.compile(compileConfig);
|
app = mainStateGraph.compile(compileConfig);
|
||||||
RunnableConfig invokeConfig = RunnableConfig.builder().build();
|
RunnableConfig invokeConfig = RunnableConfig.builder().build();
|
||||||
exe(invokeConfig, false);
|
exe(invokeConfig);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
errorWhenExe(e);
|
errorWhenExe(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void exe(RunnableConfig invokeConfig, boolean resume) {
|
private void exe(RunnableConfig invokeConfig) {
|
||||||
//不使用langgraph4j state的update相关方法,无需传入input
|
//不使用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);
|
streamingResult(wfState, outputs, sseEmitter);
|
||||||
|
|
||||||
StateSnapshot<WfNodeState> stateSnapshot = app.getState(invokeConfig);
|
StateSnapshot<WfNodeState> stateSnapshot = app.getState(invokeConfig);
|
||||||
String nextNode = stateSnapshot.config().nextNode().orElse("");
|
wfState.setProcessStatus(WORKFLOW_PROCESS_STATUS_SUCCESS);
|
||||||
//还有下个节点,表示进入中断状态,等待用户输入后继续执<E7BBAD>?
|
WorkflowRuntime updatedRuntime = workflowRuntimeService.updateOutput(wfRuntimeResp.getId(), wfState);
|
||||||
if (StringUtils.isNotBlank(nextNode) && !nextNode.equalsIgnoreCase(END)) {
|
wfNodes.stream().filter(item -> stateSnapshot.node().equals(item.getUuid()))
|
||||||
// 获取提示模板
|
.findFirst().ifPresent(wfNode -> {
|
||||||
String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.HUMAN_FEED_BACK.getValue());
|
String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.END.getValue());
|
||||||
// 获取人机交互提示信息
|
WorkflowMessageUtil.notifyAndStoreMessage(wfState, sseEmitter, wfNode, nodeMessageTemplate);
|
||||||
String intTip = nodeMessageTemplate + WorkflowUtil.getHumanFeedbackTip(nextNode, wfNodes);
|
});
|
||||||
//将等待输入信息[事件与提示词]发送到到客户端
|
sseEmitterHelper.sendComplete(user.getId(), sseEmitter, updatedRuntime.getOutput());
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void errorWhenExe(Exception e) {
|
private void errorWhenExe(Exception e) {
|
||||||
log.error("error", e);
|
log.error("error", e);
|
||||||
String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.EXCEPTION.getValue());
|
String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.EXCEPTION.getValue());
|
||||||
String errorMsg = e.getMessage();
|
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 = "并行节点中不能包含条件分<EFBFBD>?";
|
||||||
}
|
}
|
||||||
errorMsg = nodeMessageTemplate + errorMsg;
|
errorMsg = nodeMessageTemplate + (errorMsg != null ? errorMsg : e.getClass().getSimpleName());
|
||||||
// 保存会话信息且发送驱动消息事件
|
// 保存会话信息且发送驱动消息事件
|
||||||
WorkflowMessageUtil.saveWorkflowMessage(wfState, errorMsg);
|
WorkflowMessageUtil.saveWorkflowMessage(wfState, errorMsg);
|
||||||
sseEmitterHelper.sendErrorAndComplete(user.getId(), sseEmitter, errorMsg);
|
sseEmitterHelper.sendErrorAndComplete(user.getId(), sseEmitter, errorMsg);
|
||||||
@@ -267,6 +224,10 @@ public class WorkflowEngine {
|
|||||||
log.info("node:{},chunk:{}", node, chunk);
|
log.info("node:{},chunk:{}", node, chunk);
|
||||||
SSEEmitterHelper.parseAndSendPartialMsg(sseEmitter, "[NODE_CHUNK_" + node + "]", chunk);
|
SSEEmitterHelper.parseAndSendPartialMsg(sseEmitter, "[NODE_CHUNK_" + node + "]", chunk);
|
||||||
} else {
|
} else {
|
||||||
|
// __END__ 是 langgraph4j 的终止伪节点, 无对应业务节点状态, 跳过
|
||||||
|
if (END.equals(out.node())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
AbstractWfNode abstractWfNode = wfState.getCompletedNodes().stream()
|
AbstractWfNode abstractWfNode = wfState.getCompletedNodes().stream()
|
||||||
.filter(item -> item.getNode().getUuid().endsWith(out.node())).findFirst().orElse(null);
|
.filter(item -> item.getNode().getUuid().endsWith(out.node())).findFirst().orElse(null);
|
||||||
if (null != abstractWfNode) {
|
if (null != abstractWfNode) {
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import static org.bsc.langgraph4j.StateGraph.END;
|
|||||||
import static org.bsc.langgraph4j.StateGraph.START;
|
import static org.bsc.langgraph4j.StateGraph.START;
|
||||||
import static org.bsc.langgraph4j.action.AsyncEdgeAction.edge_async;
|
import static org.bsc.langgraph4j.action.AsyncEdgeAction.edge_async;
|
||||||
import static org.bsc.langgraph4j.action.AsyncNodeAction.node_async;
|
import static org.bsc.langgraph4j.action.AsyncNodeAction.node_async;
|
||||||
import static org.ruoyi.workflow.workflow.WfComponentNameEnum.HUMAN_FEEDBACK;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 负责构建工作流运行所依赖的状态图<E68081>?
|
* 负责构建工作流运行所依赖的状态图<E68081>?
|
||||||
@@ -27,7 +26,6 @@ import static org.ruoyi.workflow.workflow.WfComponentNameEnum.HUMAN_FEEDBACK;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class WorkflowGraphBuilder {
|
public class WorkflowGraphBuilder {
|
||||||
|
|
||||||
private final Map<Long, WorkflowComponent> componentIndex;
|
|
||||||
private final Map<String, WorkflowNode> nodeIndex;
|
private final Map<String, WorkflowNode> nodeIndex;
|
||||||
private final Map<String, List<WorkflowEdge>> edgesBySource;
|
private final Map<String, List<WorkflowEdge>> edgesBySource;
|
||||||
private final Map<String, List<WorkflowEdge>> edgesByTarget;
|
private final Map<String, List<WorkflowEdge>> edgesByTarget;
|
||||||
@@ -46,8 +44,6 @@ public class WorkflowGraphBuilder {
|
|||||||
List<WorkflowEdge> edges,
|
List<WorkflowEdge> edges,
|
||||||
WorkflowNodeRunner nodeRunner,
|
WorkflowNodeRunner nodeRunner,
|
||||||
WfState wfState) {
|
WfState wfState) {
|
||||||
this.componentIndex = components.stream()
|
|
||||||
.collect(Collectors.toMap(WorkflowComponent::getId, Function.identity(), (origin, ignore) -> origin));
|
|
||||||
this.nodeIndex = nodes.stream()
|
this.nodeIndex = nodes.stream()
|
||||||
.collect(Collectors.toMap(WorkflowNode::getUuid, Function.identity(), (origin, ignore) -> origin));
|
.collect(Collectors.toMap(WorkflowNode::getUuid, Function.identity(), (origin, ignore) -> origin));
|
||||||
this.edgesBySource = edges.stream().collect(Collectors.groupingBy(WorkflowEdge::getSourceNodeUuid));
|
this.edgesBySource = edges.stream().collect(Collectors.groupingBy(WorkflowEdge::getSourceNodeUuid));
|
||||||
@@ -217,14 +213,6 @@ public class WorkflowGraphBuilder {
|
|||||||
WorkflowNode wfNode = getNodeByUuid(stateGraphNodeUuid);
|
WorkflowNode wfNode = getNodeByUuid(stateGraphNodeUuid);
|
||||||
stateGraph.addNode(stateGraphNodeUuid, node_async(state -> nodeRunner.run(wfNode, state)));
|
stateGraph.addNode(stateGraphNodeUuid, node_async(state -> nodeRunner.run(wfNode, state)));
|
||||||
stateGraphList.add(stateGraph);
|
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 {
|
private void addEdgeToStateGraph(StateGraph<WfNodeState> stateGraph, String source, String target) throws GraphStateException {
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import jakarta.annotation.Resource;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ruoyi.common.chat.entity.User;
|
import org.ruoyi.common.chat.entity.User;
|
||||||
import org.ruoyi.common.chat.service.workFlow.IWorkFlowStarterService;
|
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.satoken.utils.LoginHelper;
|
||||||
import org.ruoyi.common.sse.core.SseEmitterManager;
|
import org.ruoyi.common.sse.core.SseEmitterManager;
|
||||||
|
import org.ruoyi.common.tenant.helper.TenantHelper;
|
||||||
import org.ruoyi.workflow.entity.*;
|
import org.ruoyi.workflow.entity.*;
|
||||||
import org.ruoyi.workflow.helper.SSEEmitterHelper;
|
import org.ruoyi.workflow.helper.SSEEmitterHelper;
|
||||||
import org.ruoyi.workflow.service.*;
|
import org.ruoyi.workflow.service.*;
|
||||||
@@ -58,6 +58,8 @@ public class WorkflowStarter implements IWorkFlowStarterService {
|
|||||||
Long userId = LoginHelper.getUserId();
|
Long userId = LoginHelper.getUserId();
|
||||||
// 获取登录Token(仅透传给 WfState,工作流 SSE 通过 emitter 直发,不串台)
|
// 获取登录Token(仅透传给 WfState,工作流 SSE 通过 emitter 直发,不串台)
|
||||||
String tokenValue = StpUtil.getTokenValue();
|
String tokenValue = StpUtil.getTokenValue();
|
||||||
|
// 获取当前租户ID(@Async 线程不继承请求线程的租户上下文,需显式透传)
|
||||||
|
String tenantId = TenantHelper.getTenantId();
|
||||||
// 根据会话ID连接SSE对象(每会话一个连接,避免同用户多会话串台)
|
// 根据会话ID连接SSE对象(每会话一个连接,避免同用户多会话串台)
|
||||||
SseEmitter sseEmitter = sseEmitterManager.connect(String.valueOf(sessionId));
|
SseEmitter sseEmitter = sseEmitterManager.connect(String.valueOf(sessionId));
|
||||||
if (!sseEmitterHelper.checkOrComplete(user, sseEmitter)) {
|
if (!sseEmitterHelper.checkOrComplete(user, sseEmitter)) {
|
||||||
@@ -71,41 +73,33 @@ public class WorkflowStarter implements IWorkFlowStarterService {
|
|||||||
sseEmitterHelper.sendErrorAndComplete(user.getId(), sseEmitter, A_WF_DISABLED.getInfo());
|
sseEmitterHelper.sendErrorAndComplete(user.getId(), sseEmitter, A_WF_DISABLED.getInfo());
|
||||||
return sseEmitter;
|
return sseEmitter;
|
||||||
}
|
}
|
||||||
self.asyncRun(user, workflow, userInputs, sseEmitter, userId, tokenValue, sessionId);
|
self.asyncRun(user, workflow, userInputs, sseEmitter, userId, tokenValue, sessionId, tenantId);
|
||||||
return sseEmitter;
|
return sseEmitter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Async
|
@Async
|
||||||
public void asyncRun(User user, Workflow workflow, List<ObjectNode> userInputs, SseEmitter sseEmitter, Long userId, String tokenValue, Long sessionId) {
|
public void asyncRun(User user, Workflow workflow, List<ObjectNode> userInputs, SseEmitter sseEmitter, Long userId, String tokenValue, Long sessionId, String tenantId) {
|
||||||
log.info("WorkflowEngine run,userId:{},workflowUuid:{},userInputs:{}", user.getId(), workflow.getUuid(), userInputs);
|
// @Async 线程不继承请求线程的租户上下文, 显式设置, 避免租户缓存/隔离逻辑异常
|
||||||
List<WorkflowComponent> components = workflowComponentService.getAllEnable();
|
if (tenantId != null) {
|
||||||
List<WorkflowNode> nodes = workflowNodeService.lambdaQuery()
|
TenantHelper.setDynamic(tenantId);
|
||||||
.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());
|
|
||||||
}
|
}
|
||||||
// 如果SSE连接对象不为空传入该对象(Chat调用工作流对话使用)
|
try {
|
||||||
if (null != sseEmitter){
|
log.info("WorkflowEngine run,userId:{},workflowUuid:{},userInputs:{}", user.getId(), workflow.getUuid(), userInputs);
|
||||||
workflowEngine.setSseEmitter(sseEmitter);
|
List<WorkflowComponent> components = workflowComponentService.getAllEnable();
|
||||||
// 为了让每个节点都可以发送模板消息 保持SSE对象一致(以防出现向已关闭的SSE对象发送消息)
|
List<WorkflowNode> nodes = workflowNodeService.lambdaQuery()
|
||||||
workflowEngine.getWfState().setSseEmitter(sseEmitter);
|
.eq(WorkflowNode::getWorkflowId, workflow.getId())
|
||||||
|
.eq(WorkflowNode::getIsDeleted, false)
|
||||||
|
.list();
|
||||||
|
List<WorkflowEdge> edges = workflowEdgeService.lambdaQuery()
|
||||||
|
.eq(WorkflowEdge::getWorkflowId, workflow.getId())
|
||||||
|
.eq(WorkflowEdge::getIsDeleted, false)
|
||||||
|
.list();
|
||||||
|
WorkflowEngine workflowEngine = new WorkflowEngine(workflow,
|
||||||
|
sseEmitterHelper, components, nodes, edges,
|
||||||
|
workflowRuntimeService, workflowRuntimeNodeService);
|
||||||
|
workflowEngine.run(user, userInputs, sseEmitter, userId, tokenValue, sessionId);
|
||||||
|
} finally {
|
||||||
|
TenantHelper.clearDynamic();
|
||||||
}
|
}
|
||||||
workflowEngine.resume(userInput);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package org.ruoyi.workflow.workflow;
|
|||||||
|
|
||||||
import cn.hutool.core.collection.CollStreamUtil;
|
import cn.hutool.core.collection.CollStreamUtil;
|
||||||
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.collection.CollUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import dev.langchain4j.data.message.ChatMessage;
|
import dev.langchain4j.data.message.ChatMessage;
|
||||||
import dev.langchain4j.data.message.UserMessage;
|
import dev.langchain4j.data.message.UserMessage;
|
||||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
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.base.NodeInputConfigTypeHandler;
|
||||||
import org.ruoyi.workflow.entity.WorkflowNode;
|
import org.ruoyi.workflow.entity.WorkflowNode;
|
||||||
import org.ruoyi.workflow.enums.WfIODataTypeEnum;
|
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.NodeIOData;
|
||||||
import org.ruoyi.workflow.workflow.data.NodeIODataContent;
|
import org.ruoyi.workflow.workflow.data.NodeIODataContent;
|
||||||
import org.ruoyi.workflow.workflow.def.WfNodeParamRef;
|
import org.ruoyi.workflow.workflow.def.WfNodeParamRef;
|
||||||
@@ -88,22 +86,6 @@ public class WorkflowUtil{
|
|||||||
return result;
|
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,
|
public void streamingInvokeLLM(WfState wfState, WfNodeState state, WorkflowNode node, String modelName,
|
||||||
String prompt, String nodeMessageTemplate) {
|
String prompt, String nodeMessageTemplate) {
|
||||||
log.info("stream invoke, modelName: {}", modelName);
|
log.info("stream invoke, modelName: {}", modelName);
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
package org.ruoyi.workflow.workflow.node.classifier;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class ClassifierNodeConfig {
|
|
||||||
private List categories = new ArrayList<>();
|
|
||||||
@JsonProperty("model_platform")
|
|
||||||
private String modelPlatform;
|
|
||||||
@JsonProperty("model_name")
|
|
||||||
private String modelName;
|
|
||||||
}
|
|
||||||
@@ -3,23 +3,44 @@ package org.ruoyi.workflow.workflow.node.enmus;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 节点消息模板ConfigKey枚举
|
* 节点消息模板ConfigKey枚举 <br/>
|
||||||
|
* 模板优先从 sys_config 读取(可在系统管理-配置管理中自定义), 未配置时回退到 defaultTemplate 内置默认值
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
public enum NodeMessageTemplateEnum {
|
public enum NodeMessageTemplateEnum {
|
||||||
HTTP_REQUEST("node.httpRequest.template"),
|
HTTP_REQUEST("node.httpRequest.template", "✅ HTTP请求节点:结束响应 - "),
|
||||||
MAIL_SEND("node.mailsend.template"),
|
MAIL_SEND("node.mailsend.template", "📧 发送邮箱节点:结束响应 - "),
|
||||||
IMAGE("node.image.template"),
|
IMAGE("node.image.template", "🎨 文生图节点:结束响应 - 图片URL: "),
|
||||||
HUMAN_FEED_BACK("node.humanFeedback.template"),
|
SWITCH("node.switch.template", "🔀 条件分支节点:触发 -> 跳转到节点 "),
|
||||||
SWITCH("node.switch.template"),
|
LLM_RESPONSE("node.llmAnswer.template", "🤖 LLM 节点 生成回答:"),
|
||||||
LLM_RESPONSE("node.llmAnswer.template"),
|
GOOGLE_SEARCH("node.googleSearch.template", "🔍 网络搜索节点处理完成:"),
|
||||||
KEYWORD_EXTRACTOR("node.keywordExtractor.template"),
|
EXCEPTION("node.exception.template", "🛑 工作流发生异常:"),
|
||||||
EXCEPTION("node.exception.template"),
|
END("node.end.template", "🔚 流程已执行完毕,如果您有其他需求,请随时重新发起请求。");
|
||||||
END("node.end.template");
|
|
||||||
|
|
||||||
private final String value;
|
private final String value;
|
||||||
|
|
||||||
NodeMessageTemplateEnum(String value) {
|
/**
|
||||||
|
* 内置默认模板, sys_config 未配置对应键时使用
|
||||||
|
*/
|
||||||
|
private final String defaultTemplate;
|
||||||
|
|
||||||
|
NodeMessageTemplateEnum(String value, String defaultTemplate) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
|
this.defaultTemplate = defaultTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 configKey 获取内置默认模板, 未知键返回空串
|
||||||
|
*
|
||||||
|
* @param configKey sys_config 配置键
|
||||||
|
* @return 内置默认模板
|
||||||
|
*/
|
||||||
|
public static String getDefaultTemplate(String configKey) {
|
||||||
|
for (NodeMessageTemplateEnum item : values()) {
|
||||||
|
if (item.value.equals(configKey)) {
|
||||||
|
return item.defaultTemplate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package org.ruoyi.workflow.workflow.node.googleSearch;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.ruoyi.workflow.entity.WorkflowComponent;
|
||||||
|
import org.ruoyi.workflow.entity.WorkflowNode;
|
||||||
|
import org.ruoyi.workflow.util.JsonUtil;
|
||||||
|
import org.ruoyi.workflow.util.SpringUtil;
|
||||||
|
import org.ruoyi.workflow.workflow.NodeProcessResult;
|
||||||
|
import org.ruoyi.workflow.workflow.WfNodeState;
|
||||||
|
import org.ruoyi.workflow.workflow.WfState;
|
||||||
|
import org.ruoyi.workflow.workflow.WorkflowUtil;
|
||||||
|
import org.ruoyi.workflow.workflow.data.NodeIOData;
|
||||||
|
import org.ruoyi.workflow.workflow.node.AbstractWfNode;
|
||||||
|
import org.ruoyi.workflow.workflow.node.enmus.NodeMessageTemplateEnum;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.DEFAULT_OUTPUT_PARAM_NAME;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 【扩展节点】网络搜索
|
||||||
|
* 通过智谱 Web Search API 返回适合大模型消费的结构化网页结果。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class GoogleSearchNode extends AbstractWfNode {
|
||||||
|
|
||||||
|
public GoogleSearchNode(WorkflowComponent wfComponent, WorkflowNode nodeDef, WfState wfState, WfNodeState nodeState) {
|
||||||
|
super(wfComponent, nodeDef, wfState, nodeState);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理搜索请求
|
||||||
|
* nodeConfig 格式:
|
||||||
|
* {
|
||||||
|
* "query": "搜索关键词",
|
||||||
|
* "search_engine": "search_std",
|
||||||
|
* "result_count": 10,
|
||||||
|
* "search_domain_filter": "",
|
||||||
|
* "search_recency_filter": "noLimit",
|
||||||
|
* "content_size": "medium",
|
||||||
|
* "include_image": false
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @return 搜索结果
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public NodeProcessResult onProcess() {
|
||||||
|
GoogleSearchNodeConfig config = checkAndGetConfig(GoogleSearchNodeConfig.class);
|
||||||
|
|
||||||
|
// 获取搜索关键词
|
||||||
|
String searchQuery = WorkflowUtil.renderTemplate(config.getQuery(), state.getInputs());
|
||||||
|
if (StringUtils.isBlank(searchQuery)) {
|
||||||
|
searchQuery = getFirstInputText();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(searchQuery)) {
|
||||||
|
throw new IllegalArgumentException("未提供搜索关键词");
|
||||||
|
}
|
||||||
|
searchQuery = searchQuery.trim();
|
||||||
|
if (searchQuery.length() > 70) {
|
||||||
|
throw new IllegalArgumentException("搜索关键词不能超过 70 个字符");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Web search node processing, engine: {}, result_count: {}",
|
||||||
|
config.getSearchEngine(), config.getResultCount());
|
||||||
|
|
||||||
|
String nodeMessageTemplate = getNodeMessageTemplate(NodeMessageTemplateEnum.GOOGLE_SEARCH.getValue());
|
||||||
|
notifyAndStoreMessage(wfState, nodeMessageTemplate);
|
||||||
|
|
||||||
|
ZhipuWebSearchClient searchClient = SpringUtil.getBean(ZhipuWebSearchClient.class);
|
||||||
|
ZhipuWebSearchClient.SearchResponse response = searchClient.search(
|
||||||
|
searchQuery,
|
||||||
|
config,
|
||||||
|
UUID.randomUUID().toString()
|
||||||
|
);
|
||||||
|
String searchResult = JsonUtil.toJson(response);
|
||||||
|
if (searchResult == null) {
|
||||||
|
throw new IllegalStateException("搜索结果序列化失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Web search completed, result count: {}", response.count());
|
||||||
|
notifyAndStoreMessage(wfState, nodeMessageTemplate + "返回 " + response.count() + " 条结果");
|
||||||
|
|
||||||
|
List<NodeIOData> outputs = List.of(
|
||||||
|
NodeIOData.createByText(DEFAULT_OUTPUT_PARAM_NAME, "智谱网络搜索结果", searchResult)
|
||||||
|
);
|
||||||
|
return NodeProcessResult.builder().content(outputs).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package org.ruoyi.workflow.workflow.node.googleSearch;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import jakarta.validation.constraints.Max;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class GoogleSearchNodeConfig {
|
||||||
|
/**
|
||||||
|
* 搜索查询关键词
|
||||||
|
*/
|
||||||
|
private String query;
|
||||||
|
|
||||||
|
@JsonProperty("search_engine")
|
||||||
|
@Pattern(
|
||||||
|
regexp = "search_std|search_pro|search_pro_sogou|search_pro_quark",
|
||||||
|
message = "搜索引擎参数无效"
|
||||||
|
)
|
||||||
|
private String searchEngine = "search_std";
|
||||||
|
|
||||||
|
@JsonProperty("result_count")
|
||||||
|
@Min(value = 1, message = "搜索结果数量不能小于 1")
|
||||||
|
@Max(value = 50, message = "搜索结果数量不能大于 50")
|
||||||
|
private Integer resultCount = 10;
|
||||||
|
|
||||||
|
@JsonProperty("search_domain_filter")
|
||||||
|
private String searchDomainFilter;
|
||||||
|
|
||||||
|
@JsonProperty("search_recency_filter")
|
||||||
|
@Pattern(
|
||||||
|
regexp = "oneDay|oneWeek|oneMonth|oneYear|noLimit",
|
||||||
|
message = "搜索时间范围参数无效"
|
||||||
|
)
|
||||||
|
private String searchRecencyFilter = "noLimit";
|
||||||
|
|
||||||
|
@JsonProperty("content_size")
|
||||||
|
@Pattern(regexp = "medium|high", message = "网页摘要长度参数无效")
|
||||||
|
private String contentSize = "medium";
|
||||||
|
|
||||||
|
@JsonProperty("include_image")
|
||||||
|
private Boolean includeImage = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
package org.ruoyi.workflow.workflow.node.googleSearch;
|
||||||
|
|
||||||
|
import ai.z.openapi.ZhipuAiClient;
|
||||||
|
import ai.z.openapi.service.web_search.WebSearchRequest;
|
||||||
|
import ai.z.openapi.service.web_search.WebSearchResp;
|
||||||
|
import ai.z.openapi.service.web_search.WebSearchResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.ruoyi.common.chat.domain.bo.chat.ChatModelBo;
|
||||||
|
import org.ruoyi.common.chat.service.chat.IChatModelService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智谱 Web Search 官方 SDK 适配器。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ZhipuWebSearchClient {
|
||||||
|
|
||||||
|
private static final String ZHIPU_PROVIDER_CODE = "zhipu";
|
||||||
|
private static final String DEFAULT_BASE_URL = "https://open.bigmodel.cn/api/paas/v4/";
|
||||||
|
|
||||||
|
private final ZhipuWebSearchProperties properties;
|
||||||
|
private final IChatModelService chatModelService;
|
||||||
|
|
||||||
|
public SearchResponse search(String query, GoogleSearchNodeConfig config, String requestId) {
|
||||||
|
Credential credential = resolveCredential();
|
||||||
|
ZhipuAiClient client = createClient(credential);
|
||||||
|
try {
|
||||||
|
WebSearchRequest request = WebSearchRequest.builder()
|
||||||
|
.searchQuery(query)
|
||||||
|
.searchEngine(config.getSearchEngine())
|
||||||
|
.count(config.getResultCount())
|
||||||
|
.searchDomainFilter(blankToNull(config.getSearchDomainFilter()))
|
||||||
|
.searchRecencyFilter(config.getSearchRecencyFilter())
|
||||||
|
.contentSize(config.getContentSize())
|
||||||
|
.includeImage(config.getIncludeImage())
|
||||||
|
.requestId(requestId)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
WebSearchResponse response = client.webSearch().createWebSearch(request);
|
||||||
|
if (response == null || !response.isSuccess() || response.getData() == null) {
|
||||||
|
String message = response == null ? "接口未返回响应" : StringUtils.defaultIfBlank(response.getMsg(), "未知错误");
|
||||||
|
throw new IllegalStateException("智谱 Web Search 调用失败:" + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SearchResult> results = response.getData().getWebSearchResp() == null
|
||||||
|
? List.of()
|
||||||
|
: response.getData().getWebSearchResp().stream()
|
||||||
|
.map(this::toSearchResult)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return new SearchResponse(
|
||||||
|
query,
|
||||||
|
config.getSearchEngine(),
|
||||||
|
response.getData().getRequestId(),
|
||||||
|
results.size(),
|
||||||
|
results
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ZhipuAiClient createClient(Credential credential) {
|
||||||
|
int connectTimeout = positiveOrDefault(properties.getConnectTimeout(), 10);
|
||||||
|
int readTimeout = positiveOrDefault(properties.getReadTimeout(), 30);
|
||||||
|
return ZhipuAiClient.builder()
|
||||||
|
.ofZHIPU()
|
||||||
|
.apiKey(credential.apiKey())
|
||||||
|
.baseUrl(credential.baseUrl())
|
||||||
|
.networkConfig(connectTimeout, readTimeout, readTimeout, readTimeout, TimeUnit.SECONDS)
|
||||||
|
.enableTokenCache()
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Credential resolveCredential() {
|
||||||
|
if (isUsableApiKey(properties.getApiKey())) {
|
||||||
|
return new Credential(normalizeBaseUrl(properties.getBaseUrl()), properties.getApiKey().trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatModelBo query = new ChatModelBo();
|
||||||
|
query.setProviderCode(ZHIPU_PROVIDER_CODE);
|
||||||
|
return chatModelService.queryList(query).stream()
|
||||||
|
.filter(model -> isUsableApiKey(model.getApiKey()))
|
||||||
|
.findFirst()
|
||||||
|
.map(model -> new Credential(normalizeBaseUrl(model.getApiHost()), model.getApiKey().trim()))
|
||||||
|
.orElseThrow(() -> new IllegalStateException(
|
||||||
|
"未配置智谱 API Key,请设置环境变量 ZAI_API_KEY,或在模型管理中配置 zhipu 厂商密钥"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isUsableApiKey(String apiKey) {
|
||||||
|
return StringUtils.isNotBlank(apiKey)
|
||||||
|
&& !"sk_xx".equalsIgnoreCase(apiKey.trim())
|
||||||
|
&& !"your_api_key".equalsIgnoreCase(apiKey.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeBaseUrl(String baseUrl) {
|
||||||
|
String normalized = StringUtils.defaultIfBlank(baseUrl, DEFAULT_BASE_URL).trim();
|
||||||
|
normalized = StringUtils.removeEnd(normalized, "/");
|
||||||
|
if (!normalized.endsWith("/api/paas/v4")) {
|
||||||
|
normalized += "/api/paas/v4";
|
||||||
|
}
|
||||||
|
return normalized + "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
private int positiveOrDefault(Integer value, int defaultValue) {
|
||||||
|
return value != null && value > 0 ? value : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String blankToNull(String value) {
|
||||||
|
return StringUtils.isBlank(value) ? null : value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private SearchResult toSearchResult(WebSearchResp result) {
|
||||||
|
return new SearchResult(
|
||||||
|
result.getTitle(),
|
||||||
|
result.getContent(),
|
||||||
|
result.getLink(),
|
||||||
|
result.getMedia(),
|
||||||
|
result.getIcon(),
|
||||||
|
result.getRefer(),
|
||||||
|
result.getPublishDate(),
|
||||||
|
result.getImages()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Credential(String baseUrl, String apiKey) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record SearchResponse(
|
||||||
|
String query,
|
||||||
|
String searchEngine,
|
||||||
|
String requestId,
|
||||||
|
int count,
|
||||||
|
List<SearchResult> results
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record SearchResult(
|
||||||
|
String title,
|
||||||
|
String content,
|
||||||
|
String link,
|
||||||
|
String media,
|
||||||
|
String icon,
|
||||||
|
String refer,
|
||||||
|
String publishDate,
|
||||||
|
List<String> images
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package org.ruoyi.workflow.workflow.node.googleSearch;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智谱 Web Search 配置。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "workflow.web-search.zhipu")
|
||||||
|
public class ZhipuWebSearchProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智谱国内开放平台 API 根地址。
|
||||||
|
*/
|
||||||
|
private String baseUrl = "https://open.bigmodel.cn/api/paas/v4/";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智谱 API Key。建议通过环境变量 ZAI_API_KEY 注入。
|
||||||
|
*/
|
||||||
|
private String apiKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接超时秒数。
|
||||||
|
*/
|
||||||
|
private Integer connectTimeout = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取超时秒数。
|
||||||
|
*/
|
||||||
|
private Integer readTimeout = 30;
|
||||||
|
}
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package org.ruoyi.workflow.workflow.node.humanFeedBack;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.ruoyi.workflow.entity.WorkflowComponent;
|
|
||||||
import org.ruoyi.workflow.entity.WorkflowNode;
|
|
||||||
import org.ruoyi.workflow.workflow.NodeProcessResult;
|
|
||||||
import org.ruoyi.workflow.workflow.WfNodeState;
|
|
||||||
import org.ruoyi.workflow.workflow.WfState;
|
|
||||||
import org.ruoyi.workflow.workflow.WorkflowUtil;
|
|
||||||
import org.ruoyi.workflow.workflow.data.NodeIOData;
|
|
||||||
import org.ruoyi.workflow.workflow.node.AbstractWfNode;
|
|
||||||
|
|
||||||
import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 人机交互节点实现类
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class HumanFeedbackNode extends AbstractWfNode {
|
|
||||||
|
|
||||||
public HumanFeedbackNode(WorkflowComponent component, WorkflowNode nodeDefinition, WfState wfState, WfNodeState nodeState) {
|
|
||||||
super(component, nodeDefinition, wfState, nodeState);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 人机交互节点的处理逻辑
|
|
||||||
@Override
|
|
||||||
public NodeProcessResult onProcess() {
|
|
||||||
log.info("Processing HumanFeedback node: {}", node.getTitle());
|
|
||||||
// 从状态中获取用户输入数据
|
|
||||||
Object humanFeedbackState = state.data().get(HUMAN_FEEDBACK_KEY);
|
|
||||||
if (null != humanFeedbackState) {
|
|
||||||
String userInput = humanFeedbackState.toString();
|
|
||||||
if (StringUtils.isNotBlank(userInput)) {
|
|
||||||
// 用户已提供输入,将用户输入添加到节点输入和输出中
|
|
||||||
NodeIOData feedbackData = NodeIOData.createByText("output", "default", userInput);
|
|
||||||
// 添加到输出列表,这样后续节点可以使用
|
|
||||||
state.getOutputs().add(feedbackData);
|
|
||||||
// 设置为成功状态
|
|
||||||
state.setProcessStatus(NODE_PROCESS_STATUS_SUCCESS);
|
|
||||||
log.info("Human feedback processed for node: {}, content: {}", node.getTitle(), userInput);
|
|
||||||
} else {
|
|
||||||
// 用户输入为空,设置等待状态
|
|
||||||
state.setProcessStatus(NODE_PROCESS_STATUS_DOING);
|
|
||||||
log.info("Human feedback is empty for node: {}", node.getTitle());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 没有用户输入,这可能是正常情况(等待用户输入)
|
|
||||||
// 但为了确保流程可以继续,我们仍然标记为成功
|
|
||||||
state.setProcessStatus(NODE_PROCESS_STATUS_SUCCESS);
|
|
||||||
log.info("No human feedback found for node: {}, continuing workflow", node.getTitle());
|
|
||||||
}
|
|
||||||
return new NodeProcessResult();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
package org.ruoyi.workflow.workflow.node.keywordExtractor;
|
|
||||||
|
|
||||||
import dev.langchain4j.data.message.SystemMessage;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.ruoyi.workflow.entity.WorkflowComponent;
|
|
||||||
import org.ruoyi.workflow.entity.WorkflowNode;
|
|
||||||
import org.ruoyi.workflow.util.SpringUtil;
|
|
||||||
import org.ruoyi.workflow.util.WorkflowMessageUtil;
|
|
||||||
import org.ruoyi.workflow.workflow.NodeProcessResult;
|
|
||||||
import org.ruoyi.workflow.workflow.WfNodeState;
|
|
||||||
import org.ruoyi.workflow.workflow.WfState;
|
|
||||||
import org.ruoyi.workflow.workflow.WorkflowUtil;
|
|
||||||
import org.ruoyi.workflow.workflow.data.NodeIOData;
|
|
||||||
import org.ruoyi.workflow.workflow.node.AbstractWfNode;
|
|
||||||
import org.ruoyi.workflow.workflow.node.enmus.NodeMessageTemplateEnum;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.DEFAULT_OUTPUT_PARAM_NAME;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 【节点】关键词提取节点
|
|
||||||
* 使用 LLM 从文本中提取关键词
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class KeywordExtractorNode extends AbstractWfNode {
|
|
||||||
|
|
||||||
public KeywordExtractorNode(WorkflowComponent wfComponent, WorkflowNode nodeDef, WfState wfState, WfNodeState nodeState) {
|
|
||||||
super(wfComponent, nodeDef, wfState, nodeState);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理关键词提取
|
|
||||||
* nodeConfig 格式:
|
|
||||||
* {
|
|
||||||
* "model_name": "deepseek-chat",
|
|
||||||
* "category": "llm",
|
|
||||||
* "top_n": 5,
|
|
||||||
* "prompt": "额外的提示词"
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* @return 提取的关键词列表
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public NodeProcessResult onProcess() {
|
|
||||||
KeywordExtractorNodeConfig config = checkAndGetConfig(KeywordExtractorNodeConfig.class);
|
|
||||||
|
|
||||||
// 获取输入文本
|
|
||||||
String inputText = getFirstInputText();
|
|
||||||
if (StringUtils.isBlank(inputText)) {
|
|
||||||
log.warn("Keyword extractor node has no input text, node: {}", state.getUuid());
|
|
||||||
// 返回空结果
|
|
||||||
List<NodeIOData> outputs = new ArrayList<>();
|
|
||||||
outputs.add(NodeIOData.createByText(DEFAULT_OUTPUT_PARAM_NAME, "", ""));
|
|
||||||
return NodeProcessResult.builder().content(outputs).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Keyword extractor node config: {}", config);
|
|
||||||
log.info("Input text length: {}", inputText.length());
|
|
||||||
|
|
||||||
// 构建提示词
|
|
||||||
String prompt = buildPrompt(config, inputText);
|
|
||||||
log.info("Keyword extraction prompt: {}", prompt);
|
|
||||||
|
|
||||||
// 调用 LLM 进行关键词提取
|
|
||||||
WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class);
|
|
||||||
String modelName = config.getModelName();
|
|
||||||
// 获取节点模板提示词信息
|
|
||||||
String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.KEYWORD_EXTRACTOR.getValue());
|
|
||||||
// 发送SSE事件消息
|
|
||||||
WorkflowMessageUtil.sendEmitterMessage(wfState.getSseEmitter(), node, nodeMessageTemplate);
|
|
||||||
// 使用流式调用
|
|
||||||
workflowUtil.streamingInvokeLLM(wfState, state, node, modelName, prompt, nodeMessageTemplate);
|
|
||||||
return new NodeProcessResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建关键词提取的提示词
|
|
||||||
*/
|
|
||||||
private String buildPrompt(KeywordExtractorNodeConfig config, String inputText) {
|
|
||||||
StringBuilder promptBuilder = new StringBuilder();
|
|
||||||
|
|
||||||
// 基础提示词
|
|
||||||
promptBuilder.append("请从以下文本中提取 ").append(config.getTopN()).append(" 个最重要的关键词。\n\n");
|
|
||||||
|
|
||||||
// 添加自定义提示词(如果有)
|
|
||||||
if (StringUtils.isNotBlank(config.getPrompt())) {
|
|
||||||
promptBuilder.append(config.getPrompt()).append("\n\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 输出格式要求
|
|
||||||
promptBuilder.append("要求:\n");
|
|
||||||
promptBuilder.append("1. 只返回关键词,每个关键词用逗号分隔\n");
|
|
||||||
promptBuilder.append("2. 关键词应该是名词或名词短语\n");
|
|
||||||
promptBuilder.append("3. 按重要性从高到低排序\n");
|
|
||||||
promptBuilder.append("4. 不要添加任何解释或额外的文字\n\n");
|
|
||||||
|
|
||||||
// 原始文本
|
|
||||||
promptBuilder.append("文本内容:\n");
|
|
||||||
promptBuilder.append(inputText);
|
|
||||||
|
|
||||||
return promptBuilder.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package org.ruoyi.workflow.workflow.node.keywordExtractor;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
import jakarta.validation.constraints.Max;
|
|
||||||
import jakarta.validation.constraints.Min;
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 关键词提取节点配置
|
|
||||||
*/
|
|
||||||
@EqualsAndHashCode
|
|
||||||
@Data
|
|
||||||
public class KeywordExtractorNodeConfig {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 模型分类(如:llm, embedding 等)
|
|
||||||
*/
|
|
||||||
private String category;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 模型名称
|
|
||||||
*/
|
|
||||||
@NotNull
|
|
||||||
@JsonProperty("model_name")
|
|
||||||
private String modelName;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 提取的关键词数量
|
|
||||||
*/
|
|
||||||
@Min(1)
|
|
||||||
@Max(50)
|
|
||||||
@JsonProperty("top_n")
|
|
||||||
private Integer topN = 5;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 提示词(可选)
|
|
||||||
* 用于指导关键词提取的额外说明
|
|
||||||
*/
|
|
||||||
private String prompt;
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
package org.ruoyi.workflow.workflow.node.mailSend;
|
package org.ruoyi.workflow.workflow.node.mailSend;
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSON;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.alibaba.fastjson.JSONObject;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.alibaba.fastjson.JSONValidator;
|
|
||||||
import jakarta.mail.internet.MimeMessage;
|
import jakarta.mail.internet.MimeMessage;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.ruoyi.workflow.entity.WorkflowComponent;
|
import org.ruoyi.workflow.entity.WorkflowComponent;
|
||||||
import org.ruoyi.workflow.entity.WorkflowNode;
|
import org.ruoyi.workflow.entity.WorkflowNode;
|
||||||
|
import org.ruoyi.workflow.util.JsonUtil;
|
||||||
import org.ruoyi.workflow.workflow.NodeProcessResult;
|
import org.ruoyi.workflow.workflow.NodeProcessResult;
|
||||||
import org.ruoyi.workflow.workflow.WfNodeState;
|
import org.ruoyi.workflow.workflow.WfNodeState;
|
||||||
import org.ruoyi.workflow.workflow.WfState;
|
import org.ruoyi.workflow.workflow.WfState;
|
||||||
@@ -41,10 +41,25 @@ public class MailSendNode extends AbstractWfNode {
|
|||||||
String input = getDataFromInput(inputs);
|
String input = getDataFromInput(inputs);
|
||||||
// 判断是否为JSON格式(LLM输出转换 由LLM生成格式)
|
// 判断是否为JSON格式(LLM输出转换 由LLM生成格式)
|
||||||
if (StringUtils.isNotBlank(input) && isJson(input)) {
|
if (StringUtils.isNotBlank(input) && isJson(input)) {
|
||||||
JSONObject inputJson = JSON.parseObject(input);
|
try {
|
||||||
JSONObject configJson = (JSONObject) JSON.toJSON(config);
|
// 使用统一的 JsonUtil 进行解析
|
||||||
configJson.putAll(inputJson);
|
JsonNode inputJson = JsonUtil.toJsonNode(input);
|
||||||
config = configJson.toJavaObject(MailSendNodeConfig.class);
|
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)
|
// 安全获取模板(使用 defaultString 避免 null)
|
||||||
@@ -217,9 +232,10 @@ public class MailSendNode extends AbstractWfNode {
|
|||||||
if (str == null || str.trim().isEmpty()) {
|
if (str == null || str.trim().isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 使用 try-with-resources 正确处理 JSONValidator 资源关闭
|
try {
|
||||||
try (JSONValidator validator = JSONValidator.from(str.trim())) {
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
return validator.getType() == JSONValidator.Type.Object;
|
objectMapper.readTree(str.trim());
|
||||||
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("JSON格式校验失败: {}", e.getMessage());
|
log.warn("JSON格式校验失败: {}", e.getMessage());
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package org.ruoyi.workflow.workflow.node.switcher;
|
package org.ruoyi.workflow.workflow.node.switcher;
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSON;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.alibaba.fastjson.JSONArray;
|
|
||||||
import com.alibaba.fastjson.JSONObject;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.ObjectUtils;
|
import org.apache.commons.lang3.ObjectUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
@@ -10,6 +8,7 @@ import org.ruoyi.common.core.utils.SpringUtils;
|
|||||||
import org.ruoyi.workflow.entity.WorkflowComponent;
|
import org.ruoyi.workflow.entity.WorkflowComponent;
|
||||||
import org.ruoyi.workflow.entity.WorkflowNode;
|
import org.ruoyi.workflow.entity.WorkflowNode;
|
||||||
import org.ruoyi.workflow.service.WorkflowNodeService;
|
import org.ruoyi.workflow.service.WorkflowNodeService;
|
||||||
|
import org.ruoyi.workflow.util.JsonUtil;
|
||||||
import org.ruoyi.workflow.workflow.NodeProcessResult;
|
import org.ruoyi.workflow.workflow.NodeProcessResult;
|
||||||
import org.ruoyi.workflow.workflow.WfNodeState;
|
import org.ruoyi.workflow.workflow.WfNodeState;
|
||||||
import org.ruoyi.workflow.workflow.WfState;
|
import org.ruoyi.workflow.workflow.WfState;
|
||||||
@@ -19,8 +18,6 @@ import org.ruoyi.workflow.workflow.node.enmus.NodeMessageTemplateEnum;
|
|||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 条件分支节点
|
* 条件分支节点
|
||||||
@@ -339,23 +336,30 @@ public class SwitcherNode extends AbstractWfNode {
|
|||||||
String inputConfig = workflowNode.getInputConfig();
|
String inputConfig = workflowNode.getInputConfig();
|
||||||
log.info("节点 '{}' 的输入配置: {}", nodeUuid, inputConfig);
|
log.info("节点 '{}' 的输入配置: {}", nodeUuid, inputConfig);
|
||||||
if (StringUtils.isNotBlank(inputConfig)){
|
if (StringUtils.isNotBlank(inputConfig)){
|
||||||
// 解析输入配置为JSON对象
|
try {
|
||||||
JSONObject configJson = JSON.parseObject(inputConfig);
|
// 使用统一的 JsonUtil 而不是每次创建新的 ObjectMapper
|
||||||
// 获取 user_inputs 数组
|
JsonNode configJson = JsonUtil.toJsonNode(inputConfig);
|
||||||
JSONArray userInputs = configJson.getJSONArray("user_inputs");
|
if (configJson == null) {
|
||||||
if (userInputs != null && !userInputs.isEmpty()) {
|
log.warn("节点 '{}' 的输入配置 JSON 解析结果为 null", nodeUuid);
|
||||||
// 在 user_inputs 中查找匹配的参数名,并获取对应值
|
return result;
|
||||||
Optional<String> valueOpt = userInputs.stream()
|
|
||||||
.filter(JSONObject.class::isInstance)
|
|
||||||
.map(JSONObject.class::cast)
|
|
||||||
.filter(obj -> paramName.equals(obj.getString("name")))
|
|
||||||
.map(matchedObj -> getValueFromInputs(nodeUuid, "input", inputs))
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.findFirst();
|
|
||||||
// 若找到匹配值,则更新结果
|
|
||||||
if (valueOpt.isPresent()) {
|
|
||||||
result = valueOpt.get();
|
|
||||||
}
|
}
|
||||||
|
// 获取 user_inputs 数组
|
||||||
|
JsonNode userInputs = configJson.get("user_inputs");
|
||||||
|
if (userInputs != null && userInputs.isArray()) {
|
||||||
|
// 在 user_inputs 中查找匹配的参数名,并获取对应值
|
||||||
|
for (JsonNode inputNode : userInputs) {
|
||||||
|
if (inputNode.has("name") && paramName.equals(inputNode.get("name").asText())) {
|
||||||
|
String value = getValueFromInputs(nodeUuid, "input", inputs);
|
||||||
|
if (value != null) {
|
||||||
|
result = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("解析节点 '{}' 输入配置失败,参数名: {}, 配置内容: {}", nodeUuid, paramName, inputConfig, e);
|
||||||
|
// 不抛出异常,返回默认结果,避免中断整个流程
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user