35 Commits

Author SHA1 Message Date
ageerle
8a12be349a docs: correct coding demo URL in READMEs 2026-10-22 01:04:05 +08:00
ageerle
f286f50bbe docs: update demo URLs and QR code login instructions 2026-10-21 06:43:05 +08:00
ageerle
5e66c7bd30 feat(short-drama): add director workspaces and refine video production
Add canvas and director APIs, production skill catalogs, prompt preservation, video references and Atlas H3 support. Include schema migrations, upload limits, regression coverage and the new demo URL.
2026-10-20 09:30:36 +08:00
ageerle
8ab3638012 fix(copilot): 对齐图片限制并修复 Doubao 模型与思考回放 2026-10-13 03:08:34 +08:00
ageerle
432fbb6ea4 fix(short-drama): enforce Atlas isolation and restore missing catalog routes 2026-10-10 10:22:33 +08:00
ageerle
ca6ef96200 feat: 完善短剧创作、技能目录与媒体任务恢复
保存原始创作输入,整合剧本打磨、分场规划与流式进度,接入审美及导演技能、角色声音和音乐;保持已批准媒体并完善请求去重与恢复。

统一增量 SQL 目录,补齐新安装字段并修正示例插入列,更新迁移说明;修复测试夹具与当前审阅、重启恢复行为的断言。验证:相关模块编译通过,短剧及 Atlas 回归 337 项、SSE 回归 3 项均通过。
2026-10-03 19:11:54 +08:00
ageerle
91b6b53ce9 feat: 增强短剧导演规则与素材生成及合成 2026-09-30 14:54:26 +08:00
ageerle
7c7e64311e Merge pull request #340 from ageerle/codex/mcp-remote-bearer-key
feat: support bearerKey in remote MCP tool JSON
2026-09-23 17:39:00 +08:00
ageerle
46714df604 feat: support bearer key in remote MCP tool config 2026-09-23 17:35:58 +08:00
ageerle
197dc40510 fix: return model keys in admin details and allow clearing 2026-09-22 23:05:59 +08:00
ageerle
8e3b286190 fix: restore admin-managed model keys and refresh cached clients 2026-09-21 11:08:05 +08:00
ageerle
91ca91571b test: cover external plan step visibility 2026-09-21 11:07:37 +08:00
ageerle
ce9228c5bb fix: preserve application Jackson settings for tenant requests 2026-09-21 11:07:36 +08:00
ageerle
6f19c86fe6 Merge pull request #337 from eroXianYin/main
知识库检索节点补齐严格模式、默认回复及节点注册
2026-09-21 11:02:32 +08:00
SUNLIZHAO\孙立钊
422d935b26 知识库检索节点补齐严格模式、默认回复及节点注册
- KnowledgeRetrievalNodeConfig 新增 is_strict / default_response 字段
- KnowledgeRetrievalNode 检索无结果时,严格模式开启返回默认回复,关闭返回空
- 检索模式优先级修正:节点明确选择覆盖知识库的混合检索配置
- 新增幂等注册 SQL(update 脚本 + 全量初始化),新老环境均可使用
2026-09-11 19:57:21 +08:00
ageerle
cca3090577 fix: support Atlas multimodal generation and media preview 2026-09-11 10:49:29 +08:00
ageerle
4766d2e628 docs: document URL management and local validation 2026-09-08 23:56:52 +08:00
ageerle
c9ca55b45b fix: preserve manual plan transitions during external verification 2026-09-08 23:48:59 +08:00
ageerle
bcff4fd9e3 feat: enhance coding harness, model security and system features 2026-09-08 23:33:29 +08:00
ageerle
d4ed2b8672 Merge pull request #326 from ansn366/docs/improve-windows-docker-setup
docs: improve Windows Docker deployment instructions
2026-09-05 22:27:32 +08:00
ageerle
ee838aa488 Merge pull request #328 from l-vM2k/fix-mainExecutor
fix: 修复 mainExecutor 线程池在高核CPU服务器上会空指针导致应用无法启动
2026-09-05 22:27:25 +08:00
ageerle
b27fa844e6 fix: handle missing Qdrant collections during deletion
Treat gRPC NOT_FOUND as an idempotent delete for collections, documents and fragments so first uploads can initialize their collection. Preserve other failures and add 19 regression cases.

Refs: https://gitee.com/ageerle/ruoyi-ai/issues/IKCTSX
2026-09-05 22:19:15 +08:00
Jiali
7f728c7a92 fix: 修复 mainExecutor 线程池在高核CPU服务器上会空指针导致应用无法启动
原配置从配置文件读取 corePoolSize 和 maxPoolSize,
当配置缺失时传入 null 导致 BeanCreationException。
现改为基于 CPU 核心数动态计算,并设置固定上限 100。
2026-08-26 19:10:39 +08:00
Ansn
15ffb4ec2e docs: improve Windows Docker deployment instructions 2026-08-21 11:54:57 +08:00
ageerle
f92e510665 chore: enable demo mode 2026-08-16 20:58:48 +08:00
ageerle@163.com
997548dd4b fix: expose builtin tools to LangChain4j agents 2026-08-15 23:14:08 +08:00
ageerle
25ab33c79c feat: add coding harness runtime and CMS module 2026-08-15 23:04:34 +08:00
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
696 changed files with 56738 additions and 2577 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

1
.gitignore vendored
View File

@@ -57,3 +57,4 @@ logs/
.flattened-pom.xml
/.claude/settings.local.json
/docs/docker/milvus/volumes/
/.codex-runtime/

117
README.md
View File

