8 Commits

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 17:44:39 +08:00
ageerle
0a08c489be docs: remove redundant registry commands 2026-08-04 15:57:32 +08:00
ageerle
038e7725f9 docs: add prebuilt image deployment guide 2026-08-04 15:55:17 +08:00
ageerle
4ccf249663 docs: remove obsolete WeChat image 2026-08-04 15:50:51 +08:00
ageerle
1d5b01f77f docs: remove obsolete Douyin image 2026-08-04 15:50:36 +08:00
ageerle
538dac3a46 docs: add Douyin tutorial QR code 2026-08-04 15:49:40 +08:00
ageerle
6c383dd9f5 fix: 修复智能体编辑时关联工具加载失败 (#320) 2026-08-04 15:38:25 +08:00
ageerle
9ae49f8797 fix: allow frontend dependency builds in image workflow 2026-08-04 15:30:34 +08:00
13 changed files with 901 additions and 179 deletions

View File

@@ -4,15 +4,21 @@ on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
release_tag:
description: Release tag to rebuild
required: true
type: string
concurrency:
group: publish-images-${{ github.ref_name }}
group: publish-images-${{ inputs.release_tag || github.ref_name }}
cancel-in-progress: false
env:
REGISTRY: ghcr.io
IMAGE_OWNER: ${{ github.repository_owner }}
RELEASE_TAG: ${{ github.ref_name }}
RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }}
jobs:
publish:
@@ -70,6 +76,26 @@ jobs:
path: build/ruoyi-web
fetch-depth: 1
- name: Configure frontend dependency builds
if: matrix.source == 'admin' || matrix.source == 'web'
shell: bash
run: |
python3 - "${{ matrix.dockerfile }}" <<'PY'
from pathlib import Path
import sys
dockerfile = Path(sys.argv[1])
content = dockerfile.read_text()
marker = "pnpm install --frozen-lockfile"
replacement = (
"pnpm install --frozen-lockfile "
"--config.dangerously-allow-all-builds=true"
)
if marker not in content:
raise SystemExit(f"pnpm install command not found in {dockerfile}")
dockerfile.write_text(content.replace(marker, replacement, 1))
PY
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

View File

