mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-09-13 00:14:59 +00:00
Compare commits
16 Commits
9adeee6c83
...
v3.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c175ba28d | ||
|
|
e34592c3d4 | ||
|
|
fd044a4752 | ||
|
|
6e264ad500 | ||
|
|
83fd1ee983 | ||
|
|
7640de34e5 | ||
|
|
77846601ee | ||
|
|
b80dc6e23f | ||
|
|
176cc67a5c | ||
|
|
afaa86ef6e | ||
|
|
d1a820728f | ||
|
|
11bb1dba0f | ||
|
|
345d2819d8 | ||
|
|
36e3cf8c52 | ||
|
|
06ff95cdc3 | ||
|
|
e925050207 |
107
.github/workflows/publish-images.yml
vendored
Normal file
107
.github/workflows/publish-images.yml
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
name: Publish Docker Images
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
concurrency:
|
||||
group: publish-images-${{ github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_OWNER: ${{ github.repository_owner }}
|
||||
RELEASE_TAG: ${{ github.ref_name }}
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish ${{ matrix.image }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- image: ruoyi-ai-backend
|
||||
source: local
|
||||
context: .
|
||||
dockerfile: docs/docker/ruoyi-ai/Dockerfile.backend
|
||||
- image: ruoyi-ai-mysql
|
||||
source: local
|
||||
context: .
|
||||
dockerfile: docs/docker/ruoyi-ai/Dockerfile.mysql
|
||||
- image: ruoyi-ai-admin
|
||||
source: admin
|
||||
context: build/ruoyi-admin
|
||||
dockerfile: build/ruoyi-admin/apps/web-antd/Dockerfile
|
||||
- image: ruoyi-ai-web
|
||||
source: web
|
||||
context: build/ruoyi-web
|
||||
dockerfile: build/ruoyi-web/Dockerfile.frontend
|
||||
|
||||
steps:
|
||||
- name: Checkout backend repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ env.RELEASE_TAG }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Checkout admin repository
|
||||
if: matrix.source == 'admin'
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ github.repository_owner }}/ruoyi-admin
|
||||
ref: ${{ env.RELEASE_TAG }}
|
||||
path: build/ruoyi-admin
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Checkout web repository
|
||||
if: matrix.source == 'web'
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ github.repository_owner }}/ruoyi-web
|
||||
ref: ${{ env.RELEASE_TAG }}
|
||||
path: build/ruoyi-web
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ matrix.image }}
|
||||
tags: |
|
||||
type=raw,value=${{ env.RELEASE_TAG }}
|
||||
type=raw,value=latest
|
||||
labels: |
|
||||
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
||||
org.opencontainers.image.version=${{ env.RELEASE_TAG }}
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ${{ matrix.context }}
|
||||
file: ${{ matrix.dockerfile }}
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha,scope=${{ matrix.image }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.image }}
|
||||
provenance: true
|
||||
sbom: true
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -23,6 +23,9 @@ target/
|
||||
.idea
|
||||
.claude
|
||||
.github
|
||||
!.github/
|
||||
!.github/workflows/
|
||||
!.github/workflows/**
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
@@ -1,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 测试数据。
|
||||
266
README.md
266
README.md
@@ -2,11 +2,7 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
[![Contributors][contributors-shield]][contributors-url]
|
||||
[![Forks][forks-shield]][forks-url]
|
||||
[![Stargazers][stars-shield]][stars-url]
|
||||
[![Issues][issues-shield]][issues-url]
|
||||
[![MIT License][license-shield]][license-url]
|
||||
[![Contributors][contributors-shield]][contributors-url] [![Forks][forks-shield]][forks-url] [![Stargazers][stars-shield]][stars-url] [![Issues][issues-shield]][issues-url] [![MIT License][license-shield]][license-url]
|
||||
|
||||
|
||||
<p align="center">
|
||||
@@ -17,236 +13,235 @@
|
||||
|
||||
<img src="docs/image/logo.png" alt="RuoYi AI Logo" width="120" height="120">
|
||||
|
||||
### 企业级AI助手平台
|
||||
### Enterprise-Grade AI Assistant Platform
|
||||
|
||||
*开箱即用的全栈AI平台,支持多智能体协同、Supervisor模式编排、多种决策模式、RAG技术和流程编排能力*
|
||||
*An out-of-the-box full-stack AI platform supporting multi-agent collaboration, Supervisor mode orchestration, and multiple decision models, with advanced RAG technology and visual workflow orchestration capabilities*
|
||||
|
||||
**[English](README_EN.md)** | **[📖 使用文档](https://doc.ruoyiai.chat/)** |
|
||||
**[🚀 在线体验](https://web.ruoyiai.chat/)** | **[🐛 问题反馈](https://github.com/ageerle/ruoyi-ai/issues)** | **[💡 功能建议](https://github.com/ageerle/ruoyi-ai/issues)**
|
||||
**[中文](README_ZH.md)** | **[📖 Documentation](https://doc.ruoyiai.chat/)** |
|
||||
**[🚀 Live Demo](https://web.ruoyiai.chat/)** | **[🐛 Report Issues](https://github.com/ageerle/ruoyi-ai/issues)** | **[💡 Feature Requests](https://github.com/ageerle/ruoyi-ai/issues)**
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
## ✨ 核心亮点
|
||||
|
||||
| 模块 | 现有能力
|
||||
|:---------:|---
|
||||
| **模型管理** | 多模型接入(DeepSeek/智谱/MIMO/百炼/OpenAI)、多模态理解、Coze/DIFY/FastGPT/RAGFlow平台集成
|
||||
| **知识管理** | 本地RAG + 向量库(Milvus/Weaviate/Qdrant) + 文档解析
|
||||
| **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态
|
||||
| **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点
|
||||
| **智能体管理** | 基于Langchain4j的Agent框架、Supervisor模式编排,支持多种决策模型,可以灵活搭配工具,skills
|
||||
|
||||
|
||||
### 项目源码
|
||||
## ✨ Core Features
|
||||
|
||||
| 项目模块 | GitHub 仓库 | Gitee 仓库 | GitCode 仓库 |
|
||||
| Module | Current Capabilities |
|
||||
|:---:|---|
|
||||
| **Model Management** | Multi-model integration (DeepSeek/Zhipu/MIMO/Bailian/OpenAI), multi-modal understanding, Coze/DIFY/FastGPT/RAGFlow platform integration |
|
||||
| **Knowledge Management** | Local RAG + Vector DB (Milvus/Weaviate/Qdrant) + Document parsing |
|
||||
| **Tool Management** | MCP protocol integration, Skills capability + Extensible tool ecosystem |
|
||||
| **Workflow Orchestration** | Visual workflow designer, drag-and-drop node orchestration, SSE streaming execution, currently supports model calls, email sending, manual review, and other nodes |
|
||||
| **Multi-Agent** | Agent framework based on Langchain4j, Supervisor mode orchestration, supports multiple decision models, can flexibly combine tools and skills |
|
||||
|
||||
### Project Repositories
|
||||
|
||||
| Module | GitHub Repository | Gitee Repository | GitCode Repository |
|
||||
|----------|-------------------------------------------------------|------------------------------------------------------|--------------------------------------------------------|
|
||||
| 🔧 后端服务 | [ruoyi-ai](https://github.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitee.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitcode.com/ageerle/ruoyi-ai) |
|
||||
| 🎨 用户前端 | [ruoyi-web](https://github.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitee.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitcode.com/ageerle/ruoyi-web) |
|
||||
| 🛠️ 管理后台 | [ruoyi-admin](https://github.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitee.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitcode.com/ageerle/ruoyi-admin) |
|
||||
| 🎬 短剧平台 | [ruoyi-drama](https://github.com/ageerle/ruoyi-drama) | [ruoyi-drama](https://gitee.com/ageerle/ruoyi-drama) | [ruoyi-drama](https://gitcode.com/ageerle/ruoyi-drama) |
|
||||
| 🤖 编程助手 | [ruoyi-copilot](https://github.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitee.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitcode.com/ageerle/ruoyi-copilot) |
|
||||
| 📱 小程序端 | [ruoyi-uniapp](https://github.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitee.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitcode.com/ageerle/ruoyi-uniapp) |
|
||||
| 🔧 Backend | [ruoyi-ai](https://github.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitee.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitcode.com/ageerle/ruoyi-ai) |
|
||||
| 🎨 User Frontend | [ruoyi-web](https://github.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitee.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitcode.com/ageerle/ruoyi-web) |
|
||||
| 🛠️ Admin Panel | [ruoyi-admin](https://github.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitee.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitcode.com/ageerle/ruoyi-admin) |
|
||||
| 🎬 Drama | [ruoyi-drama](https://github.com/ageerle/ruoyi-drama) | [ruoyi-drama](https://gitee.com/ageerle/ruoyi-drama) | [ruoyi-drama](https://gitcode.com/ageerle/ruoyi-drama) |
|
||||
| 🤖 Copilot | [ruoyi-copilot](https://github.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitee.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitcode.com/ageerle/ruoyi-copilot) |
|
||||
| 📱 Mini-App | [ruoyi-uniapp](https://github.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitee.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitcode.com/ageerle/ruoyi-uniapp) |
|
||||
|
||||
### 合作项目
|
||||
| 项目名称 | GitHub 仓库 | Gitee 仓库
|
||||
### Partner Projects
|
||||
| Project Name | GitHub Repository | Gitee Repository |
|
||||
|----------------|-------------------------------------------------------|------------------------------------------------------|
|
||||
| element-plus-x | [element-plus-x](https://github.com/element-plus-x/Element-Plus-X) | [element-plus-x](https://gitee.com/he-jiayue/element-plus-x) |
|
||||
| element-plus-x | [element-plus-x](https://github.com/element-plus-x/Element-Plus-X) | [element-plus-x](https://gitee.com/he-jiayue/element-plus-x) |
|
||||
|
||||
## 🛠️ 技术架构
|
||||
## 🛠️ Technical Architecture
|
||||
|
||||
### 核心框架
|
||||
- **后端架构**:Spring Boot 3.5.8 + Langchain4j
|
||||
- **数据存储**:MySQL 8.0 + Redis + 向量数据库(Milvus/Weaviate/Qdrant)
|
||||
- **前端技术**:Vue 3 + Vben Admin + element-plus-x
|
||||
- **安全认证**:Sa-Token + JWT 双重保障
|
||||
- **文档处理**:PDF、Word、Excel 解析,图像智能分析
|
||||
- **实时通信**:WebSocket 实时通信,SSE 流式响应
|
||||
- **系统监控**:完善的日志体系、性能监控、服务健康检查
|
||||
### Core Framework
|
||||
- **Backend**: Spring Boot 3.5.8 + Langchain4j
|
||||
- **Data Storage**: MySQL 8.0 + Redis + Vector Databases (Milvus/Weaviate/Qdrant)
|
||||
- **Frontend**: Vue 3 + Vben Admin + element-plus-x
|
||||
- **Security**: Sa-Token + JWT dual-layer security
|
||||
- **Document Processing**: PDF, Word, Excel parsing, intelligent image analysis
|
||||
- **Real-time Communication**: WebSocket real-time communication, SSE streaming response
|
||||
- **System Monitoring**: Comprehensive logging system, performance monitoring, service health checks
|
||||
|
||||
## 🐳 Docker 部署
|
||||
## 🐳 Docker Deployment
|
||||
|
||||
本项目提供两种 Docker 部署方式:
|
||||
This project provides two Docker deployment methods:
|
||||
|
||||
### 方式一:一键启动所有服务(推荐)
|
||||
### Method 1: One-click Start All Services (Recommended)
|
||||
|
||||
使用 `docker-compose-all.yaml` 可以一键启动所有服务(包括后端、管理端、用户端及依赖服务):
|
||||
Use `docker-compose-all.yaml` to start all services at once (including backend, admin panel, user frontend, and dependencies):
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
# Clone the repository
|
||||
git clone https://github.com/ageerle/ruoyi-ai.git
|
||||
cd ruoyi-ai
|
||||
|
||||
# 启动所有服务(从镜像仓库拉取预构建镜像)
|
||||
docker-compose -f docker-compose-all.yaml up -d
|
||||
# Start all services (pull pre-built images from GHCR)
|
||||
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml up -d
|
||||
|
||||
# 查看服务状态
|
||||
docker-compose -f docker-compose-all.yaml ps
|
||||
# Check service status
|
||||
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml ps
|
||||
|
||||
# 访问服务
|
||||
# 管理端: http://localhost:25666 (admin / admin123)
|
||||
# 用户端: http://localhost:25137
|
||||
# 后端API: http://localhost:26039
|
||||
# Access services
|
||||
# Admin Panel: http://localhost:25666 (admin / admin123)
|
||||
# User Frontend: http://localhost:25137
|
||||
# Backend API: http://localhost:26039
|
||||
```
|
||||
|
||||
### 方式二:分步部署(源码编译)
|
||||
### Method 2: Step-by-step Deployment (Source Build)
|
||||
|
||||
如果您需要从源码构建后端服务,请按照以下步骤操作:
|
||||
If you need to build backend services from source, follow these steps:
|
||||
|
||||
#### 第一步:部署后端服务
|
||||
#### Step 1: Deploy Backend Service
|
||||
|
||||
```bash
|
||||
# 进入后端项目目录
|
||||
# Enter backend project directory
|
||||
cd ruoyi-ai
|
||||
|
||||
# 启动后端服务(源码编译构建)
|
||||
# Start backend service (build from source)
|
||||
docker-compose up -d --build
|
||||
|
||||
# 等待后端服务启动完成
|
||||
# Wait for backend service to start
|
||||
docker-compose logs -f backend
|
||||
```
|
||||
|
||||
#### 第二步:部署管理端
|
||||
#### Step 2: Deploy Admin Panel
|
||||
|
||||
```bash
|
||||
# 进入管理端项目目录
|
||||
# Enter admin panel project directory
|
||||
cd ruoyi-admin
|
||||
|
||||
# 构建并启动管理端
|
||||
# Build and start admin panel
|
||||
docker-compose up -d --build
|
||||
|
||||
# 访问管理端
|
||||
# 地址: http://localhost:5666
|
||||
# Access admin panel
|
||||
# URL: http://localhost:5666
|
||||
```
|
||||
|
||||
#### 第三步:部署用户端(可选)
|
||||
#### Step 3: Deploy User Frontend (Optional)
|
||||
|
||||
```bash
|
||||
# 进入用户端项目目录
|
||||
# Enter user frontend project directory
|
||||
cd ruoyi-web
|
||||
|
||||
# 构建并启动用户端
|
||||
# Build and start user frontend
|
||||
docker-compose up -d --build
|
||||
|
||||
# 访问用户端
|
||||
# 地址: http://localhost:5137
|
||||
# Access user frontend
|
||||
# URL: http://localhost:5137
|
||||
```
|
||||
|
||||
### 服务端口说明
|
||||
### Service Ports
|
||||
|
||||
| 服务 | 一键启动端口 | 分步部署端口 | 说明 |
|
||||
| Service | One-click Port | Step-by-step Port | Description |
|
||||
|------|-------------|-------------|------|
|
||||
| 管理端 | 25666 | 5666 | 管理后台访问地址 |
|
||||
| 用户端 | 25137 | 5137 | 用户前端访问地址 |
|
||||
| 后端服务 | 26039 | 6039 | 后端 API 服务 |
|
||||
| MySQL | 23306 | 23306 | 数据库服务 |
|
||||
| Redis | 26379 | 6379 | 缓存服务 |
|
||||
| Weaviate | 28080 | 28080 | 向量数据库 |
|
||||
| MinIO API | 29000 | 9000 | 对象存储 API |
|
||||
| MinIO Console | 29090 | 9090 | 对象存储控制台 |
|
||||
| Admin Panel | 25666 | 5666 | Admin backend access |
|
||||
| User Frontend | 25137 | 5137 | User frontend access |
|
||||
| Backend Service | 26039 | 6039 | Backend API service |
|
||||
| MySQL | 23306 | 23306 | Database service |
|
||||
| Redis | 26379 | 6379 | Cache service |
|
||||
| Weaviate | 28080 | 28080 | Vector database |
|
||||
| MinIO API | 29000 | 9000 | Object storage API |
|
||||
| MinIO Console | 29090 | 9090 | Object storage console |
|
||||
|
||||
### 镜像仓库
|
||||
### Image Registry
|
||||
|
||||
所有镜像托管在阿里云容器镜像服务:
|
||||
Application images are published to GitHub Container Registry (GHCR) by GitHub Actions when a release tag such as `v3.1.0` is pushed:
|
||||
|
||||
```
|
||||
crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai
|
||||
ghcr.io/ageerle/ruoyi-ai-backend:v3.1.0
|
||||
ghcr.io/ageerle/ruoyi-ai-mysql:v3.1.0
|
||||
ghcr.io/ageerle/ruoyi-ai-admin:v3.1.0
|
||||
ghcr.io/ageerle/ruoyi-ai-web:v3.1.0
|
||||
```
|
||||
|
||||
可用镜像:
|
||||
- `mysql:v3` - MySQL 数据库(包含初始化 SQL)
|
||||
- `redis:6.2` - Redis 缓存
|
||||
- `weaviate:1.30.0` - 向量数据库
|
||||
- `minio:latest` - 对象存储
|
||||
- `ruoyi-ai-backend:latest` - 后端服务
|
||||
- `ruoyi-ai-admin:latest` - 管理端前端
|
||||
- `ruoyi-ai-web:latest` - 用户端前端
|
||||
The Compose file defaults to `latest`. To pin a release, create `docs/docker/ruoyi-ai/.env` with `RUIYI_VERSION=v3.1.0`.
|
||||
|
||||
### 常用命令
|
||||
After the first workflow run, set the four GHCR packages to `Public` in GitHub so deployment servers can pull them without logging in.
|
||||
|
||||
The same release tag must exist in `ageerle/ruoyi-admin` and `ageerle/ruoyi-web`; the publish workflow checks out those repositories at the backend release tag before building their images.
|
||||
|
||||
### Common Commands
|
||||
|
||||
```bash
|
||||
# 停止所有服务
|
||||
docker-compose -f docker-compose-all.yaml down
|
||||
# Stop all services
|
||||
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml down
|
||||
|
||||
# 查看服务日志
|
||||
docker-compose -f docker-compose-all.yaml logs -f [服务名]
|
||||
# View service logs
|
||||
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml logs -f [service-name]
|
||||
|
||||
# 重启某个服务
|
||||
docker-compose -f docker-compose-all.yaml restart [服务名]
|
||||
# Restart a service
|
||||
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml restart [service-name]
|
||||
```
|
||||
|
||||
## 📚 使用文档
|
||||
## 📚 Documentation
|
||||
|
||||
想要深入了解安装部署、功能配置和二次开发?
|
||||
Want to learn more about installation, deployment, configuration, and secondary development?
|
||||
|
||||
**👉 [完整使用文档](https://doc.ruoyiai.chat/)**
|
||||
**👉 [Complete Documentation](https://doc.ruoyiai.chat/)**
|
||||
|
||||
## 🤝 参与贡献
|
||||
## 🤝 Contributing
|
||||
|
||||
我们热烈欢迎社区贡献!无论您是资深开发者还是初学者,都可以为项目贡献力量 💪
|
||||
We warmly welcome community contributions! Whether you are a seasoned developer or just getting started, you can contribute to the project 💪
|
||||
|
||||
### 贡献方式
|
||||
### How to Contribute
|
||||
|
||||
1. **Fork** 项目到您的账户
|
||||
2. **创建分支** (`git checkout -b feature/新功能名称`)
|
||||
3. **提交代码** (`git commit -m '添加某某功能'`)
|
||||
4. **推送分支** (`git push origin feature/新功能名称`)
|
||||
5. **发起 Pull Request**
|
||||
1. **Fork** the project to your account
|
||||
2. **Create a branch** (`git checkout -b feature/new-feature-name`)
|
||||
3. **Commit your changes** (`git commit -m 'Add new feature'`)
|
||||
4. **Push to the branch** (`git push origin feature/new-feature-name`)
|
||||
5. **Create a Pull Request**
|
||||
|
||||
> 💡 **小贴士**:建议将 PR 提交到 GitHub,我们会自动同步到其他代码托管平台
|
||||
> 💡 **Tip**: We recommend submitting PRs to GitHub, we will automatically sync to other code hosting platforms
|
||||
|
||||
## 📄 开源协议
|
||||
## 📄 License
|
||||
|
||||
本项目采用 **MIT 开源协议**,详情请查看 [LICENSE](LICENSE) 文件。
|
||||
This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## 🙏 特别鸣谢
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
感谢以下优秀的开源项目为本项目提供支持:
|
||||
- [Langchain4j](https://github.com/langchain4j/langchain4j) - 强大的 Java LLM 开发框架
|
||||
- [RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus) - 成熟的企业级快速开发框架
|
||||
- [Vben Admin](https://github.com/vbenjs/vue-vben-admin) - 现代化的 Vue 后台管理模板
|
||||
Thanks to the following excellent open-source projects for their support:
|
||||
- [Langchain4j](https://github.com/langchain4j/langchain4j) - Powerful Java LLM development framework
|
||||
- [RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus) - Mature enterprise-level rapid development framework
|
||||
- [Vben Admin](https://github.com/vbenjs/vue-vben-admin) - Modern Vue admin template
|
||||
|
||||
## 💎 Sponsors
|
||||
|
||||
## 💎 赞助商
|
||||
|
||||
**感谢以下赞助商对本项目的支持:**
|
||||
**Thanks to the following sponsors for supporting this project:**
|
||||
|
||||
<a href="https://www.atlascloud.ai?ref=89F97E">
|
||||
<img src="docs/image/sponsor/atlascloud_banner.png" alt="Atlas Cloud" width="160" height="80">
|
||||
</a>
|
||||
|
||||
[访问Atlas Cloud官网](https://www.atlascloud.ai?ref=89F97E&utm_source=github&utm_campaign=ruoyi-drama) · [编程计划优惠](https://www.atlascloud.ai/console/coding-plan)
|
||||
全模态 AI 推理平台,为开发者提供统一的 AI API,支持视频生成、图像生成和大语言模型。一次接入,即可访问 **300+ 精选模型**。
|
||||
[Visit Atlas Cloud](https://www.atlascloud.ai?ref=89F97E&utm_source=github&utm_campaign=ruoyi-drama) · [Coding Plan Promotion](https://www.atlascloud.ai/console/coding-plan)
|
||||
A full-modal AI inference platform that gives developers a unified AI API, supporting video generation, image generation, and LLMs. Connect once to access **300+ curated models**.
|
||||
|
||||
<a href="https://www.volcengine.com/activity/codingplan?utm_campaign=hw&utm_content=hw&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ageerle-ruoyi-ai">
|
||||
<img src="docs/image/sponsor/huoshan.png" alt="火山引擎 CodingPlan" width="160" height="80">
|
||||
<img src="docs/image/sponsor/huoshan.png" alt="Volcengine CodingPlan" width="160" height="80">
|
||||
</a>
|
||||
|
||||
[注册即领2500万Tokens,立即前往](https://www.volcengine.com/activity/ai618?utm_campaign=hw&utm_content=hw&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ageerle-ruoyi-ai)
|
||||
享字节自研豆包模型+满血版开源 SOTA模型,覆盖文本、VLM、图像生成,全模态一站配齐:Seed-2.1、Seedream-5.0、GLM-5.2、DeepSeek等。不止编程、更能解决 Agent 复杂长程任务!
|
||||
[Sign up to claim 25 million tokens — go now](https://www.volcengine.com/activity/ai618?utm_campaign=hw&utm_content=hw&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ageerle-ruoyi-ai)
|
||||
Enjoy ByteDance's in-house Doubao models plus full-power open-source SOTA models, covering text, VLM, and image generation — all modalities in one stop: Seed-2.1, Seedream-5.0, GLM-5.2, DeepSeek, and more. Not just for coding — it can also tackle complex long-horizon Agent tasks!
|
||||
|
||||
|
||||
## 💬 社区交流
|
||||
## 💬 Community Chat
|
||||
|
||||
<div align="center">
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="docs/image/wx.png" alt="微信二维码" width="200" height="200"><br>
|
||||
<strong>扫码添加作者微信</strong><br>
|
||||
<em>邀请进群学习</em>
|
||||
<img src="docs/image/wx.png" alt="WeChat QR Code" width="200" height="200"><br>
|
||||
<strong>Scan to add author on WeChat</strong><br>
|
||||
<em>Join group for learning</em>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="docs/image/wx06.png" alt="微信二维码" width="200" height="200"><br>
|
||||
<strong>微信技术交流群</strong><br>
|
||||
<em>技术讨论</em>
|
||||
<img src="docs/image/wx06.png" alt="WeChat QR Code" width="200" height="200"><br>
|
||||
<strong>WeChat Tech Exchange Group</strong><br>
|
||||
<em>Technical discussion</em>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="docs/image/qq.png" alt="QQ群二维码" width="200" height="200"><br>
|
||||
<strong>QQ技术交流群</strong><br>
|
||||
<em>技术讨论</em>
|
||||
<img src="docs/image/qq.png" alt="QQ Group QR Code" width="200" height="200"><br>
|
||||
<strong>QQ Tech Exchange Group</strong><br>
|
||||
<em>Technical discussion</em>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
@@ -255,11 +250,12 @@ docker-compose -f docker-compose-all.yaml restart [服务名]
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
**[⭐ 点个Star支持一下](https://github.com/ageerle/ruoyi-ai)** • **[ Fork 开始贡献](https://github.com/ageerle/ruoyi-ai/fork)** • **[📚 English](README_EN.md)** • **[📖 查看完整文档](https://doc.ruoyiai.chat/)**
|
||||
**[⭐ Star to Support](https://github.com/ageerle/ruoyi-ai)** • **[Fork to Contribute](https://github.com/ageerle/ruoyi-ai/fork)** • **[📚 中文](README_ZH.md)** • **[📖 Complete Documentation](https://doc.ruoyiai.chat/)**
|
||||
|
||||
*用 ❤️ 打造,由 RuoYi AI 开源社区维护*
|
||||
*Built with ❤️, maintained by the RuoYi AI open-source community*
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
310
README_EN.md
310
README_EN.md
@@ -1,310 +0,0 @@
|
||||
|
||||
# RuoYi AI
|
||||
|
||||
<div align="center">
|
||||
|
||||
[![Contributors][contributors-shield]][contributors-url]
|
||||
[![Forks][forks-shield]][forks-url]
|
||||
[![Stargazers][stars-shield]][stars-url]
|
||||
[![Issues][issues-shield]][issues-url]
|
||||
[![MIT License][license-shield]][license-url]
|
||||
|
||||
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/13209">
|
||||
<img src="https://trendshift.io/api/badge/repositories/13209" alt="GitHub Trending">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<img src="docs/image/logo.png" alt="RuoYi AI Logo" width="120" height="120">
|
||||
|
||||
### Enterprise-Grade AI Assistant Platform
|
||||
|
||||
*An out-of-the-box full-stack AI platform supporting multi-agent collaboration, Supervisor mode orchestration, and multiple decision models, with advanced RAG technology and visual workflow orchestration capabilities*
|
||||
|
||||
**[中文](README.md)** | **[📖 Documentation](https://doc.ruoyiai.chat/)** |
|
||||
**[🚀 Live Demo](https://web.ruoyiai.chat/)** | **[🐛 Report Issues](https://github.com/ageerle/ruoyi-ai/issues)** | **[💡 Feature Requests](https://github.com/ageerle/ruoyi-ai/issues)**
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
## ✨ Core Features
|
||||
|
||||
| Module | Current Capabilities |
|
||||
|:---:|---|
|
||||
| **Model Management** | Multi-model integration (OpenAI/DeepSeek/Tongyi/Zhipu/MiniMax), multi-modal understanding, Coze/DIFY/FastGPT platform integration |
|
||||
| **Knowledge Base** | Local RAG + Vector DB (Milvus/Weaviate/Qdrant) + Document parsing |
|
||||
| **Tool Management** | MCP protocol integration, Skills capability + Extensible tool ecosystem |
|
||||
| **Workflow Orchestration** | Visual workflow designer, drag-and-drop node orchestration, SSE streaming execution, currently supports model calls, email sending, manual review nodes |
|
||||
| **Multi-Agent** | Agent framework based on Langchain4j, Supervisor mode orchestration, supports multiple decision models |
|
||||
|
||||
### Project Repositories
|
||||
|
||||
| Module | GitHub Repository | Gitee Repository | GitCode Repository |
|
||||
|----------|-------------------------------------------------------|------------------------------------------------------|--------------------------------------------------------|
|
||||
| 🔧 Backend | [ruoyi-ai](https://github.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitee.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitcode.com/ageerle/ruoyi-ai) |
|
||||
| 🎨 User Frontend | [ruoyi-web](https://github.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitee.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitcode.com/ageerle/ruoyi-web) |
|
||||
| 🛠️ Admin Panel | [ruoyi-admin](https://github.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitee.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitcode.com/ageerle/ruoyi-admin) |
|
||||
| 🎬 Drama | [ruoyi-drama](https://github.com/ageerle/ruoyi-drama) | [ruoyi-drama](https://gitee.com/ageerle/ruoyi-drama) | |
|
||||
| 🤖 Copilot | [ruoyi-copilot](https://github.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitee.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitcode.com/ageerle/ruoyi-copilot) |
|
||||
| 📱 Mini-App | [ruoyi-uniapp](https://github.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitee.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitcode.com/ageerle/ruoyi-uniapp) |
|
||||
|
||||
### Partner Projects
|
||||
| Project Name | GitHub Repository | Gitee Repository |
|
||||
|----------------|-------------------------------------------------------|------------------------------------------------------|
|
||||
| element-plus-x | [element-plus-x](https://github.com/element-plus-x/Element-Plus-X) | [element-plus-x](https://gitee.com/he-jiayue/element-plus-x) |
|
||||
|
||||
## 🛠️ Technical Architecture
|
||||
|
||||
### Core Framework
|
||||
- **Backend**: Spring Boot 3.5.8 + Langchain4j
|
||||
- **Data Storage**: MySQL 8.0 + Redis + Vector Databases (Milvus/Weaviate/Qdrant)
|
||||
- **Frontend**: Vue 3 + Vben Admin + element-plus-x
|
||||
- **Security**: Sa-Token + JWT dual-layer security
|
||||
- **Document Processing**: PDF, Word, Excel parsing, intelligent image analysis
|
||||
- **Real-time Communication**: WebSocket real-time communication, SSE streaming response
|
||||
- **System Monitoring**: Comprehensive logging system, performance monitoring, service health checks
|
||||
|
||||
## 🐳 Docker Deployment
|
||||
|
||||
This project provides two Docker deployment methods:
|
||||
|
||||
### Method 1: One-click Start All Services (Recommended)
|
||||
|
||||
Use `docker-compose-all.yaml` to start all services at once (including backend, admin panel, user frontend, and dependencies):
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/ageerle/ruoyi-ai.git
|
||||
cd ruoyi-ai
|
||||
|
||||
# Start all services (pull pre-built images from registry)
|
||||
docker-compose -f docker-compose-all.yaml up -d
|
||||
|
||||
# Check service status
|
||||
docker-compose -f docker-compose-all.yaml ps
|
||||
|
||||
# Access services
|
||||
# Admin Panel: http://localhost:25666 (admin / admin123)
|
||||
# User Frontend: http://localhost:25137
|
||||
# Backend API: http://localhost:26039
|
||||
```
|
||||
|
||||
### Method 2: Step-by-step Deployment (Source Build)
|
||||
|
||||
If you need to build backend services from source, follow these steps:
|
||||
|
||||
#### Step 1: Deploy Backend Service
|
||||
|
||||
```bash
|
||||
# Enter backend project directory
|
||||
cd ruoyi-ai
|
||||
|
||||
# Start backend service (build from source)
|
||||
docker-compose up -d --build
|
||||
|
||||
# Wait for backend service to start
|
||||
docker-compose logs -f backend
|
||||
```
|
||||
|
||||
#### Step 2: Deploy Admin Panel
|
||||
|
||||
```bash
|
||||
# Enter admin panel project directory
|
||||
cd ruoyi-admin
|
||||
|
||||
# Build and start admin panel
|
||||
docker-compose up -d --build
|
||||
|
||||
# Access admin panel
|
||||
# URL: http://localhost:5666
|
||||
```
|
||||
|
||||
#### Step 3: Deploy User Frontend (Optional)
|
||||
|
||||
```bash
|
||||
# Enter user frontend project directory
|
||||
cd ruoyi-web
|
||||
|
||||
# Build and start user frontend
|
||||
docker-compose up -d --build
|
||||
|
||||
# Access user frontend
|
||||
# URL: http://localhost:5137
|
||||
```
|
||||
|
||||
### Service Ports
|
||||
|
||||
| Service | One-click Port | Step-by-step Port | Description |
|
||||
|------|-------------|-------------|------|
|
||||
| Admin Panel | 25666 | 5666 | Admin backend access |
|
||||
| User Frontend | 25137 | 5137 | User frontend access |
|
||||
| Backend Service | 26039 | 6039 | Backend API service |
|
||||
| MySQL | 23306 | 23306 | Database service |
|
||||
| Redis | 26379 | 6379 | Cache service |
|
||||
| Weaviate | 28080 | 28080 | Vector database |
|
||||
| MinIO API | 29000 | 9000 | Object storage API |
|
||||
| MinIO Console | 29090 | 9090 | Object storage console |
|
||||
|
||||
### Image Registry
|
||||
|
||||
All images are hosted on Alibaba Cloud Container Registry:
|
||||
|
||||
```
|
||||
crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai
|
||||
```
|
||||
|
||||
Available images:
|
||||
- `mysql:v3` - MySQL database (includes initialization SQL)
|
||||
- `redis:6.2` - Redis cache
|
||||
- `weaviate:1.30.0` - Vector database
|
||||
- `minio:latest` - Object storage
|
||||
- `ruoyi-ai-backend:latest` - Backend service
|
||||
- `ruoyi-ai-admin:latest` - Admin frontend
|
||||
- `ruoyi-ai-web:latest` - User frontend
|
||||
|
||||
### Common Commands
|
||||
|
||||
```bash
|
||||
# Stop all services
|
||||
docker-compose -f docker-compose-all.yaml down
|
||||
|
||||
# View service logs
|
||||
docker-compose -f docker-compose-all.yaml logs -f [service-name]
|
||||
|
||||
# Restart a service
|
||||
docker-compose -f docker-compose-all.yaml restart [service-name]
|
||||
```
|
||||
|
||||
### MiniMax Configuration
|
||||
|
||||
The built-in MiniMax provider accepts one API Host value and selects the matching protocol adapter. Use a Base URL from this table:
|
||||
|
||||
| Region | OpenAI-compatible Base URL | Anthropic-compatible Base URL |
|
||||
| --- | --- | --- |
|
||||
| Global | `https://api.minimax.io/v1` | `https://api.minimax.io/anthropic` |
|
||||
| China | `https://api.minimaxi.com/v1` | `https://api.minimaxi.com/anthropic` |
|
||||
|
||||
For Anthropic-compatible requests, configure the Base URL ending in `/anthropic`. Do not append `/v1` or `/v1/messages`; the provider adapter derives the request path internally.
|
||||
|
||||
| Model ID | Total context | Input modalities | Thinking |
|
||||
| --- | ---: | --- | --- |
|
||||
| `MiniMax-M3` | 1,000,000 tokens | Text, image, video | Adaptive or disabled |
|
||||
| `MiniMax-M2.7` | 204,800 tokens | Text | Always on |
|
||||
|
||||
Current pay-as-you-go prices are in USD per million tokens:
|
||||
|
||||
| Model | Service tier and input range | Input | Output | Cache read | Cache write |
|
||||
| --- | --- | ---: | ---: | ---: | ---: |
|
||||
| `MiniMax-M3` | Standard, up to 512,000 input tokens | $0.30 | $1.20 | $0.06 | Not listed |
|
||||
| `MiniMax-M3` | Standard, over 512,000 input tokens | $0.60 | $2.40 | $0.12 | Not listed |
|
||||
| `MiniMax-M3` | Priority, up to 512,000 input tokens | $0.45 | $1.80 | $0.09 | Not listed |
|
||||
| `MiniMax-M3` | Priority, over 512,000 input tokens | $0.90 | $3.60 | $0.18 | Not listed |
|
||||
| `MiniMax-M2.7` | Standard | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
|
||||
See the [official API overview](https://platform.minimax.io/docs/api-reference/api-overview) and [pay-as-you-go pricing](https://platform.minimax.io/docs/guides/pricing-paygo) for current details.
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
Want to learn more about installation, deployment, configuration, and secondary development?
|
||||
|
||||
**👉 [Complete Documentation](https://doc.ruoyiai.chat/)**
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We warmly welcome community contributions! Whether you are a seasoned developer or just getting started, you can contribute to the project 💪
|
||||
|
||||
### How to Contribute
|
||||
|
||||
1. **Fork** the project to your account
|
||||
2. **Create a branch** (`git checkout -b feature/new-feature-name`)
|
||||
3. **Commit your changes** (`git commit -m 'Add new feature'`)
|
||||
4. **Push to the branch** (`git push origin feature/new-feature-name`)
|
||||
5. **Create a Pull Request**
|
||||
|
||||
> 💡 **Tip**: We recommend submitting PRs to GitHub, we will automatically sync to other code hosting platforms
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
Thanks to the following excellent open-source projects for their support:
|
||||
- [Langchain4j](https://github.com/langchain4j/langchain4j) - Powerful Java LLM development framework
|
||||
- [RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus) - Mature enterprise-level rapid development framework
|
||||
- [Vben Admin](https://github.com/vbenjs/vue-vben-admin) - Modern Vue admin template
|
||||
|
||||
## 💎 Sponsors
|
||||
|
||||
**Thanks to the following sponsors for supporting this project:**
|
||||
|
||||
<a href="https://www.atlascloud.ai?ref=89F97E">
|
||||
<img src="docs/image/sponsor/atlascloud_banner.png" alt="Atlas Cloud" width="160" height="80">
|
||||
</a>
|
||||
|
||||
[Visit Atlas Cloud](https://www.atlascloud.ai?ref=89F97E) · [Coding Plan Promotion](https://www.atlascloud.ai/console/coding-plan)
|
||||
A full-modal AI inference platform that gives developers a unified AI API, supporting video generation, image generation, and LLMs. Connect once to access **300+ curated models**.
|
||||
|
||||
<a href="https://www.volcengine.com/activity/codingplan?utm_campaign=hw&utm_content=hw&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ageerle-ruoyi-ai">
|
||||
<img src="docs/image/sponsor/huoshan.png" alt="Volcengine CodingPlan" width="160" height="80">
|
||||
</a>
|
||||
|
||||
[Volcengine CodingPlan Developer Program](https://www.volcengine.com/activity/codingplan?utm_campaign=hw&utm_content=hw&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ageerle-ruoyi-ai)
|
||||
Volcengine is ByteDance's cloud and AI service platform. Volcengine Ark provides API access to Doubao LLM, DeepSeek, and more — a one-stop AI development and inference platform for developers.
|
||||
|
||||
## 💬 Community Chat
|
||||
|
||||
<div align="center">
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="docs/image/wx.png" alt="WeChat QR Code" width="200" height="200"><br>
|
||||
<strong>Scan to add author on WeChat</strong><br>
|
||||
<em>Join group for learning</em>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="docs/image/qq.png" alt="QQ Group QR Code" width="200" height="200"><br>
|
||||
<strong>QQ Tech Exchange Group</strong><br>
|
||||
<em>Technical discussion</em>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
**[⭐ Star to Support](https://github.com/ageerle/ruoyi-ai)** • **[Fork to Contribute](https://github.com/ageerle/ruoyi-ai/fork)** • **[📚 中文](README.md)** • **[📖 Complete Documentation](https://doc.ruoyiai.chat/)**
|
||||
|
||||
*Built with ❤️, maintained by the RuoYi AI open-source community*
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Badge Links -->
|
||||
|
||||
[contributors-shield]: https://img.shields.io/github/contributors/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[contributors-url]: https://github.com/ageerle/ruoyi-ai/graphs/contributors
|
||||
|
||||
[forks-shield]: https://img.shields.io/github/forks/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[forks-url]: https://github.com/ageerle/ruoyi-ai/network/members
|
||||
|
||||
[stars-shield]: https://img.shields.io/github/stars/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[stars-url]: https://github.com/ageerle/ruoyi-ai/stargazers
|
||||
|
||||
[issues-shield]: https://img.shields.io/github/issues/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[issues-url]: https://github.com/ageerle/ruoyi-ai/issues
|
||||
|
||||
[license-shield]: https://img.shields.io/github/license/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[license-url]: https://github.com/ageerle/ruoyi-ai/blob/main/LICENSE
|
||||
282
README_ZH.md
Normal file
282
README_ZH.md
Normal file
@@ -0,0 +1,282 @@
|
||||
# RuoYi AI
|
||||
|
||||
<div align="center">
|
||||
|
||||
[![Contributors][contributors-shield]][contributors-url] [![Forks][forks-shield]][forks-url] [![Stargazers][stars-shield]][stars-url] [![Issues][issues-shield]][issues-url] [![MIT License][license-shield]][license-url]
|
||||
|
||||
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/13209">
|
||||
<img src="https://trendshift.io/api/badge/repositories/13209" alt="GitHub Trending">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<img src="docs/image/logo.png" alt="RuoYi AI Logo" width="120" height="120">
|
||||
|
||||
### 企业级AI助手平台
|
||||
|
||||
*开箱即用的全栈AI平台,支持多智能体协同、Supervisor模式编排、多种决策模式、RAG技术和流程编排能力*
|
||||
|
||||
**[English](README.md)** | **[📖 使用文档](https://doc.ruoyiai.chat/)** |
|
||||
**[🚀 在线体验](https://web.ruoyiai.chat/)** | **[🐛 问题反馈](https://github.com/ageerle/ruoyi-ai/issues)** | **[💡 功能建议](https://github.com/ageerle/ruoyi-ai/issues)**
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
## ✨ 核心亮点
|
||||
|
||||
| 模块 | 现有能力
|
||||
|:---------:|---
|
||||
| **模型管理** | 多模型接入(DeepSeek/智谱/MIMO/百炼/OpenAI)、多模态理解、Coze/DIFY/FastGPT/RAGFlow平台集成
|
||||
| **知识管理** | 本地RAG + 向量库(Milvus/Weaviate/Qdrant) + 文档解析
|
||||
| **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态
|
||||
| **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点
|
||||
| **智能体管理** | 基于Langchain4j的Agent框架、Supervisor模式编排,支持多种决策模型,可以灵活搭配工具,skills
|
||||
|
||||
|
||||
### 项目源码
|
||||
|
||||
| 项目模块 | GitHub 仓库 | Gitee 仓库 | GitCode 仓库 |
|
||||
|----------|-------------------------------------------------------|------------------------------------------------------|--------------------------------------------------------|
|
||||
| 🔧 后端服务 | [ruoyi-ai](https://github.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitee.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitcode.com/ageerle/ruoyi-ai) |
|
||||
| 🎨 用户前端 | [ruoyi-web](https://github.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitee.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitcode.com/ageerle/ruoyi-web) |
|
||||
| 🛠️ 管理后台 | [ruoyi-admin](https://github.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitee.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitcode.com/ageerle/ruoyi-admin) |
|
||||
| 🎬 短剧平台 | [ruoyi-drama](https://github.com/ageerle/ruoyi-drama) | [ruoyi-drama](https://gitee.com/ageerle/ruoyi-drama) | [ruoyi-drama](https://gitcode.com/ageerle/ruoyi-drama) |
|
||||
| 🤖 编程助手 | [ruoyi-copilot](https://github.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitee.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitcode.com/ageerle/ruoyi-copilot) |
|
||||
| 📱 小程序端 | [ruoyi-uniapp](https://github.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitee.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitcode.com/ageerle/ruoyi-uniapp) |
|
||||
|
||||
### 合作项目
|
||||
| 项目名称 | GitHub 仓库 | Gitee 仓库
|
||||
|----------------|-------------------------------------------------------|------------------------------------------------------|
|
||||
| element-plus-x | [element-plus-x](https://github.com/element-plus-x/Element-Plus-X) | [element-plus-x](https://gitee.com/he-jiayue/element-plus-x) |
|
||||
|
||||
## 🛠️ 技术架构
|
||||
|
||||
### 核心框架
|
||||
- **后端架构**:Spring Boot 3.5.8 + Langchain4j
|
||||
- **数据存储**:MySQL 8.0 + Redis + 向量数据库(Milvus/Weaviate/Qdrant)
|
||||
- **前端技术**:Vue 3 + Vben Admin + element-plus-x
|
||||
- **安全认证**:Sa-Token + JWT 双重保障
|
||||
- **文档处理**:PDF、Word、Excel 解析,图像智能分析
|
||||
- **实时通信**:WebSocket 实时通信,SSE 流式响应
|
||||
- **系统监控**:完善的日志体系、性能监控、服务健康检查
|
||||
|
||||
## 🐳 Docker 部署
|
||||
|
||||
本项目提供两种 Docker 部署方式:
|
||||
|
||||
### 方式一:一键启动所有服务(推荐)
|
||||
|
||||
使用 `docker-compose-all.yaml` 可以一键启动所有服务(包括后端、管理端、用户端及依赖服务):
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
git clone https://github.com/ageerle/ruoyi-ai.git
|
||||
cd ruoyi-ai
|
||||
|
||||
# 启动所有服务(从 GHCR 拉取预构建镜像)
|
||||
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml up -d
|
||||
|
||||
# 查看服务状态
|
||||
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml ps
|
||||
|
||||
# 访问服务
|
||||
# 管理端: http://localhost:25666 (admin / admin123)
|
||||
# 用户端: http://localhost:25137
|
||||
# 后端API: http://localhost:26039
|
||||
```
|
||||
|
||||
### 方式二:分步部署(源码编译)
|
||||
|
||||
如果您需要从源码构建后端服务,请按照以下步骤操作:
|
||||
|
||||
#### 第一步:部署后端服务
|
||||
|
||||
```bash
|
||||
# 进入后端项目目录
|
||||
cd ruoyi-ai
|
||||
|
||||
# 启动后端服务(源码编译构建)
|
||||
docker-compose up -d --build
|
||||
|
||||
# 等待后端服务启动完成
|
||||
docker-compose logs -f backend
|
||||
```
|
||||
|
||||
#### 第二步:部署管理端
|
||||
|
||||
```bash
|
||||
# 进入管理端项目目录
|
||||
cd ruoyi-admin
|
||||
|
||||
# 构建并启动管理端
|
||||
docker-compose up -d --build
|
||||
|
||||
# 访问管理端
|
||||
# 地址: http://localhost:5666
|
||||
```
|
||||
|
||||
#### 第三步:部署用户端(可选)
|
||||
|
||||
```bash
|
||||
# 进入用户端项目目录
|
||||
cd ruoyi-web
|
||||
|
||||
# 构建并启动用户端
|
||||
docker-compose up -d --build
|
||||
|
||||
# 访问用户端
|
||||
# 地址: http://localhost:5137
|
||||
```
|
||||
|
||||
### 服务端口说明
|
||||
|
||||
| 服务 | 一键启动端口 | 分步部署端口 | 说明 |
|
||||
|------|-------------|-------------|------|
|
||||
| 管理端 | 25666 | 5666 | 管理后台访问地址 |
|
||||
| 用户端 | 25137 | 5137 | 用户前端访问地址 |
|
||||
| 后端服务 | 26039 | 6039 | 后端 API 服务 |
|
||||
| MySQL | 23306 | 23306 | 数据库服务 |
|
||||
| Redis | 26379 | 6379 | 缓存服务 |
|
||||
| Weaviate | 28080 | 28080 | 向量数据库 |
|
||||
| MinIO API | 29000 | 9000 | 对象存储 API |
|
||||
| MinIO Console | 29090 | 9090 | 对象存储控制台 |
|
||||
|
||||
### 镜像仓库
|
||||
|
||||
每次推送类似 `v3.1.0` 的版本标签后,GitHub Actions 会自动构建并发布应用镜像到 GitHub Container Registry(GHCR):
|
||||
|
||||
```
|
||||
ghcr.io/ageerle/ruoyi-ai-backend:v3.1.0
|
||||
ghcr.io/ageerle/ruoyi-ai-mysql:v3.1.0
|
||||
ghcr.io/ageerle/ruoyi-ai-admin:v3.1.0
|
||||
ghcr.io/ageerle/ruoyi-ai-web:v3.1.0
|
||||
```
|
||||
|
||||
Compose 默认使用 `latest`。如需固定版本,可在 `docs/docker/ruoyi-ai/.env` 中设置 `RUIYI_VERSION=v3.1.0`。
|
||||
|
||||
首次工作流运行完成后,请在 GitHub 的 Packages 设置中将这四个 GHCR 镜像设为 `Public`,用户服务器才能免登录拉取。
|
||||
|
||||
管理端 `ageerle/ruoyi-admin` 和用户端 `ageerle/ruoyi-web` 也必须存在同名版本标签;发布工作流会使用后端发布标签检出这两个仓库后再构建镜像。
|
||||
|
||||
### 常用命令
|
||||
|
||||
```bash
|
||||
# 停止所有服务
|
||||
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml down
|
||||
|
||||
# 查看服务日志
|
||||
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml logs -f [服务名]
|
||||
|
||||
# 重启某个服务
|
||||
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml restart [服务名]
|
||||
```
|
||||
|
||||
## 📚 使用文档
|
||||
|
||||
想要深入了解安装部署、功能配置和二次开发?
|
||||
|
||||
**👉 [完整使用文档](https://doc.ruoyiai.chat/)**
|
||||
|
||||
## 🤝 参与贡献
|
||||
|
||||
我们热烈欢迎社区贡献!无论您是资深开发者还是初学者,都可以为项目贡献力量 💪
|
||||
|
||||
### 贡献方式
|
||||
|
||||
1. **Fork** 项目到您的账户
|
||||
2. **创建分支** (`git checkout -b feature/新功能名称`)
|
||||
3. **提交代码** (`git commit -m '添加某某功能'`)
|
||||
4. **推送分支** (`git push origin feature/新功能名称`)
|
||||
5. **发起 Pull Request**
|
||||
|
||||
> 💡 **小贴士**:建议将 PR 提交到 GitHub,我们会自动同步到其他代码托管平台
|
||||
|
||||
## 📄 开源协议
|
||||
|
||||
本项目采用 **MIT 开源协议**,详情请查看 [LICENSE](LICENSE) 文件。
|
||||
|
||||
## 🙏 特别鸣谢
|
||||
|
||||
感谢以下优秀的开源项目为本项目提供支持:
|
||||
- [Langchain4j](https://github.com/langchain4j/langchain4j) - 强大的 Java LLM 开发框架
|
||||
- [RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus) - 成熟的企业级快速开发框架
|
||||
- [Vben Admin](https://github.com/vbenjs/vue-vben-admin) - 现代化的 Vue 后台管理模板
|
||||
|
||||
|
||||
## 💎 赞助商
|
||||
|
||||
**感谢以下赞助商对本项目的支持:**
|
||||
|
||||
<a href="https://www.atlascloud.ai?ref=89F97E">
|
||||
<img src="docs/image/sponsor/atlascloud_banner.png" alt="Atlas Cloud" width="160" height="80">
|
||||
</a>
|
||||
|
||||
[访问Atlas Cloud官网](https://www.atlascloud.ai?ref=89F97E&utm_source=github&utm_campaign=ruoyi-drama) · [编程计划优惠](https://www.atlascloud.ai/console/coding-plan)
|
||||
全模态 AI 推理平台,为开发者提供统一的 AI API,支持视频生成、图像生成和大语言模型。一次接入,即可访问 **300+ 精选模型**。
|
||||
|
||||
<a href="https://www.volcengine.com/activity/codingplan?utm_campaign=hw&utm_content=hw&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ageerle-ruoyi-ai">
|
||||
<img src="docs/image/sponsor/huoshan.png" alt="火山引擎 CodingPlan" width="160" height="80">
|
||||
</a>
|
||||
|
||||
[注册即领2500万Tokens,立即前往](https://www.volcengine.com/activity/ai618?utm_campaign=hw&utm_content=hw&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ageerle-ruoyi-ai)
|
||||
享字节自研豆包模型+满血版开源 SOTA模型,覆盖文本、VLM、图像生成,全模态一站配齐:Seed-2.1、Seedream-5.0、GLM-5.2、DeepSeek等。不止编程、更能解决 Agent 复杂长程任务!
|
||||
|
||||
|
||||
## 💬 社区交流
|
||||
|
||||
<div align="center">
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="docs/image/wx.png" alt="微信二维码" width="200" height="200"><br>
|
||||
<strong>扫码添加作者微信</strong><br>
|
||||
<em>邀请进群学习</em>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="docs/image/wx06.png" alt="微信二维码" width="200" height="200"><br>
|
||||
<strong>微信技术交流群</strong><br>
|
||||
<em>技术讨论</em>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="docs/image/qq.png" alt="QQ群二维码" width="200" height="200"><br>
|
||||
<strong>QQ技术交流群</strong><br>
|
||||
<em>技术讨论</em>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
<div align="center">
|
||||
|
||||
**[⭐ 点个Star支持一下](https://github.com/ageerle/ruoyi-ai)** • **[ Fork 开始贡献](https://github.com/ageerle/ruoyi-ai/fork)** • **[📚 English](README.md)** • **[📖 查看完整文档](https://doc.ruoyiai.chat/)**
|
||||
|
||||
*用 ❤️ 打造,由 RuoYi AI 开源社区维护*
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Badge Links -->
|
||||
|
||||
[contributors-shield]: https://img.shields.io/github/contributors/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[contributors-url]: https://github.com/ageerle/ruoyi-ai/graphs/contributors
|
||||
|
||||
[forks-shield]: https://img.shields.io/github/forks/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[forks-url]: https://github.com/ageerle/ruoyi-ai/network/members
|
||||
|
||||
[stars-shield]: https://img.shields.io/github/stars/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[stars-url]: https://github.com/ageerle/ruoyi-ai/stargazers
|
||||
|
||||
[issues-shield]: https://img.shields.io/github/issues/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[issues-url]: https://github.com/ageerle/ruoyi-ai/issues
|
||||
|
||||
[license-shield]: https://img.shields.io/github/license/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[license-url]: https://github.com/ageerle/ruoyi-ai/blob/main/LICENSE
|
||||
6
docs/docker/ruoyi-ai/.env.example
Normal file
6
docs/docker/ruoyi-ai/.env.example
Normal file
@@ -0,0 +1,6 @@
|
||||
# GitHub Container Registry namespace owner.
|
||||
# For the upstream project this is ageerle; change it when using a fork.
|
||||
IMAGE_OWNER=ageerle
|
||||
|
||||
# Use latest for convenience, or pin a release such as v3.1.0.
|
||||
RUIYI_VERSION=latest
|
||||
@@ -10,13 +10,13 @@
|
||||
# - RuoYi-Admin (管理端前端)
|
||||
# - RuoYi-Web (用户端前端)
|
||||
#
|
||||
# 镜像仓库地址: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai
|
||||
# 镜像仓库地址: ghcr.io/ageerle
|
||||
|
||||
services:
|
||||
# ==================== MySQL 数据库 ====================
|
||||
mysql:
|
||||
# 阿里云镜像地址(包含初始化SQL)
|
||||
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/mysql:v3
|
||||
# GHCR 镜像(包含初始化SQL)
|
||||
image: ghcr.io/${IMAGE_OWNER:-ageerle}/ruoyi-ai-mysql:${RUIYI_VERSION:-latest}
|
||||
container_name: ruoyi-ai-mysql
|
||||
restart: always
|
||||
ports:
|
||||
@@ -41,7 +41,7 @@ services:
|
||||
|
||||
# ==================== Redis 缓存 ====================
|
||||
redis:
|
||||
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/redis:6.2
|
||||
image: redis:6.2
|
||||
container_name: ruoyi-ai-redis
|
||||
restart: always
|
||||
ports:
|
||||
@@ -59,7 +59,7 @@ services:
|
||||
|
||||
# ==================== Weaviate 向量数据库 ====================
|
||||
weaviate:
|
||||
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/weaviate:1.30.0
|
||||
image: semitechnologies/weaviate:1.30.0
|
||||
container_name: ruoyi-ai-weaviate
|
||||
restart: always
|
||||
ports:
|
||||
@@ -78,7 +78,7 @@ services:
|
||||
|
||||
# ==================== MinIO 对象存储 ====================
|
||||
minio:
|
||||
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/minio:latest
|
||||
image: minio/minio:latest
|
||||
container_name: ruoyi-ai-minio
|
||||
restart: always
|
||||
ports:
|
||||
@@ -95,7 +95,7 @@ services:
|
||||
|
||||
# ==================== RuoYi-AI 后端服务 ====================
|
||||
backend:
|
||||
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/ruoyi-ai-backend:latest
|
||||
image: ghcr.io/${IMAGE_OWNER:-ageerle}/ruoyi-ai-backend:${RUIYI_VERSION:-latest}
|
||||
container_name: ruoyi-ai-backend
|
||||
restart: always
|
||||
ports:
|
||||
@@ -129,7 +129,7 @@ services:
|
||||
|
||||
# ==================== RuoYi-AI 管理端前端 ====================
|
||||
admin-frontend:
|
||||
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/ruoyi-ai-admin:latest
|
||||
image: ghcr.io/${IMAGE_OWNER:-ageerle}/ruoyi-ai-admin:${RUIYI_VERSION:-latest}
|
||||
container_name: ruoyi-ai-admin
|
||||
restart: always
|
||||
ports:
|
||||
@@ -154,7 +154,7 @@ services:
|
||||
|
||||
# ==================== RuoYi-AI 用户端前端 ====================
|
||||
web-frontend:
|
||||
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/ruoyi-ai-web:latest
|
||||
image: ghcr.io/${IMAGE_OWNER:-ageerle}/ruoyi-ai-web:${RUIYI_VERSION:-latest}
|
||||
container_name: ruoyi-ai-web
|
||||
restart: always
|
||||
ports:
|
||||
|
||||
@@ -2291,11 +2291,10 @@ INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`,
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2027193296990957569, '000000', '文生图节点响应模板', 'node.image.template', '🎨 文生图节点:结束响应 - 图片URL: ', 'Y', 103, 1, '2026-02-27 09:25:20', 1, '2026-02-27 09:31:52', NULL);
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2027193820393959425, '000000', '发送邮箱节点响应模板', 'node.mailsend.template', '📧 发送邮箱节点:结束响应 - ', 'Y', 103, 1, '2026-02-27 09:27:25', 1, '2026-02-27 09:32:05', NULL);
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2027194134438277122, '000000', '结束节点响应模板', 'node.end.template', '🔚 流程已执行完毕,如果您有其他需求,请随时重新发起请求。', 'Y', 103, 1, '2026-02-27 09:28:40', 1, '2026-02-27 09:32:53', NULL);
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2027206492573335554, '000000', '人机交互节点响应模板', 'node.humanFeedback.template', '👤 人机交互节点:等待用户操作 - ', 'Y', 103, 1, '2026-02-27 10:17:46', 1, '2026-02-27 10:17:46', NULL);
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2027208880369647617, '000000', '条件分支节点响应模板', 'node.switch.template', '🔀 条件分支节点:触发 -> 跳转到节点 ', 'Y', 103, 1, '2026-02-27 10:27:15', 1, '2026-02-27 10:35:54', NULL);
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2027213914603995137, '000000', '大模型回答节点响应模板', 'node.llmAnswer.template', '🤖 LLM 节点 生成回答:', 'Y', 103, 1, '2026-02-27 10:47:16', 1, '2026-02-27 10:52:40', NULL);
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2027214387000066050, '000000', '关键词提取响应模板', 'node.keywordExtractor.template', '🔑 关键词提取节点 处理完成 : ', 'Y', 103, 1, '2026-02-27 10:49:08', 1, '2026-02-27 10:52:08', NULL);
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2027217577397391361, '000000', '工作流异常响应模板', 'node.exception.template', '🛑 工作流发生异常:', 'N', 103, 1, '2026-02-27 11:01:49', 1, '2026-02-27 11:02:01', NULL);
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2084157200000000003, '000000', '网络搜索节点响应模板', 'node.googleSearch.template', '🔍 网络搜索节点处理完成:', 'Y', 103, 1, '2026-07-29 19:40:00', 1, '2026-07-29 19:40:00', NULL);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sys_dept
|
||||
@@ -3417,7 +3416,7 @@ CREATE TABLE `t_workflow_component` (
|
||||
`tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '000000' COMMENT '租户编号',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_display_order`(`display_order` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 37 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '工作流组件库 | Workflow Component' ROW_FORMAT = DYNAMIC;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 38 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '工作流组件库 | Workflow Component' ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of t_workflow_component
|
||||
@@ -3425,9 +3424,8 @@ CREATE TABLE `t_workflow_component` (
|
||||
INSERT INTO `t_workflow_component` VALUES (17, '5cd68dccbbb411f0bb7840c2ba9a7fbc', 'Start', '开始', '流程由此开始', 0, 1, '2025-11-07 16:32:49', '2025-11-07 16:32:49', 0, '000000');
|
||||
INSERT INTO `t_workflow_component` VALUES (18, '5cd6ac69bbb411f0bb7840c2ba9a7fbc', 'End', '结束', '流程由此结束', 0, 1, '2025-11-07 16:32:49', '2025-11-07 16:32:49', 0, '000000');
|
||||
INSERT INTO `t_workflow_component` VALUES (19, '5cd6c8eabbb411f0bb7840c2ba9a7fbc', 'Answer', '生成回答', '调用大语言模型回答问题', 0, 1, '2025-11-07 16:32:49', '2025-11-07 16:32:49', 0, '000000');
|
||||
INSERT INTO `t_workflow_component` VALUES (25, '0b4369bb60dc46d6bd84ceb4e36184dc', 'KeywordExtractor', '关键词提取', '从文本中提取关键词', 0, 1, '2025-12-26 16:30:05', '2025-12-26 16:30:05', 0, '000000');
|
||||
INSERT INTO `t_workflow_component` VALUES (26, 'bb00fc2f52c74fec82ee3f99725b56bb', 'Switcher', '条件分支', '根据条件执行不同分支', 0, 1, '2025-12-26 16:30:46', '2025-12-26 16:30:46', 0, '000000');
|
||||
INSERT INTO `t_workflow_component` VALUES (36, 'f37dbcb8f0d5464d90fbb22774490a56', 'HumanFeedback', '人类', '人机沟通', 0, 1, '2025-12-30 17:37:14', '2025-12-30 17:37:14', 0, '000000');
|
||||
INSERT INTO `t_workflow_component` VALUES (37, 'a7f8c2d44e5b4c83a9d6f103c2b47e18', 'Google', '网络搜索', '调用智谱 Web Search 检索互联网信息', 40, 1, '2026-07-29 20:30:00', '2026-07-29 20:30:00', 0, '000000');
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for t_workflow_edge
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-- 补充工作流节点消息模板配置 (对应 issue IJX5VV)
|
||||
-- 背景:NodeMessageTemplateEnum 依赖以下 9 个 sys_config 键,缺失时
|
||||
-- 背景:NodeMessageTemplateEnum 依赖以下 7 个 sys_config 键,缺失时
|
||||
-- WorkflowMessageUtil.getNodeMessageTemplate 会抛出「请先配置该节点的响应模板」。
|
||||
-- 这批配置在历史提交 20d531c0 中存在,SQL 脚本合并重命名时遗失,此处恢复。
|
||||
-- 幂等:按 config_key + tenant_id 判重,可重复执行。
|
||||
@@ -20,10 +20,6 @@ INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`,
|
||||
SELECT 2027194134438277122, '000000', '结束节点响应模板', 'node.end.template', '🔚 流程已执行完毕,如果您有其他需求,请随时重新发起请求。', 'Y', 103, 1, '2026-02-27 09:28:40', 1, '2026-02-27 09:32:53', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.end.template' AND `tenant_id` = '000000');
|
||||
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||
SELECT 2027206492573335554, '000000', '人机交互节点响应模板', 'node.humanFeedback.template', '👤 人机交互节点:等待用户操作 - ', 'Y', 103, 1, '2026-02-27 10:17:46', 1, '2026-02-27 10:17:46', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.humanFeedback.template' AND `tenant_id` = '000000');
|
||||
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||
SELECT 2027208880369647617, '000000', '条件分支节点响应模板', 'node.switch.template', '🔀 条件分支节点:触发 -> 跳转到节点 ', 'Y', 103, 1, '2026-02-27 10:27:15', 1, '2026-02-27 10:35:54', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.switch.template' AND `tenant_id` = '000000');
|
||||
@@ -32,10 +28,6 @@ INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`,
|
||||
SELECT 2027213914603995137, '000000', '大模型回答节点响应模板', 'node.llmAnswer.template', '🤖 LLM 节点 生成回答:', 'Y', 103, 1, '2026-02-27 10:47:16', 1, '2026-02-27 10:52:40', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.llmAnswer.template' AND `tenant_id` = '000000');
|
||||
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||
SELECT 2027214387000066050, '000000', '关键词提取响应模板', 'node.keywordExtractor.template', '🔑 关键词提取节点 处理完成 : ', 'Y', 103, 1, '2026-02-27 10:49:08', 1, '2026-02-27 10:52:08', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.keywordExtractor.template' AND `tenant_id` = '000000');
|
||||
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||
SELECT 2027217577397391361, '000000', '工作流异常响应模板', 'node.exception.template', '🛑 工作流发生异常:', 'N', 103, 1, '2026-02-27 11:01:49', 1, '2026-02-27 11:02:01', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.exception.template' AND `tenant_id` = '000000');
|
||||
|
||||
@@ -0,0 +1,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'
|
||||
);
|
||||
128
docs/script/sql/update/update-0615-trace.sql
Normal file
128
docs/script/sql/update/update-0615-trace.sql
Normal file
@@ -0,0 +1,128 @@
|
||||
-- 链路追踪运行记录表
|
||||
CREATE TABLE IF NOT EXISTS `trace_run` (
|
||||
`id` bigint NOT NULL COMMENT '主键',
|
||||
`trace_id` varchar(64) NOT NULL COMMENT '链路ID',
|
||||
`trace_name` varchar(128) NOT NULL COMMENT '链路名称',
|
||||
`business_type` varchar(64) NOT NULL COMMENT '业务类型',
|
||||
`business_id` varchar(128) DEFAULT NULL COMMENT '业务ID',
|
||||
`user_id` bigint DEFAULT NULL COMMENT '用户ID',
|
||||
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
|
||||
`status` varchar(32) NOT NULL COMMENT '状态',
|
||||
`start_time` datetime NOT NULL COMMENT '开始时间',
|
||||
`end_time` datetime DEFAULT NULL COMMENT '结束时间',
|
||||
`duration_ms` bigint DEFAULT NULL COMMENT '耗时毫秒',
|
||||
`error_message` varchar(1000) DEFAULT NULL COMMENT '错误摘要',
|
||||
`metadata` text DEFAULT NULL COMMENT '元数据JSON',
|
||||
`create_dept` bigint DEFAULT NULL COMMENT '创建部门',
|
||||
`create_by` bigint DEFAULT NULL COMMENT '创建者',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_by` bigint DEFAULT NULL COMMENT '更新者',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE KEY `uk_trace_run_trace_id` (`trace_id`) USING BTREE,
|
||||
KEY `idx_trace_run_business` (`business_type`, `business_id`) USING BTREE,
|
||||
KEY `idx_trace_run_status_time` (`status`, `start_time`) USING BTREE,
|
||||
KEY `idx_trace_run_tenant_time` (`tenant_id`, `start_time`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='链路追踪运行记录表' ROW_FORMAT=DYNAMIC;
|
||||
|
||||
-- 链路追踪节点记录表
|
||||
CREATE TABLE IF NOT EXISTS `trace_node` (
|
||||
`id` bigint NOT NULL COMMENT '主键',
|
||||
`trace_id` varchar(64) NOT NULL COMMENT '链路ID',
|
||||
`node_id` varchar(64) NOT NULL COMMENT '节点ID',
|
||||
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
|
||||
`parent_node_id` varchar(64) DEFAULT NULL COMMENT '父节点ID',
|
||||
`node_name` varchar(128) NOT NULL COMMENT '节点名称',
|
||||
`node_type` varchar(64) NOT NULL COMMENT '节点类型',
|
||||
`depth` int DEFAULT 0 COMMENT '节点深度',
|
||||
`sort_order` int DEFAULT 0 COMMENT '排序',
|
||||
`class_name` varchar(255) DEFAULT NULL COMMENT '类名',
|
||||
`method_name` varchar(128) DEFAULT NULL COMMENT '方法名',
|
||||
`status` varchar(32) NOT NULL COMMENT '状态',
|
||||
`start_time` datetime NOT NULL COMMENT '开始时间',
|
||||
`end_time` datetime DEFAULT NULL COMMENT '结束时间',
|
||||
`duration_ms` bigint DEFAULT NULL COMMENT '耗时毫秒',
|
||||
`error_message` varchar(1000) DEFAULT NULL COMMENT '错误摘要',
|
||||
`input_payload` text DEFAULT NULL COMMENT '输入JSON',
|
||||
`output_payload` text DEFAULT NULL COMMENT '输出JSON',
|
||||
`metadata` text DEFAULT NULL COMMENT '元数据JSON',
|
||||
`create_dept` bigint DEFAULT NULL COMMENT '创建部门',
|
||||
`create_by` bigint DEFAULT NULL COMMENT '创建者',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_by` bigint DEFAULT NULL COMMENT '更新者',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
KEY `idx_trace_node_trace_id` (`trace_id`) USING BTREE,
|
||||
KEY `idx_trace_node_parent` (`trace_id`, `parent_node_id`) USING BTREE,
|
||||
KEY `idx_trace_node_time` (`trace_id`, `start_time`) USING BTREE,
|
||||
KEY `idx_trace_node_tenant_time` (`tenant_id`, `start_time`) USING BTREE,
|
||||
KEY `idx_trace_node_type_status` (`node_type`, `status`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='链路追踪节点记录表' ROW_FORMAT=DYNAMIC;
|
||||
|
||||
-- 兼容已存在的旧 trace 表:CREATE TABLE IF NOT EXISTS 不会给旧表补新字段
|
||||
SET @trace_run_add_tenant_sql = (
|
||||
SELECT IF(COUNT(*) = 0,
|
||||
'ALTER TABLE `trace_run` ADD COLUMN `tenant_id` varchar(20) DEFAULT ''000000'' COMMENT ''租户编号'' AFTER `user_id`',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'trace_run'
|
||||
AND COLUMN_NAME = 'tenant_id'
|
||||
);
|
||||
PREPARE trace_run_add_tenant_stmt FROM @trace_run_add_tenant_sql;
|
||||
EXECUTE trace_run_add_tenant_stmt;
|
||||
DEALLOCATE PREPARE trace_run_add_tenant_stmt;
|
||||
|
||||
SET @trace_run_add_tenant_idx_sql = (
|
||||
SELECT IF(COUNT(*) = 0,
|
||||
'ALTER TABLE `trace_run` ADD INDEX `idx_trace_run_tenant_time` (`tenant_id`, `start_time`) USING BTREE',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'trace_run'
|
||||
AND INDEX_NAME = 'idx_trace_run_tenant_time'
|
||||
);
|
||||
PREPARE trace_run_add_tenant_idx_stmt FROM @trace_run_add_tenant_idx_sql;
|
||||
EXECUTE trace_run_add_tenant_idx_stmt;
|
||||
DEALLOCATE PREPARE trace_run_add_tenant_idx_stmt;
|
||||
|
||||
SET @trace_node_add_tenant_sql = (
|
||||
SELECT IF(COUNT(*) = 0,
|
||||
'ALTER TABLE `trace_node` ADD COLUMN `tenant_id` varchar(20) DEFAULT ''000000'' COMMENT ''租户编号'' AFTER `node_id`',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'trace_node'
|
||||
AND COLUMN_NAME = 'tenant_id'
|
||||
);
|
||||
PREPARE trace_node_add_tenant_stmt FROM @trace_node_add_tenant_sql;
|
||||
EXECUTE trace_node_add_tenant_stmt;
|
||||
DEALLOCATE PREPARE trace_node_add_tenant_stmt;
|
||||
|
||||
SET @trace_node_add_tenant_idx_sql = (
|
||||
SELECT IF(COUNT(*) = 0,
|
||||
'ALTER TABLE `trace_node` ADD INDEX `idx_trace_node_tenant_time` (`tenant_id`, `start_time`) USING BTREE',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'trace_node'
|
||||
AND INDEX_NAME = 'idx_trace_node_tenant_time'
|
||||
);
|
||||
PREPARE trace_node_add_tenant_idx_stmt FROM @trace_node_add_tenant_idx_sql;
|
||||
EXECUTE trace_node_add_tenant_idx_stmt;
|
||||
DEALLOCATE PREPARE trace_node_add_tenant_idx_stmt;
|
||||
|
||||
-- 链路追踪监控菜单 & 按钮权限
|
||||
INSERT INTO `sys_menu`
|
||||
(`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query_param`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_dept`, `create_by`, `create_time`, `remark`)
|
||||
SELECT (SELECT COALESCE(MAX(`menu_id`), 0) + 1 FROM (SELECT `menu_id` FROM `sys_menu`) t), '链路追踪', 2, 7, 'trace', 'monitor/trace/index', '', 1, 0, 'C', '0', '0', 'monitor:trace:list', 'tabler:route', 103, 1, NOW(), '链路追踪监控菜单';
|
||||
|
||||
INSERT INTO `sys_menu`
|
||||
(`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query_param`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_dept`, `create_by`, `create_time`, `remark`)
|
||||
SELECT (SELECT COALESCE(MAX(`menu_id`), 0) + 1 FROM (SELECT `menu_id` FROM `sys_menu`) t), '链路追踪查询', (SELECT `menu_id` FROM `sys_menu` WHERE `perms` = 'monitor:trace:list' AND `menu_type` = 'C' LIMIT 1), 1, '#', '', '', 1, 0, 'F', '0', '0', 'monitor:trace:query', '#', 103, 1, NOW(), '';
|
||||
16
pom.xml
16
pom.xml
@@ -43,8 +43,6 @@
|
||||
<aws.sdk.version>2.28.22</aws.sdk.version>
|
||||
<!-- SMS 配置 -->
|
||||
<sms4j.version>3.3.5</sms4j.version>
|
||||
<!-- 限制框架中的fastjson版本 -->
|
||||
<fastjson.version>1.2.83</fastjson.version>
|
||||
<!-- 面向运行时的D-ORM依赖 -->
|
||||
<anyline.version>8.7.2-20250603</anyline.version>
|
||||
<!-- 工作流配置 -->
|
||||
@@ -63,6 +61,8 @@
|
||||
<weaviate.version>1.19.6</weaviate.version>
|
||||
<dify.version>1.2.7</dify.version>
|
||||
<coze.version>0.4.2</coze.version>
|
||||
<!-- 智谱官方 Java SDK -->
|
||||
<zai-sdk.version>0.3.5</zai-sdk.version>
|
||||
|
||||
<!-- gRPC 版本 - 解决 Milvus SDK 依赖冲突 -->
|
||||
<grpc.version>1.62.2</grpc.version>
|
||||
@@ -347,6 +347,12 @@
|
||||
<groupId>me.zhyd.oauth</groupId>
|
||||
<artifactId>JustAuth</artifactId>
|
||||
<version>${justauth.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- 离线IP地址定位库 ip2region -->
|
||||
@@ -356,12 +362,6 @@
|
||||
<version>${ip2region.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
<version>${fastjson.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-system</artifactId>
|
||||
|
||||
@@ -60,7 +60,7 @@ spring:
|
||||
# rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
|
||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
username: root
|
||||
password: 123456
|
||||
password: root
|
||||
# agent:
|
||||
# url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
# # url: jdbc:mysql://localhost:3306/agent_db
|
||||
|
||||
@@ -146,6 +146,9 @@ tenant:
|
||||
- sys_client
|
||||
- sys_oss_config
|
||||
- flow_spel
|
||||
# 链路追踪监控表:运维需跨租户全局查看,且 trace_node 在异步线程写入、租户上下文不传播,故排除租户过滤
|
||||
- trace_run
|
||||
- trace_node
|
||||
|
||||
# MyBatisPlus配置
|
||||
# https://baomidou.com/config/
|
||||
@@ -232,6 +235,15 @@ xss:
|
||||
excludeUrls:
|
||||
- /system/notice
|
||||
|
||||
--- # 链路追踪配置
|
||||
trace:
|
||||
# 是否启用链路追踪,默认 true
|
||||
# 关闭后所有埋点代码会直接透传业务逻辑,不写库、不创建上下文,零性能开销
|
||||
enabled: true
|
||||
payload:
|
||||
# 错误信息最大字符长度(格式: "异常类名: 异常消息"),超过部分会被截断丢弃
|
||||
max-error-length: 1000
|
||||
|
||||
--- # 分布式锁 lock4j 全局配置
|
||||
lock4j:
|
||||
# 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
@@ -320,6 +332,16 @@ vector-store:
|
||||
api-key:
|
||||
use-tls: false
|
||||
|
||||
# 流程编排扩展节点
|
||||
workflow:
|
||||
web-search:
|
||||
zhipu:
|
||||
# 推荐通过环境变量注入;为空时回退到模型管理中的 zhipu 厂商密钥
|
||||
api-key: ${ZAI_API_KEY:}
|
||||
base-url: ${ZHIPU_WEB_SEARCH_BASE_URL:https://open.bigmodel.cn/api/paas/v4/}
|
||||
connect-timeout: ${ZHIPU_WEB_SEARCH_CONNECT_TIMEOUT:10}
|
||||
read-timeout: ${ZHIPU_WEB_SEARCH_READ_TIMEOUT:30}
|
||||
|
||||
# 短剧成片合成
|
||||
short-drama:
|
||||
composition:
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
<module>ruoyi-common-tenant</module>
|
||||
<module>ruoyi-common-websocket</module>
|
||||
<module>ruoyi-common-sse</module>
|
||||
<module>ruoyi-common-trace</module>
|
||||
</modules>
|
||||
|
||||
<artifactId>ruoyi-common</artifactId>
|
||||
|
||||
@@ -186,6 +186,13 @@
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 链路追踪模块 -->
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-trace</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
||||
@@ -38,8 +38,8 @@
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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.ser.std.ToStringSerializer;
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
@@ -31,7 +30,6 @@ public class ChatRequest {
|
||||
* 智能体ID。传入时后端按智能体配置解析模型/工具/技能/知识库/提示词/是否深度思考。
|
||||
*/
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
@JSONField(serializeUsing = String.class)
|
||||
private Long agentId;
|
||||
|
||||
/**
|
||||
@@ -54,7 +52,6 @@ public class ChatRequest {
|
||||
* 会话id
|
||||
*/
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
@JSONField(serializeUsing = String.class)
|
||||
private Long sessionId;
|
||||
|
||||
/**
|
||||
@@ -71,7 +68,6 @@ public class ChatRequest {
|
||||
* 对话id(每个聊天窗口都不一样)
|
||||
*/
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
@JSONField(serializeUsing = String.class)
|
||||
private Long uuid;
|
||||
|
||||
/**
|
||||
|
||||
@@ -54,7 +54,6 @@ public enum ErrorEnum {
|
||||
A_WF_RUNTIME_NOT_FOUND("A00045", "工作流运行时数据找不到"),
|
||||
A_SEARCH_QUERY_IS_EMPTY("A00046", "搜索内容不能为空"),
|
||||
A_WF_COMPONENT_NOT_FOUND("A00047", "工作流基础组件找不到"),
|
||||
A_WF_RESUME_FAIL("A00048", "工作流恢复执行时失败"),
|
||||
A_MAIL_SENDER_EMPTY("A00049", "邮件发送人不能为空"),
|
||||
A_MAIL_SENDER_CONFIG_ERROR("A00050", "邮件发送人配置错误"),
|
||||
A_MAIL_RECEIVER_EMPTY("A00051", "邮件接收人不能为空"),
|
||||
|
||||
@@ -22,12 +22,4 @@ public interface IWorkFlowStarterService {
|
||||
* @return 流式输出结果
|
||||
*/
|
||||
SseEmitter streaming(User user, String workflowUuid, List<ObjectNode> userInputs, Long sessionId);
|
||||
|
||||
/**
|
||||
* 恢复工作流
|
||||
* @param runtimeUuid 运行时UUID
|
||||
* @param userInput 用户输入
|
||||
* @param sseEmitter SSE连接对象
|
||||
*/
|
||||
void resumeFlow(String runtimeUuid, String userInput, SseEmitter sseEmitter);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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.config.AuthConfig;
|
||||
import me.zhyd.oauth.config.AuthSource;
|
||||
@@ -37,11 +38,11 @@ public abstract class AbstractAuthWeChatEnterpriseRequest extends AuthDefaultReq
|
||||
public AuthToken getAccessToken(AuthCallback authCallback) {
|
||||
String response = doGetAuthorizationCode(accessTokenUrl(null));
|
||||
|
||||
JSONObject object = this.checkResponse(response);
|
||||
JsonNode object = this.checkResponse(response);
|
||||
|
||||
return AuthToken.builder()
|
||||
.accessToken(object.getString("access_token"))
|
||||
.expireIn(object.getIntValue("expires_in"))
|
||||
.accessToken(object.get("access_token").asText())
|
||||
.expireIn(object.get("expires_in").asInt())
|
||||
.code(authCallback.getCode())
|
||||
.build();
|
||||
}
|
||||
@@ -49,26 +50,25 @@ public abstract class AbstractAuthWeChatEnterpriseRequest extends AuthDefaultReq
|
||||
@Override
|
||||
public AuthUser getUserInfo(AuthToken authToken) {
|
||||
String response = doGetUserInfo(authToken);
|
||||
JSONObject object = this.checkResponse(response);
|
||||
JsonNode object = this.checkResponse(response);
|
||||
|
||||
// 返回 OpenId 或其他,均代表非当前企业用户,不支持
|
||||
// https://github.com/justauth/JustAuth/issues/227 修复bug
|
||||
if (!object.containsKey("userid")) {
|
||||
if (!object.has("userid")) {
|
||||
throw new AuthException(AuthResponseStatus.UNIDENTIFIED_PLATFORM, source);
|
||||
}
|
||||
String userId = object.getString("userid");
|
||||
String userTicket = object.getString("user_ticket");
|
||||
JSONObject userDetail = getUserDetail(authToken.getAccessToken(), userId, userTicket);
|
||||
String userId = object.get("userid").asText();
|
||||
String userTicket = object.has("user_ticket") ? object.get("user_ticket").asText() : null;
|
||||
JsonNode userDetail = getUserDetail(authToken.getAccessToken(), userId, userTicket);
|
||||
|
||||
return AuthUser.builder()
|
||||
.rawUserInfo(userDetail)
|
||||
.username(userDetail.getString("name"))
|
||||
.nickname(userDetail.getString("alias"))
|
||||
.avatar(userDetail.getString("avatar"))
|
||||
.location(userDetail.getString("address"))
|
||||
.email(userDetail.getString("email"))
|
||||
.username(userDetail.has("name") ? userDetail.get("name").asText() : null)
|
||||
.nickname(userDetail.has("alias") ? userDetail.get("alias").asText() : null)
|
||||
.avatar(userDetail.has("avatar") ? userDetail.get("avatar").asText() : null)
|
||||
.location(userDetail.has("address") ? userDetail.get("address").asText() : null)
|
||||
.email(userDetail.has("email") ? userDetail.get("email").asText() : null)
|
||||
.uuid(userId)
|
||||
.gender(AuthUserGender.getWechatRealGender(userDetail.getString("gender")))
|
||||
.gender(AuthUserGender.getWechatRealGender(userDetail.has("gender") ? userDetail.get("gender").asText() : null))
|
||||
.token(authToken)
|
||||
.source(source.toString())
|
||||
.build();
|
||||
@@ -78,16 +78,21 @@ public abstract class AbstractAuthWeChatEnterpriseRequest extends AuthDefaultReq
|
||||
* 校验请求结果
|
||||
*
|
||||
* @param response 请求结果
|
||||
* @return 如果请求结果正常,则返回JSONObject
|
||||
* @return 如果请求结果正常,则返回JsonNode
|
||||
*/
|
||||
private JSONObject checkResponse(String response) {
|
||||
JSONObject object = JSONObject.parseObject(response);
|
||||
private JsonNode checkResponse(String response) {
|
||||
try {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
JsonNode object = objectMapper.readTree(response);
|
||||
|
||||
if (object.containsKey("errcode") && object.getIntValue("errcode") != 0) {
|
||||
throw new AuthException(object.getString("errmsg"), source);
|
||||
if (object.has("errcode") && object.get("errcode").asInt() != 0) {
|
||||
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 成员票据,用于获取用户信息或敏感信息
|
||||
* @return 用户详情
|
||||
*/
|
||||
private JSONObject getUserDetail(String accessToken, String userId, String userTicket) {
|
||||
// 用户基础信息
|
||||
String userInfoUrl = UrlBuilder.fromBaseUrl("https://qyapi.weixin.qq.com/cgi-bin/user/get")
|
||||
.queryParam("access_token", accessToken)
|
||||
.queryParam("userid", userId)
|
||||
.build();
|
||||
String userInfoResponse = new HttpUtils(config.getHttpConfig()).get(userInfoUrl).getBody();
|
||||
JSONObject userInfo = checkResponse(userInfoResponse);
|
||||
private JsonNode getUserDetail(String accessToken, String userId, String userTicket) {
|
||||
try {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
// 用户敏感信息
|
||||
if (StringUtils.isNotEmpty(userTicket)) {
|
||||
String userDetailUrl = UrlBuilder.fromBaseUrl("https://qyapi.weixin.qq.com/cgi-bin/auth/getuserdetail")
|
||||
// 用户基础信息
|
||||
String userInfoUrl = UrlBuilder.fromBaseUrl("https://qyapi.weixin.qq.com/cgi-bin/user/get")
|
||||
.queryParam("access_token", accessToken)
|
||||
.queryParam("userid", userId)
|
||||
.build();
|
||||
JSONObject param = new JSONObject();
|
||||
param.put("user_ticket", userTicket);
|
||||
String userDetailResponse = new HttpUtils(config.getHttpConfig()).post(userDetailUrl, param.toJSONString()).getBody();
|
||||
JSONObject userDetail = checkResponse(userDetailResponse);
|
||||
String userInfoResponse = new HttpUtils(config.getHttpConfig()).get(userInfoUrl).getBody();
|
||||
JsonNode userInfo = checkResponse(userInfoResponse);
|
||||
|
||||
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;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.xkcoding.http.support.HttpHeader;
|
||||
import me.zhyd.oauth.cache.AuthStateCache;
|
||||
import me.zhyd.oauth.config.AuthConfig;
|
||||
@@ -52,44 +53,56 @@ public class AuthDingTalkV2Request extends AuthDefaultRequest {
|
||||
|
||||
@Override
|
||||
public AuthToken getAccessToken(AuthCallback authCallback) {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("grantType", "authorization_code");
|
||||
params.put("clientId", config.getClientId());
|
||||
params.put("clientSecret", config.getClientSecret());
|
||||
params.put("code", authCallback.getCode());
|
||||
String response = new HttpUtils(config.getHttpConfig()).post(this.source.accessToken(), JSONObject.toJSONString(params)).getBody();
|
||||
JSONObject accessTokenObject = JSONObject.parseObject(response);
|
||||
if (!accessTokenObject.containsKey("accessToken")) {
|
||||
throw new AuthException(JSONObject.toJSONString(response), source);
|
||||
try {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("grantType", "authorization_code");
|
||||
params.put("clientId", config.getClientId());
|
||||
params.put("clientSecret", config.getClientSecret());
|
||||
params.put("code", authCallback.getCode());
|
||||
|
||||
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
|
||||
public AuthUser getUserInfo(AuthToken authToken) {
|
||||
HttpHeader header = new HttpHeader();
|
||||
header.add("x-acs-dingtalk-access-token", authToken.getAccessToken());
|
||||
try {
|
||||
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();
|
||||
JSONObject object = JSONObject.parseObject(response);
|
||||
String response = new HttpUtils(config.getHttpConfig()).get(this.source.userInfo(), null, header, false).getBody();
|
||||
JsonNode object = objectMapper.readTree(response);
|
||||
|
||||
authToken.setOpenId(object.getString("openId"));
|
||||
authToken.setUnionId(object.getString("unionId"));
|
||||
return AuthUser.builder()
|
||||
.rawUserInfo(object)
|
||||
.uuid(object.getString("unionId"))
|
||||
.username(object.getString("nick"))
|
||||
.nickname(object.getString("nick"))
|
||||
.avatar(object.getString("avatarUrl"))
|
||||
.snapshotUser(object.getBooleanValue("visitor"))
|
||||
.token(authToken)
|
||||
.source(source.toString())
|
||||
.build();
|
||||
authToken.setOpenId(object.has("openId") ? object.get("openId").asText() : null);
|
||||
authToken.setUnionId(object.has("unionId") ? object.get("unionId").asText() : null);
|
||||
return AuthUser.builder()
|
||||
.uuid(object.has("unionId") ? object.get("unionId").asText() : null)
|
||||
.username(object.has("nick") ? object.get("nick").asText() : null)
|
||||
.nickname(object.has("nick") ? object.get("nick").asText() : null)
|
||||
.avatar(object.has("avatarUrl") ? object.get("avatarUrl").asText() : null)
|
||||
.snapshotUser(object.has("visitor") && object.get("visitor").asBoolean())
|
||||
.token(authToken)
|
||||
.source(source.toString())
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
throw new AuthException("获取用户信息失败: " + e.getMessage(), source);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,6 +32,16 @@ public class SseEmitterManager {
|
||||
*/
|
||||
private final static String SSE_TOPIC = "global:sse";
|
||||
|
||||
/**
|
||||
* 按会话维度管理:每个会话一个 SSE 连接,用于对话流式响应
|
||||
* Key: sessionId
|
||||
*/
|
||||
private final static Map<String, SseEmitter> SESSION_EMITTERS = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 按用户维度管理:全局通知、站内信等场景,一个用户可有多个连接(按 token 区分)
|
||||
* Key: userId, Value: token -> SseEmitter
|
||||
*/
|
||||
private final static Map<Long, Map<String, SseEmitter>> USER_TOKEN_EMITTERS = new ConcurrentHashMap<>();
|
||||
|
||||
public SseEmitterManager() {
|
||||
@@ -40,6 +50,98 @@ public class SseEmitterManager {
|
||||
.scheduleWithFixedDelay(this::sseMonitor, 60L, 60L, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
// ======================== 会话维度(对话流式响应) ========================
|
||||
|
||||
/**
|
||||
* 建立与指定会话的 SSE 连接,每个会话仅保留一个连接,重复建连会替换旧连接。
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @return SseEmitter 实例
|
||||
*/
|
||||
public SseEmitter connect(String sessionId) {
|
||||
if (sessionId == null) {
|
||||
throw new IllegalArgumentException("sessionId 不能为空");
|
||||
}
|
||||
// 关闭已存在的 SseEmitter,保证每个会话只有一个活跃连接
|
||||
SseEmitter oldEmitter = SESSION_EMITTERS.remove(sessionId);
|
||||
if (oldEmitter != null) {
|
||||
oldEmitter.complete();
|
||||
}
|
||||
|
||||
SseEmitter emitter = new SseEmitter(86400000L);
|
||||
SESSION_EMITTERS.put(sessionId, emitter);
|
||||
|
||||
emitter.onCompletion(() -> removeSessionEmitter(sessionId, emitter));
|
||||
emitter.onTimeout(() -> removeSessionEmitter(sessionId, emitter));
|
||||
emitter.onError(e -> removeSessionEmitter(sessionId, emitter));
|
||||
|
||||
try {
|
||||
emitter.send(SseEmitter.event().comment("connected"));
|
||||
} catch (IOException e) {
|
||||
SESSION_EMITTERS.remove(sessionId);
|
||||
}
|
||||
return emitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开指定会话的 SSE 连接
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
*/
|
||||
public void disconnect(String sessionId) {
|
||||
if (sessionId == null) {
|
||||
return;
|
||||
}
|
||||
SseEmitter emitter = SESSION_EMITTERS.remove(sessionId);
|
||||
if (emitter != null) {
|
||||
try {
|
||||
emitter.send(SseEmitter.event().comment("disconnected"));
|
||||
} catch (Exception exception) {
|
||||
log.error(exception.getMessage());
|
||||
}
|
||||
emitter.complete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定会话发送结构化事件
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @param eventDto SSE事件对象
|
||||
*/
|
||||
public void sendEvent(String sessionId, SseEventDto eventDto) {
|
||||
if (sessionId == null || eventDto == null) {
|
||||
return;
|
||||
}
|
||||
SseEmitter emitter = SESSION_EMITTERS.get(sessionId);
|
||||
if (emitter == null) {
|
||||
log.warn("【SSE发送失败】sessionId: {} 没有活跃的SSE连接", sessionId);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
log.debug("【SSE发送】sessionId: {}, event: {}", sessionId, eventDto.getEvent());
|
||||
emitter.send(SseEmitter.event()
|
||||
.name(eventDto.getEvent())
|
||||
.data(JSONUtil.toJsonStr(eventDto)));
|
||||
} catch (Exception e) {
|
||||
log.error("【SSE发送失败】sessionId: {}, error: {}", sessionId, e.getMessage());
|
||||
removeSessionEmitter(sessionId, emitter);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeSessionEmitter(String sessionId, SseEmitter emitter) {
|
||||
boolean removed = SESSION_EMITTERS.remove(sessionId, emitter);
|
||||
if (removed) {
|
||||
try {
|
||||
emitter.complete();
|
||||
} catch (Exception ignore) {
|
||||
// 忽略重复关闭异常
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 用户维度(全局通知) ========================
|
||||
|
||||
/**
|
||||
* 建立与指定用户的 SSE 连接
|
||||
*
|
||||
@@ -154,6 +256,23 @@ public class SseEmitterManager {
|
||||
|
||||
// 循环结束后统一清理空用户,避免并发修改异常
|
||||
toRemoveUsers.forEach(USER_TOKEN_EMITTERS::remove);
|
||||
|
||||
// 会话维度心跳:发送失败的连接移除
|
||||
if (!SESSION_EMITTERS.isEmpty()) {
|
||||
SESSION_EMITTERS.entrySet().removeIf(entry -> {
|
||||
try {
|
||||
entry.getValue().send(heartbeat);
|
||||
return false;
|
||||
} catch (Exception ex) {
|
||||
try {
|
||||
entry.getValue().complete();
|
||||
} catch (Exception ignore) {
|
||||
// 忽略重复关闭异常
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -243,9 +362,11 @@ public class SseEmitterManager {
|
||||
SseMessageDto broadcastMessage = new SseMessageDto();
|
||||
broadcastMessage.setMessage(sseMessageDto.getMessage());
|
||||
broadcastMessage.setUserIds(sseMessageDto.getUserIds());
|
||||
broadcastMessage.setSessionId(sseMessageDto.getSessionId());
|
||||
broadcastMessage.setEventDto(sseMessageDto.getEventDto());
|
||||
RedisUtils.publish(SSE_TOPIC, broadcastMessage, consumer -> {
|
||||
log.info("SSE发送主题订阅消息topic:{} session keys:{} message:{}",
|
||||
SSE_TOPIC, sseMessageDto.getUserIds(), sseMessageDto.getMessage());
|
||||
log.info("SSE发送主题订阅消息topic:{} session:{} session keys:{} message:{}",
|
||||
SSE_TOPIC, sseMessageDto.getSessionId(), sseMessageDto.getUserIds(), sseMessageDto.getMessage());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -26,4 +26,14 @@ public class SseMessageDto implements Serializable {
|
||||
* 需要发送的消息
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 按会话定向推送的会话ID(非空时优先按会话路由,忽略 userIds)
|
||||
*/
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* 结构化事件(按会话定向推送时使用,message 为兼容旧逻辑保留)
|
||||
*/
|
||||
private SseEventDto eventDto;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package org.ruoyi.common.sse.listener;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.sse.core.SseEmitterManager;
|
||||
import org.ruoyi.common.sse.dto.SseMessageDto;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
@@ -28,8 +30,20 @@ public class SseTopicListener implements ApplicationRunner, Ordered {
|
||||
@Override
|
||||
public void run(ApplicationArguments args) throws Exception {
|
||||
sseEmitterManager.subscribeMessage((message) -> {
|
||||
log.info("SSE主题订阅收到消息session keys={} message={}", message.getUserIds(), message.getMessage());
|
||||
// 如果key不为空就按照key发消息 如果为空就群发
|
||||
log.info("SSE主题订阅收到消息session:{} session keys={} message={}",
|
||||
message.getSessionId(), message.getUserIds(), message.getMessage());
|
||||
// 优先按会话路由(对话流式响应)
|
||||
if (StrUtil.isNotBlank(message.getSessionId())) {
|
||||
if (message.getEventDto() != null) {
|
||||
sseEmitterManager.sendEvent(message.getSessionId(), message.getEventDto());
|
||||
} else if (message.getMessage() != null) {
|
||||
// 兼容按会话发纯文本的场景
|
||||
sseEmitterManager.sendEvent(message.getSessionId(),
|
||||
org.ruoyi.common.sse.dto.SseEventDto.content(message.getMessage()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 否则按用户/群发路由(全局通知)
|
||||
if (CollUtil.isNotEmpty(message.getUserIds())) {
|
||||
message.getUserIds().forEach(key -> {
|
||||
sseEmitterManager.sendMessage(key, message.getMessage());
|
||||
|
||||
@@ -93,6 +93,15 @@ public class SseMessageUtils {
|
||||
MANAGER.disconnect(userId, tokenValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成指定会话的SSE连接
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
*/
|
||||
public static void completeConnection(String sessionId) {
|
||||
MANAGER.disconnect(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定的SSE会话发送结构化事件
|
||||
*
|
||||
@@ -106,6 +115,22 @@ public class SseMessageUtils {
|
||||
MANAGER.sendEvent(userId, eventDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定会话发送结构化事件(通过 Redis 广播,跨实例可达)
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @param eventDto SSE事件对象
|
||||
*/
|
||||
public static void sendEvent(String sessionId, SseEventDto eventDto) {
|
||||
if (!isEnable() || sessionId == null) {
|
||||
return;
|
||||
}
|
||||
SseMessageDto dto = new SseMessageDto();
|
||||
dto.setSessionId(sessionId);
|
||||
dto.setEventDto(eventDto);
|
||||
MANAGER.publishMessage(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送内容事件
|
||||
*
|
||||
@@ -116,6 +141,16 @@ public class SseMessageUtils {
|
||||
sendEvent(userId, SseEventDto.content(content));
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定会话发送内容事件
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @param content 内容
|
||||
*/
|
||||
public static void sendContent(String sessionId, String content) {
|
||||
sendEvent(sessionId, SseEventDto.content(content));
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送推理内容事件
|
||||
*
|
||||
@@ -126,6 +161,16 @@ public class SseMessageUtils {
|
||||
sendEvent(userId, SseEventDto.reasoning(reasoningContent));
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定会话发送推理内容事件
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @param reasoningContent 推理内容
|
||||
*/
|
||||
public static void sendReasoning(String sessionId, String reasoningContent) {
|
||||
sendEvent(sessionId, SseEventDto.reasoning(reasoningContent));
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送完成事件
|
||||
*
|
||||
@@ -135,6 +180,15 @@ public class SseMessageUtils {
|
||||
sendEvent(userId, SseEventDto.done());
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定会话发送完成事件
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
*/
|
||||
public static void sendDone(String sessionId) {
|
||||
sendEvent(sessionId, SseEventDto.done());
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送错误事件
|
||||
*
|
||||
@@ -145,6 +199,16 @@ public class SseMessageUtils {
|
||||
sendEvent(userId, SseEventDto.error(error));
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定会话发送错误事件
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @param error 错误信息
|
||||
*/
|
||||
public static void sendError(String sessionId, String error) {
|
||||
sendEvent(sessionId, SseEventDto.error(error));
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否开启
|
||||
*/
|
||||
|
||||
42
ruoyi-common/ruoyi-common-trace/pom.xml
Normal file
42
ruoyi-common/ruoyi-common-trace/pom.xml
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>ruoyi-common-trace</artifactId>
|
||||
|
||||
<description>
|
||||
ruoyi-common-trace 通用链路追踪
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-mybatis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-json</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.ruoyi.common.trace.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
|
||||
/**
|
||||
* 通用链路追踪自动配置。
|
||||
* <p>
|
||||
* 仅注册配置属性;节点采集通过 {@code TraceNodeTemplate} / {@code DefaultTraceStreamSpan} 编程式埋点完成。
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@EnableConfigurationProperties(TraceProperties.class)
|
||||
public class TraceAutoConfiguration {
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.ruoyi.common.trace.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* 通用链路追踪配置。
|
||||
*/
|
||||
@Data
|
||||
@ConfigurationProperties(prefix = "trace")
|
||||
public class TraceProperties {
|
||||
|
||||
/**
|
||||
* 是否启用链路追踪。
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
/**
|
||||
* payload 记录策略。
|
||||
*/
|
||||
private Payload payload = new Payload();
|
||||
|
||||
@Data
|
||||
public static class Payload {
|
||||
|
||||
/**
|
||||
* 错误信息最大长度。
|
||||
*/
|
||||
private int maxErrorLength = 1000;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.ruoyi.common.trace.constant;
|
||||
|
||||
/**
|
||||
* 通用链路追踪常量。
|
||||
*/
|
||||
public final class TraceConstants {
|
||||
|
||||
private TraceConstants() {
|
||||
}
|
||||
|
||||
public static final String STATUS_RUNNING = "RUNNING";
|
||||
public static final String STATUS_SUCCESS = "SUCCESS";
|
||||
public static final String STATUS_ERROR = "ERROR";
|
||||
public static final String STATUS_CANCELLED = "CANCELLED";
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package org.ruoyi.common.trace.constant;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 链路追踪中文展示常量。
|
||||
* <p>
|
||||
* 将技术标识映射为用户可读的中文标签,供前端直接展示。
|
||||
*/
|
||||
public final class TraceDisplayConstants {
|
||||
|
||||
private TraceDisplayConstants() {
|
||||
}
|
||||
|
||||
// ======================== 节点类型中文映射 ========================
|
||||
|
||||
/**
|
||||
* 通用节点类型中文标签。
|
||||
*/
|
||||
public static final Map<String, String> NODE_TYPE_LABELS = Map.ofEntries(
|
||||
Map.entry("RETRIEVAL", "知识检索"),
|
||||
Map.entry("RERANK", "重排序"),
|
||||
Map.entry("LLM_CALL", "LLM 调用"),
|
||||
Map.entry("STREAM", "流式输出"),
|
||||
Map.entry("METHOD", "方法调用"),
|
||||
Map.entry("HTTP", "HTTP 请求"),
|
||||
Map.entry("DB", "数据库"),
|
||||
Map.entry("CACHE", "缓存操作"),
|
||||
Map.entry("TASK", "异步任务"),
|
||||
Map.entry("ROOT", "根节点")
|
||||
);
|
||||
|
||||
/**
|
||||
* 业务类型中文标签。
|
||||
*/
|
||||
public static final Map<String, String> BUSINESS_TYPE_LABELS = Map.ofEntries(
|
||||
Map.entry("RAG_CHAT", "RAG 对话"),
|
||||
Map.entry("API", "API 调用"),
|
||||
Map.entry("SCHEDULED", "定时任务")
|
||||
);
|
||||
|
||||
/**
|
||||
* 状态中文标签。
|
||||
*/
|
||||
public static final Map<String, String> STATUS_LABELS = Map.ofEntries(
|
||||
Map.entry("RUNNING", "运行中"),
|
||||
Map.entry("SUCCESS", "成功"),
|
||||
Map.entry("ERROR", "失败"),
|
||||
Map.entry("CANCELLED", "已取消"),
|
||||
Map.entry("TIMEOUT", "超时")
|
||||
);
|
||||
|
||||
// ======================== 工具方法 ========================
|
||||
|
||||
/**
|
||||
* 获取节点类型中文标签,未匹配时返回原始值。
|
||||
*/
|
||||
public static String nodeTypeLabel(String nodeType) {
|
||||
if (nodeType == null) {
|
||||
return "-";
|
||||
}
|
||||
return NODE_TYPE_LABELS.getOrDefault(nodeType.toUpperCase(), nodeType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取业务类型中文标签,未匹配时返回原始值。
|
||||
*/
|
||||
public static String businessTypeLabel(String businessType) {
|
||||
if (businessType == null) {
|
||||
return "-";
|
||||
}
|
||||
return BUSINESS_TYPE_LABELS.getOrDefault(businessType.toUpperCase(), businessType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态中文标签,未匹配时返回原始值。
|
||||
*/
|
||||
public static String statusLabel(String status) {
|
||||
if (status == null) {
|
||||
return "未知";
|
||||
}
|
||||
return STATUS_LABELS.getOrDefault(status.toUpperCase(), status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将技术节点名称转为可读展示名。
|
||||
* <p>
|
||||
* 支持 kebab-case / snake_case / camelCase → 首字母大写空格分隔。
|
||||
*/
|
||||
public static String prettifyNodeName(String raw) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return "-";
|
||||
}
|
||||
String trimmed = raw.trim();
|
||||
// 已知映射优先
|
||||
Map<String, String> known = Map.ofEntries(
|
||||
Map.entry("rag-chat", "RAG 流式对话"),
|
||||
Map.entry("rag-stream-chat", "RAG 流式对话"),
|
||||
Map.entry("retrieval-engine", "知识库检索"),
|
||||
Map.entry("multi-channel-retrieval", "多路召回"),
|
||||
Map.entry("context-build", "上下文组装"),
|
||||
Map.entry("prompt-render", "Prompt 渲染"),
|
||||
Map.entry("query-rewrite-and-split", "问题改写与拆分"),
|
||||
Map.entry("intent-resolve", "意图识别"),
|
||||
Map.entry("guidance-detect", "歧义引导"),
|
||||
Map.entry("conversation-title-gen", "会话标题生成"),
|
||||
Map.entry("user-first-packet", "用户感知首包"),
|
||||
Map.entry("llm-first-packet", "LLM 首包"),
|
||||
Map.entry("llm-chat-routing", "LLM 路由调度"),
|
||||
Map.entry("llm-stream-routing", "LLM 流式路由")
|
||||
);
|
||||
if (known.containsKey(trimmed)) {
|
||||
return known.get(trimmed);
|
||||
}
|
||||
// 通用格式化: 按 [-_] 分割,每段首字母大写
|
||||
String[] parts = trimmed.split("[-_\\s]+");
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String part : parts) {
|
||||
if (part.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (sb.length() > 0) {
|
||||
sb.append(' ');
|
||||
}
|
||||
sb.append(Character.toUpperCase(part.charAt(0)));
|
||||
if (part.length() > 1) {
|
||||
sb.append(part.substring(1));
|
||||
}
|
||||
}
|
||||
return sb.length() > 0 ? sb.toString() : trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断状态是否为失败。
|
||||
*/
|
||||
public static boolean isFailed(String status) {
|
||||
if (status == null) {
|
||||
return false;
|
||||
}
|
||||
String upper = status.toUpperCase();
|
||||
return "ERROR".equals(upper) || "FAILED".equals(upper) || "TIMEOUT".equals(upper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断状态是否为成功。
|
||||
*/
|
||||
public static boolean isSuccess(String status) {
|
||||
if (status == null) {
|
||||
return false;
|
||||
}
|
||||
return "SUCCESS".equalsIgnoreCase(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断状态是否为运行中。
|
||||
*/
|
||||
public static boolean isRunning(String status) {
|
||||
if (status == null) {
|
||||
return false;
|
||||
}
|
||||
return "RUNNING".equalsIgnoreCase(status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package org.ruoyi.common.trace.core;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.trace.config.TraceProperties;
|
||||
import org.ruoyi.common.trace.constant.TraceConstants;
|
||||
import org.ruoyi.common.trace.service.TraceRecordService;
|
||||
import org.ruoyi.common.trace.util.TracePayloadUtils;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* 默认流式 trace 节点实现。
|
||||
*/
|
||||
@Slf4j
|
||||
public class DefaultTraceStreamSpan implements TraceStreamSpan {
|
||||
|
||||
private final TraceRecordService traceRecordService;
|
||||
private final TraceProperties traceProperties;
|
||||
private final String traceId;
|
||||
private final String nodeId;
|
||||
private final long startMillis;
|
||||
private final AtomicBoolean finished = new AtomicBoolean(false);
|
||||
private final AtomicBoolean detached = new AtomicBoolean(false);
|
||||
|
||||
public DefaultTraceStreamSpan(TraceRecordService traceRecordService,
|
||||
TraceProperties traceProperties,
|
||||
String traceId,
|
||||
String nodeId,
|
||||
long startMillis) {
|
||||
this.traceRecordService = traceRecordService;
|
||||
this.traceProperties = traceProperties;
|
||||
this.traceId = traceId;
|
||||
this.nodeId = nodeId;
|
||||
this.startMillis = startMillis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void detach() {
|
||||
if (detached.compareAndSet(false, true)) {
|
||||
TraceContext.popNode();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finishSuccess(String outputPayload) {
|
||||
finish(TraceConstants.STATUS_SUCCESS, null, outputPayload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finishError(Throwable throwable) {
|
||||
finish(TraceConstants.STATUS_ERROR, TracePayloadUtils.error(throwable, traceProperties), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finishCancelledIfRunning() {
|
||||
finish(TraceConstants.STATUS_CANCELLED, null, null);
|
||||
}
|
||||
|
||||
private void finish(String status, String errorMessage, String outputPayload) {
|
||||
if (!finished.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
traceRecordService.finishNode(traceId, nodeId, status, errorMessage, outputPayload,
|
||||
new Date(), System.currentTimeMillis() - startMillis);
|
||||
} catch (Exception e) {
|
||||
log.warn("结束 trace stream span 失败,traceId={}, nodeId={}", traceId, nodeId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package org.ruoyi.common.trace.core;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
|
||||
/**
|
||||
* 通用链路追踪上下文。
|
||||
*/
|
||||
public final class TraceContext {
|
||||
|
||||
private static final ThreadLocal<String> TRACE_ID = new ThreadLocal<>();
|
||||
private static final ThreadLocal<String> BUSINESS_TYPE = new ThreadLocal<>();
|
||||
private static final ThreadLocal<String> BUSINESS_ID = new ThreadLocal<>();
|
||||
private static final ThreadLocal<Long> USER_ID = new ThreadLocal<>();
|
||||
private static final ThreadLocal<String> TENANT_ID = new ThreadLocal<>();
|
||||
private static final ThreadLocal<Deque<String>> NODE_STACK = new ThreadLocal<>();
|
||||
|
||||
private TraceContext() {
|
||||
}
|
||||
|
||||
public static TraceScope begin(String traceId, String businessType, String businessId, Long userId, String tenantId) {
|
||||
TraceScope scope = new TraceScope(getTraceId(), getBusinessType(), getBusinessId(), getUserId(), getTenantId());
|
||||
TRACE_ID.set(traceId);
|
||||
BUSINESS_TYPE.set(businessType);
|
||||
BUSINESS_ID.set(businessId);
|
||||
USER_ID.set(userId);
|
||||
TENANT_ID.set(tenantId);
|
||||
NODE_STACK.remove();
|
||||
return scope;
|
||||
}
|
||||
|
||||
static void restore(String traceId, String businessType, String businessId, Long userId, String tenantId) {
|
||||
setOrRemove(TRACE_ID, traceId);
|
||||
setOrRemove(BUSINESS_TYPE, businessType);
|
||||
setOrRemove(BUSINESS_ID, businessId);
|
||||
setOrRemove(USER_ID, userId);
|
||||
setOrRemove(TENANT_ID, tenantId);
|
||||
NODE_STACK.remove();
|
||||
}
|
||||
|
||||
private static <T> void setOrRemove(ThreadLocal<T> holder, T value) {
|
||||
if (value == null) {
|
||||
holder.remove();
|
||||
} else {
|
||||
holder.set(value);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getTraceId() {
|
||||
return TRACE_ID.get();
|
||||
}
|
||||
|
||||
public static String getBusinessType() {
|
||||
return BUSINESS_TYPE.get();
|
||||
}
|
||||
|
||||
public static String getBusinessId() {
|
||||
return BUSINESS_ID.get();
|
||||
}
|
||||
|
||||
public static Long getUserId() {
|
||||
return USER_ID.get();
|
||||
}
|
||||
|
||||
public static String getTenantId() {
|
||||
return TENANT_ID.get();
|
||||
}
|
||||
|
||||
public static String currentNodeId() {
|
||||
Deque<String> stack = NODE_STACK.get();
|
||||
return stack == null ? null : stack.peek();
|
||||
}
|
||||
|
||||
public static int depth() {
|
||||
Deque<String> stack = NODE_STACK.get();
|
||||
return stack == null ? 0 : stack.size();
|
||||
}
|
||||
|
||||
public static void pushNode(String nodeId) {
|
||||
Deque<String> stack = NODE_STACK.get();
|
||||
if (stack == null) {
|
||||
stack = new ArrayDeque<>();
|
||||
NODE_STACK.set(stack);
|
||||
}
|
||||
stack.push(nodeId);
|
||||
}
|
||||
|
||||
public static void popNode() {
|
||||
Deque<String> stack = NODE_STACK.get();
|
||||
if (stack == null || stack.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
stack.pop();
|
||||
if (stack.isEmpty()) {
|
||||
NODE_STACK.remove();
|
||||
}
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
TRACE_ID.remove();
|
||||
BUSINESS_TYPE.remove();
|
||||
BUSINESS_ID.remove();
|
||||
USER_ID.remove();
|
||||
TENANT_ID.remove();
|
||||
NODE_STACK.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package org.ruoyi.common.trace.core;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.core.utils.StringUtils;
|
||||
import org.ruoyi.common.trace.config.TraceProperties;
|
||||
import org.ruoyi.common.trace.constant.TraceConstants;
|
||||
import org.ruoyi.common.trace.domain.TraceNode;
|
||||
import org.ruoyi.common.trace.service.TraceRecordService;
|
||||
import org.ruoyi.common.trace.util.TracePayloadUtils;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* 链路追踪节点模板,封装节点创建、上下文压栈、执行、结束、出栈的标准生命周期。
|
||||
* <p>
|
||||
* 用于同步方法内的 trace 埋点,消除手写 start/finish/pop 的重复代码。
|
||||
* 对于需要异步结束的场景(如流式响应),请使用 {@link DefaultTraceStreamSpan}。
|
||||
*
|
||||
* @see DefaultTraceStreamSpan
|
||||
*/
|
||||
@Slf4j
|
||||
public final class TraceNodeTemplate {
|
||||
|
||||
private TraceNodeTemplate() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 trace 节点上下文中执行业务逻辑,成功后使用 outputBuilder 生成输出摘要。
|
||||
*
|
||||
* @param traceRecordService 记录服务
|
||||
* @param traceProperties 配置
|
||||
* @param nodeName 节点名称
|
||||
* @param nodeType 节点类型
|
||||
* @param className 类名
|
||||
* @param methodName 方法名
|
||||
* @param inputPayload 输入摘要
|
||||
* @param action 业务逻辑
|
||||
* @param successOutput 成功时从结果构建输出摘要
|
||||
* @param <T> 业务返回值类型
|
||||
* @return 业务执行结果
|
||||
*/
|
||||
public static <T> T withNode(
|
||||
TraceRecordService traceRecordService,
|
||||
TraceProperties traceProperties,
|
||||
String nodeName,
|
||||
String nodeType,
|
||||
String className,
|
||||
String methodName,
|
||||
String inputPayload,
|
||||
NodeAction<T> action,
|
||||
Function<T, String> successOutput) {
|
||||
|
||||
if (!traceProperties.isEnabled() || StringUtils.isBlank(TraceContext.getTraceId())) {
|
||||
return unwrap(action);
|
||||
}
|
||||
|
||||
String traceId = TraceContext.getTraceId();
|
||||
String nodeId = UUID.randomUUID().toString().replace("-", "");
|
||||
long startMillis = System.currentTimeMillis();
|
||||
|
||||
TraceNode node = buildNode(traceId, nodeId, nodeName, nodeType,
|
||||
className, methodName, inputPayload, startMillis);
|
||||
|
||||
try {
|
||||
traceRecordService.startNode(node);
|
||||
} catch (Exception e) {
|
||||
log.warn("写入 trace 节点失败,traceId={}, nodeId={}", traceId, nodeId, e);
|
||||
return unwrap(action);
|
||||
}
|
||||
|
||||
TraceContext.pushNode(nodeId);
|
||||
try {
|
||||
T result = action.execute();
|
||||
String output = successOutput != null && result != null ? successOutput.apply(result) : null;
|
||||
finishNode(traceRecordService, traceProperties, traceId, nodeId,
|
||||
TraceConstants.STATUS_SUCCESS, null, output, startMillis);
|
||||
return result;
|
||||
} catch (Throwable ex) {
|
||||
finishNode(traceRecordService, traceProperties, traceId, nodeId,
|
||||
TraceConstants.STATUS_ERROR, ex, null, startMillis);
|
||||
throw rethrow(ex);
|
||||
} finally {
|
||||
TraceContext.popNode();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 trace 节点上下文中执行业务逻辑(无输出摘要)。
|
||||
*/
|
||||
public static <T> T withNode(
|
||||
TraceRecordService traceRecordService,
|
||||
TraceProperties traceProperties,
|
||||
String nodeName,
|
||||
String nodeType,
|
||||
String className,
|
||||
String methodName,
|
||||
String inputPayload,
|
||||
NodeAction<T> action) {
|
||||
return withNode(traceRecordService, traceProperties, nodeName, nodeType,
|
||||
className, methodName, inputPayload, action, null);
|
||||
}
|
||||
|
||||
// ======================== 内部工具方法 ========================
|
||||
|
||||
private static TraceNode buildNode(String traceId, String nodeId, String nodeName,
|
||||
String nodeType, String className, String methodName,
|
||||
String inputPayload, long startMillis) {
|
||||
TraceNode node = new TraceNode();
|
||||
node.setTraceId(traceId);
|
||||
node.setNodeId(nodeId);
|
||||
node.setParentNodeId(TraceContext.currentNodeId());
|
||||
node.setDepth(TraceContext.depth());
|
||||
node.setNodeName(nodeName);
|
||||
node.setNodeType(nodeType);
|
||||
node.setClassName(className);
|
||||
node.setMethodName(methodName);
|
||||
node.setStatus(TraceConstants.STATUS_RUNNING);
|
||||
node.setStartTime(new Date(startMillis));
|
||||
node.setInputPayload(inputPayload);
|
||||
return node;
|
||||
}
|
||||
|
||||
private static void finishNode(TraceRecordService service, TraceProperties props,
|
||||
String traceId, String nodeId, String status,
|
||||
Throwable error, String outputPayload, long startMillis) {
|
||||
try {
|
||||
service.finishNode(traceId, nodeId, status,
|
||||
TracePayloadUtils.error(error, props), outputPayload,
|
||||
new Date(), System.currentTimeMillis() - startMillis);
|
||||
} catch (Exception e) {
|
||||
log.warn("结束 trace 节点失败,traceId={}, nodeId={}", traceId, nodeId, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> T unwrap(NodeAction<T> action) {
|
||||
try {
|
||||
return action.execute();
|
||||
} catch (RuntimeException | Error e) {
|
||||
throw e;
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T extends Throwable> T rethrow(Throwable t) throws T {
|
||||
throw (T) t;
|
||||
}
|
||||
|
||||
/**
|
||||
* 可抛出 Throwable 的业务动作。
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface NodeAction<T> {
|
||||
T execute() throws Throwable;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.ruoyi.common.trace.core;
|
||||
|
||||
/**
|
||||
* Trace 上下文作用域。
|
||||
*/
|
||||
public final class TraceScope implements AutoCloseable {
|
||||
|
||||
private final String previousTraceId;
|
||||
private final String previousBusinessType;
|
||||
private final String previousBusinessId;
|
||||
private final Long previousUserId;
|
||||
private final String previousTenantId;
|
||||
|
||||
TraceScope(String previousTraceId,
|
||||
String previousBusinessType,
|
||||
String previousBusinessId,
|
||||
Long previousUserId,
|
||||
String previousTenantId) {
|
||||
this.previousTraceId = previousTraceId;
|
||||
this.previousBusinessType = previousBusinessType;
|
||||
this.previousBusinessId = previousBusinessId;
|
||||
this.previousUserId = previousUserId;
|
||||
this.previousTenantId = previousTenantId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
TraceContext.restore(previousTraceId, previousBusinessType, previousBusinessId, previousUserId, previousTenantId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.ruoyi.common.trace.core;
|
||||
|
||||
/**
|
||||
* 可跨回调结束的流式 trace 节点。
|
||||
*/
|
||||
public interface TraceStreamSpan {
|
||||
|
||||
void detach();
|
||||
|
||||
/**
|
||||
* 结束成功的流式节点,并可写入输出摘要。
|
||||
*/
|
||||
default void finishSuccess() {
|
||||
finishSuccess(null);
|
||||
}
|
||||
|
||||
void finishSuccess(String outputPayload);
|
||||
|
||||
void finishError(Throwable throwable);
|
||||
|
||||
void finishCancelledIfRunning();
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.ruoyi.common.trace.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.ruoyi.common.mybatis.core.domain.BaseEntity;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 链路追踪节点记录 trace_node。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("trace_node")
|
||||
public class TraceNode extends BaseEntity {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id")
|
||||
private Long id;
|
||||
private String traceId;
|
||||
private String nodeId;
|
||||
private String parentNodeId;
|
||||
private String nodeName;
|
||||
private String nodeType;
|
||||
private Integer depth;
|
||||
private Integer sortOrder;
|
||||
private String className;
|
||||
private String methodName;
|
||||
private String status;
|
||||
private Date startTime;
|
||||
private Date endTime;
|
||||
private Long durationMs;
|
||||
private String errorMessage;
|
||||
private String inputPayload;
|
||||
private String outputPayload;
|
||||
private String metadata;
|
||||
@TableLogic
|
||||
private String delFlag;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.ruoyi.common.trace.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.ruoyi.common.mybatis.core.domain.BaseEntity;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 链路追踪运行记录 trace_run。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("trace_run")
|
||||
public class TraceRun extends BaseEntity {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id")
|
||||
private Long id;
|
||||
private String traceId;
|
||||
private String traceName;
|
||||
private String businessType;
|
||||
private String businessId;
|
||||
private Long userId;
|
||||
private String tenantId;
|
||||
private String status;
|
||||
private Date startTime;
|
||||
private Date endTime;
|
||||
private Long durationMs;
|
||||
private String errorMessage;
|
||||
private String metadata;
|
||||
@TableLogic
|
||||
private String delFlag;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.ruoyi.common.trace.domain.bo;
|
||||
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.ruoyi.common.mybatis.core.domain.BaseEntity;
|
||||
import org.ruoyi.common.trace.domain.TraceRun;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 链路追踪运行记录查询对象。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@AutoMapper(target = TraceRun.class, reverseConvertGenerate = false)
|
||||
public class TraceRunBo extends BaseEntity {
|
||||
|
||||
private Long id;
|
||||
private String traceId;
|
||||
private String traceName;
|
||||
private String businessType;
|
||||
private String businessId;
|
||||
private Long userId;
|
||||
private String tenantId;
|
||||
private String status;
|
||||
private Date startTime;
|
||||
private Date endTime;
|
||||
private Long durationMs;
|
||||
private String errorMessage;
|
||||
private String metadata;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package org.ruoyi.common.trace.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 链路追踪详情视图对象。
|
||||
* <p>
|
||||
* 包含运行信息、节点树以及统计摘要。
|
||||
*/
|
||||
@Data
|
||||
public class TraceDetailVo implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private TraceRunVo run;
|
||||
private List<TraceNodeVo> nodes;
|
||||
private TraceStatistics statistics;
|
||||
|
||||
/**
|
||||
* 链路追踪统计摘要,帮助快速了解整体执行情况。
|
||||
*/
|
||||
@Data
|
||||
public static class TraceStatistics implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 总节点数 */
|
||||
private int totalNodes;
|
||||
|
||||
/** 成功节点数 */
|
||||
private int successCount;
|
||||
|
||||
/** 失败节点数 */
|
||||
private int failedCount;
|
||||
|
||||
/** 运行中节点数 */
|
||||
private int runningCount;
|
||||
|
||||
/** 最大调用深度 */
|
||||
private int maxDepth;
|
||||
|
||||
/** 平均耗时 (ms) */
|
||||
private long avgDurationMs;
|
||||
|
||||
/** 总链路耗时 (ms) */
|
||||
private long totalDurationMs;
|
||||
|
||||
/** 慢节点 Top N */
|
||||
private List<SlowNodeInfo> topSlowNodes = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 慢节点简要信息。
|
||||
*/
|
||||
@Data
|
||||
public static class SlowNodeInfo implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 节点 ID */
|
||||
private String nodeId;
|
||||
|
||||
/** 节点展示名称 */
|
||||
private String nodeDisplayName;
|
||||
|
||||
/** 节点类型中文标签 */
|
||||
private String nodeTypeLabel;
|
||||
|
||||
/** 耗时 (ms) */
|
||||
private long durationMs;
|
||||
|
||||
/** 占总耗时百分比 */
|
||||
private double percentOfTotal;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package org.ruoyi.common.trace.domain.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import org.ruoyi.common.trace.domain.TraceNode;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 链路追踪节点记录视图对象。
|
||||
* <p>
|
||||
* 除实体映射字段外,还提供前端可直接展示的显示标签和解析后的 payload 对象。
|
||||
*/
|
||||
@Data
|
||||
@AutoMapper(target = TraceNode.class)
|
||||
public class TraceNodeVo implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long id;
|
||||
private String traceId;
|
||||
private String nodeId;
|
||||
private String parentNodeId;
|
||||
private String nodeName;
|
||||
private String nodeType;
|
||||
private Integer depth;
|
||||
private Integer sortOrder;
|
||||
private String className;
|
||||
private String methodName;
|
||||
private String status;
|
||||
private Date startTime;
|
||||
private Date endTime;
|
||||
private Long durationMs;
|
||||
private String errorMessage;
|
||||
|
||||
/** 原始 input payload 字符串,parsedInput 解析失败时回退使用 */
|
||||
private String inputPayload;
|
||||
|
||||
/** 原始 output payload 字符串,parsedOutput 解析失败时回退使用 */
|
||||
private String outputPayload;
|
||||
|
||||
/** 原始 metadata 字符串,parsedMetadata 解析失败时回退使用 */
|
||||
private String metadata;
|
||||
|
||||
// ======================== 展示用计算字段 ========================
|
||||
|
||||
/** 节点类型中文标签,如 "知识检索"、"LLM 调用" */
|
||||
@JsonProperty("nodeTypeLabel")
|
||||
private String nodeTypeLabel;
|
||||
|
||||
/** 状态中文标签,如 "成功"、"失败"、"运行中" */
|
||||
@JsonProperty("statusLabel")
|
||||
private String statusLabel;
|
||||
|
||||
/** 节点展示名称(中文友好),从 nodeName 转换 */
|
||||
@JsonProperty("nodeDisplayName")
|
||||
private String nodeDisplayName;
|
||||
|
||||
// ======================== 解析后的 payload ========================
|
||||
|
||||
/** input payload 解析为 Map,前端可直接读取结构化字段 */
|
||||
@JsonProperty("parsedInput")
|
||||
private Map<String, Object> parsedInput;
|
||||
|
||||
/** output payload 解析为 Map,前端可直接读取结构化字段 */
|
||||
@JsonProperty("parsedOutput")
|
||||
private Map<String, Object> parsedOutput;
|
||||
|
||||
/** metadata 解析为 Map */
|
||||
@JsonProperty("parsedMetadata")
|
||||
private Map<String, Object> parsedMetadata;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package org.ruoyi.common.trace.domain.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import org.ruoyi.common.trace.domain.TraceRun;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 链路追踪运行记录视图对象。
|
||||
* <p>
|
||||
* 除实体映射字段外,还提供前端可直接展示的显示标签和解析后的 metadata 对象。
|
||||
*/
|
||||
@Data
|
||||
@AutoMapper(target = TraceRun.class)
|
||||
public class TraceRunVo implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long id;
|
||||
private String traceId;
|
||||
private String traceName;
|
||||
private String businessType;
|
||||
private String businessId;
|
||||
private Long userId;
|
||||
private String tenantId;
|
||||
private String status;
|
||||
private Date startTime;
|
||||
private Date endTime;
|
||||
private Long durationMs;
|
||||
private String errorMessage;
|
||||
|
||||
/** 原始 metadata 字符串(兼容旧版),新代码请使用 parsedMetadata */
|
||||
private String metadata;
|
||||
|
||||
// ======================== 展示用计算字段 ========================
|
||||
|
||||
/** 状态中文标签,如 "成功"、"失败"、"运行中" */
|
||||
@JsonProperty("statusLabel")
|
||||
private String statusLabel;
|
||||
|
||||
/** 业务类型中文标签,如 "RAG 对话" */
|
||||
@JsonProperty("businessTypeLabel")
|
||||
private String businessTypeLabel;
|
||||
|
||||
/** metadata 解析为 Map */
|
||||
@JsonProperty("parsedMetadata")
|
||||
private Map<String, Object> parsedMetadata;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.ruoyi.common.trace.mapper;
|
||||
|
||||
import org.ruoyi.common.mybatis.core.mapper.BaseMapperPlus;
|
||||
import org.ruoyi.common.trace.domain.TraceNode;
|
||||
import org.ruoyi.common.trace.domain.vo.TraceNodeVo;
|
||||
|
||||
/**
|
||||
* 链路追踪节点记录 Mapper。
|
||||
*/
|
||||
public interface TraceNodeMapper extends BaseMapperPlus<TraceNode, TraceNodeVo> {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.ruoyi.common.trace.mapper;
|
||||
|
||||
import org.ruoyi.common.mybatis.core.mapper.BaseMapperPlus;
|
||||
import org.ruoyi.common.trace.domain.TraceRun;
|
||||
import org.ruoyi.common.trace.domain.vo.TraceRunVo;
|
||||
|
||||
/**
|
||||
* 链路追踪运行记录 Mapper。
|
||||
*/
|
||||
public interface TraceRunMapper extends BaseMapperPlus<TraceRun, TraceRunVo> {
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.ruoyi.common.trace.service;
|
||||
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
import org.ruoyi.common.trace.domain.TraceNode;
|
||||
import org.ruoyi.common.trace.domain.TraceRun;
|
||||
import org.ruoyi.common.trace.domain.bo.TraceRunBo;
|
||||
import org.ruoyi.common.trace.domain.vo.TraceDetailVo;
|
||||
import org.ruoyi.common.trace.domain.vo.TraceNodeVo;
|
||||
import org.ruoyi.common.trace.domain.vo.TraceRunVo;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 链路追踪记录服务。
|
||||
*/
|
||||
public interface TraceRecordService {
|
||||
|
||||
void startRun(TraceRun run);
|
||||
|
||||
void finishRun(String traceId, String status, String errorMessage, Date endTime, long durationMs);
|
||||
|
||||
void startNode(TraceNode node);
|
||||
|
||||
void finishNode(String traceId, String nodeId, String status, String errorMessage, String outputPayload, Date endTime, long durationMs);
|
||||
|
||||
TableDataInfo<TraceRunVo> pageRuns(TraceRunBo bo, PageQuery pageQuery);
|
||||
|
||||
TraceRunVo getRun(String traceId);
|
||||
|
||||
List<TraceNodeVo> listNodes(String traceId);
|
||||
|
||||
TraceDetailVo getDetail(String traceId);
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package org.ruoyi.common.trace.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.core.utils.StringUtils;
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
import org.ruoyi.common.trace.constant.TraceDisplayConstants;
|
||||
import org.ruoyi.common.trace.domain.TraceNode;
|
||||
import org.ruoyi.common.trace.domain.TraceRun;
|
||||
import org.ruoyi.common.trace.domain.bo.TraceRunBo;
|
||||
import org.ruoyi.common.trace.domain.vo.TraceDetailVo;
|
||||
import org.ruoyi.common.trace.domain.vo.TraceNodeVo;
|
||||
import org.ruoyi.common.trace.domain.vo.TraceRunVo;
|
||||
import org.ruoyi.common.trace.mapper.TraceNodeMapper;
|
||||
import org.ruoyi.common.trace.mapper.TraceRunMapper;
|
||||
import org.ruoyi.common.trace.service.TraceRecordService;
|
||||
import org.ruoyi.common.trace.util.TracePayloadUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 链路追踪记录服务实现。
|
||||
* <p>
|
||||
* 在自动映射的基础上,对 VO 进行二次加工:补充展示标签、解析 payload 为结构化对象、计算统计信息。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TraceRecordServiceImpl implements TraceRecordService {
|
||||
|
||||
private static final int TOP_SLOW_NODES_LIMIT = 5;
|
||||
|
||||
private final TraceRunMapper traceRunMapper;
|
||||
private final TraceNodeMapper traceNodeMapper;
|
||||
|
||||
@Override
|
||||
public void startRun(TraceRun run) {
|
||||
try {
|
||||
traceRunMapper.insert(run);
|
||||
} catch (Exception e) {
|
||||
log.warn("写入 trace run 失败,traceId={}", run == null ? null : run.getTraceId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finishRun(String traceId, String status, String errorMessage, Date endTime, long durationMs) {
|
||||
try {
|
||||
TraceRun update = new TraceRun();
|
||||
update.setStatus(status);
|
||||
update.setErrorMessage(errorMessage);
|
||||
update.setEndTime(endTime);
|
||||
update.setDurationMs(durationMs);
|
||||
traceRunMapper.update(update, Wrappers.lambdaUpdate(TraceRun.class).eq(TraceRun::getTraceId, traceId));
|
||||
} catch (Exception e) {
|
||||
log.warn("更新 trace run 失败,traceId={}", traceId, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startNode(TraceNode node) {
|
||||
try {
|
||||
traceNodeMapper.insert(node);
|
||||
} catch (Exception e) {
|
||||
log.warn("写入 trace node 失败,traceId={}, nodeId={}",
|
||||
node == null ? null : node.getTraceId(), node == null ? null : node.getNodeId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finishNode(String traceId, String nodeId, String status, String errorMessage, String outputPayload, Date endTime, long durationMs) {
|
||||
try {
|
||||
TraceNode update = new TraceNode();
|
||||
update.setStatus(status);
|
||||
update.setErrorMessage(errorMessage);
|
||||
update.setOutputPayload(outputPayload);
|
||||
update.setEndTime(endTime);
|
||||
update.setDurationMs(durationMs);
|
||||
traceNodeMapper.update(update, Wrappers.lambdaUpdate(TraceNode.class)
|
||||
.eq(TraceNode::getTraceId, traceId)
|
||||
.eq(TraceNode::getNodeId, nodeId));
|
||||
} catch (Exception e) {
|
||||
log.warn("更新 trace node 失败,traceId={}, nodeId={}", traceId, nodeId, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TableDataInfo<TraceRunVo> pageRuns(TraceRunBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<TraceRun> wrapper = buildRunWrapper(bo == null ? new TraceRunBo() : bo);
|
||||
if (StringUtils.isBlank(pageQuery.getOrderByColumn())) {
|
||||
wrapper.orderByDesc(TraceRun::getStartTime);
|
||||
}
|
||||
Page<TraceRunVo> page = traceRunMapper.selectVoPage(pageQuery.build(), wrapper);
|
||||
if (page.getRecords() != null) {
|
||||
page.getRecords().forEach(this::enrichRunVo);
|
||||
}
|
||||
return TableDataInfo.build(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TraceRunVo getRun(String traceId) {
|
||||
TraceRunVo run = traceRunMapper.selectVoOne(Wrappers.lambdaQuery(TraceRun.class).eq(TraceRun::getTraceId, traceId));
|
||||
if (run != null) {
|
||||
enrichRunVo(run);
|
||||
}
|
||||
return run;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TraceNodeVo> listNodes(String traceId) {
|
||||
List<TraceNodeVo> nodes = traceNodeMapper.selectVoList(Wrappers.lambdaQuery(TraceNode.class)
|
||||
.eq(TraceNode::getTraceId, traceId)
|
||||
.orderByAsc(TraceNode::getStartTime)
|
||||
.orderByAsc(TraceNode::getId));
|
||||
if (nodes != null) {
|
||||
nodes.forEach(this::enrichNodeVo);
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TraceDetailVo getDetail(String traceId) {
|
||||
TraceDetailVo detail = new TraceDetailVo();
|
||||
TraceRunVo run = getRun(traceId);
|
||||
detail.setRun(run);
|
||||
|
||||
List<TraceNodeVo> nodes = listNodes(traceId);
|
||||
// 返回扁平列表,前端自行按 parentNodeId 建树
|
||||
detail.setNodes(nodes != null ? nodes : new ArrayList<>());
|
||||
|
||||
// 计算统计信息
|
||||
if (nodes != null) {
|
||||
detail.setStatistics(buildStatistics(nodes, run));
|
||||
}
|
||||
return detail;
|
||||
}
|
||||
|
||||
// ======================== VO 加工 ========================
|
||||
|
||||
/**
|
||||
* 为 TraceRunVo 补充展示标签和解析后的 metadata。
|
||||
*/
|
||||
private void enrichRunVo(TraceRunVo vo) {
|
||||
if (vo == null) {
|
||||
return;
|
||||
}
|
||||
vo.setStatusLabel(TraceDisplayConstants.statusLabel(vo.getStatus()));
|
||||
vo.setBusinessTypeLabel(TraceDisplayConstants.businessTypeLabel(vo.getBusinessType()));
|
||||
vo.setParsedMetadata(TracePayloadUtils.parseJsonToMap(vo.getMetadata()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 为 TraceNodeVo 补充展示标签和解析后的 payload。
|
||||
*/
|
||||
private void enrichNodeVo(TraceNodeVo vo) {
|
||||
if (vo == null) {
|
||||
return;
|
||||
}
|
||||
vo.setNodeTypeLabel(TraceDisplayConstants.nodeTypeLabel(vo.getNodeType()));
|
||||
vo.setStatusLabel(TraceDisplayConstants.statusLabel(vo.getStatus()));
|
||||
vo.setNodeDisplayName(TraceDisplayConstants.prettifyNodeName(vo.getNodeName()));
|
||||
vo.setParsedInput(TracePayloadUtils.parseJsonToMap(vo.getInputPayload()));
|
||||
vo.setParsedOutput(TracePayloadUtils.parseJsonToMap(vo.getOutputPayload()));
|
||||
vo.setParsedMetadata(TracePayloadUtils.parseJsonToMap(vo.getMetadata()));
|
||||
}
|
||||
|
||||
// ======================== 统计计算 ========================
|
||||
private TraceDetailVo.TraceStatistics buildStatistics(List<TraceNodeVo> nodes, TraceRunVo run) {
|
||||
TraceDetailVo.TraceStatistics stats = new TraceDetailVo.TraceStatistics();
|
||||
|
||||
int total = nodes == null ? 0 : nodes.size();
|
||||
int success = 0;
|
||||
int failed = 0;
|
||||
int running = 0;
|
||||
int maxDepth = 0;
|
||||
long totalDuration = 0;
|
||||
|
||||
for (TraceNodeVo node : nodes) {
|
||||
if (node == null) {
|
||||
continue;
|
||||
}
|
||||
if (TraceDisplayConstants.isSuccess(node.getStatus())) {
|
||||
success++;
|
||||
} else if (TraceDisplayConstants.isFailed(node.getStatus())) {
|
||||
failed++;
|
||||
} else if (TraceDisplayConstants.isRunning(node.getStatus())) {
|
||||
running++;
|
||||
}
|
||||
|
||||
int depth = node.getDepth() == null ? 0 : node.getDepth();
|
||||
if (depth > maxDepth) {
|
||||
maxDepth = depth;
|
||||
}
|
||||
|
||||
long dur = node.getDurationMs() == null ? 0 : node.getDurationMs();
|
||||
totalDuration += dur;
|
||||
}
|
||||
|
||||
stats.setTotalNodes(total);
|
||||
stats.setSuccessCount(success);
|
||||
stats.setFailedCount(failed);
|
||||
stats.setRunningCount(running);
|
||||
stats.setMaxDepth(maxDepth);
|
||||
stats.setAvgDurationMs(total > 0 ? totalDuration / total : 0);
|
||||
stats.setTotalDurationMs(run != null && run.getDurationMs() != null ? run.getDurationMs() : totalDuration);
|
||||
|
||||
// 慢节点 Top N
|
||||
long totalMs = stats.getTotalDurationMs();
|
||||
List<TraceDetailVo.SlowNodeInfo> topSlow = nodes.stream()
|
||||
.filter(n -> n != null && n.getDurationMs() != null && n.getDurationMs() > 0)
|
||||
.sorted(Comparator.comparingLong(TraceNodeVo::getDurationMs).reversed())
|
||||
.limit(TOP_SLOW_NODES_LIMIT)
|
||||
.map(n -> {
|
||||
TraceDetailVo.SlowNodeInfo info = new TraceDetailVo.SlowNodeInfo();
|
||||
info.setNodeId(n.getNodeId());
|
||||
info.setNodeDisplayName(n.getNodeDisplayName() != null ? n.getNodeDisplayName() : n.getNodeName());
|
||||
info.setNodeTypeLabel(n.getNodeTypeLabel());
|
||||
info.setDurationMs(n.getDurationMs());
|
||||
info.setPercentOfTotal(totalMs > 0 ? Math.round(n.getDurationMs() * 1000.0 / totalMs) / 10.0 : 0);
|
||||
return info;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
stats.setTopSlowNodes(topSlow);
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
// ======================== 查询辅助 ========================
|
||||
|
||||
private LambdaQueryWrapper<TraceRun> buildRunWrapper(TraceRunBo bo) {
|
||||
Map<String, Object> params = bo.getParams();
|
||||
return new LambdaQueryWrapper<TraceRun>()
|
||||
.eq(StringUtils.isNotBlank(bo.getTraceId()), TraceRun::getTraceId, bo.getTraceId())
|
||||
.like(StringUtils.isNotBlank(bo.getTraceName()), TraceRun::getTraceName, bo.getTraceName())
|
||||
.eq(StringUtils.isNotBlank(bo.getBusinessType()), TraceRun::getBusinessType, bo.getBusinessType())
|
||||
.eq(StringUtils.isNotBlank(bo.getBusinessId()), TraceRun::getBusinessId, bo.getBusinessId())
|
||||
.eq(StringUtils.isNotBlank(bo.getStatus()), TraceRun::getStatus, bo.getStatus())
|
||||
.eq(bo.getUserId() != null, TraceRun::getUserId, bo.getUserId())
|
||||
.eq(StringUtils.isNotBlank(bo.getTenantId()), TraceRun::getTenantId, bo.getTenantId())
|
||||
.between(params.get("beginTime") != null && params.get("endTime") != null,
|
||||
TraceRun::getStartTime, params.get("beginTime"), params.get("endTime"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package org.ruoyi.common.trace.util;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import org.ruoyi.common.json.handler.BigNumberSerializer;
|
||||
import org.ruoyi.common.trace.config.TraceProperties;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Trace payload 序列化、截断与解析工具。
|
||||
*/
|
||||
public final class TracePayloadUtils {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER;
|
||||
|
||||
static {
|
||||
OBJECT_MAPPER = new ObjectMapper();
|
||||
SimpleModule jsSafeModule = new SimpleModule("js-safe-numbers");
|
||||
jsSafeModule.addSerializer(Long.class, BigNumberSerializer.INSTANCE);
|
||||
jsSafeModule.addSerializer(Long.TYPE, BigNumberSerializer.INSTANCE);
|
||||
jsSafeModule.addSerializer(BigInteger.class, BigNumberSerializer.INSTANCE);
|
||||
jsSafeModule.addSerializer(BigDecimal.class, ToStringSerializer.instance);
|
||||
OBJECT_MAPPER.registerModule(jsSafeModule);
|
||||
}
|
||||
|
||||
private TracePayloadUtils() {
|
||||
}
|
||||
|
||||
public static String error(Throwable throwable, TraceProperties properties) {
|
||||
if (throwable == null) {
|
||||
return null;
|
||||
}
|
||||
int maxLength = properties == null ? 1000 : properties.getPayload().getMaxErrorLength();
|
||||
String message = throwable.getClass().getSimpleName() + ": " + (throwable.getMessage() == null ? "" : throwable.getMessage());
|
||||
return truncate(message, maxLength);
|
||||
}
|
||||
|
||||
public static String toJson(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return OBJECT_MAPPER.writeValueAsString(value);
|
||||
} catch (JsonProcessingException e) {
|
||||
return "{\"summary\":\"payload serialization failed\"}";
|
||||
}
|
||||
}
|
||||
|
||||
public static String truncate(String value, int maxLength) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (maxLength <= 0 || value.length() <= maxLength) {
|
||||
return value;
|
||||
}
|
||||
return value.substring(0, maxLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 JSON 字符串解析为 Map,用于前端展示结构化数据。
|
||||
* 解析失败时返回空 Map,不影响主流程。
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Map<String, Object> parseJsonToMap(String json) {
|
||||
if (json == null || json.isBlank()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
try {
|
||||
return OBJECT_MAPPER.readValue(json, new TypeReference<Map<String, Object>>() {});
|
||||
} catch (JsonProcessingException e) {
|
||||
// 非 JSON 字符串,返回包含原始文本的 Map
|
||||
return Collections.singletonMap("_raw", json);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user