Compare commits

..

1 Commits

Author SHA1 Message Date
fujie
f061d82409 feat(async-context-compression): release v1.4.0 with structure-aware grouping and session locking
- Introduced Atomic Message Grouping to prevent tool-calling corruption (Issue #56)
- Implemented Tail Boundary Alignment for deterministic context truncation
- Added per-chat asynchronous session locking to prevent duplicate background tasks
- Enhanced summarization traceability with message IDs and names
- Synchronized version and changelog across all documentation files
- Optimized release-prep skill to remove redundant H1 titles

Closes #56
2026-03-09 20:34:34 +08:00
47 changed files with 1269 additions and 3387 deletions

View File

@@ -1,27 +0,0 @@
# Async Context Compression Progress Mapping
> Discovered: 2026-03-10
## Context
Applies to `plugins/filters/async-context-compression/async_context_compression.py` once the inlet has already replaced early history with a synthetic summary message.
## Finding
`compressed_message_count` cannot be recalculated from the visible message list length after compression. Once a summary marker is present, the visible list mixes:
- preserved head messages that are still before the saved boundary
- one synthetic summary message
- tail messages that map to original history starting at the saved boundary
## Solution / Pattern
Store the original-history boundary on the injected summary message metadata, then recover future progress using:
- `original_count = covered_until + len(messages_after_summary_marker)`
- `target_progress = max(covered_until, original_count - keep_last)`
When the summary-model window is too small, trim newest atomic groups from the summary input so the saved boundary still matches what the summary actually covers.
## Gotchas
- If you trim from the head of the summary input, the saved progress can overstate coverage and hide messages that were never summarized.
- Status previews for the next context must convert the saved original-history boundary back into the current visible view before rebuilding head/summary/tail.
- `inlet(body["messages"])` and `outlet(body["messages"])` can both represent the full conversation while using different serializations:
- inlet may receive expanded native tool-call chains (`assistant(tool_calls) -> tool -> assistant`)
- outlet may receive a compact top-level transcript where tool calls are folded into assistant `<details type="tool_calls">` blocks
- These two views do not share a safe `compressed_message_count` coordinate system. If outlet is in the compact assistant/details view, do not persist summary progress derived from its top-level message count.

View File

@@ -1,26 +0,0 @@
# OpenWebUI Tool Call Context Inflation
> Discovered: 2026-03-11
## Context
When analyzing why the `async_context_compression` plugin sees different array lengths of `messages` between the `inlet` (e.g. 27 items) and `outlet` (e.g. 8 items) phases, especially when native tool calling (Function Calling) is involved in OpenWebUI.
## Finding
There is a fundamental disparity in how OpenWebUI serializes conversational history at different stages of the request lifecycle:
1. **Outlet (UI Rendering View)**:
After the LLM completes generation and tools have been executed, OpenWebUI's `middleware.py` (and streaming builders) bundles intermediate tool calls and their raw results. It hides them inside an HTML `<details type="tool_calls">...</details>` block within a single `role: assistant` message's `content`.
Concurrently, the actual native API tool-calling data is saved in a hidden `output` dict field attached to that message. At this stage, the `messages` array looks short (e.g., 8 items) because tool interactions are visually folded.
2. **Inlet (LLM Native View)**:
When the user sends the *next* message, the request enters `main.py` -> `process_chat_payload` -> `middleware.py:process_messages_with_output()`.
Here, OpenWebUI scans historical `assistant` messages for that hidden `output` field. If found, it completely **inflates (unfolds)** the raw data back into an exact sequence of OpenAI-compliant `tool_call` and `tool_result` messages (using `utils/misc.py:convert_output_to_messages`).
The HTML `<details>` string is entirely discarded before being sent to the LLM.
**Conclusion on Token Consumption**:
In the next turn, tool context is **NOT** compressed at all. It is fully re-expanded to its original verbose state (e.g., back to 27 items) and consumes the maximum amount of tokens required by the raw JSON arguments and results.
## Gotchas
- Any logic operating in the `outlet` phase (like background tasks) that relies on the `messages` array index will be completely misaligned with the array seen in the `inlet` phase.
- Attempting to slice or trim history based on `outlet` array lengths will cause index out-of-bounds errors or destructive cropping of recent messages.
- The only safe way to bridge these two views is either to translate the folded view back into the expanded view using `convert_output_to_messages`, or to rely on unique `id` fields (if available) rather than array indices.

View File

@@ -73,21 +73,13 @@ Create two versioned release notes files:
#### Required Sections #### Required Sections
Each file must include: Each file must include:
0. **Marketplace Badge**: A prominent button linking to the plugin on openwebui.com using shields.io (e.g., `[![](https://img.shields.io/badge/OpenWebUI%20Community-Get%20Plugin-blue?style=for-the-badge)](URL)`). 0. **Marketplace Link**: Direct link to the plugin on openwebui.com (e.g., `**[🚀 Get/Update on OpenWebUI Community](URL)**`)
1. **Overview Header**: Use `## Overview` as the first header. 1. **Overview**: One paragraph summarizing this release
2. **Summary Paragraph**: A paragraph summarizing the release. **NEVER** include the version number as a title. 2. **New Features** / **新功能**: Bulleted list of features
3. **README Link**: Direct link to the plugin's README file on GitHub. 3. **Bug Fixes** / **问题修复**: Bulleted list of fixes
4. **New Features** / **新功能**: Bulleted list of features 4. **Related Issues** / **相关 Issue**: Link to the GitHub Issue(s) resolved in this release (e.g., `**[#56](URL)**`). MANDATORY if the release resolves a reported issue.
5. **Bug Fixes** / **问题修复**: Bulleted list of fixes 5. **Related PRs** / **相关 PR**: Link to the Pull Request(s) associated with this release. (e.g., `**[#123](URL)**`). MANDATORY if the release is being prepared within an existing PR.
6. **Related Issues** / **相关 Issue**: Link to GitHub Issues. **ONLY** include if a specific issue is resolved. **NEVER use placeholders.** 6. **Migration Notes** / **迁移说明**: Breaking changes or Valve key renames (omit section if none)
7. **Related PRs** / **相关 PR**: Link to the Pull Request. **ONLY** include if the PR is already created and the ID is known. **NEVER use placeholders.**
8. **Migration Notes**: Breaking changes or Valve key renames (omit section if none)
---
## Language Standard
- **Release Notes Files**: Use **English ONLY** for the final `.md` files to maintain professional consistency on GitHub. Avoid bilingual content in the release description.
6. **Companion Plugins** / **配套插件** (optional): If a companion plugin was updated 6. **Companion Plugins** / **配套插件** (optional): If a companion plugin was updated
If a release notes file already exists for this version, update it rather than creating a new one. If a release notes file already exists for this version, update it rather than creating a new one.

View File

@@ -78,28 +78,5 @@ Plugin: {type}/{name} → v{new_version}
### Verification Status ### Verification Status
{filled-in 9-file checklist for each changed plugin} {filled-in 9-file checklist for each changed plugin}
## Post-Release: Batch Plugin Installation
After release is published, users can quickly install all plugins:
```bash
# Clone the repository
git clone https://github.com/Fu-Jie/openwebui-extensions.git
cd openwebui-extensions
# Setup API key and instance URL
echo "api_key=sk-your-api-key-here" > scripts/.env
echo "url=http://localhost:3000" >> scripts/.env
# If using remote instance, configure the baseURL:
# echo "url=http://192.168.1.10:3000" >> scripts/.env
# echo "url=https://openwebui.example.com" >> scripts/.env
# Install all plugins at once
python scripts/install_all_plugins.py
```
See: [Deployment Guide](./scripts/DEPLOYMENT_GUIDE.md)
--- ---
⚠️ **Waiting for user confirmation — no git operations will run until explicitly approved.** ⚠️ **Waiting for user confirmation — no git operations will run until explicitly approved.**

View File

@@ -436,34 +436,54 @@ jobs:
CHANGED_PLUGIN_TITLE: ${{ needs.check-changes.outputs.changed_plugin_title }} CHANGED_PLUGIN_TITLE: ${{ needs.check-changes.outputs.changed_plugin_title }}
CHANGED_PLUGIN_VERSION: ${{ needs.check-changes.outputs.changed_plugin_version }} CHANGED_PLUGIN_VERSION: ${{ needs.check-changes.outputs.changed_plugin_version }}
DETECTED_CHANGES: ${{ needs.check-changes.outputs.release_notes }} DETECTED_CHANGES: ${{ needs.check-changes.outputs.release_notes }}
COMMITS: ${{ steps.commits.outputs.commits }}
DOC_FILES: ${{ needs.check-changes.outputs.changed_doc_files }} DOC_FILES: ${{ needs.check-changes.outputs.changed_doc_files }}
run: | run: |
> release_notes.md > release_notes.md
# 1. Primary content from v*.md files (highest priority) if [ -n "$CHANGED_PLUGIN_TITLE" ] && [ -n "$CHANGED_PLUGIN_VERSION" ]; then
echo "# $CHANGED_PLUGIN_TITLE v$CHANGED_PLUGIN_VERSION" >> release_notes.md
echo "" >> release_notes.md
elif [ -n "$TITLE" ]; then
echo "# $TITLE" >> release_notes.md
echo "" >> release_notes.md
fi
# 1. Release notes from v*.md files (highest priority, shown first)
if [ -n "$DOC_FILES" ]; then if [ -n "$DOC_FILES" ]; then
RELEASE_NOTE_FILES=$(echo "$DOC_FILES" | grep -E '^plugins/.*/v[^/]*\.md$' | grep -v '_CN\.md$' || true) RELEASE_NOTE_FILES=$(echo "$DOC_FILES" | grep -E '^plugins/.*/v[^/]*\.md$' | grep -v '_CN\.md$' || true)
if [ -n "$RELEASE_NOTE_FILES" ]; then if [ -n "$RELEASE_NOTE_FILES" ]; then
while IFS= read -r file; do while IFS= read -r file; do
[ -z "$file" ] && continue [ -z "$file" ] && continue
if [ -f "$file" ]; then if [ -f "$file" ]; then
# Extract content, removing any H1 title from the file to avoid duplication python3 -c "import pathlib, re; file_path = pathlib.Path(r'''$file'''); text = file_path.read_text(encoding='utf-8'); text = re.sub(r'^#\\s+.+?(?:\\r?\\n)+', '', text, count=1, flags=re.MULTILINE); print(text.lstrip().rstrip())" >> release_notes.md
python3 -c "import pathlib, re; file_path = pathlib.Path(r'''$file'''); text = file_path.read_text(encoding='utf-8'); text = re.sub(r'^#\s+.+?(?:\r?\n)+', '', text, count=1, flags=re.MULTILINE); print(text.lstrip().rstrip())" >> release_notes.md
echo "" >> release_notes.md echo "" >> release_notes.md
fi fi
done <<< "$RELEASE_NOTE_FILES" done <<< "$RELEASE_NOTE_FILES"
fi fi
fi fi
# 2. Automated plugin version change summary # 2. Plugin version changes detected by script
if [ -z "$CHANGED_PLUGIN_TITLE" ] && [ -z "$CHANGED_PLUGIN_VERSION" ] && [ -n "$TITLE" ]; then
echo "## $TITLE" >> release_notes.md
echo "" >> release_notes.md
fi
if [ -n "$DETECTED_CHANGES" ] && ! echo "$DETECTED_CHANGES" | grep -q "No changes detected"; then if [ -n "$DETECTED_CHANGES" ] && ! echo "$DETECTED_CHANGES" | grep -q "No changes detected"; then
echo "## Version Changes" >> release_notes.md echo "## What's Changed" >> release_notes.md
echo "" >> release_notes.md echo "" >> release_notes.md
echo "$DETECTED_CHANGES" >> release_notes.md echo "$DETECTED_CHANGES" >> release_notes.md
echo "" >> release_notes.md echo "" >> release_notes.md
fi fi
# 3. Manual additional notes from workflow dispatch # 3. Commits (Conventional Commits format with body)
if [ -n "$COMMITS" ]; then
echo "## Commits" >> release_notes.md
echo "" >> release_notes.md
echo "$COMMITS" >> release_notes.md
echo "" >> release_notes.md
fi
if [ -n "$NOTES" ]; then if [ -n "$NOTES" ]; then
echo "## Additional Notes" >> release_notes.md echo "## Additional Notes" >> release_notes.md
echo "" >> release_notes.md echo "" >> release_notes.md
@@ -473,15 +493,30 @@ jobs:
cat >> release_notes.md << 'EOF' cat >> release_notes.md << 'EOF'
## Download
📦 **Download the updated plugin files below**
### Installation
#### From OpenWebUI Community
1. Open OpenWebUI Admin Panel
2. Navigate to Functions/Tools
3. Search for the plugin name
4. Click Install
#### Manual Installation
1. Download the plugin file (`.py`) from the assets below
2. Open OpenWebUI Admin Panel → Functions
3. Click "Create Function" → Import
4. Paste the plugin code
--- ---
📚 [Documentation Portal](https://fu-jie.github.io/openwebui-extensions/) 📚 [Documentation](https://fu-jie.github.io/openwebui-extensions/)
🐛 [Report Issues](https://github.com/Fu-Jie/openwebui-extensions/issues) 🐛 [Report Issues](https://github.com/Fu-Jie/openwebui-extensions/issues)
EOF EOF
echo "=== Final Release Notes ==="
cat release_notes.md
echo "=== Release Notes ===" echo "=== Release Notes ==="
cat release_notes.md cat release_notes.md

75
CHANGELOG.md Normal file
View File

@@ -0,0 +1,75 @@
# Changelog / 更新日志
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
本项目的所有重要更改都将记录在此文件中。
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
---
## [Unreleased] / 未发布
### Added / 新增
- 插件发布工作流 (Plugin release workflow)
### Changed / 变更
### Fixed / 修复
### Removed / 移除
---
## Plugin Versions / 插件版本
### Actions
| Plugin / 插件 | Version / 版本 |
|---------------|----------------|
| Smart Mind Map / 思维导图 | 0.8.0 |
| Flash Card / 闪记卡 | 0.2.1 |
| Export to Word / 导出为 Word | 0.1.0 |
| Export to Excel / 导出为 Excel | 0.3.3 |
| Deep Reading & Summary / 精读 | 0.1.0 / 2.0.0 |
| Smart Infographic / 智能信息图 | 1.3.0 |
### Filters
| Plugin / 插件 | Version / 版本 |
|---------------|----------------|
| Async Context Compression / 异步上下文压缩 | 1.1.0 |
| Context & Model Enhancement Filter | 0.2 |
| Gemini Manifold Companion | 1.7.0 |
| Gemini 多模态过滤器 | 0.3.2 |
### Pipes
| Plugin / 插件 | Version / 版本 |
|---------------|----------------|
| Gemini Manifold google_genai | 1.26.0 |
---
<!--
Release Template / 发布模板:
## [x.x.x] - YYYY-MM-DD
### Added / 新增
- New feature description
### Changed / 变更
- Change description
### Fixed / 修复
- Bug fix description
### Plugin Updates / 插件更新
- `plugin_name`: v0.x.0 -> v0.y.0
- Feature 1
- Feature 2
-->

104
ISSUE_57_ANALYSIS_REPORT.md Normal file
View File

@@ -0,0 +1,104 @@
# Markdown Normalizer 插件可靠性修复分析报告 (Issue #57)
## 1. 问题背景
根据 Issue #57 报告,`Markdown Normalizer` 在 v1.2.7 版本中存在数项严重影响可靠性的 Bug包括错误回滚失效、对内联技术内容的过度转义、配置项不生效以及调试日志潜在的隐私风险。
## 2. 核心处理流程图 (v1.2.8)
以下流程展示了插件如何在确保“不损坏原始内容”的前提下进行智能修复:
```mermaid
graph TD
Start([开始处理内容]) --> Cache[1. 内存中存入原始快照 Snapshot]
Cache --> Logic{进入修复流程}
subgraph "分层保护逻辑 (Context-Aware)"
Logic --> Block[识别并锁定 ``` 代码块]
Block --> Inline[识别并锁定 ` 行内代码]
Inline --> Math[识别并锁定 $ LaTeX 公式]
Math --> Clean[仅对非锁定区域执行转义清理]
end
Clean --> Others[执行其他规则: Thought/Details/Table等]
Others --> Check{运行是否报错?}
Check -- 否 (成功) --> Success[返回修复后的内容]
Check -- 是 (失败) --> Rollback[触发回滚: 丢弃所有修改]
Rollback --> Original[返回步骤1存储的原始快照]
Success --> End([输出结果])
Original --> End
```
## 3. 修复项详细说明
### 2.1 错误回滚机制修复 (Reliability: Error Fallback)
- **问题**:在 `normalize` 流程中,如果某个清理器抛出异常,返回的是已被部分修改的 `content`,导致输出内容损坏。
- **技术实现**
```python
def normalize(self, content: str) -> str:
original_content = content # 1. 流程开始前缓存原始快照
try:
# ... 执行一系列清理步骤 ...
return content
except Exception as e:
# 2. 任何步骤失败,立即记录日志并回滚
logger.error(f"Content normalization failed: {e}", exc_info=True)
return original_content # 确保返回的是原始快照
```
- **验证结果**:通过模拟 `RuntimeError` 验证,插件现在能 100% 回滚至原始状态。
### 2.2 上下文感知的转义保护 (Context-Aware Escaping)
- **问题**:全局替换导致正文中包含在 `` ` `` 内的代码片段如正则、Windows 路径)被破坏。
- **技术实现**
重构后的 `_fix_escape_characters` 采用了 **“分词保护策略”**,通过多层嵌套分割来确保仅在非代码上下文中进行清理:
```python
def _fix_escape_characters(self, content: str) -> str:
# 层级 1: 以 ``` 分隔代码块
parts = content.split("```")
for i in range(len(parts)):
is_code_block = (i % 2 != 0)
if is_code_block and not self.config.enable_escape_fix_in_code_blocks:
continue # 默认跳过代码块
if not is_code_block:
# 层级 2: 在非代码块正文中,以 ` 分隔内联代码
inline_parts = parts[i].split("`")
for k in range(0, len(inline_parts), 2): # 仅处理非内联代码部分
# 层级 3: 在非内联代码中,以 $ 分隔 LaTeX 公式
sub_parts = inline_parts[k].split("$")
for j in range(0, len(sub_parts), 2):
# 最终:仅在确认为“纯文本”的部分执行 clean_text
sub_parts[j] = clean_text(sub_parts[j])
inline_parts[k] = "$".join(sub_parts)
parts[i] = "`".join(inline_parts)
else:
parts[i] = clean_text(parts[i])
return "```".join(parts)
```
- **验证结果**:测试用例 `Regex: [\n\r]` 和 `C:\Windows` 在正文中保持原样,而普通文本中的 `\\n` 被正确转换。
### 2.3 配置项激活 (Configuration Enforcement)
- **问题**`enable_escape_fix_in_code_blocks` 开关在代码中被定义但未被逻辑引用。
- **修复方案**:在 `_fix_escape_characters` 处理流程中加入对该开关的判断。
- **验证结果**:当开关关闭(默认)时,代码块内容保持不变;开启时,代码块内执行转义修复。
### 2.4 默认日志策略调整 (Privacy & Performance)
- **问题**`show_debug_log` 默认为 `True`,且会将原始内容打印到浏览器控制台。
- **修复方案**:将默认值改为 `False`。
- **验证结果**:新安装或默认配置下不再主动输出全量日志,仅在用户显式开启时用于调试。
## 3. 综合测试覆盖
已建立 `comprehensive_test_markdown_normalizer.py` 测试脚本,覆盖以下场景:
1. **异常抛出回滚**:确保插件“不破坏”原始内容。
2. **内联代码保护**:验证正则和路径字符串的完整性。
3. **代码块开关控制**:验证配置项的有效性。
4. **LaTeX 命令回归测试**:确保 `\times`, `\theta` 等命令不被误触。
5. **复杂嵌套结构**:验证包含 Thought 标签、列表、内联代码及代码块的混合文本处理。
## 4. 结论
`Markdown Normalizer v1.2.8` 已解决 Issue #57 提出的所有核心可靠性问题。插件现在具备“不损坏内容”的防御性编程能力,并能更智能地感知 Markdown 上下文。
---
**报告日期**2026-03-08
**修复版本**v1.2.8

12
ISSUE_57_REPLY.md Normal file
View File

@@ -0,0 +1,12 @@
# Reply to Issue #57
I have addressed these issues in **v1.2.8** with a focus on reliability and a "Safe-by-Default" approach:
1. **Robust Error Rollback (Items 1, 4, 5)**: I implemented a full `try...except` wrapper. If any error occurs during normalization, the plugin now returns the **100% original text**. This ensures that the output is never partially modified or corrupted.
2. **Conservative Escaping (Item 2)**: To avoid breaking technical content like regex or paths, the escape fixer now strictly skips all code blocks, inline code, and LaTeX formulas by default. I have shifted toward an "opt-in" model where aggressive cleaning is disabled unless specifically requested.
3. **Fixed Configuration (Item 3)**: The `enable_escape_fix_in_code_blocks` Valve was intended to handle escaping within code blocks (e.g., for fixing flat SQL output), but there was a bug preventing it from being applied. I have fixed this, and the setting is now fully functional.
4. **Privacy & Reliability**: I have changed the default for `show_debug_log` to `False`. While it was previously enabled by default to help gather feedback and squash bugs during the initial development phase, the plugin has now undergone multiple iterations and reliability enhancements (including the new tiered protection and rollback mechanisms), making it stable enough for a "silent" and private default operation.
**Recommendation**: If you encounter SQL or data blocks that appear on a single line, you can now manually enable `enable_escape_fix_in_code_blocks` in the Valves to fix them safely.
Please update to the latest version via [OpenWebUI Community](https://openwebui.com/functions/baaa8732-9348-40b7-8359-7e009660e23c). Thank you for your valuable feedback!

View File

@@ -9,7 +9,6 @@ A collection of enhancements, plugins, and prompts for [open-webui](https://gith
<!-- STATS_START --> <!-- STATS_START -->
## 📊 Community Stats ## 📊 Community Stats
>
> ![updated](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_updated.json&style=flat) > ![updated](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_updated.json&style=flat)
| 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions | | 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions |
@@ -20,19 +19,18 @@ A collection of enhancements, plugins, and prompts for [open-webui](https://gith
| :---: | :---: | :---: | :---: | :---: | | :---: | :---: | :---: | :---: | :---: |
| ![posts](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_posts.json&style=flat) | ![downloads](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_downloads.json&style=flat) | ![views](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_views.json&style=flat) | ![upvotes](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_upvotes.json&style=flat) | ![saves](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_saves.json&style=flat) | | ![posts](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_posts.json&style=flat) | ![downloads](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_downloads.json&style=flat) | ![views](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_views.json&style=flat) | ![upvotes](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_upvotes.json&style=flat) | ![saves](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_saves.json&style=flat) |
### 🔥 Top 6 Popular Plugins
### 🔥 Top 6 Popular Plugins
| Rank | Plugin | Version | Downloads | Views | 📅 Updated | | Rank | Plugin | Version | Downloads | Views | 📅 Updated |
| :---: | :--- | :---: | :---: | :---: | :---: | | :---: | :--- | :---: | :---: | :---: | :---: |
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | ![v](https://img.shields.io/badge/v-1.0.0-blue?style=flat) | ![p1_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_dl.json&style=flat) | ![p1_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--08-gray?style=flat) | | 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | ![v](https://img.shields.io/badge/v-1.0.0-blue?style=flat) | ![p1_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_dl.json&style=flat) | ![p1_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--08-gray?style=flat) |
| 🥈 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | ![v](https://img.shields.io/badge/v-1.5.0-blue?style=flat) | ![p2_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_dl.json&style=flat) | ![p2_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--08-gray?style=flat) | | 🥈 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | ![v](https://img.shields.io/badge/v-1.5.0-blue?style=flat) | ![p2_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_dl.json&style=flat) | ![p2_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--08-gray?style=flat) |
| 🥉 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | ![v](https://img.shields.io/badge/v-1.2.7-blue?style=flat) | ![p3_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_dl.json&style=flat) | ![p3_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--08-gray?style=flat) | | 🥉 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | ![v](https://img.shields.io/badge/v-1.2.7-blue?style=flat) | ![p3_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_dl.json&style=flat) | ![p3_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--08-gray?style=flat) |
| 4⃣ | [Export to Word Enhanced](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | ![v](https://img.shields.io/badge/v-0.4.4-blue?style=flat) | ![p4_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p4_dl.json&style=flat) | ![p4_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p4_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--08-gray?style=flat) | | 4⃣ | [Export to Word Enhanced](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | ![v](https://img.shields.io/badge/v-0.4.4-blue?style=flat) | ![p4_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p4_dl.json&style=flat) | ![p4_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p4_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--08-gray?style=flat) |
| 5⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | ![v](https://img.shields.io/badge/v-1.4.1-blue?style=flat) | ![p5_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_dl.json&style=flat) | ![p5_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--11-gray?style=flat) | | 5⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | ![v](https://img.shields.io/badge/v-1.4.0-blue?style=flat) | ![p5_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_dl.json&style=flat) | ![p5_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--09-gray?style=flat) |
| 6⃣ | [AI Task Instruction Generator](https://openwebui.com/posts/ai_task_instruction_generator_9bab8b37) | ![v](https://img.shields.io/badge/v-N/A-gray?style=flat) | ![p6_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p6_dl.json&style=flat) | ![p6_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p6_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--08-gray?style=flat) | | 6⃣ | [AI Task Instruction Generator](https://openwebui.com/posts/ai_task_instruction_generator_9bab8b37) | ![v](https://img.shields.io/badge/v-N/A-gray?style=flat) | ![p6_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p6_dl.json&style=flat) | ![p6_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p6_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--08-gray?style=flat) |
### 📈 Total Downloads Trend ### 📈 Total Downloads Trend
![Activity](https://gist.githubusercontent.com/Fu-Jie/db3d95687075a880af6f1fba76d679c6/raw/chart.svg) ![Activity](https://gist.githubusercontent.com/Fu-Jie/db3d95687075a880af6f1fba76d679c6/raw/chart.svg)
*See full stats and charts in [Community Stats Report](./docs/community-stats.md)* *See full stats and charts in [Community Stats Report](./docs/community-stats.md)*
@@ -165,6 +163,13 @@ For code examples, please check the `docs/examples/` directory.
This project is a collection of resources and does not require a Python environment. Simply download the files you need and import them into your OpenWebUI instance. This project is a collection of resources and does not require a Python environment. Simply download the files you need and import them into your OpenWebUI instance.
### Using Prompts
1. Browse the `/prompts` directory and select a prompt file (`.md`).
2. Copy the file content.
3. In the OpenWebUI chat interface, click the "Prompt" button above the input box.
4. Paste the content and save.
### Using Plugins ### Using Plugins
1. **Install from OpenWebUI Community (Recommended)**: 1. **Install from OpenWebUI Community (Recommended)**:
@@ -172,14 +177,11 @@ This project is a collection of resources and does not require a Python environm
- Browse the plugins and select the one you like. - Browse the plugins and select the one you like.
- Click "Get" to import it directly into your OpenWebUI instance. - Click "Get" to import it directly into your OpenWebUI instance.
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. 2. **Manual Installation**:
- Browse the `/plugins` directory and download the plugin file (`.py`) you need.
### Using Prompts - Go to OpenWebUI **Admin Panel** -> **Settings** -> **Plugins**.
- Click the upload button and select the `.py` file you just downloaded.
1. Browse the `/prompts` directory and select a prompt file (`.md`). - Once uploaded, refresh the page to enable the plugin in your chat settings or toolbar.
2. Copy the file content.
3. In the OpenWebUI chat interface, click the "Prompt" button above the input box.
4. Paste the content and save.
### Contributing ### Contributing

View File

@@ -6,7 +6,6 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
<!-- STATS_START --> <!-- STATS_START -->
## 📊 社区统计 ## 📊 社区统计
>
> ![updated_zh](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_updated_zh.json&style=flat) > ![updated_zh](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_updated_zh.json&style=flat)
| 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 | | 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 |
@@ -17,19 +16,18 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
| :---: | :---: | :---: | :---: | :---: | | :---: | :---: | :---: | :---: | :---: |
| ![posts](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_posts.json&style=flat) | ![downloads](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_downloads.json&style=flat) | ![views](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_views.json&style=flat) | ![upvotes](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_upvotes.json&style=flat) | ![saves](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_saves.json&style=flat) | | ![posts](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_posts.json&style=flat) | ![downloads](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_downloads.json&style=flat) | ![views](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_views.json&style=flat) | ![upvotes](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_upvotes.json&style=flat) | ![saves](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_saves.json&style=flat) |
### 🔥 热门插件 Top 6
### 🔥 热门插件 Top 6
| 排名 | 插件 | 版本 | 下载 | 浏览 | 📅 更新 | | 排名 | 插件 | 版本 | 下载 | 浏览 | 📅 更新 |
| :---: | :--- | :---: | :---: | :---: | :---: | | :---: | :--- | :---: | :---: | :---: | :---: |
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | ![v](https://img.shields.io/badge/v-1.0.0-blue?style=flat) | ![p1_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_dl.json&style=flat) | ![p1_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--08-gray?style=flat) | | 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | ![v](https://img.shields.io/badge/v-1.0.0-blue?style=flat) | ![p1_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_dl.json&style=flat) | ![p1_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--08-gray?style=flat) |
| 🥈 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | ![v](https://img.shields.io/badge/v-1.5.0-blue?style=flat) | ![p2_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_dl.json&style=flat) | ![p2_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--08-gray?style=flat) | | 🥈 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | ![v](https://img.shields.io/badge/v-1.5.0-blue?style=flat) | ![p2_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_dl.json&style=flat) | ![p2_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--08-gray?style=flat) |
| 🥉 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | ![v](https://img.shields.io/badge/v-1.2.7-blue?style=flat) | ![p3_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_dl.json&style=flat) | ![p3_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--08-gray?style=flat) | | 🥉 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | ![v](https://img.shields.io/badge/v-1.2.7-blue?style=flat) | ![p3_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_dl.json&style=flat) | ![p3_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--08-gray?style=flat) |
| 4⃣ | [Export to Word Enhanced](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | ![v](https://img.shields.io/badge/v-0.4.4-blue?style=flat) | ![p4_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p4_dl.json&style=flat) | ![p4_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p4_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--08-gray?style=flat) | | 4⃣ | [Export to Word Enhanced](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | ![v](https://img.shields.io/badge/v-0.4.4-blue?style=flat) | ![p4_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p4_dl.json&style=flat) | ![p4_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p4_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--08-gray?style=flat) |
| 5⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | ![v](https://img.shields.io/badge/v-1.4.1-blue?style=flat) | ![p5_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_dl.json&style=flat) | ![p5_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--11-gray?style=flat) | | 5⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | ![v](https://img.shields.io/badge/v-1.4.0-blue?style=flat) | ![p5_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_dl.json&style=flat) | ![p5_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--09-gray?style=flat) |
| 6⃣ | [AI Task Instruction Generator](https://openwebui.com/posts/ai_task_instruction_generator_9bab8b37) | ![v](https://img.shields.io/badge/v-N/A-gray?style=flat) | ![p6_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p6_dl.json&style=flat) | ![p6_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p6_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--08-gray?style=flat) | | 6⃣ | [AI Task Instruction Generator](https://openwebui.com/posts/ai_task_instruction_generator_9bab8b37) | ![v](https://img.shields.io/badge/v-N/A-gray?style=flat) | ![p6_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p6_dl.json&style=flat) | ![p6_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p6_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--08-gray?style=flat) |
### 📈 总下载量累计趋势 ### 📈 总下载量累计趋势
![Activity](https://gist.githubusercontent.com/Fu-Jie/db3d95687075a880af6f1fba76d679c6/raw/chart.svg) ![Activity](https://gist.githubusercontent.com/Fu-Jie/db3d95687075a880af6f1fba76d679c6/raw/chart.svg)
*完整统计与趋势图请查看 [社区统计报告](./docs/community-stats.zh.md)* *完整统计与趋势图请查看 [社区统计报告](./docs/community-stats.zh.md)*
@@ -168,20 +166,4 @@ Open WebUI 的前端增强扩展:
本项目是一个资源集合,无需安装 Python 环境。你只需要下载对应的文件并导入到你的 OpenWebUI 实例中即可。 本项目是一个资源集合,无需安装 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)。
### 使用提示词
1. 浏览 `/prompts` 目录并选择一个提示词文件(`.md`)。
2. 复制文件内容。
3. 在 OpenWebUI 聊天界面中,点击输入框上方的"提示词"按钮。
4. 粘贴内容并保存。
[贡献指南](./CONTRIBUTING_CN.md) | [更新日志](./CHANGELOG.md) [贡献指南](./CONTRIBUTING_CN.md) | [更新日志](./CHANGELOG.md)

99
TEST_CASES_V1.2.8.md Normal file
View File

@@ -0,0 +1,99 @@
# Markdown Normalizer v1.2.8 测试用例集
您可以将以下内容逐个复制到 OpenWebUI 的聊天框中,以验证插件的各项修复功能。
---
## 用例 1验证 SQL 代码块换行修复 (需要手动开启配置)
**测试目的**:验证 `enable_escape_fix_in_code_blocks` 开关是否生效。
**前提条件**:请先在插件 Valves 设置中将 `enable_escape_fix_in_code_blocks` 设置为 **开启 (True)**
**复制以下内容:**
```text
请帮我美化这段 SQL 的排版,使其恢复正常换行:
```sql
SELECT * \n FROM users \n WHERE status = 'active' \n AND created_at > '2024-01-01' \n ORDER BY id DESC;
```
```
**预期效果**SQL 代码块内的 `\n` 消失,变为整齐的多行 SQL 语句。
---
## 用例 2验证上下文感知保护 (防止误伤技术内容)
**测试目的**:验证插件是否能准确识别“纯文本”和“代码区域”,只修复该修复的地方。
**配置要求**:默认配置即可。
**复制以下内容:**
```text
这是一个综合测试用例。
1. 普通文本修复测试:
这是第一行\\n这是第二行你应该看到这里发生了换行
2. 行内代码保护测试(不应被修改):
- 正则表达式:`[\n\r\t]`
- Windows 路径:`C:\Windows\System32\drivers\etc\hosts`
- 转义测试:`\\n` 应该保持字面量。
3. LaTeX 命令保护测试:
这里的数学公式 $\times \theta \nu \sum$ 应该渲染正常,反斜杠不应被修掉。
4. 现代 LaTeX 定界符转换:
\[ E = mc^2 \]
(上面这行应该被自动转换为 $$ 包围的块级公式)
```
**预期效果**
- 第一部分的 `\\n` 成功换行。
- 第二部分反引号 `` ` `` 里的内容原封不动。
- 第三部分的希腊字母公式渲染正常。
- 第四部分的 `\[` 变成了 `$$` 且能正常显示公式。
---
## 用例 3验证思维链与详情标签规范化
**测试目的**:验证对 `<thought>``<details>` 标签的排版优化。
**复制以下内容:**
```text
<thinking>
这是一个正在思考的思维链。
</thinking>
<details>
<summary>点击查看详情</summary>
这里的排版通常容易出错。
</details>
紧接着详情标签的文字(应该和上面有空行隔开)。
```
**预期效果**
- `<thinking>` 标签被统一为 `<thought>`
- `</details>` 标签下方自动注入了空行,防止与正文粘连导致渲染失效。
---
## 用例 4极端压力与回滚测试 (稳定性验证)
**测试目的**:模拟复杂嵌套环境,验证 100% 回滚机制。
**复制以下内容:**
```text
尝试混合所有复杂元素:
- 列表项 1
- 列表项 2 with `inline \\n code`
- $ \text{Math } \alpha $
```sql
-- SQL with nested issue
SELECT 'literal \n string' FROM `table`;
```
<thought>End of test</thought>
```
**预期效果**
- 无论内部处理逻辑多么复杂,插件都应保证输出稳定的结果。
- 如果模拟任何内部崩溃(技术人员可用),消息会回滚至此原始文本,不会导致页面白屏。

View File

@@ -103,6 +103,13 @@ hide:
## Quick Start ## Quick Start
### Using Prompts
1. Browse the [Prompt Library](prompts/library.md) and select a prompt
2. Click the **Copy** button to copy the prompt to your clipboard
3. In OpenWebUI, click the "Prompt" button above the input box
4. Paste the content and save
### Using Plugins ### Using Plugins
1. **Install from OpenWebUI Community (Recommended)**: 1. **Install from OpenWebUI Community (Recommended)**:
@@ -110,14 +117,11 @@ hide:
- Browse the plugins and select the one you like. - Browse the plugins and select the one you like.
- Click "Get" to import it directly into your OpenWebUI instance. - Click "Get" to import it directly into your OpenWebUI instance.
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](https://github.com/Fu-Jie/openwebui-extensions/blob/main/scripts/DEPLOYMENT_GUIDE.md) for details. 2. **Manual Installation**:
- Browse the [Plugin Center](plugins/index.md) and download the plugin file (`.py`)
### Using Prompts - Open OpenWebUI **Admin Panel****Settings****Plugins**
- Click the upload button and select the `.py` file
1. Browse the [Prompt Library](prompts/library.md) and select a prompt - Refresh the page and enable the plugin in your chat settings
2. Click the **Copy** button to copy the prompt to your clipboard
3. In OpenWebUI, click the "Prompt" button above the input box
4. Paste the content and save
--- ---

View File

@@ -103,6 +103,13 @@ hide:
## 快速入门 ## 快速入门
### 使用提示词
1. 浏览[提示词库](prompts/library.md)并选择一个提示词
2. 点击**复制**按钮将提示词复制到剪贴板
3. 在 OpenWebUI 中,点击输入框上方的"提示词"按钮
4. 粘贴内容并保存
### 使用插件 ### 使用插件
1. **从 OpenWebUI 社区安装 (推荐)**: 1. **从 OpenWebUI 社区安装 (推荐)**:
@@ -110,14 +117,11 @@ hide:
- 浏览插件列表,选择你喜欢的插件。 - 浏览插件列表,选择你喜欢的插件。
- 点击 "Get" 按钮,将其直接导入到你的 OpenWebUI 实例中。 - 点击 "Get" 按钮,将其直接导入到你的 OpenWebUI 实例中。
2. **快速安装全部插件**:想要一次性安装此项目中的所有插件到本地 OpenWebUI 实例,克隆此仓库后运行 `python scripts/install_all_plugins.py`,并在 `.env` 中配置好 API 密钥,详见[部署指南](https://github.com/Fu-Jie/openwebui-extensions/blob/main/scripts/DEPLOYMENT_GUIDE.md)。 2. **手动安装**:
- 浏览[插件中心](plugins/index.md)并下载插件文件(`.py`
### 使用提示词 - 打开 OpenWebUI **管理面板****设置****插件**
- 点击上传按钮并选择 `.py` 文件
1. 浏览[提示词库](prompts/library.md)并选择一个提示词 - 刷新页面并在聊天设置中启用插件
2. 点击**复制**按钮将提示词复制到剪贴板
3. 在 OpenWebUI 中,点击输入框上方的"提示词"按钮
4. 粘贴内容并保存
--- ---

View File

@@ -1,13 +1,15 @@
# Async Context Compression Filter # Async Context Compression Filter
**Author:** [Fu-Jie](https://github.com/Fu-Jie/openwebui-extensions) | **Version:** 1.4.1 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **License:** MIT **Author:** [Fu-Jie](https://github.com/Fu-Jie/openwebui-extensions) | **Version:** 1.4.0 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **License:** MIT
This filter reduces token consumption in long conversations through intelligent summarization and message compression while keeping conversations coherent. This filter reduces token consumption in long conversations through intelligent summarization and message compression while keeping conversations coherent.
## What's new in 1.4.1 ## What's new in 1.4.0
- **Reverse-Unfolding Mechanism**: Accurately reconstructs the expanded native tool-calling sequence during the outlet phase to permanently fix coordinate drift and missing summaries for long tool-based conversations. - **Atomic Message Grouping**: Introduced structure-aware grouping for `assistant-tool-tool-assistant` chains to prevent "No tool call found" errors.
- **Safer Tool Trimming**: Refactored `enable_tool_output_trimming` to strictly use atomic block groups for safe trimming, completely preventing JSON payload corruption. - **Tail Boundary Alignment**: Implemented automatic correction for truncation points to ensure they don't fall inside a tool-calling sequence.
- **Chat Session Locking**: Added a session-based lock to prevent multiple concurrent summary tasks for the same chat ID.
- **Enhanced Traceability**: Improved summary formatting to include message IDs, names, and metadata for better context tracking.
--- ---

View File

@@ -1,15 +1,17 @@
# 异步上下文压缩过滤器 # 异步上下文压缩过滤器
**作者:** [Fu-Jie](https://github.com/Fu-Jie/openwebui-extensions) | **版本:** 1.4.1 | **项目:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **许可证:** MIT **作者:** [Fu-Jie](https://github.com/Fu-Jie/openwebui-extensions) | **版本:** 1.4.0 | **项目:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **许可证:** MIT
> **重要提示**:为了确保所有过滤器的可维护性和易用性,每个过滤器都应附带清晰、完整的文档,以确保其功能、配置和使用方法得到充分说明。 > **重要提示**:为了确保所有过滤器的可维护性和易用性,每个过滤器都应附带清晰、完整的文档,以确保其功能、配置和使用方法得到充分说明。
本过滤器通过智能摘要和消息压缩技术,在保持对话连贯性的同时,显著降低长对话的 Token 消耗。 本过滤器通过智能摘要和消息压缩技术,在保持对话连贯性的同时,显著降低长对话的 Token 消耗。
## 1.4.1 版本更新 ## 1.4.0 版本更新
- **逆向展开机制**: 引入 `_unfold_messages` 机制以在 `outlet` 阶段精确对齐坐标系,彻底解决了由于前端视图折叠导致长轮次工具调用对话出现进度漂移或跳过生成摘要的问题 - **原子消息组 (Atomic Grouping)**: 引入结构感知的消息分组逻辑,确保工具调用链被整体保留或移除,彻底解决 "No tool call found" 错误
- **更安全的工具内容裁剪**: 重构了 `enable_tool_output_trimming`,现在严格使用原子级分组进行安全的原生工具内容裁剪,替代了激进的正则表达式匹配,防止 JSON 载荷损坏 - **尾部边界自动对齐**: 实现了截断点的自动修正逻辑,确保历史上下文截断不会落在工具调用序列中间
- **会话级异步锁**: 增加了基于 `chat_id` 的后台任务锁,防止同一会话并发触发多个总结任务。
- **元数据溯源增强**: 优化了总结输入格式,在总结中保留了消息 ID、参与者名称及关键元数据提升上下文可追踪性。
--- ---

View File

@@ -22,7 +22,7 @@ Filters act as middleware in the message pipeline:
Reduces token consumption in long conversations through intelligent summarization while maintaining coherence. Reduces token consumption in long conversations through intelligent summarization while maintaining coherence.
**Version:** 1.4.1 **Version:** 1.4.0
[:octicons-arrow-right-24: Documentation](async-context-compression.md) [:octicons-arrow-right-24: Documentation](async-context-compression.md)

View File

@@ -22,7 +22,7 @@ Filter 充当消息管线中的中间件:
通过智能总结减少长对话的 token 消耗,同时保持连贯性。 通过智能总结减少长对话的 token 消耗,同时保持连贯性。
**版本:** 1.4.1 **版本:** 1.4.0
[:octicons-arrow-right-24: 查看文档](async-context-compression.md) [:octicons-arrow-right-24: 查看文档](async-context-compression.md)

View File

@@ -128,13 +128,11 @@ We follow [Semantic Versioning](https://semver.org/):
### release.yml ### release.yml
**Triggers:** **Triggers:**
- ⭐ Push to `main` branch with `plugins/**/*.py` changes (auto-release) - ⭐ Push to `main` branch with `plugins/**/*.py` changes (auto-release)
- Manual workflow dispatch - Manual workflow dispatch
- Push of version tags (`v*`) - Push of version tags (`v*`)
**Actions:** **Actions:**
1. Detects version changes compared to last release 1. Detects version changes compared to last release
2. Collects updated plugin files 2. Collects updated plugin files
3. Generates release notes (with commit history) 3. Generates release notes (with commit history)
@@ -143,11 +141,9 @@ We follow [Semantic Versioning](https://semver.org/):
### plugin-version-check.yml ### plugin-version-check.yml
**Trigger:** **Trigger:**
- Pull requests that modify `plugins/**/*.py` - Pull requests that modify `plugins/**/*.py`
**Actions:** **Actions:**
1. Compares plugin versions between base and PR 1. Compares plugin versions between base and PR
2. Checks if version was updated 2. Checks if version was updated
3. Checks if PR description is detailed enough 3. Checks if PR description is detailed enough
@@ -191,31 +187,6 @@ python scripts/extract_plugin_versions.py --json --output versions.json
--- ---
## Installing All Plugins to Your Instance
After a release, you can quickly install all plugins to your OpenWebUI instance:
```bash
# Clone the repository
git clone https://github.com/Fu-Jie/openwebui-extensions.git
cd openwebui-extensions
# Configure API key and instance URL
echo "api_key=sk-your-api-key-here" > scripts/.env
echo "url=http://localhost:3000" >> scripts/.env
# For remote instances, set the appropriate baseURL:
# echo "url=http://192.168.1.10:3000" >> scripts/.env
# echo "url=https://openwebui.example.com" >> scripts/.env
# Install all plugins at once
python scripts/install_all_plugins.py
```
For detailed instructions, see [Deployment Guide](https://github.com/Fu-Jie/openwebui-extensions/blob/main/scripts/DEPLOYMENT_GUIDE.md).
---
## Author ## Author
Fu-Jie Fu-Jie

View File

@@ -128,13 +128,11 @@ git push origin v1.0.0
### release.yml ### release.yml
**触发条件:** **触发条件:**
- ⭐ 推送到 `main` 分支且修改了 `plugins/**/*.py`(自动发布) - ⭐ 推送到 `main` 分支且修改了 `plugins/**/*.py`(自动发布)
- 手动触发 (workflow_dispatch) - 手动触发 (workflow_dispatch)
- 推送版本标签 (`v*`) - 推送版本标签 (`v*`)
**动作:** **动作:**
1. 检测与上次 Release 的版本变化 1. 检测与上次 Release 的版本变化
2. 收集更新的插件文件 2. 收集更新的插件文件
3. 生成发布说明(含提交记录) 3. 生成发布说明(含提交记录)
@@ -143,11 +141,9 @@ git push origin v1.0.0
### plugin-version-check.yml ### plugin-version-check.yml
**触发条件:** **触发条件:**
- 修改 `plugins/**/*.py` 的 Pull Request - 修改 `plugins/**/*.py` 的 Pull Request
**动作:** **动作:**
1. 比较基础分支和 PR 的插件版本 1. 比较基础分支和 PR 的插件版本
2. 检查是否有版本更新 2. 检查是否有版本更新
3. 检查 PR 描述是否足够详细 3. 检查 PR 描述是否足够详细
@@ -189,31 +185,6 @@ python scripts/extract_plugin_versions.py --json --output versions.json
--- ---
## 批量安装所有插件到你的实例
在发布之后,你可以快速将所有插件安装到 OpenWebUI 实例:
```bash
# 克隆仓库
git clone https://github.com/Fu-Jie/openwebui-extensions.git
cd openwebui-extensions
# 配置 API 密钥和实例地址
echo "api_key=sk-your-api-key-here" > scripts/.env
echo "url=http://localhost:3000" >> scripts/.env
# 如果是远程实例,需要设置相应的 baseURL
# echo "url=http://192.168.1.10:3000" >> scripts/.env
# echo "url=https://openwebui.example.com" >> scripts/.env
# 一次性安装所有插件
python scripts/install_all_plugins.py
```
详细说明请参考 [部署指南](https://github.com/Fu-Jie/openwebui-extensions/blob/main/scripts/DEPLOYMENT_GUIDE.md)。
---
## 作者 ## 作者
Fu-Jie Fu-Jie

51
original_system_prompt.md Normal file
View File

@@ -0,0 +1,51 @@
You are a helpful assistant.
[Session Context]
- **Your Isolated Workspace**: `/app/backend/data/copilot_workspace/user_123/chat_456`
- **Active User ID**: `user_123`
- **Active Chat ID**: `chat_456`
- **Skills Directory**: `/app/backend/data/skills/shared/` — contains user-installed skills.
- **Config Directory**: `/app/backend/data/.copilot` — system configuration (Restricted).
- **CLI Tools Path**: `/app/backend/data/.copilot_tools/` — Global tools installed via npm or pip will automatically go here and be in your $PATH. Python tools are strictly isolated in a venv here.
**CRITICAL INSTRUCTION**: You MUST use the above workspace for ALL file operations.
- DO NOT create files in `/tmp` or any other system directories.
- Always interpret 'current directory' as your Isolated Workspace.
[Available Native System Tools]
The host environment is rich. Based on the official OpenWebUI Docker deployment baseline (backend image), the following CLI tools are expected to be preinstalled and globally available in $PATH:
- **Network/Data**: `curl`, `jq`, `netcat-openbsd`
- **Media/Doc**: `pandoc` (format conversion), `ffmpeg` (audio/video)
- **Build/System**: `git`, `gcc`, `make`, `build-essential`, `zstd`, `bash`
- **Python/Runtime**: `python3`, `pip3`, `uv`
- **Verification Rule**: Before installing any CLI/tool dependency, first check availability with `which <tool>` or a lightweight version probe (e.g. `<tool> --version`).
- **Python Libs**: The active virtual environment inherits `--system-site-packages`. Advanced libraries like `pandas`, `numpy`, `pillow`, `opencv-python-headless`, `pypdf`, `langchain`, `playwright`, `httpx`, and `beautifulsoup4` are ALREADY installed. Try importing them before attempting to install.
[Mode Context: Plan Mode]
You are currently operating in **Plan Mode**.
DEFINITION: Plan mode is a collaborative phase to outline multi-step plans or conduct research BEFORE any code is modified.
<workflow>
1. Clarification: If requirements/goals are ambiguous, ask questions.
2. Analysis: Analyze the codebase to understand constraints. You MAY use shell commands (e.g., `ls`, `grep`, `find`, `cat`) and other read-only tools.
3. Formulation: Generate your structured plan OR research findings.
4. Approval: Present the detailed plan directly to the user for approval via chat.
</workflow>
<key_principles>
- ZERO CODE MODIFICATION: You must NOT execute file edits, write operations, or destructive system changes. Your permissions are locked to READ/RESEARCH ONLY, with the sole exception of the progress-tracking file `plan.md`.
- SHELL USAGE: Shell execution is ENABLED for research purposes. Any attempts to modify the filesystem via shell (e.g., `sed -i`, `rm`) will be strictly blocked, except for appending to `plan.md`.
- PURE RESEARCH SUPPORT: If the user requests a pure research report, output your conclusions directly matching the plan style.
- PERSISTENCE: You MUST save your proposed plan to `/app/backend/data/.copilot/session-state/chat_456/plan.md` to sync with the UI. The UI automatically reads this file to update the plan view.
</key_principles>
<plan_format>
When presenting your findings or plan in the chat, structure it clearly:
## Plan / Report: {Title}
**TL;DR**: {Summary}
**Detailed Tasks / Steps**: {List step-by-step}
**Affected Files**:
- `path/to/file`
**Constraint/Status**: {Any constraints}
</plan_format>
Acknowledge your role as a planner and format your next response using the plan style above.

View File

@@ -1,62 +0,0 @@
# 异步上下文压缩插件:当前问题与处理状态总结
这份文档详细梳理了我们在处理 `async_context_compression`(异步上下文压缩插件)时,遭遇的“幽灵截断”问题的根本原因,以及我们目前的解决进度。
## 1. 根本原因:两种截然不同的“世界观”(数据序列化差异)
在我们之前的排查中,我曾错误地认为:`outlet`(后置处理阶段)拿到的 `body["messages"]` 是由于截断导致的残缺数据。
但根据您提供的本地运行日志,**您是对的,`body['messages']` 确实包含了完整的对话历史**。
那么为什么长度会产生 `inlet 看到 27 条`,而 `outlet 只看到 8 条` 这种巨大的差异?
原因在于OpenWebUI 的管道在进入大模型前和从大模型返回后,使用了**两种完全不同的消息格式**
### 视图 AInlet 阶段(原生 API 展开视图)
- **特点**:严格遵循 OpenAI 函数调用规范。
- **状态**:每一次工具调用、工具返回,都被视为一条独立的 message。
- **例子**:一个包含了复杂搜索的对话。
- User: 帮我查一下天气1条
- Assistant: 发起 tool_call1条
- Tool: 返回 JSON 结果1条
- ...多次往复...
- **最终总计27 条。**我们的压缩算法trim是基于这个 27 条的坐标系来计算保留多少条的。
### 视图 BOutlet 阶段UI HTML 折叠视图)
- **特点**:专为前端渲染优化的紧凑视图。
- **状态**OpenWebUI 在调用完模型后,为了让前端显示出那个好看的、可折叠的工具调用卡片,强行把中间所有的 Tool 交互过程,用 `<details type="tool_calls">...</details>` 的 HTML 代码包裹起来,塞进了一个 `role: assistant``content` 字符串里!
- **例子**:同样的对话。
- User: 帮我查一下天气1条
- Assistant: `<details>包含了好多次工具调用和结果的代码</details> 今天天气很好...`1条
- **最终总计8 条。**
**💥 灾难发生点:**
原本的插件逻辑假定 `inlet``outlet` 共享同一个坐标系。
1.`inlet` 时,系统计算出:“我需要把前 10 条消息生成摘要,保留后 17 条”。
2. 系统把“生成前10条摘要”的任务转入后台异步执行。
3. 后台任务在 `outlet` 阶段被触发,此时它拿到的消息数组变成了**视图 B总共只有 8 条)。**
4. 算法试图在只有 8 条消息的数组里,把“前 10 条消息”砍掉并替换为 1 条摘要。
5. **结果就是:数组索引越界/坐标彻底错乱,触发报错,并且可能将最新的有效消息当成旧消息删掉(过度压缩)。**
---
## 2. 目前已解决的问题 (✅ Done)
为了立刻制止这种因为“坐标系错位”导致的数据破坏我们已经落实了热修复Local v1.4.0
**✅ 添加了“折叠视图”的探针防御:**
- 我写了一个函数 `_is_compact_tool_details_view`
- 现在,当后台触发生成摘要时,系统会自动扫描 `outlet` 传来的 `messages`。只要发现里面包含 `<details type="tool_calls">` 这种带有 HTML 折叠标签的痕迹,就会**立刻终止并跳过**当前的摘要生成任务。
- **收益**彻底杜绝了因数组错位而引发的任务报错和强制裁切。UI 崩溃与历史丢失问题得到遏制。
---
## 3. 当前已解决的遗留问题 (✅ Done: 逆向展开修复)
之前因为跳过生成而引入的新限制:**包含工具调用的长轮次对话,无法自动生成“历史摘要”** 的问题,现已彻底解决。
### 最终实施的技术方案:
我们通过源码分析发现OpenWebUI 在进入 `inlet` 时会执行 `convert_output_to_messages` 还原工具调用链。因此,我们在插件的 `outlet` 阶段引入了相同的 **逆向展开 (Deflation/Unfolding)** 机制 `_unfold_messages`
现在,当后台任务拿到 `outlet` 传来的折叠视图时,不会再选择“跳过”。而是自动提取出潜藏在消息对象体内部的原生 `output` 字段,并**将其重新展开为展开视图**(比如将 8 条假象重新还原为真实的 27 条底层数据),使得它的坐标系与 `inlet` 完全对齐。
至此,带有复杂工具调用的长轮次对话也能安全地进行背景自动压缩,不再有任何截断和强制删减的风险!

View File

@@ -1,60 +0,0 @@
# 回复 dhaern — 针对最新审查的跟进
感谢您重新审查了最新版本并提出了持续精准的分析意见。以下针对您剩余的两个关切点逐一回应。
---
### 1. `enable_tool_output_trimming` — 不是功能退化,而是行为变化是有意为之
裁剪逻辑依然存在且可正常运行。以下是当前版本与之前版本的行为对比。
**当前行为(`_trim_native_tool_outputs`,第 835945 行):**
- 通过 `_get_atomic_groups` 遍历原子分组。
- 识别有效的工具调用链:`assistant(tool_calls)``tool` → [可选的 assistant 跟进消息]。
- 如果一条链内所有 `tool` 角色消息的字符数总和超过 **1,200 个字符**,则将 *tool 消息本身的内容* 折叠为一个本地化的 `[Content collapsed]` 占位符,并注入 `metadata.is_trimmed` 标志。
- 同时遍历包含 `<details type="tool_calls">` HTML 块的 assistant 消息,对其中尺寸过大的 `result` 属性进行相同的折叠处理。
-`enable_tool_output_trimming=True``function_calling=native` 时,该函数在 inlet 阶段被调用。
**与旧版本的区别:**
旧版的做法是改写 *assistant 跟进消息*,仅保留"最终答案"。新版的做法是折叠 *tool 响应内容本身*。两者都会缩减上下文体积,但新方法能够保留 tool 调用链的结构完整性(这是本次发布中原子分组工作的前提条件)。
插件头部的 docstring 里还有一段过时的描述("提取最终答案"),与实际行为相悖。最新提交中已将其更正为"将尺寸过大的原生工具输出折叠为简短占位符"。
如果您在寻找旧版本中"仅保留最终答案"的特定行为,该路径已被有意移除,因为它与本次发布引入的原子分组完整性保证相冲突。当前的折叠方案是安全的替代实现。
---
### 2. `compressed_message_count` — 修复是真实有效的;以下是坐标系追踪
您对"从已修改视图重新计算"的担忧,考虑到此前的架构背景,是完全可以理解的。以下精确说明为何当前代码不存在这一问题。
**`outlet` 中的关键变更:**
```python
db_messages = self._load_full_chat_messages(chat_id)
messages_to_unfold = db_messages if (db_messages and len(db_messages) >= len(messages)) else messages
summary_messages = self._unfold_messages(messages_to_unfold)
target_compressed_count = self._calculate_target_compressed_count(summary_messages)
```
`_load_full_chat_messages` 从 OpenWebUI 数据库中获取原始的持久化历史记录。由于在 inlet 渲染期间注入的合成 summary 消息**从未被回写到数据库**,从 DB 路径获取的 `summary_messages` 始终是干净的、未经修改的原始历史记录——没有 summary 标记,没有坐标膨胀。
在此干净列表上调用 `_calculate_target_compressed_count` 的计算逻辑如下(仍在原始历史坐标系内):
```
original_count = len(db_messages)
raw_target = original_count - keep_last
target = atomic_align(raw_target)
```
这个 `target_compressed_count` 值原封不动地传递进 `_generate_summary_async`。在异步任务内部,同一批 `db_messages` 被切片为 `messages[start:target]` 来构建 `middle_messages`。生成完成后(可能从末尾进行原子截断),保存的值为:
```python
saved_compressed_count = start_index + len(middle_messages)
```
这是原始 DB 消息列表中新摘要实际涵盖到的确切位置——不是目标值,也不是来自不同视图的估算值。
**回退路径DB 不可用时)** 使用 inlet 渲染后的 body 消息。此时 `_get_summary_view_state` 会读取注入的 summary 标记的 `covered_until` 字段(该字段在写入时已记录为原子对齐后的 `start_index`),因此 `base_progress` 已经处于原始历史坐标系内,计算可以自然延续,不会混用两种视图。
简而言之:该字段在整个调用链中现在具有唯一、一致的语义——即原始持久化消息列表中,当前摘要文本实际覆盖到的索引位置。
---
再次感谢您严格的审查。您在上次发布后标记的这两个问题已得到处理,文档中的过时描述也已更正。如果发现其他问题,欢迎继续反馈。

View File

@@ -1,60 +0,0 @@
# Reply to dhaern - Follow-up on the Latest Review
Thank you for re-checking the latest version and for the continued precise analysis. Let me address your two remaining concerns directly.
---
### 1. `enable_tool_output_trimming` — Not a regression; behavior change is intentional
The trimming logic is present and functional. Here is what it does now versus before.
**Current behavior (`_trim_native_tool_outputs`, lines 835945):**
- Iterates over atomic groups via `_get_atomic_groups`.
- Identifies valid chains: `assistant(tool_calls)``tool` → [optional assistant follow-up].
- If the combined character count of the `tool` role messages in a chain exceeds **1,200 characters**, it collapses *the tool messages themselves* to a localized `[Content collapsed]` placeholder and injects a `metadata.is_trimmed` flag.
- Separately walks assistant messages containing `<details type="tool_calls">` HTML blocks and collapses oversized `result` attributes in the same way.
- The function is called at inlet when `enable_tool_output_trimming=True` and `function_calling=native`.
**What is different from the previous version:**
The old approach rewrote the *assistant follow-up* message to keep only the "final answer". The new approach collapses the *tool response content* itself. Both reduce context size, but the new approach preserves the structural integrity of the tool-calling chain (which the atomic grouping work in this release depends on).
The docstring in the plugin header also contained a stale description ("extract only the final answer") that contradicted the actual behavior. That has been corrected in the latest commit to accurately say "collapses oversized native tool outputs to a short placeholder."
If you are looking for the specific "keep only the final answer" behavior from the old version, that path was intentionally removed because it conflicted with the atomic-group integrity guarantees introduced in this release. The current collapse approach is a safe replacement.
---
### 2. `compressed_message_count` — The fix is real; here is the coordinate trace
The concern about "recalculating from the already-modified view" is understandable given the previous architecture. Here is exactly why the current code does not have that problem.
**Key change in `outlet`:**
```python
db_messages = self._load_full_chat_messages(chat_id)
messages_to_unfold = db_messages if (db_messages and len(db_messages) >= len(messages)) else messages
summary_messages = self._unfold_messages(messages_to_unfold)
target_compressed_count = self._calculate_target_compressed_count(summary_messages)
```
`_load_full_chat_messages` fetches the raw persisted history from the OpenWebUI database. Because the synthetic summary message (injected during inlet rendering) is **never written back to the database**, `summary_messages` from the DB path is always the clean, unmodified original history — no summary marker, no coordinate inflation.
`_calculate_target_compressed_count` called on this clean list simply computes:
```
original_count = len(db_messages)
raw_target = original_count - keep_last
target = atomic_align(raw_target) # still in original-history coordinates
```
This `target_compressed_count` value is then passed into `_generate_summary_async` unchanged. Inside the async task, the same `db_messages` list is sliced to `messages[start:target]` to build `middle_messages`. After generation (with potential atomic truncation from the end), the saved value is:
```python
saved_compressed_count = start_index + len(middle_messages)
```
This is the exact position in the original DB message list up to which the new summary actually covers — not a target, not an estimate from a different view.
**The fallback path (DB unavailable)** uses the inlet-rendered body messages. In that case `_get_summary_view_state` reads `covered_until` from the injected summary marker (which was written as the atomically-aligned `start_index`), so `base_progress` is already in original-history coordinates. The calculation naturally continues from there without mixing views.
In short: the field now has a single, consistent meaning throughout the entire call chain — the index (in the original, persisted message list) up to which the current summary text actually covers.
---
Thank you again for the rigorous review. The two points you flagged after the last release are now addressed, and the documentation stale description has been corrected. Please do let us know if you spot anything else.

View File

@@ -1,13 +1,15 @@
# Async Context Compression Filter # Async Context Compression Filter
**Author:** [Fu-Jie](https://github.com/Fu-Jie/openwebui-extensions) | **Version:** 1.4.1 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **License:** MIT **Author:** [Fu-Jie](https://github.com/Fu-Jie/openwebui-extensions) | **Version:** 1.4.0 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **License:** MIT
This filter reduces token consumption in long conversations through intelligent summarization and message compression while keeping conversations coherent. This filter reduces token consumption in long conversations through intelligent summarization and message compression while keeping conversations coherent.
## What's new in 1.4.1 ## What's new in 1.4.0
- **Reverse-Unfolding Mechanism**: Accurately reconstructs the expanded native tool-calling sequence during the outlet phase to permanently fix coordinate drift and missing summaries for long tool-based conversations. - **Atomic Message Grouping**: Introduced structure-aware grouping for `assistant-tool-tool-assistant` chains to prevent "No tool call found" errors.
- **Safer Tool Trimming**: Refactored `enable_tool_output_trimming` to strictly use atomic block groups for safe trimming, completely preventing JSON payload corruption. - **Tail Boundary Alignment**: Implemented automatic correction for truncation points to ensure they don't fall inside a tool-calling sequence.
- **Chat Session Locking**: Added a session-based lock to prevent multiple concurrent summary tasks for the same chat ID.
- **Enhanced Traceability**: Improved summary formatting to include message IDs, names, and metadata for better context tracking.
--- ---

View File

@@ -1,15 +1,17 @@
# 异步上下文压缩过滤器 # 异步上下文压缩过滤器
**作者:** [Fu-Jie](https://github.com/Fu-Jie/openwebui-extensions) | **版本:** 1.4.1 | **项目:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **许可证:** MIT **作者:** [Fu-Jie](https://github.com/Fu-Jie/openwebui-extensions) | **版本:** 1.4.0 | **项目:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **许可证:** MIT
> **重要提示**:为了确保所有过滤器的可维护性和易用性,每个过滤器都应附带清晰、完整的文档,以确保其功能、配置和使用方法得到充分说明。 > **重要提示**:为了确保所有过滤器的可维护性和易用性,每个过滤器都应附带清晰、完整的文档,以确保其功能、配置和使用方法得到充分说明。
本过滤器通过智能摘要和消息压缩技术,在保持对话连贯性的同时,显著降低长对话的 Token 消耗。 本过滤器通过智能摘要和消息压缩技术,在保持对话连贯性的同时,显著降低长对话的 Token 消耗。
## 1.4.1 版本更新 ## 1.4.0 版本更新
- **逆向展开机制**: 引入 `_unfold_messages` 机制以在 `outlet` 阶段精确对齐坐标系,彻底解决了由于前端视图折叠导致长轮次工具调用对话出现进度漂移或跳过生成摘要的问题 - **原子消息组 (Atomic Grouping)**: 引入结构感知的消息分组逻辑,确保工具调用链被整体保留或移除,彻底解决 "No tool call found" 错误
- **更安全的工具内容裁剪**: 重构了 `enable_tool_output_trimming`,现在严格使用原子级分组进行安全的原生工具内容裁剪,替代了激进的正则表达式匹配,防止 JSON 载荷损坏 - **尾部边界自动对齐**: 实现了截断点的自动修正逻辑,确保历史上下文截断不会落在工具调用序列中间
- **会话级异步锁**: 增加了基于 `chat_id` 的后台任务锁,防止同一会话并发触发多个总结任务。
- **元数据溯源增强**: 优化了总结输入格式,在总结中保留了消息 ID、参与者名称及关键元数据提升上下文可追踪性。
--- ---

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

View File

@@ -1,461 +0,0 @@
import asyncio
import importlib.util
import os
import sys
import types
import unittest
PLUGIN_PATH = os.path.join(os.path.dirname(__file__), "async_context_compression.py")
MODULE_NAME = "async_context_compression_under_test"
def _ensure_module(name: str) -> types.ModuleType:
module = sys.modules.get(name)
if module is None:
module = types.ModuleType(name)
sys.modules[name] = module
return module
def _install_openwebui_stubs() -> None:
_ensure_module("open_webui")
_ensure_module("open_webui.utils")
chat_module = _ensure_module("open_webui.utils.chat")
_ensure_module("open_webui.models")
users_module = _ensure_module("open_webui.models.users")
models_module = _ensure_module("open_webui.models.models")
chats_module = _ensure_module("open_webui.models.chats")
main_module = _ensure_module("open_webui.main")
_ensure_module("fastapi")
fastapi_requests = _ensure_module("fastapi.requests")
async def generate_chat_completion(*args, **kwargs):
return {}
class DummyUsers:
pass
class DummyModels:
@staticmethod
def get_model_by_id(model_id):
return None
class DummyChats:
@staticmethod
def get_chat_by_id(chat_id):
return None
class DummyRequest:
pass
chat_module.generate_chat_completion = generate_chat_completion
users_module.Users = DummyUsers
models_module.Models = DummyModels
chats_module.Chats = DummyChats
main_module.app = object()
fastapi_requests.Request = DummyRequest
_install_openwebui_stubs()
spec = importlib.util.spec_from_file_location(MODULE_NAME, PLUGIN_PATH)
module = importlib.util.module_from_spec(spec)
sys.modules[MODULE_NAME] = module
assert spec.loader is not None
spec.loader.exec_module(module)
module.Filter._init_database = lambda self: None
class TestAsyncContextCompression(unittest.TestCase):
def setUp(self):
self.filter = module.Filter()
def test_inlet_logs_tool_trimming_outcome_when_no_oversized_outputs(self):
self.filter.valves.show_debug_log = True
self.filter.valves.enable_tool_output_trimming = True
logged_messages = []
async def fake_log(message, log_type="info", event_call=None):
logged_messages.append(message)
async def fake_user_context(__user__, __event_call__):
return {"user_language": "en-US"}
async def fake_event_call(_payload):
return True
self.filter._log = fake_log
self.filter._get_user_context = fake_user_context
self.filter._get_chat_context = lambda body, metadata=None: {
"chat_id": "",
"message_id": "",
}
self.filter._get_latest_summary = lambda chat_id: None
body = {
"params": {"function_calling": "native"},
"messages": [
{
"role": "assistant",
"tool_calls": [{"id": "call_1", "type": "function"}],
"content": "",
},
{"role": "tool", "content": "short result"},
{"role": "assistant", "content": "Final answer"},
],
}
asyncio.run(self.filter.inlet(body, __event_call__=fake_event_call))
self.assertTrue(
any("Tool trimming check:" in message for message in logged_messages)
)
self.assertTrue(
any(
"no oversized native tool outputs were found" in message
for message in logged_messages
)
)
def test_inlet_logs_tool_trimming_skip_reason_when_disabled(self):
self.filter.valves.show_debug_log = True
self.filter.valves.enable_tool_output_trimming = False
logged_messages = []
async def fake_log(message, log_type="info", event_call=None):
logged_messages.append(message)
async def fake_user_context(__user__, __event_call__):
return {"user_language": "en-US"}
async def fake_event_call(_payload):
return True
self.filter._log = fake_log
self.filter._get_user_context = fake_user_context
self.filter._get_chat_context = lambda body, metadata=None: {
"chat_id": "",
"message_id": "",
}
self.filter._get_latest_summary = lambda chat_id: None
body = {"messages": [], "params": {"function_calling": "native"}}
asyncio.run(self.filter.inlet(body, __event_call__=fake_event_call))
self.assertTrue(
any("Tool trimming skipped: tool trimming disabled" in message for message in logged_messages)
)
def test_normalize_native_tool_call_ids_keeps_links_aligned(self):
long_tool_call_id = "call_abcdefghijklmnopqrstuvwxyz_1234567890abcd"
messages = [
{
"role": "assistant",
"tool_calls": [
{
"id": long_tool_call_id,
"type": "function",
"function": {"name": "search", "arguments": "{}"},
}
],
"content": "",
},
{
"role": "tool",
"tool_call_id": long_tool_call_id,
"content": "tool result",
},
]
normalized_count = self.filter._normalize_native_tool_call_ids(messages)
normalized_id = messages[0]["tool_calls"][0]["id"]
self.assertEqual(normalized_count, 1)
self.assertLessEqual(len(normalized_id), 40)
self.assertNotEqual(normalized_id, long_tool_call_id)
self.assertEqual(messages[1]["tool_call_id"], normalized_id)
def test_trim_native_tool_outputs_restores_real_behavior(self):
messages = [
{
"role": "assistant",
"tool_calls": [{"id": "call_1", "type": "function"}],
"content": "",
},
{"role": "tool", "content": "x" * 1600},
{"role": "assistant", "content": "Final answer"},
]
trimmed_count = self.filter._trim_native_tool_outputs(messages, "en-US")
self.assertEqual(trimmed_count, 1)
self.assertEqual(messages[1]["content"], "... [Content collapsed] ...")
self.assertTrue(messages[1]["metadata"]["is_trimmed"])
self.assertTrue(messages[2]["metadata"]["tool_outputs_trimmed"])
self.assertIn("Final answer", messages[2]["content"])
self.assertIn("Tool outputs trimmed", messages[2]["content"])
def test_trim_native_tool_outputs_supports_embedded_tool_call_cards(self):
messages = [
{
"role": "assistant",
"content": (
'<details type="tool_calls" done="true" id="call-1" '
'name="execute_code" arguments="&quot;{}&quot;" '
f'result="&quot;{"x" * 1600}&quot;">\n'
"<summary>Tool Executed</summary>\n"
"</details>\n"
"Final answer"
),
}
]
trimmed_count = self.filter._trim_native_tool_outputs(messages, "en-US")
self.assertEqual(trimmed_count, 1)
self.assertIn(
'result="&quot;... [Content collapsed] ...&quot;"',
messages[0]["content"],
)
self.assertNotIn("x" * 200, messages[0]["content"])
self.assertTrue(messages[0]["metadata"]["tool_outputs_trimmed"])
def test_function_calling_mode_reads_params_fallback(self):
self.assertEqual(
self.filter._get_function_calling_mode(
{"params": {"function_calling": "native"}}
),
"native",
)
def test_function_calling_mode_infers_native_from_message_shape(self):
self.assertEqual(
self.filter._get_function_calling_mode(
{
"messages": [
{
"role": "assistant",
"tool_calls": [{"id": "call_1", "type": "function"}],
"content": "",
},
{"role": "tool", "content": "tool result"},
]
}
),
"native",
)
def test_trim_native_tool_outputs_handles_pending_tool_chain(self):
messages = [
{
"role": "assistant",
"tool_calls": [{"id": "call_1", "type": "function"}],
"content": "",
},
{"role": "tool", "content": "x" * 1600},
]
trimmed_count = self.filter._trim_native_tool_outputs(messages, "en-US")
self.assertEqual(trimmed_count, 1)
self.assertEqual(messages[1]["content"], "... [Content collapsed] ...")
self.assertTrue(messages[1]["metadata"]["is_trimmed"])
def test_target_progress_uses_original_history_coordinates(self):
self.filter.valves.keep_last = 2
summary_message = self.filter._build_summary_message(
"older summary", "en-US", 6
)
messages = [
{"role": "system", "content": "System prompt"},
summary_message,
{"role": "user", "content": "Question 1"},
{"role": "assistant", "content": "Answer 1"},
{"role": "user", "content": "Question 2"},
{"role": "assistant", "content": "Answer 2"},
]
self.assertEqual(self.filter._get_original_history_count(messages), 10)
self.assertEqual(self.filter._calculate_target_compressed_count(messages), 8)
def test_load_full_chat_messages_rebuilds_active_history_branch(self):
class FakeChats:
@staticmethod
def get_chat_by_id(chat_id):
return types.SimpleNamespace(
chat={
"history": {
"currentId": "m3",
"messages": {
"m1": {
"id": "m1",
"role": "user",
"content": "Question",
},
"m2": {
"id": "m2",
"role": "assistant",
"content": "Tool call",
"tool_calls": [{"id": "call_1"}],
"parentId": "m1",
},
"m3": {
"id": "m3",
"role": "tool",
"content": "Tool result",
"tool_call_id": "call_1",
"parentId": "m2",
},
},
}
}
)
original_chats = module.Chats
module.Chats = FakeChats
try:
messages = self.filter._load_full_chat_messages("chat-1")
finally:
module.Chats = original_chats
self.assertEqual([message["id"] for message in messages], ["m1", "m2", "m3"])
self.assertEqual(messages[2]["role"], "tool")
def test_outlet_unfolds_compact_tool_details_view(self):
compact_messages = [
{"role": "user", "content": "U1"},
{
"role": "assistant",
"content": (
'<details type="tool_calls" done="true" id="call-1" '
'name="search_notes" arguments="&quot;{}&quot;" '
f'result="&quot;{"x" * 3000}&quot;">\n'
"<summary>Tool Executed</summary>\n"
"</details>\n"
"Answer 1"
),
},
{"role": "user", "content": "U2"},
{
"role": "assistant",
"content": (
'<details type="tool_calls" done="true" id="call-2" '
'name="merge_notes" arguments="&quot;{}&quot;" '
f'result="&quot;{"y" * 4000}&quot;">\n'
"<summary>Tool Executed</summary>\n"
"</details>\n"
"Answer 2"
),
},
]
async def fake_user_context(__user__, __event_call__):
return {"user_language": "en-US"}
async def noop_log(*args, **kwargs):
return None
create_task_called = False
def fake_create_task(coro):
nonlocal create_task_called
create_task_called = True
coro.close()
return None
self.filter._get_user_context = fake_user_context
self.filter._get_chat_context = lambda body, metadata=None: {
"chat_id": "chat-1",
"message_id": "msg-1",
}
self.filter._should_skip_compression = lambda body, model: False
self.filter._log = noop_log
# Set a low threshold so the task is guaranteed to trigger
self.filter.valves.compression_threshold_tokens = 100
original_create_task = asyncio.create_task
asyncio.create_task = fake_create_task
try:
asyncio.run(
self.filter.outlet(
{"model": "test-model", "messages": compact_messages},
__event_call__=None,
)
)
finally:
asyncio.create_task = original_create_task
self.assertTrue(create_task_called)
def test_summary_save_progress_matches_truncated_input(self):
self.filter.valves.keep_first = 1
self.filter.valves.keep_last = 1
self.filter.valves.summary_model = "fake-summary-model"
self.filter.valves.summary_model_max_context = 0
captured = {}
events = []
async def mock_emitter(event):
events.append(event)
async def mock_summary_llm(
previous_summary,
new_conversation_text,
body,
user_data,
__event_call__,
):
return "new summary"
def mock_save_summary(chat_id, summary, compressed_count):
captured["chat_id"] = chat_id
captured["summary"] = summary
captured["compressed_count"] = compressed_count
async def noop_log(*args, **kwargs):
return None
self.filter._log = noop_log
self.filter._call_summary_llm = mock_summary_llm
self.filter._save_summary = mock_save_summary
self.filter._get_model_thresholds = lambda model_id: {
"max_context_tokens": 3500
}
self.filter._calculate_messages_tokens = lambda messages: len(messages) * 1000
self.filter._count_tokens = lambda text: 1000
messages = [
{"role": "system", "content": "System prompt"},
{"role": "user", "content": "Question 1"},
{"role": "assistant", "content": "Answer 1"},
{"role": "user", "content": "Question 2"},
{"role": "assistant", "content": "Answer 2"},
{"role": "user", "content": "Question 3"},
]
asyncio.run(
self.filter._generate_summary_async(
messages=messages,
chat_id="chat-1",
body={"model": "fake-summary-model"},
user_data={"id": "user-1"},
target_compressed_count=5,
lang="en-US",
__event_emitter__=mock_emitter,
__event_call__=None,
)
)
self.assertEqual(captured["chat_id"], "chat-1")
self.assertEqual(captured["summary"], "new summary")
self.assertEqual(captured["compressed_count"], 2)
self.assertTrue(any(event["type"] == "status" for event in events))
if __name__ == "__main__":
unittest.main()

View File

@@ -1,10 +1,8 @@
[![](https://img.shields.io/badge/OpenWebUI%20Community-Get%20Plugin-blue?style=for-the-badge)](https://openwebui.com/posts/async_context_compression_b1655bc8)
## Overview ## Overview
This release focuses on improving the structural integrity of chat history when using function-calling models and enhancing task reliability through concurrent task management. It introduces "Atomic Message Grouping" to prevent chat context corruption and a session-based locking mechanism to ensure stable background operations. **[🚀 Get/Update on OpenWebUI Community](https://openwebui.com/posts/async_context_compression_b1655bc8)**
**[📖 README](https://github.com/Fu-Jie/openwebui-extensions/blob/main/plugins/filters/async-context-compression/README.md)** This release focuses on improving the structural integrity of chat history when using function-calling models and enhancing task reliability through concurrent task management. Version 1.4.0 introduces "Atomic Message Grouping" to prevent chat context corruption and a session-based locking mechanism to ensure stable background operations.
## New Features ## New Features
@@ -22,3 +20,7 @@ This release focuses on improving the structural integrity of chat history when
## Related Issues ## Related Issues
- **[#56](https://github.com/Fu-Jie/openwebui-extensions/issues/56)**: Tool-Calling context corruption and concurrent summary tasks. - **[#56](https://github.com/Fu-Jie/openwebui-extensions/issues/56)**: Tool-Calling context corruption and concurrent summary tasks.
## Related PRs
- **[#61](https://github.com/Fu-Jie/openwebui-extensions/pull/61)**: (Placeholder) Full implementation of structure-aware grouping.

View File

@@ -1,6 +1,8 @@
[![](https://img.shields.io/badge/OpenWebUI%20%E7%A4%BE%E5%8C%BA-%E8%8E%B7%E5%8F%96%E6%8F%92%E4%BB%B6-blue?style=for-the-badge)](https://openwebui.com/posts/async_context_compression_b1655bc8) ## 概述
本次发布重点优化了在使用工具调用Function Calling模型时对话历史的结构完整性并通过并发任务管理增强了系统的可靠性。新版本引入了“原子消息组”逻辑以防止上下文损坏并增加了会话级锁定机制以确保后台任务的稳定运行。 **[🚀 在 OpenWebUI 社区获取/更新](https://openwebui.com/posts/async_context_compression_b1655bc8)**
本次发布重点优化了在使用工具调用Function Calling模型时对话历史的结构完整性并通过并发任务管理增强了系统的可靠性。1.4.0 版本引入了“原子消息组”逻辑以防止上下文损坏,并增加了会话级锁定机制以确保后台任务的稳定运行。
## 新功能 ## 新功能
@@ -18,3 +20,7 @@
## 相关 Issue ## 相关 Issue
- **[#56](https://github.com/Fu-Jie/openwebui-extensions/issues/56)**: 修复工具调用上下文损坏及并发总结任务冲突问题。 - **[#56](https://github.com/Fu-Jie/openwebui-extensions/issues/56)**: 修复工具调用上下文损坏及并发总结任务冲突问题。
## 相关 PR
- **[#61](https://github.com/Fu-Jie/openwebui-extensions/pull/61)**: (占位符) 结构感知消息分组的完整实现。

View File

@@ -1,17 +0,0 @@
[![](https://img.shields.io/badge/OpenWebUI%20Community-Get%20Plugin-blue?style=for-the-badge)](https://openwebui.com/f/fujie/async_context_compression)
## Overview
This release addresses the critical progress coordinate drift issue in OpenWebUI's `outlet` phase, ensuring robust summarization for long tool-calling conversations.
[View on GitHub](https://github.com/Fu-Jie/openwebui-extensions/blob/main/plugins/filters/async-context-compression/README.md)
- **New Features**
- **Reverse-Unfolding Mechanism**: Accurately reconstructs the expanded native tool-calling sequence during the outlet phase to permanently fix coordinate drift and missing summaries for long tool-based conversations.
- **Safer Tool Trimming**: Refactored `enable_tool_output_trimming` to strictly use atomic block groups for safe trimming, completely preventing JSON payload corruption.
- **Bug Fixes**
- Fixed coordinate drift where `compressed_message_count` could lose track due to OpenWebUI's frontend view truncating tool calls.
- **Related Issues**
- Closes #56

View File

@@ -1,6 +1,6 @@
# MkDocs Documentation Dependencies # MkDocs Documentation Dependencies
# Core MkDocs # Core MkDocs
mkdocs>=1.5.0,<2.0.0 mkdocs>=1.5.0
# Material Theme for MkDocs # Material Theme for MkDocs
mkdocs-material>=9.5.0 mkdocs-material>=9.5.0

View File

@@ -1,26 +0,0 @@
# OpenWebUI Bulk Installer Configuration
#
# Instructions:
# - api_key: Copy from OpenWebUI Settings (starts with sk-)
# - url: OpenWebUI server address (supports localhost, IP, and domain)
#
# URL Examples:
# - Local: http://localhost:3000
# - Remote IP: http://192.168.1.10:3000
# - Domain: https://openwebui.example.com
#
# 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)
# Configure the baseURL where your OpenWebUI instance is running
url=http://localhost:3000
# Alternatively, use environment variable format (both methods are equivalent)
# OPENWEBUI_API_KEY=sk-your-api-key-here
# OPENWEBUI_BASE_URL=http://localhost:3000

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -285,8 +285,9 @@ def format_release_notes(
prev_ver = prev_manifest.get("version") or prev.get("version") prev_ver = prev_manifest.get("version") or prev.get("version")
readme_url = _get_readme_url(curr.get("file_path", "")) readme_url = _get_readme_url(curr.get("file_path", ""))
readme_link = f" | [📖 README]({readme_url})" if readme_url else "" lines.append(f"- **{curr_title}**: v{prev_ver} → v{curr_ver}")
lines.append(f"- **{curr_title}**: v{prev_ver} → v{curr_ver}{readme_link}") if readme_url:
lines.append(f" - 📖 [README]({readme_url})")
lines.append("") lines.append("")
if comparison["removed"] and not ignore_removed: if comparison["removed"] and not ignore_removed:

View File

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

View File

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

View File

@@ -1,173 +0,0 @@
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)]