feat: 新增插件系统、多种插件类型、开发指南及多语言文档。
This commit is contained in:
117
docs/examples/action_plugin_export_to_excel_example_cn.md
Normal file
117
docs/examples/action_plugin_export_to_excel_example_cn.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# `Export to Excel` 插件深度解析:文件生成与下载实战
|
||||
|
||||
## 引言
|
||||
|
||||
`Export to Excel` 是一个非常实用的 `Action` 插件,它能智能地从 AI 的回答中提取 Markdown 表格,将其转换为格式精美的 Excel 文件,并直接在用户的浏览器中触发下载。
|
||||
|
||||
这个插件是一个绝佳的实战案例,它完整地展示了如何实现一个“数据处理 -> 文件生成 -> 前端交互”的闭环。通过解析它,开发者可以学习到如何在 Open WebUI 插件中利用强大的 Python 数据科学生态(如 `pandas`),以及如何实现将后端生成的文件无缝传递给用户。
|
||||
|
||||
## 核心工作流
|
||||
|
||||
该插件的工作流程清晰而高效,可以概括为以下六个步骤:
|
||||
|
||||
1. **解析 (Parse)**: 使用正则表达式从最后一条聊天消息中精准地提取一个或多个 Markdown 表格。
|
||||
2. **分析 (Analyze)**: 智能地查找表格上下文中的 Markdown 标题(`#`, `##` 等),并以此为依据生成有意义的 Excel 工作簿及工作表(Sheet)的名称。
|
||||
3. **生成 (Generate)**: 将解析出的表格数据转换为 `pandas.DataFrame` 对象,这是进行后续处理的基础。
|
||||
4. **格式化与保存 (Format & Save)**: 利用 `pandas` 和 `XlsxWriter` 引擎,在服务器的临时目录中创建一个带有自定义样式(如颜色、对齐、自动列宽)的、符合专业规范的 `.xlsx` 文件。
|
||||
5. **传输与下载 (Transfer & Download)**: 将生成的 Excel 文件内容读取为字节流,进行 Base64 编码,然后通过 `__event_call__` 将编码后的字符串和一段 JavaScript 代码发送到前端。JS 代码在浏览器中解码数据、创建 Blob 对象并触发下载。
|
||||
6. **清理 (Cleanup)**: 下载触发后,立即删除服务器上的临时 Excel 文件,确保不占用服务器资源。
|
||||
|
||||
---
|
||||
|
||||
## 关键开发模式与技术剖析
|
||||
|
||||
### 1. 纯 Python 数据处理生态的威力
|
||||
|
||||
与一些需要深度集成 Open WebUI 后端模型的插件不同,`Export to Excel` 的核心功能完全由通用的 Python 库驱动,这展示了 Open WebUI 插件生态的开放性。
|
||||
|
||||
- **`re` (正则表达式)**: 用于从纯文本消息中稳健地解析出结构化的表格数据。
|
||||
- **`pandas`**: Python 数据分析的事实标准。插件用它来将原始的列表数据转换为强大的 DataFrame,为写入 Excel 提供了极大的便利。
|
||||
- **`xlsxwriter`**: 一个与 `pandas` 无缝集成的库,用于创建具有丰富格式的 Excel 文件,远比 `pandas` 默认的引擎功能更强大。
|
||||
|
||||
**启示**: 开发者可以将庞大而成熟的 Python 第三方库生态无缝地引入到 Open WebUI 插件中,以实现各种复杂的功能。
|
||||
|
||||
### 2. 智能文本上下文分析
|
||||
|
||||
一个优秀的插件不仅应完成任务,还应尽可能“智能”地理解用户意图。该插件的 `generate_names_from_content` 方法就是一个很好的例子。
|
||||
|
||||
- **目标**: 避免生成如 `output.xlsx` 或 `Sheet1` 这样无意义的文件/工作表名。
|
||||
- **实现**:
|
||||
1. 首先,遍历消息内容,找出所有的 Markdown 标题(`#` 到 `######`)及其所在的行号。
|
||||
2. 对于每一个提取出的表格,在所有位于其上方的标题中,选择**行号最大**(即距离最近)的一个作为该表格的名称。
|
||||
3. 如果只有一个表格,则直接使用其名称作为工作簿的名称。
|
||||
4. 如果有多个表格,则使用整篇消息中的**第一个标题**作为工作簿的名称。
|
||||
5. 如果找不到任何标题,则优雅地回退到默认命名方案(如 `用户_20231026.xlsx`)。
|
||||
|
||||
**启示**: 通过对上下文(而不只是目标数据本身)的简单分析,可以极大地提升插件的用户体验。
|
||||
|
||||
### 3. 高质量文件生成 (`pandas` + `xlsxwriter`)
|
||||
|
||||
简单地调用 `df.to_excel()` 只能生成一个“能用”的文件。而此插件通过 `apply_chinese_standard_formatting` 方法展示了如何生成一个“专业”的文件。
|
||||
|
||||
- **引擎选择**: `pd.ExcelWriter(file_path, engine="xlsxwriter")` 是关键,它允许我们访问底层的 `workbook` 和 `worksheet` 对象。
|
||||
- **核心格式化技术**:
|
||||
- **自定义单元格样式**: 通过 `workbook.add_format()` 创建多种样式(如表头、文本、数字、日期),并分别定义字体、颜色、边框、对齐方式等。
|
||||
- **智能内容对齐**: 遵循标准的制表规范,实现了“文本左对齐、数值右对齐、标题/日期/序号居中对齐”。
|
||||
- **中文字符感知列宽**: `calculate_text_width` 方法在计算内容宽度时,将中文字符(及标点)的宽度视为英文字符的两倍,确保了自动调整列宽 (`worksheet.set_column`) 对中文内容同样有效,避免了文字溢出。
|
||||
- **动态行高**: `calculate_text_height` 方法会根据单元格内容的换行符和折行情况计算所需行数,并以此为依据设置行高 (`worksheet.set_row`),确保了包含长文本的单元格也能完整显示。
|
||||
|
||||
**启示**: 魔鬼在细节中。对生成文件的精细格式化是区分“玩具”和“工具”的重要标准。
|
||||
|
||||
### 4. 后端文件生成与下载的标准模式
|
||||
|
||||
如何在 `Action` 插件中安全、高效地让用户下载后端生成的文件?`export_to_excel` 展示了目前**最佳的、也是标准的实现模式**。
|
||||
|
||||
**流程详解**:
|
||||
|
||||
1. **在服务器临时位置创建文件**:
|
||||
```python
|
||||
filename = f"{workbook_name}.xlsx"
|
||||
excel_file_path = os.path.join("app", "backend", "data", "temp", filename)
|
||||
# ... 使用 pandas 保存文件到 excel_file_path ...
|
||||
```
|
||||
2. **将文件读入内存并编码**:
|
||||
```python
|
||||
with open(excel_file_path, "rb") as file:
|
||||
file_content = file.read()
|
||||
base64_blob = base64.b64encode(file_content).decode("utf-8")
|
||||
```
|
||||
3. **通过 `__event_call__` 发送数据和下载指令**:
|
||||
- 将 Base64 字符串和文件名嵌入一段预设的 JavaScript 代码中。
|
||||
- 这段 JS 的作用是在浏览器端解码 Base64、创建文件 Blob、生成一个隐藏的下载链接 (`<a>` 标签),然后模拟用户点击该链接。
|
||||
|
||||
```python
|
||||
js_code = f"""
|
||||
const base64Data = "{base64_blob}";
|
||||
// ... JS 解码并创建下载链接的代码 ...
|
||||
a.download = "{filename}";
|
||||
a.click();
|
||||
"""
|
||||
await __event_call__({"type": "execute", "data": {"code": js_code}})
|
||||
```
|
||||
4. **立即清理临时文件**:
|
||||
```python
|
||||
if os.path.exists(excel_file_path):
|
||||
os.remove(excel_file_path)
|
||||
```
|
||||
|
||||
**模式优势**:
|
||||
- **安全**: 不会暴露服务器的任何文件路径或创建公共的下载 URL。
|
||||
- **无状态**: 服务器上不保留任何用户生成的文件,请求结束后立即清理,节约了存储空间。
|
||||
- **体验好**: 对用户来说,点击按钮后直接弹出浏览器下载框,体验非常流畅。
|
||||
|
||||
### 5. 优雅的错误处理
|
||||
|
||||
插件的 `action` 方法被一个完整的 `try...except` 块包裹。
|
||||
- 当 `extract_tables_from_message` 找不到表格时,它会主动抛出 `HTTPException`。
|
||||
- 在 `except` 块中,插件会通过 `__event_emitter__` 向前端发送一个内容为“没有找到可以导出的表格!”的错误通知 (`notification`),并更新状态栏 (`status`),清晰地告知用户发生了什么。
|
||||
|
||||
**启示**: 任何可能失败的操作都应被捕获,并向用户提供清晰、友好的错误反馈。
|
||||
|
||||
## 总结
|
||||
|
||||
`Export to Excel` 插件是一个将数据处理与前端交互完美结合的典范。通过学习它,我们可以掌握:
|
||||
- 如何利用 `pandas` 和 `xlsxwriter` 等库在后端生成专业品质的二进制文件。
|
||||
- 如何通过 `__event_call__` 这一强大的机制,实现从后端到前端的文件传输和下载触发。
|
||||
- 服务器临时文件的创建、使用和清理这一完整的、安全的生命周期管理模式。
|
||||
- 如何通过解析上下文来提升插件的“智能化”和用户体验。
|
||||
291
docs/examples/action_plugin_smart_mind_map_example_cn.md
Normal file
291
docs/examples/action_plugin_smart_mind_map_example_cn.md
Normal file
@@ -0,0 +1,291 @@
|
||||
# Open WebUI Action 插件开发范例:智绘心图
|
||||
|
||||
## 引言
|
||||
|
||||
“智绘心图” (`smart-mind-map`) 是一个功能强大的 Open WebUI Action 插件。它通过分析用户提供的文本,利用大语言模型(LLM)提取关键信息,并最终生成一个可交互的、可视化的思维导图。本文档将深入解析其源码 (`思维导图.py`),提炼其中蕴含的插件开发知识与最佳实践,为开发者提供一个高质量的参考范例。
|
||||
|
||||
## 核心开发知识点
|
||||
|
||||
- **插件元数据定义**: 如何通过文件头注释定义插件的标题、图标、版本和描述。
|
||||
- **可配置参数 (`Valves`)**: 如何为插件提供灵活的配置选项。
|
||||
- **异步 `action` 方法**: 插件主入口的实现方式及其核心参数的使用。
|
||||
- **实时前端交互 (`EventEmitter`)**: 如何向用户发送实时状态更新和通知。
|
||||
- **与 LLM 交互**: 如何构建动态 Prompt、调用内置 LLM 服务并处理返回结果。
|
||||
- **富文本 (HTML/JS) 输出**: 如何生成包含复杂前端逻辑的 HTML 内容,并将其嵌入聊天响应中。
|
||||
- **健壮性设计**: 如何实现输入验证、全面的错误处理和日志记录。
|
||||
- **访问 Open WebUI 核心模型**: 如何与 Open WebUI 的数据模型(如 `Users`)交互。
|
||||
|
||||
---
|
||||
|
||||
### 1. 插件元数据定义
|
||||
|
||||
Open WebUI 通过文件顶部的特定格式注释来识别和展示插件信息。
|
||||
|
||||
**代码示例 (`思维导图.py`):**
|
||||
```python
|
||||
"""
|
||||
title: 智绘心图
|
||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+CiAgPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMyIgZmlsbD0iY3VycmVudENvbG9yIi8+CiAgPGxpbmUgeDE9IjEyIiB5MT0iOSIgeDI9IjEyIiB5Mj0iNCIvPgogIDxjaXJjbGUgY3g9IjEyIiBjeT0iMyIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjEyIiB5MT0iMTUiIHgyPSIxMiIgeTI9IjIwIi8+CiAgPGNpcmNsZSBjeD0iMTIiIGN5PSIyMSIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjkiIHkxPSIxMiIgeDI9IjQiIHkyPSIxMiIvPgogIDxjaXJjbGUgY3g9IjMiIGN5PSIxMiIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjE1IiB5MT0iMTIiIHgyPSIyMCIgeTI9IjEyIi8+CiAgPGNpcmNsZSBjeD0iMjEiIGN5PSIxMiIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjEwLjUiIHkxPSྡ1LjUiIHgyPSI2IiB5Mj0iNiIvPgogIDxjaXJjbGUgY3g9IjUiIGN5PSI1Iigcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjEzLjUiIHkxPSྡ5LjUgeDI9IjE1IiB5Mj0iNiIvPgogIDxjaXJjbGUgY3g9IjE5IiBjeT0iNSIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9ྡ1LjUgeTE9ྡ3MuNSB4Mj0iNiIgeTI9IjE4Ii8+CiAgPGNpcmNsZSBjeD0iNSIgY3k9IjE5IiByPSྡ1LjUiLz4KICA8bGluZSB4MT0ྡzIuNSB5MT0ྡzIuNSB4Mj0iNSIgeTI9IjE4Ii8+CiAgPGNpcmNsZSBjeD0ྡ5IiBjeT0ྡ5IiByPSྡ1LjUiLz4KPC9zdmc+Cg==
|
||||
version: 0.7.2
|
||||
description: 智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。
|
||||
"""
|
||||
```
|
||||
**知识点**:
|
||||
- `title`: 插件在 UI 中显示的名称。
|
||||
- `icon_url`: 插件的图标,支持 base64 编码的 SVG,以实现无依赖的矢量图标。
|
||||
- `version`: 插件的版本号。
|
||||
- `description`: 插件的功能简介。
|
||||
|
||||
---
|
||||
|
||||
### 2. 可配置参数 (`Valves`)
|
||||
|
||||
通过在 `Action` 类内部定义一个 `Valves` Pydantic 模型,可以为插件创建可在 Web UI 中配置的参数。
|
||||
|
||||
**代码示例 (`思维导图.py`):**
|
||||
```python
|
||||
class Action:
|
||||
class Valves(BaseModel):
|
||||
show_status: bool = Field(
|
||||
default=True, description="是否在聊天界面显示操作状态更新。"
|
||||
)
|
||||
LLM_MODEL_ID: str = Field(
|
||||
default="gemini-2.5-flash",
|
||||
description="用于文本分析的内置LLM模型ID。",
|
||||
)
|
||||
MIN_TEXT_LENGTH: int = Field(
|
||||
default=100, description="进行思维导图分析所需的最小文本长度(字符数)。"
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.valves = self.Valves()
|
||||
```
|
||||
**知识点**:
|
||||
- `Valves` 类继承自 `pydantic.BaseModel`。
|
||||
- 每个字段都是一个配置项,`default` 是默认值,`description` 会在 UI 中作为提示信息显示。
|
||||
- 在 `__init__` 中实例化 `self.valves`,之后可以通过 `self.valves.PARAMETER_NAME` 来访问配置值。
|
||||
|
||||
---
|
||||
|
||||
### 3. 异步 `action` 方法
|
||||
|
||||
`action` 方法是插件的执行入口,它是一个异步函数,接收 Open WebUI 传入的上下文信息。
|
||||
|
||||
**代码示例 (`思维导图.py`):**
|
||||
```python
|
||||
async def action(
|
||||
self,
|
||||
body: dict,
|
||||
__user__: Optional[Dict[str, Any]] = None,
|
||||
__event_emitter__: Optional[Any] = None,
|
||||
__request__: Optional[Request] = None,
|
||||
) -> Optional[dict]:
|
||||
# ... 插件逻辑 ...
|
||||
return body
|
||||
```
|
||||
**知识点**:
|
||||
- `body`: 包含当前聊天上下文的字典,最重要的是 `body.get("messages")`,它包含了完整的消息历史。
|
||||
- `__user__`: 包含当前用户信息的字典,如 `id`, `name`, `language` 等。插件中演示了如何兼容其为 `dict` 或 `list` 的情况。
|
||||
- `__event_emitter__`: 一个可调用的异步函数,用于向前端发送事件,是实现实时反馈的关键。
|
||||
- `__request__`: FastAPI 的 `Request` 对象,用于访问底层请求信息,例如在调用 `generate_chat_completion` 时需要传递。
|
||||
- **返回值**: `action` 方法需要返回修改后的 `body` 字典,其中包含了插件生成的响应。
|
||||
|
||||
---
|
||||
|
||||
### 4. 实时前端交互 (`EventEmitter`)
|
||||
|
||||
使用 `__event_emitter__` 可以极大地提升用户体验,让用户了解插件的执行进度。
|
||||
|
||||
**代码示例 (`思维导图.py`):**
|
||||
```python
|
||||
# 发送通知 (Toast)
|
||||
await __event_emitter__(
|
||||
{
|
||||
"type": "notification",
|
||||
"data": {
|
||||
"type": "info", # 'info', 'success', 'warning', 'error'
|
||||
"content": "智绘心图已启动,正在为您生成思维导图...",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
# 发送状态更新 (Status Bar)
|
||||
await __event_emitter__(
|
||||
{
|
||||
"type": "status",
|
||||
"data": {
|
||||
"description": "智绘心图: 深入分析文本结构...",
|
||||
"done": False, # False 表示进行中
|
||||
"hidden": False,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
# 任务完成
|
||||
await __event_emitter__(
|
||||
{
|
||||
"type": "status",
|
||||
"data": {
|
||||
"description": "智绘心图: 绘制完成!",
|
||||
"done": True, # True 表示已完成
|
||||
"hidden": False, # True 可以让成功状态自动隐藏
|
||||
},
|
||||
}
|
||||
)
|
||||
```
|
||||
**知识点**:
|
||||
- **通知 (`notification`)**: 在屏幕角落弹出短暂的提示信息,适合用于触发、成功或失败的即时反馈。
|
||||
- **状态 (`status`)**: 在聊天输入框上方显示一个持久的状态条,适合展示多步骤任务的当前进度。`done: True` 会标记任务完成。
|
||||
|
||||
---
|
||||
|
||||
### 5. 与 LLM 交互
|
||||
|
||||
插件的核心功能通常依赖于 LLM。`智绘心图` 演示了如何构建一个结构化的 Prompt 并调用 LLM。
|
||||
|
||||
**代码示例 (`思维导图.py`):**
|
||||
```python
|
||||
# 1. 构建动态 Prompt
|
||||
SYSTEM_PROMPT_MINDMAP_ASSISTANT = "..." # 系统指令
|
||||
USER_PROMPT_GENERATE_MINDMAP = "..." # 用户指令模板
|
||||
|
||||
formatted_user_prompt = USER_PROMPT_GENERATE_MINDMAP.format(
|
||||
user_name=user_name,
|
||||
# ... 注入其他上下文信息
|
||||
long_text_content=long_text_content,
|
||||
)
|
||||
|
||||
# 2. 准备 LLM 请求体
|
||||
llm_payload = {
|
||||
"model": self.valves.LLM_MODEL_ID,
|
||||
"messages": [
|
||||
{"role": "system", "content": SYSTEM_PROMPT_MINDMAP_ASSISTANT},
|
||||
{"role": "user", "content": formatted_user_prompt},
|
||||
],
|
||||
"temperature": 0.5,
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
# 3. 获取用户对象并调用 LLM
|
||||
from open_webui.utils.chat import generate_chat_completion
|
||||
from open_webui.models.users import Users
|
||||
|
||||
user_obj = Users.get_user_by_id(user_id)
|
||||
llm_response = await generate_chat_completion(
|
||||
__request__, llm_payload, user_obj
|
||||
)
|
||||
|
||||
# 4. 处理响应
|
||||
assistant_response_content = llm_response["choices"][0]["message"]["content"]
|
||||
markdown_syntax = self._extract_markdown_syntax(assistant_response_content)
|
||||
```
|
||||
**知识点**:
|
||||
- **Prompt 工程**: 将系统指令和用户指令分离。在用户指令中动态注入上下文信息(如用户名、时间、语言),可以使 LLM 的输出更具个性化和准确性。
|
||||
- **调用工具**: 使用 `open_webui.utils.chat.generate_chat_completion` 是与 Open WebUI 内置 LLM 服务交互的标准方式。
|
||||
- **用户上下文**: 调用 `generate_chat_completion` 需要传递 `user_obj`,这可能用于权限控制、计费或模型特定的用户标识。通过 `open_webui.models.users.Users.get_user_by_id` 获取该对象。
|
||||
- **响应解析**: LLM 的响应需要被解析。该插件使用正则表达式从返回的文本中提取核心的 Markdown 内容,并提供了回退机制。
|
||||
|
||||
---
|
||||
|
||||
### 6. 富文本 (HTML/JS) 输出
|
||||
|
||||
Action 插件的一大亮点是能够生成 HTML,从而在聊天界面中渲染丰富的交互式内容。
|
||||
|
||||
**代码示例 (`思维导图.py`):**
|
||||
```python
|
||||
# 1. 定义 HTML 模板
|
||||
HTML_TEMPLATE_MINDMAP = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- 引入 Markmap.js 等外部库 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/markmap-view@0.17"></script>
|
||||
<style>...</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 动态内容占位符 -->
|
||||
<div id="markmap-container-{unique_id}"></div>
|
||||
<script type="text/template" id="markdown-source-{unique_id}">{markdown_syntax}</script>
|
||||
<script>
|
||||
// 嵌入的 JavaScript 逻辑
|
||||
(function() {
|
||||
const uniqueId = "{unique_id}";
|
||||
// ... 渲染逻辑、事件处理 ...
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
# 2. 注入动态内容
|
||||
final_html_content =
|
||||
HTML_TEMPLATE_MINDMAP.replace("{unique_id}", unique_id)
|
||||
.replace("{markdown_syntax}", markdown_syntax)
|
||||
# ... 替换其他占位符
|
||||
|
||||
# 3. 嵌入到聊天响应中
|
||||
html_embed_tag = f"```html\n{final_html_content}\n```"
|
||||
body["messages"][-1]["content"] = f"{long_text_content}\n\n{html_embed_tag}"
|
||||
```
|
||||
**知识点**:
|
||||
- **HTML 模板**: 将静态 HTML/CSS/JS 代码定义为模板字符串,使用占位符(如 `{unique_id}`)来注入动态数据。
|
||||
- **嵌入 JS**: 可以在 HTML 中直接嵌入 JavaScript 代码,用于处理前端交互逻辑,如渲染图表、绑定按钮事件等。`智绘心图` 的 JS 代码负责调用 Markmap.js 库来渲染思维导图,并实现了“复制 SVG”和“复制 Markdown”的按钮功能。
|
||||
- **唯一 ID**: 使用 `unique_id` 是一个好习惯,可以防止在同一页面上多次使用该插件时发生 DOM 元素 ID 冲突。
|
||||
- **响应格式**: 最终的 HTML 内容需要被包裹在 ````html\n...\n```` 代码块中,Open WebUI 的前端会自动识别并渲染它。
|
||||
- **内容追加**: 插件将生成的 HTML 追加到原始用户输入之后,而不是替换它,保留了上下文。
|
||||
|
||||
---
|
||||
|
||||
### 7. 健壮性设计
|
||||
|
||||
一个生产级的插件必须具备良好的健壮性。
|
||||
|
||||
**代码示例 (`思维导图.py`):**
|
||||
```python
|
||||
# 输入验证
|
||||
if len(long_text_content) < self.valves.MIN_TEXT_LENGTH:
|
||||
# ... 返回警告信息 ...
|
||||
return {"messages": [...]}
|
||||
|
||||
# 完整的异常捕获
|
||||
try:
|
||||
# ... 核心逻辑 ...
|
||||
except Exception as e:
|
||||
error_message = f"智绘心图处理失败: {str(e)}"
|
||||
logger.error(f"智绘心图错误: {error_message}", exc_info=True)
|
||||
|
||||
# 向前端发送错误通知
|
||||
if __event_emitter__:
|
||||
await __event_emitter__(...)
|
||||
|
||||
# 在聊天中显示错误信息
|
||||
body["messages"][-1]["content"] = f"❌ **错误:** {user_facing_error}"
|
||||
|
||||
# 日志记录
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info("Action started")
|
||||
logger.error("Error occurred", exc_info=True)
|
||||
```
|
||||
**知识点**:
|
||||
- **输入验证**: 在执行核心逻辑前,对输入(如文本长度)进行检查,可以避免不必要的资源消耗和潜在错误。
|
||||
- **`try...except` 块**: 将主要逻辑包裹在 `try` 块中,并捕获 `Exception`,确保任何意外失败都能被优雅地处理。
|
||||
- **用户友好的错误反馈**: 在 `except` 块中,不仅要记录详细的错误日志(`logger.error`),还要通过 `EventEmitter` 和聊天消息向用户提供清晰、可操作的错误提示。
|
||||
- **日志**: 使用 `logging` 模块记录关键步骤和错误信息,是调试和监控插件运行状态的重要手段。`exc_info=True` 会记录完整的堆栈跟踪。
|
||||
|
||||
---
|
||||
|
||||
### 总结
|
||||
|
||||
`智绘心图` 插件是一个优秀的 Open WebUI Action 开发学习案例。它全面展示了如何利用 Action 插件的各项功能,构建一个交互性强、用户体验好、功能完整且健壮的 AI 应用。
|
||||
|
||||
**最佳实践总结**:
|
||||
- **明确元数据**: 为你的插件提供清晰的 `title`, `icon`, `description`。
|
||||
- **提供配置**: 使用 `Valves` 让插件更灵活。
|
||||
- **善用反馈**: 积极使用 `EventEmitter` 提供实时状态和通知。
|
||||
- **结构化 Prompt**: 精心设计的 Prompt 是高质量输出的保证。
|
||||
- **拥抱富文本**: 利用 HTML 和 JS 创造丰富的交互体验。
|
||||
- **防御性编程**: 始终考虑输入验证和错误处理。
|
||||
- **详细日志**: 记录日志是排查问题的关键。
|
||||
|
||||
通过学习和借鉴`智绘心图`的设计模式,开发者可以更高效地构建出属于自己的高质量 Open WebUI 插件。
|
||||
@@ -0,0 +1,235 @@
|
||||
# Open WebUI Filter 插件开发范例:异步上下文压缩
|
||||
|
||||
## 引言
|
||||
|
||||
“异步上下文压缩” (`async-context-compression`) 是一个功能先进的 Open WebUI `Filter` 插件。它旨在通过在后台异步地对长对话历史进行智能摘要,来显著减少发送给大语言模型(LLM)的 Token 数量,从而在节约成本的同时保持对话的连贯性。
|
||||
|
||||
本文档将深入剖析其源码,提炼其作为高级 `Filter` 插件所展示的设计模式与开发技巧,特别是关于**异步处理**、**数据库集成**和**复杂消息流控制**等方面。
|
||||
|
||||
## 核心开发知识点
|
||||
|
||||
- **Filter 插件结构 (`inlet` / `outlet`)**: 掌握过滤器在请求生命周期中的两个核心切入点。
|
||||
- **异步后台任务**: 如何使用 `asyncio.create_task` 执行耗时操作而不阻塞用户响应。
|
||||
- **数据库持久化**: 如何使用 SQLAlchemy 与数据库(PostgreSQL/SQLite)集成,实现数据的持久化存储。
|
||||
- **高级 `Valves` 配置**: 如何使用 Pydantic 的 `@model_validator` 实现复杂的跨字段配置验证。
|
||||
- **复杂消息体处理**: 如何安全地操作和修改包含多模态内容的消息结构。
|
||||
- **从插件内部调用 LLM**: 在插件中调用 LLM 服务以实现“插件调用插件”的元功能。
|
||||
- **环境变量依赖与初始化**: 如何处理对外部环境变量的依赖,并在插件初始化时进行安全配置。
|
||||
|
||||
---
|
||||
|
||||
### 1. Filter 插件结构 (`inlet` / `outlet`)
|
||||
|
||||
`Filter` 插件通过 `inlet` 和 `outlet` 两个方法,在请求发送给 LLM **之前**和 LLM 响应返回 **之后**对消息进行处理。
|
||||
|
||||
- `inlet(self, body: dict, ...)`: 在请求发送前执行。此插件用它来检查是否存在历史摘要,如果存在,则用摘要替换部分历史消息,从而“压缩”上下文。
|
||||
- `outlet(self, body: dict, ...)`: 在收到 LLM 响应后执行。此插件用它来判断对话是否达到了需要生成摘要的长度阈值,如果是,则触发一个后台任务来生成新的摘要,以供**下一次**对话使用。
|
||||
|
||||
这种“读旧,写新”的异步策略是该插件的核心设计。
|
||||
|
||||
**代码示例 (`async_context_compression.py`):**
|
||||
```python
|
||||
class Filter:
|
||||
def inlet(self, body: dict, ...) -> dict:
|
||||
"""
|
||||
在发送到 LLM 之前执行。
|
||||
应用已有的摘要来压缩本次请求的上下文。
|
||||
"""
|
||||
# 1. 从数据库加载已保存的摘要
|
||||
saved_summary = self._load_summary(chat_id, body)
|
||||
|
||||
# 2. 如果摘要存在且消息足够长
|
||||
if saved_summary and len(messages) > total_kept_count:
|
||||
# 3. 替换中间的消息为摘要
|
||||
body["messages"] = compressed_messages
|
||||
|
||||
return body
|
||||
|
||||
async def outlet(self, body: dict, ...) -> dict:
|
||||
"""
|
||||
在 LLM 响应完成后执行。
|
||||
检查是否需要为下一次请求生成新的摘要。
|
||||
"""
|
||||
# 1. 检查消息总数是否达到阈值
|
||||
if len(messages) >= self.valves.compression_threshold:
|
||||
# 2. 创建一个异步后台任务来生成摘要,不阻塞当前响应
|
||||
asyncio.create_task(
|
||||
self._generate_summary_async(...)
|
||||
)
|
||||
|
||||
return body
|
||||
```
|
||||
**知识点**:
|
||||
- `inlet` 和 `outlet` 分别作用于请求流的不同阶段,实现了功能的解耦。
|
||||
- `inlet` 负责**消费**摘要,`outlet` 负责**生产**摘要,两者通过数据库解耦。
|
||||
|
||||
---
|
||||
|
||||
### 2. 异步后台任务
|
||||
|
||||
对于耗时操作(如调用 LLM 生成摘要),为了不让用户等待,必须采用异步后台处理。这是高级插件必备的技巧。
|
||||
|
||||
**代码示例 (`async_context_compression.py`):**
|
||||
```python
|
||||
# 在 outlet 方法中
|
||||
async def outlet(self, ...):
|
||||
if len(messages) >= self.valves.compression_threshold:
|
||||
# 核心:创建一个后台任务,并立即返回,不等待其完成
|
||||
asyncio.create_task(
|
||||
self._generate_summary_async(messages, chat_id, body, __user__)
|
||||
)
|
||||
return body
|
||||
|
||||
# 后台任务的具体实现
|
||||
async def _generate_summary_async(self, ...):
|
||||
"""
|
||||
在后台异步生成摘要。
|
||||
"""
|
||||
try:
|
||||
# 1. 提取需要被摘要的消息
|
||||
messages_to_summarize = ...
|
||||
|
||||
# 2. 将消息格式化为纯文本
|
||||
conversation_text = self._format_messages_for_summary(messages_to_summarize)
|
||||
|
||||
# 3. 调用 LLM 生成摘要
|
||||
summary = await self._call_summary_llm(conversation_text, body, user_data)
|
||||
|
||||
# 4. 将新摘要存入数据库
|
||||
self._save_summary(chat_id, summary, body)
|
||||
except Exception as e:
|
||||
# 错误处理
|
||||
...
|
||||
```
|
||||
**知识点**:
|
||||
- `asyncio.create_task()`: 这是实现“即发即忘”(fire-and-forget)模式的关键。它将一个协程(`_generate_summary_async`)提交到事件循环中运行,而当前函数(`outlet`)可以继续执行并立即返回,从而确保了前端的快速响应。
|
||||
- **健壮性**: 后台任务必须有自己独立的 `try...except` 块,以防止其内部的失败影响到主程序的稳定性。
|
||||
|
||||
---
|
||||
|
||||
### 3. 数据库持久化 (SQLAlchemy)
|
||||
|
||||
为了在不同对话回合乃至服务重启后都能保留摘要,插件集成了数据库。
|
||||
|
||||
**代码示例 (`async_context_compression.py`):**
|
||||
```python
|
||||
# 1. 依赖环境变量
|
||||
database_url = os.getenv("DATABASE_URL")
|
||||
|
||||
# 2. 定义数据模型
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
Base = declarative_base()
|
||||
|
||||
class ChatSummary(Base):
|
||||
__tablename__ = "chat_summary"
|
||||
id = Column(Integer, primary_key=True)
|
||||
chat_id = Column(String(255), unique=True, index=True)
|
||||
summary = Column(Text)
|
||||
# ... 其他字段
|
||||
|
||||
# 3. 初始化数据库连接
|
||||
def _init_database(self):
|
||||
database_url = os.getenv("DATABASE_URL")
|
||||
if not database_url:
|
||||
# ... 错误处理 ...
|
||||
return
|
||||
|
||||
# 根据 URL 前缀选择驱动 (PostgreSQL/SQLite)
|
||||
if database_url.startswith("sqlite"): ...
|
||||
elif database_url.startswith("postgres"): ...
|
||||
|
||||
self._db_engine = create_engine(database_url, ...)
|
||||
self._SessionLocal = sessionmaker(bind=self._db_engine)
|
||||
Base.metadata.create_all(bind=self._db_engine) # 自动建表
|
||||
|
||||
# 4. 封装 CRUD 操作
|
||||
def _save_summary(self, chat_id: str, summary: str, body: dict):
|
||||
session = self._SessionLocal()
|
||||
try:
|
||||
# ... 查询、更新或创建记录 ...
|
||||
session.commit()
|
||||
finally:
|
||||
session.close()
|
||||
```
|
||||
**知识点**:
|
||||
- **配置驱动**: 插件依赖 `DATABASE_URL` 环境变量,并在 `_init_database` 中进行解析,实现了对不同数据库(PostgreSQL, SQLite)的兼容。
|
||||
- **ORM 模型**: 使用 SQLAlchemy 的声明式基类定义 `ChatSummary` 表结构,使数据库操作对象化,更易于维护。
|
||||
- **自动建表**: `Base.metadata.create_all()` 会在插件首次运行时自动检查并创建不存在的表,简化了部署。
|
||||
- **会话管理**: 使用 `sessionmaker` 创建会话,并通过 `try...finally` 确保会话在使用后被正确关闭,这是管理数据库连接的标准实践。
|
||||
|
||||
---
|
||||
|
||||
### 4. 高级 `Valves` 配置
|
||||
|
||||
除了简单的默认值,`Valves` 还可以通过 Pydantic 的验证器实现更复杂的逻辑。
|
||||
|
||||
**代码示例 (`async_context_compression.py`):**
|
||||
```python
|
||||
from pydantic import model_validator
|
||||
|
||||
class Valves(BaseModel):
|
||||
compression_threshold: int = Field(...)
|
||||
keep_first: int = Field(...)
|
||||
keep_last: int = Field(...)
|
||||
# ... 其他配置
|
||||
|
||||
@model_validator(mode="after")
|
||||
def check_thresholds(self) -> "Valves":
|
||||
kept_count = self.keep_first + self.keep_last
|
||||
if self.compression_threshold <= kept_count:
|
||||
raise ValueError(
|
||||
f"compression_threshold ({self.compression_threshold}) 必须大于 "
|
||||
f"keep_first 和 keep_last 的总和。"
|
||||
)
|
||||
return self
|
||||
```
|
||||
**知识点**:
|
||||
- `@model_validator(mode="after")`: 这个装饰器允许你在所有字段都已赋值**之后**,执行一个自定义的验证函数。
|
||||
- **跨字段验证**: 该插件用它来确保 `compression_threshold` 必须大于 `keep_first` 和 `keep_last` 之和,保证了插件逻辑的正确性,避免了无效配置。
|
||||
|
||||
---
|
||||
|
||||
### 5. 复杂消息体处理
|
||||
|
||||
Open WebUI 的消息体 `content` 可能是简单的字符串,也可能是用于多模态的列表。插件必须能稳健地处理这两种情况。
|
||||
|
||||
**代码示例 (`async_context_compression.py`):**
|
||||
```python
|
||||
def _inject_summary_to_first_message(self, message: dict, summary: str) -> dict:
|
||||
content = message.get("content", "")
|
||||
summary_block = f"【历史对话摘要】\n{summary}\n..."
|
||||
|
||||
if isinstance(content, list): # 多模态内容
|
||||
new_content = []
|
||||
summary_inserted = False
|
||||
for part in content:
|
||||
if part.get("type") == "text" and not summary_inserted:
|
||||
# 将摘要追加到第一个文本部分的前面
|
||||
new_content.append({"type": "text", "text": summary_block + part.get("text", "")})
|
||||
summary_inserted = True
|
||||
else:
|
||||
new_content.append(part)
|
||||
message["content"] = new_content
|
||||
elif isinstance(content, str): # 纯文本
|
||||
message["content"] = summary_block + content
|
||||
|
||||
return message
|
||||
```
|
||||
**知识点**:
|
||||
- **类型检查**: 通过 `isinstance(content, list)` 判断消息是否为多模态类型。
|
||||
- **安全注入**: 在处理多模态列表时,代码会遍历所有 `part`,找到第一个文本部分进行注入,同时保持其他部分(如图片)不变。这确保了插件的兼容性和稳定性。
|
||||
|
||||
---
|
||||
|
||||
### 总结
|
||||
|
||||
`异步上下文压缩` 插件是学习如何构建生产级 Open WebUI `Filter` 的绝佳案例。它不仅展示了 `Filter` 的基本用法,更深入地探讨了在 Web 服务中至关重要的**异步处理**和**持久化存储**。
|
||||
|
||||
**高级实践总结**:
|
||||
- **分离读写**: 利用 `inlet` 和 `outlet` 的生命周期,结合数据库,实现异步的“读写分离”模式。
|
||||
- **非阻塞设计**: 通过 `asyncio.create_task` 将耗时操作移出主请求/响应循环,保证用户体验的流畅性。
|
||||
- **外部依赖管理**: 优雅地处理对环境变量和数据库的依赖,并在初始化时提供清晰的日志和错误提示。
|
||||
- **健壮配置**: 利用模型验证器 (`@model_validator`) 防止用户设置出不符合逻辑的参数。
|
||||
- **兼容性处理**: 在操作消息体时,充分考虑多模态等复杂数据结构,确保插件的广泛适用性。
|
||||
|
||||
通过研究此插件,开发者可以掌握构建需要与外部服务(如数据库)交互、执行复杂后台任务的高级 `Filter` 的核心技能。
|
||||
@@ -0,0 +1,163 @@
|
||||
# `Gemini Manifold Companion` 深度解析:高级 `Filter` 与 `Pipe` 协同开发
|
||||
|
||||
## 引言
|
||||
|
||||
`Gemini Manifold Companion` 是一个 `Filter` 插件,但它的设计目标并非独立运作,而是作为 `Gemini Manifold` 这个 `Pipe` 插件的“伴侣”或“增强包”。它通过在请求到达 `Pipe` 之前和响应返回给用户之后进行一系列精巧的操作,解锁了许多 Open WebUI 原生界面不支持的、`Pipe` 专属的强大功能(如 Google Search, Code Execution 等)。
|
||||
|
||||
本文档将深度解析这个“伴侣插件”的设计模式,重点阐述其如何通过**拦截与翻译**、**跨阶段通信**和**异步 I/O** 等高级技巧,实现与 `Pipe` 插件的完美协同。
|
||||
|
||||
## 核心工作流:拦截与翻译 (Hijack and Translate)
|
||||
|
||||
`Companion` 插件的核心价值体现在其 `inlet` 方法中。它像一个智能的“请求路由器”,在不修改 Open WebUI 前端代码的情况下,将前端的通用功能开关“翻译”成 `Pipe` 插件能理解的专属指令。
|
||||
|
||||
**目标**: 拦截前端通用的功能请求(如“网络搜索”),阻止 Open WebUI 的默认行为,并将其转换为 `Pipe` 插件的专属功能标记。
|
||||
|
||||
#### 实现步骤 (`inlet` 方法):
|
||||
|
||||
1. **识别目标 `Pipe`**: 过滤器首先会检查当前请求是否发往它需要辅助的 `gemini_manifold`。如果不是,则直接返回,不做任何操作。这是伴侣插件模式的基础。
|
||||
|
||||
```python
|
||||
# _get_model_name 会判断当前模型是否由 gemini_manifold 提供
|
||||
canonical_model_name, is_manifold = self._get_model_name(body)
|
||||
if not is_manifold:
|
||||
return body # 不是目标,直接放行
|
||||
```
|
||||
|
||||
2. **拦截功能开关**: 插件检查前端请求的 `body["features"]` 中,`web_search` 是否为 `True`。
|
||||
|
||||
3. **执行“拦截与翻译”**:
|
||||
- **拦截 (Hijack)**: 如果 `web_search` 为 `True`,插件会立即将其改回 `False`。这一步至关重要,它阻止了 Open WebUI 触发其内置的、通用的 RAG 或网页搜索流程。
|
||||
```python
|
||||
features["web_search"] = False
|
||||
```
|
||||
- **翻译 (Translate)**: 紧接着,插件会在一个更深层的、用于插件间通信的 `metadata` 字典中,添加一个**自定义的**、`Pipe` 插件能识别的标志。
|
||||
```python
|
||||
# metadata["features"] 是一个专为插件间通信设计的字典
|
||||
metadata_features["google_search_tool"] = True
|
||||
```
|
||||
|
||||
4. **传递其他指令**: 除了功能开关,`Companion` 还会做一些其他的预处理,例如:
|
||||
- **绕过 RAG**: 如果用户开启了 `BYPASS_BACKEND_RAG`,它会清空 `body["files"]` 数组,并设置 `metadata_features["upload_documents"] = True`,告知 `Pipe` 插件“文件由你来处理”。
|
||||
- **强制流式**: `Pipe` 插件通常返回 `AsyncGenerator`,需要前端以流式模式处理。`Companion` 会强制设置 `body["stream"] = True`,同时将用户原始的流式/非流式选择保存在 `metadata` 中,供 `Pipe` 后续判断。
|
||||
|
||||
**设计模式的价值**: 这种模式实现了极高的解耦。前端只需使用标准的功能开关,而 `Pipe` 插件可以定义任意复杂的、私有的功能集。`Companion` 过滤器则充当了两者之间的智能适配器,使得在不改动核心代码的情况下,扩展后端功能成为可能。
|
||||
|
||||
---
|
||||
|
||||
## 高级技巧 1: `Pipe` -> `Filter` 的跨阶段通信
|
||||
|
||||
**问题**: `Pipe` 在处理过程中生成了重要数据(如包含搜索结果的 `grounding_metadata`),但 `Filter` 的 `outlet` 方法在 `Pipe` 执行**之后**才运行。如何将数据从 `Pipe` 安全地传递给 `Filter`?
|
||||
|
||||
**解决方案**: `request.app.state`,一个在单次 HTTP 请求生命周期内持续存在的共享状态对象。
|
||||
|
||||
#### 实现流程:
|
||||
|
||||
1. **`Pipe` 插件中 (数据写入)**:
|
||||
- 在 `gemini_manifold.py` 的 `_do_post_processing` 阶段(响应流结束后),`Pipe` 会从 Google API 的响应中提取 `grounding_metadata`。
|
||||
- 然后,它使用 `setattr` 将这些数据动态地附加到 `request.app.state` 对象上,使用一个包含 `chat_id` 和 `message_id` 的唯一键。
|
||||
|
||||
```python
|
||||
# 在 gemini_manifold.py 中 (示意代码)
|
||||
def _do_post_processing(self, ..., __request__: Request):
|
||||
app_state = __request__.app.state
|
||||
grounding_key = f"grounding_{chat_id}_{message_id}"
|
||||
|
||||
# 将数据存入请求状态
|
||||
setattr(app_state, grounding_key, grounding_metadata)
|
||||
```
|
||||
|
||||
2. **`Companion Filter` 中 (数据读取)**:
|
||||
- 在 `outlet` 方法中,`Filter` 可以访问同一个 `__request__` 对象。
|
||||
- 它使用 `getattr` 和相同的唯一键,从 `request.app.state` 中安全地取出 `Pipe` 之前存入的数据。
|
||||
|
||||
```python
|
||||
# 在 gemini_manifold_companion.py 的 outlet 方法中
|
||||
async def outlet(self, ..., __request__: Request, ...):
|
||||
app_state = __request__.app.state
|
||||
grounding_key = f"grounding_{chat_id}_{message_id}"
|
||||
|
||||
# 从请求状态中读取数据
|
||||
stored_metadata = getattr(app_state, grounding_key, None)
|
||||
|
||||
if stored_metadata:
|
||||
# 成功获取 Pipe 传来的数据,进行后续处理
|
||||
# (如注入引用标记、解析 URL 等)
|
||||
...
|
||||
|
||||
# 清理状态,避免内存泄漏
|
||||
delattr(app_state, grounding_key)
|
||||
```
|
||||
|
||||
**设计模式的价值**: `request.app.state` 充当了在同一次请求处理链中、不同插件(特别是 `Pipe` 和 `Filter`)之间传递复杂数据的“秘密信道”,是实现高级协同功能的关键。
|
||||
|
||||
---
|
||||
|
||||
## 高级技巧 2: 在 `outlet` 中执行异步 I/O
|
||||
|
||||
**问题**: `grounding_metadata` 中的搜索结果 URL 是 Google 的重定向链接,需要通过网络请求解析成最终的真实网址才能展示给用户。如果在 `outlet` 中同步执行这些请求,会阻塞整个响应流程。
|
||||
|
||||
**解决方案**: 利用 `outlet` 是 `async` 函数的特性,执行并发的异步网络请求。
|
||||
|
||||
#### 实现流程 (`_resolve_and_emit_sources` 方法):
|
||||
|
||||
1. **收集任务**: 从 `grounding_metadata` 中提取所有需要解析的 URL。
|
||||
2. **创建会话**: 使用 `aiohttp.ClientSession` 创建一个异步 HTTP 客户端会话。
|
||||
3. **并发执行**:
|
||||
- 为每个 URL 创建一个 `_resolve_url` 协程任务。
|
||||
- 使用 `asyncio.gather` 并发地执行所有 URL 解析任务。
|
||||
4. **处理结果**: 等待所有解析完成后,将最终的真实 URL 和其他元数据组合成 `sources` 列表。
|
||||
5. **发送事件**: 通过 `__event_emitter__` 将包含最终 `sources` 的 `chat:completion` 事件发送给前端。
|
||||
|
||||
**代码示例 (逻辑简化):**
|
||||
```python
|
||||
async def _resolve_and_emit_sources(self, ...):
|
||||
# ... 提取所有待解析的 URL ...
|
||||
urls_to_resolve = [...]
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
# 为每个 URL 创建一个异步解析任务
|
||||
tasks = [self._resolve_url(session, url) for url in urls_to_resolve]
|
||||
# 并发执行所有任务
|
||||
results = await asyncio.gather(*tasks)
|
||||
|
||||
# ... 处理解析结果 ...
|
||||
resolved_sources = [...]
|
||||
|
||||
# 通过事件发射器将最终结果发送给前端
|
||||
await event_emitter({"type": "chat:completion", "data": {"sources": resolved_sources}})
|
||||
```
|
||||
**设计模式的价值**: 即使在请求处理的最后阶段 (`outlet`),也能够执行高效、非阻塞的 I/O 操作,极大地丰富了插件的能力,而不会牺牲用户体验。
|
||||
|
||||
---
|
||||
|
||||
## 高级技巧 3: 动态日志级别
|
||||
|
||||
**问题**: 如何在不重启服务的情况下,动态调整一个插件的日志详细程度,以便于在线上环境中进行调试?
|
||||
|
||||
**解决方案**: 在 `inlet` 中检查配置变化,并动态地添加/移除 `loguru` 的日志处理器 (Handler)。
|
||||
|
||||
#### 实现流程:
|
||||
|
||||
1. **`__init__`**: 插件初始化时,根据 `Valves` 中的 `LOG_LEVEL` 配置,添加一个带特定过滤器(只输出本插件日志)和格式化器的 `loguru` handler。
|
||||
2. **`inlet`**: 在每次请求进入时,都比较当前阀门中的 `LOG_LEVEL` 与插件实例中保存的 `self.log_level` 是否一致。
|
||||
3. **动态更新**:
|
||||
- 如果不一致,说明管理员修改了配置。
|
||||
- 插件会调用 `log.remove()` 移除旧的 handler。
|
||||
- 然后调用 `log.add()`,使用新的日志级别添加一个新的 handler。
|
||||
- 最后更新 `self.log_level`。
|
||||
|
||||
**设计模式的价值**: 这使得插件的日志管理变得极其灵活。管理员只需在 Web UI 中修改插件的 `LOG_LEVEL` 配置,即可立即(在下一次请求时)看到更详细或更简洁的日志输出,极大地提升了生产环境中的问题排查效率。
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
`Gemini Manifold Companion` 是一个教科书级的“伴侣插件”范例,它揭示了 `Filter` 插件的巨大潜力。通过学习它,我们可以掌握:
|
||||
|
||||
- **协同设计模式**: 如何让 `Filter` 与 `Pipe` 协同工作,以实现标准 UI 之外的复杂功能。
|
||||
- **指令翻译**: 使用 `metadata` 作为 `Filter` 向 `Pipe` 传递“秘密指令”的通信渠道。
|
||||
- **跨阶段状态共享**: 使用 `request.app.state` 作为 `Pipe` 向 `Filter` 回传数据的“临时内存”。
|
||||
- **全异步流程**: 即使在请求的末端 (`outlet`),也能利用 `asyncio` 和 `aiohttp` 执行高效的异步 I/O 操作。
|
||||
- **动态运维能力**: 实现如动态日志级别这样的功能,让插件更易于在生产环境中管理和调试。
|
||||
|
||||
这些高级技巧共同构成了一个强大、解耦且可扩展的插件生态系统,是所有 Open WebUI 插件开发者进阶的必经之路。
|
||||
134
docs/examples/filter_plugin_inject_env_example_cn.md
Normal file
134
docs/examples/filter_plugin_inject_env_example_cn.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# `Inject Env` 插件深度解析:动态修改请求与上下文注入
|
||||
|
||||
## 引言
|
||||
|
||||
`Inject Env` 是一个 `Filter` 插件的绝佳范例,它清晰地展示了过滤器的核心价值:在请求到达 LLM **之前** (`inlet` 阶段) 对其进行拦截和动态修改。
|
||||
|
||||
该插件的核心功能包括:
|
||||
1. 将用户的环境变量(如姓名、当前时间)自动注入到对话的起始位置。
|
||||
2. 根据当前使用的模型和用户信息,智能地开启、关闭或重定向“网络搜索”功能。
|
||||
3. 为特定模型补充必要的 API 参数(如 `chat_id`)。
|
||||
|
||||
通过解析这个插件,开发者可以掌握如何构建一个能够感知上下文(用户、模型、环境变量)并据此动态调整请求内容的智能过滤器。
|
||||
|
||||
---
|
||||
|
||||
## 核心工作流 (`inlet` 方法)
|
||||
|
||||
该插件的所有逻辑都集中在 `inlet` 方法中,其工作流程可以分解为:
|
||||
|
||||
1. **注入上下文**: 从 `__metadata__` 参数中获取用户环境变量,并将其作为一个格式化的 Markdown 块,智能地插入到第一条用户消息的开头。
|
||||
2. **控制功能**: 分析当前请求的模型名称 (`body['model']`) 和用户信息 (`__user__`),应用一系列规则来决定如何处理“网络搜索”功能。
|
||||
3. **补充参数**: 根据模型信息 (`__model__`),为特定的模型(如 `cfchatqwen`)在请求体 `body` 中补充其所需的 `chat_id` 等参数。
|
||||
|
||||
---
|
||||
|
||||
## 关键开发模式与技术剖析
|
||||
|
||||
### 1. 利用 `__metadata__` 和 `__model__` 获取丰富上下文
|
||||
|
||||
`Filter` 插件的 `inlet` 方法可以接收 `__metadata__` 和 `__model__` 这两个非常有用的参数,它们是插件感知上下文、实现智能化逻辑的关键。
|
||||
|
||||
- **`__metadata__["variables"]` (环境变量)**:
|
||||
- **功能**: 这是一个由 Open WebUI 自动填充的、包含当前请求上下文信息的字典。
|
||||
- **内容**: 它预置了一系列模板变量,如:
|
||||
- `{{USER_NAME}}`: 当前用户名
|
||||
- `{{CURRENT_DATETIME}}`: 当前日期时间
|
||||
- `{{CURRENT_WEEKDAY}}`: 当前星期
|
||||
- `{{CURRENT_TIMEZONE}}`: 当前时区
|
||||
- `{{USER_LANGUAGE}}`: 用户的语言设置
|
||||
- **价值**: 这是在插件中获取用户和环境信息的**标准方式**,无需手动计算。`Inject Env` 插件正是利用这个字典来构建注入到消息中的 Markdown 文本。
|
||||
|
||||
- **`__model__` (模型信息)**:
|
||||
- **功能**: 这是一个包含了当前交易所用模型详细信息的字典。
|
||||
- **内容**: 开发者可以从中获取模型的 `id`、`info.base_model_id`(对于自定义模型,指向其基础模型)等。
|
||||
- **价值**: 允许插件根据不同的模型或模型家族(例如,检查 `base_model_id` 是否以 `qwen` 开头)来执行不同的逻辑分支。
|
||||
|
||||
**代码示例:**
|
||||
```python
|
||||
def inlet(
|
||||
self,
|
||||
body: dict,
|
||||
__metadata__: Optional[dict] = None,
|
||||
__model__: Optional[dict] = None,
|
||||
) -> dict:
|
||||
# 从 __metadata__ 获取环境变量
|
||||
variables = __metadata__.get("variables", {})
|
||||
if variables:
|
||||
variable_markdown = f"- **用户姓名**:{variables.get('{{USER_NAME}}', '')}\n"
|
||||
# ... 注入到消息中 ...
|
||||
|
||||
# 从 __model__ 获取模型基础 ID
|
||||
if "openai" in __model__:
|
||||
base_model_id = __model__["openai"]["id"]
|
||||
else:
|
||||
base_model_id = __model__["info"]["base_model_id"]
|
||||
|
||||
if base_model_id.startswith("cfchatqwen"):
|
||||
# ... 执行针对 qwen 模型的特定逻辑 ...
|
||||
```
|
||||
|
||||
### 2. 健壮的消息内容注入
|
||||
|
||||
向用户的消息中动态添加内容时,必须考虑多种情况以确保插件的健壮性。`insert_user_env_info` 函数为此提供了完美的示范。
|
||||
|
||||
- **幂等性注入 (Idempotent Injection)**:
|
||||
- **问题**: 如果每次都简单地在消息前添加内容,当用户连续对话时,环境变量块会被重复注入,造成内容冗余。
|
||||
- **解决方案**: 在注入前,先用正则表达式 `re.search()` 检查消息中是否**已存在**环境变量块。
|
||||
- 如果**存在**,则使用 `re.sub()` 将其**替换**为最新的内容。
|
||||
- 如果**不存在**,才在消息开头**添加**新内容。
|
||||
- **价值**: 保证了无论 `inlet` 被调用多少次,环境变量信息在消息中只会出现一次,并且始终保持最新。
|
||||
|
||||
- **兼容多模态消息**:
|
||||
- **问题**: 用户的消息 `content` 可能是纯文本字符串,也可能是一个包含文本和图片的列表(`[{'type':'text', ...}, {'type':'image_url', ...}]`)。简单地进行字符串拼接会破坏多模态结构。
|
||||
- **解决方案**:
|
||||
1. 使用 `isinstance(content, list)` 检查内容是否为列表。
|
||||
2. 如果是列表,则遍历它,找到 `type` 为 `text` 的那部分。
|
||||
3. 对文本部分执行上述的“幂等性注入”逻辑。
|
||||
4. 如果列表中**没有**文本部分(例如,用户只发了一张图片),则**主动插入**一个新的文本部分 `{'type': 'text', 'text': ...}` 到列表的开头。
|
||||
|
||||
**启示**: 对消息体的任何修改都必须考虑其数据结构(`str` 或 `list`),并进行相应的处理,以确保插件的广泛兼容性。
|
||||
|
||||
### 3. 基于模型的动态路由与功能切换
|
||||
|
||||
`change_web_search` 函数是“拦截与翻译”模式的又一个精彩应用,并且引入了更高级的“模型重定向”技巧。
|
||||
|
||||
- **模式一:参数翻译 (适用于通义千问)**
|
||||
- **场景**: `qwen-max` 模型可能不认识 Open WebUI 的标准 `web_search` 开关,而是需要一个名为 `enable_search` 的参数。
|
||||
- **实现**:
|
||||
1. 拦截:`features["web_search"] = False`
|
||||
2. 翻译:`body.setdefault("enable_search", True)`
|
||||
- **效果**: 对用户透明地将会话切换到了模型的原生搜索模式。
|
||||
|
||||
- **模式二:模型重定向 (适用于 Deepseek/Gemini 等)**
|
||||
- **场景**: 某个模型系列(如 `deepseek`)本身不支持搜索,但其提供商部署了一个带搜索功能的版本,其模型名称可能是 `deepseek-chat-search`。
|
||||
- **实现**:
|
||||
1. 检查当前模型是否为 `cfdeepseek-deepseek` 且**不**以 `-search` 结尾。
|
||||
2. 如果是,则**直接修改请求体中的模型名称**: `body["model"] = body["model"] + "-search"`。
|
||||
3. 最后,禁用标准的 `web_search` 开关:`features["web_search"] = False`。
|
||||
- **效果**: 这种方式巧妙地将用户的请求“重定向”到了一个功能更强的模型版本,而用户在前端选择的仍然是普通模型。这为插件开发者提供了极大的灵活性,可以创建功能增强的“虚拟模型”。
|
||||
|
||||
### 4. 用户特定逻辑
|
||||
|
||||
插件还可以根据用户信息执行特定逻辑,这对于 A/B 测试、灰度发布或为特定用户提供定制功能非常有用。
|
||||
|
||||
**代码示例:**
|
||||
```python
|
||||
# 从 __user__ 参数中获取用户邮箱
|
||||
user_email = __user__.get("email")
|
||||
|
||||
# 为特定用户禁用网络搜索
|
||||
if user_email == "yi204o@qq.com":
|
||||
features["web_search"] = False
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
`Inject Env` 插件虽然代码量不大,但它像一把精准的手术刀,展示了 `Filter` 插件在请求预处理阶段的强大能力。通过学习它,我们可以掌握:
|
||||
|
||||
- **利用上下文**: 如何充分利用 `__metadata__` 和 `__model__` 参数,让插件变得“智能”和“情境感知”。
|
||||
- **稳健地修改内容**: 如何在不破坏多模态结构和保证幂等性的前提下,向用户消息中注入信息。
|
||||
- **高级功能控制**: 如何通过“参数翻译”和“模型重定向”等高级技巧,实现对模型功能(如网络搜索)的精细化控制。
|
||||
- **构建模板**: 这个插件是任何需要在请求发送前注入动态信息(如 Prompt Engineering、上下文增强、参数调整)的过滤器的绝佳起点。
|
||||
|
||||
```
|
||||
1838
docs/examples/gemini_manifold_plugin_examples.md
Normal file
1838
docs/examples/gemini_manifold_plugin_examples.md
Normal file
File diff suppressed because it is too large
Load Diff
185
docs/examples/pipe_plugin_gemini_manifold_example_cn.md
Normal file
185
docs/examples/pipe_plugin_gemini_manifold_example_cn.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# `Gemini Manifold` 插件深度解析:高级 `Pipe` 插件开发指南
|
||||
|
||||
## 引言
|
||||
|
||||
`Gemini Manifold` (`gemini_manifold.py`) 不仅仅是一个连接到 Google AI 服务的 `Pipe` 插件,它更是一个集成了高级架构设计、复杂功能和最佳实践的“瑞士军刀”。它作为 Open WebUI 与 Google Gemini 及 Vertex AI 之间的桥梁,全面展示了如何构建一个生产级的、功能丰富的、高性能且用户体验良好的 `Pipe` 插件。
|
||||
|
||||
本文档是对该插件的**深度解析**,旨在帮助开发者通过剖析一个顶级的范例,掌握 Open WebUI 高级插件的开发思想与核心技术。
|
||||
|
||||
## Part 1: 复杂配置管理艺术 (`Valves` 系统)
|
||||
|
||||
在复杂的应用场景中,配置管理需要同时兼顾安全性、灵活性和多用户隔离。`Gemini Manifold` 通过一个精巧的双层 `Valves` 系统完美地解决了这个问题。
|
||||
|
||||
**目标**: 解决多用户、多环境下的配置灵活性与安全性问题。
|
||||
|
||||
#### 1.1 双层结构:`Valves` 与 `UserValves`
|
||||
|
||||
- **`Pipe.Valves` (管理员层)**: 定义了插件的全局默认配置,由管理员在 Open WebUI 的设置界面中配置。这些是插件运行的基础。
|
||||
|
||||
```python
|
||||
class Pipe:
|
||||
class Valves(BaseModel):
|
||||
GEMINI_API_KEY: str | None = Field(default=None)
|
||||
USE_VERTEX_AI: bool = Field(default=False)
|
||||
USER_MUST_PROVIDE_AUTH_CONFIG: bool = Field(default=False)
|
||||
AUTH_WHITELIST: str | None = Field(default=None)
|
||||
# ... 40+ 其他全局配置
|
||||
```
|
||||
|
||||
- **`Pipe.UserValves` (用户层)**: 允许每个用户在每次请求时,通过请求体(`body`)传入自己的配置,用于临时覆盖管理员的默认设置。
|
||||
|
||||
```python
|
||||
class Pipe:
|
||||
class UserValves(BaseModel):
|
||||
GEMINI_API_KEY: str | None = Field(default=None)
|
||||
USE_VERTEX_AI: bool | None | Literal[""] = Field(default=None)
|
||||
# ... 其他用户可覆盖的配置
|
||||
```
|
||||
|
||||
#### 1.2 核心合并逻辑 `_get_merged_valves`
|
||||
|
||||
该函数在每次请求时被调用,负责将 `UserValves` 合并到 `Valves` 中,生成最终生效的配置。
|
||||
|
||||
#### 1.3 关键模式:强制认证与白名单
|
||||
|
||||
这是该配置系统中最精妙的部分,专为需要进行成本分摊和安全管控的团队环境设计。
|
||||
|
||||
- **场景**: 公司希望员工使用自己的 API Key,而不是共用一个高额度的 Key。
|
||||
- **实现**:
|
||||
1. 管理员在 `Valves` 中设置 `USER_MUST_PROVIDE_AUTH_CONFIG: True`。
|
||||
2. 同时,可以将少数特权用户(如测试人员)的邮箱加入 `AUTH_WHITELIST`。
|
||||
3. 在合并配置时,插件会检查当前用户是否在白名单内。
|
||||
- **非白名单用户**: **强制**使用其在 `UserValves` 中提供的 `GEMINI_API_KEY`,并**禁用**管理员配置的 `USE_VERTEX_AI`。如果用户没提供 Key,请求会失败。
|
||||
- **白名单用户**: 不受此限制,可以正常使用管理员配置的默认值。
|
||||
|
||||
这种设计通过代码强制执行了组织的策略,比单纯的文档约定要可靠得多。
|
||||
|
||||
## Part 2: 高性能文件上传与缓存 (`FilesAPIManager`)
|
||||
|
||||
`FilesAPIManager` 是该插件的性能核心,它通过一套复杂但高效的机制,解决了文件上传中的重复、并发和性能三大难题。
|
||||
|
||||
**目标**: 避免重复上传,减少API调用,并在高并发下保持稳定。
|
||||
|
||||
#### 2.1 核心概念:内容寻址 (Content-Addressable Storage)
|
||||
|
||||
- **原理**: 文件的唯一标识符**不是文件名**,而是其**文件内容的哈希值**。插件使用 `xxhash`(一种速度极快的非加密哈希算法)来计算文件哈希。
|
||||
- **优势**: 无论一个文件被上传多少次,只要内容不变,其哈希值就永远相同。这意味着插件只需为每个独一无二的文件内容执行一次上传操作。
|
||||
|
||||
#### 2.2 实现:三级缓存路径 (Hot/Warm/Cold Path)
|
||||
|
||||
`FilesAPIManager` 的 `get_or_upload_file` 方法实现了精妙的三级缓存策略:
|
||||
|
||||
1. **Hot Path (内存缓存)**:
|
||||
- **实现**: 使用 `aiocache` 将“文件哈希 -> `types.File` 对象”的映射关系缓存在内存中。`types.File` 对象包含了 Google API 返回的文件 URI 和过期时间。
|
||||
- **流程**: 收到文件后,先查内存缓存。如果命中,直接返回 `types.File` 对象,无任何网络 I/O,速度最快。
|
||||
|
||||
2. **Warm Path (无状态恢复)**:
|
||||
- **场景**: 内存缓存未命中(例如服务重启,内存被清空)。
|
||||
- **实现**: 插件根据文件哈希构造一个**确定性的文件名**(`deterministic_name = f"files/owui-v1-{content_hash}"`),然后直接调用 `client.aio.files.get()` 尝试从 Google API 获取该文件。
|
||||
- **优势**: 如果文件之前被上传过,这次 `get` 调用就会成功,并返回文件的状态信息。这样**仅用一次轻量的 `GET` 请求就恢复了文件状态,避免了昂贵的重新上传**。
|
||||
|
||||
3. **Cold Path (文件上传)**:
|
||||
- **场景**: Hot 和 Warm 路径全部失败,说明这确实是一个新文件(或者在 Google 服务器上已过期)。
|
||||
- **实现**: 执行完整的文件上传流程,并将成功后的 `types.File` 对象存入内存缓存(Hot Path),以备后续使用。
|
||||
|
||||
#### 2.3 关键模式:并发上传安全
|
||||
|
||||
- **问题**: 如果 10 个用户同时上传同一个大文件,会发生什么?
|
||||
- **解决方案**: 使用 `asyncio.Lock` 结合 "双重检查锁定" (Double-Checked Locking) 模式。
|
||||
1. 为每一个**文件哈希**维护一个独立的 `asyncio.Lock`。
|
||||
2. 当一个任务进入 `get_or_upload_file` 时,它会先尝试获取该文件哈希对应的锁。
|
||||
3. **第一个任务**会成功获取锁,并继续执行 Warm/Cold Path 逻辑。
|
||||
4. **后续 9个任务**会被阻塞在 `async with lock:` 处,异步等待。
|
||||
5. 第一个任务完成后,它会将结果写入缓存并释放锁。
|
||||
6. 后续 9 个任务依次获取到锁,但它们在获取锁之后会**再次检查缓存**。此时,它们会发现缓存中已有数据,于是直接从缓存返回,不再执行任何网络操作。
|
||||
|
||||
这个模式优雅地解决了并发上传的资源浪费和竞态问题。
|
||||
|
||||
## Part 3: 异步并发与流程编排
|
||||
|
||||
为了在处理复杂请求(例如,包含多个文件的消息)时保持前端的流畅响应,插件大量使用了 `asyncio` 的高级特性。
|
||||
|
||||
**目标**: 最大化 I/O 效率,缩短用户的等待时间。
|
||||
|
||||
#### 3.1 `asyncio.gather`:并发处理所有消息
|
||||
|
||||
`GeminiContentBuilder.build_contents` 方法是并发处理的典范。它没有按顺序循环处理每条消息,而是:
|
||||
1. 为对话历史中的**每一条消息**创建一个 `_process_message_turn` 协程任务。
|
||||
2. 将所有任务放入一个列表。
|
||||
3. 使用 `await asyncio.gather(*tasks)` **同时启动并等待所有任务完成**。
|
||||
|
||||
这意味着,如果一条消息包含 5 个待上传的文件,另一条包含 3 个,这 8 个文件的上传和处理是**并行进行**的,总耗时取决于最慢的那个文件,而不是所有文件耗时的总和。
|
||||
|
||||
#### 3.2 `asyncio.Queue`:解耦的进度汇报
|
||||
|
||||
`UploadStatusManager` 展示了如何通过生产者-消费者模型实现优雅的进度汇报。
|
||||
|
||||
- **生产者 (上传任务)**:
|
||||
- 当一个 `_process_message_turn` 任务确定需要上传文件时,它会向一个共享的 `asyncio.Queue` 中 `put` 一个 `('REGISTER_UPLOAD',)` 元组。
|
||||
- 上传完成后,它会 `put` 一个 `('COMPLETE_UPLOAD',)` 元组。
|
||||
|
||||
- **消费者 (`UploadStatusManager`)**:
|
||||
- 它在一个独立的后台任务 (`asyncio.create_task`) 中运行,循环地从队列中 `get` 消息。
|
||||
- 每当收到 `REGISTER_UPLOAD`,它就将预期总数加一。
|
||||
- 每当收到 `COMPLETE_UPLOAD`,它就将完成数加一。
|
||||
- 每次计数变化后,它会重新计算进度(例如,“正在上传 3/8…”),并通过 `EventEmitter` 发送给前端。
|
||||
|
||||
这种设计将“执行业务逻辑”(上传)和“汇报进度”两个职责完全解耦。上传任务只管“生产”状态事件,进度管理器只管“消费”事件并更新 UI,代码非常清晰。
|
||||
|
||||
## Part 4: 响应处理与前端兼容性
|
||||
|
||||
**目标**: 提供流畅、信息丰富且绝对不会“搞乱”前端页面的用户体验。
|
||||
|
||||
#### 4.1 统一响应处理器 `_unified_response_processor`
|
||||
|
||||
- **问题**: Google API 同时支持流式(streaming)和非流式(non-streaming)两种响应模式,如果为两种模式都写一套处理逻辑,代码会很冗余。
|
||||
- **解决方案**: `pipe` 方法的核心返回部分,无论是哪种模式,最终都会调用 `_unified_response_processor`。
|
||||
- 对于**流式**响应,直接将 API 返回的异步生成器传入。
|
||||
- 对于**非流式**响应,它会先将单个响应对象包装成一个只含一项的简单异步生成器。
|
||||
- **效果**: `_unified_response_processor` 内部只需用一套 `async for` 循环逻辑即可处理所有情况,极大地简化了代码。
|
||||
|
||||
#### 4.2 后置元数据处理 `_do_post_processing`
|
||||
|
||||
- **问题**: 像 Token 使用量 (`usage`)、搜索引用来源 (`sources`) 等信息,只有在整个响应完全生成后才能获得。如果和内容混在一起发送,会影响流式输出的体验。
|
||||
- **解决方案**: `_unified_response_processor` 在主内容流(`choices`)完全结束后,会进入后置处理阶段。它会调用 `_do_post_processing` 来提取这些元数据,并通过 `EventEmitter` 的 `emit_completion` 或 `emit_usage` 方法,作为**独立的、附加的事件**发送给前端。
|
||||
|
||||
#### 4.3 前端兼容性技巧 `_disable_special_tags`
|
||||
|
||||
- **问题**: LLM 很可能在思考过程中生成 `<think>...</think>` 或 `<details>...</details>` 这样的 XML/HTML 风格标签。如果这些文本原样发送到前端,浏览器会尝试将其解析为 HTML 元素,导致页面布局错乱或内容丢失。
|
||||
- **解决方案**: 一个极其巧妙的技巧——在这些特殊标签的开头注入一个**零宽度空格(Zero-Width Space, ZWS, `\u200b`)**。
|
||||
- 例如,将 `<think>` 替换为 `<think>` (后者尖括号后多一个 ZWS)。
|
||||
- 这个改动对人类用户完全不可见,但对于浏览器的 HTML 解析器来说,`<think>` 不再是一个合法的标签名,因此它会被当作纯文本处理,从而保证了前端渲染的绝对安全。
|
||||
- 当需要将这段历史作为上下文发回给模型时,再通过 `_enable_special_tags` 将这些 ZWS 移除,恢复原始文本。
|
||||
|
||||
## Part 5: 与 Open WebUI 和 Google API 的深度集成
|
||||
|
||||
`Gemini Manifold` 充分利用了 Open WebUI 的框架特性和 Google API 的高级功能。
|
||||
|
||||
#### 5.1 `pipes` 方法与模型缓存
|
||||
|
||||
- `pipes()` 方法负责向 Open WebUI 注册所有可用的 Gemini 模型。
|
||||
- 它使用了 `@cached` 装饰器,这意味着对 Google API 的 `list_models` 调用结果会被缓存。只要插件配置(如 API Key, 白名单等)不变,后续的 `pipes` 调用会直接从缓存返回,避免了不必要的网络请求。
|
||||
|
||||
#### 5.2 多源内容处理 (`_genai_parts_from_text`)
|
||||
|
||||
`GeminiContentBuilder` 的核心能力之一是从一段文本中智能地解析出多种类型的内容。
|
||||
- 它使用正则表达式一次性地从用户输入中匹配出 Markdown 图片链接 (`![]()`) 和 YouTube 视频链接。
|
||||
- 对于匹配到的每一种 URI,它都会分派给统一的 `_genai_part_from_uri` 方法处理。
|
||||
- `_genai_part_from_uri` 内部进一步区分 URI 类型(是本地文件、data URI 还是 YouTube 链接),并调用相应的处理器(例如,从数据库读取文件、解码 base64、或解析 YouTube URL 参数)。
|
||||
|
||||
#### 5.3 与 Open WebUI 数据库交互
|
||||
|
||||
为了处理用户上传的文件,插件需要访问 Open WebUI 的内部数据库。
|
||||
- 它通过 `from open_webui.models.files import Files` 导入 `Files` 模型。
|
||||
- 在 `_get_file_data` 方法中,它调用 `Files.get_file_by_id(file_id)` 来获取文件的元数据(如存储路径、MIME 类型)。
|
||||
- **关键点**: 由于数据库 API 是同步阻塞的,插件明智地使用了 `await asyncio.to_thread(Files.get_file_by_id, file_id)`,将同步调用放入一个独立的线程中执行,从而避免了对主异步事件循环的阻塞。
|
||||
|
||||
## 总结
|
||||
|
||||
`Gemini Manifold` 是一个教科书级别的 Open WebUI `Pipe` 插件。它展示了超越简单 API 调用的高级插件应该具备的特质:
|
||||
- **架构思维**: 通过职责分离的类和清晰的流程编排来管理复杂性。
|
||||
- **性能意识**: 在所有 I/O 密集型操作中,都将性能优化(缓存、并发)放在首位。
|
||||
- **用户为本**: 通过丰富的、非阻塞的实时反馈,极大地提升了用户体验。
|
||||
- **健壮与安全**: 通过精巧的技巧和周密的错误处理,确保插件在各种异常情况下都能稳定运行。
|
||||
|
||||
对于任何希望超越基础,构建企业级、高性能 Open WebUI 插件的开发者而言,`Gemini Manifold` 的每一行代码都值得细细品味。
|
||||
Reference in New Issue
Block a user