@@ -23,7 +23,13 @@
</div>
## 🚀 Live Demo
| Service | URL | Login Method |
|---|---|---|
| Short Drama Demo | [drama.ruoyiai.chat](https://drama.ruoyiai.chat/) | QR code login |
| User Frontend Demo | [web.ruoyiai.chat](https://web.ruoyiai.chat/) | QR code login |
| Coding Demo | [code.ruoyiai.chat](https://code.ruoyiai.chat/) | QR code login |
## ✨ Core Features
@@ -69,24 +75,78 @@ 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):
#### Requirements
- Docker Engine(Linux/macOS)或 Docker Desktop(Windows)
- Docker Compose V2
#### Linux / macOS
```bash
# Clone the repository
git clone https://github.com/ageerle/ruoyi-ai.git
# Clone the v3.1.0 release
git clone --depth 1 --branch v3.1.0 https://github.com/ageerle/ruoyi-ai.git
cd ruoyi-ai
# 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
```
#### Windows PowerShell
If you are using Windows with Docker Desktop, you can use PowerShell to execute the following commands:
```PowerShell
# Clone the v3.1.0 release
git clone --depth 1 --branch v3.1.0 https://github.com/ageerle/ruoyi-ai.git
cd ruoyi-ai
# Create the environment configuration file
Copy-Item docs\docker\ruoyi-ai\.env.example docs\docker\ruoyi-ai\.env
# Pin the image version
(Get-Content docs\docker\ruoyi-ai\.env) `
-replace '^RUIYI_VERSION=.*', 'RUIYI_VERSION=v3.1.0' |
Set-Content docs\docker\ruoyi-ai\.env
# Check the environment configuration
Get-Content 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 --env-file docs\docker\ruoyi-ai\.env `
-f docs\docker\ruoyi-ai\docker-compose-all.yaml ps
```
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,42 +203,15 @@ 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?
**👉 [Complete Documentation](https://doc.ruoyiai.chat/)**
- [Model and API Key configuration in ruoyi-admin (Chinese)](docs/model-configuration.md)
- [Dify Cloud setup for local development (Chinese)](docs/dify-cloud.md)
## 🤝 Contributing
We warmly welcome community contributions! Whether you are a seasoned developer or just getting started, you can contribute to the project 💪
@@ -234,9 +267,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>
## 🚀 演示地址
| 服务 | 访问地址 | 登录方式 |
|---|---|---|
| 短剧演示环境 | [drama.ruoyiai.chat](https://drama.ruoyiai.chat/) | 扫码登录 |
| 用户端演示环境 | [web.ruoyiai.chat](https://web.ruoyiai.chat/) | 扫码登录 |
| 编程演示环境 | [code.ruoyiai.chat](https://code.ruoyiai.chat/) | 扫码登录 |
## ✨ 核心亮点
| 模块 | 现有能力
@@ -68,24 +76,73 @@
### 方式一:一键启动所有服务(推荐)
使用 `docker-compose-all.yaml` 可以一键启动所有服务(包括后端、管理端、用户端及依赖服务):
#### 环境要求
- Docker Engine(Linux/macOS)或 Docker Desktop(Windows)
- Docker Compose V2
#### Linux / macOS
```bash
# 克隆仓库
git clone https://github.com/ageerle/ruoyi-ai.git
# 克隆 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
```
#### Windows PowerShell
如果您使用 Windows + Docker Desktop,可以使用 PowerShell 执行以下命令:
```PowerShell
# 克隆 v3.1.0 版本
git clone --depth 1 --branch v3.1.0 https://github.com/ageerle/ruoyi-ai.git
cd ruoyi-ai
# 创建环境配置文件
Copy-Item docs\docker\ruoyi-ai\.env.example docs\docker\ruoyi-ai\.env
# 固定镜像版本
(Get-Content docs\docker\ruoyi-ai\.env) `
-replace '^RUIYI_VERSION=.*', 'RUIYI_VERSION=v3.1.0' |
Set-Content docs\docker\ruoyi-ai\.env
# 查看环境配置
Get-Content docs\docker\ruoyi-ai\.env
# 从 GHCR 拉取预构建镜像并启动全部服务
docker compose --env-file docs\docker\ruoyi-ai\.env `
-f docs\docker\ruoyi-ai\docker-compose-all.yaml pull
docker compose --env-file docs\docker\ruoyi-ai\.env `
-f docs\docker\ruoyi-ai\docker-compose-all.yaml up -d
# 查看服务状态
docker compose --env-file docs\docker\ruoyi-ai\.env `
-f docs\docker\ruoyi-ai\docker-compose-all.yaml ps
```
默认 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,42 +199,15 @@ docker-compose up -d --build
| 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/)**
- [在 ruoyi-admin 中配置模型和 API Key](docs/model-configuration.md)
- [本地开发接入 Dify 官方云服务](docs/dify-cloud.md)
## 🤝 参与贡献
我们热烈欢迎社区贡献!无论您是资深开发者还是初学者,都可以为项目贡献力量 💪
@@ -235,9 +265,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>

50
docs/dify-cloud.md Normal file
View File

@@ -0,0 +1,50 @@
# Dify 官方云服务配置
本指南适用于本地运行 RuoYi-AI Java 后端、连接 Dify 官方云服务的场景。
## 准备 Dify 应用
在 Dify 中创建并发布聊天应用,先在 Dify 自己的调试页面确认可以正常回答。首次接入可以使用 Chatbot(聊天助手)。在该应用的 API 访问页面创建应用 API Key。
应用 Key 对应具体的 Dify 应用;知识库 Key 不能用于聊天。Dify 官方云服务的 API 基础地址为 `https://api.dify.ai/v1`。参见 [Dify API 入门](https://docs.dify.ai/en/api-reference/guides/get-started) 和 [聊天应用 API](https://docs.dify.ai/en/api-reference/guides/chat)。
## 在后台配置 Key
API Key 统一在 **ruoyi-admin → 对话管理 → 模型管理** 中直接填写并保存,无需配置后端环境变量。
## 模型管理填写示例
先确认厂商管理中的 `Dify` 已启用,再在模型管理中修改 Dify 模型:
| 字段 | 填写内容 |
| --- | --- |
| 模型分类 | 聊天(`chat`) |
| 模型供应商 | Dify(编码 `dify`) |
| 模型名称 | `dify-chat` |
| 模型描述 | Dify 聊天助手 |
| 请求地址 | `https://api.dify.ai/v1` |
| 密钥 | 从 Dify 应用复制的真实 API Key,如 `app-...` |
| 是否显示 | 是 |
直接填写真实应用 Key,不需要 `env:` 或 `Bearer ` 前缀。编辑时回显已保存的 Key,默认隐藏,点击眼睛可查看原文。输入内容原样保存,清空后保存会清除原 Key。保存后新的调用读取最新配置,无需重启后端。
请求地址填写到 `/v1`,不要追加 `/chat-messages`,也不要填写 Dify 应用的网页分享链接。模型名称是本系统的选择标识,可以取一个不重复的应用名称;实际调用哪个 Dify 应用由应用 Key 决定。
已有数据库直接在模型管理中按上述内容保存即可,无需重新导入全量 SQL。保存后在聊天页面选择该模型,发送一条简单消息验证。
## 适用范围
- 官方云服务使用 `https://api.dify.ai/v1`;自建 Dify 填写后端可以访问的服务 API 地址。
- 不同模型可以填写不同的应用 Key,分别连接对应的 Dify 应用。
- 当前适配器使用聊天消息接口,普通 Workflow 应用不能通过此模型配置调用。
- 当前请求的自定义 `inputs` 为空。接入的聊天应用应能在不提供额外必填输入变量的情况下运行。
- 当前聊天适配器没有实现 Dify 的 Human Input 表单交互和文件上传流程;首次验证请使用简单文本聊天应用。
## 常见情况
| 现象 | 检查方法 |
| --- | --- |
| 后台无法保存 | 检查厂商是否启用,以及模型分类、名称等必填项是否齐全。 |
| 查看已保存的密钥 | 编辑时默认隐藏,点击密钥框右侧的眼睛查看原文;清空后保存会清除密钥。 |
| Dify 返回鉴权失败 | 在模型管理中填写当前应用的有效 API Key,检查复制时是否带入空格或 `Bearer ` 前缀。 |
| Dify 返回应用类型或输入参数错误 | 检查是否使用聊天应用,以及应用是否要求额外输入变量;先在 Dify 中验证应用可以回答。 |

View File

@@ -6,7 +6,7 @@ FROM mysql:8.0
ENV TZ=Asia/Shanghai
# MySQL 官方入口会按文件名顺序执行初始化目录中的 SQL。
# ruoyi-ai.sql 会导入 MYSQL_DATABASE,snail_job_mysql.sql 会自行创建并切换数据库。
# 主库统一为 ruoyi-ai,与初始化 SQL 和 Compose 中的数据库配置保持一致。
COPY docs/script/sql/ruoyi-ai.sql /docker-entrypoint-initdb.d/01-ruoyi-ai.sql
COPY docs/script/sql/snail_job_mysql.sql /docker-entrypoint-initdb.d/02-snail-job.sql

View File

@@ -23,7 +23,7 @@ services:
- "23306:3306"
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: ruoyi-ai-agent
MYSQL_DATABASE: ruoyi-ai
TZ: Asia/Shanghai
volumes:
- mysql-data:/var/lib/mysql
@@ -105,13 +105,19 @@ services:
# MySQL 配置
SPRING_DATASOURCE_DYNAMIC_PRIMARY: master
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_DRIVERCLASSNAME: com.mysql.cj.jdbc.Driver
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_URL: jdbc:mysql://mysql:3306/ruoyi-ai-agent?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_URL: jdbc:mysql://mysql:3306/ruoyi-ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_USERNAME: root
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_PASSWORD: root
# Redis 配置
SPRING_DATA_REDIS_HOST: redis
SPRING_DATA_REDIS_PORT: 6379
SPRING_DATA_REDIS_DATABASE: 0
# Allow large presentation files for knowledge-base ingestion.
SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE: 600MB
SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE: 620MB
VECTOR_STORE_TYPE: weaviate
VECTOR_STORE_WEAVIATE_PROTOCOL: http
VECTOR_STORE_WEAVIATE_HOST: weaviate:8080
# 日志配置
LOGGING_LEVEL_ORG_RUOYI: info
LOGGING_LEVEL_ORG_SPRINGFRAMEWORK: warn

View File

@@ -18,7 +18,7 @@ services:
- "23306:3306"
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: ruoyi-ai-agent
MYSQL_DATABASE: ruoyi-ai
TZ: Asia/Shanghai
volumes:
- ../../script/sql/ruoyi-ai.sql:/docker-entrypoint-initdb.d/01-ruoyi-ai.sql:ro
@@ -110,7 +110,7 @@ services:
# MySQL 配置
SPRING_DATASOURCE_DYNAMIC_PRIMARY: master
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_DRIVERCLASSNAME: com.mysql.cj.jdbc.Driver
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_URL: jdbc:mysql://mysql:3306/ruoyi-ai-agent?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_URL: jdbc:mysql://mysql:3306/ruoyi-ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_USERNAME: root
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_PASSWORD: root
# Redis 配置

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

View File

@@ -0,0 +1,34 @@
# 模型与 API Key 配置
模型的 API Key 统一在 **ruoyi-admin → 对话管理 → 模型管理** 中配置。
1. 在“厂商管理”中选择并启用对应厂商,填写服务 API 地址。
2. 在“模型管理”中新建或编辑模型,选择厂商、分类,填写服务商提供的模型 ID。
3. 在“密钥”框直接粘贴真实 API Key,保存模型。
4. 在 `ruoyi-web` 中选择模型使用;向量和重排模型则在对应知识库配置中选择。
需要 API Key 的厂商适配器使用该模型保存的 Key,不要求固定的 Key 前缀或环境变量名称。自定义 OpenAI、Anthropic 服务的地址与 Key 也在模型管理中配置,无需设置配对环境变量。无需鉴权的本地模型可以不填 Key。
## 修改与批量更新
- 编辑模型时回显已保存的 Key,默认以密码形式隐藏,点击眼睛可查看原文;每次重新打开编辑框都会恢复隐藏。
- 密钥按输入内容原样保存,不限制前缀或格式,也不去除首尾空格。清空后保存会清除原 Key。
- 按厂商批量更新接口会将提交的 Key 保存到该厂商的模型中。
- 保存后新的模型调用会读取最新配置,无需重启后端。正在进行的请求继续使用开始时的配置。
- 模型列表、聊天用户端和导出文件不返回 Key。
## 已有模型
如果模型中原来保存的是 `env:ATLAS_API_KEY` 等文本,请在后台将其替换为真实 Key。密钥字段按填写内容直接使用,不解析 `env:` 引用。无需重新导入数据库。
## 示例
| 场景 | 厂商编码 | 密钥框填写内容 |
| --- | --- | --- |
| 千问聊天、向量或重排 | `qianwen` | 阿里云百炼的 API Key |
| DeepSeek | `deepseek` | DeepSeek 控制台创建的 API Key |
| Atlas 多媒体 | `atlas` | Atlas 控制台创建的 API Key |
| Dify 聊天应用 | `dify` | 对应应用的 `app-...` Key |
| 自定义 OpenAI / Anthropic | `custom_api` / `custom_anthropic` | 对应服务的 API Key |
填写的地址、模型 ID 和 Key 应对应同一个服务账号。模型是否可用、支持哪些能力以及额度限制,以服务商实际响应为准。

60
docs/script/sql/README.md Normal file
View File

@@ -0,0 +1,60 @@
# SQL 脚本说明
## 全新安装
执行 [ruoyi-ai.sql](ruoyi-ai.sql) 即可创建 `ruoyi-ai` 主库和 `snail_job` 调度库,并导入初始化数据。
```sh
mysql -uroot -p < ruoyi-ai.sql
```
主 SQL 已合并下表列出的基础更新,以及原始想法、技能绑定、剧本打磨字段、模型优先级和 PC 登录超时结构。新安装仍需执行下面“后续功能更新”中注明的补充脚本。[snail_job_mysql.sql](snail_job_mysql.sql) 仅用于单独初始化调度库,执行主 SQL 后无需再执行它。
全量初始化包含 `DROP TABLE` 和调度库的 `DROP DATABASE`,已有数据库升级应使用对应的增量脚本。
## 已有数据库升级
连接目标主库后,按文件名升序执行尚未应用的更新脚本;不要根据文件重命名重复执行已应用的更新。
- `2026-06-15-trace.sql` 原名为 `update-0615-trace.sql`,内容未变;其中菜单插入没有判重,不应重复执行。
- `2026-08-30-mcp-market-tool-tenant.sql` 是一次性迁移,执行前检查脚本内的预检要求;部分执行失败后应按实际表结构续跑。
- 其余脚本包含重复执行保护,具体适用条件见各脚本注释。
## 命名约定
更新脚本统一采用 `YYYY-MM-DD-功能描述.sql`,功能描述使用小写英文和连字符,例如 `2026-09-01-sys-url.sql`。保留已有更新日期,同日脚本按文件名升序执行,并确保依赖顺序一致。
全量入口保留 `ruoyi-ai.sql`,独立调度库入口保留 `snail_job_mysql.sql`。
## 主 SQL 合并清单
截至 2026-09-01,`update` 目录的 9 个脚本均已体现在主 SQL 的最终表结构和初始化数据中。结构变更直接并入建表语句,重复配置按最终值保留一份;旧库回填及兼容逻辑保留在增量脚本中。
| 更新脚本 | 主 SQL 中对应内容 |
| --- | --- |
| [2026-06-15-trace.sql](update/2026-06-15-trace.sql) | `trace_run`、`trace_node` 表、租户字段和索引,以及链路追踪菜单 |
| [2026-07-20-knowledge-fragment-fid.sql](update/2026-07-20-knowledge-fragment-fid.sql) | 附件 `file_hash`、片段 `fid`、可空的 `doc_id` 和知识库租户索引 |
| [2026-07-21-chat-provider-icon-length.sql](update/2026-07-21-chat-provider-icon-length.sql) | `chat_provider.provider_icon` 扩展为 `varchar(1000)` |
| [2026-07-21-sys-config-node-template.sql](update/2026-07-21-sys-config-node-template.sql) | 7 个工作流节点消息模板配置 |
| [2026-07-24-register-default-role-and-missing-menus.sql](update/2026-07-24-register-default-role-and-missing-menus.sql) | 默认注册角色、角色菜单关联、默认角色配置,以及会话、附件和片段权限 |
| [2026-07-29-sys-config-node-template-all.sql](update/2026-07-29-sys-config-node-template-all.sql) | 工作流全部 8 个模板,前 7 个配置与上一批模板合并去重 |
| [2026-07-29-zhipu-web-search-node.sql](update/2026-07-29-zhipu-web-search-node.sql) | 智谱网络搜索节点及其消息模板;内部组件名保留 `Google` 以兼容已有流程 |
| [2026-08-30-mcp-market-tool-tenant.sql](update/2026-08-30-mcp-market-tool-tenant.sql) | `mcp_market_tool` 租户、审计字段和 `idx_tenant_market` 索引 |
| [2026-09-01-sys-url.sql](update/2026-09-01-sys-url.sql) | `sys_url` 表、5 个菜单权限和 2 条公开链接 |
后续增加更新脚本时,同步修改主 SQL 中对应的表结构或初始化数据,并更新本清单及主 SQL 文件头。
## 后续功能更新
以下脚本统一位于 `update/`。已有数据库按日期升级,保留原始输入、已批准媒体和提供商密钥;不要执行全量初始化。
| 脚本 | 内容与新安装要求 |
| --- | --- |
| [知识库检索节点](update/2026-09-11-knowledge-retrieval-node.sql) | 补充节点注册,新安装也需执行 |
| [短剧视觉资产](update/2026-09-30-short-drama-visual-asset.sql) | 补充异步视觉资产表,新安装也需执行 |
| [剧本打磨与 Seedance 2.5](update/2026-09-30-script-refinement-seedance-25.sql) | 打磨字段已并入全量结构;新安装执行以补充视频模型记录 |
| [技能绑定](update/2026-10-01-short-drama-skill-bindings.sql) | 项目审美与导演绑定,幂等;全量结构已包含 |
| [模型优先级](update/2026-10-02-chat-model-sort-order.sql) | 幂等增加优先级列,保留已自定义优先级;全量结构已包含 |
| [PC 登录空闲时间](update/2026-10-02-pc-login-idle-timeout.sql) | 仅调整原 30 分钟配置;全量默认已更新,已签发令牌需重新登录 |
| [文字模型默认优先级](update/2026-10-03-short-drama-flash-default.sql) | 先应用模型优先级迁移,再调整已有 Flash / Seed 模型排序,不新增模型或密钥 |
| [原始创作输入](update/2026-10-03-short-drama-original-idea.sql) | 幂等增加独立原文列,不用生成概要回填;全量结构已包含 |

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,41 @@
-- ----------------------------
-- MCP 市场工具租户与审计列补齐
-- 2026-08-30
-- ----------------------------
-- 本脚本只迁移结构和归属信息,不删除无法关联到市场的孤儿记录。
-- 孤儿记录的 tenant_id 保持 NULL,因此租户归属查询会 fail closed。
-- 这是一次性迁移脚本:两个 ALTER TABLE 都会隐式提交。若执行中断或仅完成部分 DDL,
-- 必须先核对实际表结构后逐句续跑,禁止直接重跑整个文件。
-- 本机受控执行入口为 scripts/apply-live-migrations.ps1;先使用 -PreflightOnly,
-- 再以同一个显式 DPAPI 备份路径执行正式迁移。生产环境仍需按自身发布流程审计后执行。
-- 首次 DDL 前预检;上线前应确认结果为 0,避免新增列后留下无法可信回填的归属。
SELECT COUNT(*) AS `preflight_invalid_market_owner_count`
FROM `mcp_market_tool` AS `tool`
LEFT JOIN `mcp_market_info` AS `market` ON `market`.`id` = `tool`.`market_id`
WHERE `market`.`id` IS NULL
OR `market`.`tenant_id` IS NULL;
ALTER TABLE `mcp_market_tool`
ADD COLUMN `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '租户编号' AFTER `local_tool_id`,
ADD COLUMN `create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门' AFTER `tenant_id`,
ADD COLUMN `create_by` bigint NULL DEFAULT NULL COMMENT '创建者' AFTER `create_dept`,
ADD COLUMN `update_by` bigint NULL DEFAULT NULL COMMENT '更新者' AFTER `create_time`,
ADD COLUMN `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间' AFTER `update_by`;
UPDATE `mcp_market_tool` AS `tool`
INNER JOIN `mcp_market_info` AS `market` ON `market`.`id` = `tool`.`market_id`
SET `tool`.`tenant_id` = `market`.`tenant_id`;
ALTER TABLE `mcp_market_tool`
MODIFY COLUMN `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '000000' COMMENT '租户编号',
ADD INDEX `idx_tenant_market` (`tenant_id`, `market_id`);
-- 上线前应确认结果为 0;统计覆盖父市场缺失、子表租户为空、父子租户为空或不一致。
SELECT COUNT(*) AS `invalid_market_tool_tenant_count`
FROM `mcp_market_tool` AS `tool`
LEFT JOIN `mcp_market_info` AS `market` ON `market`.`id` = `tool`.`market_id`
WHERE `market`.`id` IS NULL
OR `tool`.`tenant_id` IS NULL
OR `market`.`tenant_id` IS NULL
OR NOT (BINARY `tool`.`tenant_id` <=> BINARY `market`.`tenant_id`);

View File

@@ -0,0 +1,76 @@
-- ----------------------------------------------------------------------------
-- URL 管理模块增量脚本(可重复执行)
-- 1) 创建 sys_url 表
-- 2) 新增「系统管理 -> URL管理」菜单及 4 个按钮权限(按 perms 去重,幂等)
-- 3) 插入两条初始公开网站链接(按 name + url 去重,幂等)
-- 适用于已存在数据库的升级;全新安装请直接使用 ruoyi-ai.sql
-- ----------------------------------------------------------------------------
-- ----------------------------
-- 1. URL 管理表 sys_url
-- ----------------------------
CREATE TABLE IF NOT EXISTS `sys_url` (
`url_id` bigint NOT NULL AUTO_INCREMENT COMMENT '链接ID',
`tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '000000' COMMENT '租户编号',
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '链接名称',
`url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'HTTP(S) 地址',
`description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '链接说明',
`sort_order` int NULL DEFAULT 0 COMMENT '显示顺序',
`status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '状态(0正常 1停用)',
`create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门',
`create_by` bigint NULL DEFAULT NULL COMMENT '创建者',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_by` bigint NULL DEFAULT NULL COMMENT '更新者',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`url_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'URL 管理表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- 2. 菜单 & 按钮权限(幂等:perms 不存在时才插入)
-- 父菜单 1 = 系统管理目录
-- ----------------------------
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), 'URL管理', 1, 12, 'url', 'system/url/index', '', 1, 0, 'C', '0', '0', 'system:url:list', 'mdi:link-variant', 103, 1, NOW(), 'URL管理菜单'
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `perms` = 'system:url:list' AND `menu_type` = 'C');
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), 'URL查询', (SELECT `menu_id` FROM `sys_menu` WHERE `perms` = 'system:url:list' AND `menu_type` = 'C' LIMIT 1), 1, '#', '', '', 1, 0, 'F', '0', '0', 'system:url:query', '#', 103, 1, NOW(), ''
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `perms` = 'system:url:query' AND `menu_type` = 'F');
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), 'URL新增', (SELECT `menu_id` FROM `sys_menu` WHERE `perms` = 'system:url:list' AND `menu_type` = 'C' LIMIT 1), 2, '#', '', '', 1, 0, 'F', '0', '0', 'system:url:add', '#', 103, 1, NOW(), ''
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `perms` = 'system:url:add' AND `menu_type` = 'F');
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), 'URL修改', (SELECT `menu_id` FROM `sys_menu` WHERE `perms` = 'system:url:list' AND `menu_type` = 'C' LIMIT 1), 3, '#', '', '', 1, 0, 'F', '0', '0', 'system:url:edit', '#', 103, 1, NOW(), ''
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `perms` = 'system:url:edit' AND `menu_type` = 'F');
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), 'URL删除', (SELECT `menu_id` FROM `sys_menu` WHERE `perms` = 'system:url:list' AND `menu_type` = 'C' LIMIT 1), 4, '#', '', '', 1, 0, 'F', '0', '0', 'system:url:remove', '#', 103, 1, NOW(), ''
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `perms` = 'system:url:remove' AND `menu_type` = 'F');
-- ----------------------------
-- 3. 初始公开网站链接(幂等:name + url 均不存在时才插入)
-- ----------------------------
INSERT INTO `sys_url`
(`tenant_id`, `name`, `url`, `description`, `sort_order`, `status`, `create_dept`, `create_by`, `create_time`, `remark`)
SELECT '000000', '若依官网', 'https://ruoyi.vip', 'RuoYi 官方网站,提供文档、源码与社区支持', 1, '0', 103, 1, NOW(), '初始公开链接'
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM `sys_url` WHERE `name` = '若依官网' AND `url` = 'https://ruoyi.vip');
INSERT INTO `sys_url`
(`tenant_id`, `name`, `url`, `description`, `sort_order`, `status`, `create_dept`, `create_by`, `create_time`, `remark`)
SELECT '000000', 'RuoYi-AI 开源仓库', 'https://github.com/ageerle/ruoyi-ai', 'RuoYi-AI 项目 GitHub 仓库', 2, '0', 103, 1, NOW(), '初始公开链接'
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM `sys_url` WHERE `name` = 'RuoYi-AI 开源仓库' AND `url` = 'https://github.com/ageerle/ruoyi-ai');

View File

@@ -0,0 +1,24 @@
-- 知识库检索工作流节点注册
-- 为新安装和已有环境提供幂等的节点注册
INSERT INTO `t_workflow_component`
(`uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`,
`create_time`, `update_time`, `is_deleted`, `tenant_id`)
SELECT
'b8e3c1d55f6a4d92b0e7f214d3c58a29',
'KnowledgeRetrieval',
'知识库检索',
'从知识库中检索相关内容,支持向量检索和混合检索',
30,
1,
NOW(),
NOW(),
0,
'000000'
FROM DUAL
WHERE NOT EXISTS (
SELECT 1
FROM `t_workflow_component`
WHERE `name` = 'KnowledgeRetrieval'
AND `tenant_id` = '000000'
);

View File

@@ -0,0 +1,19 @@
-- Run after the base ruoyi-ai.sql. Preserves existing scripts and provider keys.
DROP PROCEDURE IF EXISTS migrate_drama_preparation;
DELIMITER $$
CREATE PROCEDURE migrate_drama_preparation()
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='short_drama_script' AND column_name='creation_mode') THEN
ALTER TABLE short_drama_script ADD creation_mode varchar(32) DEFAULT 'standard', ADD source_materials longtext, ADD worldbuilding longtext, ADD revision_notes longtext;
END IF;
END$$
DELIMITER ;
CALL migrate_drama_preparation();
DROP PROCEDURE migrate_drama_preparation;
INSERT INTO chat_model (id,category,model_name,provider_code,model_describe,model_show,api_host,api_key,create_dept,create_by,create_time,update_by,update_time,remark,tenant_id)
SELECT 2106002500000000001,'video','bytedance/seedance-2.5/reference-to-video','atlas','Seedance 2.5 多模态参考生视频(字节跳动)','Y',
COALESCE((SELECT api_host FROM (SELECT api_host FROM chat_model WHERE provider_code='atlas' AND category='video' AND tenant_id=0 ORDER BY id DESC LIMIT 1) h),'https://api.atlascloud.ai/api/v1'),
COALESCE((SELECT api_key FROM (SELECT api_key FROM chat_model WHERE provider_code='atlas' AND category='video' AND tenant_id=0 ORDER BY id DESC LIMIT 1) k),''),
103,1,NOW(),1,NOW(),'Atlas Seedance 2.5 reference-to-video; 4-30s, up to 30 images and 10 audio references',0
WHERE NOT EXISTS (SELECT 1 FROM chat_model WHERE model_name='bytedance/seedance-2.5/reference-to-video' AND tenant_id=0);

View File

@@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS short_drama_visual_asset (
id bigint NOT NULL PRIMARY KEY,
project_id bigint NOT NULL,
tenant_id varchar(20) NOT NULL DEFAULT '000000',
storyboard_id bigint DEFAULT NULL,
asset_key varchar(180) NOT NULL,
kind varchar(24) NOT NULL,
title varchar(255) NOT NULL,
prompt longtext,
reference_images longtext,
source_hash varchar(64),
status varchar(24) NOT NULL DEFAULT 'pending',
image_url text,
model varchar(255),
prediction_id varchar(255),
error_message text,
create_time datetime DEFAULT CURRENT_TIMESTAMP,
update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_project_asset (project_id,asset_key),
KEY idx_storyboard (storyboard_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='道具资产和逐镜关键帧,可续跑';

View File

@@ -0,0 +1,13 @@
-- MySQL idempotent migration; run in the configured application database.
SET @skill_aesthetic_sql = (SELECT IF(COUNT(*) = 0,
'ALTER TABLE short_drama_project ADD COLUMN aesthetic_skill_name VARCHAR(64) NULL COMMENT ''Selected aesthetic skill catalog name''',
'SELECT 1') FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'short_drama_project' AND column_name = 'aesthetic_skill_name');
PREPARE skill_aesthetic_stmt FROM @skill_aesthetic_sql;
EXECUTE skill_aesthetic_stmt;
DEALLOCATE PREPARE skill_aesthetic_stmt;
SET @skill_director_sql = (SELECT IF(COUNT(*) = 0,
'ALTER TABLE short_drama_project ADD COLUMN director_skill_name VARCHAR(64) NULL COMMENT ''Selected director skill catalog name''',
'SELECT 1') FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'short_drama_project' AND column_name = 'director_skill_name');
PREPARE skill_director_stmt FROM @skill_director_sql;
EXECUTE skill_director_stmt;
DEALLOCATE PREPARE skill_director_stmt;

View File

@@ -0,0 +1,8 @@
-- Idempotent schema upgrade; preserve credentials and explicitly customized priorities.
SET @sort_order_sql = (SELECT IF(COUNT(*) = 0,
'ALTER TABLE chat_model ADD COLUMN sort_order INT NOT NULL DEFAULT 100 COMMENT ''默认模型优先级,升序'' AFTER category',
'SELECT 1') FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'chat_model' AND column_name = 'sort_order');
PREPARE sort_order_stmt FROM @sort_order_sql;
EXECUTE sort_order_stmt;
DEALLOCATE PREPARE sort_order_stmt;
UPDATE chat_model SET sort_order = 0 WHERE provider_code = 'atlas' AND sort_order = 100 AND model_name IN ('bytedance/doubao-seed-2.1-pro-260628', 'openai/gpt-image-2.5-flare/text-to-image', 'bytedance/seedance-2.0-mini/reference-to-video');

View File

@@ -0,0 +1,7 @@
-- PC 管理后台和短剧工作台:无操作 12 小时后登录失效(单位:秒)。
-- 仅迁移原 30 分钟策略,保留已自定义的客户端设置。
UPDATE sys_client
SET active_timeout = 43200
WHERE device_type = 'pc' AND active_timeout = 1800;
-- 已部署环境通过 /system/client 更新相同字段以同步清理客户端缓存。
-- 已签发的令牌需重新登录后使用新策略。

View File

@@ -0,0 +1,6 @@
-- Existing Atlas keys/hosts remain intact. Default priority is maintained through model management.
-- Apply after 2026-10-02-chat-model-sort-order.sql.
UPDATE chat_model SET sort_order = 10
WHERE provider_code = 'atlas' AND category = 'chat' AND model_name = 'bytedance/doubao-seed-2.1-pro-260628' AND sort_order = 0;
UPDATE chat_model SET sort_order = 0
WHERE provider_code = 'atlas' AND category = 'chat' AND model_name = 'deepseek-ai/deepseek-v4.1-flash';

View File

@@ -0,0 +1,8 @@
-- Store the exact creation input separately from generated project descriptions.
-- Existing rows stay NULL unless their original request can be recovered.
SET @original_idea_sql = (SELECT IF(COUNT(*) = 0,
'ALTER TABLE short_drama_project ADD COLUMN original_idea LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT ''Original story creation input'' AFTER description',
'SELECT 1') FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'short_drama_project' AND column_name = 'original_idea');
PREPARE original_idea_stmt FROM @original_idea_sql;
EXECUTE original_idea_stmt;
DEALLOCATE PREPARE original_idea_stmt;

View File

@@ -0,0 +1,8 @@
-- Full image prompt histories can exceed TEXT's 65535 byte capacity after a few revisions.
-- Enlarge both the history and undo columns while retaining all earlier candidates.
ALTER TABLE short_drama_character_appearance
MODIFY COLUMN image_descriptions MEDIUMTEXT NULL COMMENT '每张图片对应的提示词(JSON数组)',
MODIFY COLUMN previous_descriptions MEDIUMTEXT NULL COMMENT '上一轮提示词列表(撤销用,JSON数组)';
ALTER TABLE short_drama_location
MODIFY COLUMN image_descriptions MEDIUMTEXT NULL COMMENT '每张图片对应的提示词(JSON数组)',
MODIFY COLUMN previous_descriptions MEDIUMTEXT NULL COMMENT '上一轮提示词列表(撤销用,JSON数组)';

View File

@@ -0,0 +1,7 @@
-- Add ordered multi-selection without rewriting existing project bindings.
SET @storyboard_skills_sql = (SELECT IF(COUNT(*) = 0,
'ALTER TABLE short_drama_project ADD COLUMN storyboard_skill_names TEXT NULL COMMENT ''Ordered storyboard skill catalog names as JSON''',
'SELECT 1') FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'short_drama_project' AND column_name = 'storyboard_skill_names');
PREPARE storyboard_skills_stmt FROM @storyboard_skills_sql;
EXECUTE storyboard_skills_stmt;
DEALLOCATE PREPARE storyboard_skills_stmt;

47
docs/tenant-management.md Normal file
View File

@@ -0,0 +1,47 @@
# 新增租户接口
开启 `tenant.enable: true` 后,超级管理员可通过 `POST /system/tenant` 创建租户。请求需要登录凭证和 `system:tenant:add` 权限;使用反向代理时,请按部署配置添加接口前缀。
## 请求参数
请求使用 `Content-Type: application/json`。
| 参数 | 说明 |
| --- | --- |
| `companyName` | 企业名称,必填且不能与已有租户重复。 |
| `contactUserName` | 联系人,必填。 |
| `contactPhone` | 联系电话,必填。 |
| `username` | 租户初始管理员用户名,必填。 |
| `password` | 租户初始管理员密码,必填。 |
| `packageId` | 已存在的租户套餐 ID,必填。建议使用字符串传输,避免浏览器对大整数的精度损失。 |
| `expireTime` | 过期时间,使用 `yyyy-MM-dd HH:mm:ss`,例如 `2027-09-11 00:00:00`;不传或传 `null` 表示不限制有效期。未带时区的时间按后端运行时区解释。 |
| `accountCount` | 可创建的用户数量,`-1` 表示不限制。 |
## 请求示例
请将套餐 ID 替换为当前系统中的有效套餐,并设置实际使用的管理员账号和密码。
```json
{
"companyName": "示例企业",
"contactUserName": "联系人",
"contactPhone": "13800000000",
"username": "tenant_admin",
"password": "ChangeMe_123456!",
"packageId": "2018611998196109314",
"expireTime": "2027-09-11 00:00:00",
"accountCount": -1
}
```
## 日期配置
后端默认日期格式配置位于 `ruoyi-admin/src/main/resources/application.yml`:
```yaml
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
```
扩展应用的 `ObjectMapper` 时,使用 Spring 注入的 `Jackson2ObjectMapperBuilder`,以保留全局日期格式、自定义 Jackson 模块和构建器配置。

35
docs/url-management.md Normal file
View File

@@ -0,0 +1,35 @@
# URL 管理
URL 管理用于维护 Copilot 新建编程任务页的常用链接。管理端入口为“系统管理 → URL管理”(`/system/url`),支持新增、编辑、批量删除、名称和状态筛选及分页。
## 安装与权限
已有数据库执行 [增量脚本](script/sql/update/2026-09-01-sys-url.sql),可重复执行。新安装使用包含 URL 表、菜单及初始链接的全量 `docs/script/sql/ruoyi-ai.sql`。脚本创建 `sys_url` 表和菜单;普通管理角色需分配对应菜单及按钮权限。
| 接口 | 权限 | 用途 |
| --- | --- | --- |
| `GET /system/url/list` | `system:url:list` | 名称、状态筛选和分页 |
| `GET /system/url/{urlId}` | `system:url:query` | 编辑回填 |
| `POST /system/url` | `system:url:add` | 新增 |
| `PUT /system/url` | `system:url:edit` | 编辑 |
| `DELETE /system/url/{urlIds}` | `system:url:remove` | 删除,多个 ID 以逗号分隔 |
| `GET /system/url/shortcuts` | `coding:harness:use` 或 `system:url:list` | 当前租户的已启用链接 |
链接地址仅允许带有效主机名的 HTTP(S) 地址。状态 `0` 表示启用,`1` 表示停用;列表和快捷入口按 `sortOrder`、`urlId` 升序排列。数据查询和修改遵循当前租户隔离。
## 使用
在管理端填写名称、地址、说明、顺序和状态后保存。Copilot 点击“新建编程任务”会刷新快捷入口;停用的链接不显示。卡片显示名称、说明和外链标识,并以 `noopener,noreferrer` 在新标签页打开。加载中、空列表和请求失败均有提示,失败时可重试。
后端实现位于 `ruoyi-modules/ruoyi-system` 的 `SysUrlController`、`SysUrlServiceImpl`;管理端位于 `ruoyi-admin/apps/web-antd/src/views/system/url`;Copilot 使用 `UrlShortcutCards.vue` 和 `useUrlShortcuts.ts`。
## 2026-09-08 最终验收
- 后端打包、管理端构建和 Copilot 构建通过;Copilot 构建包含类型检查。
- URL 接口 14/14,覆盖 CRUD、分页、排序、输入校验、权限和租户隔离。
- Copilot 单元测试 18/18,卡片状态与点击检查 7/7,EXTERNAL 计划工具回归 2/2。
- 增量 SQL 连续执行两次通过,全量 SQL 的 URL 模块在临时隔离库通过。
- 浏览器完成新增、编辑、筛选、停用隐藏、重新启用刷新和卡片说明检查;临时数据已清理。
- 管理端全项目仍有 103 项既有类型诊断,URL 模块为 0 项。
本轮还修复了 EXTERNAL 模式隐藏 `plan_step` 的框架问题,使模型能在证据校验后完成计划。原生运行和计划均已到达 `COMPLETED`。本轮为本地验收;未包含远程部署。新标签页打开参数通过组件测试确认,本轮未新增真实弹窗或手机尺寸实测结论。

View File

@@ -4,9 +4,6 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
/**
* 启动程序
*
@@ -16,66 +13,10 @@ import java.net.ServerSocket;
public class RuoYiAIApplication {
public static void main(String[] args) {
killPortProcess(6039);
SpringApplication application = new SpringApplication(RuoYiAIApplication.class);
application.setApplicationStartup(new BufferingApplicationStartup(2048));
application.run(args);
System.out.println("(♥◠‿◠)ノ゙ RuoYi-AI启动成功 ლ(´ڡ`ლ)");
}
/**
* 检查并终止占用指定端口的进程
*
* @param port 端口号
*/
private static void killPortProcess(int port) {
try {
if (!isPortInUse(port)) {
return;
}
System.out.println("端口 " + port + " 已被占用,正在查找并终止进程...");
ProcessBuilder pb = new ProcessBuilder("netstat", "-ano");
Process process = pb.start();
java.io.BufferedReader reader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
if (line.contains(":" + port + " ") && line.contains("LISTENING")) {
String[] parts = line.trim().split("\\s+");
String pid = parts[parts.length - 1];
System.out.println("找到占用端口 " + port + " 的进程 PID: " + pid + ",正在终止...");
ProcessBuilder killPb = new ProcessBuilder("taskkill", "/F", "/PID", pid);
Process killProcess = killPb.start();
int exitCode = killProcess.waitFor();
if (exitCode == 0) {
System.out.println("进程 " + pid + " 已成功终止");
} else {
System.out.println("终止进程 " + pid + " 失败,exitCode: " + exitCode);
}
break;
}
}
// 等待一小段时间确保端口释放
Thread.sleep(500);
} catch (Exception e) {
System.out.println("检查/终止端口进程时发生异常: " + e.getMessage());
}
}
/**
* 检查端口是否被占用
*/
private static boolean isPortInUse(int port) {
try (ServerSocket socket = new ServerSocket()) {
socket.bind(new InetSocketAddress(port));
return false;
} catch (Exception e) {
return true;
}
}
}

