chore: update default port from 3003 to 3000 and improve installation docs

- Change all default port references from 3003 to 3000 across scripts and documentation
- Add quick installation guide for batch plugin installation to main README (EN & CN)
- Simplify installation options by removing manual installation instructions
- Update deployment guides and examples to reflect new default port
This commit is contained in:
fujie
2026-03-09 21:42:17 +08:00
parent 62e78ace5c
commit ae0fa1d39a
16 changed files with 1712 additions and 691 deletions

View File

@@ -177,11 +177,7 @@ This project is a collection of resources and does not require a Python environm
- Browse the plugins and select the one you like.
- Click "Get" to import it directly into your OpenWebUI instance.
2. **Manual Installation**:
- Browse the `/plugins` directory and download the plugin file (`.py`) you need.
- Go to OpenWebUI **Admin Panel** -> **Settings** -> **Plugins**.
- Click the upload button and select the `.py` file you just downloaded.
- Once uploaded, refresh the page to enable the plugin in your chat settings or toolbar.
2. **Quick Install All Plugins**: To install all plugins to your local OpenWebUI instance at once, clone this repo and run `python scripts/install_all_plugins.py` after configuring your API key in `.env` — see [Deployment Guide](./scripts/DEPLOYMENT_GUIDE.md) for details.
### Contributing

View File

