mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-09-13 08:25:00 +00:00
Compare commits
134 Commits
accac603cf
...
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 | ||
|
|
3071bfd0f9 | ||
|
|
7bb938c145 | ||
|
|
75b21d3633 | ||
|
|
7ed9d8def4 | ||
|
|
63ed7ddb02 | ||
|
|
11696a016d | ||
|
|
5d14eb20af | ||
|
|
1a10104751 | ||
|
|
f95cb17933 | ||
|
|
0687b49542 | ||
|
|
27ad00ac3a | ||
|
|
c84d6247b0 | ||
|
|
f582f38570 | ||
|
|
13800dc389 | ||
|
|
619d9b1e84 | ||
|
|
556cc93f14 | ||
|
|
a50375616e | ||
|
|
e33447d023 | ||
|
|
7be277b3e6 | ||
|
|
5fa385e90b | ||
|
|
0a78966737 | ||
|
|
0eb7f00867 | ||
|
|
00ce7f2d98 | ||
|
|
003c066361 | ||
|
|
a5e7c59fd4 | ||
|
|
a2dbdb30ff | ||
|
|
772e4af9bf | ||
|
|
a8bd4b47a0 | ||
|
|
a59ddf6070 | ||
|
|
797ecbb054 | ||
|
|
b6b78afea9 | ||
|
|
02240f3fd0 | ||
|
|
1600ab384e | ||
|
|
418805a1ef | ||
|
|
4c4b52bca7 | ||
|
|
7245259bc4 | ||
|
|
6690c8204c | ||
|
|
d8fc597f85 | ||
|
|
5a716da5a6 | ||
|
|
a916f14efc | ||
|
|
523628ade6 | ||
|
|
2259a2f717 | ||
|
|
8df37274da | ||
|
|
393057ab24 | ||
|
|
ee8c882b6f | ||
|
|
69ec2a33a4 | ||
|
|
1cd8ae1cd9 |
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
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -23,6 +23,9 @@ target/
|
|||||||
.idea
|
.idea
|
||||||
.claude
|
.claude
|
||||||
.github
|
.github
|
||||||
|
!.github/
|
||||||
|
!.github/workflows/
|
||||||
|
!.github/workflows/**
|
||||||
*.iws
|
*.iws
|
||||||
*.iml
|
*.iml
|
||||||
*.ipr
|
*.ipr
|
||||||
@@ -44,6 +47,8 @@ nbdist/
|
|||||||
*.log.gz
|
*.log.gz
|
||||||
*.xml.versionsBackup
|
*.xml.versionsBackup
|
||||||
*.swp
|
*.swp
|
||||||
|
data/
|
||||||
|
logs/
|
||||||
|
|
||||||
!*/build/*.java
|
!*/build/*.java
|
||||||
!*/build/*.html
|
!*/build/*.html
|
||||||
@@ -51,3 +56,4 @@ nbdist/
|
|||||||
|
|
||||||
.flattened-pom.xml
|
.flattened-pom.xml
|
||||||
/.claude/settings.local.json
|
/.claude/settings.local.json
|
||||||
|
/docs/docker/milvus/volumes/
|
||||||
|
|||||||
291
README.md
291
README.md
@@ -2,11 +2,7 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[![Contributors][contributors-shield]][contributors-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]
|
||||||
[![Forks][forks-shield]][forks-url]
|
|
||||||
[![Stargazers][stars-shield]][stars-url]
|
|
||||||
[![Issues][issues-shield]][issues-url]
|
|
||||||
[![MIT License][license-shield]][license-url]
|
|
||||||
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -17,117 +13,235 @@
|
|||||||
|
|
||||||
<img src="docs/image/logo.png" alt="RuoYi AI Logo" width="120" height="120">
|
<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)** |
|
**[中文](README_ZH.md)** | **[📖 Documentation](https://doc.ruoyiai.chat/)** |
|
||||||
**[🚀 在线体验](https://web.pandarobot.chat)** | **[🐛 问题反馈](https://github.com/ageerle/ruoyi-ai/issues)** | **[💡 功能建议](https://github.com/ageerle/ruoyi-ai/issues)**
|
**[🚀 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>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## ✨ 核心亮点
|
## ✨ Core Features
|
||||||
|
|
||||||
| 模块 | 现有能力 | 扩展方向 |
|
| Module | Current Capabilities |
|
||||||
|:---:|---|---|
|
|:---:|---|
|
||||||
| **模型管理** | 多模型接入(OpenAI/DeepSeek/通义/智谱)、多模态理解、Coze/DIFY/FastGPT平台集成 | 自动模式、容错机制 |
|
| **Model Management** | Multi-model integration (DeepSeek/Zhipu/MIMO/Bailian/OpenAI), multi-modal understanding, Coze/DIFY/FastGPT/RAGFlow platform integration |
|
||||||
| **知识库** | 本地RAG + 向量库(Milvus/Weaviate) + 知识图谱 + 文档解析 +重排序 | 音频视频解析、知识出处 |
|
| **Knowledge Management** | Local RAG + Vector DB (Milvus/Weaviate/Qdrant) + Document parsing |
|
||||||
| **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态 | 工具插件市场、toolAgent自动加载工具 |
|
| **Tool Management** | MCP protocol integration, Skills capability + Extensible tool ecosystem |
|
||||||
| **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点 | 更多节点类型 |
|
| **Workflow Orchestration** | Visual workflow designer, drag-and-drop node orchestration, SSE streaming execution, currently supports model calls, email sending, manual review, and other nodes |
|
||||||
| **多智能体** | 基于Langchain4j的Agent框架、Supervisor模式编排,支持多种决策模型 | 智能体可配置 |
|
| **Multi-Agent** | Agent framework based on Langchain4j, Supervisor mode orchestration, supports multiple decision models, can flexibly combine tools and skills |
|
||||||
| **AI编程** | 智能代码分析、项目脚手架生成、Copilot助手 | 代码生成优化 |
|
|
||||||
|
|
||||||
## 🚀 快速体验
|
### Project Repositories
|
||||||
|
|
||||||
### 在线演示
|
| Module | GitHub Repository | Gitee Repository | GitCode Repository |
|
||||||
|
|
||||||
| 平台 | 地址 | 账号 |
|
|
||||||
|:------:|---|---|
|
|
||||||
| 用户端 | [web.pandarobot.chat](https://web.pandarobot.chat) | admin / admin123 |
|
|
||||||
| 管理后台 | [admin.pandarobot.chat](https://admin.pandarobot.chat) | admin / admin123 |
|
|
||||||
|
|
||||||
### 项目源码
|
|
||||||
|
|
||||||
| 项目模块 | 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) |
|
| 🔧 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) |
|
||||||
| 🎨 用户前端 | [ruoyi-web](https://github.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitee.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitcode.com/ageerle/ruoyi-web) |
|
| 🎨 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) |
|
||||||
| 🛠️ 管理后台 | [ruoyi-admin](https://github.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitee.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitcode.com/ageerle/ruoyi-admin) |
|
| 🛠️ 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) |
|
||||||
|
|
||||||
### 合作项目
|
### Partner Projects
|
||||||
| 项目名称 | GitHub 仓库 | Gitee 仓库
|
| 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
|
||||||
|
|
||||||
### 核心框架
|
### Core Framework
|
||||||
- **后端架构**:Spring Boot 4.0 + Spring ai 2.0 + Langchain4j
|
- **Backend**: Spring Boot 3.5.8 + Langchain4j
|
||||||
- **数据存储**:MySQL 8.0 + Redis + 向量数据库(Milvus/Weaviate)
|
- **Data Storage**: MySQL 8.0 + Redis + Vector Databases (Milvus/Weaviate/Qdrant)
|
||||||
- **前端技术**:Vue 3 + Vben Admin + element-plus-x
|
- **Frontend**: Vue 3 + Vben Admin + element-plus-x
|
||||||
- **安全认证**:Sa-Token + JWT 双重保障
|
- **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)
|
||||||
|
|
||||||
**👉 [完整使用文档](https://doc.pandarobot.chat)**
|
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 GHCR)
|
||||||
|
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml up -d
|
||||||
|
|
||||||
### 贡献方式
|
# Check service status
|
||||||
|
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml ps
|
||||||
|
|
||||||
1. **Fork** 项目到您的账户
|
# Access services
|
||||||
2. **创建分支** (`git checkout -b feature/新功能名称`)
|
# Admin Panel: http://localhost:25666 (admin / admin123)
|
||||||
3. **提交代码** (`git commit -m '添加某某功能'`)
|
# User Frontend: http://localhost:25137
|
||||||
4. **推送分支** (`git push origin feature/新功能名称`)
|
# Backend API: http://localhost:26039
|
||||||
5. **发起 Pull Request**
|
```
|
||||||
|
|
||||||
> 💡 **小贴士**:建议将 PR 提交到 GitHub,我们会自动同步到其他代码托管平台
|
### Method 2: Step-by-step Deployment (Source Build)
|
||||||
|
|
||||||
## 📄 开源协议
|
If you need to build backend services from source, follow these steps:
|
||||||
|
|
||||||
本项目采用 **MIT 开源协议**,详情请查看 [LICENSE](LICENSE) 文件。
|
#### Step 1: Deploy Backend Service
|
||||||
|
|
||||||
## 🙏 特别鸣谢
|
```bash
|
||||||
|
# Enter backend project directory
|
||||||
|
cd ruoyi-ai
|
||||||
|
|
||||||
感谢以下优秀的开源项目为本项目提供支持:
|
# Start backend service (build from source)
|
||||||
- [Spring AI Alibaba Copilot](https://github.com/spring-ai-alibaba/copilot) - 基于spring-ai-alibaba
|
docker-compose up -d --build
|
||||||
的智能编码助手
|
|
||||||
- [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 后台管理模板
|
|
||||||
|
|
||||||
## 🌐 生态伙伴
|
# Wait for backend service to start
|
||||||
|
docker-compose logs -f backend
|
||||||
|
```
|
||||||
|
|
||||||
- [PPIO 派欧云](https://ppinfra.com/user/register?invited_by=P8QTUY&utm_source=github_ruoyi-ai) - 提供高性价比的 GPU
|
#### Step 2: Deploy Admin Panel
|
||||||
算力和模型 API 服务
|
|
||||||
- [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_ruoyi) - 万卡RTX40系GPU+海内外主流模型API服务,秒级响应,按量计费,新客免费用。
|
|
||||||
|
|
||||||
## 优秀开源项目及社区推荐
|
```bash
|
||||||
- [imaiwork](https://gitee.com/tsinghua-open/imaiwork) - AI手机开源版,AI获客手机项目,基于无障碍模式,RPA,比豆包AI手机更强大。
|
# 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
|
||||||
|
|
||||||
|
Application images are published to GitHub Container Registry (GHCR) by GitHub Actions when a release tag such as `v3.1.0` is pushed:
|
||||||
|
|
||||||
|
```
|
||||||
|
ghcr.io/ageerle/ruoyi-ai-backend:v3.1.0
|
||||||
|
ghcr.io/ageerle/ruoyi-ai-mysql:v3.1.0
|
||||||
|
ghcr.io/ageerle/ruoyi-ai-admin:v3.1.0
|
||||||
|
ghcr.io/ageerle/ruoyi-ai-web:v3.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
The Compose file defaults to `latest`. To pin a release, create `docs/docker/ruoyi-ai/.env` with `RUIYI_VERSION=v3.1.0`.
|
||||||
|
|
||||||
|
After the first workflow run, set the four GHCR packages to `Public` in GitHub so deployment servers can pull them without logging in.
|
||||||
|
|
||||||
|
The same release tag must exist in `ageerle/ruoyi-admin` and `ageerle/ruoyi-web`; the publish workflow checks out those repositories at the backend release tag before building their images.
|
||||||
|
|
||||||
|
### Common Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stop all services
|
||||||
|
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml down
|
||||||
|
|
||||||
|
# View service logs
|
||||||
|
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml logs -f [service-name]
|
||||||
|
|
||||||
|
# Restart a service
|
||||||
|
docker compose -f docs/docker/ruoyi-ai/docker-compose-all.yaml restart [service-name]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Documentation
|
||||||
|
|
||||||
|
Want to learn more about installation, deployment, configuration, and secondary development?
|
||||||
|
|
||||||
|
**👉 [Complete Documentation](https://doc.ruoyiai.chat/)**
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
We warmly welcome community contributions! Whether you are a seasoned developer or just getting started, you can contribute to the project 💪
|
||||||
|
|
||||||
|
### How to Contribute
|
||||||
|
|
||||||
|
1. **Fork** the project to your account
|
||||||
|
2. **Create a branch** (`git checkout -b feature/new-feature-name`)
|
||||||
|
3. **Commit your changes** (`git commit -m 'Add new feature'`)
|
||||||
|
4. **Push to the branch** (`git push origin feature/new-feature-name`)
|
||||||
|
5. **Create a Pull Request**
|
||||||
|
|
||||||
|
> 💡 **Tip**: We recommend submitting PRs to GitHub, we will automatically sync to other code hosting platforms
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## 🙏 Acknowledgments
|
||||||
|
|
||||||
|
Thanks to the following excellent open-source projects for their support:
|
||||||
|
- [Langchain4j](https://github.com/langchain4j/langchain4j) - Powerful Java LLM development framework
|
||||||
|
- [RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus) - Mature enterprise-level rapid development framework
|
||||||
|
- [Vben Admin](https://github.com/vbenjs/vue-vben-admin) - Modern Vue admin template
|
||||||
|
|
||||||
|
## 💎 Sponsors
|
||||||
|
|
||||||
|
**Thanks to the following sponsors for supporting this project:**
|
||||||
|
|
||||||
|
<a href="https://www.atlascloud.ai?ref=89F97E">
|
||||||
|
<img src="docs/image/sponsor/atlascloud_banner.png" alt="Atlas Cloud" width="160" height="80">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
[Visit Atlas Cloud](https://www.atlascloud.ai?ref=89F97E&utm_source=github&utm_campaign=ruoyi-drama) · [Coding Plan Promotion](https://www.atlascloud.ai/console/coding-plan)
|
||||||
|
A full-modal AI inference platform that gives developers a unified AI API, supporting video generation, image generation, and LLMs. Connect once to access **300+ curated models**.
|
||||||
|
|
||||||
|
<a href="https://www.volcengine.com/activity/codingplan?utm_campaign=hw&utm_content=hw&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ageerle-ruoyi-ai">
|
||||||
|
<img src="docs/image/sponsor/huoshan.png" alt="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">
|
<div align="center">
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<img src="docs/image/wx.png" alt="微信二维码" width="200" height="200"><br>
|
<img src="docs/image/wx.png" alt="WeChat QR Code" width="200" height="200"><br>
|
||||||
<strong>扫码添加作者微信</strong><br>
|
<strong>Scan to add author on WeChat</strong><br>
|
||||||
<em>邀请进群学习</em>
|
<em>Join group for learning</em>
|
||||||
</td>
|
</td>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<img src="docs/image/qq.png" alt="QQ群二维码" width="200" height="200"><br>
|
<img src="docs/image/wx06.png" alt="WeChat QR Code" width="200" height="200"><br>
|
||||||
<strong>QQ技术交流群</strong><br>
|
<strong>WeChat Tech Exchange Group</strong><br>
|
||||||
<em>技术讨论</em>
|
<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>
|
</td>
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
@@ -137,34 +251,11 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
## 📺 视频教程
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<table>
|
**[⭐ 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/)**
|
||||||
<tr>
|
|
||||||
<td align="center">
|
|
||||||
<img src="docs/image/dy.png" alt="微信二维码" width="200" height="200"><br>
|
|
||||||
<strong>打开抖音扫一扫</strong><br>
|
|
||||||
<em>获取免费视频教程</em>
|
|
||||||
</td>
|
|
||||||
<td align="center">
|
|
||||||
<img src="docs/image/bibi.png" alt="QQ群二维码" width="200" height="200"><br>
|
|
||||||
<strong>打开B站扫一扫</strong><br>
|
|
||||||
<em>获取免费视频教程</em>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
</tr>
|
*Built with ❤️, maintained by the RuoYi AI open-source community*
|
||||||
</table>
|
|
||||||
|
|
||||||
</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)**
|
|
||||||
|
|
||||||
*用 ❤️ 打造,由 RuoYi AI 开源社区维护*
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
153
README_EN.md
153
README_EN.md
@@ -1,153 +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 | Extension Direction |
|
|
||||||
|:---:|---|---|
|
|
||||||
| **Model Management** | Multi-model integration (OpenAI/DeepSeek/Tongyi/Zhipu), multi-modal understanding, Coze/DIFY/FastGPT platform integration | Auto mode, fault tolerance |
|
|
||||||
| **Knowledge Base** | Local RAG + Vector DB (Milvus/Weaviate) + Knowledge Graph + Document parsing + Reranking | Audio/video parsing, knowledge source |
|
|
||||||
| **Tool Management** | MCP protocol integration, Skills capability + Extensible tool ecosystem | Tool plugin marketplace, toolAgent auto-loading |
|
|
||||||
| **Workflow Orchestration** | Visual workflow designer, drag-and-drop node orchestration, SSE streaming execution, currently supports model (with RAG) calls, email sending, manual review nodes | More node types |
|
|
||||||
| **Multi-Agent** | Agent framework based on Langchain4j, Supervisor mode orchestration, supports multiple decision models | Configurable agents |
|
|
||||||
| **AI Coding** | Intelligent code analysis, project scaffolding generation, Copilot assistant | Code generation optimization |
|
|
||||||
|
|
||||||
## 🚀 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)
|
|
||||||
- **Frontend**: Vue 3 + Vben Admin + element-plus-x
|
|
||||||
- **Security**: Sa-Token + JWT dual-layer security
|
|
||||||
|
|
||||||
|
|
||||||
## 📚 Documentation
|
|
||||||
|
|
||||||
Want to learn more about installation, deployment, configuration, and secondary development?
|
|
||||||
|
|
||||||
**👉 [Complete Documentation](https://doc.pandarobot.chat)**
|
|
||||||
|
|
||||||
## 🤝 Contributing
|
|
||||||
|
|
||||||
We warmly welcome community contributions! Whether you are a seasoned developer or just getting started, you can contribute to the project 💪
|
|
||||||
|
|
||||||
### How to Contribute
|
|
||||||
|
|
||||||
1. **Fork** the project to your account
|
|
||||||
2. **Create a branch** (`git checkout -b feature/new-feature-name`)
|
|
||||||
3. **Commit your changes** (`git commit -m 'Add new feature'`)
|
|
||||||
4. **Push to the branch** (`git push origin feature/new-feature-name`)
|
|
||||||
5. **Create a Pull Request**
|
|
||||||
|
|
||||||
> 💡 **Tip**: We recommend submitting PRs to GitHub, we will automatically sync to other code hosting platforms
|
|
||||||
|
|
||||||
## 📄 License
|
|
||||||
|
|
||||||
This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details.
|
|
||||||
|
|
||||||
## 🙏 Acknowledgments
|
|
||||||
|
|
||||||
Thanks to the following excellent open-source projects for their support:
|
|
||||||
- [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.
|
|
||||||
|
|
||||||
## Outstanding Open-Source Projects and Community Recommendations
|
|
||||||
- [imaiwork](https://gitee.com/tsinghua-open/imaiwork) - Open-source AI phone, AI customer acquisition phone project, based on accessibility mode and RPA, more powerful than Doubao AI phone.
|
|
||||||
|
|
||||||
## 💬 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:
|
services:
|
||||||
etcd:
|
etcd:
|
||||||
container_name: milvus-etcd
|
container_name: ruoyi-rag-milvus-etcd
|
||||||
image: quay.io/coreos/etcd:v3.5.18
|
image: quay.io/coreos/etcd:v3.5.18
|
||||||
environment:
|
environment:
|
||||||
- ETCD_AUTO_COMPACTION_MODE=revision
|
- ETCD_AUTO_COMPACTION_MODE=revision
|
||||||
@@ -19,14 +17,11 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
|
|
||||||
minio:
|
minio:
|
||||||
container_name: milvus-minio
|
container_name: ruoyi-rag-milvus-minio
|
||||||
image: minio/minio:RELEASE.2023-03-20T20-16-18Z
|
image: minio/minio:RELEASE.2023-03-20T20-16-18Z
|
||||||
environment:
|
environment:
|
||||||
MINIO_ACCESS_KEY: minioadmin
|
MINIO_ACCESS_KEY: minioadmin
|
||||||
MINIO_SECRET_KEY: minioadmin
|
MINIO_SECRET_KEY: minioadmin
|
||||||
ports:
|
|
||||||
- "9001:9001"
|
|
||||||
- "9000:9000"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data
|
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data
|
||||||
command: minio server /minio_data --console-address ":9001"
|
command: minio server /minio_data --console-address ":9001"
|
||||||
@@ -37,7 +32,7 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
|
|
||||||
standalone:
|
standalone:
|
||||||
container_name: milvus-standalone
|
container_name: ruoyi-rag-milvus
|
||||||
image: milvusdb/milvus:v2.5.7
|
image: milvusdb/milvus:v2.5.7
|
||||||
command: ["milvus", "run", "standalone"]
|
command: ["milvus", "run", "standalone"]
|
||||||
security_opt:
|
security_opt:
|
||||||
@@ -61,7 +56,7 @@ services:
|
|||||||
- "minio"
|
- "minio"
|
||||||
|
|
||||||
attu:
|
attu:
|
||||||
container_name: attu
|
container_name: ruoyi-rag-attu
|
||||||
image: zilliz/attu:v2.5.7
|
image: zilliz/attu:v2.5.7
|
||||||
environment:
|
environment:
|
||||||
MILVUS_URL: milvus-standalone:19530
|
MILVUS_URL: milvus-standalone:19530
|
||||||
@@ -72,4 +67,4 @@ services:
|
|||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
name: milvus
|
name: ruoyi-rag-milvus
|
||||||
|
|||||||
20
docs/docker/qdrant/docker-compose.yml
Normal file
20
docs/docker/qdrant/docker-compose.yml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
services:
|
||||||
|
qdrant:
|
||||||
|
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
|
||||||
52
docs/docker/ruoyi-ai/Dockerfile.backend
Normal file
52
docs/docker/ruoyi-ai/Dockerfile.backend
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# RuoYi-AI 后端 Dockerfile
|
||||||
|
# 基于 Maven + OpenJDK 17
|
||||||
|
|
||||||
|
FROM maven:3.9-eclipse-temurin-17 AS builder
|
||||||
|
|
||||||
|
# 设置工作目录
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# 复制 pom.xml 和源码
|
||||||
|
COPY pom.xml .
|
||||||
|
COPY ruoyi-admin ./ruoyi-admin
|
||||||
|
COPY ruoyi-common ./ruoyi-common
|
||||||
|
COPY ruoyi-modules ./ruoyi-modules
|
||||||
|
COPY ruoyi-extend ./ruoyi-extend
|
||||||
|
|
||||||
|
|
||||||
|
# 构建项目 (使用 prod profile)
|
||||||
|
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
|
||||||
|
|
||||||
|
# 从构建阶段复制 jar 包
|
||||||
|
COPY --from=builder /build/ruoyi-admin/target/ruoyi-admin.jar ./ruoyi-admin.jar
|
||||||
|
|
||||||
|
# 创建日志目录
|
||||||
|
RUN mkdir -p /ruoyi/server/logs /ruoyi/server/temp
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 6039
|
||||||
|
|
||||||
|
# 启动命令
|
||||||
|
ENTRYPOINT ["java", "-Djava.io.tmpdir=/ruoyi/server/temp", "-jar", "ruoyi-admin.jar", "--spring.profiles.active=prod"]
|
||||||
19
docs/docker/ruoyi-ai/Dockerfile.mysql
Normal file
19
docs/docker/ruoyi-ai/Dockerfile.mysql
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# 基于官方MySQL 8.0镜像构建自定义镜像
|
||||||
|
# 构建命令: docker build -t registry.cn-hangzhou.aliyuncs.com/ruoyi-ai/mysql:v3 -f Dockerfile.mysql .
|
||||||
|
FROM mysql:8.0
|
||||||
|
|
||||||
|
# 设置时区
|
||||||
|
ENV TZ=Asia/Shanghai
|
||||||
|
|
||||||
|
# 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", \
|
||||||
|
"--character-set-server=utf8mb4", \
|
||||||
|
"--collation-server=utf8mb4_general_ci", \
|
||||||
|
"--explicit_defaults_for_timestamp=true", \
|
||||||
|
"--lower_case_table_names=1", \
|
||||||
|
"--skip-ssl"]
|
||||||
181
docs/docker/ruoyi-ai/docker-compose-all.yaml
Normal file
181
docs/docker/ruoyi-ai/docker-compose-all.yaml
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
# RuoYi-AI 一键启动全部服务
|
||||||
|
# 使用方式: docker-compose up -d
|
||||||
|
#
|
||||||
|
# 包含服务:
|
||||||
|
# - MySQL 8.0 (数据库,包含初始化SQL)
|
||||||
|
# - Redis 6.2 (缓存)
|
||||||
|
# - Weaviate (向量数据库)
|
||||||
|
# - MinIO (对象存储)
|
||||||
|
# - RuoYi-Backend (后端服务)
|
||||||
|
# - RuoYi-Admin (管理端前端)
|
||||||
|
# - RuoYi-Web (用户端前端)
|
||||||
|
#
|
||||||
|
# 镜像仓库地址: ghcr.io/ageerle
|
||||||
|
|
||||||
|
services:
|
||||||
|
# ==================== MySQL 数据库 ====================
|
||||||
|
mysql:
|
||||||
|
# GHCR 镜像(包含初始化SQL)
|
||||||
|
image: ghcr.io/${IMAGE_OWNER:-ageerle}/ruoyi-ai-mysql:${RUIYI_VERSION:-latest}
|
||||||
|
container_name: ruoyi-ai-mysql
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "23306:3306"
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: root
|
||||||
|
MYSQL_DATABASE: ruoyi-ai-agent
|
||||||
|
TZ: Asia/Shanghai
|
||||||
|
volumes:
|
||||||
|
- mysql-data:/var/lib/mysql
|
||||||
|
healthcheck:
|
||||||
|
# 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
|
||||||
|
start_period: 60s
|
||||||
|
networks:
|
||||||
|
- ruoyi-net
|
||||||
|
|
||||||
|
# ==================== Redis 缓存 ====================
|
||||||
|
redis:
|
||||||
|
image: redis:6.2
|
||||||
|
container_name: ruoyi-ai-redis
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "26379:6379"
|
||||||
|
volumes:
|
||||||
|
- redis-data:/data
|
||||||
|
command: redis-server --appendonly yes
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- ruoyi-net
|
||||||
|
|
||||||
|
# ==================== Weaviate 向量数据库 ====================
|
||||||
|
weaviate:
|
||||||
|
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"
|
||||||
|
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
|
||||||
|
CLUSTER_HOSTNAME: node1
|
||||||
|
volumes:
|
||||||
|
- weaviate-data:/var/lib/weaviate
|
||||||
|
networks:
|
||||||
|
- ruoyi-net
|
||||||
|
|
||||||
|
# ==================== MinIO 对象存储 ====================
|
||||||
|
minio:
|
||||||
|
image: minio/minio:latest
|
||||||
|
container_name: ruoyi-ai-minio
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "29000:9000"
|
||||||
|
- "29090:9090"
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: ruoyi
|
||||||
|
MINIO_ROOT_PASSWORD: ruoyi123
|
||||||
|
volumes:
|
||||||
|
- minio-data:/data
|
||||||
|
command: server /data --console-address ":9090"
|
||||||
|
networks:
|
||||||
|
- ruoyi-net
|
||||||
|
|
||||||
|
# ==================== RuoYi-AI 后端服务 ====================
|
||||||
|
backend:
|
||||||
|
image: ghcr.io/${IMAGE_OWNER:-ageerle}/ruoyi-ai-backend:${RUIYI_VERSION:-latest}
|
||||||
|
container_name: ruoyi-ai-backend
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "26039:6039"
|
||||||
|
environment:
|
||||||
|
TZ: Asia/Shanghai
|
||||||
|
# MySQL 配置
|
||||||
|
SPRING_DATASOURCE_DYNAMIC_PRIMARY: master
|
||||||
|
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_DRIVERCLASSNAME: com.mysql.cj.jdbc.Driver
|
||||||
|
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_URL: jdbc:mysql://mysql:3306/ruoyi-ai-agent?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
|
||||||
|
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_USERNAME: root
|
||||||
|
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_PASSWORD: root
|
||||||
|
# Redis 配置
|
||||||
|
SPRING_DATA_REDIS_HOST: redis
|
||||||
|
SPRING_DATA_REDIS_PORT: 6379
|
||||||
|
SPRING_DATA_REDIS_DATABASE: 0
|
||||||
|
# 日志配置
|
||||||
|
LOGGING_LEVEL_ORG_RUOYI: info
|
||||||
|
LOGGING_LEVEL_ORG_SPRINGFRAMEWORK: warn
|
||||||
|
SYS_UPLOAD_PATH: /ruoyi/upload
|
||||||
|
volumes:
|
||||||
|
- logs-data:/ruoyi/server/logs
|
||||||
|
- upload-data:/ruoyi/upload
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_started
|
||||||
|
networks:
|
||||||
|
- ruoyi-net
|
||||||
|
|
||||||
|
# ==================== RuoYi-AI 管理端前端 ====================
|
||||||
|
admin-frontend:
|
||||||
|
image: ghcr.io/${IMAGE_OWNER:-ageerle}/ruoyi-ai-admin:${RUIYI_VERSION:-latest}
|
||||||
|
container_name: ruoyi-ai-admin
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "25666:5666"
|
||||||
|
environment:
|
||||||
|
# 后端 API 地址 - 运行时动态配置(无需重新构建镜像)
|
||||||
|
# nginx upstream 配置不需要 http:// 前缀,直接使用 host:port
|
||||||
|
UPSTREAM_HOST: backend:6039
|
||||||
|
# 资源限制 - 防止 CPU 和内存耗尽
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '2'
|
||||||
|
memory: 3G
|
||||||
|
reservations:
|
||||||
|
cpus: '1'
|
||||||
|
memory: 1G
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
networks:
|
||||||
|
- ruoyi-net
|
||||||
|
|
||||||
|
# ==================== RuoYi-AI 用户端前端 ====================
|
||||||
|
web-frontend:
|
||||||
|
image: ghcr.io/${IMAGE_OWNER:-ageerle}/ruoyi-ai-web:${RUIYI_VERSION:-latest}
|
||||||
|
container_name: ruoyi-ai-web
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "25137:5137"
|
||||||
|
environment:
|
||||||
|
UPSTREAM_URL: http://backend:6039
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
networks:
|
||||||
|
- ruoyi-net
|
||||||
|
|
||||||
|
# ==================== 网络配置 ====================
|
||||||
|
networks:
|
||||||
|
ruoyi-net:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
# ==================== 数据卷配置 ====================
|
||||||
|
volumes:
|
||||||
|
mysql-data:
|
||||||
|
redis-data:
|
||||||
|
weaviate-data:
|
||||||
|
minio-data:
|
||||||
|
logs-data:
|
||||||
|
upload-data:
|
||||||
146
docs/docker/ruoyi-ai/docker-compose.yaml
Normal file
146
docs/docker/ruoyi-ai/docker-compose.yaml
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
# RuoYi-AI 一键启动后端服务
|
||||||
|
# 使用方式: docker-compose up -d --build
|
||||||
|
#
|
||||||
|
# 包含服务:
|
||||||
|
# - MySQL 8.0 (数据库)
|
||||||
|
# - Redis 6.2 (缓存)
|
||||||
|
# - Weaviate (向量数据库)
|
||||||
|
# - MinIO (对象存储)
|
||||||
|
# - RuoYi-Backend (后端服务,源码编译)
|
||||||
|
|
||||||
|
services:
|
||||||
|
# MySQL 数据库
|
||||||
|
mysql:
|
||||||
|
image: mysql:8.0.33
|
||||||
|
container_name: ruoyi-ai-mysql
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "23306:3306"
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: root
|
||||||
|
MYSQL_DATABASE: ruoyi-ai-agent
|
||||||
|
TZ: Asia/Shanghai
|
||||||
|
volumes:
|
||||||
|
- ../../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
|
||||||
|
--character-set-server=utf8mb4
|
||||||
|
--collation-server=utf8mb4_general_ci
|
||||||
|
--explicit_defaults_for_timestamp=true
|
||||||
|
--lower_case_table_names=1
|
||||||
|
--skip-ssl
|
||||||
|
healthcheck:
|
||||||
|
# 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
|
||||||
|
start_period: 60s
|
||||||
|
networks:
|
||||||
|
- ruoyi-net
|
||||||
|
|
||||||
|
# Redis 缓存
|
||||||
|
redis:
|
||||||
|
image: redis:6.2
|
||||||
|
container_name: ruoyi-ai-redis
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
volumes:
|
||||||
|
- redis-data:/data
|
||||||
|
command: redis-server --appendonly yes
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- ruoyi-net
|
||||||
|
|
||||||
|
# Weaviate 向量数据库
|
||||||
|
weaviate:
|
||||||
|
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
|
||||||
|
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
|
||||||
|
CLUSTER_HOSTNAME: node1
|
||||||
|
volumes:
|
||||||
|
- weaviate-data:/var/lib/weaviate
|
||||||
|
networks:
|
||||||
|
- ruoyi-net
|
||||||
|
|
||||||
|
# MinIO 对象存储
|
||||||
|
minio:
|
||||||
|
image: minio/minio
|
||||||
|
container_name: ruoyi-ai-minio
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "9000:9000"
|
||||||
|
- "9090:9090"
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: ruoyi
|
||||||
|
MINIO_ROOT_PASSWORD: ruoyi123
|
||||||
|
volumes:
|
||||||
|
- minio-data:/data
|
||||||
|
command: server /data --console-address ":9090"
|
||||||
|
networks:
|
||||||
|
- ruoyi-net
|
||||||
|
|
||||||
|
# RuoYi-AI 后端服务 (源码编译)
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.backend
|
||||||
|
container_name: ruoyi-ai-backend
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "26039:6039"
|
||||||
|
environment:
|
||||||
|
TZ: Asia/Shanghai
|
||||||
|
# MySQL 配置
|
||||||
|
SPRING_DATASOURCE_DYNAMIC_PRIMARY: master
|
||||||
|
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_DRIVERCLASSNAME: com.mysql.cj.jdbc.Driver
|
||||||
|
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_URL: jdbc:mysql://mysql:3306/ruoyi-ai-agent?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
|
||||||
|
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_USERNAME: root
|
||||||
|
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_PASSWORD: root
|
||||||
|
# Redis 配置
|
||||||
|
SPRING_DATA_REDIS_HOST: redis
|
||||||
|
SPRING_DATA_REDIS_PORT: 6379
|
||||||
|
SPRING_DATA_REDIS_DATABASE: 0
|
||||||
|
# 日志配置
|
||||||
|
LOGGING_LEVEL_ORG_RUOYI: info
|
||||||
|
LOGGING_LEVEL_ORG_SPRINGFRAMEWORK: warn
|
||||||
|
SYS_UPLOAD_PATH: /ruoyi/upload # 新增:对应 sys.upload.path
|
||||||
|
volumes:
|
||||||
|
- logs-data:/ruoyi/server/logs
|
||||||
|
- upload-data:/ruoyi/upload
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_started
|
||||||
|
networks:
|
||||||
|
- ruoyi-net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
ruoyi-net:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
# 数据卷 支持手动指定 空为默认值
|
||||||
|
volumes:
|
||||||
|
mysql-data:
|
||||||
|
redis-data:
|
||||||
|
weaviate-data:
|
||||||
|
minio-data:
|
||||||
|
logs-data:
|
||||||
|
upload-data:
|
||||||
@@ -5,12 +5,12 @@ services:
|
|||||||
- --host
|
- --host
|
||||||
- 0.0.0.0
|
- 0.0.0.0
|
||||||
- --port
|
- --port
|
||||||
- '6038'
|
- '8080'
|
||||||
- --scheme
|
- --scheme
|
||||||
- http
|
- http
|
||||||
image: semitechnologies/weaviate:1.19.7
|
image: semitechnologies/weaviate:1.30.0
|
||||||
ports:
|
ports:
|
||||||
- 6038:6038
|
- 28080:8080
|
||||||
- 50051:50051
|
- 50051:50051
|
||||||
volumes:
|
volumes:
|
||||||
- weaviate_data:/var/lib/weaviate
|
- 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 |
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(), '';
|
||||||
84
pom.xml
84
pom.xml
@@ -13,7 +13,7 @@
|
|||||||
<description>全栈式AI开发平台</description>
|
<description>全栈式AI开发平台</description>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<revision>3.0.0</revision>
|
<revision>3.1.0</revision>
|
||||||
<spring-boot.version>3.5.8</spring-boot.version>
|
<spring-boot.version>3.5.8</spring-boot.version>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
@@ -43,8 +43,6 @@
|
|||||||
<aws.sdk.version>2.28.22</aws.sdk.version>
|
<aws.sdk.version>2.28.22</aws.sdk.version>
|
||||||
<!-- SMS 配置 -->
|
<!-- SMS 配置 -->
|
||||||
<sms4j.version>3.3.5</sms4j.version>
|
<sms4j.version>3.3.5</sms4j.version>
|
||||||
<!-- 限制框架中的fastjson版本 -->
|
|
||||||
<fastjson.version>1.2.83</fastjson.version>
|
|
||||||
<!-- 面向运行时的D-ORM依赖 -->
|
<!-- 面向运行时的D-ORM依赖 -->
|
||||||
<anyline.version>8.7.2-20250603</anyline.version>
|
<anyline.version>8.7.2-20250603</anyline.version>
|
||||||
<!-- 工作流配置 -->
|
<!-- 工作流配置 -->
|
||||||
@@ -54,16 +52,25 @@
|
|||||||
<!-- Jackson XML -->
|
<!-- Jackson XML -->
|
||||||
<jackson-dataformat-xml.version>2.18.2</jackson-dataformat-xml.version>
|
<jackson-dataformat-xml.version>2.18.2</jackson-dataformat-xml.version>
|
||||||
<!-- AI 相关依赖 -->
|
<!-- AI 相关依赖 -->
|
||||||
<langchain4j.version>1.11.0</langchain4j.version>
|
|
||||||
<langchain4j.community.version>1.11.0-beta19</langchain4j.community.version>
|
<langchain4j.version>1.17.2</langchain4j.version>
|
||||||
<langgraph4j.version>1.5.3</langgraph4j.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>
|
<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格式 -->
|
<!-- Apache Commons Compress - 用于POI处理ZIP格式 -->
|
||||||
<commons-compress.version>1.27.1</commons-compress.version>
|
<commons-compress.version>1.27.1</commons-compress.version>
|
||||||
|
|
||||||
|
|
||||||
<avatar-generator.version>1.1.0</avatar-generator.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>
|
<jsoup.version>1.21.2</jsoup.version>
|
||||||
<knife4j.version>4.4.0</knife4j.version>
|
<knife4j.version>4.4.0</knife4j.version>
|
||||||
<swagger-annotations.version>2.2.8</swagger-annotations.version>
|
<swagger-annotations.version>2.2.8</swagger-annotations.version>
|
||||||
@@ -77,7 +84,7 @@
|
|||||||
<maven-surefire-plugin.version>3.5.3</maven-surefire-plugin.version>
|
<maven-surefire-plugin.version>3.5.3</maven-surefire-plugin.version>
|
||||||
<flatten-maven-plugin.version>1.3.0</flatten-maven-plugin.version>
|
<flatten-maven-plugin.version>1.3.0</flatten-maven-plugin.version>
|
||||||
<!-- 打包默认跳过测试 -->
|
<!-- 打包默认跳过测试 -->
|
||||||
<skipTests>true</skipTests>
|
<skipTests>false</skipTests>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<profiles>
|
<profiles>
|
||||||
@@ -129,6 +136,15 @@
|
|||||||
<scope>import</scope>
|
<scope>import</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- gRPC BOM - 解决 Milvus SDK 依赖冲突,强制统一版本 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.grpc</groupId>
|
||||||
|
<artifactId>grpc-bom</artifactId>
|
||||||
|
<version>${grpc.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- hutool 的依赖配置-->
|
<!-- hutool 的依赖配置-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
@@ -331,6 +347,12 @@
|
|||||||
<groupId>me.zhyd.oauth</groupId>
|
<groupId>me.zhyd.oauth</groupId>
|
||||||
<artifactId>JustAuth</artifactId>
|
<artifactId>JustAuth</artifactId>
|
||||||
<version>${justauth.version}</version>
|
<version>${justauth.version}</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>com.alibaba</groupId>
|
||||||
|
<artifactId>fastjson</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 离线IP地址定位库 ip2region -->
|
<!-- 离线IP地址定位库 ip2region -->
|
||||||
@@ -340,36 +362,18 @@
|
|||||||
<version>${ip2region.version}</version>
|
<version>${ip2region.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.alibaba</groupId>
|
|
||||||
<artifactId>fastjson</artifactId>
|
|
||||||
<version>${fastjson.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.ruoyi</groupId>
|
<groupId>org.ruoyi</groupId>
|
||||||
<artifactId>ruoyi-system</artifactId>
|
<artifactId>ruoyi-system</artifactId>
|
||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.ruoyi</groupId>
|
|
||||||
<artifactId>ruoyi-job</artifactId>
|
|
||||||
<version>${revision}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.ruoyi</groupId>
|
<groupId>org.ruoyi</groupId>
|
||||||
<artifactId>ruoyi-generator</artifactId>
|
<artifactId>ruoyi-generator</artifactId>
|
||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.ruoyi</groupId>
|
|
||||||
<artifactId>ruoyi-demo</artifactId>
|
|
||||||
<version>${revision}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.ruoyi</groupId>
|
<groupId>org.ruoyi</groupId>
|
||||||
<artifactId>ruoyi-chat</artifactId>
|
<artifactId>ruoyi-chat</artifactId>
|
||||||
@@ -383,13 +387,6 @@
|
|||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 微信模块 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.ruoyi</groupId>
|
|
||||||
<artifactId>ruoyi-wechat</artifactId>
|
|
||||||
<version>${revision}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- AI流程编排模块 -->
|
<!-- AI流程编排模块 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.ruoyi</groupId>
|
<groupId>org.ruoyi</groupId>
|
||||||
@@ -397,13 +394,6 @@
|
|||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 企业微信SDK -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.github.binarywang</groupId>
|
|
||||||
<artifactId>weixin-java-cp</artifactId>
|
|
||||||
<version>${weixin-java-cp.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- Jackson XML -->
|
<!-- Jackson XML -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||||
@@ -514,20 +504,8 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<resource>
|
<resource>
|
||||||
<directory>src/main/resources</directory>
|
<directory>src/main/resources</directory>
|
||||||
<!-- 关闭过滤 -->
|
|
||||||
<filtering>false</filtering>
|
<filtering>false</filtering>
|
||||||
</resource>
|
</resource>
|
||||||
<resource>
|
|
||||||
<directory>src/main/resources</directory>
|
|
||||||
<!-- 引入所有 匹配文件进行过滤 -->
|
|
||||||
<includes>
|
|
||||||
<include>application*</include>
|
|
||||||
<include>bootstrap*</include>
|
|
||||||
<include>banner*</include>
|
|
||||||
</includes>
|
|
||||||
<!-- 启用过滤 即该资源中的变量将会被过滤器中的值替换 -->
|
|
||||||
<filtering>true</filtering>
|
|
||||||
</resource>
|
|
||||||
</resources>
|
</resources>
|
||||||
</build>
|
</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:17.0.16-cds
|
||||||
#FROM bellsoft/liberica-openjdk-rocky:21.0.8-cds
|
|
||||||
#FROM findepi/graalvm:java17-native
|
|
||||||
|
|
||||||
LABEL maintainer="Lion Li"
|
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 \
|
RUN mkdir -p /ruoyi/server/logs \
|
||||||
/ruoyi/server/temp \
|
/ruoyi/server/temp \
|
||||||
/ruoyi/skywalking/agent
|
/ruoyi/skywalking/agent
|
||||||
|
|
||||||
WORKDIR /ruoyi/server
|
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}
|
EXPOSE ${SERVER_PORT}
|
||||||
# 暴露 snail job 客户端端口 用于定时任务调度中心通信
|
|
||||||
EXPOSE ${SNAIL_PORT}
|
EXPOSE ${SNAIL_PORT}
|
||||||
|
|
||||||
ADD ./target/ruoyi-admin.jar ./app.jar
|
ADD ./target/ruoyi-admin.jar ./app.jar
|
||||||
|
|
||||||
SHELL ["/bin/bash", "-c"]
|
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} \
|
-Dsnail-job.port=${SNAIL_PORT} \
|
||||||
# 应用名称 如果想区分集群节点监控 改成不同的名称即可
|
|
||||||
#-Dskywalking.agent.service_name=ruoyi-server \
|
|
||||||
#-javaagent:/ruoyi/skywalking/agent/skywalking-agent.jar \
|
|
||||||
-XX:+HeapDumpOnOutOfMemoryError -XX:+UseZGC ${JAVA_OPTS} \
|
-XX:+HeapDumpOnOutOfMemoryError -XX:+UseZGC ${JAVA_OPTS} \
|
||||||
-jar app.jar
|
-jar app.jar
|
||||||
|
|
||||||
|
|||||||
@@ -70,23 +70,12 @@
|
|||||||
<artifactId>ruoyi-system</artifactId>
|
<artifactId>ruoyi-system</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.ruoyi</groupId>
|
|
||||||
<artifactId>ruoyi-job</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- 代码生成-->
|
<!-- 代码生成-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.ruoyi</groupId>
|
<groupId>org.ruoyi</groupId>
|
||||||
<artifactId>ruoyi-generator</artifactId>
|
<artifactId>ruoyi-generator</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- demo模块 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.ruoyi</groupId>
|
|
||||||
<artifactId>ruoyi-demo</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.ruoyi</groupId>
|
<groupId>org.ruoyi</groupId>
|
||||||
<artifactId>ruoyi-chat</artifactId>
|
<artifactId>ruoyi-chat</artifactId>
|
||||||
@@ -98,12 +87,6 @@
|
|||||||
<artifactId>ruoyi-workflow</artifactId>
|
<artifactId>ruoyi-workflow</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 微信模块 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.ruoyi</groupId>
|
|
||||||
<artifactId>ruoyi-wechat</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- AI流程编排模块 -->
|
<!-- AI流程编排模块 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.ruoyi</groupId>
|
<groupId>org.ruoyi</groupId>
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import org.springframework.boot.SpringApplication;
|
|||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
|
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 class RuoYiAIApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
killPortProcess(6039);
|
||||||
SpringApplication application = new SpringApplication(RuoYiAIApplication.class);
|
SpringApplication application = new SpringApplication(RuoYiAIApplication.class);
|
||||||
application.setApplicationStartup(new BufferingApplicationStartup(2048));
|
application.setApplicationStartup(new BufferingApplicationStartup(2048));
|
||||||
application.run(args);
|
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:
|
metadata:
|
||||||
username: ${spring.boot.admin.client.username}
|
username: ${spring.boot.admin.client.username}
|
||||||
userpassword: ${spring.boot.admin.client.password}
|
userpassword: ${spring.boot.admin.client.password}
|
||||||
username: @monitor.username@
|
username: ${MONITOR_USERNAME:ruoyi}
|
||||||
password: @monitor.password@
|
password: ${MONITOR_PASSWORD:123456}
|
||||||
|
|
||||||
--- # mcp配置信息
|
--- # mcp配置信息
|
||||||
mcp:
|
mcp:
|
||||||
@@ -27,12 +27,12 @@ snail-job:
|
|||||||
enabled: false
|
enabled: false
|
||||||
# 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务
|
# 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务
|
||||||
group: "ruoyi_group"
|
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"
|
token: "SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT"
|
||||||
server:
|
server:
|
||||||
host: 127.0.0.1
|
host: 127.0.0.1
|
||||||
port: 17888
|
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}
|
namespace: ${spring.profiles.active}
|
||||||
# 随主应用端口漂移
|
# 随主应用端口漂移
|
||||||
port: 2${server.port}
|
port: 2${server.port}
|
||||||
|
|||||||
272
ruoyi-admin/src/main/resources/application-prod.yml
Normal file
272
ruoyi-admin/src/main/resources/application-prod.yml
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
|
||||||
|
--- # 监控中心配置
|
||||||
|
spring.boot.admin.client:
|
||||||
|
# 增加客户端开关
|
||||||
|
enabled: false
|
||||||
|
url: http://localhost:9090/admin
|
||||||
|
instance:
|
||||||
|
service-host-type: IP
|
||||||
|
metadata:
|
||||||
|
username: ${spring.boot.admin.client.username}
|
||||||
|
userpassword: ${spring.boot.admin.client.password}
|
||||||
|
username: ${MONITOR_USERNAME:ruoyi}
|
||||||
|
password: ${MONITOR_PASSWORD:123456}
|
||||||
|
|
||||||
|
--- # mcp配置信息
|
||||||
|
mcp:
|
||||||
|
sse:
|
||||||
|
enabled: false
|
||||||
|
url: http://localhost:8085/sse
|
||||||
|
|
||||||
|
--- # 上传文件地址
|
||||||
|
sys:
|
||||||
|
upload:
|
||||||
|
path: D:\\DownLoad
|
||||||
|
|
||||||
|
--- # snail-job 配置
|
||||||
|
snail-job:
|
||||||
|
enabled: false
|
||||||
|
# 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务
|
||||||
|
group: "ruoyi_group"
|
||||||
|
# SnailJob 接入验证令牌 详见 docs/script/sql/ruoyi-ai-v3_mysql8.sql `sj_group_config` 表
|
||||||
|
token: "SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT"
|
||||||
|
server:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 17888
|
||||||
|
# 命名空间UUID 详见 docs/script/sql/ruoyi-ai-v3_mysql8.sql `sj_namespace`表`unique_id`字段
|
||||||
|
namespace: ${spring.profiles.active}
|
||||||
|
# 随主应用端口漂移
|
||||||
|
port: 2${server.port}
|
||||||
|
# 客户端ip指定
|
||||||
|
host:
|
||||||
|
|
||||||
|
--- # 数据源配置
|
||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
type: com.zaxxer.hikari.HikariDataSource
|
||||||
|
# 动态数据源文档 https://www.kancloud.cn/tracy5546/dynamic-datasource/content
|
||||||
|
dynamic:
|
||||||
|
# 性能分析插件(有性能损耗 不建议生产环境使用)
|
||||||
|
p6spy: true
|
||||||
|
# 设置默认的数据源或者数据源组,默认值即为 master
|
||||||
|
primary: master
|
||||||
|
# 严格模式 匹配不到数据源则报错
|
||||||
|
strict: true
|
||||||
|
datasource:
|
||||||
|
# 主库数据源
|
||||||
|
master:
|
||||||
|
type: ${spring.datasource.type}
|
||||||
|
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?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||||
|
username: root
|
||||||
|
password: root
|
||||||
|
# agent:
|
||||||
|
# url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||||
|
# # url: jdbc:mysql://localhost:3306/agent_db
|
||||||
|
# username: root
|
||||||
|
# password: root
|
||||||
|
# driverClassName: com.mysql.cj.jdbc.Driver
|
||||||
|
|
||||||
|
hikari:
|
||||||
|
# 最大连接池数量
|
||||||
|
maxPoolSize: 20
|
||||||
|
# 最小空闲线程数量
|
||||||
|
minIdle: 10
|
||||||
|
# 配置获取连接等待超时的时间
|
||||||
|
connectionTimeout: 30000
|
||||||
|
# 校验超时时间
|
||||||
|
validationTimeout: 5000
|
||||||
|
# 空闲连接存活最大时间,默认10分钟
|
||||||
|
idleTimeout: 600000
|
||||||
|
# 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认30分钟
|
||||||
|
maxLifetime: 1800000
|
||||||
|
# 多久检查一次连接的活性
|
||||||
|
keepaliveTime: 30000
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
--- # 上传文件地址
|
||||||
|
sys:
|
||||||
|
upload:
|
||||||
|
path: D:\\DownLoad
|
||||||
|
|
||||||
|
--- # redis 单机配置(单机与集群只能开启一个另一个需要注释掉)
|
||||||
|
spring.data:
|
||||||
|
redis:
|
||||||
|
# 地址
|
||||||
|
host: localhost
|
||||||
|
# 端口,默认为6379
|
||||||
|
port: 6379
|
||||||
|
# 数据库索引
|
||||||
|
database: 0
|
||||||
|
# redis 密码必须配置
|
||||||
|
# password: 123456
|
||||||
|
# 连接超时时间
|
||||||
|
timeout: 10s
|
||||||
|
# 是否开启ssl
|
||||||
|
ssl.enabled: false
|
||||||
|
|
||||||
|
# redisson 配置
|
||||||
|
redisson:
|
||||||
|
# redis key前缀
|
||||||
|
keyPrefix:
|
||||||
|
# 线程池数量
|
||||||
|
threads: 4
|
||||||
|
# Netty线程池数量
|
||||||
|
nettyThreads: 8
|
||||||
|
# 单节点配置
|
||||||
|
singleServerConfig:
|
||||||
|
# 客户端名称 不能用中文
|
||||||
|
clientName: ruoyi-ai
|
||||||
|
# 最小空闲连接数
|
||||||
|
connectionMinimumIdleSize: 8
|
||||||
|
# 连接池大小
|
||||||
|
connectionPoolSize: 32
|
||||||
|
# 连接空闲超时,单位:毫秒
|
||||||
|
idleConnectionTimeout: 10000
|
||||||
|
# 命令等待超时,单位:毫秒
|
||||||
|
timeout: 3000
|
||||||
|
# 发布和订阅连接池大小
|
||||||
|
subscriptionConnectionPoolSize: 50
|
||||||
|
|
||||||
|
--- # mail 邮件发送
|
||||||
|
mail:
|
||||||
|
enabled: false
|
||||||
|
host: smtp.163.com
|
||||||
|
port: 465
|
||||||
|
# 是否需要用户名密码验证
|
||||||
|
auth: true
|
||||||
|
# 发送方,遵循RFC-822标准
|
||||||
|
from: xxx@163.com
|
||||||
|
# 用户名(注意:如果使用foxmail邮箱,此处user为qq号)
|
||||||
|
user: xxx@163.com
|
||||||
|
# 密码(注意,某些邮箱需要为SMTP服务单独设置密码,详情查看相关帮助)
|
||||||
|
pass: xxxxxxxxxx
|
||||||
|
# 使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。
|
||||||
|
starttlsEnable: true
|
||||||
|
# 使用SSL安全连接
|
||||||
|
sslEnable: true
|
||||||
|
# SMTP超时时长,单位毫秒,缺省值不超时
|
||||||
|
timeout: 0
|
||||||
|
# Socket连接超时值,单位毫秒,缺省值不超时
|
||||||
|
connectionTimeout: 0
|
||||||
|
|
||||||
|
--- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
|
||||||
|
# https://sms4j.com/doc3/ 差异配置文档地址 支持单厂商多配置,可以配置多个同时使用
|
||||||
|
sms:
|
||||||
|
# 配置源类型用于标定配置来源(interface,yaml)
|
||||||
|
config-type: yaml
|
||||||
|
# 用于标定yml中的配置是否开启短信拦截,接口配置不受此限制
|
||||||
|
restricted: true
|
||||||
|
# 短信拦截限制单手机号每分钟最大发送,只对开启了拦截的配置有效
|
||||||
|
minute-max: 1
|
||||||
|
# 短信拦截限制单手机号每日最大发送量,只对开启了拦截的配置有效
|
||||||
|
account-max: 30
|
||||||
|
# 以下配置来自于 org.dromara.sms4j.provider.config.BaseConfig类中
|
||||||
|
blends:
|
||||||
|
# 唯一ID 用于发送短信寻找具体配置 随便定义别用中文即可
|
||||||
|
# 可以同时存在两个相同厂商 例如: ali1 ali2 两个不同的阿里短信账号 也可用于区分租户
|
||||||
|
config1:
|
||||||
|
# 框架定义的厂商名称标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
|
||||||
|
supplier: alibaba
|
||||||
|
# 有些称为accessKey有些称之为apiKey,也有称为sdkKey或者appId。
|
||||||
|
access-key-id: 您的accessKey
|
||||||
|
# 称为accessSecret有些称之为apiSecret
|
||||||
|
access-key-secret: 您的accessKeySecret
|
||||||
|
signature: 您的短信签名
|
||||||
|
sdk-app-id: 您的sdkAppId
|
||||||
|
config2:
|
||||||
|
# 厂商标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
|
||||||
|
supplier: tencent
|
||||||
|
access-key-id: 您的accessKey
|
||||||
|
access-key-secret: 您的accessKeySecret
|
||||||
|
signature: 您的短信签名
|
||||||
|
sdk-app-id: 您的sdkAppId
|
||||||
|
|
||||||
|
|
||||||
|
--- # 三方授权
|
||||||
|
justauth:
|
||||||
|
# 前端外网访问地址
|
||||||
|
address: http://localhost:80
|
||||||
|
type:
|
||||||
|
maxkey:
|
||||||
|
# maxkey 服务器地址
|
||||||
|
# 注意 如下均配置均不需要修改 maxkey 已经内置好了数据
|
||||||
|
server-url: http://sso.maxkey.top
|
||||||
|
client-id: 876892492581044224
|
||||||
|
client-secret: x1Y5MTMwNzIwMjMxNTM4NDc3Mzche8
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=maxkey
|
||||||
|
topiam:
|
||||||
|
# topiam 服务器地址
|
||||||
|
server-url: http://127.0.0.1:1898/api/v1/authorize/y0q************spq***********8ol
|
||||||
|
client-id: 449c4*********937************759
|
||||||
|
client-secret: ac7***********1e0************28d
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=topiam
|
||||||
|
scopes: [openid, email, phone, profile]
|
||||||
|
qq:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=qq
|
||||||
|
union-id: false
|
||||||
|
weibo:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=weibo
|
||||||
|
gitee:
|
||||||
|
client-id: 91436b7940090d09c72c7daf85b959cfd5f215d67eea73acbf61b6b590751a98
|
||||||
|
client-secret: 02c6fcfd70342980cd8dd2f2c06c1a350645d76c754d7a264c4e125f9ba915ac
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=gitee
|
||||||
|
dingtalk:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=dingtalk
|
||||||
|
baidu:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=baidu
|
||||||
|
csdn:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=csdn
|
||||||
|
coding:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=coding
|
||||||
|
coding-group-name: xx
|
||||||
|
oschina:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=oschina
|
||||||
|
alipay_wallet:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=alipay_wallet
|
||||||
|
alipay-public-key: MIIB**************DAQAB
|
||||||
|
wechat_open:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=wechat_open
|
||||||
|
wechat_mp:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=wechat_mp
|
||||||
|
wechat_enterprise:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=wechat_enterprise
|
||||||
|
agent-id: 1000002
|
||||||
|
gitlab:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=gitlab
|
||||||
|
gitea:
|
||||||
|
# 前端改动 https://gitee.com/JavaLionLi/plus-ui/pulls/204
|
||||||
|
# gitea 服务器地址
|
||||||
|
server-url: https://demo.gitea.com
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=gitea
|
||||||
|
|
||||||
|
AGENT_ALLOWED_TABLES: "abtest_rule,abtest_project,agent_ban_log,agent_ban_logs,agent_install_sub_task,agent_install_sum_task,agent_install_task"
|
||||||
@@ -20,6 +20,10 @@ server:
|
|||||||
# 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载
|
# 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载
|
||||||
worker: 256
|
worker: 256
|
||||||
|
|
||||||
|
--- # 小程序对话 WebSocket 兜底默认模型(前端未传 model 且无智能体绑定时使用)
|
||||||
|
chat:
|
||||||
|
default-model: deepseek-v4-flash
|
||||||
|
|
||||||
captcha:
|
captcha:
|
||||||
# 是否启用验证码校验
|
# 是否启用验证码校验
|
||||||
enable: false
|
enable: false
|
||||||
@@ -35,7 +39,7 @@ captcha:
|
|||||||
# 日志配置
|
# 日志配置
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
org.ruoyi: @logging.level@
|
org.ruoyi: ${LOGGING_LEVEL_ORG_RUOYI:info}
|
||||||
org.springframework: warn
|
org.springframework: warn
|
||||||
org.mybatis.spring.mapper: error
|
org.mybatis.spring.mapper: error
|
||||||
org.apache.fury: warn
|
org.apache.fury: warn
|
||||||
@@ -75,7 +79,7 @@ spring:
|
|||||||
# 国际化资源文件路径
|
# 国际化资源文件路径
|
||||||
basename: i18n/messages
|
basename: i18n/messages
|
||||||
profiles:
|
profiles:
|
||||||
active: @profiles.active@
|
active: ${SPRING_PROFILES_ACTIVE:dev}
|
||||||
# 文件上传
|
# 文件上传
|
||||||
servlet:
|
servlet:
|
||||||
multipart:
|
multipart:
|
||||||
@@ -125,9 +129,7 @@ security:
|
|||||||
- /*/api-docs/**
|
- /*/api-docs/**
|
||||||
- /warm-flow-ui/config
|
- /warm-flow-ui/config
|
||||||
- /workflow/run
|
- /workflow/run
|
||||||
- /user/qrcode
|
- /coding/**
|
||||||
- /user/login/qrcode
|
|
||||||
- /weixin/check
|
|
||||||
# 多租户配置
|
# 多租户配置
|
||||||
tenant:
|
tenant:
|
||||||
# 是否开启
|
# 是否开启
|
||||||
@@ -144,6 +146,9 @@ tenant:
|
|||||||
- sys_client
|
- sys_client
|
||||||
- sys_oss_config
|
- sys_oss_config
|
||||||
- flow_spel
|
- flow_spel
|
||||||
|
# 链路追踪监控表:运维需跨租户全局查看,且 trace_node 在异步线程写入、租户上下文不传播,故排除租户过滤
|
||||||
|
- trace_run
|
||||||
|
- trace_node
|
||||||
|
|
||||||
# MyBatisPlus配置
|
# MyBatisPlus配置
|
||||||
# https://baomidou.com/config/
|
# https://baomidou.com/config/
|
||||||
@@ -206,6 +211,7 @@ springdoc:
|
|||||||
name: ageerle
|
name: ageerle
|
||||||
email: ageerle@163.com
|
email: ageerle@163.com
|
||||||
url: https://gitee.com/ageerle/ruoyi-ai
|
url: https://gitee.com/ageerle/ruoyi-ai
|
||||||
|
|
||||||
#这里定义了两个分组,可定义多个,也可以不定义
|
#这里定义了两个分组,可定义多个,也可以不定义
|
||||||
group-configs:
|
group-configs:
|
||||||
- group: 1.演示模块
|
- group: 1.演示模块
|
||||||
@@ -229,6 +235,15 @@ xss:
|
|||||||
excludeUrls:
|
excludeUrls:
|
||||||
- /system/notice
|
- /system/notice
|
||||||
|
|
||||||
|
--- # 链路追踪配置
|
||||||
|
trace:
|
||||||
|
# 是否启用链路追踪,默认 true
|
||||||
|
# 关闭后所有埋点代码会直接透传业务逻辑,不写库、不创建上下文,零性能开销
|
||||||
|
enabled: true
|
||||||
|
payload:
|
||||||
|
# 错误信息最大字符长度(格式: "异常类名: 异常消息"),超过部分会被截断丢弃
|
||||||
|
max-error-length: 1000
|
||||||
|
|
||||||
--- # 分布式锁 lock4j 全局配置
|
--- # 分布式锁 lock4j 全局配置
|
||||||
lock4j:
|
lock4j:
|
||||||
# 获取分布式锁超时时间,默认为 3000 毫秒
|
# 获取分布式锁超时时间,默认为 3000 毫秒
|
||||||
@@ -247,12 +262,6 @@ management:
|
|||||||
show-details: ALWAYS
|
show-details: ALWAYS
|
||||||
logfile:
|
logfile:
|
||||||
external-file: ./logs/sys-console.log
|
external-file: ./logs/sys-console.log
|
||||||
health:
|
|
||||||
# ⚠️ 禁用 Neo4j 健康检查
|
|
||||||
# Spring Boot Actuator 会自动为 Neo4j 创建健康检查器
|
|
||||||
# 这会导致应用在启动时尝试连接到 Neo4j
|
|
||||||
neo4j:
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
--- # 默认/推荐使用sse推送
|
--- # 默认/推荐使用sse推送
|
||||||
sse:
|
sse:
|
||||||
@@ -268,6 +277,25 @@ websocket:
|
|||||||
# 设置访问源地址
|
# 设置访问源地址
|
||||||
allowedOrigins: '*'
|
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工作流配置
|
||||||
warm-flow:
|
warm-flow:
|
||||||
# 是否开启工作流,默认true
|
# 是否开启工作流,默认true
|
||||||
@@ -283,93 +311,68 @@ warm-flow:
|
|||||||
|
|
||||||
# 向量库配置
|
# 向量库配置
|
||||||
vector-store:
|
vector-store:
|
||||||
# 向量存储类型 可选(weaviate/milvus)
|
# 向量存储类型 可选(weaviate/milvus/qdrant)
|
||||||
# 如需修改向量库类型,请修改此配置值!
|
# 如需修改向量库类型,请修改此配置值!
|
||||||
|
# 注意:需与 docker-compose 实际部署的向量库保持一致(当前 compose 内置 weaviate,映射端口 28080)
|
||||||
type: weaviate
|
type: weaviate
|
||||||
# Weaviate配置
|
# Weaviate配置
|
||||||
weaviate:
|
weaviate:
|
||||||
protocol: http
|
protocol: http
|
||||||
host: 127.0.0.1:6038
|
host: 127.0.0.1:28080
|
||||||
classname: LocalKnowledge
|
classname: LocalKnowledge
|
||||||
# Milvus配置
|
# Milvus配置
|
||||||
milvus:
|
milvus:
|
||||||
url: http://localhost:19530
|
url: http://localhost:19530
|
||||||
collectionname: LocalKnowledge
|
collectionname: LocalKnowledge
|
||||||
|
# Qdrant配置
|
||||||
|
qdrant:
|
||||||
|
host: localhost
|
||||||
|
port: 6334
|
||||||
|
collectionname: LocalKnowledge
|
||||||
|
api-key:
|
||||||
|
use-tls: false
|
||||||
|
|
||||||
chat:
|
# 流程编排扩展节点
|
||||||
memory:
|
workflow:
|
||||||
enabled: true
|
web-search:
|
||||||
maxMessages: 20
|
zhipu:
|
||||||
persistenceEnabled: true
|
# 推荐通过环境变量注入;为空时回退到模型管理中的 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}
|
||||||
|
|
||||||
# 企业微信应用
|
# 短剧成片合成
|
||||||
wechat:
|
short-drama:
|
||||||
cp:
|
composition:
|
||||||
corpId:
|
ffmpeg-path: ${FFMPEG_PATH:ffmpeg}
|
||||||
appConfigs:
|
ffprobe-path: ${FFPROBE_PATH:ffprobe}
|
||||||
- agentId:
|
fps: ${SHORT_DRAMA_COMPOSITION_FPS:30}
|
||||||
secret: ''
|
audio-sample-rate: ${SHORT_DRAMA_COMPOSITION_AUDIO_SAMPLE_RATE:48000}
|
||||||
token: ''
|
video-codec: ${SHORT_DRAMA_COMPOSITION_VIDEO_CODEC:libx264}
|
||||||
aesKey: ''
|
audio-codec: ${SHORT_DRAMA_COMPOSITION_AUDIO_CODEC:aac}
|
||||||
|
preset: ${SHORT_DRAMA_COMPOSITION_PRESET:medium}
|
||||||
--- # Neo4j 知识图谱配置
|
crf: ${SHORT_DRAMA_COMPOSITION_CRF:20}
|
||||||
neo4j:
|
audio-bitrate: ${SHORT_DRAMA_COMPOSITION_AUDIO_BITRATE:192k}
|
||||||
uri: bolt://117.72.192.162:7687
|
max-clips: ${SHORT_DRAMA_COMPOSITION_MAX_CLIPS:100}
|
||||||
username: neo4j
|
max-transition-seconds: ${SHORT_DRAMA_COMPOSITION_MAX_TRANSITION_SECONDS:2.0}
|
||||||
password: MySecurePass123!
|
minimum-output-bytes: ${SHORT_DRAMA_COMPOSITION_MINIMUM_OUTPUT_BYTES:1024}
|
||||||
database: neo4j
|
max-process-output-bytes: ${SHORT_DRAMA_COMPOSITION_MAX_PROCESS_OUTPUT_BYTES:1048576}
|
||||||
max-connection-pool-size: 50
|
max-source-bytes: ${SHORT_DRAMA_COMPOSITION_MAX_SOURCE_BYTES:536870912}
|
||||||
connection-timeout-seconds: 30
|
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}
|
||||||
knowledge:
|
job-stale-after: ${SHORT_DRAMA_COMPOSITION_JOB_STALE_AFTER:45m}
|
||||||
graph:
|
watermark-font-file: ${SHORT_DRAMA_COMPOSITION_WATERMARK_FONT_FILE:}
|
||||||
# 是否启用知识图谱功能
|
worker-core-size: ${SHORT_DRAMA_COMPOSITION_WORKER_CORE_SIZE:1}
|
||||||
enabled: false
|
worker-max-size: ${SHORT_DRAMA_COMPOSITION_WORKER_MAX_SIZE:2}
|
||||||
# 图数据库类型: neo4j 或 apache-age
|
worker-queue-capacity: ${SHORT_DRAMA_COMPOSITION_WORKER_QUEUE_CAPACITY:8}
|
||||||
database-type: neo4j
|
storage-mode: ${SHORT_DRAMA_COMPOSITION_STORAGE_MODE:local}
|
||||||
# 是否自动创建索引
|
local-output-directory: ${SHORT_DRAMA_COMPOSITION_LOCAL_OUTPUT_DIRECTORY:logs/short-drama-compositions}
|
||||||
auto-create-index: true
|
download:
|
||||||
# 批量处理大小
|
connect-timeout: ${SHORT_DRAMA_DOWNLOAD_CONNECT_TIMEOUT:30s}
|
||||||
batch-size: 1000
|
call-timeout: ${SHORT_DRAMA_DOWNLOAD_CALL_TIMEOUT:10m}
|
||||||
# 最大重试次数
|
max-redirects: ${SHORT_DRAMA_DOWNLOAD_MAX_REDIRECTS:5}
|
||||||
max-retry-count: 3
|
# 生产环境建议配置为视频供应商或 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}
|
||||||
extraction:
|
|
||||||
# 置信度阈值(低于此值的实体将被过滤)
|
|
||||||
confidence-threshold: 0.7
|
|
||||||
# 最大实体数量(每个文档)
|
|
||||||
max-entities-per-doc: 100
|
|
||||||
# 最大关系数量(每个文档)
|
|
||||||
max-relations-per-doc: 200
|
|
||||||
# 文本分片大小(用于长文档)
|
|
||||||
chunk-size: 2000
|
|
||||||
# 分片重叠大小
|
|
||||||
chunk-overlap: 200
|
|
||||||
|
|
||||||
# 查询配置
|
|
||||||
query:
|
|
||||||
# 默认查询限制数量
|
|
||||||
default-limit: 100
|
|
||||||
# 最大查询限制数量
|
|
||||||
max-limit: 1000
|
|
||||||
# 路径查询最大深度
|
|
||||||
max-path-depth: 5
|
|
||||||
# 查询超时时间(秒)
|
|
||||||
timeout-seconds: 30
|
|
||||||
# 是否启用查询缓存
|
|
||||||
cache-enabled: true
|
|
||||||
# 缓存过期时间(分钟)
|
|
||||||
cache-expire-minutes: 60
|
|
||||||
|
|
||||||
--- # MCP 模块配置
|
|
||||||
app:
|
|
||||||
mcp:
|
|
||||||
client:
|
|
||||||
# 请求超时时间(秒)
|
|
||||||
request-timeout: 30
|
|
||||||
# 连接超时时间(秒)
|
|
||||||
connection-timeout: 10
|
|
||||||
# 最大重试次数
|
|
||||||
max-retries: 3
|
|
||||||
|
|||||||
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
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||||
|
xmlns="http://schemas.openxmlformats.org/drawingml/2006/lockedCanvas"
|
||||||
|
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||||
|
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||||
|
elementFormDefault="qualified"
|
||||||
|
targetNamespace="http://schemas.openxmlformats.org/drawingml/2006/lockedCanvas">
|
||||||
|
<xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||||
|
schemaLocation="dml-main.xsd"/>
|
||||||
|
<xsd:element name="lockedCanvas" type="a:CT_GvmlGroupShape"/>
|
||||||
|
</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