View File

@@ -17,6 +17,26 @@ mcp:
enabled: false
url: http://localhost:8085/sse
--- # Local Harness workspace boundary. Override the environment variable to narrow it further.
coding:
harness:
workspace:
shared-root: ${CODING_HARNESS_WORKSPACE_SHARED_ROOT:D:/Project}
journal:
# Keep local projection disabled unless an operator explicitly chooses a destination.
enabled: ${CODING_HARNESS_JOURNAL_ENABLED:false}
output-directory: ${CODING_HARNESS_JOURNAL_OUTPUT_DIRECTORY:./data/coding-harness-journal}
# 本地实战也必须显式提供 Docker CLI 绝对路径与不可变镜像;默认保持关闭。
tools:
execute-process:
enabled: ${CODING_HARNESS_EXECUTE_PROCESS_ENABLED:false}
sandbox:
docker-executable: ${CODING_HARNESS_DOCKER_EXECUTABLE:}
docker-executable-sha256: ${CODING_HARNESS_DOCKER_EXECUTABLE_SHA256:}
docker-config-directory: ${CODING_HARNESS_DOCKER_CONFIG_DIRECTORY:}
docker-host: ${CODING_HARNESS_DOCKER_HOST:}
image: ${CODING_HARNESS_SANDBOX_IMAGE:}
--- # 上传文件地址
sys:
upload:
@@ -46,7 +66,7 @@ spring:
# 动态数据源文档 https://www.kancloud.cn/tracy5546/dynamic-datasource/content
dynamic:
# 性能分析插件(有性能损耗 不建议生产环境使用)
p6spy: true
p6spy: false
# 设置默认的数据源或者数据源组,默认值即为 master
primary: master
# 严格模式 匹配不到数据源则报错