@@ -23,7 +23,13 @@
</div>
## 🚀 Live Demo
| Service | URL | Default Account |
|---|---|---|
| Admin Panel | http://129.226.199.247:25666 | admin / admin123 |
| User Frontend | http://129.226.199.247:25137 | admin / admin123 |
| Commercial Edition | https://web.ruoyiai.chat | WeChat QR code login |
## ✨ Core Features
@@ -71,22 +77,42 @@ This project provides two Docker deployment methods:
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
# Requirements: Docker Engine and Docker Compose V2
# Clone the v3.1.0 release
git clone --depth 1 --branch v3.1.0 https://github.com/ageerle/ruoyi-ai.git
cd ruoyi-ai
# Start all services (pull pre-built images from GHCR)
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml up -d
# Pin the image version. Public GHCR images do not require docker login.
cp docs/docker/ruoyi-ai/.env.example docs/docker/ruoyi-ai/.env
sed -i 's/^RUIYI_VERSION=.*/RUIYI_VERSION=v3.1.0/' docs/docker/ruoyi-ai/.env
# Pull pre-built images from GHCR and start all services
docker compose --env-file docs/docker/ruoyi-ai/.env \
-f docs/docker/ruoyi-ai/docker-compose-all.yaml pull
docker compose --env-file docs/docker/ruoyi-ai/.env \
-f docs/docker/ruoyi-ai/docker-compose-all.yaml up -d
# Check service status
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml ps
docker compose --env-file docs/docker/ruoyi-ai/.env \
-f docs/docker/ruoyi-ai/docker-compose-all.yaml ps
# Access services
# Admin Panel: http://localhost:25666 (admin / admin123)
# User Frontend: http://localhost:25137
# Backend API: http://localhost:26039
# Access services (replace SERVER_IP with the server address)
# Admin Panel: http://SERVER_IP:25666 (admin / admin123)
# User Frontend: http://SERVER_IP:25137
# Backend API: http://SERVER_IP:26039
```
The default Compose file also publishes MySQL (`23306`), Redis (`26379`),
Weaviate (`28080`), and MinIO (`29000`/`29090`). For production deployments,
change the default MySQL and MinIO passwords and expose only the application
ports through the firewall or a reverse proxy.
To upgrade to another published release, update `RUIYI_VERSION` in
`docs/docker/ruoyi-ai/.env`, then run `docker compose pull` and
`docker compose up -d` with the same `--env-file` and `-f` options. Do not use
`docker compose down -v` unless you intend to delete persistent data volumes.
### Method 2: Step-by-step Deployment (Source Build)
If you need to build backend services from source, follow these steps:
@@ -143,36 +169,6 @@ docker-compose up -d --build
| 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:
```
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
```
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
# Stop all services
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml down
# View service logs
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml logs -f [service-name]
# 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?
@@ -234,9 +230,9 @@ Enjoy ByteDance's in-house Doubao models plus full-power open-source SOTA models
<em>Join group for learning</em>
</td>
<td align="center">
<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>
<img src="docs/image/douyin.png" alt="Douyin QR Code" width="200" height="200"><br>
<strong>Douyin Video Tutorials</strong><br>
<em>Open Douyin, scan & follow to watch video tutorials</em>
</td>
<td align="center">
<img src="docs/image/qq.png" alt="QQ Group QR Code" width="200" height="200"><br>

View File

@@ -23,6 +23,14 @@
</div>
## 🚀 演示地址
| 服务 | 访问地址 | 默认账号 |
|---|---|---|
| 管理端 | http://129.226.199.247:25666 | admin / admin123 |
| 用户端 | http://129.226.199.247:25137 | admin / admin123 |
| 商业版 | https://web.ruoyiai.chat | 微信扫码登录 |
## ✨ 核心亮点
| 模块 | 现有能力
@@ -70,22 +78,39 @@
使用 `docker-compose-all.yaml` 可以一键启动所有服务(包括后端、管理端、用户端及依赖服务):
```bash
# 克隆仓库
git clone https://github.com/ageerle/ruoyi-ai.git
# 环境要求Docker Engine 和 Docker Compose V2
# 克隆 v3.1.0 版本
git clone --depth 1 --branch v3.1.0 https://github.com/ageerle/ruoyi-ai.git
cd ruoyi-ai
# 启动所有服务(从 GHCR 拉取预构建镜像)
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml up -d
# 固定镜像版本。GHCR 镜像已公开,无需 docker login
cp docs/docker/ruoyi-ai/.env.example docs/docker/ruoyi-ai/.env
sed -i 's/^RUIYI_VERSION=.*/RUIYI_VERSION=v3.1.0/' docs/docker/ruoyi-ai/.env
# 从 GHCR 拉取预构建镜像并启动全部服务
docker compose --env-file docs/docker/ruoyi-ai/.env \
-f docs/docker/ruoyi-ai/docker-compose-all.yaml pull
docker compose --env-file docs/docker/ruoyi-ai/.env \
-f docs/docker/ruoyi-ai/docker-compose-all.yaml up -d
# 查看服务状态
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml ps
docker compose --env-file docs/docker/ruoyi-ai/.env \
-f docs/docker/ruoyi-ai/docker-compose-all.yaml ps
# 访问服务
# 管理端: http://localhost:25666 (admin / admin123)
# 用户端: http://localhost:25137
# 后端API: http://localhost:26039
# 访问服务(将 SERVER_IP 替换为服务器地址)
# 管理端: http://SERVER_IP:25666 (admin / admin123)
# 用户端: http://SERVER_IP:25137
# 后端API: http://SERVER_IP:26039
```
默认 Compose 还会发布 MySQL`23306`、Redis`26379`、Weaviate`28080`)和
MinIO`29000`/`29090`)端口。生产环境请修改 MySQL 和 MinIO 默认密码,并通过防火墙或反向代理只开放应用端口。
升级到其他已发布版本时,修改 `docs/docker/ruoyi-ai/.env` 中的 `RUIYI_VERSION`,然后使用相同的
`--env-file``-f` 参数执行 `docker compose pull``docker compose up -d`。除非确定要删除持久化数据卷,
不要执行 `docker compose down -v`
### 方式二:分步部署(源码编译)
如果您需要从源码构建后端服务,请按照以下步骤操作:
@@ -142,36 +167,6 @@ docker-compose up -d --build
| MinIO API | 29000 | 9000 | 对象存储 API |
| MinIO Console | 29090 | 9090 | 对象存储控制台 |
### 镜像仓库
每次推送类似 `v3.1.0` 的版本标签后GitHub Actions 会自动构建并发布应用镜像到 GitHub Container RegistryGHCR
```
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 [服务名]
```
## 📚 使用文档
想要深入了解安装部署、功能配置和二次开发?
@@ -235,9 +230,9 @@ docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml restart [服务
<em>邀请进群学习</em>
</td>
<td align="center">
<img src="docs/image/wx06.png" alt="微信二维码" width="200" height="200"><br>
<strong>微信技术交流群</strong><br>
<em>技术讨论</em>
<img src="docs/image/douyin.png" alt="抖音二维码" 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>

BIN
docs/image/douyin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
package org.ruoyi.trace;
package org.ruoyi.argtrace;
/**
* RAG trace 业务与节点类型常量

View File

@@ -1,4 +1,4 @@
package org.ruoyi.trace;
package org.ruoyi.argtrace;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;

View File

@@ -48,11 +48,11 @@ public class McpToolController extends BaseController {
*/
@SaCheckPermission("mcp:tool:list")
@GetMapping("/all")
public McpToolListResult listAll(
public R<McpToolListResult> listAll(
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String type,
@RequestParam(required = false) String status) {
return mcpToolService.listTools(keyword, type, status);
return R.ok(mcpToolService.listTools(keyword, type, status));
}
/**

View File

@@ -80,8 +80,8 @@ import org.ruoyi.service.chat.impl.memory.PersistentChatMemoryStore;
import org.ruoyi.service.knowledge.IKnowledgeInfoService;
import org.ruoyi.service.retrieval.KnowledgeRetrievalService;
import org.ruoyi.service.knowledge.retriever.CustomVectorRetriever;
import org.ruoyi.trace.RagTraceNodeTypes;
import org.ruoyi.trace.RagTracePayloadBuilder;
import org.ruoyi.argtrace.RagTraceNodeTypes;
import org.ruoyi.argtrace.RagTracePayloadBuilder;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@@ -348,7 +348,7 @@ public class ChatServiceFacade implements IChatService {
.subAgents(skillsAgent, searchAgent, sqlAgent, chartGenerationAgent, echartsAgent, chitChatAgent)
.supervisorContext("仅当请求是问候或简单闲聊、不需要任何数据、搜索、技能或图表时,才使用 chitChatAgent;"
+ "其余情况必须使用对应的专业 Agent")
.responseStrategy(SupervisorResponseStrategy.SUMMARY);
.responseStrategy(SupervisorResponseStrategy.LAST);
SupervisorAgent supervisor = supervisorBuilder.build();
// 知识库增强:智能体绑定了知识库时,对 supervisor 输入做一次 RAG 增强(全程唯一一次检索)

View File

@@ -21,8 +21,8 @@ import org.ruoyi.mapper.knowledge.KnowledgeFragmentMapper;
import org.ruoyi.service.rerank.RerankModelService;
import org.ruoyi.service.retrieval.KnowledgeRetrievalService;
import org.ruoyi.service.vector.VectorStoreService;
import org.ruoyi.trace.RagTraceNodeTypes;
import org.ruoyi.trace.RagTracePayloadBuilder;
import org.ruoyi.argtrace.RagTraceNodeTypes;
import org.ruoyi.argtrace.RagTracePayloadBuilder;
import org.springframework.stereotype.Service;
import java.util.*;
@@ -128,7 +128,7 @@ public class KnowledgeRetrievalServiceImpl implements KnowledgeRetrievalService
// 如果启用重排序,适当扩大召回数量
int originalMaxResults = queryVectorBo.getMaxResults() != null ? queryVectorBo.getMaxResults() : 10;
int targetMaxResults = originalMaxResults;
if (Boolean.TRUE.equals(queryVectorBo.getEnableRerank()) &&
if (Boolean.TRUE.equals(queryVectorBo.getEnableRerank()) &&
StringUtils.isNotBlank(queryVectorBo.getRerankModelName())) {
targetMaxResults = originalMaxResults * RERANK_EXPANSION_FACTOR;
}
@@ -137,7 +137,7 @@ public class KnowledgeRetrievalServiceImpl implements KnowledgeRetrievalService
if (!Boolean.TRUE.equals(queryVectorBo.getEnableHybrid())) {
QueryVectorBo vectorQuery = copyOf(queryVectorBo, targetMaxResults);
List<KnowledgeRetrievalVo> results = vectorStoreService.search(vectorQuery);
// 应用基础相似度阈值过滤(如果有)
if (queryVectorBo.getSimilarityThreshold() != null) {
results = results.stream()
@@ -214,7 +214,7 @@ public class KnowledgeRetrievalServiceImpl implements KnowledgeRetrievalService
rerankInputPayload);
try {
RerankModelService rerankModel = rerankModelFactory.createModel(queryVectorBo.getRerankModelName());
List<String> contents = coarseResults.stream()
.map(KnowledgeRetrievalVo::getContent)
.collect(Collectors.toList());
@@ -241,7 +241,7 @@ public class KnowledgeRetrievalServiceImpl implements KnowledgeRetrievalService
// 按新分排序
reranked.sort((a, b) -> b.getScore().compareTo(a.getScore()));
// 截断到 topN
List<KnowledgeRetrievalVo> results = reranked.subList(0, Math.min(topN, reranked.size()));
finishTraceNode(traceNode, TraceConstants.STATUS_SUCCESS, null,
@@ -285,9 +285,9 @@ public class KnowledgeRetrievalServiceImpl implements KnowledgeRetrievalService
List<KnowledgeRetrievalVo> fusedResults = new ArrayList<>();
for (Map.Entry<String, KnowledgeRetrievalVo> entry : allMap.entrySet()) {
String id = entry.getKey();
double finalScore = (1 - alpha) * vectorScores.getOrDefault(id, 0.0) +
double finalScore = (1 - alpha) * vectorScores.getOrDefault(id, 0.0) +
alpha * keywordScores.getOrDefault(id, 0.0);
KnowledgeRetrievalVo vo = entry.getValue();
vo.setScore(finalScore * 60.0); // 归一化缩放
fusedResults.add(vo);

View File

@@ -1,78 +0,0 @@
package org.ruoyi.trace;
import org.junit.jupiter.api.Test;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.domain.bo.vector.QueryVectorBo;
import org.ruoyi.domain.vo.knowledge.KnowledgeRetrievalVo;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class RagTracePayloadBuilderTest {
@Test
void chatRequestSummaryShouldHandleNullsAndAvoidPromptBody() {
ChatRequest request = new ChatRequest();
request.setSessionId(100L);
request.setModel("qwen-plus");
request.setKnowledgeId("200");
request.setContent("secret prompt body");
ChatModelVo model = new ChatModelVo();
model.setProviderCode("dashscope");
request.setChatModelVo(model);
String payload = RagTracePayloadBuilder.chatRequestSummary(request);
assertTrue(payload.contains("\"sessionId\":100"));
assertTrue(payload.contains("\"contentLength\":18"));
assertTrue(payload.contains("\"providerCode\":\"dashscope\""));
assertFalse(payload.contains("secret prompt body"));
}
@Test
void retrievalInputSummaryShouldUseSummaryOnly() {
QueryVectorBo query = new QueryVectorBo();
query.setKid("200");
query.setQuery("private retrieval query");
query.setMaxResults(5);
query.setEnableRerank(true);
query.setRerankModelName("gte-rerank");
query.setRerankTopN(null);
String payload = RagTracePayloadBuilder.retrievalInputSummary(query);
assertTrue(payload.contains("\"kid\":\"200\""));
assertTrue(payload.contains("\"queryLength\":23"));
assertTrue(payload.contains("\"enableRerank\":true"));
assertFalse(payload.contains("private retrieval query"));
}
@Test
void retrievalOutputSummaryShouldAvoidFragmentContent() {
KnowledgeRetrievalVo result = new KnowledgeRetrievalVo();
result.setId("fragment-1");
result.setDocId("doc-1");
result.setIdx(1);
result.setScore(0.85);
result.setContent("sensitive knowledge fragment");
String payload = RagTracePayloadBuilder.retrievalOutputSummary(List.of(result));
assertTrue(payload.contains("\"resultCount\":1"));
assertTrue(payload.contains("\"contentLength\":28"));
assertTrue(payload.contains("\"fragment-1\""));
assertFalse(payload.contains("sensitive knowledge fragment"));
}
@Test
void summariesShouldAcceptNullValuesWithoutMapOfNpe() {
assertTrue(RagTracePayloadBuilder.chatRequestSummary(null).contains("\"requestPresent\":false"));
assertTrue(RagTracePayloadBuilder.retrievalInputSummary(null).contains("\"queryPresent\":false"));
assertTrue(RagTracePayloadBuilder.retrievalOutputSummary(null).contains("\"resultCount\":0"));
assertTrue(RagTracePayloadBuilder.rerankInputSummary(null, 0, null).contains("\"candidateCount\":0"));
}
}