mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-11-14 15:00:40 +00:00
Compare commits
35 Commits
v3.1.0
...
8a12be349a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a12be349a | ||
|
|
f286f50bbe | ||
|
|
5e66c7bd30 | ||
|
|
8ab3638012 | ||
|
|
432fbb6ea4 | ||
|
|
ca6ef96200 | ||
|
|
91b6b53ce9 | ||
|
|
7c7e64311e | ||
|
|
46714df604 | ||
|
|
197dc40510 | ||
|
|
8e3b286190 | ||
|
|
91ca91571b | ||
|
|
ce9228c5bb | ||
|
|
6f19c86fe6 | ||
|
|
422d935b26 | ||
|
|
cca3090577 | ||
|
|
4766d2e628 | ||
|
|
c9ca55b45b | ||
|
|
bcff4fd9e3 | ||
|
|
d4ed2b8672 | ||
|
|
ee838aa488 | ||
|
|
b27fa844e6 | ||
|
|
7f728c7a92 | ||
|
|
15ffb4ec2e | ||
|
|
f92e510665 | ||
|
|
997548dd4b | ||
|
|
25ab33c79c | ||
|
|
9d439d1dcf | ||
|
|
0a08c489be | ||
|
|
038e7725f9 | ||
|
|
4ccf249663 | ||
|
|
1d5b01f77f | ||
|
|
538dac3a46 | ||
|
|
6c383dd9f5 | ||
|
|
9ae49f8797 |
30
.github/workflows/publish-images.yml
vendored
30
.github/workflows/publish-images.yml
vendored
@@ -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
1
.gitignore
vendored
@@ -57,3 +57,4 @@ logs/
|
||||
.flattened-pom.xml
|
||||
/.claude/settings.local.json
|
||||
/docs/docker/milvus/volumes/
|
||||
/.codex-runtime/
|
||||
|
||||
117
README.md
117
README.md
@@ -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>
|
||||
|
||||
114
README_ZH.md
114
README_ZH.md
@@ -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
50
docs/dify-cloud.md
Normal 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 中验证应用可以回答。 |
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
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 |
34
docs/model-configuration.md
Normal file
34
docs/model-configuration.md
Normal 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
60
docs/script/sql/README.md
Normal 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
41
docs/script/sql/update/2026-08-30-mcp-market-tool-tenant.sql
Normal file
41
docs/script/sql/update/2026-08-30-mcp-market-tool-tenant.sql
Normal 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`);
|
||||
76
docs/script/sql/update/2026-09-01-sys-url.sql
Normal file
76
docs/script/sql/update/2026-09-01-sys-url.sql
Normal 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');
|
||||
@@ -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'
|
||||
);
|
||||
@@ -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);
|
||||
@@ -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='道具资产和逐镜关键帧,可续跑';
|
||||
@@ -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;
|
||||
@@ -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');
|
||||
@@ -0,0 +1,7 @@
|
||||
-- PC 管理后台和短剧工作台:无操作 12 小时后登录失效(单位:秒)。
|
||||
-- 仅迁移原 30 分钟策略,保留已自定义的客户端设置。
|
||||
UPDATE sys_client
|
||||
SET active_timeout = 43200
|
||||
WHERE device_type = 'pc' AND active_timeout = 1800;
|
||||
-- 已部署环境通过 /system/client 更新相同字段以同步清理客户端缓存。
|
||||
-- 已签发的令牌需重新登录后使用新策略。
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
@@ -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数组)';
|
||||
@@ -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
47
docs/tenant-management.md
Normal 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
35
docs/url-management.md
Normal 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`。本轮为本地验收;未包含远程部署。新标签页打开参数通过组件测试确认,本轮未新增真实弹窗或手机尺寸实测结论。
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
# 严格模式 匹配不到数据源则报错
|
||||
|
||||
@@ -47,7 +47,7 @@ spring:
|
||||
# 动态数据源文档 https://www.kancloud.cn/tracy5546/dynamic-datasource/content
|
||||
dynamic:
|
||||
# 性能分析插件(有性能损耗 不建议生产环境使用)
|
||||
p6spy: true
|
||||
p6spy: false
|
||||
# 设置默认的数据源或者数据源组,默认值即为 master
|
||||
primary: master
|
||||
# 严格模式 匹配不到数据源则报错
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 对话模型详情
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,9 @@ public interface IChatModelService {
|
||||
*/
|
||||
List<ChatModelVo> queryList(ChatModelBo bo);
|
||||
|
||||
/** 查询当前租户中厂商已启用的模型,供用户选择或业务调用。 */
|
||||
List<ChatModelVo> queryAvailableList(ChatModelBo bo);
|
||||
|
||||
/**
|
||||
* 新增模型管理
|
||||
*
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user