View File

@@ -47,7 +47,7 @@ spring:
# 动态数据源文档 https://www.kancloud.cn/tracy5546/dynamic-datasource/content
dynamic:
# 性能分析插件(有性能损耗 不建议生产环境使用)
p6spy: true
p6spy: false
# 设置默认的数据源或者数据源组,默认值即为 master
primary: master
# 严格模式 匹配不到数据源则报错

View File

@@ -20,6 +20,35 @@ server:
# 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载
worker: 256
# execute_process 只能经由固定镜像的 Docker OS sandbox;缺任一配置均 fail closed。
coding:
harness:
journal:
# Operator HTML projection is opt-in; production paths must be chosen by deployment config.
enabled: ${CODING_HARNESS_JOURNAL_ENABLED:false}
output-directory: ${CODING_HARNESS_JOURNAL_OUTPUT_DIRECTORY:./data/coding-harness-journal}
budget:
# 模型回合上限;工具额度按每回合最多 3 次保持同比例,避免工具预算先于回合耗尽。
# No task iteration or wall-time cutoff; context compaction supports continued work.
# Optional cumulative spending ceilings (0 = disabled).
max-tool-calls: 0
max-input-tokens: 0
max-output-tokens: 0
tools:
execute-process:
enabled: ${CODING_HARNESS_EXECUTE_PROCESS_ENABLED:false}
sandbox:
# 必须是宿主 Docker CLI 的绝对路径;不会搜索或继承宿主 PATH/HOME。
docker-executable: ${CODING_HARNESS_DOCKER_EXECUTABLE:}
# 对 CLI 文件内容做每次控制调用前复核,不能只信文件名或绝对路径。
docker-executable-sha256: ${CODING_HARNESS_DOCKER_EXECUTABLE_SHA256:}
# 必须是绝对、无链接且始终为空的专用目录,阻断宿主 config.json/proxy/credential helper。
docker-config-directory: ${CODING_HARNESS_DOCKER_CONFIG_DIRECTORY:}
# 只接受平台本地 Docker Engine(Windows named pipe / Unix socket),拒绝远端 context。
docker-host: ${CODING_HARNESS_DOCKER_HOST:}
# 仅接受 evaluator 批准的本地 sha256:<64 hex> image ID,永不 pull/build。
image: ${CODING_HARNESS_SANDBOX_IMAGE:}
--- # 小程序对话 WebSocket 兜底默认模型(前端未传 model 且无智能体绑定时使用)
chat:
default-model: deepseek-v4-flash
@@ -43,6 +72,15 @@ logging:
org.springframework: warn
org.mybatis.spring.mapper: error
org.apache.fury: warn
# MCP traffic/stdout may contain tool arguments, results or credentials.
MCP: "OFF"
dev.langchain4j.mcp: "OFF"
dev.langchain4j.mcp.McpToolProvider: "OFF"
dev.langchain4j.mcp.client.logging.DefaultMcpLogMessageHandler: "OFF"
dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport: "OFF"
dev.langchain4j.mcp.client.transport.stdio.ProcessStderrHandler: "OFF"
dev.langchain4j.mcp.transport.stdio.JsonRpcIoHandler: "ERROR"
dev.langchain4j.mcp.client.transport.http.StreamableHttpMcpTransport: "OFF"
config: classpath:logback-plus.xml
# 用户配置
@@ -84,9 +122,9 @@ spring:
servlet:
multipart:
# 单个文件大小
max-file-size: 10MB
max-file-size: 30MB
# 设置总上传的文件大小
max-request-size: 20MB
max-request-size: 40MB
mvc:
# 设置静态资源路径 防止所有请求都去查静态资源
static-path-pattern: /static/**
@@ -129,7 +167,6 @@ security:
- /*/api-docs/**
- /warm-flow-ui/config
- /workflow/run
- /coding/**
# 多租户配置
tenant:
# 是否开启
@@ -234,6 +271,9 @@ xss:
# 排除链接
excludeUrls:
- /system/notice
# Coding prompts contain source text (for example TypeScript <T>), rendered as escaped Markdown.
- /coding/harness/sessions/*/runs
- /coding/harness/sessions/*/runs/*/inputs
--- # 链路追踪配置
trace:
@@ -280,7 +320,7 @@ websocket:
--- # 演示模式配置
demo:
# 是否开启演示模式(开启后所有写操作将被拦截)
enabled: false
enabled: ${DEMO_ENABLED:false}
# 提示消息
message: "演示模式,不允许操作"
# 排除的路径(这些路径不受演示模式限制)
@@ -375,4 +415,4 @@ short-drama:
max-redirects: ${SHORT_DRAMA_DOWNLOAD_MAX_REDIRECTS:5}
# 生产环境建议配置为视频供应商或 OSS/CDN 域名,多个值用逗号分隔。
allowed-hosts: ${SHORT_DRAMA_DOWNLOAD_ALLOWED_HOSTS:}
fake-ip-allowed-hosts: ${SHORT_DRAMA_DOWNLOAD_FAKE_IP_ALLOWED_HOSTS:atlas-media.oss-us-west-1.aliyuncs.com}
fake-ip-allowed-hosts: ${SHORT_DRAMA_DOWNLOAD_FAKE_IP_ALLOWED_HOSTS:atlas-media.oss-us-west-1.aliyuncs.com,ark-acg-ap-southeast-1.tos-ap-southeast-1.volces.com}

