mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-09-13 00:14:59 +00:00
Compare commits
88 Commits
3071bfd0f9
...
v3.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c175ba28d | ||
|
|
e34592c3d4 | ||
|
|
fd044a4752 | ||
|
|
6e264ad500 | ||
|
|
83fd1ee983 | ||
|
|
7640de34e5 | ||
|
|
77846601ee | ||
|
|
b80dc6e23f | ||
|
|
176cc67a5c | ||
|
|
afaa86ef6e | ||
|
|
d1a820728f | ||
|
|
11bb1dba0f | ||
|
|
345d2819d8 | ||
|
|
36e3cf8c52 | ||
|
|
06ff95cdc3 | ||
|
|
9adeee6c83 | ||
|
|
e42d35f3bf | ||
|
|
a69d1f51b9 | ||
|
|
911ae6cb1f | ||
|
|
9a2f326e42 | ||
|
|
46a8d6b552 | ||
|
|
cf5423f69f | ||
|
|
177375a7b7 | ||
|
|
016bdbdd94 | ||
|
|
41e0dcb528 | ||
|
|
787e447d09 | ||
|
|
9c092ae3bb | ||
|
|
42bc8fea95 | ||
|
|
c4fc1e6fcf | ||
|
|
6f6d0893ec | ||
|
|
a6a55202a3 | ||
|
|
24bee53f9e | ||
|
|
afc9d3d17d | ||
|
|
97ce15d116 | ||
|
|
879fe48945 | ||
|
|
5d834d924a | ||
|
|
fece90b307 | ||
|
|
06d110ed16 | ||
|
|
dfcadda2bb | ||
|
|
3c9546ded9 | ||
|
|
a3d82e092e | ||
|
|
96a1b741df | ||
|
|
e7b7d1d084 | ||
|
|
efc30c3ac8 | ||
|
|
2ae13aafd4 | ||
|
|
e925050207 | ||
|
|
7aa67cfc2f | ||
|
|
6d38e2bf25 | ||
|
|
8ed5e61f63 | ||
|
|
c3c89f8cc8 | ||
|
|
a3a6bdbc55 | ||
|
|
07d9fc1f45 | ||
|
|
73a60e3591 | ||
|
|
c79848c654 | ||
|
|
ec092a11c3 | ||
|
|
9a7b727413 | ||
|
|
b8d16b7669 | ||
|
|
058a4aee2a | ||
|
|
1b50c7f9f1 | ||
|
|
e7f53fd55f | ||
|
|
07bdc5e585 | ||
|
|
e1b8a5f011 | ||
|
|
80ca76ea37 | ||
|
|
2c6ff66830 | ||
|
|
4f79a66559 | ||
|
|
22883b4334 | ||
|
|
081da6d18d | ||
|
|
74eb5b2530 | ||
|
|
b0328fe0ef | ||
|
|
2ee0aae57e | ||
|
|
d9c3de660a | ||
|
|
ccbf5c9520 | ||
|
|
c4f7c1f5d0 | ||
|
|
1208c46cca | ||
|
|
06a63c377e | ||
|
|
c1fc02894b | ||
|
|
0fa25032a3 | ||
|
|
28ad29d6ed | ||
|
|
bf7b5eac72 | ||
|
|
d602b805bd | ||
|
|
9cf18904bb | ||
|
|
2f39fa0f53 | ||
|
|
d2005cfa48 | ||
|
|
4e38f853f3 | ||
|
|
3cfb185dde | ||
|
|
ef99c540bb | ||
|
|
b9097b4989 | ||
|
|
5d14eb20af |
107
.github/workflows/publish-images.yml
vendored
Normal file
107
.github/workflows/publish-images.yml
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
name: Publish Docker Images
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
concurrency:
|
||||
group: publish-images-${{ github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_OWNER: ${{ github.repository_owner }}
|
||||
RELEASE_TAG: ${{ github.ref_name }}
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish ${{ matrix.image }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- image: ruoyi-ai-backend
|
||||
source: local
|
||||
context: .
|
||||
dockerfile: docs/docker/ruoyi-ai/Dockerfile.backend
|
||||
- image: ruoyi-ai-mysql
|
||||
source: local
|
||||
context: .
|
||||
dockerfile: docs/docker/ruoyi-ai/Dockerfile.mysql
|
||||
- image: ruoyi-ai-admin
|
||||
source: admin
|
||||
context: build/ruoyi-admin
|
||||
dockerfile: build/ruoyi-admin/apps/web-antd/Dockerfile
|
||||
- image: ruoyi-ai-web
|
||||
source: web
|
||||
context: build/ruoyi-web
|
||||
dockerfile: build/ruoyi-web/Dockerfile.frontend
|
||||
|
||||
steps:
|
||||
- name: Checkout backend repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ env.RELEASE_TAG }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Checkout admin repository
|
||||
if: matrix.source == 'admin'
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ github.repository_owner }}/ruoyi-admin
|
||||
ref: ${{ env.RELEASE_TAG }}
|
||||
path: build/ruoyi-admin
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Checkout web repository
|
||||
if: matrix.source == 'web'
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ github.repository_owner }}/ruoyi-web
|
||||
ref: ${{ env.RELEASE_TAG }}
|
||||
path: build/ruoyi-web
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ matrix.image }}
|
||||
tags: |
|
||||
type=raw,value=${{ env.RELEASE_TAG }}
|
||||
type=raw,value=latest
|
||||
labels: |
|
||||
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
||||
org.opencontainers.image.version=${{ env.RELEASE_TAG }}
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ${{ matrix.context }}
|
||||
file: ${{ matrix.dockerfile }}
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha,scope=${{ matrix.image }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.image }}
|
||||
provenance: true
|
||||
sbom: true
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -23,6 +23,9 @@ target/
|
||||
.idea
|
||||
.claude
|
||||
.github
|
||||
!.github/
|
||||
!.github/workflows/
|
||||
!.github/workflows/**
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
@@ -45,6 +48,7 @@ nbdist/
|
||||
*.xml.versionsBackup
|
||||
*.swp
|
||||
data/
|
||||
logs/
|
||||
|
||||
!*/build/*.java
|
||||
!*/build/*.html
|
||||
@@ -52,3 +56,4 @@ data/
|
||||
|
||||
.flattened-pom.xml
|
||||
/.claude/settings.local.json
|
||||
/docs/docker/milvus/volumes/
|
||||
|
||||
274
README.md
274
README.md
@@ -2,11 +2,7 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
[![Contributors][contributors-shield]][contributors-url]
|
||||
[![Forks][forks-shield]][forks-url]
|
||||
[![Stargazers][stars-shield]][stars-url]
|
||||
[![Issues][issues-shield]][issues-url]
|
||||
[![MIT License][license-shield]][license-url]
|
||||
[![Contributors][contributors-shield]][contributors-url] [![Forks][forks-shield]][forks-url] [![Stargazers][stars-shield]][stars-url] [![Issues][issues-shield]][issues-url] [![MIT License][license-shield]][license-url]
|
||||
|
||||
|
||||
<p align="center">
|
||||
@@ -17,232 +13,235 @@
|
||||
|
||||
<img src="docs/image/logo.png" alt="RuoYi AI Logo" width="120" height="120">
|
||||
|
||||
### 企业级AI助手平台
|
||||
### Enterprise-Grade AI Assistant Platform
|
||||
|
||||
*开箱即用的全栈AI平台,支持多智能体协同、Supervisor模式编排、多种决策模式、RAG技术和流程编排能力*
|
||||
*An out-of-the-box full-stack AI platform supporting multi-agent collaboration, Supervisor mode orchestration, and multiple decision models, with advanced RAG technology and visual workflow orchestration capabilities*
|
||||
|
||||
**[English](README_EN.md)** | **[📖 使用文档](https://doc.pandarobot.chat)** |
|
||||
**[🚀 在线体验](https://web.pandarobot.chat)** | **[🐛 问题反馈](https://github.com/ageerle/ruoyi-ai/issues)** | **[💡 功能建议](https://github.com/ageerle/ruoyi-ai/issues)**
|
||||
**[中文](README_ZH.md)** | **[📖 Documentation](https://doc.ruoyiai.chat/)** |
|
||||
**[🚀 Live Demo](https://web.ruoyiai.chat/)** | **[🐛 Report Issues](https://github.com/ageerle/ruoyi-ai/issues)** | **[💡 Feature Requests](https://github.com/ageerle/ruoyi-ai/issues)**
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
## ✨ 核心亮点
|
||||
|
||||
| 模块 | 现有能力
|
||||
|:----------:|---
|
||||
| **模型管理** | 多模型接入(OpenAI/DeepSeek/通义/智谱)、多模态理解、Coze/DIFY/FastGPT平台集成
|
||||
| **知识管理** | 本地RAG + 向量库(Milvus/Weaviate/Qdrant) + 文档解析
|
||||
| **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态
|
||||
| **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点
|
||||
| **多智能体** | 基于Langchain4j的Agent框架、Supervisor模式编排,支持多种决策模型
|
||||
|
||||
## 🚀 快速体验
|
||||
## ✨ Core Features
|
||||
|
||||
### 在线演示
|
||||
| Module | Current Capabilities |
|
||||
|:---:|---|
|
||||
| **Model Management** | Multi-model integration (DeepSeek/Zhipu/MIMO/Bailian/OpenAI), multi-modal understanding, Coze/DIFY/FastGPT/RAGFlow platform integration |
|
||||
| **Knowledge Management** | Local RAG + Vector DB (Milvus/Weaviate/Qdrant) + Document parsing |
|
||||
| **Tool Management** | MCP protocol integration, Skills capability + Extensible tool ecosystem |
|
||||
| **Workflow Orchestration** | Visual workflow designer, drag-and-drop node orchestration, SSE streaming execution, currently supports model calls, email sending, manual review, and other nodes |
|
||||
| **Multi-Agent** | Agent framework based on Langchain4j, Supervisor mode orchestration, supports multiple decision models, can flexibly combine tools and skills |
|
||||
|
||||
| 平台 | 地址 | 账号 |
|
||||
|:------:|---|---|
|
||||
| 用户端 | [web.pandarobot.chat](https://web.pandarobot.chat) | admin / admin123 |
|
||||
| 管理后台 | [admin.pandarobot.chat](https://admin.pandarobot.chat) | admin / admin123 |
|
||||
### Project Repositories
|
||||
|
||||
### 项目源码
|
||||
|
||||
| 项目模块 | GitHub 仓库 | Gitee 仓库 | GitCode 仓库 |
|
||||
| Module | GitHub Repository | Gitee Repository | GitCode Repository |
|
||||
|----------|-------------------------------------------------------|------------------------------------------------------|--------------------------------------------------------|
|
||||
| 🔧 后端服务 | [ruoyi-ai](https://github.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitee.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitcode.com/ageerle/ruoyi-ai) |
|
||||
| 🎨 用户前端 | [ruoyi-web](https://github.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitee.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitcode.com/ageerle/ruoyi-web) |
|
||||
| 🛠️ 管理后台 | [ruoyi-admin](https://github.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitee.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitcode.com/ageerle/ruoyi-admin) |
|
||||
| 🔧 Backend | [ruoyi-ai](https://github.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitee.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitcode.com/ageerle/ruoyi-ai) |
|
||||
| 🎨 User Frontend | [ruoyi-web](https://github.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitee.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitcode.com/ageerle/ruoyi-web) |
|
||||
| 🛠️ Admin Panel | [ruoyi-admin](https://github.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitee.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitcode.com/ageerle/ruoyi-admin) |
|
||||
| 🎬 Drama | [ruoyi-drama](https://github.com/ageerle/ruoyi-drama) | [ruoyi-drama](https://gitee.com/ageerle/ruoyi-drama) | [ruoyi-drama](https://gitcode.com/ageerle/ruoyi-drama) |
|
||||
| 🤖 Copilot | [ruoyi-copilot](https://github.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitee.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitcode.com/ageerle/ruoyi-copilot) |
|
||||
| 📱 Mini-App | [ruoyi-uniapp](https://github.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitee.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitcode.com/ageerle/ruoyi-uniapp) |
|
||||
|
||||
### 合作项目
|
||||
| 项目名称 | GitHub 仓库 | Gitee 仓库
|
||||
### Partner Projects
|
||||
| Project Name | GitHub Repository | Gitee Repository |
|
||||
|----------------|-------------------------------------------------------|------------------------------------------------------|
|
||||
| element-plus-x | [element-plus-x](https://github.com/element-plus-x/Element-Plus-X) | [element-plus-x](https://gitee.com/he-jiayue/element-plus-x) |
|
||||
| element-plus-x | [element-plus-x](https://github.com/element-plus-x/Element-Plus-X) | [element-plus-x](https://gitee.com/he-jiayue/element-plus-x) |
|
||||
|
||||
## 🛠️ 技术架构
|
||||
## 🛠️ Technical Architecture
|
||||
|
||||
### 核心框架
|
||||
- **后端架构**:Spring Boot 4.0 + Spring ai 2.0 + Langchain4j
|
||||
- **数据存储**:MySQL 8.0 + Redis + 向量数据库(Milvus/Weaviate/Qdrant)
|
||||
- **前端技术**:Vue 3 + Vben Admin + element-plus-x
|
||||
- **安全认证**:Sa-Token + JWT 双重保障
|
||||
### Core Framework
|
||||
- **Backend**: Spring Boot 3.5.8 + Langchain4j
|
||||
- **Data Storage**: MySQL 8.0 + Redis + Vector Databases (Milvus/Weaviate/Qdrant)
|
||||
- **Frontend**: Vue 3 + Vben Admin + element-plus-x
|
||||
- **Security**: Sa-Token + JWT dual-layer security
|
||||
- **Document Processing**: PDF, Word, Excel parsing, intelligent image analysis
|
||||
- **Real-time Communication**: WebSocket real-time communication, SSE streaming response
|
||||
- **System Monitoring**: Comprehensive logging system, performance monitoring, service health checks
|
||||
|
||||
## 🐳 Docker Deployment
|
||||
|
||||
- **文档处理**:PDF、Word、Excel 解析,图像智能分析
|
||||
- **实时通信**:WebSocket 实时通信,SSE 流式响应
|
||||
- **系统监控**:完善的日志体系、性能监控、服务健康检查
|
||||
This project provides two Docker deployment methods:
|
||||
|
||||
## 🐳 Docker 部署
|
||||
### Method 1: One-click Start All Services (Recommended)
|
||||
|
||||
本项目提供两种 Docker 部署方式:
|
||||
|
||||
### 方式一:一键启动所有服务(推荐)
|
||||
|
||||
使用 `docker-compose-all.yaml` 可以一键启动所有服务(包括后端、管理端、用户端及依赖服务):
|
||||
Use `docker-compose-all.yaml` to start all services at once (including backend, admin panel, user frontend, and dependencies):
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
# Clone the repository
|
||||
git clone https://github.com/ageerle/ruoyi-ai.git
|
||||
cd ruoyi-ai
|
||||
|
||||
# 启动所有服务(从镜像仓库拉取预构建镜像)
|
||||
docker-compose -f docker-compose-all.yaml up -d
|
||||
# Start all services (pull pre-built images from GHCR)
|
||||
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml up -d
|
||||
|
||||
# 查看服务状态
|
||||
docker-compose -f docker-compose-all.yaml ps
|
||||
# Check service status
|
||||
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml ps
|
||||
|
||||
# 访问服务
|
||||
# 管理端: http://localhost:25666 (admin / admin123)
|
||||
# 用户端: http://localhost:25137
|
||||
# 后端API: http://localhost:26039
|
||||
# Access services
|
||||
# Admin Panel: http://localhost:25666 (admin / admin123)
|
||||
# User Frontend: http://localhost:25137
|
||||
# Backend API: http://localhost:26039
|
||||
```
|
||||
|
||||
### 方式二:分步部署(源码编译)
|
||||
### Method 2: Step-by-step Deployment (Source Build)
|
||||
|
||||
如果您需要从源码构建后端服务,请按照以下步骤操作:
|
||||
If you need to build backend services from source, follow these steps:
|
||||
|
||||
#### 第一步:部署后端服务
|
||||
#### Step 1: Deploy Backend Service
|
||||
|
||||
```bash
|
||||
# 进入后端项目目录
|
||||
# Enter backend project directory
|
||||
cd ruoyi-ai
|
||||
|
||||
# 启动后端服务(源码编译构建)
|
||||
# Start backend service (build from source)
|
||||
docker-compose up -d --build
|
||||
|
||||
# 等待后端服务启动完成
|
||||
# Wait for backend service to start
|
||||
docker-compose logs -f backend
|
||||
```
|
||||
|
||||
#### 第二步:部署管理端
|
||||
#### Step 2: Deploy Admin Panel
|
||||
|
||||
```bash
|
||||
# 进入管理端项目目录
|
||||
# Enter admin panel project directory
|
||||
cd ruoyi-admin
|
||||
|
||||
# 构建并启动管理端
|
||||
# Build and start admin panel
|
||||
docker-compose up -d --build
|
||||
|
||||
# 访问管理端
|
||||
# 地址: http://localhost:5666
|
||||
# Access admin panel
|
||||
# URL: http://localhost:5666
|
||||
```
|
||||
|
||||
#### 第三步:部署用户端(可选)
|
||||
#### Step 3: Deploy User Frontend (Optional)
|
||||
|
||||
```bash
|
||||
# 进入用户端项目目录
|
||||
# Enter user frontend project directory
|
||||
cd ruoyi-web
|
||||
|
||||
# 构建并启动用户端
|
||||
# Build and start user frontend
|
||||
docker-compose up -d --build
|
||||
|
||||
# 访问用户端
|
||||
# 地址: http://localhost:5137
|
||||
# Access user frontend
|
||||
# URL: http://localhost:5137
|
||||
```
|
||||
|
||||
### 服务端口说明
|
||||
### Service Ports
|
||||
|
||||
| 服务 | 一键启动端口 | 分步部署端口 | 说明 |
|
||||
| Service | One-click Port | Step-by-step Port | Description |
|
||||
|------|-------------|-------------|------|
|
||||
| 管理端 | 25666 | 5666 | 管理后台访问地址 |
|
||||
| 用户端 | 25137 | 5137 | 用户前端访问地址 |
|
||||
| 后端服务 | 26039 | 6039 | 后端 API 服务 |
|
||||
| MySQL | 23306 | 23306 | 数据库服务 |
|
||||
| Redis | 26379 | 6379 | 缓存服务 |
|
||||
| Weaviate | 28080 | 28080 | 向量数据库 |
|
||||
| MinIO API | 29000 | 9000 | 对象存储 API |
|
||||
| MinIO Console | 29090 | 9090 | 对象存储控制台 |
|
||||
| Admin Panel | 25666 | 5666 | Admin backend access |
|
||||
| User Frontend | 25137 | 5137 | User frontend access |
|
||||
| Backend Service | 26039 | 6039 | Backend API service |
|
||||
| MySQL | 23306 | 23306 | Database service |
|
||||
| Redis | 26379 | 6379 | Cache service |
|
||||
| Weaviate | 28080 | 28080 | Vector database |
|
||||
| MinIO API | 29000 | 9000 | Object storage API |
|
||||
| MinIO Console | 29090 | 9090 | Object storage console |
|
||||
|
||||
### 镜像仓库
|
||||
### Image Registry
|
||||
|
||||
所有镜像托管在阿里云容器镜像服务:
|
||||
Application images are published to GitHub Container Registry (GHCR) by GitHub Actions when a release tag such as `v3.1.0` is pushed:
|
||||
|
||||
```
|
||||
crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai
|
||||
ghcr.io/ageerle/ruoyi-ai-backend:v3.1.0
|
||||
ghcr.io/ageerle/ruoyi-ai-mysql:v3.1.0
|
||||
ghcr.io/ageerle/ruoyi-ai-admin:v3.1.0
|
||||
ghcr.io/ageerle/ruoyi-ai-web:v3.1.0
|
||||
```
|
||||
|
||||
可用镜像:
|
||||
- `mysql:v3` - MySQL 数据库(包含初始化 SQL)
|
||||
- `redis:6.2` - Redis 缓存
|
||||
- `weaviate:1.30.0` - 向量数据库
|
||||
- `minio:latest` - 对象存储
|
||||
- `ruoyi-ai-backend:latest` - 后端服务
|
||||
- `ruoyi-ai-admin:latest` - 管理端前端
|
||||
- `ruoyi-ai-web:latest` - 用户端前端
|
||||
The Compose file defaults to `latest`. To pin a release, create `docs/docker/ruoyi-ai/.env` with `RUIYI_VERSION=v3.1.0`.
|
||||
|
||||
### 常用命令
|
||||
After the first workflow run, set the four GHCR packages to `Public` in GitHub so deployment servers can pull them without logging in.
|
||||
|
||||
The same release tag must exist in `ageerle/ruoyi-admin` and `ageerle/ruoyi-web`; the publish workflow checks out those repositories at the backend release tag before building their images.
|
||||
|
||||
### Common Commands
|
||||
|
||||
```bash
|
||||
# 停止所有服务
|
||||
docker-compose -f docker-compose-all.yaml down
|
||||
# Stop all services
|
||||
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml down
|
||||
|
||||
# 查看服务日志
|
||||
docker-compose -f docker-compose-all.yaml logs -f [服务名]
|
||||
# View service logs
|
||||
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml logs -f [service-name]
|
||||
|
||||
# 重启某个服务
|
||||
docker-compose -f docker-compose-all.yaml restart [服务名]
|
||||
# Restart a service
|
||||
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml restart [service-name]
|
||||
```
|
||||
|
||||
## 📚 使用文档
|
||||
## 📚 Documentation
|
||||
|
||||
想要深入了解安装部署、功能配置和二次开发?
|
||||
Want to learn more about installation, deployment, configuration, and secondary development?
|
||||
|
||||
**👉 [完整使用文档](https://doc.pandarobot.chat)**
|
||||
**👉 [Complete Documentation](https://doc.ruoyiai.chat/)**
|
||||
|
||||
遇到知识库或 RAG 回答异常问题?
|
||||
## 🤝 Contributing
|
||||
|
||||
**👉 [RAG 回答异常排查手册](docs/troubleshooting/rag-failures.md)**
|
||||
We warmly welcome community contributions! Whether you are a seasoned developer or just getting started, you can contribute to the project 💪
|
||||
|
||||
---
|
||||
### How to Contribute
|
||||
|
||||
## 🤝 参与贡献
|
||||
1. **Fork** the project to your account
|
||||
2. **Create a branch** (`git checkout -b feature/new-feature-name`)
|
||||
3. **Commit your changes** (`git commit -m 'Add new feature'`)
|
||||
4. **Push to the branch** (`git push origin feature/new-feature-name`)
|
||||
5. **Create a Pull Request**
|
||||
|
||||
我们热烈欢迎社区贡献!无论您是资深开发者还是初学者,都可以为项目贡献力量 💪
|
||||
> 💡 **Tip**: We recommend submitting PRs to GitHub, we will automatically sync to other code hosting platforms
|
||||
|
||||
### 贡献方式
|
||||
## 📄 License
|
||||
|
||||
1. **Fork** 项目到您的账户
|
||||
2. **创建分支** (`git checkout -b feature/新功能名称`)
|
||||
3. **提交代码** (`git commit -m '添加某某功能'`)
|
||||
4. **推送分支** (`git push origin feature/新功能名称`)
|
||||
5. **发起 Pull Request**
|
||||
This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details.
|
||||
|
||||
> 💡 **小贴士**:建议将 PR 提交到 GitHub,我们会自动同步到其他代码托管平台
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
## 📄 开源协议
|
||||
Thanks to the following excellent open-source projects for their support:
|
||||
- [Langchain4j](https://github.com/langchain4j/langchain4j) - Powerful Java LLM development framework
|
||||
- [RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus) - Mature enterprise-level rapid development framework
|
||||
- [Vben Admin](https://github.com/vbenjs/vue-vben-admin) - Modern Vue admin template
|
||||
|
||||
本项目采用 **MIT 开源协议**,详情请查看 [LICENSE](LICENSE) 文件。
|
||||
## 💎 Sponsors
|
||||
|
||||
## 🙏 特别鸣谢
|
||||
**Thanks to the following sponsors for supporting this project:**
|
||||
|
||||
感谢以下优秀的开源项目为本项目提供支持:
|
||||
- [Spring AI Alibaba Copilot](https://github.com/spring-ai-alibaba/copilot) - 基于spring-ai-alibaba
|
||||
的智能编码助手
|
||||
- [Langchain4j](https://github.com/langchain4j/langchain4j) - 强大的 Java LLM 开发框架
|
||||
- [RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus) - 成熟的企业级快速开发框架
|
||||
- [Vben Admin](https://github.com/vbenjs/vue-vben-admin) - 现代化的 Vue 后台管理模板
|
||||
<a href="https://www.atlascloud.ai?ref=89F97E">
|
||||
<img src="docs/image/sponsor/atlascloud_banner.png" alt="Atlas Cloud" width="160" height="80">
|
||||
</a>
|
||||
|
||||
## 🌐 生态伙伴
|
||||
[Visit Atlas Cloud](https://www.atlascloud.ai?ref=89F97E&utm_source=github&utm_campaign=ruoyi-drama) · [Coding Plan Promotion](https://www.atlascloud.ai/console/coding-plan)
|
||||
A full-modal AI inference platform that gives developers a unified AI API, supporting video generation, image generation, and LLMs. Connect once to access **300+ curated models**.
|
||||
|
||||
- [PPIO 派欧云](https://ppinfra.com/user/register?invited_by=P8QTUY&utm_source=github_ruoyi-ai) - 提供高性价比的 GPU
|
||||
算力和模型 API 服务
|
||||
- [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_ruoyi) - 万卡RTX40系GPU+海内外主流模型API服务,秒级响应,按量计费,新客免费用。
|
||||
<a href="https://www.volcengine.com/activity/codingplan?utm_campaign=hw&utm_content=hw&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ageerle-ruoyi-ai">
|
||||
<img src="docs/image/sponsor/huoshan.png" alt="Volcengine CodingPlan" width="160" height="80">
|
||||
</a>
|
||||
|
||||
## 💬 社区交流
|
||||
[Sign up to claim 25 million tokens — go now](https://www.volcengine.com/activity/ai618?utm_campaign=hw&utm_content=hw&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ageerle-ruoyi-ai)
|
||||
Enjoy ByteDance's in-house Doubao models plus full-power open-source SOTA models, covering text, VLM, and image generation — all modalities in one stop: Seed-2.1, Seedream-5.0, GLM-5.2, DeepSeek, and more. Not just for coding — it can also tackle complex long-horizon Agent tasks!
|
||||
|
||||
## 💬 Community Chat
|
||||
|
||||
<div align="center">
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="docs/image/wx.png" alt="微信二维码" width="200" height="200"><br>
|
||||
<strong>扫码添加作者微信</strong><br>
|
||||
<em>邀请进群学习</em>
|
||||
<img src="docs/image/wx.png" alt="WeChat QR Code" width="200" height="200"><br>
|
||||
<strong>Scan to add author on WeChat</strong><br>
|
||||
<em>Join group for learning</em>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="docs/image/qq.png" alt="QQ群二维码" width="200" height="200"><br>
|
||||
<strong>QQ技术交流群</strong><br>
|
||||
<em>技术讨论</em>
|
||||
<img src="docs/image/wx06.png" alt="WeChat QR Code" width="200" height="200"><br>
|
||||
<strong>WeChat Tech Exchange Group</strong><br>
|
||||
<em>Technical discussion</em>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="docs/image/qq.png" alt="QQ Group QR Code" width="200" height="200"><br>
|
||||
<strong>QQ Tech Exchange Group</strong><br>
|
||||
<em>Technical discussion</em>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
@@ -251,11 +250,12 @@ docker-compose -f docker-compose-all.yaml restart [服务名]
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
**[⭐ 点个Star支持一下](https://github.com/ageerle/ruoyi-ai)** • **[ Fork 开始贡献](https://github.com/ageerle/ruoyi-ai/fork)** • **[📚 English](README_EN.md)** • **[📖 查看完整文档](https://doc.pandarobot.chat)**
|
||||
**[⭐ Star to Support](https://github.com/ageerle/ruoyi-ai)** • **[Fork to Contribute](https://github.com/ageerle/ruoyi-ai/fork)** • **[📚 中文](README_ZH.md)** • **[📖 Complete Documentation](https://doc.ruoyiai.chat/)**
|
||||
|
||||
*用 ❤️ 打造,由 RuoYi AI 开源社区维护*
|
||||
*Built with ❤️, maintained by the RuoYi AI open-source community*
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
272
README_EN.md
272
README_EN.md
@@ -1,272 +0,0 @@
|
||||
|
||||
# RuoYi AI
|
||||
|
||||
<div align="center">
|
||||
|
||||
[![Contributors][contributors-shield]][contributors-url]
|
||||
[![Forks][forks-shield]][forks-url]
|
||||
[![Stargazers][stars-shield]][stars-url]
|
||||
[![Issues][issues-shield]][issues-url]
|
||||
[![MIT License][license-shield]][license-url]
|
||||
|
||||
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/13209">
|
||||
<img src="https://trendshift.io/api/badge/repositories/13209" alt="GitHub Trending">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<img src="docs/image/logo.png" alt="RuoYi AI Logo" width="120" height="120">
|
||||
|
||||
### Enterprise-Grade AI Assistant Platform
|
||||
|
||||
*An out-of-the-box full-stack AI platform supporting multi-agent collaboration, Supervisor mode orchestration, and multiple decision models, with advanced RAG technology and visual workflow orchestration capabilities*
|
||||
|
||||
**[中文](README.md)** | **[📖 Documentation](https://doc.pandarobot.chat)** |
|
||||
**[🚀 Live Demo](https://web.pandarobot.chat)** | **[🐛 Report Issues](https://github.com/ageerle/ruoyi-ai/issues)** | **[💡 Feature Requests](https://github.com/ageerle/ruoyi-ai/issues)**
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
## ✨ Core Features
|
||||
|
||||
| Module | Current Capabilities |
|
||||
|:---:|---|
|
||||
| **Model Management** | Multi-model integration (OpenAI/DeepSeek/Tongyi/Zhipu), multi-modal understanding, Coze/DIFY/FastGPT platform integration |
|
||||
| **Knowledge Base** | Local RAG + Vector DB (Milvus/Weaviate/Qdrant) + Document parsing |
|
||||
| **Tool Management** | MCP protocol integration, Skills capability + Extensible tool ecosystem |
|
||||
| **Workflow Orchestration** | Visual workflow designer, drag-and-drop node orchestration, SSE streaming execution, currently supports model calls, email sending, manual review nodes |
|
||||
| **Multi-Agent** | Agent framework based on Langchain4j, Supervisor mode orchestration, supports multiple decision models |
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Live Demo
|
||||
|
||||
| Platform | URL | Account |
|
||||
|:------:|---|---|
|
||||
| User Frontend | [web.pandarobot.chat](https://web.pandarobot.chat) | admin / admin123 |
|
||||
| Admin Panel | [admin.pandarobot.chat](https://admin.pandarobot.chat) | admin / admin123 |
|
||||
|
||||
### Project Repositories
|
||||
|
||||
| Module | GitHub Repository | Gitee Repository | GitCode Repository |
|
||||
|----------|-------------------------------------------------------|------------------------------------------------------|--------------------------------------------------------|
|
||||
| 🔧 Backend | [ruoyi-ai](https://github.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitee.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitcode.com/ageerle/ruoyi-ai) |
|
||||
| 🎨 User Frontend | [ruoyi-web](https://github.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitee.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitcode.com/ageerle/ruoyi-web) |
|
||||
| 🛠️ Admin Panel | [ruoyi-admin](https://github.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitee.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitcode.com/ageerle/ruoyi-admin) |
|
||||
|
||||
### Partner Projects
|
||||
| Project Name | GitHub Repository | Gitee Repository |
|
||||
|----------------|-------------------------------------------------------|------------------------------------------------------|
|
||||
| element-plus-x | [element-plus-x](https://github.com/element-plus-x/Element-Plus-X) | [element-plus-x](https://gitee.com/he-jiayue/element-plus-x) |
|
||||
|
||||
## 🛠️ Technical Architecture
|
||||
|
||||
### Core Framework
|
||||
- **Backend**: Spring Boot 4.0 + Spring AI 2.0 + Langchain4j
|
||||
- **Data Storage**: MySQL 8.0 + Redis + Vector Databases (Milvus/Weaviate/Qdrant)
|
||||
- **Frontend**: Vue 3 + Vben Admin + element-plus-x
|
||||
- **Security**: Sa-Token + JWT dual-layer security
|
||||
|
||||
|
||||
- **Document Processing**: PDF, Word, Excel parsing, intelligent image analysis
|
||||
- **Real-time Communication**: WebSocket real-time communication, SSE streaming response
|
||||
- **System Monitoring**: Comprehensive logging system, performance monitoring, service health checks
|
||||
|
||||
## 🐳 Docker Deployment
|
||||
|
||||
This project provides two Docker deployment methods:
|
||||
|
||||
### Method 1: One-click Start All Services (Recommended)
|
||||
|
||||
Use `docker-compose-all.yaml` to start all services at once (including backend, admin panel, user frontend, and dependencies):
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/ageerle/ruoyi-ai.git
|
||||
cd ruoyi-ai
|
||||
|
||||
# Start all services (pull pre-built images from registry)
|
||||
docker-compose -f docker-compose-all.yaml up -d
|
||||
|
||||
# Check service status
|
||||
docker-compose -f docker-compose-all.yaml ps
|
||||
|
||||
# Access services
|
||||
# Admin Panel: http://localhost:25666 (admin / admin123)
|
||||
# User Frontend: http://localhost:25137
|
||||
# Backend API: http://localhost:26039
|
||||
```
|
||||
|
||||
### Method 2: Step-by-step Deployment (Source Build)
|
||||
|
||||
If you need to build backend services from source, follow these steps:
|
||||
|
||||
#### Step 1: Deploy Backend Service
|
||||
|
||||
```bash
|
||||
# Enter backend project directory
|
||||
cd ruoyi-ai
|
||||
|
||||
# Start backend service (build from source)
|
||||
docker-compose up -d --build
|
||||
|
||||
# Wait for backend service to start
|
||||
docker-compose logs -f backend
|
||||
```
|
||||
|
||||
#### Step 2: Deploy Admin Panel
|
||||
|
||||
```bash
|
||||
# Enter admin panel project directory
|
||||
cd ruoyi-admin
|
||||
|
||||
# Build and start admin panel
|
||||
docker-compose up -d --build
|
||||
|
||||
# Access admin panel
|
||||
# URL: http://localhost:5666
|
||||
```
|
||||
|
||||
#### Step 3: Deploy User Frontend (Optional)
|
||||
|
||||
```bash
|
||||
# Enter user frontend project directory
|
||||
cd ruoyi-web
|
||||
|
||||
# Build and start user frontend
|
||||
docker-compose up -d --build
|
||||
|
||||
# Access user frontend
|
||||
# URL: http://localhost:5137
|
||||
```
|
||||
|
||||
### Service Ports
|
||||
|
||||
| Service | One-click Port | Step-by-step Port | Description |
|
||||
|------|-------------|-------------|------|
|
||||
| Admin Panel | 25666 | 5666 | Admin backend access |
|
||||
| User Frontend | 25137 | 5137 | User frontend access |
|
||||
| Backend Service | 26039 | 6039 | Backend API service |
|
||||
| MySQL | 23306 | 23306 | Database service |
|
||||
| Redis | 26379 | 6379 | Cache service |
|
||||
| Weaviate | 28080 | 28080 | Vector database |
|
||||
| MinIO API | 29000 | 9000 | Object storage API |
|
||||
| MinIO Console | 29090 | 9090 | Object storage console |
|
||||
|
||||
### Image Registry
|
||||
|
||||
All images are hosted on Alibaba Cloud Container Registry:
|
||||
|
||||
```
|
||||
crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai
|
||||
```
|
||||
|
||||
Available images:
|
||||
- `mysql:v3` - MySQL database (includes initialization SQL)
|
||||
- `redis:6.2` - Redis cache
|
||||
- `weaviate:1.30.0` - Vector database
|
||||
- `minio:latest` - Object storage
|
||||
- `ruoyi-ai-backend:latest` - Backend service
|
||||
- `ruoyi-ai-admin:latest` - Admin frontend
|
||||
- `ruoyi-ai-web:latest` - User frontend
|
||||
|
||||
### Common Commands
|
||||
|
||||
```bash
|
||||
# Stop all services
|
||||
docker-compose -f docker-compose-all.yaml down
|
||||
|
||||
# View service logs
|
||||
docker-compose -f docker-compose-all.yaml logs -f [service-name]
|
||||
|
||||
# Restart a service
|
||||
docker-compose -f docker-compose-all.yaml restart [service-name]
|
||||
```
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
Want to learn more about installation, deployment, configuration, and secondary development?
|
||||
|
||||
**👉 [Complete Documentation](https://doc.pandarobot.chat)**
|
||||
|
||||
Experiencing issues with knowledge base or RAG responses?
|
||||
|
||||
**👉 [RAG Troubleshooting Guide](docs/troubleshooting/rag-failures.md)**
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We warmly welcome community contributions! Whether you are a seasoned developer or just getting started, you can contribute to the project 💪
|
||||
|
||||
### How to Contribute
|
||||
|
||||
1. **Fork** the project to your account
|
||||
2. **Create a branch** (`git checkout -b feature/new-feature-name`)
|
||||
3. **Commit your changes** (`git commit -m 'Add new feature'`)
|
||||
4. **Push to the branch** (`git push origin feature/new-feature-name`)
|
||||
5. **Create a Pull Request**
|
||||
|
||||
> 💡 **Tip**: We recommend submitting PRs to GitHub, we will automatically sync to other code hosting platforms
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
Thanks to the following excellent open-source projects for their support:
|
||||
- [Spring AI Alibaba Copilot](https://github.com/spring-ai-alibaba/copilot) - Intelligent coding assistant based on spring-ai-alibaba
|
||||
- [Langchain4j](https://github.com/langchain4j/langchain4j) - Powerful Java LLM development framework
|
||||
- [RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus) - Mature enterprise-level rapid development framework
|
||||
- [Vben Admin](https://github.com/vbenjs/vue-vben-admin) - Modern Vue admin template
|
||||
|
||||
## 🌐 Ecosystem Partners
|
||||
|
||||
- [PPIO Cloud](https://ppinfra.com/user/register?invited_by=P8QTUY&utm_source=github_ruoyi-ai) - Provides cost-effective GPU computing and model API services
|
||||
- [Youyun Intelligent Computing](https://www.compshare.cn/?ytag=GPU_YY-gh_ruoyi) - Thousands of RTX40 series GPUs + mainstream models API services, second-level response, pay-per-use, free for new customers.
|
||||
|
||||
|
||||
## 💬 Community Chat
|
||||
|
||||
<div align="center">
|
||||
|
||||
**[📱 Join Telegram Group](
|
||||
https://t.me/+LqooQAc5HxRmYmE1)**
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
**[⭐ Star to Support](https://github.com/ageerle/ruoyi-ai)** • **[Fork to Contribute](https://github.com/ageerle/ruoyi-ai/fork)** • **[📚 中文](README.md)** • **[📖 Complete Documentation](https://doc.pandarobot.chat)**
|
||||
|
||||
*Built with ❤️, maintained by the RuoYi AI open-source community*
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Badge Links -->
|
||||
|
||||
[contributors-shield]: https://img.shields.io/github/contributors/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[contributors-url]: https://github.com/ageerle/ruoyi-ai/graphs/contributors
|
||||
|
||||
[forks-shield]: https://img.shields.io/github/forks/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[forks-url]: https://github.com/ageerle/ruoyi-ai/network/members
|
||||
|
||||
[stars-shield]: https://img.shields.io/github/stars/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[stars-url]: https://github.com/ageerle/ruoyi-ai/stargazers
|
||||
|
||||
[issues-shield]: https://img.shields.io/github/issues/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[issues-url]: https://github.com/ageerle/ruoyi-ai/issues
|
||||
|
||||
[license-shield]: https://img.shields.io/github/license/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[license-url]: https://github.com/ageerle/ruoyi-ai/blob/main/LICENSE
|
||||
282
README_ZH.md
Normal file
282
README_ZH.md
Normal file
@@ -0,0 +1,282 @@
|
||||
# RuoYi AI
|
||||
|
||||
<div align="center">
|
||||
|
||||
[![Contributors][contributors-shield]][contributors-url] [![Forks][forks-shield]][forks-url] [![Stargazers][stars-shield]][stars-url] [![Issues][issues-shield]][issues-url] [![MIT License][license-shield]][license-url]
|
||||
|
||||
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/13209">
|
||||
<img src="https://trendshift.io/api/badge/repositories/13209" alt="GitHub Trending">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<img src="docs/image/logo.png" alt="RuoYi AI Logo" width="120" height="120">
|
||||
|
||||
### 企业级AI助手平台
|
||||
|
||||
*开箱即用的全栈AI平台,支持多智能体协同、Supervisor模式编排、多种决策模式、RAG技术和流程编排能力*
|
||||
|
||||
**[English](README.md)** | **[📖 使用文档](https://doc.ruoyiai.chat/)** |
|
||||
**[🚀 在线体验](https://web.ruoyiai.chat/)** | **[🐛 问题反馈](https://github.com/ageerle/ruoyi-ai/issues)** | **[💡 功能建议](https://github.com/ageerle/ruoyi-ai/issues)**
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
## ✨ 核心亮点
|
||||
|
||||
| 模块 | 现有能力
|
||||
|:---------:|---
|
||||
| **模型管理** | 多模型接入(DeepSeek/智谱/MIMO/百炼/OpenAI)、多模态理解、Coze/DIFY/FastGPT/RAGFlow平台集成
|
||||
| **知识管理** | 本地RAG + 向量库(Milvus/Weaviate/Qdrant) + 文档解析
|
||||
| **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态
|
||||
| **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点
|
||||
| **智能体管理** | 基于Langchain4j的Agent框架、Supervisor模式编排,支持多种决策模型,可以灵活搭配工具,skills
|
||||
|
||||
|
||||
### 项目源码
|
||||
|
||||
| 项目模块 | GitHub 仓库 | Gitee 仓库 | GitCode 仓库 |
|
||||
|----------|-------------------------------------------------------|------------------------------------------------------|--------------------------------------------------------|
|
||||
| 🔧 后端服务 | [ruoyi-ai](https://github.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitee.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitcode.com/ageerle/ruoyi-ai) |
|
||||
| 🎨 用户前端 | [ruoyi-web](https://github.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitee.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitcode.com/ageerle/ruoyi-web) |
|
||||
| 🛠️ 管理后台 | [ruoyi-admin](https://github.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitee.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitcode.com/ageerle/ruoyi-admin) |
|
||||
| 🎬 短剧平台 | [ruoyi-drama](https://github.com/ageerle/ruoyi-drama) | [ruoyi-drama](https://gitee.com/ageerle/ruoyi-drama) | [ruoyi-drama](https://gitcode.com/ageerle/ruoyi-drama) |
|
||||
| 🤖 编程助手 | [ruoyi-copilot](https://github.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitee.com/ageerle/ruoyi-copilot) | [ruoyi-copilot](https://gitcode.com/ageerle/ruoyi-copilot) |
|
||||
| 📱 小程序端 | [ruoyi-uniapp](https://github.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitee.com/ageerle/ruoyi-uniapp) | [ruoyi-uniapp](https://gitcode.com/ageerle/ruoyi-uniapp) |
|
||||
|
||||
### 合作项目
|
||||
| 项目名称 | GitHub 仓库 | Gitee 仓库
|
||||
|----------------|-------------------------------------------------------|------------------------------------------------------|
|
||||
| element-plus-x | [element-plus-x](https://github.com/element-plus-x/Element-Plus-X) | [element-plus-x](https://gitee.com/he-jiayue/element-plus-x) |
|
||||
|
||||
## 🛠️ 技术架构
|
||||
|
||||
### 核心框架
|
||||
- **后端架构**:Spring Boot 3.5.8 + Langchain4j
|
||||
- **数据存储**:MySQL 8.0 + Redis + 向量数据库(Milvus/Weaviate/Qdrant)
|
||||
- **前端技术**:Vue 3 + Vben Admin + element-plus-x
|
||||
- **安全认证**:Sa-Token + JWT 双重保障
|
||||
- **文档处理**:PDF、Word、Excel 解析,图像智能分析
|
||||
- **实时通信**:WebSocket 实时通信,SSE 流式响应
|
||||
- **系统监控**:完善的日志体系、性能监控、服务健康检查
|
||||
|
||||
## 🐳 Docker 部署
|
||||
|
||||
本项目提供两种 Docker 部署方式:
|
||||
|
||||
### 方式一:一键启动所有服务(推荐)
|
||||
|
||||
使用 `docker-compose-all.yaml` 可以一键启动所有服务(包括后端、管理端、用户端及依赖服务):
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
git clone https://github.com/ageerle/ruoyi-ai.git
|
||||
cd ruoyi-ai
|
||||
|
||||
# 启动所有服务(从 GHCR 拉取预构建镜像)
|
||||
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml up -d
|
||||
|
||||
# 查看服务状态
|
||||
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml ps
|
||||
|
||||
# 访问服务
|
||||
# 管理端: http://localhost:25666 (admin / admin123)
|
||||
# 用户端: http://localhost:25137
|
||||
# 后端API: http://localhost:26039
|
||||
```
|
||||
|
||||
### 方式二:分步部署(源码编译)
|
||||
|
||||
如果您需要从源码构建后端服务,请按照以下步骤操作:
|
||||
|
||||
#### 第一步:部署后端服务
|
||||
|
||||
```bash
|
||||
# 进入后端项目目录
|
||||
cd ruoyi-ai
|
||||
|
||||
# 启动后端服务(源码编译构建)
|
||||
docker-compose up -d --build
|
||||
|
||||
# 等待后端服务启动完成
|
||||
docker-compose logs -f backend
|
||||
```
|
||||
|
||||
#### 第二步:部署管理端
|
||||
|
||||
```bash
|
||||
# 进入管理端项目目录
|
||||
cd ruoyi-admin
|
||||
|
||||
# 构建并启动管理端
|
||||
docker-compose up -d --build
|
||||
|
||||
# 访问管理端
|
||||
# 地址: http://localhost:5666
|
||||
```
|
||||
|
||||
#### 第三步:部署用户端(可选)
|
||||
|
||||
```bash
|
||||
# 进入用户端项目目录
|
||||
cd ruoyi-web
|
||||
|
||||
# 构建并启动用户端
|
||||
docker-compose up -d --build
|
||||
|
||||
# 访问用户端
|
||||
# 地址: http://localhost:5137
|
||||
```
|
||||
|
||||
### 服务端口说明
|
||||
|
||||
| 服务 | 一键启动端口 | 分步部署端口 | 说明 |
|
||||
|------|-------------|-------------|------|
|
||||
| 管理端 | 25666 | 5666 | 管理后台访问地址 |
|
||||
| 用户端 | 25137 | 5137 | 用户前端访问地址 |
|
||||
| 后端服务 | 26039 | 6039 | 后端 API 服务 |
|
||||
| MySQL | 23306 | 23306 | 数据库服务 |
|
||||
| Redis | 26379 | 6379 | 缓存服务 |
|
||||
| Weaviate | 28080 | 28080 | 向量数据库 |
|
||||
| MinIO API | 29000 | 9000 | 对象存储 API |
|
||||
| MinIO Console | 29090 | 9090 | 对象存储控制台 |
|
||||
|
||||
### 镜像仓库
|
||||
|
||||
每次推送类似 `v3.1.0` 的版本标签后,GitHub Actions 会自动构建并发布应用镜像到 GitHub Container Registry(GHCR):
|
||||
|
||||
```
|
||||
ghcr.io/ageerle/ruoyi-ai-backend:v3.1.0
|
||||
ghcr.io/ageerle/ruoyi-ai-mysql:v3.1.0
|
||||
ghcr.io/ageerle/ruoyi-ai-admin:v3.1.0
|
||||
ghcr.io/ageerle/ruoyi-ai-web:v3.1.0
|
||||
```
|
||||
|
||||
Compose 默认使用 `latest`。如需固定版本,可在 `docs/docker/ruoyi-ai/.env` 中设置 `RUIYI_VERSION=v3.1.0`。
|
||||
|
||||
首次工作流运行完成后,请在 GitHub 的 Packages 设置中将这四个 GHCR 镜像设为 `Public`,用户服务器才能免登录拉取。
|
||||
|
||||
管理端 `ageerle/ruoyi-admin` 和用户端 `ageerle/ruoyi-web` 也必须存在同名版本标签;发布工作流会使用后端发布标签检出这两个仓库后再构建镜像。
|
||||
|
||||
### 常用命令
|
||||
|
||||
```bash
|
||||
# 停止所有服务
|
||||
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml down
|
||||
|
||||
# 查看服务日志
|
||||
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml logs -f [服务名]
|
||||
|
||||
# 重启某个服务
|
||||
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml restart [服务名]
|
||||
```
|
||||
|
||||
## 📚 使用文档
|
||||
|
||||
想要深入了解安装部署、功能配置和二次开发?
|
||||
|
||||
**👉 [完整使用文档](https://doc.ruoyiai.chat/)**
|
||||
|
||||
## 🤝 参与贡献
|
||||
|
||||
我们热烈欢迎社区贡献!无论您是资深开发者还是初学者,都可以为项目贡献力量 💪
|
||||
|
||||
### 贡献方式
|
||||
|
||||
1. **Fork** 项目到您的账户
|
||||
2. **创建分支** (`git checkout -b feature/新功能名称`)
|
||||
3. **提交代码** (`git commit -m '添加某某功能'`)
|
||||
4. **推送分支** (`git push origin feature/新功能名称`)
|
||||
5. **发起 Pull Request**
|
||||
|
||||
> 💡 **小贴士**:建议将 PR 提交到 GitHub,我们会自动同步到其他代码托管平台
|
||||
|
||||
## 📄 开源协议
|
||||
|
||||
本项目采用 **MIT 开源协议**,详情请查看 [LICENSE](LICENSE) 文件。
|
||||
|
||||
## 🙏 特别鸣谢
|
||||
|
||||
感谢以下优秀的开源项目为本项目提供支持:
|
||||
- [Langchain4j](https://github.com/langchain4j/langchain4j) - 强大的 Java LLM 开发框架
|
||||
- [RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus) - 成熟的企业级快速开发框架
|
||||
- [Vben Admin](https://github.com/vbenjs/vue-vben-admin) - 现代化的 Vue 后台管理模板
|
||||
|
||||
|
||||
## 💎 赞助商
|
||||
|
||||
**感谢以下赞助商对本项目的支持:**
|
||||
|
||||
<a href="https://www.atlascloud.ai?ref=89F97E">
|
||||
<img src="docs/image/sponsor/atlascloud_banner.png" alt="Atlas Cloud" width="160" height="80">
|
||||
</a>
|
||||
|
||||
[访问Atlas Cloud官网](https://www.atlascloud.ai?ref=89F97E&utm_source=github&utm_campaign=ruoyi-drama) · [编程计划优惠](https://www.atlascloud.ai/console/coding-plan)
|
||||
全模态 AI 推理平台,为开发者提供统一的 AI API,支持视频生成、图像生成和大语言模型。一次接入,即可访问 **300+ 精选模型**。
|
||||
|
||||
<a href="https://www.volcengine.com/activity/codingplan?utm_campaign=hw&utm_content=hw&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ageerle-ruoyi-ai">
|
||||
<img src="docs/image/sponsor/huoshan.png" alt="火山引擎 CodingPlan" width="160" height="80">
|
||||
</a>
|
||||
|
||||
[注册即领2500万Tokens,立即前往](https://www.volcengine.com/activity/ai618?utm_campaign=hw&utm_content=hw&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ageerle-ruoyi-ai)
|
||||
享字节自研豆包模型+满血版开源 SOTA模型,覆盖文本、VLM、图像生成,全模态一站配齐:Seed-2.1、Seedream-5.0、GLM-5.2、DeepSeek等。不止编程、更能解决 Agent 复杂长程任务!
|
||||
|
||||
|
||||
## 💬 社区交流
|
||||
|
||||
<div align="center">
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="docs/image/wx.png" alt="微信二维码" width="200" height="200"><br>
|
||||
<strong>扫码添加作者微信</strong><br>
|
||||
<em>邀请进群学习</em>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="docs/image/wx06.png" alt="微信二维码" width="200" height="200"><br>
|
||||
<strong>微信技术交流群</strong><br>
|
||||
<em>技术讨论</em>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="docs/image/qq.png" alt="QQ群二维码" width="200" height="200"><br>
|
||||
<strong>QQ技术交流群</strong><br>
|
||||
<em>技术讨论</em>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
<div align="center">
|
||||
|
||||
**[⭐ 点个Star支持一下](https://github.com/ageerle/ruoyi-ai)** • **[ Fork 开始贡献](https://github.com/ageerle/ruoyi-ai/fork)** • **[📚 English](README.md)** • **[📖 查看完整文档](https://doc.ruoyiai.chat/)**
|
||||
|
||||
*用 ❤️ 打造,由 RuoYi AI 开源社区维护*
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Badge Links -->
|
||||
|
||||
[contributors-shield]: https://img.shields.io/github/contributors/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[contributors-url]: https://github.com/ageerle/ruoyi-ai/graphs/contributors
|
||||
|
||||
[forks-shield]: https://img.shields.io/github/forks/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[forks-url]: https://github.com/ageerle/ruoyi-ai/network/members
|
||||
|
||||
[stars-shield]: https://img.shields.io/github/stars/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[stars-url]: https://github.com/ageerle/ruoyi-ai/stargazers
|
||||
|
||||
[issues-shield]: https://img.shields.io/github/issues/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[issues-url]: https://github.com/ageerle/ruoyi-ai/issues
|
||||
|
||||
[license-shield]: https://img.shields.io/github/license/ageerle/ruoyi-ai.svg?style=flat-square
|
||||
|
||||
[license-url]: https://github.com/ageerle/ruoyi-ai/blob/main/LICENSE
|
||||
@@ -1,8 +1,6 @@
|
||||
version: '3.5'
|
||||
|
||||
services:
|
||||
etcd:
|
||||
container_name: milvus-etcd
|
||||
container_name: ruoyi-rag-milvus-etcd
|
||||
image: quay.io/coreos/etcd:v3.5.18
|
||||
environment:
|
||||
- ETCD_AUTO_COMPACTION_MODE=revision
|
||||
@@ -19,14 +17,11 @@ services:
|
||||
retries: 3
|
||||
|
||||
minio:
|
||||
container_name: milvus-minio
|
||||
container_name: ruoyi-rag-milvus-minio
|
||||
image: minio/minio:RELEASE.2023-03-20T20-16-18Z
|
||||
environment:
|
||||
MINIO_ACCESS_KEY: minioadmin
|
||||
MINIO_SECRET_KEY: minioadmin
|
||||
ports:
|
||||
- "9001:9001"
|
||||
- "9000:9000"
|
||||
volumes:
|
||||
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data
|
||||
command: minio server /minio_data --console-address ":9001"
|
||||
@@ -37,7 +32,7 @@ services:
|
||||
retries: 3
|
||||
|
||||
standalone:
|
||||
container_name: milvus-standalone
|
||||
container_name: ruoyi-rag-milvus
|
||||
image: milvusdb/milvus:v2.5.7
|
||||
command: ["milvus", "run", "standalone"]
|
||||
security_opt:
|
||||
@@ -61,7 +56,7 @@ services:
|
||||
- "minio"
|
||||
|
||||
attu:
|
||||
container_name: attu
|
||||
container_name: ruoyi-rag-attu
|
||||
image: zilliz/attu:v2.5.7
|
||||
environment:
|
||||
MILVUS_URL: milvus-standalone:19530
|
||||
@@ -72,4 +67,4 @@ services:
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: milvus
|
||||
name: ruoyi-rag-milvus
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
---
|
||||
services:
|
||||
qdrant:
|
||||
image: qdrant/qdrant:latest
|
||||
container_name: ruoyi-rag-qdrant
|
||||
image: qdrant/qdrant:v1.17.0
|
||||
ports:
|
||||
- 6333:6333
|
||||
- 6334:6334
|
||||
volumes:
|
||||
- qdrant_data:/qdrant/storage
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "bash -c 'exec 3<>/dev/tcp/127.0.0.1/6333 && printf \"GET /healthz HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n\" >&3 && grep -q \"200 OK\" <&3'"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
start_period: 10s
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
qdrant_data:
|
||||
...
|
||||
|
||||
6
docs/docker/ruoyi-ai/.env.example
Normal file
6
docs/docker/ruoyi-ai/.env.example
Normal file
@@ -0,0 +1,6 @@
|
||||
# GitHub Container Registry namespace owner.
|
||||
# For the upstream project this is ageerle; change it when using a fork.
|
||||
IMAGE_OWNER=ageerle
|
||||
|
||||
# Use latest for convenience, or pin a release such as v3.1.0.
|
||||
RUIYI_VERSION=latest
|
||||
@@ -20,6 +20,22 @@ RUN mvn clean package -Pprod -DskipTests
|
||||
# 最终运行镜像
|
||||
FROM eclipse-temurin:17-jre-alpine
|
||||
|
||||
RUN set -eux; \
|
||||
apk add --no-cache ffmpeg; \
|
||||
for filter in scale pad fps setsar format tpad trim settb setpts xfade \
|
||||
aresample aformat apad atrim asetpts anullsrc concat acrossfade; do \
|
||||
ffmpeg -hide_banner -filters 2>/dev/null | grep -Eq "[[:space:]]${filter}[[:space:]]"; \
|
||||
done; \
|
||||
ffmpeg -hide_banner -h filter=xfade 2>&1 | grep -q dissolve; \
|
||||
ffmpeg -hide_banner -h filter=xfade 2>&1 | grep -q fadeblack; \
|
||||
ffmpeg -hide_banner -h filter=xfade 2>&1 | grep -q slideleft; \
|
||||
ffmpeg -hide_banner -encoders 2>/dev/null | grep -Eq '[[:space:]]libx264[[:space:]]'; \
|
||||
ffmpeg -hide_banner -encoders 2>/dev/null | grep -Eq '[[:space:]]aac[[:space:]]'; \
|
||||
ffprobe -hide_banner -version >/dev/null
|
||||
|
||||
ENV FFMPEG_PATH=/usr/bin/ffmpeg \
|
||||
FFPROBE_PATH=/usr/bin/ffprobe
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
@@ -27,10 +43,10 @@ WORKDIR /app
|
||||
COPY --from=builder /build/ruoyi-admin/target/ruoyi-admin.jar ./ruoyi-admin.jar
|
||||
|
||||
# 创建日志目录
|
||||
RUN mkdir -p /ruoyi/server/logs
|
||||
RUN mkdir -p /ruoyi/server/logs /ruoyi/server/temp
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 6039
|
||||
|
||||
# 启动命令
|
||||
ENTRYPOINT ["java", "-jar", "ruoyi-admin.jar", "--spring.profiles.active=prod"]
|
||||
ENTRYPOINT ["java", "-Djava.io.tmpdir=/ruoyi/server/temp", "-jar", "ruoyi-admin.jar", "--spring.profiles.active=prod"]
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
# 基于官方MySQL 8.0镜像构建自定义镜像
|
||||
# 构建命令: docker build -t registry.cn-hangzhou.aliyuncs.com/ruoyi-ai/mysql:v3 -f Dockerfile.mysql .
|
||||
FROM mysql:8.0.33
|
||||
FROM mysql:8.0
|
||||
|
||||
# 设置时区
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
# 复制初始化脚本和SQL文件到镜像中
|
||||
COPY docs/script/docker/mysql/init/init-db.sh /docker-entrypoint-initdb.d/init-db.sh
|
||||
COPY docs/script/sql/ruoyi-ai-v3_mysql8.sql /docker-entrypoint-initdb.d/ruoyi-ai-v3_mysql8.sql
|
||||
|
||||
# 设置脚本可执行权限
|
||||
RUN chmod +x /docker-entrypoint-initdb.d/init-db.sh
|
||||
# MySQL 官方入口会按文件名顺序执行初始化目录中的 SQL。
|
||||
# ruoyi-ai.sql 会导入 MYSQL_DATABASE,snail_job_mysql.sql 会自行创建并切换数据库。
|
||||
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
|
||||
|
||||
# MySQL启动参数
|
||||
CMD ["--default-authentication-plugin=mysql_native_password", \
|
||||
@@ -18,4 +16,4 @@ CMD ["--default-authentication-plugin=mysql_native_password", \
|
||||
"--collation-server=utf8mb4_general_ci", \
|
||||
"--explicit_defaults_for_timestamp=true", \
|
||||
"--lower_case_table_names=1", \
|
||||
"--skip-ssl"]
|
||||
"--skip-ssl"]
|
||||
|
||||
@@ -10,15 +10,13 @@
|
||||
# - RuoYi-Admin (管理端前端)
|
||||
# - RuoYi-Web (用户端前端)
|
||||
#
|
||||
# 镜像仓库地址: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai
|
||||
|
||||
version: '3.8'
|
||||
# 镜像仓库地址: ghcr.io/ageerle
|
||||
|
||||
services:
|
||||
# ==================== MySQL 数据库 ====================
|
||||
mysql:
|
||||
# 阿里云镜像地址(包含初始化SQL)
|
||||
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/mysql:v3
|
||||
# GHCR 镜像(包含初始化SQL)
|
||||
image: ghcr.io/${IMAGE_OWNER:-ageerle}/ruoyi-ai-mysql:${RUIYI_VERSION:-latest}
|
||||
container_name: ruoyi-ai-mysql
|
||||
restart: always
|
||||
ports:
|
||||
@@ -30,7 +28,10 @@ services:
|
||||
volumes:
|
||||
- mysql-data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-proot"]
|
||||
# Force TCP: during first-time initialization MySQL exposes a temporary
|
||||
# socket-only server. A socket ping would mark the service healthy before
|
||||
# the real network listener (used by the backend) is ready.
|
||||
test: ["CMD", "mysqladmin", "ping", "--protocol=tcp", "-h", "127.0.0.1", "-u", "root", "-proot"]
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
@@ -40,7 +41,7 @@ services:
|
||||
|
||||
# ==================== Redis 缓存 ====================
|
||||
redis:
|
||||
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/redis:6.2
|
||||
image: redis:6.2
|
||||
container_name: ruoyi-ai-redis
|
||||
restart: always
|
||||
ports:
|
||||
@@ -58,14 +59,14 @@ services:
|
||||
|
||||
# ==================== Weaviate 向量数据库 ====================
|
||||
weaviate:
|
||||
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/weaviate:1.30.0
|
||||
image: semitechnologies/weaviate:1.30.0
|
||||
container_name: ruoyi-ai-weaviate
|
||||
restart: always
|
||||
ports:
|
||||
- "28080:8080"
|
||||
environment:
|
||||
QUERY_DEFAULTS_LIMIT: 25
|
||||
AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: true
|
||||
AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: "true"
|
||||
PERSISTENCE_DATA_PATH: /var/lib/weaviate
|
||||
DEFAULT_VECTORIZER_MODULE: none
|
||||
ENABLE_MODULES: text2vec-cohere,text2vec-huggingface,text2vec-palm,text2vec-openai,generative-openai,generative-cohere,generative-palm,ref2vec-centroid,reranker-cohere,qna-openai
|
||||
@@ -77,7 +78,7 @@ services:
|
||||
|
||||
# ==================== MinIO 对象存储 ====================
|
||||
minio:
|
||||
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/minio:latest
|
||||
image: minio/minio:latest
|
||||
container_name: ruoyi-ai-minio
|
||||
restart: always
|
||||
ports:
|
||||
@@ -94,7 +95,7 @@ services:
|
||||
|
||||
# ==================== RuoYi-AI 后端服务 ====================
|
||||
backend:
|
||||
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/ruoyi-ai-backend:latest
|
||||
image: ghcr.io/${IMAGE_OWNER:-ageerle}/ruoyi-ai-backend:${RUIYI_VERSION:-latest}
|
||||
container_name: ruoyi-ai-backend
|
||||
restart: always
|
||||
ports:
|
||||
@@ -128,7 +129,7 @@ services:
|
||||
|
||||
# ==================== RuoYi-AI 管理端前端 ====================
|
||||
admin-frontend:
|
||||
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/ruoyi-ai-admin:latest
|
||||
image: ghcr.io/${IMAGE_OWNER:-ageerle}/ruoyi-ai-admin:${RUIYI_VERSION:-latest}
|
||||
container_name: ruoyi-ai-admin
|
||||
restart: always
|
||||
ports:
|
||||
@@ -153,7 +154,7 @@ services:
|
||||
|
||||
# ==================== RuoYi-AI 用户端前端 ====================
|
||||
web-frontend:
|
||||
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/ruoyi-ai-web:latest
|
||||
image: ghcr.io/${IMAGE_OWNER:-ageerle}/ruoyi-ai-web:${RUIYI_VERSION:-latest}
|
||||
container_name: ruoyi-ai-web
|
||||
restart: always
|
||||
ports:
|
||||
@@ -177,4 +178,4 @@ volumes:
|
||||
weaviate-data:
|
||||
minio-data:
|
||||
logs-data:
|
||||
upload-data:
|
||||
upload-data:
|
||||
|
||||
@@ -21,8 +21,8 @@ services:
|
||||
MYSQL_DATABASE: ruoyi-ai-agent
|
||||
TZ: Asia/Shanghai
|
||||
volumes:
|
||||
- ./docs/script/docker/mysql/init/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh:ro
|
||||
- ./docs/script/sql/ruoyi-ai-v3_mysql8.sql:/docker-entrypoint-initdb.d/ruoyi-ai-v3_mysql8.sql:ro
|
||||
- ../../script/sql/ruoyi-ai.sql:/docker-entrypoint-initdb.d/01-ruoyi-ai.sql:ro
|
||||
- ../../script/sql/snail_job_mysql.sql:/docker-entrypoint-initdb.d/02-snail-job.sql:ro
|
||||
- mysql-data:/var/lib/mysql
|
||||
command:
|
||||
--default-authentication-plugin=mysql_native_password
|
||||
@@ -32,7 +32,9 @@ services:
|
||||
--lower_case_table_names=1
|
||||
--skip-ssl
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-proot"]
|
||||
# Do not let the socket-only initialization server satisfy the check.
|
||||
# The backend connects over TCP and must wait for that listener.
|
||||
test: ["CMD", "mysqladmin", "ping", "--protocol=tcp", "-h", "127.0.0.1", "-u", "root", "-proot"]
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
|
||||
@@ -5,12 +5,12 @@ services:
|
||||
- --host
|
||||
- 0.0.0.0
|
||||
- --port
|
||||
- '6038'
|
||||
- '8080'
|
||||
- --scheme
|
||||
- http
|
||||
image: semitechnologies/weaviate:1.19.7
|
||||
image: semitechnologies/weaviate:1.30.0
|
||||
ports:
|
||||
- 6038:6038
|
||||
- 28080:8080
|
||||
- 50051:50051
|
||||
volumes:
|
||||
- weaviate_data:/var/lib/weaviate
|
||||
|
||||
BIN
docs/image/01.png
Normal file
BIN
docs/image/01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 298 KiB |
BIN
docs/image/02.png
Normal file
BIN
docs/image/02.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 185 KiB |
BIN
docs/image/03.png
Normal file
BIN
docs/image/03.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 316 KiB |
BIN
docs/image/sponsor/atlascloud_banner.png
Normal file
BIN
docs/image/sponsor/atlascloud_banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
docs/image/sponsor/huoshan.png
Normal file
BIN
docs/image/sponsor/huoshan.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
BIN
docs/image/wx06.png
Normal file
BIN
docs/image/wx06.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
@@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
# 数据库初始化脚本
|
||||
# 使用 --force 参数确保即使出错也继续执行
|
||||
|
||||
echo "开始初始化数据库..."
|
||||
|
||||
# 使用 --force 参数忽略错误继续执行
|
||||
mysql -uroot -proot ruoyi-ai-agent --force < /docker-entrypoint-initdb.d/ruoyi-ai-v3_mysql8.sql
|
||||
|
||||
echo "数据库初始化完成"
|
||||
86
docs/script/install-ffmpeg-windows.ps1
Normal file
86
docs/script/install-ffmpeg-windows.ps1
Normal file
@@ -0,0 +1,86 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$Force
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Find-Executable([string]$Name) {
|
||||
$command = Get-Command $Name -ErrorAction SilentlyContinue
|
||||
if ($command) {
|
||||
return $command.Source
|
||||
}
|
||||
|
||||
$roots = @(
|
||||
(Join-Path $env:LOCALAPPDATA 'Microsoft\WinGet\Packages'),
|
||||
(Join-Path $env:USERPROFILE 'scoop\apps\ffmpeg'),
|
||||
'C:\ProgramData\chocolatey\bin',
|
||||
'C:\ffmpeg'
|
||||
)
|
||||
foreach ($root in $roots) {
|
||||
if (-not (Test-Path $root)) { continue }
|
||||
$match = Get-ChildItem -Path $root -Filter "$Name.exe" -Recurse -ErrorAction SilentlyContinue |
|
||||
Select-Object -First 1 -ExpandProperty FullName
|
||||
if ($match) { return $match }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
$ffmpeg = Find-Executable 'ffmpeg'
|
||||
$ffprobe = Find-Executable 'ffprobe'
|
||||
|
||||
if ($Force -or -not $ffmpeg -or -not $ffprobe) {
|
||||
$winget = Get-Command winget -ErrorAction SilentlyContinue
|
||||
if (-not $winget) {
|
||||
throw '未找到 winget。请先从 Microsoft Store 安装“应用安装程序”,或使用项目 Dockerfile 运行后端。'
|
||||
}
|
||||
|
||||
Write-Host '正在通过 winget 安装 FFmpeg...' -ForegroundColor Cyan
|
||||
& winget install --id Gyan.FFmpeg --exact --source winget `
|
||||
--accept-package-agreements --accept-source-agreements
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "winget 安装失败,退出码: $LASTEXITCODE"
|
||||
}
|
||||
|
||||
$ffmpeg = Find-Executable 'ffmpeg'
|
||||
$ffprobe = Find-Executable 'ffprobe'
|
||||
}
|
||||
|
||||
if (-not $ffmpeg -or -not $ffprobe) {
|
||||
throw 'FFmpeg 已安装但未能定位 ffmpeg.exe 或 ffprobe.exe。请重新打开 PowerShell 后再次运行脚本。'
|
||||
}
|
||||
|
||||
$binDirectory = Split-Path -Parent $ffmpeg
|
||||
[Environment]::SetEnvironmentVariable('FFMPEG_PATH', $ffmpeg, 'User')
|
||||
[Environment]::SetEnvironmentVariable('FFPROBE_PATH', $ffprobe, 'User')
|
||||
|
||||
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||
$pathParts = @($userPath -split ';' | Where-Object { $_ })
|
||||
if ($pathParts -notcontains $binDirectory) {
|
||||
$newPath = (@($pathParts) + $binDirectory) -join ';'
|
||||
[Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
|
||||
}
|
||||
|
||||
$env:FFMPEG_PATH = $ffmpeg
|
||||
$env:FFPROBE_PATH = $ffprobe
|
||||
if (($env:Path -split ';') -notcontains $binDirectory) {
|
||||
$env:Path = "$binDirectory;$env:Path"
|
||||
}
|
||||
|
||||
Write-Host "ffmpeg: $ffmpeg" -ForegroundColor Green
|
||||
Write-Host "ffprobe: $ffprobe" -ForegroundColor Green
|
||||
|
||||
& $ffmpeg -hide_banner -version | Select-Object -First 1
|
||||
& $ffprobe -hide_banner -version | Select-Object -First 1
|
||||
|
||||
$encoders = & $ffmpeg -hide_banner -encoders 2>&1 | Out-String
|
||||
if ($encoders -notmatch '\blibx264\b') {
|
||||
throw '当前 FFmpeg 不包含 libx264 编码器,无法满足短剧合成配置。'
|
||||
}
|
||||
if ($encoders -notmatch '\baac\b') {
|
||||
throw '当前 FFmpeg 不包含 AAC 编码器,无法满足短剧合成配置。'
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Write-Host '安装与配置完成。请完全重启 IntelliJ IDEA 和 ruoyi-ai 后端服务。' -ForegroundColor Yellow
|
||||
Write-Host '重启后,Spring 将从 FFMPEG_PATH 和 FFPROBE_PATH 读取绝对路径。'
|
||||
File diff suppressed because one or more lines are too long
545
docs/script/sql/snail_job_mysql.sql
Normal file
545
docs/script/sql/snail_job_mysql.sql
Normal file
File diff suppressed because it is too large
Load Diff
58
docs/script/sql/update/2026-07-20-knowledge-fragment-fid.sql
Normal file
58
docs/script/sql/update/2026-07-20-knowledge-fragment-fid.sql
Normal file
@@ -0,0 +1,58 @@
|
||||
-- RAG metadata migration (MySQL 8). Safe to execute repeatedly.
|
||||
-- 注意:MySQL 8 不支持 ALTER TABLE ... ADD COLUMN IF NOT EXISTS(仅 MariaDB 支持),
|
||||
-- 因此列的增量添加统一用 information_schema 守卫 + PREPARE 实现幂等。
|
||||
SET @add_file_hash_col = IF(EXISTS(
|
||||
SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE()
|
||||
AND table_name = 'knowledge_attach' AND column_name = 'file_hash'),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE `knowledge_attach` ADD COLUMN `file_hash` varchar(64) NULL DEFAULT NULL COMMENT ''文件SHA-256摘要'' AFTER `doc_id`');
|
||||
PREPARE stmt FROM @add_file_hash_col; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
ALTER TABLE `knowledge_attach`
|
||||
MODIFY COLUMN `doc_id` varchar(32) NULL DEFAULT NULL COMMENT '文档ID';
|
||||
|
||||
SET @add_file_hash = IF(EXISTS(
|
||||
SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE()
|
||||
AND table_name = 'knowledge_attach' AND index_name = 'uk_knowledge_file_hash'),
|
||||
'SELECT 1', 'ALTER TABLE `knowledge_attach` ADD UNIQUE INDEX `uk_knowledge_file_hash` (`knowledge_id`, `file_hash`)');
|
||||
PREPARE stmt FROM @add_file_hash; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @add_fid_col = IF(EXISTS(
|
||||
SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE()
|
||||
AND table_name = 'knowledge_fragment' AND column_name = 'fid'),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE `knowledge_fragment` ADD COLUMN `fid` varchar(32) NULL DEFAULT NULL COMMENT ''向量库片段ID'' AFTER `id`');
|
||||
PREPARE stmt FROM @add_fid_col; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
ALTER TABLE `knowledge_fragment`
|
||||
MODIFY COLUMN `doc_id` varchar(32) NULL DEFAULT NULL COMMENT '文档ID';
|
||||
|
||||
UPDATE `knowledge_fragment`
|
||||
SET `fid` = LOWER(MD5(CONCAT('knowledge_fragment:', `id`)))
|
||||
WHERE `fid` IS NULL OR `fid` = '';
|
||||
|
||||
SET @drop_idx_fid = IF(EXISTS(
|
||||
SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE()
|
||||
AND table_name = 'knowledge_fragment' AND index_name = 'idx_fid'),
|
||||
'ALTER TABLE `knowledge_fragment` DROP INDEX `idx_fid`', 'SELECT 1');
|
||||
PREPARE stmt FROM @drop_idx_fid; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @add_uk_fid = IF(EXISTS(
|
||||
SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE()
|
||||
AND table_name = 'knowledge_fragment' AND index_name = 'uk_fid'),
|
||||
'SELECT 1', 'ALTER TABLE `knowledge_fragment` ADD UNIQUE INDEX `uk_fid` (`fid`)');
|
||||
PREPARE stmt FROM @add_uk_fid; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
ALTER TABLE `knowledge_fragment` MODIFY COLUMN `fid` varchar(32) NOT NULL COMMENT '向量库片段ID';
|
||||
|
||||
SET @add_tenant_user = IF(EXISTS(
|
||||
SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE()
|
||||
AND table_name = 'knowledge_info' AND index_name = 'idx_tenant_user'),
|
||||
'SELECT 1', 'ALTER TABLE `knowledge_info` ADD INDEX `idx_tenant_user` (`tenant_id`, `user_id`)');
|
||||
PREPARE stmt FROM @add_tenant_user; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @add_tenant_share = IF(EXISTS(
|
||||
SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE()
|
||||
AND table_name = 'knowledge_info' AND index_name = 'idx_tenant_share'),
|
||||
'SELECT 1', 'ALTER TABLE `knowledge_info` ADD INDEX `idx_tenant_share` (`tenant_id`, `share`)');
|
||||
PREPARE stmt FROM @add_tenant_share; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
@@ -0,0 +1,7 @@
|
||||
-- 加宽 chat_provider.provider_icon 字段 (对应 issue IHPUDA)
|
||||
-- 背景:文件系统使用 minio 私有桶时,厂商图标存的是带签名的临时访问 URL,
|
||||
-- 长度常超过 255,导致「Data too long for column 'provider_icon'」。
|
||||
-- MODIFY COLUMN 可重复执行。
|
||||
|
||||
ALTER TABLE `chat_provider`
|
||||
MODIFY COLUMN `provider_icon` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '厂商图标';
|
||||
@@ -0,0 +1,33 @@
|
||||
-- 补充工作流节点消息模板配置 (对应 issue IJX5VV)
|
||||
-- 背景:NodeMessageTemplateEnum 依赖以下 7 个 sys_config 键,缺失时
|
||||
-- WorkflowMessageUtil.getNodeMessageTemplate 会抛出「请先配置该节点的响应模板」。
|
||||
-- 这批配置在历史提交 20d531c0 中存在,SQL 脚本合并重命名时遗失,此处恢复。
|
||||
-- 幂等:按 config_key + tenant_id 判重,可重复执行。
|
||||
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||
SELECT 2027192921483309058, '000000', 'HTTP请求节点响应模板', 'node.httpRequest.template', '✅ HTTP请求节点:结束响应 - ', 'Y', 103, 1, '2026-02-27 09:23:51', 1, '2026-02-27 09:31:41', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.httpRequest.template' AND `tenant_id` = '000000');
|
||||
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||
SELECT 2027193296990957569, '000000', '文生图节点响应模板', 'node.image.template', '🎨 文生图节点:结束响应 - 图片URL: ', 'Y', 103, 1, '2026-02-27 09:25:20', 1, '2026-02-27 09:31:52', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.image.template' AND `tenant_id` = '000000');
|
||||
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||
SELECT 2027193820393959425, '000000', '发送邮箱节点响应模板', 'node.mailsend.template', '📧 发送邮箱节点:结束响应 - ', 'Y', 103, 1, '2026-02-27 09:27:25', 1, '2026-02-27 09:32:05', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.mailsend.template' AND `tenant_id` = '000000');
|
||||
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||
SELECT 2027194134438277122, '000000', '结束节点响应模板', 'node.end.template', '🔚 流程已执行完毕,如果您有其他需求,请随时重新发起请求。', 'Y', 103, 1, '2026-02-27 09:28:40', 1, '2026-02-27 09:32:53', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.end.template' AND `tenant_id` = '000000');
|
||||
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||
SELECT 2027208880369647617, '000000', '条件分支节点响应模板', 'node.switch.template', '🔀 条件分支节点:触发 -> 跳转到节点 ', 'Y', 103, 1, '2026-02-27 10:27:15', 1, '2026-02-27 10:35:54', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.switch.template' AND `tenant_id` = '000000');
|
||||
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||
SELECT 2027213914603995137, '000000', '大模型回答节点响应模板', 'node.llmAnswer.template', '🤖 LLM 节点 生成回答:', 'Y', 103, 1, '2026-02-27 10:47:16', 1, '2026-02-27 10:52:40', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.llmAnswer.template' AND `tenant_id` = '000000');
|
||||
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||
SELECT 2027217577397391361, '000000', '工作流异常响应模板', 'node.exception.template', '🛑 工作流发生异常:', 'N', 103, 1, '2026-02-27 11:01:49', 1, '2026-02-27 11:02:01', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.exception.template' AND `tenant_id` = '000000');
|
||||
@@ -0,0 +1,119 @@
|
||||
-- 注册链路修复:补齐缺失的菜单权限标识 + 新建注册默认角色
|
||||
-- 关联问题:
|
||||
-- #3 注册用户无默认角色 -> 无权限访问 AI 对话界面
|
||||
-- #4 sys_menu 缺 system:session:* / system:attach:* / system:fragment:*,
|
||||
-- 且 知识管理 父菜单 perms 为 knowledge:info:list,与控制器
|
||||
-- KnowledgeInfoController 用的 system:info:list 对不上,:list 无法授权。
|
||||
-- 本脚本幂等,可重复执行(INSERT IGNORE + UPDATE 天然幂等)。
|
||||
-- 新增 menu_id / role_id / config_id 统一使用 2099010100000000xxx 段,
|
||||
-- 与现有 snowflake id(2xxxxxxxxxxxxxxxxx)不冲突。
|
||||
|
||||
-- ============================================================
|
||||
-- 1. 修正 知识管理 父菜单权限标识:knowledge:info:list -> system:info:list
|
||||
-- ============================================================
|
||||
UPDATE `sys_menu`
|
||||
SET `perms` = 'system:info:list'
|
||||
WHERE `menu_id` = 2006681261898813441 AND `perms` = 'knowledge:info:list';
|
||||
|
||||
-- ============================================================
|
||||
-- 2. 新增 会话管理 菜单组(system:session:*)
|
||||
-- 父菜单=对话管理(2000209300188356609)。
|
||||
-- 管理端暂无 chat/session 页面,故 C 菜单设为隐藏(visible=1),
|
||||
-- 权限标识仍可在角色管理中授权给用户端使用。
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO `sys_menu` VALUES
|
||||
(2099010100000000001, '会话管理', 2000209300188356609, 6, 'session', NULL, NULL, 1, 0, 'C', '1', '0', 'system:session:list', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, '会话管理菜单'),
|
||||
(2099010100000000002, '会话管理查询', 2099010100000000001, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:session:query', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''),
|
||||
(2099010100000000003, '会话管理新增', 2099010100000000001, 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:session:add', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''),
|
||||
(2099010100000000004, '会话管理修改', 2099010100000000001, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:session:edit', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''),
|
||||
(2099010100000000005, '会话管理删除', 2099010100000000001, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:session:remove', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''),
|
||||
(2099010100000000006, '会话管理导出', 2099010100000000001, 5, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:session:export', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, '');
|
||||
|
||||
-- ============================================================
|
||||
-- 3. 新增 知识附件 菜单组(system:attach:*)
|
||||
-- 父菜单=对话管理(2000209300188356609),组件对应管理端 knowledge/attach/index。
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO `sys_menu` VALUES
|
||||
(2099010100000000010, '知识附件', 2000209300188356609, 7, 'attach', 'knowledge/attach/index', NULL, 1, 0, 'C', '0', '0', 'system:attach:list', 'ant-design:paper-clip-outlined', 103, 1, '2026-07-24 00:00:00', NULL, NULL, '知识附件菜单'),
|
||||
(2099010100000000011, '知识附件查询', 2099010100000000010, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:attach:query', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''),
|
||||
(2099010100000000012, '知识附件新增', 2099010100000000010, 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:attach:add', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''),
|
||||
(2099010100000000013, '知识附件修改', 2099010100000000010, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:attach:edit', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''),
|
||||
(2099010100000000014, '知识附件删除', 2099010100000000010, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:attach:remove', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''),
|
||||
(2099010100000000015, '知识附件导出', 2099010100000000010, 5, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:attach:export', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, '');
|
||||
|
||||
-- ============================================================
|
||||
-- 4. 新增 知识片段 菜单组(system:fragment:*)
|
||||
-- 父菜单=对话管理(2000209300188356609),组件对应管理端 knowledge/fragment/index。
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO `sys_menu` VALUES
|
||||
(2099010100000000020, '知识片段', 2000209300188356609, 8, 'fragment', 'knowledge/fragment/index', NULL, 1, 0, 'C', '0', '0', 'system:fragment:list', 'ant-design:file-text-outlined', 103, 1, '2026-07-24 00:00:00', NULL, NULL, '知识片段菜单'),
|
||||
(2099010100000000021, '知识片段查询', 2099010100000000020, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:fragment:query', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''),
|
||||
(2099010100000000022, '知识片段新增', 2099010100000000020, 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:fragment:add', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''),
|
||||
(2099010100000000023, '知识片段修改', 2099010100000000020, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:fragment:edit', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''),
|
||||
(2099010100000000024, '知识片段删除', 2099010100000000020, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:fragment:remove', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''),
|
||||
(2099010100000000025, '知识片段导出', 2099010100000000020, 5, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:fragment:export', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, '');
|
||||
|
||||
-- ============================================================
|
||||
-- 5. 新建 普通用户 角色(租户 000000,data_scope=5 仅本人)
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO `sys_role` VALUES
|
||||
(2099010100000000030, '000000', '普通用户', 'user', 2, '5', 1, 1, '0', '0', 103, 1, '2026-07-24 00:00:00', NULL, NULL, '注册用户默认角色');
|
||||
|
||||
-- ============================================================
|
||||
-- 6. 角色-菜单关联:普通用户 拥有用户端 AI 对话所需权限
|
||||
-- 会话管理(001-006) + 聊天消息(2000210914680823809 及 810-814)
|
||||
-- + 知识管理(2006681261898813441 及 442-446)
|
||||
-- + 知识附件(010-015) + 知识片段(020-025)
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES
|
||||
-- 会话管理
|
||||
(2099010100000000030, 2099010100000000001),
|
||||
(2099010100000000030, 2099010100000000002),
|
||||
(2099010100000000030, 2099010100000000003),
|
||||
(2099010100000000030, 2099010100000000004),
|
||||
(2099010100000000030, 2099010100000000005),
|
||||
(2099010100000000030, 2099010100000000006),
|
||||
-- 聊天消息
|
||||
(2099010100000000030, 2000210914680823809),
|
||||
(2099010100000000030, 2000210914680823810),
|
||||
(2099010100000000030, 2000210914680823811),
|
||||
(2099010100000000030, 2000210914680823812),
|
||||
(2099010100000000030, 2000210914680823813),
|
||||
(2099010100000000030, 2000210914680823814),
|
||||
-- 知识管理
|
||||
(2099010100000000030, 2006681261898813441),
|
||||
(2099010100000000030, 2006681261898813442),
|
||||
(2099010100000000030, 2006681261898813443),
|
||||
(2099010100000000030, 2006681261898813444),
|
||||
(2099010100000000030, 2006681261898813445),
|
||||
(2099010100000000030, 2006681261898813446),
|
||||
-- 知识附件
|
||||
(2099010100000000030, 2099010100000000010),
|
||||
(2099010100000000030, 2099010100000000011),
|
||||
(2099010100000000030, 2099010100000000012),
|
||||
(2099010100000000030, 2099010100000000013),
|
||||
(2099010100000000030, 2099010100000000014),
|
||||
(2099010100000000030, 2099010100000000015),
|
||||
-- 知识片段
|
||||
(2099010100000000030, 2099010100000000020),
|
||||
(2099010100000000030, 2099010100000000021),
|
||||
(2099010100000000030, 2099010100000000022),
|
||||
(2099010100000000030, 2099010100000000023),
|
||||
(2099010100000000030, 2099010100000000024),
|
||||
(2099010100000000030, 2099010100000000025);
|
||||
|
||||
-- ============================================================
|
||||
-- 7. 注册默认角色配置项 sys.register.defaultRoleId
|
||||
-- 后端 SysRegisterService 注册成功后读取此配置为新用户绑定角色;
|
||||
-- 值为空字符串时不绑定(保留旧行为)。
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO `sys_config` VALUES
|
||||
(2099010100000000031, '000000', '注册-默认角色ID', 'sys.register.defaultRoleId', '2099010100000000030', 'Y', 103, 1, '2026-07-24 00:00:00', NULL, NULL, '新注册用户绑定的默认角色ID,留空则不绑定');
|
||||
|
||||
-- ============================================================
|
||||
-- 8. 前置条件(运营项,默认不执行):开启用户注册
|
||||
-- sys.account.registerUser 默认 false,后端 AuthController.register
|
||||
-- 会据此返回「当前系统没有开启注册功能」。若要开放注册,取消注释执行。
|
||||
-- ============================================================
|
||||
-- UPDATE `sys_config` SET `config_value` = 'true'
|
||||
-- WHERE `config_key` = 'sys.account.registerUser' AND `tenant_id` = '000000';
|
||||
@@ -0,0 +1,39 @@
|
||||
-- 一次性补全工作流节点消息模板配置 (NodeMessageTemplateEnum 全部 8 个键)
|
||||
-- 背景:节点执行时 WorkflowMessageUtil.getNodeMessageTemplate 从 sys_config 读取展示模板,
|
||||
-- 历史库中这批配置缺失, 导致运行工作流抛「请先配置该节点的响应模板」。
|
||||
-- 其中前 7 个见 2026-07-21-sys-config-node-template.sql,
|
||||
-- Google Search 为此前从未入库的节点模板。
|
||||
-- 说明:代码已增加内置默认模板兜底, 本脚本为可选, 执行后模板可在 系统管理-配置管理 中自定义。
|
||||
-- 幂等:按 config_key + tenant_id 判重, 可重复执行。
|
||||
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||
SELECT 2027192921483309058, '000000', 'HTTP请求节点响应模板', 'node.httpRequest.template', '✅ HTTP请求节点:结束响应 - ', 'Y', 103, 1, '2026-02-27 09:23:51', 1, '2026-02-27 09:31:41', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.httpRequest.template' AND `tenant_id` = '000000');
|
||||
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||
SELECT 2027193296990957569, '000000', '文生图节点响应模板', 'node.image.template', '🎨 文生图节点:结束响应 - 图片URL: ', 'Y', 103, 1, '2026-02-27 09:25:20', 1, '2026-02-27 09:31:52', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.image.template' AND `tenant_id` = '000000');
|
||||
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||
SELECT 2027193820393959425, '000000', '发送邮箱节点响应模板', 'node.mailsend.template', '📧 发送邮箱节点:结束响应 - ', 'Y', 103, 1, '2026-02-27 09:27:25', 1, '2026-02-27 09:32:05', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.mailsend.template' AND `tenant_id` = '000000');
|
||||
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||
SELECT 2027194134438277122, '000000', '结束节点响应模板', 'node.end.template', '🔚 流程已执行完毕,如果您有其他需求,请随时重新发起请求。', 'Y', 103, 1, '2026-02-27 09:28:40', 1, '2026-02-27 09:32:53', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.end.template' AND `tenant_id` = '000000');
|
||||
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||
SELECT 2027208880369647617, '000000', '条件分支节点响应模板', 'node.switch.template', '🔀 条件分支节点:触发 -> 跳转到节点 ', 'Y', 103, 1, '2026-02-27 10:27:15', 1, '2026-02-27 10:35:54', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.switch.template' AND `tenant_id` = '000000');
|
||||
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||
SELECT 2027213914603995137, '000000', '大模型回答节点响应模板', 'node.llmAnswer.template', '🤖 LLM 节点 生成回答:', 'Y', 103, 1, '2026-02-27 10:47:16', 1, '2026-02-27 10:52:40', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.llmAnswer.template' AND `tenant_id` = '000000');
|
||||
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||
SELECT 2027217577397391361, '000000', '工作流异常响应模板', 'node.exception.template', '🛑 工作流发生异常:', 'N', 103, 1, '2026-02-27 11:01:49', 1, '2026-02-27 11:02:01', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.exception.template' AND `tenant_id` = '000000');
|
||||
|
||||
INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||
SELECT 2084157200000000003, '000000', '网络搜索节点响应模板', 'node.googleSearch.template', '🔍 网络搜索节点处理完成:', 'Y', 103, 1, '2026-07-29 19:40:00', 1, '2026-07-29 19:40:00', NULL
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'node.googleSearch.template' AND `tenant_id` = '000000');
|
||||
66
docs/script/sql/update/2026-07-29-zhipu-web-search-node.sql
Normal file
66
docs/script/sql/update/2026-07-29-zhipu-web-search-node.sql
Normal file
@@ -0,0 +1,66 @@
|
||||
-- 智谱 Web Search 工作流扩展节点
|
||||
-- 内部组件名继续使用 Google,以兼容现有前端组件和已保存流程。
|
||||
|
||||
UPDATE `t_workflow_component`
|
||||
SET `title` = '网络搜索',
|
||||
`remark` = '调用智谱 Web Search 检索互联网信息',
|
||||
`display_order` = 40,
|
||||
`is_enable` = 1,
|
||||
`is_deleted` = 0,
|
||||
`update_time` = NOW()
|
||||
WHERE `name` = 'Google'
|
||||
AND `tenant_id` = '000000';
|
||||
|
||||
INSERT INTO `t_workflow_component`
|
||||
(`uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`,
|
||||
`create_time`, `update_time`, `is_deleted`, `tenant_id`)
|
||||
SELECT
|
||||
'a7f8c2d44e5b4c83a9d6f103c2b47e18',
|
||||
'Google',
|
||||
'网络搜索',
|
||||
'调用智谱 Web Search 检索互联网信息',
|
||||
40,
|
||||
1,
|
||||
NOW(),
|
||||
NOW(),
|
||||
0,
|
||||
'000000'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM `t_workflow_component`
|
||||
WHERE `name` = 'Google'
|
||||
AND `tenant_id` = '000000'
|
||||
);
|
||||
|
||||
UPDATE `sys_config`
|
||||
SET `config_name` = '网络搜索节点响应模板',
|
||||
`config_value` = '🔍 网络搜索节点处理完成:',
|
||||
`update_time` = NOW()
|
||||
WHERE `config_key` = 'node.googleSearch.template'
|
||||
AND `tenant_id` = '000000';
|
||||
|
||||
INSERT INTO `sys_config`
|
||||
(`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`,
|
||||
`config_type`, `create_dept`, `create_by`, `create_time`, `update_by`,
|
||||
`update_time`, `remark`)
|
||||
SELECT
|
||||
2084157200000000003,
|
||||
'000000',
|
||||
'网络搜索节点响应模板',
|
||||
'node.googleSearch.template',
|
||||
'🔍 网络搜索节点处理完成:',
|
||||
'Y',
|
||||
103,
|
||||
1,
|
||||
NOW(),
|
||||
1,
|
||||
NOW(),
|
||||
'智谱 Web Search 工作流扩展节点'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM `sys_config`
|
||||
WHERE `config_key` = 'node.googleSearch.template'
|
||||
AND `tenant_id` = '000000'
|
||||
);
|
||||
128
docs/script/sql/update/update-0615-trace.sql
Normal file
128
docs/script/sql/update/update-0615-trace.sql
Normal file
@@ -0,0 +1,128 @@
|
||||
-- 链路追踪运行记录表
|
||||
CREATE TABLE IF NOT EXISTS `trace_run` (
|
||||
`id` bigint NOT NULL COMMENT '主键',
|
||||
`trace_id` varchar(64) NOT NULL COMMENT '链路ID',
|
||||
`trace_name` varchar(128) NOT NULL COMMENT '链路名称',
|
||||
`business_type` varchar(64) NOT NULL COMMENT '业务类型',
|
||||
`business_id` varchar(128) DEFAULT NULL COMMENT '业务ID',
|
||||
`user_id` bigint DEFAULT NULL COMMENT '用户ID',
|
||||
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
|
||||
`status` varchar(32) NOT NULL COMMENT '状态',
|
||||
`start_time` datetime NOT NULL COMMENT '开始时间',
|
||||
`end_time` datetime DEFAULT NULL COMMENT '结束时间',
|
||||
`duration_ms` bigint DEFAULT NULL COMMENT '耗时毫秒',
|
||||
`error_message` varchar(1000) DEFAULT NULL COMMENT '错误摘要',
|
||||
`metadata` text DEFAULT NULL COMMENT '元数据JSON',
|
||||
`create_dept` bigint DEFAULT NULL COMMENT '创建部门',
|
||||
`create_by` bigint DEFAULT NULL COMMENT '创建者',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_by` bigint DEFAULT NULL COMMENT '更新者',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE KEY `uk_trace_run_trace_id` (`trace_id`) USING BTREE,
|
||||
KEY `idx_trace_run_business` (`business_type`, `business_id`) USING BTREE,
|
||||
KEY `idx_trace_run_status_time` (`status`, `start_time`) USING BTREE,
|
||||
KEY `idx_trace_run_tenant_time` (`tenant_id`, `start_time`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='链路追踪运行记录表' ROW_FORMAT=DYNAMIC;
|
||||
|
||||
-- 链路追踪节点记录表
|
||||
CREATE TABLE IF NOT EXISTS `trace_node` (
|
||||
`id` bigint NOT NULL COMMENT '主键',
|
||||
`trace_id` varchar(64) NOT NULL COMMENT '链路ID',
|
||||
`node_id` varchar(64) NOT NULL COMMENT '节点ID',
|
||||
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
|
||||
`parent_node_id` varchar(64) DEFAULT NULL COMMENT '父节点ID',
|
||||
`node_name` varchar(128) NOT NULL COMMENT '节点名称',
|
||||
`node_type` varchar(64) NOT NULL COMMENT '节点类型',
|
||||
`depth` int DEFAULT 0 COMMENT '节点深度',
|
||||
`sort_order` int DEFAULT 0 COMMENT '排序',
|
||||
`class_name` varchar(255) DEFAULT NULL COMMENT '类名',
|
||||
`method_name` varchar(128) DEFAULT NULL COMMENT '方法名',
|
||||
`status` varchar(32) NOT NULL COMMENT '状态',
|
||||
`start_time` datetime NOT NULL COMMENT '开始时间',
|
||||
`end_time` datetime DEFAULT NULL COMMENT '结束时间',
|
||||
`duration_ms` bigint DEFAULT NULL COMMENT '耗时毫秒',
|
||||
`error_message` varchar(1000) DEFAULT NULL COMMENT '错误摘要',
|
||||
`input_payload` text DEFAULT NULL COMMENT '输入JSON',
|
||||
`output_payload` text DEFAULT NULL COMMENT '输出JSON',
|
||||
`metadata` text DEFAULT NULL COMMENT '元数据JSON',
|
||||
`create_dept` bigint DEFAULT NULL COMMENT '创建部门',
|
||||
`create_by` bigint DEFAULT NULL COMMENT '创建者',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_by` bigint DEFAULT NULL COMMENT '更新者',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
KEY `idx_trace_node_trace_id` (`trace_id`) USING BTREE,
|
||||
KEY `idx_trace_node_parent` (`trace_id`, `parent_node_id`) USING BTREE,
|
||||
KEY `idx_trace_node_time` (`trace_id`, `start_time`) USING BTREE,
|
||||
KEY `idx_trace_node_tenant_time` (`tenant_id`, `start_time`) USING BTREE,
|
||||
KEY `idx_trace_node_type_status` (`node_type`, `status`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='链路追踪节点记录表' ROW_FORMAT=DYNAMIC;
|
||||
|
||||
-- 兼容已存在的旧 trace 表:CREATE TABLE IF NOT EXISTS 不会给旧表补新字段
|
||||
SET @trace_run_add_tenant_sql = (
|
||||
SELECT IF(COUNT(*) = 0,
|
||||
'ALTER TABLE `trace_run` ADD COLUMN `tenant_id` varchar(20) DEFAULT ''000000'' COMMENT ''租户编号'' AFTER `user_id`',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'trace_run'
|
||||
AND COLUMN_NAME = 'tenant_id'
|
||||
);
|
||||
PREPARE trace_run_add_tenant_stmt FROM @trace_run_add_tenant_sql;
|
||||
EXECUTE trace_run_add_tenant_stmt;
|
||||
DEALLOCATE PREPARE trace_run_add_tenant_stmt;
|
||||
|
||||
SET @trace_run_add_tenant_idx_sql = (
|
||||
SELECT IF(COUNT(*) = 0,
|
||||
'ALTER TABLE `trace_run` ADD INDEX `idx_trace_run_tenant_time` (`tenant_id`, `start_time`) USING BTREE',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'trace_run'
|
||||
AND INDEX_NAME = 'idx_trace_run_tenant_time'
|
||||
);
|
||||
PREPARE trace_run_add_tenant_idx_stmt FROM @trace_run_add_tenant_idx_sql;
|
||||
EXECUTE trace_run_add_tenant_idx_stmt;
|
||||
DEALLOCATE PREPARE trace_run_add_tenant_idx_stmt;
|
||||
|
||||
SET @trace_node_add_tenant_sql = (
|
||||
SELECT IF(COUNT(*) = 0,
|
||||
'ALTER TABLE `trace_node` ADD COLUMN `tenant_id` varchar(20) DEFAULT ''000000'' COMMENT ''租户编号'' AFTER `node_id`',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'trace_node'
|
||||
AND COLUMN_NAME = 'tenant_id'
|
||||
);
|
||||
PREPARE trace_node_add_tenant_stmt FROM @trace_node_add_tenant_sql;
|
||||
EXECUTE trace_node_add_tenant_stmt;
|
||||
DEALLOCATE PREPARE trace_node_add_tenant_stmt;
|
||||
|
||||
SET @trace_node_add_tenant_idx_sql = (
|
||||
SELECT IF(COUNT(*) = 0,
|
||||
'ALTER TABLE `trace_node` ADD INDEX `idx_trace_node_tenant_time` (`tenant_id`, `start_time`) USING BTREE',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'trace_node'
|
||||
AND INDEX_NAME = 'idx_trace_node_tenant_time'
|
||||
);
|
||||
PREPARE trace_node_add_tenant_idx_stmt FROM @trace_node_add_tenant_idx_sql;
|
||||
EXECUTE trace_node_add_tenant_idx_stmt;
|
||||
DEALLOCATE PREPARE trace_node_add_tenant_idx_stmt;
|
||||
|
||||
-- 链路追踪监控菜单 & 按钮权限
|
||||
INSERT INTO `sys_menu`
|
||||
(`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query_param`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_dept`, `create_by`, `create_time`, `remark`)
|
||||
SELECT (SELECT COALESCE(MAX(`menu_id`), 0) + 1 FROM (SELECT `menu_id` FROM `sys_menu`) t), '链路追踪', 2, 7, 'trace', 'monitor/trace/index', '', 1, 0, 'C', '0', '0', 'monitor:trace:list', 'tabler:route', 103, 1, NOW(), '链路追踪监控菜单';
|
||||
|
||||
INSERT INTO `sys_menu`
|
||||
(`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query_param`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_dept`, `create_by`, `create_time`, `remark`)
|
||||
SELECT (SELECT COALESCE(MAX(`menu_id`), 0) + 1 FROM (SELECT `menu_id` FROM `sys_menu`) t), '链路追踪查询', (SELECT `menu_id` FROM `sys_menu` WHERE `perms` = 'monitor:trace:list' AND `menu_type` = 'C' LIMIT 1), 1, '#', '', '', 1, 0, 'F', '0', '0', 'monitor:trace:query', '#', 103, 1, NOW(), '';
|
||||
@@ -1,352 +0,0 @@
|
||||
<a id="top"></a>
|
||||
|
||||
# RAG 常见故障排查(16 问题清单)
|
||||
|
||||
当知识库已经接入,系统也能正常回答,但结果仍然出现命中错误、引用旧内容、推理漂移、跨轮次失忆,或部署后表面可用但实际异常时,最常见的问题不是“模型不行”,而是**不同层的故障被混在一起处理**。
|
||||
|
||||
这份页面不重新发明一套新方法。
|
||||
它直接使用一份固定的 **16 问题清单** 作为排查主轴,让你先把问题标到正确的 **No.X**,再决定下一步查哪里、改哪里,而不是一次性乱改检索、模型、切块、会话和部署配置。
|
||||
|
||||
这份清单的核心目的只有一个:
|
||||
|
||||
**先把问题放进正确的故障域,再做修复。**
|
||||
|
||||
快速导航:
|
||||
[这页怎么用](#how-to-use) | [标签说明](#legend) | [常见症状入口](#symptoms) | [16 问题清单](#map16) | [按层排查](#by-layer) |
|
||||
|
||||
---
|
||||
|
||||
<a id="how-to-use"></a>
|
||||
|
||||
## 一、这页怎么用
|
||||
|
||||
这不是一篇“从头到尾照着做”的传统教程。
|
||||
它更像一张固定的 RAG 故障地图,作用是先帮助你**判断故障属于哪一种类型**。
|
||||
|
||||
建议按下面顺序使用:
|
||||
|
||||
### 1. 先看现象,不要先改配置
|
||||
|
||||
先回答两个问题:
|
||||
|
||||
1. 你看到的故障,最像哪一种症状
|
||||
2. 这个故障更像发生在输入检索层、推理层、状态层,还是部署层
|
||||
|
||||
在还没判断层级之前,不要先一起改这些东西:
|
||||
|
||||
- 检索条数
|
||||
- 切块大小
|
||||
- 会话配置
|
||||
- 模型参数
|
||||
- 部署顺序
|
||||
- 依赖服务
|
||||
|
||||
如果先全部一起动,问题通常只会更难定位。
|
||||
|
||||
### 2. 先给问题打上 No.X 标签
|
||||
|
||||
这份页面最重要的动作,不是“立刻修好”,而是先做一件小事:
|
||||
|
||||
**给当前问题贴上最接近的 No.X。**
|
||||
|
||||
例如:
|
||||
|
||||
- 检索结果看起来相似,但其实答非所问,先看 `No.1` 或 `No.5`
|
||||
- 切块是对的,但结论还是错,先看 `No.2`
|
||||
- 系统回答很自信,但没有根据,先看 `No.4`
|
||||
- 刚部署完就炸,先看 `No.14` 到 `No.16`
|
||||
|
||||
### 3. 一次只排一个故障域
|
||||
|
||||
同一个表面现象,背后可能是不同层的问题。
|
||||
例如“答案不对”既可能是:
|
||||
|
||||
- `No.1` 检索漂移
|
||||
- `No.2` 理解塌陷
|
||||
- `No.4` 自信乱答
|
||||
- `No.8` 根本看不到错误路径
|
||||
|
||||
所以这张表的用法不是“多选全改”,而是:
|
||||
|
||||
**先挑最接近的一项,优先验证这一项是否成立。**
|
||||
|
||||
[返回顶部](#top) | [下一节:标签说明](#legend)
|
||||
|
||||
---
|
||||
|
||||
<a id="legend"></a>
|
||||
|
||||
## 二、标签说明
|
||||
|
||||
这份 16 问题清单本身已经带有层级 / 标签结构。
|
||||
这些标签不是装饰,而是用来帮助你快速判断故障发生在哪一层。
|
||||
|
||||
### 1. 层级标签
|
||||
|
||||
- `[IN]`:输入与检索
|
||||
输入、切块、召回、语义匹配、可见性问题
|
||||
|
||||
- `[RE]`:推理与规划
|
||||
理解、推理、归纳、逻辑链、抽象处理问题
|
||||
|
||||
- `[ST]`:状态与上下文
|
||||
会话、记忆、上下文连续性、多代理状态问题
|
||||
|
||||
- `[OP]`:基础设施与部署
|
||||
启动顺序、依赖就绪、部署锁死、预部署状态问题
|
||||
|
||||
### 2. `{OBS}` 标签
|
||||
|
||||
带 `{OBS}` 的项,通常都和“**你是否看得见问题是怎么坏掉的**”有关。
|
||||
它们往往不是单纯回答错误,而是:
|
||||
|
||||
- 错误路径不可见
|
||||
- 漂移过程不可见
|
||||
- 状态熔化过程不可见
|
||||
- 多代理覆盖过程不可见
|
||||
|
||||
所以一旦你发现“我知道结果错,但我根本看不到它是怎么错的”,通常就已经很接近 `{OBS}` 类问题了。
|
||||
|
||||
### 3. 为什么要保留这些标签
|
||||
|
||||
因为同样叫“答错了”,实际含义完全不同。
|
||||
|
||||
例如:
|
||||
|
||||
- `[IN]` 的答错,常常是**拿错材料**
|
||||
- `[RE]` 的答错,常常是**拿对材料但理解错**
|
||||
- `[ST]` 的答错,常常是**前文断掉、状态漂移**
|
||||
- `[OP]` 的答错,常常是**系统根本没在完整状态下运行**
|
||||
|
||||
如果不先分层,就会掉进典型的 RAG 地狱:
|
||||
表面在改答案,实际上在盲修。
|
||||
|
||||
[返回顶部](#top) | [下一节:常见症状入口](#symptoms)
|
||||
|
||||
---
|
||||
|
||||
<a id="symptoms"></a>
|
||||
|
||||
## 三、常见症状入口
|
||||
|
||||
如果你现在还不知道该从哪一项开始,就先从症状入口反查。
|
||||
|
||||
### 1. 检索返回了错误内容,或看起来相关但其实不回答问题
|
||||
|
||||
这类问题最常见的是:
|
||||
“有命中,但命中的不是该用的内容。”
|
||||
|
||||
优先看:
|
||||
|
||||
- [No.1](#no1) `幻觉与切块漂移`
|
||||
- [No.5](#no5) `语义 ≠ 向量嵌入`
|
||||
- [No.8](#no8) `调试是一个黑箱`
|
||||
|
||||
### 2. 切块本身是对的,但最终答案还是错的
|
||||
|
||||
这类问题不是简单没检索到,而是后面那层坏了。
|
||||
|
||||
优先看:
|
||||
|
||||
- [No.2](#no2) `解释塌陷`
|
||||
- [No.4](#no4) `虚张声势 / 过度自信`
|
||||
- [No.6](#no6) `逻辑塌陷与恢复`
|
||||
|
||||
### 3. 多步任务一开始正常,后面越来越偏
|
||||
|
||||
这类问题通常不是单点错误,而是中途漂移或熔化。
|
||||
|
||||
优先看:
|
||||
|
||||
- [No.3](#no3) `长推理链`
|
||||
- [No.6](#no6) `逻辑塌陷与恢复`
|
||||
- [No.9](#no9) `熵塌陷`
|
||||
|
||||
### 4. 多轮对话后开始失忆,跨轮次接不上
|
||||
|
||||
这类问题一般已经进入状态层。
|
||||
|
||||
优先看:
|
||||
|
||||
- [No.7](#no7) `跨会话记忆断裂`
|
||||
- [No.9](#no9) `熵塌陷`
|
||||
- [No.13](#no13) `多代理混乱`
|
||||
|
||||
### 5. 遇到抽象、逻辑、规则、符号关系就崩
|
||||
|
||||
这类问题通常不是检索空,而是推理结构扛不住。
|
||||
|
||||
优先看:
|
||||
|
||||
- [No.11](#no11) `符号塌陷`
|
||||
- [No.12](#no12) `哲学递归`
|
||||
|
||||
### 6. 你根本不知道错在哪一层,只知道结果不对
|
||||
|
||||
这类问题先不要乱调参数。
|
||||
先解决“不可见”的问题。
|
||||
|
||||
优先看:
|
||||
|
||||
- [No.8](#no8) `调试是一个黑箱`
|
||||
|
||||
### 7. 刚部署完最容易炸,首轮调用异常,重启后偶尔恢复
|
||||
|
||||
这类问题通常不在答案逻辑,而在部署状态。
|
||||
|
||||
优先看:
|
||||
|
||||
- [No.14](#no14) `引导启动顺序`
|
||||
- [No.15](#no15) `部署死锁`
|
||||
- [No.16](#no16) `预部署塌陷`
|
||||
|
||||
[返回顶部](#top) | [下一节:16 问题清单](#map16)
|
||||
|
||||
---
|
||||
|
||||
<a id="map16"></a>
|
||||
|
||||
## 四、16 问题清单(固定主表)
|
||||
|
||||
下面这 16 项按固定顺序使用。
|
||||
不要先重组,不要先混类,先判断最接近哪一个 **No.X**。
|
||||
|
||||
| # | 问题域(含层级/标签) | 会坏在哪里 |
|
||||
|---|---|---|
|
||||
| <a id="no1"></a> 1 | `[IN] 幻觉与切块漂移 {OBS}` | 检索返回错误/无关内容 |
|
||||
| <a id="no2"></a> 2 | `[RE] 解释塌陷` | 切块是对的,逻辑是错的 |
|
||||
| <a id="no3"></a> 3 | `[RE] 长推理链 {OBS}` | 在多步任务中逐步漂移 |
|
||||
| <a id="no4"></a> 4 | `[RE] 虚张声势 / 过度自信` | 自信但没有根据的回答 |
|
||||
| <a id="no5"></a> 5 | `[IN] 语义 ≠ 向量嵌入 {OBS}` | 余弦匹配 ≠ 真实语义 |
|
||||
| <a id="no6"></a> 6 | `[RE] 逻辑塌陷与恢复 {OBS}` | 走入死胡同,需要受控重置 |
|
||||
| <a id="no7"></a> 7 | `[ST] 跨会话记忆断裂` | 线索丢失,没有连续性 |
|
||||
| <a id="no8"></a> 8 | `[IN] 调试是一个黑箱 {OBS}` | 看不到故障路径 |
|
||||
| <a id="no9"></a> 9 | `[ST] 熵塌陷` | 注意力熔化,输出失去连贯性 |
|
||||
| <a id="no10"></a> 10 | `[RE] 创造力冻结` | 平直、字面化输出 |
|
||||
| <a id="no11"></a> 11 | `[RE] 符号塌陷` | 抽象/逻辑性提示词失效 |
|
||||
| <a id="no12"></a> 12 | `[RE] 哲学递归` | 自我引用循环、悖论陷阱 |
|
||||
| <a id="no13"></a> 13 | `[ST] 多代理混乱 {OBS}` | 代理互相覆盖或使逻辑错位 |
|
||||
| <a id="no14"></a> 14 | `[OP] 引导启动顺序` | 依赖未就绪时服务先启动 |
|
||||
| <a id="no15"></a> 15 | `[OP] 部署死锁` | 基础设施中的循环等待 |
|
||||
| <a id="no16"></a> 16 | `[OP] 预部署塌陷 {OBS}` | 首次调用时版本错配 / 缺少密钥 |
|
||||
|
||||
这张表是主表。
|
||||
如果你时间很少,只做一件事也行:
|
||||
|
||||
**先从这 16 项里选出最接近的一项。**
|
||||
|
||||
[返回顶部](#top) | [下一节:按层排查](#by-layer)
|
||||
|
||||
---
|
||||
|
||||
<a id="by-layer"></a>
|
||||
|
||||
## 五、按层排查:不要改错层
|
||||
|
||||
这一节不重写 16 项,只是告诉你:
|
||||
当你已经选到某个 No.X 时,第一眼应该优先查哪一层。
|
||||
|
||||
### A. `[IN]` 层:先确认你拿到的是不是对的材料
|
||||
|
||||
对应编号:
|
||||
|
||||
- [No.1](#no1)
|
||||
- [No.5](#no5)
|
||||
- [No.8](#no8)
|
||||
|
||||
这层最常见的误判是:
|
||||
|
||||
“我以为系统理解错了,其实它一开始就拿错了东西。”
|
||||
|
||||
如果你命中了弱相关片段、表面相似文本、错误切块,后面推理再强也没用。
|
||||
所以 `[IN]` 层优先看的是:
|
||||
|
||||
1. 原始召回内容到底是什么
|
||||
2. 命中的片段是否只是“相似”,而不是“正确”
|
||||
3. 你是否能看到检索过程,还是整个过程像黑箱
|
||||
|
||||
这层如果没先排好,后面的推理诊断通常会失真。
|
||||
|
||||
### B. `[RE]` 层:材料可能是对的,但系统用错了
|
||||
|
||||
对应编号:
|
||||
|
||||
- [No.2](#no2)
|
||||
- [No.3](#no3)
|
||||
- [No.4](#no4)
|
||||
- [No.6](#no6)
|
||||
- [No.10](#no10)
|
||||
- [No.11](#no11)
|
||||
- [No.12](#no12)
|
||||
|
||||
这层最常见的误判是:
|
||||
|
||||
“我以为是检索坏了,其实是后面理解、归纳、逻辑链坏了。”
|
||||
|
||||
例如:
|
||||
|
||||
- 切块是对的,但结论错了 → 常见是 `No.2`
|
||||
- 多步任务中途开始偏 → 常见是 `No.3`
|
||||
- 回答很笃定,但完全站不住 → 常见是 `No.4`
|
||||
- 遇到抽象规则就崩 → 常见是 `No.11`
|
||||
- 陷入循环解释 → 常见是 `No.12`
|
||||
|
||||
如果 `[IN]` 层已经基本没问题,答案还是不对,就应该优先回到 `[RE]` 层判断是哪一种塌陷。
|
||||
|
||||
### C. `[ST]` 层:单轮正常,不代表状态层正常
|
||||
|
||||
对应编号:
|
||||
|
||||
- [No.7](#no7)
|
||||
- [No.9](#no9)
|
||||
- [No.13](#no13)
|
||||
|
||||
这层最常见的误判是:
|
||||
|
||||
“单轮看起来还行,所以我以为系统没问题。”
|
||||
|
||||
其实很多 RAG 地狱不是单轮错误,而是:
|
||||
|
||||
- 多轮之后前文断掉
|
||||
- 上下文越来越乱
|
||||
- 多角色、多代理之间互相覆盖
|
||||
|
||||
如果你发现:
|
||||
|
||||
- 第一轮没事,后面越来越歪
|
||||
- 切换角色后前面的约束消失
|
||||
- 多个步骤之间状态彼此污染
|
||||
|
||||
那就不要再只盯着检索条数了,应该直接回到 `[ST]` 层看 `No.7 / No.9 / No.13`。
|
||||
|
||||
### D. `[OP]` 层:别把部署问题误诊成回答问题
|
||||
|
||||
对应编号:
|
||||
|
||||
- [No.14](#no14)
|
||||
- [No.15](#no15)
|
||||
- [No.16](#no16)
|
||||
|
||||
这层最常见的误判是:
|
||||
|
||||
“答案不稳定,所以我先去调模型或检索。”
|
||||
|
||||
但如果系统根本没有在完整状态下启动,所有上层表现都会像鬼打墙。
|
||||
尤其是这些情况:
|
||||
|
||||
- 依赖还没就绪,服务先起了 → `No.14`
|
||||
- 多个组件互相等待,长期半可用 → `No.15`
|
||||
- 首次调用就因为版本、密钥、环境没对齐而塌陷 → `No.16`
|
||||
|
||||
只要你看到“刚部署最容易出事”“首轮异常最严重”“重启后暂时恢复”,就要优先怀疑 `[OP]` 层,而不是先改提示词或参数。
|
||||
|
||||
[返回顶部](#top) |
|
||||
|
||||
---
|
||||
|
||||
<a id="issue-report"></a>
|
||||
|
||||
|
||||
## 六、快速返回
|
||||
|
||||
[返回顶部](#top) | [这页怎么用](#how-to-use) | [标签说明](#legend) | [常见症状入口](#symptoms) | [16 问题清单](#map16) | [按层排查](#by-layer)
|
||||
@@ -1,42 +0,0 @@
|
||||
## 接口信息
|
||||
|
||||
**接口路径**: `POST /resource/oss/upload`
|
||||
**请求类型**: `multipart/form-data`
|
||||
**权限要求**: `system:oss:upload`
|
||||
**业务类型**: [INSERT]
|
||||
|
||||
### 接口描述
|
||||
上传OSS对象存储接口,用于将文件上传到对象存储服务。
|
||||
|
||||
### 请求参数
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
| ---- | ------------- | ---- | ------ |
|
||||
| file | MultipartFile | 是 | 要上传的文件 |
|
||||
|
||||
### 请求头
|
||||
- `Content-Type`: `multipart/form-data`
|
||||
|
||||
### 返回值
|
||||
返回 `R<SysOssUploadVo>` 类型,包含以下字段:
|
||||
| 字段名 | 类型 | 说明 |
|
||||
| -------- | ------ | ------- |
|
||||
| url | String | 文件访问URL |
|
||||
| fileName | String | 原始文件名 |
|
||||
| ossId | String | 文件ID |
|
||||
|
||||
### 响应示例
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "操作成功",
|
||||
"data": {
|
||||
"url": "fileid://xxx",
|
||||
"fileName": "example.jpg",
|
||||
"ossId": "123"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 异常情况
|
||||
- 当上传文件为空时,返回错误信息:"上传文件不能为空"
|
||||
47
pom.xml
47
pom.xml
@@ -13,7 +13,7 @@
|
||||
<description>全栈式AI开发平台</description>
|
||||
|
||||
<properties>
|
||||
<revision>3.0.0</revision>
|
||||
<revision>3.1.0</revision>
|
||||
<spring-boot.version>3.5.8</spring-boot.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
@@ -43,8 +43,6 @@
|
||||
<aws.sdk.version>2.28.22</aws.sdk.version>
|
||||
<!-- SMS 配置 -->
|
||||
<sms4j.version>3.3.5</sms4j.version>
|
||||
<!-- 限制框架中的fastjson版本 -->
|
||||
<fastjson.version>1.2.83</fastjson.version>
|
||||
<!-- 面向运行时的D-ORM依赖 -->
|
||||
<anyline.version>8.7.2-20250603</anyline.version>
|
||||
<!-- 工作流配置 -->
|
||||
@@ -54,18 +52,25 @@
|
||||
<!-- Jackson XML -->
|
||||
<jackson-dataformat-xml.version>2.18.2</jackson-dataformat-xml.version>
|
||||
<!-- AI 相关依赖 -->
|
||||
<langchain4j.version>1.11.0</langchain4j.version>
|
||||
<langchain4j.community.version>1.11.0-beta19</langchain4j.community.version>
|
||||
<langgraph4j.version>1.5.3</langgraph4j.version>
|
||||
|
||||
<langchain4j.version>1.17.2</langchain4j.version>
|
||||
<langchain4j.beta.version>1.17.2-beta27</langchain4j.beta.version>
|
||||
<langchain4j.community.version>1.17.0-beta27</langchain4j.community.version>
|
||||
<langgraph4j.version>1.8.20</langgraph4j.version>
|
||||
|
||||
<weaviate.version>1.19.6</weaviate.version>
|
||||
<dify.version>1.0.7</dify.version>
|
||||
<dify.version>1.2.7</dify.version>
|
||||
<coze.version>0.4.2</coze.version>
|
||||
<!-- 智谱官方 Java SDK -->
|
||||
<zai-sdk.version>0.3.5</zai-sdk.version>
|
||||
|
||||
<!-- gRPC 版本 - 解决 Milvus SDK 依赖冲突 -->
|
||||
<grpc.version>1.62.2</grpc.version>
|
||||
<!-- Apache Commons Compress - 用于POI处理ZIP格式 -->
|
||||
<commons-compress.version>1.27.1</commons-compress.version>
|
||||
|
||||
|
||||
<avatar-generator.version>1.1.0</avatar-generator.version>
|
||||
<javassist.version>3.30.2-GA</javassist.version>
|
||||
<jsoup.version>1.21.2</jsoup.version>
|
||||
<knife4j.version>4.4.0</knife4j.version>
|
||||
<swagger-annotations.version>2.2.8</swagger-annotations.version>
|
||||
@@ -79,7 +84,7 @@
|
||||
<maven-surefire-plugin.version>3.5.3</maven-surefire-plugin.version>
|
||||
<flatten-maven-plugin.version>1.3.0</flatten-maven-plugin.version>
|
||||
<!-- 打包默认跳过测试 -->
|
||||
<skipTests>true</skipTests>
|
||||
<skipTests>false</skipTests>
|
||||
</properties>
|
||||
|
||||
<profiles>
|
||||
@@ -342,6 +347,12 @@
|
||||
<groupId>me.zhyd.oauth</groupId>
|
||||
<artifactId>JustAuth</artifactId>
|
||||
<version>${justauth.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- 离线IP地址定位库 ip2region -->
|
||||
@@ -351,12 +362,6 @@
|
||||
<version>${ip2region.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
<version>${fastjson.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-system</artifactId>
|
||||
@@ -499,20 +504,8 @@
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<!-- 关闭过滤 -->
|
||||
<filtering>false</filtering>
|
||||
</resource>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<!-- 引入所有 匹配文件进行过滤 -->
|
||||
<includes>
|
||||
<include>application*</include>
|
||||
<include>bootstrap*</include>
|
||||
<include>banner*</include>
|
||||
</includes>
|
||||
<!-- 启用过滤 即该资源中的变量将会被过滤器中的值替换 -->
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
</build>
|
||||
|
||||
|
||||
@@ -1,31 +1,59 @@
|
||||
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
|
||||
# FFmpeg-enabled variant of ruoyi-admin/Dockerfile.
|
||||
FROM bellsoft/liberica-openjdk-rocky:17.0.16-cds
|
||||
#FROM bellsoft/liberica-openjdk-rocky:21.0.8-cds
|
||||
#FROM findepi/graalvm:java17-native
|
||||
|
||||
LABEL maintainer="Lion Li"
|
||||
|
||||
# Rocky base repositories do not provide the full codec build. RPM Fusion Free
|
||||
# supplies ffmpeg with libx264, while the checks below prevent a reduced build
|
||||
# from reaching production unnoticed.
|
||||
RUN set -eux; \
|
||||
dnf -y install dnf-plugins-core epel-release; \
|
||||
. /etc/os-release; \
|
||||
rocky_major="${VERSION_ID%%.*}"; \
|
||||
if [ "${rocky_major}" -ge 9 ]; then \
|
||||
dnf config-manager --set-enabled crb; \
|
||||
else \
|
||||
dnf config-manager --set-enabled powertools; \
|
||||
fi; \
|
||||
dnf -y install "https://mirrors.rpmfusion.org/free/el/rpmfusion-free-release-${rocky_major}.noarch.rpm"; \
|
||||
dnf -y install ffmpeg; \
|
||||
for filter in scale pad fps setsar format tpad trim settb setpts xfade \
|
||||
aresample aformat apad atrim asetpts anullsrc concat acrossfade; do \
|
||||
ffmpeg -hide_banner -filters 2>/dev/null | grep -Eq "[[:space:]]${filter}[[:space:]]"; \
|
||||
done; \
|
||||
ffmpeg -hide_banner -h filter=xfade 2>&1 | grep -q dissolve; \
|
||||
ffmpeg -hide_banner -h filter=xfade 2>&1 | grep -q fadeblack; \
|
||||
ffmpeg -hide_banner -h filter=xfade 2>&1 | grep -q slideleft; \
|
||||
ffmpeg -hide_banner -encoders 2>/dev/null | grep -Eq '[[:space:]]libx264[[:space:]]'; \
|
||||
ffmpeg -hide_banner -encoders 2>/dev/null | grep -Eq '[[:space:]]aac[[:space:]]'; \
|
||||
ffprobe -hide_banner -version >/dev/null; \
|
||||
dnf clean all; \
|
||||
rm -rf /var/cache/dnf
|
||||
|
||||
RUN mkdir -p /ruoyi/server/logs \
|
||||
/ruoyi/server/temp \
|
||||
/ruoyi/skywalking/agent
|
||||
|
||||
WORKDIR /ruoyi/server
|
||||
|
||||
ENV SERVER_PORT=8080 SNAIL_PORT=28080 LANG=C.UTF-8 LC_ALL=C.UTF-8 JAVA_OPTS=""
|
||||
ENV SERVER_PORT=8080 \
|
||||
SNAIL_PORT=28080 \
|
||||
LANG=C.UTF-8 \
|
||||
LC_ALL=C.UTF-8 \
|
||||
JAVA_OPTS="" \
|
||||
FFMPEG_PATH=/usr/bin/ffmpeg \
|
||||
FFPROBE_PATH=/usr/bin/ffprobe
|
||||
|
||||
EXPOSE ${SERVER_PORT}
|
||||
# 暴露 snail job 客户端端口 用于定时任务调度中心通信
|
||||
EXPOSE ${SNAIL_PORT}
|
||||
|
||||
ADD ./target/ruoyi-admin.jar ./app.jar
|
||||
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -Dserver.port=${SERVER_PORT} \
|
||||
ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom \
|
||||
-Djava.io.tmpdir=/ruoyi/server/temp \
|
||||
-Dserver.port=${SERVER_PORT} \
|
||||
-Dsnail-job.port=${SNAIL_PORT} \
|
||||
# 应用名称 如果想区分集群节点监控 改成不同的名称即可
|
||||
#-Dskywalking.agent.service_name=ruoyi-server \
|
||||
#-javaagent:/ruoyi/skywalking/agent/skywalking-agent.jar \
|
||||
-XX:+HeapDumpOnOutOfMemoryError -XX:+UseZGC ${JAVA_OPTS} \
|
||||
-jar app.jar
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ 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;
|
||||
|
||||
/**
|
||||
* 启动程序
|
||||
*
|
||||
@@ -13,10 +16,66 @@ import org.springframework.boot.context.metrics.buffering.BufferingApplicationSt
|
||||
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启动成功 ლ(´ڡ`ლ)゙");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ spring.boot.admin.client:
|
||||
metadata:
|
||||
username: ${spring.boot.admin.client.username}
|
||||
userpassword: ${spring.boot.admin.client.password}
|
||||
username: @monitor.username@
|
||||
password: @monitor.password@
|
||||
username: ${MONITOR_USERNAME:ruoyi}
|
||||
password: ${MONITOR_PASSWORD:123456}
|
||||
|
||||
--- # mcp配置信息
|
||||
mcp:
|
||||
@@ -27,12 +27,12 @@ snail-job:
|
||||
enabled: false
|
||||
# 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务
|
||||
group: "ruoyi_group"
|
||||
# SnailJob 接入验证令牌 详见 script/sql/ry_job.sql `sj_group_config` 表
|
||||
# SnailJob 接入验证令牌 详见 docs/script/sql/ruoyi-ai-v3_mysql8.sql `sj_group_config` 表
|
||||
token: "SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT"
|
||||
server:
|
||||
host: 127.0.0.1
|
||||
port: 17888
|
||||
# 命名空间UUID 详见 script/sql/ry_job.sql `sj_namespace`表`unique_id`字段
|
||||
# 命名空间UUID 详见 docs/script/sql/ruoyi-ai-v3_mysql8.sql `sj_namespace`表`unique_id`字段
|
||||
namespace: ${spring.profiles.active}
|
||||
# 随主应用端口漂移
|
||||
port: 2${server.port}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
--- # 监控中心配置
|
||||
spring.boot.admin.client:
|
||||
# 增加客户端开关
|
||||
@@ -8,8 +9,8 @@ spring.boot.admin.client:
|
||||
metadata:
|
||||
username: ${spring.boot.admin.client.username}
|
||||
userpassword: ${spring.boot.admin.client.password}
|
||||
username: @monitor.username@
|
||||
password: @monitor.password@
|
||||
username: ${MONITOR_USERNAME:ruoyi}
|
||||
password: ${MONITOR_PASSWORD:123456}
|
||||
|
||||
--- # mcp配置信息
|
||||
mcp:
|
||||
@@ -27,12 +28,12 @@ snail-job:
|
||||
enabled: false
|
||||
# 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务
|
||||
group: "ruoyi_group"
|
||||
# SnailJob 接入验证令牌 详见 script/sql/ry_job.sql `sj_group_config` 表
|
||||
# SnailJob 接入验证令牌 详见 docs/script/sql/ruoyi-ai-v3_mysql8.sql `sj_group_config` 表
|
||||
token: "SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT"
|
||||
server:
|
||||
host: 127.0.0.1
|
||||
port: 17888
|
||||
# 命名空间UUID 详见 script/sql/ry_job.sql `sj_namespace`表`unique_id`字段
|
||||
# 命名空间UUID 详见 docs/script/sql/ruoyi-ai-v3_mysql8.sql `sj_namespace`表`unique_id`字段
|
||||
namespace: ${spring.profiles.active}
|
||||
# 随主应用端口漂移
|
||||
port: 2${server.port}
|
||||
@@ -58,7 +59,7 @@ spring:
|
||||
driverClassName: com.mysql.cj.jdbc.Driver
|
||||
# jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
|
||||
# rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
|
||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-ai-agent?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
username: root
|
||||
password: root
|
||||
# agent:
|
||||
|
||||
@@ -20,6 +20,10 @@ server:
|
||||
# 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载
|
||||
worker: 256
|
||||
|
||||
--- # 小程序对话 WebSocket 兜底默认模型(前端未传 model 且无智能体绑定时使用)
|
||||
chat:
|
||||
default-model: deepseek-v4-flash
|
||||
|
||||
captcha:
|
||||
# 是否启用验证码校验
|
||||
enable: false
|
||||
@@ -35,7 +39,7 @@ captcha:
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
org.ruoyi: @logging.level@
|
||||
org.ruoyi: ${LOGGING_LEVEL_ORG_RUOYI:info}
|
||||
org.springframework: warn
|
||||
org.mybatis.spring.mapper: error
|
||||
org.apache.fury: warn
|
||||
@@ -75,7 +79,7 @@ spring:
|
||||
# 国际化资源文件路径
|
||||
basename: i18n/messages
|
||||
profiles:
|
||||
active: @profiles.active@
|
||||
active: ${SPRING_PROFILES_ACTIVE:dev}
|
||||
# 文件上传
|
||||
servlet:
|
||||
multipart:
|
||||
@@ -125,6 +129,7 @@ security:
|
||||
- /*/api-docs/**
|
||||
- /warm-flow-ui/config
|
||||
- /workflow/run
|
||||
- /coding/**
|
||||
# 多租户配置
|
||||
tenant:
|
||||
# 是否开启
|
||||
@@ -141,6 +146,9 @@ tenant:
|
||||
- sys_client
|
||||
- sys_oss_config
|
||||
- flow_spel
|
||||
# 链路追踪监控表:运维需跨租户全局查看,且 trace_node 在异步线程写入、租户上下文不传播,故排除租户过滤
|
||||
- trace_run
|
||||
- trace_node
|
||||
|
||||
# MyBatisPlus配置
|
||||
# https://baomidou.com/config/
|
||||
@@ -227,6 +235,15 @@ xss:
|
||||
excludeUrls:
|
||||
- /system/notice
|
||||
|
||||
--- # 链路追踪配置
|
||||
trace:
|
||||
# 是否启用链路追踪,默认 true
|
||||
# 关闭后所有埋点代码会直接透传业务逻辑,不写库、不创建上下文,零性能开销
|
||||
enabled: true
|
||||
payload:
|
||||
# 错误信息最大字符长度(格式: "异常类名: 异常消息"),超过部分会被截断丢弃
|
||||
max-error-length: 1000
|
||||
|
||||
--- # 分布式锁 lock4j 全局配置
|
||||
lock4j:
|
||||
# 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
@@ -260,6 +277,25 @@ websocket:
|
||||
# 设置访问源地址
|
||||
allowedOrigins: '*'
|
||||
|
||||
--- # 演示模式配置
|
||||
demo:
|
||||
# 是否开启演示模式(开启后所有写操作将被拦截)
|
||||
enabled: false
|
||||
# 提示消息
|
||||
message: "演示模式,不允许操作"
|
||||
# 排除的路径(这些路径不受演示模式限制)
|
||||
excludes:
|
||||
- /login
|
||||
- /logout
|
||||
- /register
|
||||
- /captcha/**
|
||||
- /auth/**
|
||||
- /chat/send
|
||||
- /system/session/**
|
||||
- /system/message/**
|
||||
- /system/attach/**
|
||||
- /system/fragment/**
|
||||
- /system/info/**
|
||||
--- # warm-flow工作流配置
|
||||
warm-flow:
|
||||
# 是否开启工作流,默认true
|
||||
@@ -277,11 +313,12 @@ warm-flow:
|
||||
vector-store:
|
||||
# 向量存储类型 可选(weaviate/milvus/qdrant)
|
||||
# 如需修改向量库类型,请修改此配置值!
|
||||
type: milvus
|
||||
# 注意:需与 docker-compose 实际部署的向量库保持一致(当前 compose 内置 weaviate,映射端口 28080)
|
||||
type: weaviate
|
||||
# Weaviate配置
|
||||
weaviate:
|
||||
protocol: http
|
||||
host: 127.0.0.1:6038
|
||||
host: 127.0.0.1:28080
|
||||
classname: LocalKnowledge
|
||||
# Milvus配置
|
||||
milvus:
|
||||
@@ -294,3 +331,48 @@ vector-store:
|
||||
collectionname: LocalKnowledge
|
||||
api-key:
|
||||
use-tls: false
|
||||
|
||||
# 流程编排扩展节点
|
||||
workflow:
|
||||
web-search:
|
||||
zhipu:
|
||||
# 推荐通过环境变量注入;为空时回退到模型管理中的 zhipu 厂商密钥
|
||||
api-key: ${ZAI_API_KEY:}
|
||||
base-url: ${ZHIPU_WEB_SEARCH_BASE_URL:https://open.bigmodel.cn/api/paas/v4/}
|
||||
connect-timeout: ${ZHIPU_WEB_SEARCH_CONNECT_TIMEOUT:10}
|
||||
read-timeout: ${ZHIPU_WEB_SEARCH_READ_TIMEOUT:30}
|
||||
|
||||
# 短剧成片合成
|
||||
short-drama:
|
||||
composition:
|
||||
ffmpeg-path: ${FFMPEG_PATH:ffmpeg}
|
||||
ffprobe-path: ${FFPROBE_PATH:ffprobe}
|
||||
fps: ${SHORT_DRAMA_COMPOSITION_FPS:30}
|
||||
audio-sample-rate: ${SHORT_DRAMA_COMPOSITION_AUDIO_SAMPLE_RATE:48000}
|
||||
video-codec: ${SHORT_DRAMA_COMPOSITION_VIDEO_CODEC:libx264}
|
||||
audio-codec: ${SHORT_DRAMA_COMPOSITION_AUDIO_CODEC:aac}
|
||||
preset: ${SHORT_DRAMA_COMPOSITION_PRESET:medium}
|
||||
crf: ${SHORT_DRAMA_COMPOSITION_CRF:20}
|
||||
audio-bitrate: ${SHORT_DRAMA_COMPOSITION_AUDIO_BITRATE:192k}
|
||||
max-clips: ${SHORT_DRAMA_COMPOSITION_MAX_CLIPS:100}
|
||||
max-transition-seconds: ${SHORT_DRAMA_COMPOSITION_MAX_TRANSITION_SECONDS:2.0}
|
||||
minimum-output-bytes: ${SHORT_DRAMA_COMPOSITION_MINIMUM_OUTPUT_BYTES:1024}
|
||||
max-process-output-bytes: ${SHORT_DRAMA_COMPOSITION_MAX_PROCESS_OUTPUT_BYTES:1048576}
|
||||
max-source-bytes: ${SHORT_DRAMA_COMPOSITION_MAX_SOURCE_BYTES:536870912}
|
||||
max-total-source-bytes: ${SHORT_DRAMA_COMPOSITION_MAX_TOTAL_SOURCE_BYTES:2147483648}
|
||||
probe-timeout: ${SHORT_DRAMA_COMPOSITION_PROBE_TIMEOUT:30s}
|
||||
process-timeout: ${SHORT_DRAMA_COMPOSITION_PROCESS_TIMEOUT:30m}
|
||||
job-stale-after: ${SHORT_DRAMA_COMPOSITION_JOB_STALE_AFTER:45m}
|
||||
watermark-font-file: ${SHORT_DRAMA_COMPOSITION_WATERMARK_FONT_FILE:}
|
||||
worker-core-size: ${SHORT_DRAMA_COMPOSITION_WORKER_CORE_SIZE:1}
|
||||
worker-max-size: ${SHORT_DRAMA_COMPOSITION_WORKER_MAX_SIZE:2}
|
||||
worker-queue-capacity: ${SHORT_DRAMA_COMPOSITION_WORKER_QUEUE_CAPACITY:8}
|
||||
storage-mode: ${SHORT_DRAMA_COMPOSITION_STORAGE_MODE:local}
|
||||
local-output-directory: ${SHORT_DRAMA_COMPOSITION_LOCAL_OUTPUT_DIRECTORY:logs/short-drama-compositions}
|
||||
download:
|
||||
connect-timeout: ${SHORT_DRAMA_DOWNLOAD_CONNECT_TIMEOUT:30s}
|
||||
call-timeout: ${SHORT_DRAMA_DOWNLOAD_CALL_TIMEOUT:10m}
|
||||
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}
|
||||
|
||||
30
ruoyi-admin/src/main/resources/skills/docx/LICENSE.txt
Normal file
30
ruoyi-admin/src/main/resources/skills/docx/LICENSE.txt
Normal file
@@ -0,0 +1,30 @@
|
||||
© 2025 Anthropic, PBC. All rights reserved.
|
||||
|
||||
LICENSE: Use of these materials (including all code, prompts, assets, files,
|
||||
and other components of this Skill) is governed by your agreement with
|
||||
Anthropic regarding use of Anthropic's services. If no separate agreement
|
||||
exists, use is governed by Anthropic's Consumer Terms of Service or
|
||||
Commercial Terms of Service, as applicable:
|
||||
https://www.anthropic.com/legal/consumer-terms
|
||||
https://www.anthropic.com/legal/commercial-terms
|
||||
Your applicable agreement is referred to as the "Agreement." "Services" are
|
||||
as defined in the Agreement.
|
||||
|
||||
ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the
|
||||
contrary, users may not:
|
||||
|
||||
- Extract these materials from the Services or retain copies of these
|
||||
materials outside the Services
|
||||
- Reproduce or copy these materials, except for temporary copies created
|
||||
automatically during authorized use of the Services
|
||||
- Create derivative works based on these materials
|
||||
- Distribute, sublicense, or transfer these materials to any third party
|
||||
- Make, offer to sell, sell, or import any inventions embodied in these
|
||||
materials
|
||||
- Reverse engineer, decompile, or disassemble these materials
|
||||
|
||||
The receipt, viewing, or possession of these materials does not convey or
|
||||
imply any license or right beyond those expressly granted above.
|
||||
|
||||
Anthropic retains all right, title, and interest in these materials,
|
||||
including all copyrights, patents, and other intellectual property rights.
|
||||
590
ruoyi-admin/src/main/resources/skills/docx/SKILL.md
Normal file
590
ruoyi-admin/src/main/resources/skills/docx/SKILL.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
"""Accept all tracked changes in a DOCX file using LibreOffice.
|
||||
|
||||
Requires LibreOffice (soffice) to be installed.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from office.soffice import get_soffice_env
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
LIBREOFFICE_PROFILE = "/tmp/libreoffice_docx_profile"
|
||||
MACRO_DIR = f"{LIBREOFFICE_PROFILE}/user/basic/Standard"
|
||||
|
||||
ACCEPT_CHANGES_MACRO = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE script:module PUBLIC "-//OpenOffice.org//DTD OfficeDocument 1.0//EN" "module.dtd">
|
||||
<script:module xmlns:script="http://openoffice.org/2000/script" script:name="Module1" script:language="StarBasic">
|
||||
Sub AcceptAllTrackedChanges()
|
||||
Dim document As Object
|
||||
Dim dispatcher As Object
|
||||
|
||||
document = ThisComponent.CurrentController.Frame
|
||||
dispatcher = createUnoService("com.sun.star.frame.DispatchHelper")
|
||||
|
||||
dispatcher.executeDispatch(document, ".uno:AcceptAllTrackedChanges", "", 0, Array())
|
||||
ThisComponent.store()
|
||||
ThisComponent.close(True)
|
||||
End Sub
|
||||
</script:module>"""
|
||||
|
||||
|
||||
def accept_changes(
|
||||
input_file: str,
|
||||
output_file: str,
|
||||
) -> tuple[None, str]:
|
||||
input_path = Path(input_file)
|
||||
output_path = Path(output_file)
|
||||
|
||||
if not input_path.exists():
|
||||
return None, f"Error: Input file not found: {input_file}"
|
||||
|
||||
if not input_path.suffix.lower() == ".docx":
|
||||
return None, f"Error: Input file is not a DOCX file: {input_file}"
|
||||
|
||||
try:
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(input_path, output_path)
|
||||
except Exception as e:
|
||||
return None, f"Error: Failed to copy input file to output location: {e}"
|
||||
|
||||
if not _setup_libreoffice_macro():
|
||||
return None, "Error: Failed to setup LibreOffice macro"
|
||||
|
||||
cmd = [
|
||||
"soffice",
|
||||
"--headless",
|
||||
f"-env:UserInstallation=file://{LIBREOFFICE_PROFILE}",
|
||||
"--norestore",
|
||||
"vnd.sun.star.script:Standard.Module1.AcceptAllTrackedChanges?language=Basic&location=application",
|
||||
str(output_path.absolute()),
|
||||
]
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
check=False,
|
||||
env=get_soffice_env(),
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
return (
|
||||
None,
|
||||
f"Successfully accepted all tracked changes: {input_file} -> {output_file}",
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
return None, f"Error: LibreOffice failed: {result.stderr}"
|
||||
|
||||
return (
|
||||
None,
|
||||
f"Successfully accepted all tracked changes: {input_file} -> {output_file}",
|
||||
)
|
||||
|
||||
|
||||
def _setup_libreoffice_macro() -> bool:
|
||||
macro_dir = Path(MACRO_DIR)
|
||||
macro_file = macro_dir / "Module1.xba"
|
||||
|
||||
if macro_file.exists() and "AcceptAllTrackedChanges" in macro_file.read_text():
|
||||
return True
|
||||
|
||||
if not macro_dir.exists():
|
||||
subprocess.run(
|
||||
[
|
||||
"soffice",
|
||||
"--headless",
|
||||
f"-env:UserInstallation=file://{LIBREOFFICE_PROFILE}",
|
||||
"--terminate_after_init",
|
||||
],
|
||||
capture_output=True,
|
||||
timeout=10,
|
||||
check=False,
|
||||
env=get_soffice_env(),
|
||||
)
|
||||
macro_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
macro_file.write_text(ACCEPT_CHANGES_MACRO)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to setup LibreOffice macro: {e}")
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Accept all tracked changes in a DOCX file"
|
||||
)
|
||||
parser.add_argument("input_file", help="Input DOCX file with tracked changes")
|
||||
parser.add_argument(
|
||||
"output_file", help="Output DOCX file (clean, no tracked changes)"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
_, message = accept_changes(args.input_file, args.output_file)
|
||||
print(message)
|
||||
|
||||
if "Error" in message:
|
||||
raise SystemExit(1)
|
||||
318
ruoyi-admin/src/main/resources/skills/docx/scripts/comment.py
Normal file
318
ruoyi-admin/src/main/resources/skills/docx/scripts/comment.py
Normal file
@@ -0,0 +1,318 @@
|
||||
"""Add comments to DOCX documents.
|
||||
|
||||
Usage:
|
||||
python comment.py unpacked/ 0 "Comment text"
|
||||
python comment.py unpacked/ 1 "Reply text" --parent 0
|
||||
|
||||
Text should be pre-escaped XML (e.g., & for &, ’ for smart quotes).
|
||||
|
||||
After running, add markers to document.xml:
|
||||
<w:commentRangeStart w:id="0"/>
|
||||
... commented content ...
|
||||
<w:commentRangeEnd w:id="0"/>
|
||||
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="0"/></w:r>
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import random
|
||||
import shutil
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import defusedxml.minidom
|
||||
|
||||
TEMPLATE_DIR = Path(__file__).parent / "templates"
|
||||
NS = {
|
||||
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
|
||||
"w14": "http://schemas.microsoft.com/office/word/2010/wordml",
|
||||
"w15": "http://schemas.microsoft.com/office/word/2012/wordml",
|
||||
"w16cid": "http://schemas.microsoft.com/office/word/2016/wordml/cid",
|
||||
"w16cex": "http://schemas.microsoft.com/office/word/2018/wordml/cex",
|
||||
}
|
||||
|
||||
COMMENT_XML = """\
|
||||
<w:comment w:id="{id}" w:author="{author}" w:date="{date}" w:initials="{initials}">
|
||||
<w:p w14:paraId="{para_id}" w14:textId="77777777">
|
||||
<w:r>
|
||||
<w:rPr><w:rStyle w:val="CommentReference"/></w:rPr>
|
||||
<w:annotationRef/>
|
||||
</w:r>
|
||||
<w:r>
|
||||
<w:rPr>
|
||||
<w:color w:val="000000"/>
|
||||
<w:sz w:val="20"/>
|
||||
<w:szCs w:val="20"/>
|
||||
</w:rPr>
|
||||
<w:t>{text}</w:t>
|
||||
</w:r>
|
||||
</w:p>
|
||||
</w:comment>"""
|
||||
|
||||
COMMENT_MARKER_TEMPLATE = """
|
||||
Add to document.xml (markers must be direct children of w:p, never inside w:r):
|
||||
<w:commentRangeStart w:id="{cid}"/>
|
||||
<w:r>...</w:r>
|
||||
<w:commentRangeEnd w:id="{cid}"/>
|
||||
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="{cid}"/></w:r>"""
|
||||
|
||||
REPLY_MARKER_TEMPLATE = """
|
||||
Nest markers inside parent {pid}'s markers (markers must be direct children of w:p, never inside w:r):
|
||||
<w:commentRangeStart w:id="{pid}"/><w:commentRangeStart w:id="{cid}"/>
|
||||
<w:r>...</w:r>
|
||||
<w:commentRangeEnd w:id="{cid}"/><w:commentRangeEnd w:id="{pid}"/>
|
||||
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="{pid}"/></w:r>
|
||||
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="{cid}"/></w:r>"""
|
||||
|
||||
|
||||
def _generate_hex_id() -> str:
|
||||
return f"{random.randint(0, 0x7FFFFFFE):08X}"
|
||||
|
||||
|
||||
SMART_QUOTE_ENTITIES = {
|
||||
"\u201c": "“",
|
||||
"\u201d": "”",
|
||||
"\u2018": "‘",
|
||||
"\u2019": "’",
|
||||
}
|
||||
|
||||
|
||||
def _encode_smart_quotes(text: str) -> str:
|
||||
for char, entity in SMART_QUOTE_ENTITIES.items():
|
||||
text = text.replace(char, entity)
|
||||
return text
|
||||
|
||||
|
||||
def _append_xml(xml_path: Path, root_tag: str, content: str) -> None:
|
||||
dom = defusedxml.minidom.parseString(xml_path.read_text(encoding="utf-8"))
|
||||
root = dom.getElementsByTagName(root_tag)[0]
|
||||
ns_attrs = " ".join(f'xmlns:{k}="{v}"' for k, v in NS.items())
|
||||
wrapper_dom = defusedxml.minidom.parseString(f"<root {ns_attrs}>{content}</root>")
|
||||
for child in wrapper_dom.documentElement.childNodes:
|
||||
if child.nodeType == child.ELEMENT_NODE:
|
||||
root.appendChild(dom.importNode(child, True))
|
||||
output = _encode_smart_quotes(dom.toxml(encoding="UTF-8").decode("utf-8"))
|
||||
xml_path.write_text(output, encoding="utf-8")
|
||||
|
||||
|
||||
def _find_para_id(comments_path: Path, comment_id: int) -> str | None:
|
||||
dom = defusedxml.minidom.parseString(comments_path.read_text(encoding="utf-8"))
|
||||
for c in dom.getElementsByTagName("w:comment"):
|
||||
if c.getAttribute("w:id") == str(comment_id):
|
||||
for p in c.getElementsByTagName("w:p"):
|
||||
if pid := p.getAttribute("w14:paraId"):
|
||||
return pid
|
||||
return None
|
||||
|
||||
|
||||
def _get_next_rid(rels_path: Path) -> int:
|
||||
dom = defusedxml.minidom.parseString(rels_path.read_text(encoding="utf-8"))
|
||||
max_rid = 0
|
||||
for rel in dom.getElementsByTagName("Relationship"):
|
||||
rid = rel.getAttribute("Id")
|
||||
if rid and rid.startswith("rId"):
|
||||
try:
|
||||
max_rid = max(max_rid, int(rid[3:]))
|
||||
except ValueError:
|
||||
pass
|
||||
return max_rid + 1
|
||||
|
||||
|
||||
def _has_relationship(rels_path: Path, target: str) -> bool:
|
||||
dom = defusedxml.minidom.parseString(rels_path.read_text(encoding="utf-8"))
|
||||
for rel in dom.getElementsByTagName("Relationship"):
|
||||
if rel.getAttribute("Target") == target:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _has_content_type(ct_path: Path, part_name: str) -> bool:
|
||||
dom = defusedxml.minidom.parseString(ct_path.read_text(encoding="utf-8"))
|
||||
for override in dom.getElementsByTagName("Override"):
|
||||
if override.getAttribute("PartName") == part_name:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _ensure_comment_relationships(unpacked_dir: Path) -> None:
|
||||
rels_path = unpacked_dir / "word" / "_rels" / "document.xml.rels"
|
||||
if not rels_path.exists():
|
||||
return
|
||||
|
||||
if _has_relationship(rels_path, "comments.xml"):
|
||||
return
|
||||
|
||||
dom = defusedxml.minidom.parseString(rels_path.read_text(encoding="utf-8"))
|
||||
root = dom.documentElement
|
||||
next_rid = _get_next_rid(rels_path)
|
||||
|
||||
rels = [
|
||||
(
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments",
|
||||
"comments.xml",
|
||||
),
|
||||
(
|
||||
"http://schemas.microsoft.com/office/2011/relationships/commentsExtended",
|
||||
"commentsExtended.xml",
|
||||
),
|
||||
(
|
||||
"http://schemas.microsoft.com/office/2016/09/relationships/commentsIds",
|
||||
"commentsIds.xml",
|
||||
),
|
||||
(
|
||||
"http://schemas.microsoft.com/office/2018/08/relationships/commentsExtensible",
|
||||
"commentsExtensible.xml",
|
||||
),
|
||||
]
|
||||
|
||||
for rel_type, target in rels:
|
||||
rel = dom.createElement("Relationship")
|
||||
rel.setAttribute("Id", f"rId{next_rid}")
|
||||
rel.setAttribute("Type", rel_type)
|
||||
rel.setAttribute("Target", target)
|
||||
root.appendChild(rel)
|
||||
next_rid += 1
|
||||
|
||||
rels_path.write_bytes(dom.toxml(encoding="UTF-8"))
|
||||
|
||||
|
||||
def _ensure_comment_content_types(unpacked_dir: Path) -> None:
|
||||
ct_path = unpacked_dir / "[Content_Types].xml"
|
||||
if not ct_path.exists():
|
||||
return
|
||||
|
||||
if _has_content_type(ct_path, "/word/comments.xml"):
|
||||
return
|
||||
|
||||
dom = defusedxml.minidom.parseString(ct_path.read_text(encoding="utf-8"))
|
||||
root = dom.documentElement
|
||||
|
||||
overrides = [
|
||||
(
|
||||
"/word/comments.xml",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml",
|
||||
),
|
||||
(
|
||||
"/word/commentsExtended.xml",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml",
|
||||
),
|
||||
(
|
||||
"/word/commentsIds.xml",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.commentsIds+xml",
|
||||
),
|
||||
(
|
||||
"/word/commentsExtensible.xml",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtensible+xml",
|
||||
),
|
||||
]
|
||||
|
||||
for part_name, content_type in overrides:
|
||||
override = dom.createElement("Override")
|
||||
override.setAttribute("PartName", part_name)
|
||||
override.setAttribute("ContentType", content_type)
|
||||
root.appendChild(override)
|
||||
|
||||
ct_path.write_bytes(dom.toxml(encoding="UTF-8"))
|
||||
|
||||
|
||||
def add_comment(
|
||||
unpacked_dir: str,
|
||||
comment_id: int,
|
||||
text: str,
|
||||
author: str = "Claude",
|
||||
initials: str = "C",
|
||||
parent_id: int | None = None,
|
||||
) -> tuple[str, str]:
|
||||
word = Path(unpacked_dir) / "word"
|
||||
if not word.exists():
|
||||
return "", f"Error: {word} not found"
|
||||
|
||||
para_id, durable_id = _generate_hex_id(), _generate_hex_id()
|
||||
ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
comments = word / "comments.xml"
|
||||
first_comment = not comments.exists()
|
||||
if first_comment:
|
||||
shutil.copy(TEMPLATE_DIR / "comments.xml", comments)
|
||||
_ensure_comment_relationships(Path(unpacked_dir))
|
||||
_ensure_comment_content_types(Path(unpacked_dir))
|
||||
_append_xml(
|
||||
comments,
|
||||
"w:comments",
|
||||
COMMENT_XML.format(
|
||||
id=comment_id,
|
||||
author=author,
|
||||
date=ts,
|
||||
initials=initials,
|
||||
para_id=para_id,
|
||||
text=text,
|
||||
),
|
||||
)
|
||||
|
||||
ext = word / "commentsExtended.xml"
|
||||
if not ext.exists():
|
||||
shutil.copy(TEMPLATE_DIR / "commentsExtended.xml", ext)
|
||||
if parent_id is not None:
|
||||
parent_para = _find_para_id(comments, parent_id)
|
||||
if not parent_para:
|
||||
return "", f"Error: Parent comment {parent_id} not found"
|
||||
_append_xml(
|
||||
ext,
|
||||
"w15:commentsEx",
|
||||
f'<w15:commentEx w15:paraId="{para_id}" w15:paraIdParent="{parent_para}" w15:done="0"/>',
|
||||
)
|
||||
else:
|
||||
_append_xml(
|
||||
ext,
|
||||
"w15:commentsEx",
|
||||
f'<w15:commentEx w15:paraId="{para_id}" w15:done="0"/>',
|
||||
)
|
||||
|
||||
ids = word / "commentsIds.xml"
|
||||
if not ids.exists():
|
||||
shutil.copy(TEMPLATE_DIR / "commentsIds.xml", ids)
|
||||
_append_xml(
|
||||
ids,
|
||||
"w16cid:commentsIds",
|
||||
f'<w16cid:commentId w16cid:paraId="{para_id}" w16cid:durableId="{durable_id}"/>',
|
||||
)
|
||||
|
||||
extensible = word / "commentsExtensible.xml"
|
||||
if not extensible.exists():
|
||||
shutil.copy(TEMPLATE_DIR / "commentsExtensible.xml", extensible)
|
||||
_append_xml(
|
||||
extensible,
|
||||
"w16cex:commentsExtensible",
|
||||
f'<w16cex:commentExtensible w16cex:durableId="{durable_id}" w16cex:dateUtc="{ts}"/>',
|
||||
)
|
||||
|
||||
action = "reply" if parent_id is not None else "comment"
|
||||
return para_id, f"Added {action} {comment_id} (para_id={para_id})"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
p = argparse.ArgumentParser(description="Add comments to DOCX documents")
|
||||
p.add_argument("unpacked_dir", help="Unpacked DOCX directory")
|
||||
p.add_argument("comment_id", type=int, help="Comment ID (must be unique)")
|
||||
p.add_argument("text", help="Comment text")
|
||||
p.add_argument("--author", default="Claude", help="Author name")
|
||||
p.add_argument("--initials", default="C", help="Author initials")
|
||||
p.add_argument("--parent", type=int, help="Parent comment ID (for replies)")
|
||||
args = p.parse_args()
|
||||
|
||||
para_id, msg = add_comment(
|
||||
args.unpacked_dir,
|
||||
args.comment_id,
|
||||
args.text,
|
||||
args.author,
|
||||
args.initials,
|
||||
args.parent,
|
||||
)
|
||||
print(msg)
|
||||
if "Error" in msg:
|
||||
sys.exit(1)
|
||||
cid = args.comment_id
|
||||
if args.parent is not None:
|
||||
print(REPLY_MARKER_TEMPLATE.format(pid=args.parent, cid=cid))
|
||||
else:
|
||||
print(COMMENT_MARKER_TEMPLATE.format(cid=cid))
|
||||
@@ -0,0 +1,199 @@
|
||||
"""Merge adjacent runs with identical formatting in DOCX.
|
||||
|
||||
Merges adjacent <w:r> elements that have identical <w:rPr> properties.
|
||||
Works on runs in paragraphs and inside tracked changes (<w:ins>, <w:del>).
|
||||
|
||||
Also:
|
||||
- Removes rsid attributes from runs (revision metadata that doesn't affect rendering)
|
||||
- Removes proofErr elements (spell/grammar markers that block merging)
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import defusedxml.minidom
|
||||
|
||||
|
||||
def merge_runs(input_dir: str) -> tuple[int, str]:
|
||||
doc_xml = Path(input_dir) / "word" / "document.xml"
|
||||
|
||||
if not doc_xml.exists():
|
||||
return 0, f"Error: {doc_xml} not found"
|
||||
|
||||
try:
|
||||
dom = defusedxml.minidom.parseString(doc_xml.read_text(encoding="utf-8"))
|
||||
root = dom.documentElement
|
||||
|
||||
_remove_elements(root, "proofErr")
|
||||
_strip_run_rsid_attrs(root)
|
||||
|
||||
containers = {run.parentNode for run in _find_elements(root, "r")}
|
||||
|
||||
merge_count = 0
|
||||
for container in containers:
|
||||
merge_count += _merge_runs_in(container)
|
||||
|
||||
doc_xml.write_bytes(dom.toxml(encoding="UTF-8"))
|
||||
return merge_count, f"Merged {merge_count} runs"
|
||||
|
||||
except Exception as e:
|
||||
return 0, f"Error: {e}"
|
||||
|
||||
|
||||
|
||||
|
||||
def _find_elements(root, tag: str) -> list:
|
||||
results = []
|
||||
|
||||
def traverse(node):
|
||||
if node.nodeType == node.ELEMENT_NODE:
|
||||
name = node.localName or node.tagName
|
||||
if name == tag or name.endswith(f":{tag}"):
|
||||
results.append(node)
|
||||
for child in node.childNodes:
|
||||
traverse(child)
|
||||
|
||||
traverse(root)
|
||||
return results
|
||||
|
||||
|
||||
def _get_child(parent, tag: str):
|
||||
for child in parent.childNodes:
|
||||
if child.nodeType == child.ELEMENT_NODE:
|
||||
name = child.localName or child.tagName
|
||||
if name == tag or name.endswith(f":{tag}"):
|
||||
return child
|
||||
return None
|
||||
|
||||
|
||||
def _get_children(parent, tag: str) -> list:
|
||||
results = []
|
||||
for child in parent.childNodes:
|
||||
if child.nodeType == child.ELEMENT_NODE:
|
||||
name = child.localName or child.tagName
|
||||
if name == tag or name.endswith(f":{tag}"):
|
||||
results.append(child)
|
||||
return results
|
||||
|
||||
|
||||
def _is_adjacent(elem1, elem2) -> bool:
|
||||
node = elem1.nextSibling
|
||||
while node:
|
||||
if node == elem2:
|
||||
return True
|
||||
if node.nodeType == node.ELEMENT_NODE:
|
||||
return False
|
||||
if node.nodeType == node.TEXT_NODE and node.data.strip():
|
||||
return False
|
||||
node = node.nextSibling
|
||||
return False
|
||||
|
||||
|
||||
|
||||
|
||||
def _remove_elements(root, tag: str):
|
||||
for elem in _find_elements(root, tag):
|
||||
if elem.parentNode:
|
||||
elem.parentNode.removeChild(elem)
|
||||
|
||||
|
||||
def _strip_run_rsid_attrs(root):
|
||||
for run in _find_elements(root, "r"):
|
||||
for attr in list(run.attributes.values()):
|
||||
if "rsid" in attr.name.lower():
|
||||
run.removeAttribute(attr.name)
|
||||
|
||||
|
||||
|
||||
|
||||
def _merge_runs_in(container) -> int:
|
||||
merge_count = 0
|
||||
run = _first_child_run(container)
|
||||
|
||||
while run:
|
||||
while True:
|
||||
next_elem = _next_element_sibling(run)
|
||||
if next_elem and _is_run(next_elem) and _can_merge(run, next_elem):
|
||||
_merge_run_content(run, next_elem)
|
||||
container.removeChild(next_elem)
|
||||
merge_count += 1
|
||||
else:
|
||||
break
|
||||
|
||||
_consolidate_text(run)
|
||||
run = _next_sibling_run(run)
|
||||
|
||||
return merge_count
|
||||
|
||||
|
||||
def _first_child_run(container):
|
||||
for child in container.childNodes:
|
||||
if child.nodeType == child.ELEMENT_NODE and _is_run(child):
|
||||
return child
|
||||
return None
|
||||
|
||||
|
||||
def _next_element_sibling(node):
|
||||
sibling = node.nextSibling
|
||||
while sibling:
|
||||
if sibling.nodeType == sibling.ELEMENT_NODE:
|
||||
return sibling
|
||||
sibling = sibling.nextSibling
|
||||
return None
|
||||
|
||||
|
||||
def _next_sibling_run(node):
|
||||
sibling = node.nextSibling
|
||||
while sibling:
|
||||
if sibling.nodeType == sibling.ELEMENT_NODE:
|
||||
if _is_run(sibling):
|
||||
return sibling
|
||||
sibling = sibling.nextSibling
|
||||
return None
|
||||
|
||||
|
||||
def _is_run(node) -> bool:
|
||||
name = node.localName or node.tagName
|
||||
return name == "r" or name.endswith(":r")
|
||||
|
||||
|
||||
def _can_merge(run1, run2) -> bool:
|
||||
rpr1 = _get_child(run1, "rPr")
|
||||
rpr2 = _get_child(run2, "rPr")
|
||||
|
||||
if (rpr1 is None) != (rpr2 is None):
|
||||
return False
|
||||
if rpr1 is None:
|
||||
return True
|
||||
return rpr1.toxml() == rpr2.toxml()
|
||||
|
||||
|
||||
def _merge_run_content(target, source):
|
||||
for child in list(source.childNodes):
|
||||
if child.nodeType == child.ELEMENT_NODE:
|
||||
name = child.localName or child.tagName
|
||||
if name != "rPr" and not name.endswith(":rPr"):
|
||||
target.appendChild(child)
|
||||
|
||||
|
||||
def _consolidate_text(run):
|
||||
t_elements = _get_children(run, "t")
|
||||
|
||||
for i in range(len(t_elements) - 1, 0, -1):
|
||||
curr, prev = t_elements[i], t_elements[i - 1]
|
||||
|
||||
if _is_adjacent(prev, curr):
|
||||
prev_text = prev.firstChild.data if prev.firstChild else ""
|
||||
curr_text = curr.firstChild.data if curr.firstChild else ""
|
||||
merged = prev_text + curr_text
|
||||
|
||||
if prev.firstChild:
|
||||
prev.firstChild.data = merged
|
||||
else:
|
||||
prev.appendChild(run.ownerDocument.createTextNode(merged))
|
||||
|
||||
if merged.startswith(" ") or merged.endswith(" "):
|
||||
prev.setAttribute("xml:space", "preserve")
|
||||
elif prev.hasAttribute("xml:space"):
|
||||
prev.removeAttribute("xml:space")
|
||||
|
||||
run.removeChild(curr)
|
||||
@@ -0,0 +1,197 @@
|
||||
"""Simplify tracked changes by merging adjacent w:ins or w:del elements.
|
||||
|
||||
Merges adjacent <w:ins> elements from the same author into a single element.
|
||||
Same for <w:del> elements. This makes heavily-redlined documents easier to
|
||||
work with by reducing the number of tracked change wrappers.
|
||||
|
||||
Rules:
|
||||
- Only merges w:ins with w:ins, w:del with w:del (same element type)
|
||||
- Only merges if same author (ignores timestamp differences)
|
||||
- Only merges if truly adjacent (only whitespace between them)
|
||||
"""
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
import defusedxml.minidom
|
||||
|
||||
WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
|
||||
|
||||
def simplify_redlines(input_dir: str) -> tuple[int, str]:
|
||||
doc_xml = Path(input_dir) / "word" / "document.xml"
|
||||
|
||||
if not doc_xml.exists():
|
||||
return 0, f"Error: {doc_xml} not found"
|
||||
|
||||
try:
|
||||
dom = defusedxml.minidom.parseString(doc_xml.read_text(encoding="utf-8"))
|
||||
root = dom.documentElement
|
||||
|
||||
merge_count = 0
|
||||
|
||||
containers = _find_elements(root, "p") + _find_elements(root, "tc")
|
||||
|
||||
for container in containers:
|
||||
merge_count += _merge_tracked_changes_in(container, "ins")
|
||||
merge_count += _merge_tracked_changes_in(container, "del")
|
||||
|
||||
doc_xml.write_bytes(dom.toxml(encoding="UTF-8"))
|
||||
return merge_count, f"Simplified {merge_count} tracked changes"
|
||||
|
||||
except Exception as e:
|
||||
return 0, f"Error: {e}"
|
||||
|
||||
|
||||
def _merge_tracked_changes_in(container, tag: str) -> int:
|
||||
merge_count = 0
|
||||
|
||||
tracked = [
|
||||
child
|
||||
for child in container.childNodes
|
||||
if child.nodeType == child.ELEMENT_NODE and _is_element(child, tag)
|
||||
]
|
||||
|
||||
if len(tracked) < 2:
|
||||
return 0
|
||||
|
||||
i = 0
|
||||
while i < len(tracked) - 1:
|
||||
curr = tracked[i]
|
||||
next_elem = tracked[i + 1]
|
||||
|
||||
if _can_merge_tracked(curr, next_elem):
|
||||
_merge_tracked_content(curr, next_elem)
|
||||
container.removeChild(next_elem)
|
||||
tracked.pop(i + 1)
|
||||
merge_count += 1
|
||||
else:
|
||||
i += 1
|
||||
|
||||
return merge_count
|
||||
|
||||
|
||||
def _is_element(node, tag: str) -> bool:
|
||||
name = node.localName or node.tagName
|
||||
return name == tag or name.endswith(f":{tag}")
|
||||
|
||||
|
||||
def _get_author(elem) -> str:
|
||||
author = elem.getAttribute("w:author")
|
||||
if not author:
|
||||
for attr in elem.attributes.values():
|
||||
if attr.localName == "author" or attr.name.endswith(":author"):
|
||||
return attr.value
|
||||
return author
|
||||
|
||||
|
||||
def _can_merge_tracked(elem1, elem2) -> bool:
|
||||
if _get_author(elem1) != _get_author(elem2):
|
||||
return False
|
||||
|
||||
node = elem1.nextSibling
|
||||
while node and node != elem2:
|
||||
if node.nodeType == node.ELEMENT_NODE:
|
||||
return False
|
||||
if node.nodeType == node.TEXT_NODE and node.data.strip():
|
||||
return False
|
||||
node = node.nextSibling
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _merge_tracked_content(target, source):
|
||||
while source.firstChild:
|
||||
child = source.firstChild
|
||||
source.removeChild(child)
|
||||
target.appendChild(child)
|
||||
|
||||
|
||||
def _find_elements(root, tag: str) -> list:
|
||||
results = []
|
||||
|
||||
def traverse(node):
|
||||
if node.nodeType == node.ELEMENT_NODE:
|
||||
name = node.localName or node.tagName
|
||||
if name == tag or name.endswith(f":{tag}"):
|
||||
results.append(node)
|
||||
for child in node.childNodes:
|
||||
traverse(child)
|
||||
|
||||
traverse(root)
|
||||
return results
|
||||
|
||||
|
||||
def get_tracked_change_authors(doc_xml_path: Path) -> dict[str, int]:
|
||||
if not doc_xml_path.exists():
|
||||
return {}
|
||||
|
||||
try:
|
||||
tree = ET.parse(doc_xml_path)
|
||||
root = tree.getroot()
|
||||
except ET.ParseError:
|
||||
return {}
|
||||
|
||||
namespaces = {"w": WORD_NS}
|
||||
author_attr = f"{{{WORD_NS}}}author"
|
||||
|
||||
authors: dict[str, int] = {}
|
||||
for tag in ["ins", "del"]:
|
||||
for elem in root.findall(f".//w:{tag}", namespaces):
|
||||
author = elem.get(author_attr)
|
||||
if author:
|
||||
authors[author] = authors.get(author, 0) + 1
|
||||
|
||||
return authors
|
||||
|
||||
|
||||
def _get_authors_from_docx(docx_path: Path) -> dict[str, int]:
|
||||
try:
|
||||
with zipfile.ZipFile(docx_path, "r") as zf:
|
||||
if "word/document.xml" not in zf.namelist():
|
||||
return {}
|
||||
with zf.open("word/document.xml") as f:
|
||||
tree = ET.parse(f)
|
||||
root = tree.getroot()
|
||||
|
||||
namespaces = {"w": WORD_NS}
|
||||
author_attr = f"{{{WORD_NS}}}author"
|
||||
|
||||
authors: dict[str, int] = {}
|
||||
for tag in ["ins", "del"]:
|
||||
for elem in root.findall(f".//w:{tag}", namespaces):
|
||||
author = elem.get(author_attr)
|
||||
if author:
|
||||
authors[author] = authors.get(author, 0) + 1
|
||||
return authors
|
||||
except (zipfile.BadZipFile, ET.ParseError):
|
||||
return {}
|
||||
|
||||
|
||||
def infer_author(modified_dir: Path, original_docx: Path, default: str = "Claude") -> str:
|
||||
modified_xml = modified_dir / "word" / "document.xml"
|
||||
modified_authors = get_tracked_change_authors(modified_xml)
|
||||
|
||||
if not modified_authors:
|
||||
return default
|
||||
|
||||
original_authors = _get_authors_from_docx(original_docx)
|
||||
|
||||
new_changes: dict[str, int] = {}
|
||||
for author, count in modified_authors.items():
|
||||
original_count = original_authors.get(author, 0)
|
||||
diff = count - original_count
|
||||
if diff > 0:
|
||||
new_changes[author] = diff
|
||||
|
||||
if not new_changes:
|
||||
return default
|
||||
|
||||
if len(new_changes) == 1:
|
||||
return next(iter(new_changes))
|
||||
|
||||
raise ValueError(
|
||||
f"Multiple authors added new changes: {new_changes}. "
|
||||
"Cannot infer which author to validate."
|
||||
)
|
||||
@@ -0,0 +1,159 @@
|
||||
"""Pack a directory into a DOCX, PPTX, or XLSX file.
|
||||
|
||||
Validates with auto-repair, condenses XML formatting, and creates the Office file.
|
||||
|
||||
Usage:
|
||||
python pack.py <input_directory> <output_file> [--original <file>] [--validate true|false]
|
||||
|
||||
Examples:
|
||||
python pack.py unpacked/ output.docx --original input.docx
|
||||
python pack.py unpacked/ output.pptx --validate false
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import shutil
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
import defusedxml.minidom
|
||||
|
||||
from validators import DOCXSchemaValidator, PPTXSchemaValidator, RedliningValidator
|
||||
|
||||
def pack(
|
||||
input_directory: str,
|
||||
output_file: str,
|
||||
original_file: str | None = None,
|
||||
validate: bool = True,
|
||||
infer_author_func=None,
|
||||
) -> tuple[None, str]:
|
||||
input_dir = Path(input_directory)
|
||||
output_path = Path(output_file)
|
||||
suffix = output_path.suffix.lower()
|
||||
|
||||
if not input_dir.is_dir():
|
||||
return None, f"Error: {input_dir} is not a directory"
|
||||
|
||||
if suffix not in {".docx", ".pptx", ".xlsx"}:
|
||||
return None, f"Error: {output_file} must be a .docx, .pptx, or .xlsx file"
|
||||
|
||||
if validate and original_file:
|
||||
original_path = Path(original_file)
|
||||
if original_path.exists():
|
||||
success, output = _run_validation(
|
||||
input_dir, original_path, suffix, infer_author_func
|
||||
)
|
||||
if output:
|
||||
print(output)
|
||||
if not success:
|
||||
return None, f"Error: Validation failed for {input_dir}"
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_content_dir = Path(temp_dir) / "content"
|
||||
shutil.copytree(input_dir, temp_content_dir)
|
||||
|
||||
for pattern in ["*.xml", "*.rels"]:
|
||||
for xml_file in temp_content_dir.rglob(pattern):
|
||||
_condense_xml(xml_file)
|
||||
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
for f in temp_content_dir.rglob("*"):
|
||||
if f.is_file():
|
||||
zf.write(f, f.relative_to(temp_content_dir))
|
||||
|
||||
return None, f"Successfully packed {input_dir} to {output_file}"
|
||||
|
||||
|
||||
def _run_validation(
|
||||
unpacked_dir: Path,
|
||||
original_file: Path,
|
||||
suffix: str,
|
||||
infer_author_func=None,
|
||||
) -> tuple[bool, str | None]:
|
||||
output_lines = []
|
||||
validators = []
|
||||
|
||||
if suffix == ".docx":
|
||||
author = "Claude"
|
||||
if infer_author_func:
|
||||
try:
|
||||
author = infer_author_func(unpacked_dir, original_file)
|
||||
except ValueError as e:
|
||||
print(f"Warning: {e} Using default author 'Claude'.", file=sys.stderr)
|
||||
|
||||
validators = [
|
||||
DOCXSchemaValidator(unpacked_dir, original_file),
|
||||
RedliningValidator(unpacked_dir, original_file, author=author),
|
||||
]
|
||||
elif suffix == ".pptx":
|
||||
validators = [PPTXSchemaValidator(unpacked_dir, original_file)]
|
||||
|
||||
if not validators:
|
||||
return True, None
|
||||
|
||||
total_repairs = sum(v.repair() for v in validators)
|
||||
if total_repairs:
|
||||
output_lines.append(f"Auto-repaired {total_repairs} issue(s)")
|
||||
|
||||
success = all(v.validate() for v in validators)
|
||||
|
||||
if success:
|
||||
output_lines.append("All validations PASSED!")
|
||||
|
||||
return success, "\n".join(output_lines) if output_lines else None
|
||||
|
||||
|
||||
def _condense_xml(xml_file: Path) -> None:
|
||||
try:
|
||||
with open(xml_file, encoding="utf-8") as f:
|
||||
dom = defusedxml.minidom.parse(f)
|
||||
|
||||
for element in dom.getElementsByTagName("*"):
|
||||
if element.tagName.endswith(":t"):
|
||||
continue
|
||||
|
||||
for child in list(element.childNodes):
|
||||
if (
|
||||
child.nodeType == child.TEXT_NODE
|
||||
and child.nodeValue
|
||||
and child.nodeValue.strip() == ""
|
||||
) or child.nodeType == child.COMMENT_NODE:
|
||||
element.removeChild(child)
|
||||
|
||||
xml_file.write_bytes(dom.toxml(encoding="UTF-8"))
|
||||
except Exception as e:
|
||||
print(f"ERROR: Failed to parse {xml_file.name}: {e}", file=sys.stderr)
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Pack a directory into a DOCX, PPTX, or XLSX file"
|
||||
)
|
||||
parser.add_argument("input_directory", help="Unpacked Office document directory")
|
||||
parser.add_argument("output_file", help="Output Office file (.docx/.pptx/.xlsx)")
|
||||
parser.add_argument(
|
||||
"--original",
|
||||
help="Original file for validation comparison",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--validate",
|
||||
type=lambda x: x.lower() == "true",
|
||||
default=True,
|
||||
metavar="true|false",
|
||||
help="Run validation with auto-repair (default: true)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
_, message = pack(
|
||||
args.input_directory,
|
||||
args.output_file,
|
||||
original_file=args.original,
|
||||
validate=args.validate,
|
||||
)
|
||||
print(message)
|
||||
|
||||
if "Error" in message:
|
||||
sys.exit(1)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,146 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||
xmlns="http://schemas.openxmlformats.org/drawingml/2006/chartDrawing"
|
||||
targetNamespace="http://schemas.openxmlformats.org/drawingml/2006/chartDrawing"
|
||||
elementFormDefault="qualified">
|
||||
<xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||
schemaLocation="dml-main.xsd"/>
|
||||
<xsd:complexType name="CT_ShapeNonVisual">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvSpPr" type="a:CT_NonVisualDrawingShapeProps" minOccurs="1" maxOccurs="1"
|
||||
/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Shape">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="nvSpPr" type="CT_ShapeNonVisual" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:element name="txBody" type="a:CT_TextBody" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="macro" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="textlink" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="fLocksText" type="xsd:boolean" use="optional" default="true"/>
|
||||
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_ConnectorNonVisual">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvCxnSpPr" type="a:CT_NonVisualConnectorProperties" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Connector">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="nvCxnSpPr" type="CT_ConnectorNonVisual" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="macro" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_PictureNonVisual">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvPicPr" type="a:CT_NonVisualPictureProperties" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Picture">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="nvPicPr" type="CT_PictureNonVisual" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="blipFill" type="a:CT_BlipFillProperties" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="macro" type="xsd:string" use="optional" default=""/>
|
||||
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_GraphicFrameNonVisual">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvGraphicFramePr" type="a:CT_NonVisualGraphicFrameProperties"
|
||||
minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_GraphicFrame">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="nvGraphicFramePr" type="CT_GraphicFrameNonVisual" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
<xsd:element name="xfrm" type="a:CT_Transform2D" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element ref="a:graphic" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="macro" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_GroupShapeNonVisual">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvGrpSpPr" type="a:CT_NonVisualGroupDrawingShapeProps" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_GroupShape">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="nvGrpSpPr" type="CT_GroupShapeNonVisual" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="grpSpPr" type="a:CT_GroupShapeProperties" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xsd:element name="sp" type="CT_Shape"/>
|
||||
<xsd:element name="grpSp" type="CT_GroupShape"/>
|
||||
<xsd:element name="graphicFrame" type="CT_GraphicFrame"/>
|
||||
<xsd:element name="cxnSp" type="CT_Connector"/>
|
||||
<xsd:element name="pic" type="CT_Picture"/>
|
||||
</xsd:choice>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:group name="EG_ObjectChoices">
|
||||
<xsd:sequence>
|
||||
<xsd:choice minOccurs="1" maxOccurs="1">
|
||||
<xsd:element name="sp" type="CT_Shape"/>
|
||||
<xsd:element name="grpSp" type="CT_GroupShape"/>
|
||||
<xsd:element name="graphicFrame" type="CT_GraphicFrame"/>
|
||||
<xsd:element name="cxnSp" type="CT_Connector"/>
|
||||
<xsd:element name="pic" type="CT_Picture"/>
|
||||
</xsd:choice>
|
||||
</xsd:sequence>
|
||||
</xsd:group>
|
||||
<xsd:simpleType name="ST_MarkerCoordinate">
|
||||
<xsd:restriction base="xsd:double">
|
||||
<xsd:minInclusive value="0.0"/>
|
||||
<xsd:maxInclusive value="1.0"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_Marker">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="x" type="ST_MarkerCoordinate" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="y" type="ST_MarkerCoordinate" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_RelSizeAnchor">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="from" type="CT_Marker"/>
|
||||
<xsd:element name="to" type="CT_Marker"/>
|
||||
<xsd:group ref="EG_ObjectChoices"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_AbsSizeAnchor">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="from" type="CT_Marker"/>
|
||||
<xsd:element name="ext" type="a:CT_PositiveSize2D"/>
|
||||
<xsd:group ref="EG_ObjectChoices"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:group name="EG_Anchor">
|
||||
<xsd:choice>
|
||||
<xsd:element name="relSizeAnchor" type="CT_RelSizeAnchor"/>
|
||||
<xsd:element name="absSizeAnchor" type="CT_AbsSizeAnchor"/>
|
||||
</xsd:choice>
|
||||
</xsd:group>
|
||||
<xsd:complexType name="CT_Drawing">
|
||||
<xsd:sequence>
|
||||
<xsd:group ref="EG_Anchor" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
</xsd:schema>
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user