# OpenWebUI 插件开发权威指南

> 本指南整合了官方文档、SDK 详解及最佳实践，旨在为开发者提供一份从入门到精通的系统化教程。

## 📚 目录

1.  [插件开发快速入门](#1-插件开发快速入门)
2.  [核心概念与 SDK 详解](#2-核心概念与-sdk-详解)
3.  [插件类型深度解析](#3-插件类型深度解析)
    *   [Action (动作)](#31-action-动作)
    *   [Filter (过滤器)](#32-filter-过滤器)
    *   [Pipe (管道)](#33-pipe-管道)
4.  [高级开发模式](#4-高级开发模式)
5.  [最佳实践与设计原则](#5-最佳实践与设计原则)
6.  [故障排查](#6-故障排查)

---

## 1. 插件开发快速入门

### 1.1 什么是 OpenWebUI 插件？

OpenWebUI 插件（官方称为 "Functions"）是扩展平台功能的主要方式。它们运行在后端 Python 环境中，允许你：
*   🔌 **集成新模型**：通过 Pipe 接入 Claude、Gemini 或自定义 RAG。
*   🎨 **增强交互**：通过 Action 在消息旁添加按钮（如"导出"、"生成图表"）。
*   🔧 **干预流程**：通过 Filter 在请求前后修改数据（如注入上下文、敏感词过滤）。

### 1.2 你的第一个插件 (Hello World)

保存以下代码为 `hello.py` 并上传到 OpenWebUI 的 **Functions** 面板：

```python
"""
title: Hello World Action
author: Demo
version: 1.0.0
"""

from pydantic import BaseModel, Field
from typing import Optional

class Action:
    class Valves(BaseModel):
        greeting: str = Field(default="你好", description="问候语")

    def __init__(self):
        self.valves = self.Valves()

    async def action(
        self,
        body: dict,
        __event_emitter__=None,
        __user__=None
    ) -> Optional[dict]:
        user_name = __user__.get("name", "朋友") if __user__ else "朋友"
        
        if __event_emitter__:
            await __event_emitter__({
                "type": "notification",
                "data": {"type": "success", "content": f"{self.valves.greeting}, {user_name}!"}
            })
        return body
```

---

## 2. 核心概念与 SDK 详解

### 2.1 ⚠️ 重要：同步与异步

OpenWebUI 插件运行在 `asyncio` 事件循环中。
*   **原则**：所有 I/O 操作（数据库、文件、网络）必须非阻塞。
*   **陷阱**：直接调用同步方法（如 `time.sleep`, `requests.get`）会卡死整个服务器。
*   **解决**：使用 `await asyncio.to_thread(sync_func, ...)` 包装同步调用。

### 2.2 核心参数详解

所有插件方法（`inlet`, `outlet`, `pipe`, `action`）都支持注入以下特殊参数：

| 参数名 | 类型 | 说明 |
| :--- | :--- | :--- |
| `body` | `dict` | **核心数据**。包含 `messages`, `model`, `stream` 等请求信息。 |
| `__user__` | `dict` | **当前用户**。包含 `id`, `name`, `role`, `valves` (用户配置) 等。 |
| `__metadata__` | `dict` | **元数据**。包含 `chat_id`, `message_id`。其中 `variables` 字段包含 `{{USER_NAME}}`, `{{CURRENT_TIME}}` 等预置变量。 |
| `__request__` | `Request` | **FastAPI 请求对象**。可访问 `app.state` 进行跨插件通信。 |
| `__event_emitter__` | `func` | **单向通知**。用于发送 Toast 通知或状态条更新。 |
| `__event_call__` | `func` | **双向交互**。用于在前端执行 JS 代码、弹出确认框或输入框。 |

### 2.3 配置系统 (Valves)

*   **`Valves`**: 管理员全局配置。
*   **`UserValves`**: 用户级配置（优先级更高，可覆盖全局）。

```python
class Filter:
    class Valves(BaseModel):
        API_KEY: str = Field(default="", description="全局 API Key")
        
    class UserValves(BaseModel):
        API_KEY: str = Field(default="", description="用户私有 API Key")
        
    def inlet(self, body, __user__):
        # 优先使用用户的 Key
        user_valves = __user__.get("valves", self.UserValves())
        api_key = user_valves.API_KEY or self.valves.API_KEY
```

---

## 3. 插件类型深度解析

### 3.1 Action (动作)

**定位**：在消息下方添加按钮，用户点击触发。

**高级用法：前端执行 JavaScript (文件下载示例)**

```python
import base64

async def action(self, body, __event_call__):
    # 1. 后端生成内容
    content = "Hello OpenWebUI".encode()
    b64 = base64.b64encode(content).decode()
    
    # 2. 发送 JS 到前端执行
    js = f"""
    const blob = new Blob([atob('{b64}')], {{type: 'text/plain'}});
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = 'hello.txt';
    a.click();
    """
    await __event_call__({"type": "execute", "data": {"code": js}})
```

### 3.2 Filter (过滤器)

**定位**：中间件，拦截并修改请求/响应。

*   **`inlet`**: 请求前。用于注入上下文、修改模型参数。
*   **`outlet`**: 响应后。用于格式化输出、保存日志。
*   **`stream`**: 流式处理中。用于实时敏感词过滤。

**示例：注入环境变量**

```python
async def inlet(self, body, __metadata__):
    vars = __metadata__.get("variables", {})
    context = f"当前时间: {vars.get('{{CURRENT_DATETIME}}')}"
    
    # 注入到 System Prompt 或第一条消息
    if body.get("messages"):
        body["messages"][0]["content"] += f"\n\n{context}"
    return body
```

### 3.3 Pipe (管道)

**定位**：自定义模型/代理。

**示例：简单的 OpenAI 代理**

```python
import requests

class Pipe:
    def pipes(self):
        return [{"id": "my-gpt", "name": "My GPT Wrapper"}]

    def pipe(self, body):
        # 可以在这里修改 body，例如强制添加 prompt
        headers = {"Authorization": f"Bearer {self.valves.API_KEY}"}
        r = requests.post("https://api.openai.com/v1/chat/completions", json=body, headers=headers, stream=True)
        return r.iter_lines()
```

---

## 4. 高级开发模式

### 4.1 Pipe 与 Filter 协同
利用 `__request__.app.state` 在不同插件间共享数据。
*   **Pipe**: `__request__.app.state.search_results = [...]`
*   **Filter (Outlet)**: 读取 `search_results` 并将其格式化为引用链接附加到回复末尾。

### 4.2 异步后台任务
不阻塞用户响应，在后台执行耗时操作（如生成总结、存库）。

```python
import asyncio

async def outlet(self, body, __metadata__):
    asyncio.create_task(self.background_job(__metadata__["chat_id"]))
    return body

async def background_job(self, chat_id):
    # 执行耗时操作...
    pass
```

### 4.3 JS 渲染并嵌入 Markdown (Data URL 嵌入)

对于需要复杂前端渲染（如 AntV 图表、Mermaid 图表）但希望结果**持久化为纯 Markdown 格式**的场景，推荐使用 Data URL 嵌入模式：

#### 工作流程

```
┌──────────────────────────────────────────────────────────────┐
│  1. Python Action                                             │
│     ├── 分析消息内容                                           │
│     ├── 调用 LLM 生成结构化数据（可选）                         │
│     └── 通过 __event_call__ 发送 JS 代码到前端                 │
├──────────────────────────────────────────────────────────────┤
│  2. Browser JS (通过 __event_call__)                          │
│     ├── 动态加载可视化库                                       │
│     ├── 离屏渲染 SVG/Canvas                                    │
│     ├── 使用 toDataURL() 导出 Base64 Data URL                  │
│     └── 通过 REST API 更新消息内容                             │
├──────────────────────────────────────────────────────────────┤
│  3. Markdown 渲染                                             │
│     └── 显示 ![描述](data:image/svg+xml;base64,...)           │
└──────────────────────────────────────────────────────────────┘
```

#### Python 端（发送 JS 执行）

```python
async def action(self, body, __event_call__, __metadata__, ...):
    chat_id = self._extract_chat_id(body, __metadata__)
    message_id = self._extract_message_id(body, __metadata__)
    
    # 生成 JS 代码
    js_code = self._generate_js_code(
        chat_id=chat_id,
        message_id=message_id,
        data=processed_data,
    )
    
    # 执行 JS
    if __event_call__:
        await __event_call__({
            "type": "execute",
            "data": {"code": js_code}
        })
```

#### JavaScript 端（渲染并回写）

```javascript
(async function() {
    // 1. 加载可视化库
    if (typeof VisualizationLib === 'undefined') {
        await new Promise((resolve, reject) => {
            const script = document.createElement('script');
            script.src = 'https://cdn.example.com/lib.min.js';
            script.onload = resolve;
            script.onerror = reject;
            document.head.appendChild(script);
        });
    }
    
    // 2. 创建离屏容器
    const container = document.createElement('div');
    container.style.cssText = 'position:absolute;left:-9999px;';
    document.body.appendChild(container);
    
    // 3. 渲染可视化
    const instance = new VisualizationLib({ container });
    instance.render(data);
    
    // 4. 导出为 Data URL
    const dataUrl = await instance.toDataURL({ type: 'svg', embedResources: true });
    
    // 5. 清理
    instance.destroy();
    document.body.removeChild(container);
    
    // 6. 生成 Markdown 图片
    const markdownImage = `![图表](${dataUrl})`;
    
    // 7. 通过 API 更新消息
    const token = localStorage.getItem("token");
    await fetch(`/api/v1/chats/${chatId}/messages/${messageId}/event`, {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "Authorization": `Bearer ${token}`
        },
        body: JSON.stringify({
            type: "chat:message",
            data: { content: originalContent + "\n\n" + markdownImage }
        })
    });
})();
```

#### 优势

- **纯 Markdown 输出**：结果是标准的 Markdown 图片语法，无需 HTML 代码块
- **自包含**：图片以 Base64 Data URL 嵌入，无外部依赖
- **持久化**：通过 API 回写，消息重新加载后图片仍然存在
- **跨平台**：任何支持 Markdown 图片的客户端都能显示

#### HTML 注入 vs JS 渲染嵌入 Markdown

| 特性 | HTML 注入 | JS 渲染 + Markdown 图片 |
|------|----------|------------------------|
| 输出格式 | HTML 代码块 | Markdown 图片 |
| 交互性 | ✅ 支持按钮、动画 | ❌ 静态图片 |
| 外部依赖 | 需要加载 JS 库 | 无（图片自包含） |
| 持久化 | 依赖浏览器渲染 | ✅ 永久可见 |
| 文件导出 | 需特殊处理 | ✅ 直接导出 |
| 适用场景 | 交互式内容 | 信息图、图表快照 |

#### 参考实现

- `plugins/actions/js-render-poc/infographic_markdown.py` - AntV 信息图 + Data URL
- `plugins/actions/js-render-poc/js_render_poc.py` - 基础概念验证

## 5. 最佳实践与设计原则

### 5.1 命名与定位
*   **简短有力**：如 "闪记卡", "精读"。避免 "文本分析助手" 这种泛词。
*   **功能互补**：不要重复造轮子，明确你的插件解决了什么特定问题。

### 5.2 用户体验 (UX)
*   **反馈及时**：耗时操作前先发送 `notification` ("正在生成...")。
*   **视觉美观**：Action 输出 HTML 时，使用现代化的 CSS（圆角、阴影、渐变）。
*   **智能引导**：检测到文本过短时，提示用户"建议输入更多内容以获得更好结果"。

### 5.3 错误处理
永远不要让插件静默失败。捕获异常并通过 `__event_emitter__` 告知用户。

```python
try:
    # 业务逻辑
except Exception as e:
    await __event_emitter__({
        "type": "notification",
        "data": {"type": "error", "content": f"处理失败: {str(e)}"}
    })
```

---

## 6. 故障排查

*   **HTML 不显示？** 确保包裹在 ` ```html ... ``` ` 代码块中。
*   **数据库报错？** 检查是否在 `async` 函数中直接调用了同步的 DB 方法，请使用 `asyncio.to_thread`。
*   **参数未生效？** 检查 `Valves` 定义是否正确，以及是否被 `UserValves` 覆盖。