View File

@@ -0,0 +1,143 @@
package org.ruoyi.config;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.ruoyi.common.json.config.JacksonConfig;
import org.ruoyi.system.domain.bo.SysTenantBo;
import org.ruoyi.workflow.config.BeanConfig;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.http.MockHttpInputMessage;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@Tag("dev")
class TenantRequestJacksonTest {
private static final String TENANT_REQUEST = """
{
"contactPhone": "13700000000",
"expireTime": "2027-09-11 00:00:00",
"accountCount": -1,
"companyName": "第一集",
"contactUserName": "刘备",
"username": "user",
"password": "123456",
"packageId": "2018611998196109314"
}
""";
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(JacksonConfig.class, JacksonAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class))
.withUserConfiguration(BeanConfig.class);
@Test
void readsTenantRequestWithApplicationDateFormat() {
contextRunner.withPropertyValues("spring.jackson.date-format=yyyy-MM-dd HH:mm:ss").run(context -> {
MappingJackson2HttpMessageConverter converter = context.getBean(MappingJackson2HttpMessageConverter.class);
assertSame(context.getBean(ObjectMapper.class), converter.getObjectMapper());
SysTenantBo tenant = readTenant(converter, TENANT_REQUEST);
assertEquals(expirationDate(), tenant.getExpireTime());
assertEquals(2018611998196109314L, tenant.getPackageId());
assertEquals(-1L, tenant.getAccountCount());
assertEquals("第一集", tenant.getCompanyName());
assertEquals("user", tenant.getUsername());
});
}
@Test
void keepsRegisteredDateDeserializerWithoutDateFormatProperty() {
contextRunner.run(context -> {
SysTenantBo tenant = readTenant(context.getBean(MappingJackson2HttpMessageConverter.class), TENANT_REQUEST);
assertEquals(expirationDate(), tenant.getExpireTime());
});
}
@Test
void honorsConfiguredDateSerializationFormat() {
contextRunner.withPropertyValues("spring.jackson.date-format=yyyy/MM/dd HH:mm:ss").run(context -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
String json = mapper.writeValueAsString(new DateValue(expirationDate()));
assertEquals("{\"expireTime\":\"2027/09/11 00:00:00\"}", json);
});
}
@Test
void preservesWorkflowJsonRoundTripAndNullOmission() {
contextRunner.run(context -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
WorkflowValue value = new WorkflowValue(2018611998196109314L,
LocalDateTime.of(2027, 9, 11, 0, 0), Optional.of("workflow"), null);
String json = mapper.writeValueAsString(value);
JsonNode tree = mapper.readTree(json);
assertTrue(tree.get("id").isTextual());
assertEquals("2018611998196109314", tree.get("id").asText());
assertEquals("2027-09-11 00:00:00", tree.get("createdAt").asText());
assertEquals("workflow", tree.get("name").asText());
assertFalse(tree.has("remark"));
assertEquals(value, mapper.readValue(json, WorkflowValue.class));
});
}
@Test
void acceptsNullExpirationDate() {
contextRunner.run(context -> {
String request = TENANT_REQUEST.replace("\"2027-09-11 00:00:00\"", "null");
SysTenantBo tenant = readTenant(context.getBean(MappingJackson2HttpMessageConverter.class), request);
assertNull(tenant.getExpireTime());
});
}
@Test
void rejectsInvalidExpirationDate() {
contextRunner.run(context -> {
String request = TENANT_REQUEST.replace("2027-09-11 00:00:00", "invalid-date");
MappingJackson2HttpMessageConverter converter = context.getBean(MappingJackson2HttpMessageConverter.class);
assertThrows(HttpMessageNotReadableException.class, () -> readTenant(converter, request));
});
}
private static SysTenantBo readTenant(MappingJackson2HttpMessageConverter converter, String json) throws Exception {
MockHttpInputMessage input = new MockHttpInputMessage(json.getBytes(StandardCharsets.UTF_8));
input.getHeaders().setContentType(MediaType.APPLICATION_JSON);
return (SysTenantBo) converter.read(SysTenantBo.class, input);
}
private static Date expirationDate() {
return Date.from(LocalDateTime.of(2027, 9, 11, 0, 0).atZone(ZoneId.systemDefault()).toInstant());
}
private record DateValue(Date expireTime) {
}
private record WorkflowValue(Long id, LocalDateTime createdAt, Optional<String> name, String remark) {
}
}

