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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
20
scripts/.env.example
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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 ✅
|
||||
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
322
scripts/deploy_tool.py
Normal 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)
|
||||
441
scripts/install_all_plugins.py
Normal file
441
scripts/install_all_plugins.py
Normal 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())
|
||||
@@ -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
|
||||
|
||||
|
||||
173
tests/scripts/test_install_all_plugins.py
Normal file
173
tests/scripts/test_install_all_plugins.py
Normal 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)]
|
||||
Reference in New Issue
Block a user