diff --git a/README.md b/README.md index 978850f..27dd4ed 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/README_CN.md b/README_CN.md index f613b1d..6bcd355 100644 --- a/README_CN.md +++ b/README_CN.md @@ -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) diff --git a/scripts/.env.example b/scripts/.env.example new file mode 100644 index 0000000..5087534 --- /dev/null +++ b/scripts/.env.example @@ -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 diff --git a/scripts/DEPLOYMENT_GUIDE.md b/scripts/DEPLOYMENT_GUIDE.md index 2b6c7bf..e23cc55 100644 --- a/scripts/DEPLOYMENT_GUIDE.md +++ b/scripts/DEPLOYMENT_GUIDE.md @@ -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 diff --git a/scripts/DEPLOYMENT_SUMMARY.md b/scripts/DEPLOYMENT_SUMMARY.md index 55a33d5..b4b87d8 100644 --- a/scripts/DEPLOYMENT_SUMMARY.md +++ b/scripts/DEPLOYMENT_SUMMARY.md @@ -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) diff --git a/scripts/QUICK_START.md b/scripts/QUICK_START.md index bbf019a..f12237e 100644 --- a/scripts/QUICK_START.md +++ b/scripts/QUICK_START.md @@ -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` diff --git a/scripts/README.md b/scripts/README.md index 4ef855d..5d2cf5f 100644 --- a/scripts/README.md +++ b/scripts/README.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 ✅ + diff --git a/scripts/UPDATE_MECHANISM.md b/scripts/UPDATE_MECHANISM.md index ccfab37..fdcf98e 100644 --- a/scripts/UPDATE_MECHANISM.md +++ b/scripts/UPDATE_MECHANISM.md @@ -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`) diff --git a/scripts/UPDATE_QUICK_REF.md b/scripts/UPDATE_QUICK_REF.md index 46333ae..ec6dd4f 100644 --- a/scripts/UPDATE_QUICK_REF.md +++ b/scripts/UPDATE_QUICK_REF.md @@ -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` diff --git a/scripts/deploy_async_context_compression.py b/scripts/deploy_async_context_compression.py index 92eccc5..cd0d094 100644 --- a/scripts/deploy_async_context_compression.py +++ b/scripts/deploy_async_context_compression.py @@ -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() diff --git a/scripts/deploy_filter.py b/scripts/deploy_filter.py index b4db9d8..8202cd7 100644 --- a/scripts/deploy_filter.py +++ b/scripts/deploy_filter.py @@ -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 diff --git a/scripts/deploy_pipe.py b/scripts/deploy_pipe.py index 1dc9a93..056df07 100644 --- a/scripts/deploy_pipe.py +++ b/scripts/deploy_pipe.py @@ -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) ) diff --git a/scripts/deploy_tool.py b/scripts/deploy_tool.py new file mode 100644 index 0000000..ddbf7c0 --- /dev/null +++ b/scripts/deploy_tool.py @@ -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 # 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) diff --git a/scripts/install_all_plugins.py b/scripts/install_all_plugins.py new file mode 100644 index 0000000..16112d7 --- /dev/null +++ b/scripts/install_all_plugins.py @@ -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()) diff --git a/scripts/verify_deployment_tools.py b/scripts/verify_deployment_tools.py index 83ac61c..877f5d0 100644 --- a/scripts/verify_deployment_tools.py +++ b/scripts/verify_deployment_tools.py @@ -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 diff --git a/tests/scripts/test_install_all_plugins.py b/tests/scripts/test_install_all_plugins.py new file mode 100644 index 0000000..6d4978e --- /dev/null +++ b/tests/scripts/test_install_all_plugins.py @@ -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)]