View File

@@ -68,6 +68,12 @@
<artifactId>swagger-annotations</artifactId>
<version>${swagger-annotations.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -1,10 +1,12 @@
package org.ruoyi.common.chat.domain.bo.chat;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.github.linpeilie.annotations.AutoMapper;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.ruoyi.common.chat.entity.chat.ChatModel;
import org.ruoyi.common.core.validate.AddGroup;
import org.ruoyi.common.core.validate.EditGroup;
@@ -33,6 +35,9 @@ public class ChatModelBo extends BaseEntity {
@NotBlank(message = "模型分类不能为空", groups = { AddGroup.class, EditGroup.class })
private String category;
/** 默认模型优先级,数值越小越优先;同值按ID排序。 */
private Integer sortOrder;
/**
* 模型名称
*/
@@ -68,6 +73,9 @@ public class ChatModelBo extends BaseEntity {
/**
* 密钥
*/
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@ToString.Exclude
@EqualsAndHashCode.Exclude
private String apiKey;
/**

View File

@@ -0,0 +1,14 @@
package org.ruoyi.common.chat.domain.bo.chat;
import lombok.Data;
/** Minimal, non-secret query contract for user-facing model selection. */
@Data
public class ChatModelSelectQuery {
/** Optional model category; defaults to chat at the controller boundary. */
private String category;
/** Optional exact provider filter. */
private String providerCode;
}

View File

@@ -1,8 +1,10 @@
package org.ruoyi.common.chat.domain.bo.chat;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.ruoyi.common.mybatis.core.domain.BaseEntity;
/**
@@ -24,7 +26,10 @@ public class ModelBatchKeyBo extends BaseEntity {
/**
* 密钥
*/
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@NotBlank(message = "密钥不能为空")
@ToString.Exclude
@EqualsAndHashCode.Exclude
private String apiKey;
}

View File

@@ -75,6 +75,19 @@ public class ChatRequest {
*/
private Boolean enableThinking = false;
/**
* 是否回放上下文中已经存在的模型思考内容。
* 仅供需要在非思考动作轮中延续 DeepSeek 工具协议的内部调用使用。
*/
private Boolean replayThinking = false;
/**
* Doubao-Seed-Evolving 思考等级(reasoning_effort)。
* 取值 none/minimal/low/medium/high/xhigh/max;null 表示不发送 Doubao 专属参数。
* 仅在 custom_api 供应商承载 Doubao 模型时使用,其他模型忽略此字段。
*/
private String reasoningEffort;
/**
* 对话模型详情
*/

View File

@@ -0,0 +1,23 @@
package org.ruoyi.common.chat.domain.vo.chat;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.beans.BeanUtils;
/** 模型管理编辑详情,允许有查询权限的管理员回显已保存的密钥。 */
public class ChatModelDetailVo extends ChatModelVo {
@Override
@JsonProperty(access = JsonProperty.Access.READ_WRITE)
public String getApiKey() {
return super.getApiKey();
}
public static ChatModelDetailVo from(ChatModelVo model) {
if (model == null) {
return null;
}
ChatModelDetailVo detail = new ChatModelDetailVo();
BeanUtils.copyProperties(model, detail);
return detail;
}
}

View File

@@ -0,0 +1,45 @@
package org.ruoyi.common.chat.domain.vo.chat;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* Public model option exposed to clients that can select a chat model.
*
* <p>This deliberately contains only the fields required to identify and display a model.
* Operational configuration, including endpoints and credentials, remains server-side.</p>
*/
@Data
public class ChatModelSelectVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** Model configuration identifier. */
private Long id;
/** Configured model name. */
private String modelName;
/** User-facing model description; clients can fall back to modelName when blank. */
private String modelDescribe;
/** Provider identifier used for display. */
private String providerCode;
private String category;
private Integer sortOrder;
public static ChatModelSelectVo from(ChatModelVo source) {
ChatModelSelectVo target = new ChatModelSelectVo();
target.setId(source.getId());
target.setCategory(source.getCategory());
target.setSortOrder(source.getSortOrder());
target.setModelName(source.getModelName());
target.setModelDescribe(source.getModelDescribe());
target.setProviderCode(source.getProviderCode());
return target;
}
}

View File

@@ -3,8 +3,11 @@ package org.ruoyi.common.chat.domain.vo.chat;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.ruoyi.common.chat.entity.chat.ChatModel;
import java.io.Serial;
@@ -18,7 +21,7 @@ import java.io.Serializable;
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = ChatModel.class)
@AutoMapper(target = ChatModel.class, convertGenerate = false)
public class ChatModelVo implements Serializable {
@Serial
@@ -36,6 +39,9 @@ public class ChatModelVo implements Serializable {
@ExcelProperty(value = "模型分类")
private String category;
/** 默认模型优先级,数值越小越优先;同值按ID排序。 */
private Integer sortOrder;
/**
* 模型名称
*/
@@ -74,9 +80,11 @@ public class ChatModelVo implements Serializable {
private String apiHost;
/**
* 密钥
* 列表、日志及导出不包含密钥;管理员编辑详情由 ChatModelDetailVo 回显原值。
*/
@ExcelProperty(value = "密钥")
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@ToString.Exclude
@EqualsAndHashCode.Exclude
private String apiKey;
/**

View File

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.ruoyi.common.tenant.core.TenantEntity;
import java.io.Serial;
@@ -33,6 +34,9 @@ public class ChatModel extends TenantEntity {
*/
private String category;
/** 默认模型优先级,数值越小越优先;同值按ID排序。 */
private Integer sortOrder;
/**
* 模型名称
*/
@@ -67,6 +71,8 @@ public class ChatModel extends TenantEntity {
/**
* 密钥
*/
@ToString.Exclude
@EqualsAndHashCode.Exclude
private String apiKey;
/**

View File

@@ -47,4 +47,6 @@ public class ImageContext {
* 参考图 URL(图生图用)
*/
private String image;
/** Ordered reference images for multi-subject image editing. */
private java.util.List<String> referenceImages;
}

View File

@@ -25,12 +25,18 @@ public class VideoContext {
private String quality;
/** Provider output resolution; distinct from aspect ratio and quality. */
private String resolution;
/** 参考图 URL(图生视频模式,单图) */
private String imageUrl;
/** 多参考图 URL(多参考图生视频模式,配合 @imageN prompt 使用) */
private java.util.List<String> referenceImages;
/** Motion/camera reference clips; distinct from appearance reference images. */
private java.util.List<String> referenceVideos;
/** 参考音频 URL 列表(对白口型对齐模式) */
private java.util.List<String> referenceAudios;

View File

@@ -0,0 +1,44 @@
package org.ruoyi.common.chat.security;
import java.net.URI;
/** Normalizes the OpenAI/Anthropic endpoint configured in model management. */
public final class CustomApiCredentialPolicy {
public static final String OPENAI_PROVIDER = "custom_api";
public static final String ANTHROPIC_PROVIDER = "custom_anthropic";
private CustomApiCredentialPolicy() {
}
public static boolean isCustomProvider(String providerCode) {
return OPENAI_PROVIDER.equals(providerCode) || ANTHROPIC_PROVIDER.equals(providerCode);
}
/** Accepts a base URL or the selected protocol's complete chat endpoint. */
public static String normalizeBaseUrl(String providerCode, String apiHost) {
if (!isCustomProvider(providerCode)) {
throw new IllegalArgumentException("不支持的自定义厂商协议");
}
if (apiHost == null || apiHost.isBlank()) {
throw new IllegalArgumentException("请填写自定义厂商的请求地址");
}
URI uri = URI.create(apiHost);
if (uri.getHost() == null || !("http".equalsIgnoreCase(uri.getScheme())
|| "https".equalsIgnoreCase(uri.getScheme()))) {
throw new IllegalArgumentException("请求地址必须是有效的 HTTP 或 HTTPS 地址");
}
String normalized = apiHost.replaceAll("/+$", "");
String suffix = ANTHROPIC_PROVIDER.equals(providerCode) ? "/messages" : "/chat/completions";
String otherSuffix = ANTHROPIC_PROVIDER.equals(providerCode) ? "/chat/completions" : "/messages";
if (normalized.endsWith(otherSuffix)) {
throw new IllegalArgumentException("请求地址与所选自定义厂商协议不匹配");
}
if (normalized.endsWith(suffix)) {
normalized = normalized.substring(0, normalized.length() - suffix.length());
} else if (URI.create(normalized).getPath().isEmpty()) {
normalized += "/v1";
}
return normalized;
}
}

View File

@@ -49,6 +49,9 @@ public interface IChatModelService {
*/
List<ChatModelVo> queryList(ChatModelBo bo);
/** 查询当前租户中厂商已启用的模型,供用户选择或业务调用。 */
List<ChatModelVo> queryAvailableList(ChatModelBo bo);
/**
* 新增模型管理
*

View File

@@ -0,0 +1,40 @@
package org.ruoyi.common.chat.security;
import jakarta.validation.Validation;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.ruoyi.common.chat.domain.bo.chat.ChatModelBo;
import org.ruoyi.common.chat.domain.bo.chat.ModelBatchKeyBo;
import org.ruoyi.common.core.validate.AddGroup;
import static org.junit.jupiter.api.Assertions.*;
@Tag("dev")
class DifyApiKeyConfigurationTest {
@Test
void acceptsAnAppKeyForCloudAndSelfHostedDify() {
try (var factory = Validation.buildDefaultValidatorFactory()) {
for (String host : new String[]{"https://api.dify.ai/v1", "http://localhost:8080/v1"}) {
var model = new ChatModelBo();
model.setCategory("chat");
model.setProviderCode("dify");
model.setModelName("dify-chat");
model.setApiHost(host);
model.setApiKey("app-test-key");
assertTrue(factory.getValidator().validate(model, AddGroup.class).isEmpty());
}
}
}
@Test
void batchUpdatesStillRequireAProviderAndANonblankKey() {
var batch = new ModelBatchKeyBo();
batch.setProviderCode("dify");
batch.setApiKey("");
try (var factory = Validation.buildDefaultValidatorFactory()) {
assertFalse(factory.getValidator().validate(batch).isEmpty());
batch.setApiKey("app-test-key");
assertTrue(factory.getValidator().validate(batch).isEmpty());
}
}
}

View File

@@ -0,0 +1,88 @@
package org.ruoyi.common.chat.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.validation.Validation;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.ruoyi.common.chat.domain.bo.chat.ChatModelBo;
import org.ruoyi.common.chat.domain.bo.chat.ModelBatchKeyBo;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelDetailVo;
import org.ruoyi.common.core.validate.AddGroup;
import org.ruoyi.common.core.validate.EditGroup;
import static org.junit.jupiter.api.Assertions.*;
@Tag("dev")
class ModelApiKeyValidationTest {
@ParameterizedTest
@ValueSource(strings = {"qianwen", "zhipu", "openai", "deepseek", "ppio", "atlas",
"minimax", "dify", "coze", "xiaomi", "custom_api", "custom_anthropic"})
void acceptsAdminConfiguredKeysForEveryProvider(String provider) {
var model = new ChatModelBo();
model.setId(1L);
model.setCategory("chat");
model.setProviderCode(provider);
model.setModelName("configured-model");
model.setApiHost("http://localhost:11434/v1");
model.setApiKey("test-key-" + provider);
var batch = new ModelBatchKeyBo();
batch.setProviderCode(provider);
batch.setApiKey(model.getApiKey());
try (var factory = Validation.buildDefaultValidatorFactory()) {
var validator = factory.getValidator();
assertTrue(validator.validate(model, AddGroup.class, EditGroup.class).isEmpty());
assertTrue(validator.validate(batch).isEmpty());
}
}
@Test
void acceptsKeylessLocalModelsAndDoesNotRequireHiddenHostField() {
var model = new ChatModelBo();
model.setCategory("chat");
model.setProviderCode("ollama");
model.setModelName("local-model");
try (var factory = Validation.buildDefaultValidatorFactory()) {
assertTrue(factory.getValidator().validate(model, AddGroup.class).isEmpty());
}
}
@Test
void keyRemainsWritableButIsNotSerializedOrIncludedInToString() throws Exception {
var mapper = new ObjectMapper();
var model = mapper.readValue("{\"providerCode\":\"qianwen\",\"apiKey\":\"test-secret\"}", ChatModelBo.class);
assertEquals("test-secret", model.getApiKey());
var view = new ChatModelVo();
view.setApiKey(model.getApiKey());
assertFalse(mapper.writeValueAsString(model).contains("test-secret"));
assertFalse(mapper.writeValueAsString(view).contains("apiKey"));
assertFalse(model.toString().contains("test-secret"));
assertFalse(view.toString().contains("test-secret"));
}
@Test
void adminDetailReturnsExactKeyWithoutChangingListOrLogSerialization() throws Exception {
var mapper = new ObjectMapper();
var model = new ChatModelVo();
model.setId(12L);
model.setApiKey(" 任意/key: value ");
var detail = ChatModelDetailVo.from(model);
assertEquals(model.getApiKey(), mapper.readTree(mapper.writeValueAsString(detail)).get("apiKey").asText());
assertEquals(model.getId(), detail.getId());
assertFalse(mapper.writeValueAsString(model).contains("apiKey"));
assertFalse(detail.toString().contains(model.getApiKey()));
assertNull(ChatModelDetailVo.from(null));
}
@Test
void customEndpointsNeedNoDeploymentVariablesAndSupportLocalServices() {
assertEquals("http://localhost:11434/v1", CustomApiCredentialPolicy.normalizeBaseUrl(
"custom_api", "http://localhost:11434/v1/chat/completions"));
assertEquals("https://example.com/v1", CustomApiCredentialPolicy.normalizeBaseUrl(
"custom_anthropic", "https://example.com/v1/messages"));
assertThrows(IllegalArgumentException.class, () -> CustomApiCredentialPolicy.normalizeBaseUrl(
"custom_anthropic", "https://example.com/v1/chat/completions"));
}
}

View File

@@ -8,6 +8,7 @@ import org.ruoyi.common.core.utils.SpringUtils;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.core.task.VirtualThreadTaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@@ -39,6 +40,7 @@ public class ThreadPoolConfig {
/**
* 执行周期性或定时任务
*/
@Primary
@Bean(name = "scheduledExecutorService")
protected ScheduledExecutorService scheduledExecutorService() {
// daemon 必须为 true

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