@@ -166,4 +166,13 @@ Open WebUI 的前端增强扩展:
本项目是一个资源集合,无需安装 Python 环境。你只需要下载对应的文件并导入到你的 OpenWebUI 实例中即可。
### 使用插件
1. **从官方社区安装(推荐)**
- 访问我的主页:[Fu-Jie 的个人页面](https://openwebui.com/u/Fu-Jie)
- 浏览插件并选择你喜欢的
- 点击"Get"按钮直接导入到你的 OpenWebUI 实例
2. **快速安装所有插件**:如果想一次性安装此项目中的所有插件到本地 OpenWebUI 实例,克隆此仓库后运行 `python scripts/install_all_plugins.py`,并在 `.env` 中配置好 API 密钥,详见 [部署指南](./scripts/DEPLOYMENT_GUIDE.md)。
[贡献指南](./CONTRIBUTING_CN.md) | [更新日志](./CHANGELOG.md)

20
scripts/.env.example Normal file
View File

@@ -0,0 +1,20 @@
# OpenWebUI Bulk Installer Configuration
#
# Instructions:
# - api_key: Copy from OpenWebUI Settings (starts with sk-)
# - url: OpenWebUI server address
#
# Environment variable precedence (highest to lowest):
# 1. OPENWEBUI_API_KEY / OPENWEBUI_URL environment variables
# 2. OPENWEBUI_BASE_URL environment variable
# 3. Configuration in this .env file
# API Key (required)
api_key=sk-your-api-key-here
# OpenWebUI server address (required)
url=http://localhost:3000
# Alternatively, use environment variable format (both methods are equivalent)
# OPENWEBUI_API_KEY=sk-your-api-key-here
# OPENWEBUI_BASE_URL=http://localhost:3000

View File

@@ -1,147 +1,147 @@
# 🚀 本地部署脚本指南 (Local Deployment Guide)
# 🚀 Local Deployment Scripts Guide
## 概述
## Overview
本目录包含用于将开发中的插件部署到本地 OpenWebUI 实例的自动化脚本。它们可以快速推送代码更改而无需重启 OpenWebUI
This directory contains automated scripts for deploying plugins in development to a local OpenWebUI instance. They enable quick code pushes without restarting OpenWebUI.
## 前置条件
## Prerequisites
1. **OpenWebUI 运行中**: 确保 OpenWebUI 在本地运行(默认 `http://localhost:3003`
2. **API 密钥**: 需要一个有效的 OpenWebUI API 密钥
3. **环境文件**: 在此目录创建 `.env` 文件,包含 API 密钥:
1. **OpenWebUI Running**: Make sure OpenWebUI is running locally (default `http://localhost:3000`)
2. **API Key**: You need a valid OpenWebUI API key
3. **Environment File**: Create a `.env` file in this directory containing your API key:
```
api_key=sk-xxxxxxxxxxxxx
```
## 快速开始
## Quick Start
### 部署 Pipe 插件
### Deploy a Pipe Plugin
```bash
# 部署 GitHub Copilot SDK Pipe
# Deploy GitHub Copilot SDK Pipe
python deploy_pipe.py
```
### 部署 Filter 插件
### Deploy a Filter Plugin
```bash
# 部署 async_context_compression Filter(默认)
# Deploy async_context_compression Filter (default)
python deploy_filter.py
# 部署指定的 Filter 插件
# Deploy a specific Filter plugin
python deploy_filter.py my-filter-name
# 列出所有可用的 Filter
# List all available Filters
python deploy_filter.py --list
```
## 脚本说明
## Script Documentation
### `deploy_filter.py` — Filter 插件部署工具
### `deploy_filter.py` — Filter Plugin Deployment Tool
用于部署 Filter 类型的插件(如消息过滤、上下文压缩等)。
Used to deploy Filter-type plugins (such as message filtering, context compression, etc.).
**主要特性**:
- ✅ 从 Python 文件自动提取元数据(版本、作者、描述等)
- ✅ 尝试更新现有插件,若不存在则创建新插件
- ✅ 支持多个 Filter 插件管理
- ✅ 详细的错误提示和连接诊断
**Key Features**:
- ✅ Auto-extracts metadata from Python files (version, author, description, etc.)
- ✅ Attempts to update existing plugins, creates if not found
- ✅ Supports multiple Filter plugin management
- ✅ Detailed error messages and connection diagnostics
**用法**:
**Usage**:
```bash
# 默认部署 async_context_compression
# Deploy async_context_compression (default)
python deploy_filter.py
# 部署其他 Filter
# Deploy other Filters
python deploy_filter.py async-context-compression
python deploy_filter.py workflow-guide
# 列出所有可用 Filter
# List all available Filters
python deploy_filter.py --list
python deploy_filter.py -l
```
**工作流程**:
1. 从 `.env` 加载 API 密钥
2. 查找目标 Filter 插件目录
3. 读取 Python 源文件
4. docstring 提取元数据(title, version, author, description, etc.
5. 构建 API 请求负载
6. 发送更新请求到 OpenWebUI
7. 若更新失败,自动尝试创建新插件
8. 显示结果和诊断信息
**Workflow**:
1. Load API key from `.env`
2. Find target Filter plugin directory
3. Read Python source file
4. Extract metadata from docstring (title, version, author, description, etc.)
5. Build API request payload
6. Send update request to OpenWebUI
7. If update fails, auto-attempt to create new plugin
8. Display results and diagnostic info
### `deploy_pipe.py` — Pipe 插件部署工具
### `deploy_pipe.py` — Pipe Plugin Deployment Tool
用于部署 Pipe 类型的插件(如 GitHub Copilot SDK)。
Used to deploy Pipe-type plugins (such as GitHub Copilot SDK).
**使用**:
**Usage**:
```bash
python deploy_pipe.py
```
## 获取 API 密钥
## Get an API Key
### 方法 1: 使用现有用户令牌(推荐)
### Method 1: Use Existing User Token (Recommended)
1. 打开 OpenWebUI 界面
2. 点击用户头像 → Settings(设置)
3. 找到 API Keys 部分
4. 复制你的 API 密钥sk-开头)
5. 粘贴到 `.env` 文件中
1. Open OpenWebUI interface
2. Click user avatar → Settings
3. Find the API Keys section
4. Copy your API key (starts with sk-)
5. Paste into `.env` file
### 方法 2: 创建长期 API 密钥
### Method 2: Create a Long-term API Key
在 OpenWebUI 设置中创建专用于部署的长期 API 密钥。
Create a dedicated long-term API key in OpenWebUI Settings for deployment purposes.
## 故障排除
## Troubleshooting
### "Connection error: Could not reach OpenWebUI at localhost:3003"
### "Connection error: Could not reach OpenWebUI at localhost:3000"
**原因**: OpenWebUI 未运行或端口不同
**Cause**: OpenWebUI is not running or port is different
**解决方案**:
- 确保 OpenWebUI 正在运行
- 检查 OpenWebUI 实际监听的端口(通常是 3000 或 3003
- 根据需要编辑脚本中的 URL
**Solution**:
- Make sure OpenWebUI is running
- Check which port OpenWebUI is actually listening on (usually 3000)
- Edit the URL in the script if needed
### ".env file not found"
**原因**: 未创建 `.env` 文件
**Cause**: `.env` file was not created
**解决方案**:
**Solution**:
```bash
echo "api_key=sk-your-api-key-here" > .env
```
### "Filter 'xxx' not found"
**原因**: Filter 目录名不正确
**Cause**: Filter directory name is incorrect
**解决方案**:
**Solution**:
```bash
# 列出所有可用的 Filter
# List all available Filters
python deploy_filter.py --list
```
### "Failed to update or create. Status: 401"
**原因**: API 密钥无效或过期
**Cause**: API key is invalid or expired
**解决方案**:
1. 验证 API 密钥的有效性
2. 获取新的 API 密钥
3. 更新 `.env` 文件
**Solution**:
1. Verify your API key is valid
2. Generate a new API key
3. Update the `.env` file
## 工作流示例
## Workflow Examples
### 开发并部署新的 Filter
### Develop and Deploy a New Filter
```bash
# 1. plugins/filters/ 创建新的 Filter 目录
# 1. Create new Filter directory in plugins/filters/
mkdir plugins/filters/my-new-filter
# 2. 创建 my_new_filter.py 文件,包含必要的元数据:
# 2. Create my_new_filter.py with required metadata:
# """
# title: My New Filter
# author: Your Name
@@ -149,58 +149,58 @@ mkdir plugins/filters/my-new-filter
# description: Filter description
# """
# 3. 部署到本地 OpenWebUI
# 3. Deploy to local OpenWebUI
cd scripts
python deploy_filter.py my-new-filter
# 4. OpenWebUI UI 中测试插件
# 4. Test the plugin in OpenWebUI UI
# 5. 继续迭代开发
# ... 修改代码 ...
# 5. Continue development
# ... modify code ...
# 6. 重新部署(自动覆盖)
# 6. Re-deploy (auto-overwrites)
python deploy_filter.py my-new-filter
```
### 修复 Bug 并快速部署
### Fix a Bug and Deploy Quickly
```bash
# 1. 修改源代码
# 1. Modify the source code
# vim ../plugins/filters/async-context-compression/async_context_compression.py
# 2. 立即部署到本地
# 2. Deploy immediately to local
python deploy_filter.py async-context-compression
# 3. OpenWebUI 中测试修复
# (无需重启 OpenWebUI
# 3. Test the fix in OpenWebUI
# (No need to restart OpenWebUI)
```
## 安全注意事项
## Security Considerations
⚠️ **重要**:
- ✅ `.env` 文件添加到 `.gitignore`(避免提交敏感信息)
- ✅ 不要在版本控制中提交 API 密钥
- ✅ 仅在可信的网络环境中使用
- ✅ 定期轮换 API 密钥
⚠️ **Important**:
- ✅ Add `.env` file to `.gitignore` (avoid committing sensitive info)
- ✅ Never commit API keys to version control
- ✅ Use only on trusted networks
- ✅ Rotate API keys periodically
## 文件结构
## File Structure
```
scripts/
├── deploy_filter.py # Filter 插件部署工具
├── deploy_pipe.py # Pipe 插件部署工具
├── .env # API 密钥(本地,不提交)
├── README.md # 本文件
├── deploy_filter.py # Filter plugin deployment tool
├── deploy_pipe.py # Pipe plugin deployment tool
├── .env # API key (local, not committed)
├── README.md # This file
└── ...
```
## 参考资源
## Reference Resources
- [OpenWebUI 文档](https://docs.openwebui.com/)
- [插件开发指南](../docs/development/plugin-guide.md)
- [Filter 插件示例](../plugins/filters/)
- [OpenWebUI Documentation](https://docs.openwebui.com/)
- [Plugin Development Guide](../docs/development/plugin-guide.md)
- [Filter Plugin Examples](../plugins/filters/)
---
**最后更新**: 2026-03-09
**作者**: Fu-Jie
**Last Updated**: 2026-03-09
**Author**: Fu-Jie

View File

@@ -1,114 +1,114 @@
# 📦 Async Context Compression — 本地部署工具 (Local Deployment Tools)
# 📦 Async Context Compression — Local Deployment Tools
## 🎯 功能概述
## 🎯 Feature Overview
`async_context_compression` Filter 插件添加了完整的本地部署工具链,支持快速迭代开发无需重启 OpenWebUI
Added a complete local deployment toolchain for the `async_context_compression` Filter plugin, supporting fast iterative development without restarting OpenWebUI.
## 📋 新增文件
## 📋 New Files
### 1. **deploy_filter.py** — Filter 插件部署脚本
- **位置**: `scripts/deploy_filter.py`
- **功能**: 自动部署 Filter 类插件到本地 OpenWebUI 实例
- **特性**:
- Python docstring 自动提取元数据
-智能版本号识别(semantic versioning
-支持多个 Filter 插件管理
-自动更新或创建插件
-详细的错误诊断和连接测试
-列表指令查看所有可用 Filter
- **代码行数**: ~300
### 1. **deploy_filter.py** — Filter Plugin Deployment Script
- **Location**: `scripts/deploy_filter.py`
- **Function**: Auto-deploy Filter-type plugins to local OpenWebUI instance
- **Features**:
-Auto-extract metadata from Python docstring
-Smart semantic version recognition
-Support multiple Filter plugin management
-Auto-update or create plugins
-Detailed error diagnostics and connection testing
-List command to view all available Filters
- **Code Lines**: ~300
### 2. **DEPLOYMENT_GUIDE.md** — 完整部署指南
- **位置**: `scripts/DEPLOYMENT_GUIDE.md`
- **内容**:
- 前置条件和快速开始
- 脚本详细说明
- API 密钥获取方法
- 故障排除指南
- 分步工作流示例
### 2. **DEPLOYMENT_GUIDE.md** — Complete Deployment Guide
- **Location**: `scripts/DEPLOYMENT_GUIDE.md`
- **Contents**:
- Prerequisites and quick start
- Detailed script documentation
- API key retrieval method
- Troubleshooting guide
- Step-by-step workflow examples
### 3. **QUICK_START.md** — 快速参考卡片
- **位置**: `scripts/QUICK_START.md`
- **内容**:
- 一行命令部署
- 前置步骤
- 常见命令表格
- 故障诊断速查表
- CI/CD 集成示例
### 3. **QUICK_START.md** — Quick Reference Card
- **Location**: `scripts/QUICK_START.md`
- **Contents**:
- One-line deployment command
- Setup steps
- Common commands table
- Troubleshooting quick-reference table
- CI/CD integration examples
### 4. **test_deploy_filter.py** — 单元测试套件
- **位置**: `tests/scripts/test_deploy_filter.py`
- **测试覆盖**:
- ✅ Filter 文件发现 (3 个测试)
-元数据提取 (3 个测试)
- ✅ API 负载构建 (4 个测试)
- **测试通过率**: 10/10 ✅
### 4. **test_deploy_filter.py** — Unit Test Suite
- **Location**: `tests/scripts/test_deploy_filter.py`
- **Test Coverage**:
- ✅ Filter file discovery (3 tests)
-Metadata extraction (3 tests)
- ✅ API payload building (4 tests)
- **Pass Rate**: 10/10 ✅
## 🚀 使用方式
## 🚀 Usage
### 基本部署(一行命令)
### Basic Deploy (One-liner)
```bash
cd scripts
python deploy_filter.py
```
### 列出所有可用 Filter
### List All Available Filters
```bash
python deploy_filter.py --list
```
### 部署指定 Filter
### Deploy Specific Filter
```bash
python deploy_filter.py folder-memory
python deploy_filter.py context_enhancement_filter
```
## 🔧 工作原理
## 🔧 How It Works
```
┌─────────────────────────────────────────────────────────────┐
│ 1. 加载 API 密钥 (.env) │
│ 1. Load API key (.env) │
└──────────────────┬──────────────────────────────────────────┘
┌──────────────────▼──────────────────────────────────────────┐
│ 2. 查找 Filter 插件文件
│ - 从名称推断文件路径
│ - 支持 hyphen-case snake_case 查找
│ 2. Find Filter plugin file
│ - Infer file path from name
│ - Support hyphen-case and snake_case lookup
└──────────────────┬──────────────────────────────────────────┘
┌──────────────────▼──────────────────────────────────────────┐
│ 3. 读取 Python 源代码
│ - 提取 docstring 元数据
│ 3. Read Python source code
│ - Extract docstring metadata
│ - title, version, author, description, openwebui_id │
└──────────────────┬──────────────────────────────────────────┘
┌──────────────────▼──────────────────────────────────────────┐
│ 4. 构建 API 请求负载
│ - 组装 manifest meta 信息
│ - 包含完整源代码内容
│ 4. Build API request payload
│ - Assemble manifest and meta info
│ - Include complete source code content
└──────────────────┬──────────────────────────────────────────┘
┌──────────────────▼──────────────────────────────────────────┐
│ 5. 发送请求
│ - POST /api/v1/functions/id/{id}/update (更新)
│ - POST /api/v1/functions/create (创建备用)
│ 5. Send request
│ - POST /api/v1/functions/id/{id}/update (update)
│ - POST /api/v1/functions/create (create fallback)
└──────────────────┬──────────────────────────────────────────┘
┌──────────────────▼──────────────────────────────────────────┐
│ 6. 显示结果和诊断
│ - ✅ 更新/创建成功
│ - ❌ 错误信息和解决建议
│ 6. Display results and diagnostics
│ - ✅ Update/create success
│ - ❌ Error messages and solutions
└─────────────────────────────────────────────────────────────┘
```
## 📊 支持的 Filter 列表
## 📊 Supported Filters List
脚本自动发现以下 Filter
Script auto-discovers the following Filters:
| Filter 名称 | Python 文件 | 版本 |
| Filter Name | Python File | Version |
|-----------|-----------|------|
| async-context-compression | async_context_compression.py | 1.3.0+ |
| chat-session-mapping-filter | chat_session_mapping_filter.py | 0.1.0+ |
@@ -118,11 +118,11 @@ python deploy_filter.py context_enhancement_filter
| markdown_normalizer | markdown_normalizer.py | 1.2.8+ |
| web_gemini_multimodel_filter | web_gemini_multimodel_filter.py | 0.3.2+ |
## ⚙️ 技术细节
## ⚙️ Technical Details
### 元数据提取
### Metadata Extraction
脚本从 Python 文件顶部的 docstring 中提取元数据:
Script extracts metadata from the docstring at the top of Python file:
```python
"""
@@ -137,111 +137,111 @@ openwebui_id: b1655bc8-6de9-4cad-8cb5-a6f7829a02ce
"""
```
**支持的元数据字段**:
- `title` — Filter 显示名称
- `id`唯一标识符
- `author`作者名称
- `author_url`作者主页链接
- `funding_url`项目链接
- `description`功能描述
- `version`语义化版本号
- `openwebui_id` — OpenWebUI UUID (可选)
**Supported Metadata Fields**:
- `title` — Filter display name
- `id`Unique identifier
- `author`Author name
- `author_url`Author homepage
- `funding_url`Project link
- `description`Feature description
- `version`Semantic version number
- `openwebui_id` — OpenWebUI UUID (optional)
### API 集成
### API Integration
脚本使用 OpenWebUI REST API
Script uses OpenWebUI REST API:
```
POST /api/v1/functions/id/{filter_id}/update
- 更新现有 Filter
- HTTP 200: 更新成功
- HTTP 404: Filter 不存在,自动尝试创建
- Update existing Filter
- HTTP 200: Update success
- HTTP 404: Filter not found, auto-attempt create
POST /api/v1/functions/create
- 创建新 Filter
- HTTP 200: 创建成功
- Create new Filter
- HTTP 200: Creation success
```
**认证**: Bearer token (API 密钥方式)
**Authentication**: Bearer token (API key method)
## 🔐 安全性
## 🔐 Security
### API 密钥管理
### API Key Management
```bash
# 1. 创建 .env 文件
# 1. Create .env file
echo "api_key=sk-your-key-here" > scripts/.env
# 2. .env 添加到 .gitignore
# 2. Add .env to .gitignore
echo "scripts/.env" >> .gitignore
# 3. 不要提交 API 密钥
# 3. Don't commit API key
git add scripts/.gitignore
git commit -m "chore: add .env to gitignore"
```
### 最佳实践
### Best Practices
-使用长期认证令牌(而不是短期 JWT
-定期轮换 API 密钥
-限制密钥权限范围
-在可信网络中使用
-生产环境使用 CI/CD 秘密管理
-Use long-term auth tokens (not short-term JWT)
-Rotate API keys periodically
-Limit key permission scope
-Use only on trusted networks
-Use CI/CD secret management in production
## 🧪 测试验证
## 🧪 Test Verification
### 运行测试套件
### Run Test Suite
```bash
pytest tests/scripts/test_deploy_filter.py -v
```
### 测试覆盖范围
### Test Coverage
```
✅ TestFilterDiscovery (3 个测试)
✅ TestFilterDiscovery (3 tests)
- test_find_async_context_compression
- test_find_nonexistent_filter
- test_find_filter_with_underscores
✅ TestMetadataExtraction (3 个测试)
✅ TestMetadataExtraction (3 tests)
- test_extract_metadata_from_async_compression
- test_extract_metadata_empty_file
- test_extract_metadata_multiline_docstring
✅ TestPayloadBuilding (4 个测试)
✅ TestPayloadBuilding (4 tests)
- test_build_filter_payload_basic
- test_payload_has_required_fields
- test_payload_with_openwebui_id
✅ TestVersionExtraction (1 个测试)
✅ TestVersionExtraction (1 test)
- test_extract_valid_version
结果: 10/10 通过
Result: 10/10 PASSED
```
## 💡 常见用例
## 💡 Common Use Cases
### 用例 1: 修复 Bug 后快速测试
### Use Case 1: Quick Test After Bug Fix
```bash
# 1. 修改代码
# 1. Modify code
vim plugins/filters/async-context-compression/async_context_compression.py
# 2. 立即部署(不需要重启 OpenWebUI
# 2. Deploy immediately (no OpenWebUI restart needed)
cd scripts && python deploy_filter.py
# 3. OpenWebUI 中测试修复
# 4. 重复迭代(返回步骤 1
# 3. Test fix in OpenWebUI
# 4. Iterate (return to step 1)
```
### 用例 2: 开发新的 Filter
### Use Case 2: Develop New Filter
```bash
# 1. 创建新 Filter 目录
# 1. Create new Filter directory
mkdir plugins/filters/my-new-filter
# 2. 编写代码(包含必要的 docstring 元数据)
# 2. Write code (include required docstring metadata)
cat > plugins/filters/my-new-filter/my_new_filter.py << 'EOF'
"""
title: My New Filter
@@ -254,33 +254,33 @@ class Filter:
# ... implementation ...
EOF
# 3. 首次部署(创建)
# 3. First deployment (create)
cd scripts && python deploy_filter.py my-new-filter
# 4. OpenWebUI UI 测试
# 5. 重复更新
# 4. Test in OpenWebUI UI
# 5. Repeat updates
cd scripts && python deploy_filter.py my-new-filter
```
### 用例 3: 版本更新和发布
### Use Case 3: Version Update and Release
```bash
# 1. 更新版本号
# 1. Update version number
vim plugins/filters/async-context-compression/async_context_compression.py
# 修改: version: 1.3.0 → version: 1.4.0
# Change: version: 1.3.0 → version: 1.4.0
# 2. 部署新版本
# 2. Deploy new version
cd scripts && python deploy_filter.py
# 3. 测试通过后提交
# 3. After testing, commit
git add plugins/filters/async-context-compression/
git commit -m "feat(filters): update async-context-compression to 1.4.0"
git push
```
## 🔄 CI/CD 集成
## 🔄 CI/CD Integration
### GitHub Actions 示例
### GitHub Actions Example
```yaml
name: Deploy Filter on Release
@@ -308,71 +308,71 @@ jobs:
api_key: ${{ secrets.OPENWEBUI_API_KEY }}
```
## 📚 参考文档
## 📚 Reference Documentation
- [完整部署指南](DEPLOYMENT_GUIDE.md)
- [快速参考卡片](QUICK_START.md)
- [测试套件](../tests/scripts/test_deploy_filter.py)
- [插件开发指南](../docs/development/plugin-guide.md)
- [OpenWebUI 文档](https://docs.openwebui.com/)
- [Complete Deployment Guide](DEPLOYMENT_GUIDE.md)
- [Quick Reference Card](QUICK_START.md)
- [Test Suite](../tests/scripts/test_deploy_filter.py)
- [Plugin Development Guide](../docs/development/plugin-guide.md)
- [OpenWebUI Documentation](https://docs.openwebui.com/)
## 🎓 学习资源
## 🎓 Learning Resources
### 架构理解
### Architecture Understanding
```
OpenWebUI 系统设计
OpenWebUI System Design
Filter 插件类型定义
Filter Plugin Type Definition
REST API 接口 (/api/v1/functions)
REST API Interface (/api/v1/functions)
本地部署脚本实现 (deploy_filter.py)
Local Deployment Script Implementation (deploy_filter.py)
元数据提取和投递
Metadata Extraction and Delivery
```
### 调试技巧
### Debugging Tips
1. **启用详细日志**:
1. **Enable Verbose Logging**:
```bash
python deploy_filter.py 2>&1 | tee deploy.log
```
2. **测试 API 连接**:
2. **Test API Connection**:
```bash
curl -X GET http://localhost:3003/api/v1/functions \
curl -X GET http://localhost:3000/api/v1/functions \
-H "Authorization: Bearer $API_KEY"
```
3. **验证 .env 文件**:
3. **Verify .env File**:
```bash
grep "api_key=" scripts/.env
```
## 📞 故障排除
## 📞 Troubleshooting
| 问题 | 诊断 | 解决方案 |
|------|------|----------|
| Connection error | OpenWebUI 地址/端口不对 | 检查 localhost:3003修改 URL 如需要 |
| .env not found | 未创建配置文件 | `echo "api_key=sk-..." > scripts/.env` |
| Filter not found | 插件名称错误 | 运行 `python deploy_filter.py --list` |
| Status 401 | API 密钥无效/过期 | 更新 `.env` 中的密钥 |
| Status 500 | 服务器错误 | 检查 OpenWebUI 服务日志 |
| Issue | Diagnosis | Solution |
|-------|-----------|----------|
| Connection error | Wrong OpenWebUI address/port | Check localhost:3000; modify URL if needed |
| .env not found | Config file not created | `echo "api_key=sk-..." > scripts/.env` |
| Filter not found | Wrong Plugin name | Run `python deploy_filter.py --list` |
| Status 401 | Invalid/expired API key | Update key in `.env` |
| Status 500 | Server error | Check OpenWebUI service logs |
## ✨ 特色功能
## ✨ Highlight Features
| 特性 | 描述 |
|------|------|
| 🔍 自动发现 | 自动查找所有 Filter 插件 |
| 📊 元数据提取 | 从代码自动提取版本和元数据 |
| ♻️ 自动更新 | 智能处理更新或创建 |
| 🛡️ 错误处理 | 详细的错误提示和诊断信息 |
| 🚀 快速迭代 | 秒级部署,无需重启 |
| 🧪 完整测试 | 10 个单元测试覆盖核心功能 |
| Feature | Description |
|---------|-------------|
| 🔍 Auto Discovery | Automatically find all Filter plugins |
| 📊 Metadata Extraction | Auto-extract version and metadata from code |
| ♻️ Auto-update | Smart handling of update or create |
| 🛡️ Error Handling | Detailed error messages and diagnostics |
| 🚀 Fast Iteration | Second-level deployment, no restart |
| 🧪 Complete Testing | 10 unit tests covering core functions |
---
**最后更新**: 2026-03-09
**作者**: Fu-Jie
**项目**: [openwebui-extensions](https://github.com/Fu-Jie/openwebui-extensions)
**Last Updated**: 2026-03-09
**Author**: Fu-Jie
**Project**: [openwebui-extensions](https://github.com/Fu-Jie/openwebui-extensions)

View File

@@ -1,76 +1,76 @@
# ⚡ 快速部署参考 (Quick Deployment Reference)
# ⚡ Quick Deployment Reference
## 一行命令部署
## One-line Deploy Commands
```bash
# 部署 async_context_compression Filter(默认)
# Deploy async_context_compression Filter (default)
cd scripts && python deploy_filter.py
# 列出所有可用 Filter
# List all available Filters
cd scripts && python deploy_filter.py --list
```
## 前置步骤(仅需一次)
## Setup Steps (One time only)
```bash
# 1. 进入 scripts 目录
# 1. Enter scripts directory
cd scripts
# 2. 创建 .env 文件,包含 OpenWebUI API 密钥
# 2. Create .env file with your OpenWebUI API key
echo "api_key=sk-your-api-key-here" > .env
# 3. 确保 OpenWebUI 运行在 localhost:3003
# 3. Make sure OpenWebUI is running on localhost:3000
```
## 获取 API 密钥
## Get Your API Key
1. 打开 OpenWebUI → 用户头像 → Settings
2. 找到 "API Keys" 部分
3. 复制密钥sk-开头)
4. 粘贴到 `.env` 文件
1. Open OpenWebUI → user avatar → Settings
2. Find "API Keys" section
3. Copy your key (starts with sk-)
4. Paste into `.env` file
## 部署流程
## Deployment Workflow
```bash
# 1. 编辑插件代码
# 1. Edit plugin code
vim ../plugins/filters/async-context-compression/async_context_compression.py
# 2. 部署到本地
# 2. Deploy to local
python deploy_filter.py
# 3. OpenWebUI 测试(无需重启)
# 3. Test in OpenWebUI (no restart needed)
# 4. 重复部署(自动覆盖)
# 4. Deploy again (auto-overwrites)
python deploy_filter.py
```
## 常见命令
## Common Commands
| 命令 | 说明 |
|------|------|
| `python deploy_filter.py` | 部署 async_context_compression |
| `python deploy_filter.py filter-name` | 部署指定 Filter |
| `python deploy_filter.py --list` | 列出所有可用 Filter |
| `python deploy_pipe.py` | 部署 GitHub Copilot SDK Pipe |
| Command | Description |
|---------|-------------|
| `python deploy_filter.py` | Deploy async_context_compression |
| `python deploy_filter.py filter-name` | Deploy specific Filter |
| `python deploy_filter.py --list` | List all available Filters |
| `python deploy_pipe.py` | Deploy GitHub Copilot SDK Pipe |
## 故障诊断
## Troubleshooting
| 错误 | 原因 | 解决方案 |
|------|------|----------|
| Connection error | OpenWebUI 未运行 | 启动 OpenWebUI 或检查端口 |
| .env not found | 未创建配置文件 | `echo "api_key=sk-..." > .env` |
| Filter not found | Filter 名称错误 | 运行 `python deploy_filter.py --list` |
| Status 401 | API 密钥无效 | 更新 `.env` 中的密钥 |
| Error | Cause | Solution |
|-------|-------|----------|
| Connection error | OpenWebUI not running | Start OpenWebUI or check port |
| .env not found | Config file not created | `echo "api_key=sk-..." > .env` |
| Filter not found | Filter name is wrong | Run `python deploy_filter.py --list` |
| Status 401 | API key invalid | Update key in `.env` |
## 文件位置
## File Locations
```
openwebui-extensions/
├── scripts/
│ ├── deploy_filter.py ← Filter 部署工具
│ ├── deploy_pipe.py ← Pipe 部署工具
│ ├── .env ← API 密钥(不提交)
│ └── DEPLOYMENT_GUIDE.md ← 完整指南
│ ├── deploy_filter.py ← Filter deployment tool
│ ├── deploy_pipe.py ← Pipe deployment tool
│ ├── .env ← API key (don't commit)
│ └── DEPLOYMENT_GUIDE.md ← Full guide
└── plugins/
└── filters/
@@ -80,26 +80,26 @@ openwebui-extensions/
└── README_CN.md
```
## 工作流建议
## Suggested Workflow
### 快速迭代开发
### Fast Iterative Development
```bash
# Terminal 1: 启动 OpenWebUI(如果未运行)
docker run -d -p 3003:8080 ghcr.io/open-webui/open-webui:latest
# Terminal 1: Start OpenWebUI (if not running)
docker run -d -p 3000:8080 ghcr.io/open-webui/open-webui:latest
# Terminal 2: 开发环节(重复执行)
# Terminal 2: Development loop (repeated)
cd scripts
code ../plugins/filters/async-context-compression/ # 编辑代码
python deploy_filter.py # 部署
# → OpenWebUI 测试
# → 返回编辑,重复
code ../plugins/filters/async-context-compression/ # Edit code
python deploy_filter.py # Deploy
# → Test in OpenWebUI
# → Go back to edit, repeat
```
### CI/CD 集成
### CI/CD Integration
```bash
# GitHub Actions
# In GitHub Actions
- name: Deploy filter to staging
run: |
cd scripts
@@ -110,4 +110,4 @@ python deploy_filter.py # 部署
---
📚 **更多帮助**: 查看 `DEPLOYMENT_GUIDE.md`
📚 **More Help**: See `DEPLOYMENT_GUIDE.md`

View File

@@ -1,70 +1,84 @@
# 🚀 部署脚本使用指南 (Deployment Scripts Guide)
# 🚀 Deployment Scripts Guide
## 📁 新增部署工具
## 📁 Deployment Tools
为了支持快速本地部署 async_context_compression 和其他 Filter 插件,我们添加了以下文件:
To support quick local deployment of async_context_compression and other Filter plugins, we've added the following files:
### 具体文件列表
### File Inventory
```
scripts/
├── deploy_filter.py ✨ 通用 Filter 部署工具
├── deploy_async_context_compression.py ✨ Async Context Compression 快捷部署
├── deploy_pipe.py (已有) Pipe 部署工具
├── DEPLOYMENT_GUIDE.md ✨ 完整部署指南
├── DEPLOYMENT_SUMMARY.md ✨ 部署功能总结
├── QUICK_START.md ✨ 快速参考卡片
├── .env (需要创建) API 密钥配置
── ...其他现有脚本
├── install_all_plugins.py ✨ Batch install Action/Filter/Pipe/Tool plugins
├── deploy_filter.py ✨ Generic Filter deployment tool
├── deploy_tool.py ✨ Tool plugin deployment tool
├── deploy_async_context_compression.py ✨ Async Context Compression quick deploy
├── deploy_pipe.py (existing) Pipe deployment tool
├── DEPLOYMENT_GUIDE.md ✨ Complete deployment guide
├── DEPLOYMENT_SUMMARY.md ✨ Deploy feature summary
── QUICK_START.md ✨ Quick reference card
├── .env (create as needed) API key configuration
└── ...other existing scripts
```
## ⚡ 快速开始 (30 秒)
## ⚡ Quick Start (30 seconds)
### 步骤 1: 准备 API 密钥
### Step 1: Prepare Your API Key
```bash
cd scripts
# 获取你的 OpenWebUI API 密钥:
# 1. 打开 OpenWebUI → 用户菜单 → Settings
# 2. 找到 "API Keys" 部分
# 3. 复制你的密钥(以 sk- 开头)
# Get your OpenWebUI API key:
# 1. Open OpenWebUI → User menu → Settings
# 2. Find the "API Keys" section
# 3. Copy your key (starts with sk-)
# 创建 .env 文件
echo "api_key=sk-你的密钥" > .env
# Create .env file
cat > .env <<'EOF'
api_key=sk-your-key-here
url=http://localhost:3000
EOF
```
### 步骤 2: 部署异步上下文压缩
### Step 2a: Install All Plugins (Recommended)
```bash
# 最简单的方式 - 专用脚本
python install_all_plugins.py
```
### Step 2b: Or Deploy Individual Plugins
```bash
# Easiest way - dedicated script
python deploy_async_context_compression.py
# 或使用通用脚本
# Or use generic script
python deploy_filter.py
# 或指定插件名称
# Or specify plugin name
python deploy_filter.py async-context-compression
# Or deploy a Tool
python deploy_tool.py
```
## 📋 部署工具详解
## 📋 Deployment Tools Detailed
### 1⃣ `deploy_async_context_compression.py` — 专用部署脚本
### 1⃣ `deploy_async_context_compression.py` — Dedicated Deployment Script
**最简单的部署方式!**
**The simplest way to deploy!**
```bash
cd scripts
python deploy_async_context_compression.py
```
**特点**:
-专为 async_context_compression 优化
-清晰的部署步骤和确认
-友好的错误提示
-部署成功后显示后续步骤
**Features**:
-Optimized specifically for async_context_compression
-Clear deployment steps and confirmation
-Friendly error messages
-Shows next steps after successful deployment
**输出样例**:
**Sample Output**:
```
======================================================================
🚀 Deploying Async Context Compression Filter Plugin
@@ -79,269 +93,314 @@ python deploy_async_context_compression.py
======================================================================
Next steps:
1. Open OpenWebUI in your browser: http://localhost:3003
1. Open OpenWebUI in your browser: http://localhost:3000
2. Go to Settings → Filters
3. Enable 'Async Context Compression'
4. Configure Valves as needed
5. Start using the filter in conversations
```
### 2⃣ `deploy_filter.py` — 通用 Filter 部署工具
### 2⃣ `deploy_filter.py` — Generic Filter Deployment Tool
**支持所有 Filter 插件!**
**Supports all Filter plugins!**
```bash
# 部署默认的 async_context_compression
# Deploy default async_context_compression
python deploy_filter.py
# 部署其他 Filter
# Deploy other Filters
python deploy_filter.py folder-memory
python deploy_filter.py context_enhancement_filter
# 列出所有可用 Filter
# List all available Filters
python deploy_filter.py --list
```
**特点**:
-通用的 Filter 部署工具
-支持多个插件
-自动元数据提取
-智能更新/创建逻辑
-完整的错误诊断
**Features**:
-Generic Filter deployment tool
-Supports multiple plugins
-Auto metadata extraction
-Smart update/create logic
-Complete error diagnostics
### 3⃣ `deploy_pipe.py` — Pipe 部署工具
### 3⃣ `deploy_pipe.py` — Pipe Deployment Tool
```bash
python deploy_pipe.py
```
用于部署 Pipe 类型的插件(如 GitHub Copilot SDK)。
Used to deploy Pipe-type plugins (like GitHub Copilot SDK).
## 🔧 工作原理
```
你的代码变更
运行部署脚本
脚本读取对应插件文件
从代码自动提取元数据 (title, version, author, etc.)
构建 API 请求
发送到本地 OpenWebUI
OpenWebUI 更新或创建插件
立即生效!(无需重启)
```
## 📊 可部署的 Filter 列表
使用 `python deploy_filter.py --list` 查看所有可用 Filter
| Filter 名称 | Python 文件 | 描述 |
|-----------|-----------|------|
| **async-context-compression** | async_context_compression.py | 异步上下文压缩 |
| chat-session-mapping-filter | chat_session_mapping_filter.py | 聊天会话映射 |
| context_enhancement_filter | context_enhancement_filter.py | 上下文增强 |
| folder-memory | folder_memory.py | 文件夹记忆 |
| github_copilot_sdk_files_filter | github_copilot_sdk_files_filter.py | Copilot SDK Files |
| markdown_normalizer | markdown_normalizer.py | Markdown 规范化 |
| web_gemini_multimodel_filter | web_gemini_multimodel_filter.py | Gemini 多模态 |
## 🎯 常见使用场景
### 场景 1: 开发新功能后部署
### 3⃣+ `deploy_tool.py` — Tool Deployment Tool
```bash
# 1. 修改代码
# Deploy default Tool
python deploy_tool.py
# Or specify a specific Tool
python deploy_tool.py openwebui-skills-manager
```
**Features**:
- ✅ Supports Tools plugin deployment
- ✅ Auto-detects `Tools` class definition
- ✅ Smart update/create logic
- ✅ Complete error diagnostics
**Use Case**:
Deploy or reinstall a specific Tool individually, or deploy only Tools without running full batch installation. The script now calls OpenWebUI's native `/api/v1/tools/*` endpoints.
### 4⃣ `install_all_plugins.py` — Batch Installation Script
One-command installation of all repository plugins that meet these criteria:
- Located in `plugins/actions`, `plugins/filters`, `plugins/pipes`, `plugins/tools`
- Plugin header contains `openwebui_id`
- Filename is not in Chinese characters
- Filename does not end with `_cn.py`
```bash
# Check which plugins will be installed
python install_all_plugins.py --list
# Dry-run without calling API
python install_all_plugins.py --dry-run
# Actually install all supported types (including Action/Filter/Pipe/Tool)
python install_all_plugins.py
# Install only specific types
python install_all_plugins.py --types pipe action
```
The script prioritizes updating existing plugins and automatically creates new ones.
**Tool Integration**: Tool-type plugins now automatically use OpenWebUI's native `/api/v1/tools/create` and `/api/v1/tools/id/{id}/update` endpoints, no longer reusing the `functions` endpoint.
## 🔧 How It Works
```
Your code changes
Run deployment script
Script reads the corresponding plugin file
Auto-extracts metadata from code (title, version, author, etc.)
Builds API request
Sends to local OpenWebUI
OpenWebUI updates or creates plugin
Takes effect immediately! (no restart needed)
```
## 📊 Available Filter List
Use `python deploy_filter.py --list` to see all available Filters:
| Filter Name | Python File | Description |
|-----------|-----------|------|
| **async-context-compression** | async_context_compression.py | Async context compression |
| chat-session-mapping-filter | chat_session_mapping_filter.py | Chat session mapping |
| context_enhancement_filter | context_enhancement_filter.py | Context enhancement |
| folder-memory | folder_memory.py | Folder memory |
| github_copilot_sdk_files_filter | github_copilot_sdk_files_filter.py | Copilot SDK Files |
| markdown_normalizer | markdown_normalizer.py | Markdown normalization |
| web_gemini_multimodel_filter | web_gemini_multimodel_filter.py | Gemini multimodal |
## 🎯 Common Use Cases
### Scenario 1: Deploy After Feature Development
```bash
# 1. Modify code
vim ../plugins/filters/async-context-compression/async_context_compression.py
# 2. 更新版本号(可选)
# 2. Update version number (optional)
# version: 1.3.0 → 1.3.1
# 3. 部署
# 3. Deploy
python deploy_async_context_compression.py
# 4. OpenWebUI 中测试
# → 无需重启,立即生效!
# 4. Test in OpenWebUI
# → No restart needed, takes effect immediately!
# 5. 继续开发,重复上述步骤
# 5. Continue development and repeat
```
### 场景 2: 修复 Bug 并快速验证
### Scenario 2: Fix Bug and Verify Quickly
```bash
# 1. 定位并修复 Bug
# 1. Find and fix bug
vim ../plugins/filters/async-context-compression/async_context_compression.py
# 2. 快速部署验证
# 2. Quick deploy to verify
python deploy_async_context_compression.py
# 3. 在 OpenWebUI 测试 Bug 修复
# 一键部署,秒级反馈!
# 3. Test bug fix in OpenWebUI
# One-command deploy, instant feedback!
```
### 场景 3: 部署多个 Filter
### Scenario 3: Deploy Multiple Filters
```bash
# 部署所有需要更新的 Filter
# Deploy all Filters that need updates
python deploy_filter.py async-context-compression
python deploy_filter.py folder-memory
python deploy_filter.py context_enhancement_filter
```
## 🔐 安全提示
## 🔐 Security Tips
### 管理 API 密钥
### Manage API Keys
```bash
# 1. 创建 .env只在本地
# 1. Create .env (local only)
echo "api_key=sk-your-key" > .env
# 2. 添加到 .gitignore(防止提交)
# 2. Add to .gitignore (prevent commit)
echo "scripts/.env" >> ../.gitignore
# 3. 验证不会被提交
git status # 应该看不到 .env
# 3. Verify it won't be committed
git status # should not show .env
# 4. 定期轮换密钥
# → OpenWebUI Settings 中生成新密钥
# → 更新 .env 文件
# 4. Rotate keys regularly
# → Generate new key in OpenWebUI Settings
# → Update .env file
```
### ✅ 安全检查清单
### ✅ Security Checklist
- [ ] `.env` 文件在 `.gitignore`
- [ ] 从不在代码中硬编码 API 密钥
- [ ] 定期轮换 API 密钥
- [ ] 仅在可信网络中使用
- [ ] 生产环境使用 CI/CD 秘密管理
- [ ] `.env` file is in `.gitignore`
- [ ] Never hardcode API keys in code
- [ ] Rotate API keys periodically
- [ ] Use only on trusted networks
- [ ] Use CI/CD secret management in production
## ❌ 故障排除
## ❌ Troubleshooting
### 问题 1: "Connection error"
### Issue 1: "Connection error"
```
❌ Connection error: Could not reach OpenWebUI at localhost:3003
❌ Connection error: Could not reach OpenWebUI at localhost:3000
Make sure OpenWebUI is running and accessible.
```
**解决方案**:
**Solution**:
```bash
# 1. 检查 OpenWebUI 是否运行
curl http://localhost:3003
# 1. Check if OpenWebUI is running
curl http://localhost:3000
# 2. 如果端口不同,编辑脚本中的 URL
# 默认: http://localhost:3003
# 修改位置: deploy_filter.py 中的 "localhost:3003"
# 2. If port is different, edit URL in script
# Default: http://localhost:3000
# Location: "localhost:3000" in deploy_filter.py
# 3. 检查防火墙设置
# 3. Check firewall settings
```
### 问题 2: ".env file not found"
### Issue 2: ".env file not found"
```
❌ [ERROR] .env file not found at .env
Please create it with: api_key=sk-xxxxxxxxxxxx
```
**解决方案**:
**Solution**:
```bash
echo "api_key=sk-your-api-key" > .env
cat .env # 验证文件已创建
cat .env # verify file created
```
### 问题 3: "Filter not found"
### Issue 3: "Filter not found"
```
❌ [ERROR] Filter 'xxx' not found in .../plugins/filters
```
**解决方案**:
**Solution**:
```bash
# 列出所有可用 Filter
# List all available Filters
python deploy_filter.py --list
# 使用正确的名称重试
# Retry with correct name
python deploy_filter.py async-context-compression
```
### 问题 4: "Status 401" (Unauthorized)
### Issue 4: "Status 401" (Unauthorized)
```
❌ Failed to update or create. Status: 401
Error: {"error": "Unauthorized"}
```
**解决方案**:
**Solution**:
```bash
# 1. 验证 API 密钥是否正确
# 1. Verify API key is correct
grep "api_key=" .env
# 2. 在 OpenWebUI 中检查密钥是否仍然有效
# Settings → API Keys → 检查
# 2. Check if key is still valid in OpenWebUI
# Settings → API Keys → Check
# 3. 生成新密钥并更新 .env
# 3. Generate new key and update .env
echo "api_key=sk-new-key" > .env
```
## 📖 文档导航
## 📖 Documentation Navigation
| 文档 | 描述 |
| Document | Description |
|------|------|
| **README.md** (本文件) | 快速参考和常见问题 |
| [QUICK_START.md](QUICK_START.md) | 一页速查表 |
| [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md) | 完整详细指南 |
| [DEPLOYMENT_SUMMARY.md](DEPLOYMENT_SUMMARY.md) | 技术架构说明 |
| **README.md** (this file) | Quick reference and FAQs |
| [QUICK_START.md](QUICK_START.md) | One-page cheat sheet |
| [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md) | Complete detailed guide |
| [DEPLOYMENT_SUMMARY.md](DEPLOYMENT_SUMMARY.md) | Technical architecture |
## 🧪 验证部署成功
## 🧪 Verify Deployment Success
### 方式 1: 检查脚本输出
### Method 1: Check Script Output
```bash
python deploy_async_context_compression.py
# 成功标志:
# Success indicator:
✅ Successfully updated 'Async Context Compression' filter!
```
### 方式 2: 在 OpenWebUI 中验证
### Method 2: Verify in OpenWebUI
1. 打开 OpenWebUI: http://localhost:3003
2. 进入 Settings → Filters
3. 查看 "Async Context Compression" 是否列出
4. 查看版本号是否正确(应该是最新的)
1. Open OpenWebUI: http://localhost:3000
2. Go to Settings → Filters
3. Check if 'Async Context Compression' is listed
4. Verify version number is correct (should be latest)
### 方式 3: 测试插件功能
### Method 3: Test Plugin Functionality
1. 打开一个新对话
2. 启用 "Async Context Compression" Filter
3. 进行多轮对话,验证压缩和总结功能正常
1. Open a new conversation
2. Enable 'Async Context Compression' Filter
3. Have multiple-turn conversation and verify compression/summarization works
## 💡 Advanced Usage
## 💡 高级用法
### 自动化部署测试
### Automated Deploy & Test
```bash
#!/bin/bash
# deploy_and_test.sh
echo "部署插件..."
echo "Deploying plugin..."
python scripts/deploy_async_context_compression.py
if [ $? -eq 0 ]; then
echo "✅ 部署成功,运行测试..."
echo "✅ Deploy successful, running tests..."
python -m pytest tests/plugins/filters/async-context-compression/ -v
else
echo "❌ 部署失败"
echo "❌ Deploy failed"
exit 1
fi
```
### CI/CD 集成
### CI/CD Integration
```yaml
# .github/workflows/deploy.yml
@@ -362,55 +421,56 @@ jobs:
api_key: ${{ secrets.OPENWEBUI_API_KEY }}
```
## 📞 获取帮助
## 📞 Getting Help
### 检查脚本状态
### Check Script Status
```bash
# 列出所有可用脚本
# List all available scripts
ls -la scripts/*.py
# 检查部署脚本是否存在
# Check if deployment scripts exist
ls -la scripts/deploy_*.py
```
### 查看脚本版本
### View Script Help
```bash
# 查看脚本帮助
python scripts/deploy_filter.py --help # 如果支持的话
# View help (if supported)
python scripts/deploy_filter.py --help # if supported
python scripts/deploy_async_context_compression.py --help
```
### 调试模式
### Debug Mode
```bash
# 保存输出到日志文件
# Save output to log file
python scripts/deploy_async_context_compression.py | tee deploy.log
# 检查日志
# Check log
cat deploy.log
```
---
## 📝 文件清单
## 📝 File Checklist
新增的部署相关文件:
Newly created deployment-related files:
```
✨ scripts/deploy_filter.py (新增) ~300
✨ scripts/deploy_async_context_compression.py (新增) ~70
✨ scripts/DEPLOYMENT_GUIDE.md (新增) 完整指南
✨ scripts/DEPLOYMENT_SUMMARY.md (新增) 技术总结
✨ scripts/QUICK_START.md (新增) 快速参考
📄 tests/scripts/test_deploy_filter.py (新增) 10 个单元测试
✨ scripts/deploy_filter.py (new) ~300 lines
✨ scripts/deploy_async_context_compression.py (new) ~70 lines
✨ scripts/DEPLOYMENT_GUIDE.md (new) complete guide
✨ scripts/DEPLOYMENT_SUMMARY.md (new) technical summary
✨ scripts/QUICK_START.md (new) quick reference
📄 tests/scripts/test_deploy_filter.py (new) 10 unit tests
所有文件已创建并测试通过!
All files created and tested successfully!
```
---
**最后更新**: 2026-03-09
**脚本状态**: ✅ Ready for production
**测试覆盖**: 10/10 通过
**Last Updated**: 2026-03-09
**Script Status**: ✅ Ready for production
**Test Coverage**: 10/10 passed

View File

@@ -1,45 +1,45 @@
# 🔄 部署脚本的更新机制 (Deployment Update Mechanism)
# 🔄 Deployment Scripts Update Mechanism
## 核心答案
## Core Answer
**是的,再次部署会自动更新!**
**Yes, re-deploying automatically updates the plugin!**
部署脚本采用**智能两阶段策略**
1. 🔄 **优先尝试更新** (UPDATE) — 如果插件已存在
2. 📝 **自动创建** (CREATE) — 如果更新失败(插件不存在)
The deployment script uses a **smart two-stage strategy**:
1. 🔄 **Try UPDATE First** (if plugin exists)
2. 📝 **Auto CREATE** (if update fails — plugin doesn't exist)
## 工作流程图
## Workflow Diagram
```
运行部署脚本
Run deploy script
读取本地代码和元数据
Read local code and metadata
发送 UPDATE 请求到 OpenWebUI
Send UPDATE request to OpenWebUI
├─ HTTP 200 ✅
│ └─ 插件已存在 → 更新成功!
│ └─ Plugin exists → Update successful!
└─ 其他状态代码 (404, 400)
└─ 插件不存在或更新失败
└─ Other status codes (404, 400, etc.)
└─ Plugin doesn't exist or update failed
发送 CREATE 请求
Send CREATE request
├─ HTTP 200 ✅
│ └─ 创建成功!
│ └─ Creation successful!
└─ 失败
└─ 显示错误信息
└─ Failed
└─ Display error message
```
## 详细步骤分析
## Detailed Step-by-step
### 步骤 1⃣: 尝试更新 (UPDATE)
### Step 1⃣: Try UPDATE First
```python
# 代码位置: deploy_filter.py 220-230
# Code location: deploy_filter.py line 220-230
update_url = "http://localhost:3003/api/v1/functions/id/{filter_id}/update"
update_url = "http://localhost:3000/api/v1/functions/id/{filter_id}/update"
response = requests.post(
update_url,
@@ -53,24 +53,24 @@ if response.status_code == 200:
return True
```
**这一步**:
- 向 OpenWebUI API 发送 **POST** `/api/v1/functions/id/{filter_id}/update`
- 如果返回 **HTTP 200**,说明插件已存在且成功更新
- 包含的内容:
- 完整的最新代码
- 元数据 (title, version, author, description)
- 清单信息 (manifest)
**What Happens**:
- Send **POST** to `/api/v1/functions/id/{filter_id}/update`
- If returns **HTTP 200**, plugin exists and update succeeded
- Includes:
- Complete latest code
- Metadata (title, version, author, description, etc.)
- Manifest information
### 步骤 2⃣: 若更新失败,尝试创建 (CREATE)
### Step 2⃣: If UPDATE Fails, Try CREATE
```python
# 代码位置: deploy_filter.py 231-245
# Code location: deploy_filter.py line 231-245
if response.status_code != 200:
print(f"⚠️ Update failed with status {response.status_code}, "
"attempting to create instead...")
create_url = "http://localhost:3003/api/v1/functions/create"
create_url = "http://localhost:3000/api/v1/functions/create"
res_create = requests.post(
create_url,
headers=headers,
@@ -83,96 +83,96 @@ if response.status_code != 200:
return True
```
**这一步**:
- 如果更新失败 (HTTP ≠ 200),自动尝试创建
- `/api/v1/functions/create` 发送 **POST** 请求
- 使用**相同的 payload**(代码、元数据都一样)
- 如果创建成功,第一次部署到 OpenWebUI
**What Happens**:
- If update fails (HTTP ≠ 200), auto-attempt create
- Send **POST** to `/api/v1/functions/create`
- Uses **same payload** (code, metadata identical)
- If creation succeeds, first deployment to OpenWebUI
## 实际使用场景
## Real-world Scenarios
### 场景 A: 第一次部署
### Scenario A: First Deployment
```bash
$ python deploy_async_context_compression.py
📦 Deploying filter 'Async Context Compression' (version 1.3.0)...
File: .../async_context_compression.py
⚠️ Update failed with status 404, attempting to create instead... ← 第一次,插件不存在
✅ Successfully created 'Async Context Compression' filter! ← 创建成功
⚠️ Update failed with status 404, attempting to create instead... ← First time, plugin doesn't exist
✅ Successfully created 'Async Context Compression' filter! ← Creation succeeds
```
**发生的事**:
1. 尝试 UPDATE → 失败 (HTTP 404 — 插件不存在)
2. 自动尝试 CREATE → 成功 (HTTP 200)
3. 插件被创建到 OpenWebUI
**What Happens**:
1. Try UPDATE → fails (HTTP 404 — plugin doesn't exist)
2. Auto-try CREATE → succeeds (HTTP 200)
3. Plugin created in OpenWebUI
---
### 场景 B: 再次部署 (修改代码后)
### Scenario B: Re-deploy After Code Changes
```bash
# 第一次修改代码,再次部署
# Made first code change, deploying again
$ python deploy_async_context_compression.py
📦 Deploying filter 'Async Context Compression' (version 1.3.1)...
File: .../async_context_compression.py
✅ Successfully updated 'Async Context Compression' filter! ← 直接更新!
✅ Successfully updated 'Async Context Compression' filter! ← Direct update!
```
**发生的事**:
1. 读取修改后的代码
2. 尝试 UPDATE → 成功 (HTTP 200 — 插件已存在)
3. OpenWebUI 中的插件被更新为最新代码
4. **无需重启 OpenWebUI**,立即生效!
**What Happens**:
1. Read modified code
2. Try UPDATE → succeeds (HTTP 200 — plugin exists)
3. Plugin in OpenWebUI updated to latest code
4. **No need to restart OpenWebUI**, takes effect immediately!
---
### 场景 C: 多次快速迭代
### Scenario C: Multiple Fast Iterations
```bash
# 第1次修改
# 1st change
$ python deploy_async_context_compression.py
✅ Successfully updated 'Async Context Compression' filter!
# 第2次修改
# 2nd change
$ python deploy_async_context_compression.py
✅ Successfully updated 'Async Context Compression' filter!
# 第3次修改
# 3rd change
$ python deploy_async_context_compression.py
✅ Successfully updated 'Async Context Compression' filter!
# ... 无限制地重复 ...
# ... repeat infinitely ...
```
**特点**:
- 🚀 每次更新只需 5 秒
- 📝 每次都是增量更新
-无需重启 OpenWebUI
- 🔄 可以无限制地重复
**Characteristics**:
- 🚀 Each update takes only 5 seconds
- 📝 Each is an incremental update
-No need to restart OpenWebUI
- 🔄 Can repeat indefinitely
## 更新的内容清单
## What Gets Updated
每次部署时,以下内容会被更新:
Each deployment updates the following:
**代码**全部最新的 Python 代码
**版本号** docstring 自动提取
**标题**插件的显示名称
**作者信息** — author, author_url
**描述**plugin description
**元数据** — funding_url, openwebui_id
**Code**All latest Python code
**Version**Auto-extracted from docstring
**Title**Plugin display name
**Author Info** — author, author_url
**Description**Plugin description
**Metadata** — funding_url, openwebui_id, etc.
**配置不会被覆盖** — 用户在 OpenWebUI 中设置的 Valves 配置保持不变
**Configuration NOT Overwritten** — User's Valves settings in OpenWebUI stay unchanged
## 版本号管理
## Version Number Management
### 更新时版本号会变吗?
### Does Version Change on Update?
**是的,会变!**
**Yes!**
```python
# async_context_compression.py 的 docstring
# docstring in async_context_compression.py
"""
title: Async Context Compression
@@ -180,124 +180,124 @@ version: 1.3.0
"""
```
**每次部署时**:
1. 脚本从 docstring 读取版本号
2. 发送给 OpenWebUI 的 manifest 包含这个版本号
3. 如果代码中改了版本号,部署时会更新到新版本
**Each deployment**:
1. Script reads version from docstring
2. Sends this version in manifest to OpenWebUI
3. If you change version in code, deployment updates to new version
**最佳实践**:
**Best Practice**:
```bash
# 1. 修改代码
# 1. Modify code
vim async_context_compression.py
# 2. 更新版本号(在 docstring 中)
# 版本: 1.3.0 → 1.3.1
# 2. Update version (in docstring)
# version: 1.3.0 → 1.3.1
# 3. 部署
# 3. Deploy
python deploy_async_context_compression.py
# 结果: OpenWebUI 中显示版本 1.3.1
# Result: OpenWebUI shows version 1.3.1
```
## 部署失败的情况
## Deployment Failure Cases
### 情况 1: 网络错误
### Case 1: Network Error
```bash
❌ Connection error: Could not reach OpenWebUI at localhost:3003
❌ Connection error: Could not reach OpenWebUI at localhost:3000
Make sure OpenWebUI is running and accessible.
```
**原因**: OpenWebUI 未运行或端口错误
**解决**: 检查 OpenWebUI 是否在运行
**Cause**: OpenWebUI not running or wrong port
**Solution**: Check if OpenWebUI is running
### 情况 2: API 密钥无效
### Case 2: Invalid API Key
```bash
❌ Failed to update or create. Status: 401
Error: {"error": "Unauthorized"}
```
**原因**: .env 中的 API 密钥无效或过期
**解决**: 更新 `.env` 文件中的 api_key
**Cause**: API key in .env is invalid or expired
**Solution**: Update api_key in `.env` file
### 情况 3: 服务器错误
### Case 3: Server Error
```bash
❌ Failed to update or create. Status: 500
Error: Internal server error
```
**原因**: OpenWebUI 服务器内部错误
**解决**: 检查 OpenWebUI 日志
**Cause**: OpenWebUI server internal error
**Solution**: Check OpenWebUI logs
## 设置版本号的最佳实践
## Setting Version Numbers — Best Practices
### 语义化版本 (Semantic Versioning)
### Semantic Versioning
遵循 `MAJOR.MINOR.PATCH` 格式:
Follow `MAJOR.MINOR.PATCH` format:
```python
"""
version: 1.3.0
│ │ │
│ │ └─ PATCH: Bug 修复 (1.3.0 → 1.3.1)
│ └────── MINOR: 新功能 (1.3.0 → 1.4.0)
└───────── MAJOR: 破坏性变更 (1.3.0 → 2.0.0)
│ │ └─ PATCH: Bug fixes (1.3.0 → 1.3.1)
│ └────── MINOR: New features (1.3.0 → 1.4.0)
└───────── MAJOR: Breaking changes (1.3.0 → 2.0.0)
"""
```
**例子**:
**Examples**:
```python
# Bug 修复 (PATCH)
# Bug fix (PATCH)
version: 1.3.0 1.3.1
# 新功能 (MINOR)
# New feature (MINOR)
version: 1.3.0 1.4.0
# 重大更新 (MAJOR)
# Major update (MAJOR)
version: 1.3.0 2.0.0
```
## 完整的迭代工作流
## Complete Iteration Workflow
```bash
# 1. 首次部署
# 1. First deployment
cd scripts
python deploy_async_context_compression.py
# 结果: 创建插件 (第一次)
# Result: Plugin created (first time)
# 2. 修改代码
# 2. Modify code
vim ../plugins/filters/async-context-compression/async_context_compression.py
# 修改内容...
# Edit code...
# 3. 再次部署 (自动更新)
# 3. Deploy again (auto-update)
python deploy_async_context_compression.py
# 结果: 更新插件 (立即生效,无需重启 OpenWebUI)
# Result: Plugin updated (takes effect immediately, no OpenWebUI restart)
# 4. 重复步骤 2-3无限次迭代
# 每次修改 → 每次部署 → 立即测试 → 继续改进
# 4. Repeat steps 2-3 indefinitely
# Modify → Deploy → Test → Improve → Repeat
```
## 自动更新的优势
## Benefits of Auto-update
| 优势 | 说明 |
|-----|------|
| ⚡ **快速迭代** | 修改代码 → 部署 (5) → 测试,无需等待 |
| 🔄 **自动检测** | 无需手动判断是创建还是更新 |
| 📝 **版本管理** | 版本号自动从代码提取 |
| ✅ **无需重启** | OpenWebUI 无需重启,配置保持不变 |
| 🛡️ **安全更新** | 用户配置 (Valves) 不会被覆盖 |
| Benefit | Details |
|---------|---------|
| ⚡ **Fast Iteration** | Code change → Deploy (5s) → Test, no waiting |
| 🔄 **Auto-detection** | No manual decision between create/update |
| 📝 **Version Management** | Version auto-extracted from code |
| ✅ **No Restart Needed** | OpenWebUI runs continuously, config stays same |
| 🛡️ **Safe Updates** | User settings (Valves) never overwritten |
## 禁用自动更新? ❌
## Disable Auto-update? ❌
通常**不需要**禁用自动更新,因为:
Usually **not needed** because:
1.更新是幂等的 (多次更新相同代码 = 无变化)
2.用户配置不会被修改
3.版本号自动管理
4.失败时自动回退
1.Updates are idempotent (same code deployed multiple times = no change)
2.User configuration not modified
3.Version numbers auto-managed
4.Failures auto-rollback
但如果真的需要控制,可以:
- 手动修改脚本 (修改 `deploy_filter.py`)

View File

@@ -1,91 +1,91 @@
# 🔄 快速参考:部署更新机制 (Quick Reference)
# 🔄 Quick Reference: Deployment Update Mechanism
## 最简短的答案
## The Shortest Answer
**再次部署会自动更新。**
**Re-deploying automatically updates the plugin.**
## 工作原理 (30 秒理解)
## How It Works (30-second understanding)
```
每次运行部署脚本:
1. 优先尝试 UPDATE如果插件已存在→ 更新成功
2. 失败时自动 CREATE第一次部署时→ 创建成功
Each time you run the deploy script:
1. Priority: try UPDATE (if plugin exists) → succeeds
2. Fallback: auto CREATE (first deployment) → succeeds
结果:
不管第几次部署,脚本都能正确处理
无需手动判断创建还是更新
立即生效,无需重启
Result:
Works correctly every time, regardless of deployment count
No manual judgement needed between create vs update
Takes effect immediately, no restart needed
```
## 三个场景
## Three Scenarios
| 场景 | 发生什么 | 结果 |
|------|---------|------|
| **第1次部署** | UPDATE 失败 → CREATE 成功 | ✅ 插件被创建 |
| **修改代码后再次部署** | UPDATE 直接成功 | ✅ 插件立即更新 |
| **未修改,重复部署** | UPDATE 成功 (无任何变化) | ✅ 无效果 (安全) |
| Scenario | What Happens | Result |
|----------|-------------|--------|
| **First deployment** | UPDATE fails → CREATE succeeds | ✅ Plugin created |
| **Deploy after code change** | UPDATE succeeds directly | ✅ Plugin updates instantly |
| **Deploy without changes** | UPDATE succeeds (no change) | ✅ Safe (no effect) |
## 开发流程
## Development Workflow
```bash
# 1. 第一次部署
# 1. First deployment
python deploy_async_context_compression.py
# 结果: ✅ Created
# Result: ✅ Created
# 2. 修改代码
# 2. Modify code
vim ../plugins/filters/async-context-compression/async_context_compression.py
# 编辑...
# Edit...
# 3. 再次部署 (自动更新)
# 3. Deploy again (auto-update)
python deploy_async_context_compression.py
# 结果: ✅ Updated
# Result: ✅ Updated
# 4. 继续修改,重复部署
# ... 可以无限重复 ...
# 4. Continue editing and redeploying
# ... can repeat infinitely ...
```
## 关键点
## Key Points
**自动化** — 不用管是更新还是创建
**快速**每次部署 5 秒
**安全**用户配置不会被覆盖
**即时**无需重启 OpenWebUI
**版本管理** — 自动从代码提取版本号
**Automated** — No need to worry about create vs update
**Fast**Each deployment takes 5 seconds
**Safe**User configuration never gets overwritten
**Instant**No need to restart OpenWebUI
**Version Management** — Auto-extracted from code
## 版本号怎么管理?
## How to Manage Version Numbers?
修改代码中的版本号:
Modify the version in your code:
```python
# async_context_compression.py
"""
version: 1.3.0 → 1.3.1 (修复 Bug)
version: 1.3.0 → 1.4.0 (新功能)
version: 1.3.0 → 2.0.0 (重大更新)
version: 1.3.0 → 1.3.1 (Bug fixes)
version: 1.3.0 → 1.4.0 (New features)
version: 1.3.0 → 2.0.0 (Major updates)
"""
```
然后部署,脚本会自动读取新版本号并更新。
Then deploy, the script will auto-read the new version and update.
## 常见问题速答
## Quick Q&A
**Q: 用户的配置会被覆盖吗?**
A: ❌ 不会,Valves 配置保持不变
**Q: Will user configuration be overwritten?**
A: ❌ No, Valves configuration stays the same
**Q: 需要重启 OpenWebUI 吗?**
A: ❌ 不需要,立即生效
**Q: Do I need to restart OpenWebUI?**
A: ❌ No, takes effect immediately
**Q: 更新失败了会怎样?**
A: ✅ 安全,保持原有插件不变
**Q: What if update fails?**
A: ✅ Safe, keeps original plugin intact
**Q: 可以无限制地重复部署吗?**
A: ✅ 可以,完全幂等
**Q: Can I deploy unlimited times?**
A: ✅ Yes, completely idempotent
## 一行总结
## One-liner Summary
> 首次部署创建插件之后每次部署自动更新5 秒即时反馈,无需重启。
> First deployment creates plugin, subsequent deployments auto-update, 5-second feedback, no restart needed.
---
📖 详细文档:`scripts/UPDATE_MECHANISM.md`
📖 Full docs: `scripts/UPDATE_MECHANISM.md`

View File

@@ -12,7 +12,7 @@ To get started:
1. Create .env file with your OpenWebUI API key:
echo "api_key=sk-your-key-here" > .env
2. Make sure OpenWebUI is running on localhost:3003
2. Make sure OpenWebUI is running on localhost:3000
3. Run this script:
python deploy_async_context_compression.py
@@ -45,7 +45,7 @@ def main():
print("=" * 70)
print()
print("Next steps:")
print(" 1. Open OpenWebUI in your browser: http://localhost:3003")
print(" 1. Open OpenWebUI in your browser: http://localhost:3000")
print(" 2. Go to Settings → Filters")
print(" 3. Enable 'Async Context Compression'")
print(" 4. Configure Valves as needed")
@@ -58,7 +58,7 @@ def main():
print("=" * 70)
print()
print("Troubleshooting:")
print(" • Check that OpenWebUI is running: http://localhost:3003")
print(" • Check that OpenWebUI is running: http://localhost:3000")
print(" • Verify API key in .env file")
print(" • Check network connectivity")
print()

View File

@@ -211,8 +211,8 @@ def deploy_filter(filter_name: str = DEFAULT_FILTER) -> bool:
}
# 6. Send update request
update_url = "http://localhost:3003/api/v1/functions/id/{}/update".format(filter_id)
create_url = "http://localhost:3003/api/v1/functions/create"
update_url = "http://localhost:3000/api/v1/functions/id/{}/update".format(filter_id)
create_url = "http://localhost:3000/api/v1/functions/create"
print(f"📦 Deploying filter '{title}' (version {version})...")
print(f" File: {file_path}")
@@ -257,7 +257,7 @@ def deploy_filter(filter_name: str = DEFAULT_FILTER) -> bool:
except requests.exceptions.ConnectionError:
print(
"❌ Connection error: Could not reach OpenWebUI at localhost:3003"
"❌ Connection error: Could not reach OpenWebUI at localhost:3000"
)
print(" Make sure OpenWebUI is running and accessible.")
return False

View File

@@ -9,7 +9,7 @@ SCRIPT_DIR = Path(__file__).parent
ENV_FILE = SCRIPT_DIR / ".env"
URL = (
"http://localhost:3003/api/v1/functions/id/github_copilot_official_sdk_pipe/update"
"http://localhost:3000/api/v1/functions/id/github_copilot_official_sdk_pipe/update"
)
FILE_PATH = SCRIPT_DIR.parent / "plugins/pipes/github-copilot-sdk/github_copilot_sdk.py"
@@ -103,7 +103,7 @@ def deploy_pipe() -> None:
print(
f"⚠️ Update failed with status {response.status_code}, attempting to create instead..."
)
CREATE_URL = "http://localhost:3003/api/v1/functions/create"
CREATE_URL = "http://localhost:3000/api/v1/functions/create"
res_create = requests.post(
CREATE_URL, headers=headers, data=json.dumps(payload)
)

322
scripts/deploy_tool.py Normal file
View File

@@ -0,0 +1,322 @@
#!/usr/bin/env python3
"""
Deploy Tools plugins to OpenWebUI instance.
This script deploys tool plugins to a running OpenWebUI instance.
It reads the plugin metadata and submits it to the local API.
Usage:
python deploy_tool.py # Deploy OpenWebUI Skills Manager Tool
python deploy_tool.py <tool_name> # Deploy specific tool
python deploy_tool.py --list # List available tools
"""
import requests
import json
import os
import re
import sys
from pathlib import Path
from typing import Optional, Dict, Any
# ─── Configuration ───────────────────────────────────────────────────────────
SCRIPT_DIR = Path(__file__).parent
ENV_FILE = SCRIPT_DIR / ".env"
TOOLS_DIR = SCRIPT_DIR.parent / "plugins/tools"
# Default target tool
DEFAULT_TOOL = "openwebui-skills-manager"
def _load_api_key() -> str:
"""Load API key from .env file in the same directory as this script."""
env_values = {}
if ENV_FILE.exists():
for line in ENV_FILE.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
env_values[key.strip().lower()] = value.strip().strip('"').strip("'")
api_key = (
os.getenv("OPENWEBUI_API_KEY")
or os.getenv("api_key")
or env_values.get("api_key")
or env_values.get("openwebui_api_key")
)
if not api_key:
raise ValueError(
f"Missing api_key. Please create {ENV_FILE} with: "
"api_key=sk-xxxxxxxxxxxx"
)
return api_key
def _get_base_url() -> str:
"""Load base URL from .env file or environment."""
env_values = {}
if ENV_FILE.exists():
for line in ENV_FILE.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
env_values[key.strip().lower()] = value.strip().strip('"').strip("'")
base_url = (
os.getenv("OPENWEBUI_URL")
or os.getenv("OPENWEBUI_BASE_URL")
or os.getenv("url")
or env_values.get("url")
or env_values.get("openwebui_url")
or env_values.get("openwebui_base_url")
)
if not base_url:
raise ValueError(
f"Missing url. Please create {ENV_FILE} with: "
"url=http://localhost:3000"
)
return base_url.rstrip("/")
def _find_tool_file(tool_name: str) -> Optional[Path]:
"""Find the main Python file for a tool.
Args:
tool_name: Directory name of the tool (e.g., 'openwebui-skills-manager')
Returns:
Path to the main Python file, or None if not found.
"""
tool_dir = TOOLS_DIR / tool_name
if not tool_dir.exists():
return None
# Try to find a .py file matching the tool name
py_files = list(tool_dir.glob("*.py"))
# Prefer a file with the tool name (with hyphens converted to underscores)
preferred_name = tool_name.replace("-", "_") + ".py"
for py_file in py_files:
if py_file.name == preferred_name:
return py_file
# Otherwise, return the first .py file (usually the only one)
if py_files:
return py_files[0]
return None
def _extract_metadata(content: str) -> Dict[str, Any]:
"""Extract metadata from the plugin docstring."""
metadata = {}
# Extract docstring
match = re.search(r'"""(.*?)"""', content, re.DOTALL)
if not match:
return metadata
docstring = match.group(1)
# Extract key-value pairs
for line in docstring.split("\n"):
line = line.strip()
if ":" in line and not line.startswith("#") and not line.startswith(""):
parts = line.split(":", 1)
key = parts[0].strip().lower()
value = parts[1].strip()
metadata[key] = value
return metadata
def _build_tool_payload(
tool_name: str, file_path: Path, content: str, metadata: Dict[str, Any]
) -> Dict[str, Any]:
"""Build the payload for the tool update/create API."""
tool_id = metadata.get("id", tool_name).replace("-", "_")
title = metadata.get("title", tool_name)
author = metadata.get("author", "Fu-Jie")
author_url = metadata.get("author_url", "https://github.com/Fu-Jie/openwebui-extensions")
funding_url = metadata.get("funding_url", "https://github.com/open-webui")
description = metadata.get("description", f"Tool plugin: {title}")
version = metadata.get("version", "1.0.0")
openwebui_id = metadata.get("openwebui_id", "")
payload = {
"id": tool_id,
"name": title,
"meta": {
"description": description,
"manifest": {
"title": title,
"author": author,
"author_url": author_url,
"funding_url": funding_url,
"description": description,
"version": version,
"type": "tool",
},
"type": "tool",
},
"content": content,
}
# Add openwebui_id if available
if openwebui_id:
payload["meta"]["manifest"]["openwebui_id"] = openwebui_id
return payload
def deploy_tool(tool_name: str = DEFAULT_TOOL) -> bool:
"""Deploy a tool plugin to OpenWebUI.
Args:
tool_name: Directory name of the tool to deploy
Returns:
True if successful, False otherwise
"""
# 1. Load API key and base URL
try:
api_key = _load_api_key()
base_url = _get_base_url()
except ValueError as e:
print(f"[ERROR] {e}")
return False
# 2. Find tool file
file_path = _find_tool_file(tool_name)
if not file_path:
print(f"[ERROR] Tool '{tool_name}' not found in {TOOLS_DIR}")
print(f"[INFO] Available tools:")
for d in TOOLS_DIR.iterdir():
if d.is_dir() and not d.name.startswith("_"):
print(f" - {d.name}")
return False
# 3. Read local source file
if not file_path.exists():
print(f"[ERROR] Source file not found: {file_path}")
return False
content = file_path.read_text(encoding="utf-8")
metadata = _extract_metadata(content)
if not metadata:
print(f"[ERROR] Could not extract metadata from {file_path}")
return False
version = metadata.get("version", "1.0.0")
title = metadata.get("title", tool_name)
tool_id = metadata.get("id", tool_name).replace("-", "_")
# 4. Build payload
payload = _build_tool_payload(tool_name, file_path, content, metadata)
# 5. Build headers
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
}
# 6. Send update request through the native tool endpoints
update_url = f"{base_url}/api/v1/tools/id/{tool_id}/update"
create_url = f"{base_url}/api/v1/tools/create"
print(f"📦 Deploying tool '{title}' (version {version})...")
print(f" File: {file_path}")
try:
# Try update first
response = requests.post(
update_url,
headers=headers,
data=json.dumps(payload),
timeout=10,
)
if response.status_code == 200:
print(f"✅ Successfully updated '{title}' tool!")
return True
else:
print(
f"⚠️ Update failed with status {response.status_code}, "
"attempting to create instead..."
)
# Try create if update fails
res_create = requests.post(
create_url,
headers=headers,
data=json.dumps(payload),
timeout=10,
)
if res_create.status_code == 200:
print(f"✅ Successfully created '{title}' tool!")
return True
else:
print(f"❌ Failed to update or create. Status: {res_create.status_code}")
try:
error_msg = res_create.json()
print(f" Error: {error_msg}")
except:
print(f" Response: {res_create.text[:500]}")
return False
except requests.exceptions.ConnectionError:
print(
"❌ Connection error: Could not reach OpenWebUI at {base_url}"
)
print(" Make sure OpenWebUI is running and accessible.")
return False
except requests.exceptions.Timeout:
print("❌ Request timeout: OpenWebUI took too long to respond")
return False
except Exception as e:
print(f"❌ Request error: {e}")
return False
def list_tools() -> None:
"""List all available tools."""
print("📋 Available tools:")
tools = [d.name for d in TOOLS_DIR.iterdir() if d.is_dir() and not d.name.startswith("_")]
if not tools:
print(" (No tools found)")
return
for tool_name in sorted(tools):
tool_dir = TOOLS_DIR / tool_name
py_file = _find_tool_file(tool_name)
if py_file:
content = py_file.read_text(encoding="utf-8")
metadata = _extract_metadata(content)
title = metadata.get("title", tool_name)
version = metadata.get("version", "?")
print(f" - {tool_name:<30} {title:<40} v{version}")
else:
print(f" - {tool_name:<30} (no Python file found)")
if __name__ == "__main__":
if len(sys.argv) > 1:
if sys.argv[1] == "--list" or sys.argv[1] == "-l":
list_tools()
else:
tool_name = sys.argv[1]
success = deploy_tool(tool_name)
sys.exit(0 if success else 1)
else:
# Deploy default tool
success = deploy_tool()
sys.exit(0 if success else 1)

View File

@@ -0,0 +1,441 @@
#!/usr/bin/env python3
"""
Bulk install OpenWebUI plugins from this repository.
This script installs plugins from the local repository into a target OpenWebUI
instance. It only installs plugins that:
- live under plugins/actions, plugins/filters, plugins/pipes, or plugins/tools
- contain an `openwebui_id` in the plugin header docstring
- do not use a Chinese filename
- do not use a `_cn.py` localized filename suffix
Supported Plugin Types:
- Action (standard Function class)
- Filter (standard Function class)
- Pipe (standard Function class)
- Tool (native Tools class via /api/v1/tools endpoints)
Configuration:
Create `scripts/.env` with:
api_key=sk-your-api-key
url=http://localhost:3000
Usage:
python scripts/install_all_plugins.py
python scripts/install_all_plugins.py --list
python scripts/install_all_plugins.py --dry-run
python scripts/install_all_plugins.py --types pipe action filter tool
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional, Sequence, Tuple
import requests
SCRIPT_DIR = Path(__file__).resolve().parent
REPO_ROOT = SCRIPT_DIR.parent
ENV_FILE = SCRIPT_DIR / ".env"
DEFAULT_TIMEOUT = 20
DEFAULT_TYPES = ("pipe", "action", "filter", "tool")
SKIP_PREFIXES = ("test_", "verify_")
DOCSTRING_PATTERN = re.compile(r'^\s*"""\n(.*?)\n"""', re.DOTALL)
PLUGIN_TYPE_DIRS = {
"action": REPO_ROOT / "plugins" / "actions",
"filter": REPO_ROOT / "plugins" / "filters",
"pipe": REPO_ROOT / "plugins" / "pipes",
"tool": REPO_ROOT / "plugins" / "tools",
}
@dataclass(frozen=True)
class PluginCandidate:
plugin_type: str
file_path: Path
metadata: Dict[str, str]
content: str
function_id: str
@property
def title(self) -> str:
return self.metadata.get("title", self.file_path.stem)
@property
def version(self) -> str:
return self.metadata.get("version", "unknown")
def _load_env_file(env_path: Path = ENV_FILE) -> Dict[str, str]:
values: Dict[str, str] = {}
if not env_path.exists():
return values
for raw_line in env_path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
key_lower = key.strip().lower()
values[key_lower] = value.strip().strip('"').strip("'")
return values
def load_config(env_path: Path = ENV_FILE) -> Tuple[str, str]:
env_values = _load_env_file(env_path)
api_key = (
os.getenv("OPENWEBUI_API_KEY")
or os.getenv("api_key")
or env_values.get("api_key")
or env_values.get("openwebui_api_key")
)
base_url = (
os.getenv("OPENWEBUI_URL")
or os.getenv("OPENWEBUI_BASE_URL")
or os.getenv("url")
or env_values.get("url")
or env_values.get("openwebui_url")
or env_values.get("openwebui_base_url")
)
missing = []
if not api_key:
missing.append("api_key")
if not base_url:
missing.append("url")
if missing:
joined = ", ".join(missing)
raise ValueError(
f"Missing required config: {joined}. "
f"Please set them in environment variables or {env_path}."
)
return api_key, normalize_base_url(base_url)
def normalize_base_url(url: str) -> str:
normalized = url.strip()
if not normalized:
raise ValueError("URL cannot be empty.")
return normalized.rstrip("/")
def extract_metadata(content: str) -> Dict[str, str]:
match = DOCSTRING_PATTERN.search(content)
if not match:
return {}
metadata: Dict[str, str] = {}
for raw_line in match.group(1).splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or ":" not in line:
continue
key, value = line.split(":", 1)
metadata[key.strip().lower()] = value.strip()
return metadata
def contains_non_ascii_filename(file_path: Path) -> bool:
try:
file_path.stem.encode("ascii")
return False
except UnicodeEncodeError:
return True
def should_skip_plugin_file(file_path: Path) -> Optional[str]:
stem = file_path.stem.lower()
if contains_non_ascii_filename(file_path):
return "non-ascii filename"
if stem.endswith("_cn"):
return "localized _cn file"
if stem.startswith(SKIP_PREFIXES):
return "test or helper script"
return None
def slugify_function_id(value: str) -> str:
slug = re.sub(r"[^a-z0-9]+", "_", value.lower()).strip("_")
slug = re.sub(r"_+", "_", slug)
return slug or "plugin"
def build_function_id(file_path: Path, metadata: Dict[str, str]) -> str:
if metadata.get("id"):
return slugify_function_id(metadata["id"])
if metadata.get("title"):
return slugify_function_id(metadata["title"])
return slugify_function_id(file_path.stem)
def has_tools_class(content: str) -> bool:
"""Check if plugin content defines a Tools class instead of Function class."""
return "\nclass Tools:" in content or "\nclass Tools (" in content
def build_payload(candidate: PluginCandidate) -> Dict[str, object]:
manifest = dict(candidate.metadata)
manifest.setdefault("title", candidate.title)
manifest.setdefault("author", "Fu-Jie")
manifest.setdefault(
"author_url", "https://github.com/Fu-Jie/openwebui-extensions"
)
manifest.setdefault("funding_url", "https://github.com/open-webui")
manifest.setdefault(
"description", f"{candidate.plugin_type.title()} plugin: {candidate.title}"
)
manifest.setdefault("version", "1.0.0")
manifest["type"] = candidate.plugin_type
if candidate.plugin_type == "tool":
return {
"id": candidate.function_id,
"name": manifest["title"],
"meta": {
"description": manifest["description"],
"manifest": {},
},
"content": candidate.content,
"access_grants": [],
}
return {
"id": candidate.function_id,
"name": manifest["title"],
"meta": {
"description": manifest["description"],
"manifest": manifest,
"type": candidate.plugin_type,
},
"content": candidate.content,
}
def build_api_urls(base_url: str, candidate: PluginCandidate) -> Tuple[str, str]:
if candidate.plugin_type == "tool":
return (
f"{base_url}/api/v1/tools/id/{candidate.function_id}/update",
f"{base_url}/api/v1/tools/create",
)
return (
f"{base_url}/api/v1/functions/id/{candidate.function_id}/update",
f"{base_url}/api/v1/functions/create",
)
def discover_plugins(plugin_types: Sequence[str]) -> Tuple[List[PluginCandidate], List[Tuple[Path, str]]]:
candidates: List[PluginCandidate] = []
skipped: List[Tuple[Path, str]] = []
for plugin_type in plugin_types:
plugin_dir = PLUGIN_TYPE_DIRS[plugin_type]
if not plugin_dir.exists():
continue
for file_path in sorted(plugin_dir.rglob("*.py")):
skip_reason = should_skip_plugin_file(file_path)
if skip_reason:
skipped.append((file_path, skip_reason))
continue
content = file_path.read_text(encoding="utf-8")
metadata = extract_metadata(content)
if not metadata:
skipped.append((file_path, "missing plugin header"))
continue
if not metadata.get("openwebui_id"):
skipped.append((file_path, "missing openwebui_id"))
continue
candidates.append(
PluginCandidate(
plugin_type=plugin_type,
file_path=file_path,
metadata=metadata,
content=content,
function_id=build_function_id(file_path, metadata),
)
)
candidates.sort(key=lambda item: (item.plugin_type, item.file_path.as_posix()))
skipped.sort(key=lambda item: item[0].as_posix())
return candidates, skipped
def install_plugin(
candidate: PluginCandidate,
api_key: str,
base_url: str,
timeout: int = DEFAULT_TIMEOUT,
) -> Tuple[bool, str]:
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
}
payload = build_payload(candidate)
update_url, create_url = build_api_urls(base_url, candidate)
try:
update_response = requests.post(
update_url,
headers=headers,
data=json.dumps(payload),
timeout=timeout,
)
if 200 <= update_response.status_code < 300:
return True, "updated"
create_response = requests.post(
create_url,
headers=headers,
data=json.dumps(payload),
timeout=timeout,
)
if 200 <= create_response.status_code < 300:
return True, "created"
message = _response_message(create_response)
return False, f"create failed ({create_response.status_code}): {message}"
except requests.exceptions.Timeout:
return False, "request timed out"
except requests.exceptions.ConnectionError:
return False, f"cannot connect to {base_url}"
except Exception as exc:
return False, str(exc)
def _response_message(response: requests.Response) -> str:
try:
return json.dumps(response.json(), ensure_ascii=False)
except Exception:
return response.text[:500]
def print_candidates(candidates: Sequence[PluginCandidate]) -> None:
if not candidates:
print("No installable plugins found.")
return
print(f"Found {len(candidates)} installable plugins:")
for candidate in candidates:
relative_path = candidate.file_path.relative_to(REPO_ROOT)
print(
f" - [{candidate.plugin_type}] {candidate.title} "
f"v{candidate.version} -> {relative_path}"
)
def print_skipped_summary(skipped: Sequence[Tuple[Path, str]]) -> None:
if not skipped:
return
counts: Dict[str, int] = {}
for _, reason in skipped:
counts[reason] = counts.get(reason, 0) + 1
summary = ", ".join(f"{reason}: {count}" for reason, count in sorted(counts.items()))
print(f"Skipped {len(skipped)} files ({summary}).")
def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Install repository plugins into an OpenWebUI instance."
)
parser.add_argument(
"--types",
nargs="+",
choices=sorted(PLUGIN_TYPE_DIRS.keys()),
default=list(DEFAULT_TYPES),
help="Plugin types to install. Defaults to all supported types.",
)
parser.add_argument(
"--list",
action="store_true",
help="List installable plugins without calling the API.",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be installed without calling the API.",
)
parser.add_argument(
"--timeout",
type=int,
default=DEFAULT_TIMEOUT,
help=f"Request timeout in seconds. Default: {DEFAULT_TIMEOUT}.",
)
return parser.parse_args(argv)
def main(argv: Optional[Sequence[str]] = None) -> int:
args = parse_args(argv)
candidates, skipped = discover_plugins(args.types)
print_candidates(candidates)
print_skipped_summary(skipped)
if args.list or args.dry_run:
return 0
if not candidates:
print("Nothing to install.")
return 1
try:
api_key, base_url = load_config()
except ValueError as exc:
print(f"[ERROR] {exc}")
return 1
print(f"Installing to: {base_url}")
success_count = 0
failed_candidates = []
for candidate in candidates:
relative_path = candidate.file_path.relative_to(REPO_ROOT)
print(
f"\nInstalling [{candidate.plugin_type}] {candidate.title} "
f"v{candidate.version} ({relative_path})"
)
ok, message = install_plugin(
candidate=candidate,
api_key=api_key,
base_url=base_url,
timeout=args.timeout,
)
if ok:
success_count += 1
print(f" [OK] {message}")
else:
failed_candidates.append(candidate)
print(f" [FAILED] {message}")
print(f"\n" + "="*80)
print(
f"Finished: {success_count}/{len(candidates)} plugins installed successfully."
)
if failed_candidates:
print(f"\n{len(failed_candidates)} plugin(s) failed to install:")
for candidate in failed_candidates:
print(f"{candidate.title} ({candidate.plugin_type})")
print(f" → Check the error message above")
print()
print("="*80)
return 0 if success_count == len(candidates) else 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -14,23 +14,23 @@ def main():
base_dir = Path(__file__).parent.parent
print("\n" + "="*80)
print("异步上下文压缩本地部署工具 — 验证状态")
print("Async Context Compression Local Deployment Tools — Verification Status")
print("="*80 + "\n")
files_to_check = {
"🐍 Python 脚本": [
"🐍 Python Scripts": [
"scripts/deploy_async_context_compression.py",
"scripts/deploy_filter.py",
"scripts/deploy_pipe.py",
],
"📖 部署文档": [
"📖 Deployment Documentation": [
"scripts/README.md",
"scripts/QUICK_START.md",
"scripts/DEPLOYMENT_GUIDE.md",
"scripts/DEPLOYMENT_SUMMARY.md",
"plugins/filters/async-context-compression/DEPLOYMENT_REFERENCE.md",
],
"🧪 测试文件": [
"🧪 Test Files": [
"tests/scripts/test_deploy_filter.py",
],
}
@@ -59,24 +59,24 @@ def main():
print("\n" + "="*80)
if all_exist:
print("所有部署工具文件已准备就绪!")
print("All deployment tool files are ready!")
print("="*80 + "\n")
print("🚀 快速开始3 种方式):\n")
print("🚀 Quick Start (3 ways):\n")
print(" 方式 1: 最简单 (推荐)")
print(" Method 1: Easiest (Recommended)")
print(" ─────────────────────────────────────────────────────────")
print(" cd scripts")
print(" python deploy_async_context_compression.py")
print()
print(" 方式 2: 通用工具")
print(" Method 2: Generic Tool")
print(" ─────────────────────────────────────────────────────────")
print(" cd scripts")
print(" python deploy_filter.py")
print()
print(" 方式 3: 部署其他 Filter")
print(" Method 3: Deploy Other Filters")
print(" ─────────────────────────────────────────────────────────")
print(" cd scripts")
print(" python deploy_filter.py --list")
@@ -84,18 +84,18 @@ def main():
print()
print("="*80 + "\n")
print("📚 文档参考:\n")
print("快速开始: scripts/QUICK_START.md")
print("完整指南: scripts/DEPLOYMENT_GUIDE.md")
print("技术总结: scripts/DEPLOYMENT_SUMMARY.md")
print("脚本说明: scripts/README.md")
print("测试覆盖: pytest tests/scripts/test_deploy_filter.py -v")
print("📚 Documentation References:\n")
print("Quick Start: scripts/QUICK_START.md")
print("Complete Guide: scripts/DEPLOYMENT_GUIDE.md")
print("Technical Summary: scripts/DEPLOYMENT_SUMMARY.md")
print("Script Info: scripts/README.md")
print("Test Coverage: pytest tests/scripts/test_deploy_filter.py -v")
print()
print("="*80 + "\n")
return 0
else:
print("某些文件缺失!")
print("Some files are missing!")
print("="*80 + "\n")
return 1

View File

@@ -0,0 +1,173 @@
import importlib.util
import sys
from pathlib import Path
import pytest
MODULE_PATH = Path(__file__).resolve().parents[2] / "scripts" / "install_all_plugins.py"
SPEC = importlib.util.spec_from_file_location("install_all_plugins", MODULE_PATH)
install_all_plugins = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
sys.modules[SPEC.name] = install_all_plugins
SPEC.loader.exec_module(install_all_plugins)
PLUGIN_HEADER = '''"""
title: Example Plugin
version: 1.0.0
openwebui_id: 12345678-1234-1234-1234-123456789abc
description: Example description.
"""
'''
def write_plugin(path: Path, header: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(header + "\nclass Action:\n pass\n", encoding="utf-8")
def test_should_skip_plugin_file_filters_localized_and_helper_names():
assert (
install_all_plugins.should_skip_plugin_file(Path("flash_card_cn.py"))
== "localized _cn file"
)
assert (
install_all_plugins.should_skip_plugin_file(Path("verify_generation.py"))
== "test or helper script"
)
assert (
install_all_plugins.should_skip_plugin_file(Path("测试.py"))
== "non-ascii filename"
)
assert install_all_plugins.should_skip_plugin_file(Path("flash_card.py")) is None
def test_build_function_id_prefers_id_then_title_then_filename():
from_id = install_all_plugins.build_function_id(
Path("dummy.py"), {"id": "Async Context Compression"}
)
from_title = install_all_plugins.build_function_id(
Path("dummy.py"), {"title": "GitHub Copilot Official SDK Pipe"}
)
from_file = install_all_plugins.build_function_id(Path("dummy_plugin.py"), {})
assert from_id == "async_context_compression"
assert from_title == "github_copilot_official_sdk_pipe"
assert from_file == "dummy_plugin"
def test_build_payload_uses_native_tool_shape_for_tools():
candidate = install_all_plugins.PluginCandidate(
plugin_type="tool",
file_path=Path("plugins/tools/demo/demo_tool.py"),
metadata={
"title": "Demo Tool",
"description": "Demo tool description",
"openwebui_id": "12345678-1234-1234-1234-123456789abc",
},
content='class Tools:\n pass\n',
function_id="demo_tool",
)
payload = install_all_plugins.build_payload(candidate)
assert payload == {
"id": "demo_tool",
"name": "Demo Tool",
"meta": {
"description": "Demo tool description",
"manifest": {},
},
"content": 'class Tools:\n pass\n',
"access_grants": [],
}
def test_build_api_urls_uses_tool_endpoints_for_tools():
candidate = install_all_plugins.PluginCandidate(
plugin_type="tool",
file_path=Path("plugins/tools/demo/demo_tool.py"),
metadata={"title": "Demo Tool"},
content='class Tools:\n pass\n',
function_id="demo_tool",
)
update_url, create_url = install_all_plugins.build_api_urls(
"http://localhost:3000", candidate
)
assert update_url == "http://localhost:3000/api/v1/tools/id/demo_tool/update"
assert create_url == "http://localhost:3000/api/v1/tools/create"
def test_discover_plugins_only_returns_supported_openwebui_plugins(tmp_path, monkeypatch):
actions_dir = tmp_path / "plugins" / "actions"
filters_dir = tmp_path / "plugins" / "filters"
pipes_dir = tmp_path / "plugins" / "pipes"
tools_dir = tmp_path / "plugins" / "tools"
write_plugin(actions_dir / "flash-card" / "flash_card.py", PLUGIN_HEADER)
write_plugin(actions_dir / "flash-card" / "flash_card_cn.py", PLUGIN_HEADER)
write_plugin(actions_dir / "infographic" / "verify_generation.py", PLUGIN_HEADER)
write_plugin(filters_dir / "missing-id" / "missing_id.py", '"""\ntitle: Missing ID\n"""\n')
write_plugin(pipes_dir / "sdk" / "github_copilot_sdk.py", PLUGIN_HEADER)
write_plugin(tools_dir / "skills" / "openwebui_skills_manager.py", PLUGIN_HEADER)
monkeypatch.setattr(
install_all_plugins,
"PLUGIN_TYPE_DIRS",
{
"action": actions_dir,
"filter": filters_dir,
"pipe": pipes_dir,
"tool": tools_dir,
},
)
monkeypatch.setattr(install_all_plugins, "REPO_ROOT", tmp_path)
candidates, skipped = install_all_plugins.discover_plugins(
["action", "filter", "pipe", "tool"]
)
candidate_names = [candidate.file_path.name for candidate in candidates]
skipped_reasons = {path.name: reason for path, reason in skipped}
assert candidate_names == [
"flash_card.py",
"github_copilot_sdk.py",
"openwebui_skills_manager.py",
]
assert skipped_reasons["missing_id.py"] == "missing openwebui_id"
assert skipped_reasons["flash_card_cn.py"] == "localized _cn file"
assert skipped_reasons["verify_generation.py"] == "test or helper script"
@pytest.mark.parametrize(
("header", "expected_reason"),
[
('"""\ntitle: Missing ID\n"""\n', "missing openwebui_id"),
("class Action:\n pass\n", "missing plugin header"),
],
)
def test_discover_plugins_reports_missing_metadata(tmp_path, monkeypatch, header, expected_reason):
action_dir = tmp_path / "plugins" / "actions"
plugin_file = action_dir / "demo" / "demo.py"
write_plugin(plugin_file, header)
monkeypatch.setattr(
install_all_plugins,
"PLUGIN_TYPE_DIRS",
{
"action": action_dir,
"filter": tmp_path / "plugins" / "filters",
"pipe": tmp_path / "plugins" / "pipes",
"tool": tmp_path / "plugins" / "tools",
},
)
monkeypatch.setattr(install_all_plugins, "REPO_ROOT", tmp_path)
candidates, skipped = install_all_plugins.discover_plugins(["action"])
assert candidates == []
assert skipped == [(plugin_file, expected_reason)]