跳到主要内容

🔔 事件 (Events):在 Open WebUI 中使用 __event_emitter____event_call__

Open WebUI 的插件架构绝非仅用于处理简单的输入并生成静态输出 —— 更核心的在于与前端 UI 界面和用户之间进行实时的交互式通信。为了让您的工具 (Tools)、函数 (Functions) 以及管道 (Pipes) 更加灵动且具备生命力,Open WebUI 在底层通过 __event_emitter____event_call__ 助手函数,为您提供了一个功能强大的内置事件系统。

本篇指南将为您详尽剖析什么是事件如何从您的 Python 代码中触发它们,以及您可以调用的完整事件类型全览(绝不仅仅局限于 "input" 一种)。


🌊 什么是事件?

事件 (Events) 是指您的后端代码(工具 Tool 或函数 Function)向 Web 前端 UI 实时投递的通知或交互式请求。它们允许您动态更新聊天对话内容、显示弹窗提示、请求用户二次确认、在前端执行 JavaScript 代码等。

  • 事件可以通过 __event_emitter__ 助手函数进行单向投递(用于单向状态流更新),或通过 __event_call__ 助手函数发起双向通信(当您需要用户输入值或点击确认时,如确认框、密码输入等)。

生动的比喻: 可以把事件理解为您的插件在聊天界面上自主拉起的“系统推送通知”或“模态对话框”,从而将呆板的对话文本升级为极具交互感和现代感的富 UI 体验。


🏁 可用范围

原生 Python 工具与函数

事件机制对于直接在 Open WebUI 界面中编写的原生 Python 工具和函数完全可用的,您可以直接在入参中声明并调用 __event_emitter____event_call__

外部工具 (OpenAPI & MCP)

部署在外部的工具服务同样可以通过专用的 REST 接口向平台投递事件。当 Open WebUI 配置了 ENABLE_FORWARD_USER_INFO_HEADERS=True 环境变量时,会在向外部工具发送的每一次请求中自动附加以下 HTTP 头部信息:

请求头 (Header)描述
X-Open-WebUI-Chat-Id触发此外部工具的聊天对话 ID
X-Open-WebUI-Message-Id与本次工具调用相关联的消息 ID

您的外部工具服务可以捕获这些请求头,并通过以下 API 接口将事件推送回前端 UI:

POST /api/v1/chats/{chat_id}/messages/{message_id}/event

详细的使用说明请参考下文的外部工具事件章节。


🧰 基础用法

发送一个事件

您可以在工具 Tool 或函数 Function 逻辑的任意位置,通过调用以下代码单向发送一个状态更新事件:

await __event_emitter__(
    {
        "type": "status",  # 请参阅下文支持的事件类型列表
        "data": {
            "description": "已开始处理数据!",
            "done": False,
            "hidden": False,
        },
    }
)

无需在 Payload 中手动指定类似 chat_idmessage_id 的路由参数 —— Open WebUI 在底层会自动为您打上这些标记。

交互式事件

当您的业务逻辑需要中途暂停,直到用户在前端操作完毕再继续执行时(例如要求用户输入敏感密码、点击 OK/Cancel 确认框、或者在客户端执行前端逻辑),请使用 __event_call__ 助手函数:

result = await __event_call__(
    {
        "type": "input",  # 也可以是 "confirmation" 或 "execute"
        "data": {
            "title": "请输入您的访问密码",
            "message": "执行此高敏感操作需要密码校验",
            "placeholder": "请输入密码",
        },
    }
)

# 此时 Python 代码会挂起等待,result 将接收到用户在前端输入的文本值
可配置的超时时间

在默认情况下,__event_call__ 最多会挂起等待用户响应 300 秒(5 分钟)。如果超时用户仍未操作,平台将自动抛出超时异常。该超时时长可以通过 WEBSOCKET_EVENT_CALLER_TIMEOUT 环境变量在服务端进行调整。如果您的交互比较复杂(如需要用户填写长表单或上传文件),建议适当调大此数值。


📜 事件 Payload 数据结构

无论是使用 emitter 还是 call,事件 Payload 的基本 JSON 结构均为:

{
  "type": "event_type",   // 请参阅下方的支持列表
  "data": { ... }         // 该事件类型特有的 Payload 数据
}

在绝大多数开发场景中,您仅需关心 "type""data"。Open WebUI 会自动处理事件在底层的路由和分发。


🗂 支持的事件类型全览

以下是 Open WebUI 核心渲染引擎支持的全部 type 参数值、对应的渲染效果及其具体的数据 Payload 结构。

事件类型 type适用场景对应的数据 Payload 结构与示例
status在消息上方显示当前任务的执行进度条/状态历史链{"description": str, "done": bool, "hidden": bool}
chat:completion向前端推送 LLM 的生成结果(内部高阶格式,一般无需手动调用)
chat:message:delta,
message
向当前正在生成的消息末尾追加文本字词(增量拼接){"content": "待拼接的纯文本"}
chat:message,
replace
彻底重写并覆盖当前消息的全部正文内容{"content": "覆盖后的完整新文本"}
chat:message:files,
files
动态更新或覆盖附加在当前消息上的文件列表{"files": [...]}(传入 Open WebUI 的文件对象数组)
chat:title动态更新当前聊天会话的标题标题字符串字面量 OR {"title": "新标题"}
chat:tags动态更新当前聊天会话的分类标签组标签字符串数组或对象
source,
citation
为大模型回复添加 RAG 引用源,或展示代码执行结果针对代码执行:请参见下文说明。
notification在用户前端界面弹出气泡提示(Toast 弹窗){"type": "info" | "success" | "error" | "warning", "content": "提示文字"}
confirmation
(必须使用 __event_call__
弹出全局的“确定/取消”模态确认框{"title": "框标题", "message": "提示说明文字"}
input
(必须使用 __event_call__
弹出单行文本输入框以获取用户键盘输入{"title": "标题", "message": "提示", "placeholder": "占位提示", "value": ..., "type": "password"} (type 为可选)
execute
(支持 __event_call____event_emitter__
直接在用户的浏览器全局沙箱中执行任意 JavaScript 代码。使用 call 可以获取返回值,使用 emitter 则是单向发射{"code": "待执行的 JavaScript 源码字符串"}
chat:message:favorite动态将当前消息标记为喜欢(收藏/置顶)或取消{"favorite": bool}

高阶/自定义事件:

  • 您完全可以声明自己的专属事件类型,并在您自定义的前端 UI 组件中捕获并处理它们(或使用平台后续的事件扩展插槽)。

❗ 特定事件类型详解

status

在 UI 界面上渲染一条优雅的任务进度/执行状态链:

await __event_emitter__(
    {
        "type": "status",
        "data": {
            "description": "第 1 步 (共 3 步): 正在尝试抓取远程数据...",
            "done": False,
            "hidden": False,
        },
    }
)

done 字段说明

done 字段决定了前端界面该行状态文本是否展示流光加载动画 (Shimmer animation)

done 的值对应的视觉表现效果
false(或缺省)状态文字左侧显示加载图标,整行呈现呼吸/流光加载动效 —— 提示用户“该步骤正在处理中”
true加载动画停止,整行呈现静态常亮 —— 提示用户“该步骤已执行完毕”

后端系统并不会读取或分析 done 属性,它仅仅将其转发给前端渲染器。该流光效果完全由前端 CSS 动画负责表达。

始终在最后发送 done: True

如果您在插件中发射了 status 事件,请务必在状态链执行的最后,发送至少一条包含 done: True 的事件。否则,前端最后那条状态提示会永远闪烁呼吸流光,给用户造成该插件“一直在死循环处理中,从未真正结束”的错觉 —— 即使您的 Python 逻辑其实早就返回了结果。

# ✅ 正确范例
await __event_emitter__({"type": "status", "data": {"description": "正在分析文件内容...", "done": False}})
# ... 执行耗时逻辑 ...
await __event_emitter__({"type": "status", "data": {"description": "分析完毕!", "done": True}})

# ⚠️ 错误范例 —— 加载流光会永久闪烁
await __event_emitter__({"type": "status", "data": {"description": "正在分析文件内容...", "done": False}})
# ... 执行完毕后直接返回,但没有发送 done: True 事件

hidden 字段说明

hidden 设为 true 时,该状态信息会被悄悄追加到消息的 statusHistory 历史数组中,但不会渲染在前端当前的状态栏中。这非常适合用于记录一些只给开发排查使用的后台痕迹,而不想打扰到普通用户的视线。

此外,当大模型正文 message.content 还空空如也,且最后发射的状态属性被标记为了 hidden: true(或完全没有发送过任何状态)时,前端系统会自动拉起一个优美的 Skeleton 骨架屏加载动画,确保界面不会发生突兀的闪烁。


chat:message:delta 或 message

流式拼装正文(在已有的消息文字后像打字机一样增量拼接):

await __event_emitter__(
    {
        "type": "chat:message:delta",  # 也可以直接简写为 "message"
        "data": {
            "content": "这是一段"
        },
    }
)

# 紧接着,随着后台生成的继续:
await __event_emitter__(
    {
        "type": "chat:message:delta",
        "data": {
            "content": "陆续拼装进来的文本片段。"
        },
    }
)

chat:message 或 replace

完全重构正文(将已有的消息正文整体用新文本彻底覆盖):

await __event_emitter__(
    {
        "type": "chat:message",  # 也可以直接简写为 "replace"
        "data": {
            "content": "这是一份全新的、将旧内容整体替换掉的终版答复。"
        },
    }
)

files 或 chat:message:files

附加或更新文件列表:

await __event_emitter__(
    {
        "type": "files",  # 也可以写为 "chat:message:files"
        "data": {
            "files": [
               # 传入符合 Open WebUI 规范的后端文件字典对象
            ]
        },
    }
)

embeds 或 chat:message:embeds

为助手发送的回复消息卡片下方挂载一个高度动态的富 UI HTML 网页。Payload 中 embeds 列表里的每一个子元素均代表一个字符串,该字符串会被直接灌入到嵌入的 iframe 沙箱组件的 srcsrcdoc 属性中:

  • 如果字符串以 http://https://// 开头,平台会自动将其识别为外部网页 URL 并拉起加载。
  • 否则,系统会将其识别为原生的 HTML 源码,并在本地进行流式渲染展示(Open WebUI 渲染器会自动进行智能判别)。

其完整的 Payload 数据格式为:

await __event_emitter__(
    {
        "type": "embeds",  # 推荐使用短名称;这有利于将其持久化存入 DB 中
        "data": {
            "embeds": ["<html><body><h3>我的卡片标题</h3></body></html>", "https://example.com/widget"],
            "replace": False,  # 可选参数,默认为 False
        },
    }
)

追加 (Append) 与替换 (Replace) 的对比

在默认情况下,后续发射的 embeds 事件内容会被追加到已有卡片的尾部 —— 多次发送会纵向累加。如果您正在构建一个需要随着后台处理而不断自我刷新更新的独立动态卡片,请将 data.replace 设为 True;这会直接使用当前发送的数组去覆盖替换掉消息上的整个 embeds 数组。(当您传入一个空数组 []replace: True 时,便能彻底清空该消息上的所有挂载卡片。)

参数模式运行行为表现适合的典型业务场景
replace 缺省 / 设为 False每次发送的新卡片都会被追加到已有的 message.embeds 数组尾部。一次性工具或操作,在执行完毕后只在最下方投递一个最终看板。
replace: True消息上的所有旧卡片都会被抹除,整体替换为当前发送的卡片数组。需要在执行期间源源不断刷新内容状态的组件(如动态数字进度条、实时滚动监控看板、轮询的状态指标卡)。

引入 replace 参数的核心目的在于:避免用户在刷新或重新加载网页时,在聊天历史中看到五六个废弃的、不同执行阶段的过期历史进度条堆叠在一起 —— 确保有且仅有最新的那份状态被留存下来。

示例:在工具执行的最后追加一个卡片

async def get_weather_dashboard(self, city: str, __event_emitter__) -> str:
    html = build_dashboard_html(city)  # 您的 HTML 生成逻辑
    await __event_emitter__(
        {
            "type": "embeds",
            "data": {"embeds": [html]},
        }
    )
    return f"已成功渲染 {city} 的天气动态看板。"

这与从工具中直接返回一个 HTMLResponse 完全等价 —— 事实上,在只需要投递单一卡片时,我们强烈推荐使用 富 UI 嵌入集成开发手册 中介绍的方法,那是一种更为精炼优雅的实现路径。

示例:在管道中动态刷新进度条卡片

async def pipe(self, body: dict, __event_emitter__):
    for step in range(1, 6):
        progress_html = f"""
            <div style="font-family:system-ui;padding:1rem;">
              <h3>系统正在为您索引知识文档库</h3>
              <progress value="{step}" max="5" style="width:100%;"></progress>
              <p>当前处理进度:第 {step} 步 (共 5 步)…</p>
            </div>
        """
        await __event_emitter__(
            {
                "type": "embeds",
                "data": {
                    "embeds": [progress_html],
                    "replace": True,  # 使用覆盖替换,绝不堆叠历史卡片
                },
            }
        )
        await asyncio.sleep(2)

    # 渲染最终完成态看板 —— 同样使用 replace 覆盖,确保消息下永远只有一张卡片
    await __event_emitter__(
        {
            "type": "embeds",
            "data": {"embeds": ["<div>✅ 知识文档索引构建完毕。</div>"], "replace": True},
        }
    )
    yield "系统准备就绪。"

这样一来,无论用户何时重新打开此对话,message.embeds 中将只有最新且唯一的“✅ 知识文档索引构建完毕”卡片,前面加载过程中的 5 个过渡态进度条卡片都已被完美擦除。

示例:清空当前消息的所有卡片

await __event_emitter__(
    {
        "type": "embeds",
        "data": {"embeds": [], "replace": True},
    }
)

注意事项

  • 数据持久化必须声明短名称。虽然发送 "embeds""chat:message:embeds" 前端都能正常解析,但有且仅有短名称 "embeds" 会被后端存储到数据库中 —— 请严格参考后文持久化支持列表。声明长别名时,replace 覆盖覆盖机制也不会起效。
  • 安全沙箱环境与高宽控制:挂载的富卡片都会渲染在配置了 allow-scriptsallow-popupsallow-downloads 的高度安全的 iframe 沙箱中。关于沙箱自适应高度调整、Alpine.js 及 Chart.js 的依赖自动注入等进阶内容,请阅读 富 UI 嵌入 - 沙箱与安全策略 专题文档。
  • 外部工具支持:外部工具接口同样可以向 /api/v1/chats/{chatId}/messages/{messageId}/event 推送完全一致的 embeds (包含 replace: true) 数据 Payload —— 请参考下文说明

chat:title

动态更新对话标题:

await __event_emitter__(
    {
        "type": "chat:title",
        "data": {
            "title": "市场宏观数据看板会话"
        },
    }
)

chat:tags

动态更新对话分类标签组:

await __event_emitter__(
    {
        "type": "chat:tags",
        "data": {
            "tags": ["金融分析", "AI 洞察", "日常周报"]
        },
    }
)

source or citation(及代码执行)

追加一个参考资料/引用源头:

await __event_emitter__(
    {
        "type": "source",  # 也可以写为 "citation"
        "data": {
            # 填入符合 Open WebUI 规范的 Source 数据字典对象
        }
    }
)

针对代码解释器执行状态(用于在前端渲染炫酷的代码执行状态过程):

await __event_emitter__(
    {
        "type": "source",
        "data": {
            # 填入符合 Open WebUI 规范的 Code Source 数据字典对象
        }
    }
)

notification

触发系统的气泡提示弹窗(Toast 通知):

await __event_emitter__(
    {
        "type": "notification",
        "data": {
            "type": "success",  # 支持 "success", "info", "warning", "error"
            "content": "您的复杂后端处理逻辑已成功执行完毕!"
        }
    }
)

chat:message:favorite

动态将当前消息标记为喜欢(收藏/置顶)或取消:

await __event_emitter__(
    {
        "type": "chat:message:favorite",
        "data": {
            "favorite": True  # 设为 False 即可取消收藏
        }
    }
)

它的实际底层工作机制是: 该事件会强制 Open WebUI 前端立即重写并更新其浏览器本地缓存中关于该消息的“favorite(喜欢)”状态。如果不发射此事件,而是一个 Action 动作函数 只是静默在数据库底层改写了 message.favorite 字段,由于前端也维护了自己的状态,当前端稍后发起下一次对话自动保存(auto-save)时,有可能会用前端的缓存去覆盖抹除掉您在后端数据库里的改动。该事件可以确保前端 UI 与后端 DB 完美保持步调一致。

专门为 Actions 动作插件设计

尽管在技术层面上您可以从任何插件(工具、管道、过滤器)里发射该事件,但它在 Actions 动作插件中最有意义,也最契合其设计定位。Actions 动作插件专为操作已有消息而生,可以直接修改数据库字段。如果只是在 pipe 或 tool 里发送它,它只会临时刷新一下前端的红心显示,当稍后前端自动发起数据库同步时,由于插件没有配套修改底层 DB 字段,该状态依然会被前端自动抹除复原。

界面展现位置:

  • 消息操作工具栏:设置为 True 时,该消息正下方的“心形”图标会变为实心填充状态,表明该消息已被收藏。
  • 对话回顾概览:已被收藏(置顶)的消息会在对话树的概览视图中被高亮标出,方便用户日后一眼定位到最核心的历史结论。

示例:“Pin Message(置顶消息)”动作插件实现

若想参考此事件在实际生产插件中的完美落地,请在 Open WebUI Community 社区站点搜索 Pin Message Action on Open WebUI Community。该精妙的 Action 详细展示了如何一边安全地改写底层数据库,一边用 chat:message:favorite 极其流畅地驱动前端心形收藏图标进行高亮反馈。


confirmation(需要 __event_call__

拉起前端的“确认/取消”模态选择框,并捕获用户在屏幕上的点击结果:

result = await __event_call__(
    {
        "type": "confirmation",
        "data": {
            "title": "您确定要继续吗?",
            "message": "此操作非常危险,有可能会抹除您当前的本地配置数据。"
        }
    }
)

if result:  # 用户点击了“确定”按钮
    await __event_emitter__({
        "type": "notification",
        "data": {"type": "success", "content": "用户已确认授权,继续执行。"}
    })
else:       # 用户点击了“取消”按钮,或者关闭了弹窗
    await __event_emitter__({
        "type": "notification",
        "data": {"type": "warning", "content": "用户已取消当前危险操作。"}
    })

input(需要 __event_call__

拉起一个快捷弹窗输入框,索要用户的文本输入:

result = await __event_call__(
    {
        "type": "input",
        "data": {
            "title": "请输入您的名字",
            "message": "为了给您建立专属文件夹,系统需要确认您的名字。",
            "placeholder": "请输入名字全称"
        }
    }
)

user_input = result
await __event_emitter__(
    {
        "type": "notification",
        "data": {"type": "info", "content": f"系统收到您的名字:{user_input}"}
    }
)

掩码/密码输入

为了保护极其敏感的数据的录入过程(如 API Key 密钥、个人登录密码),只需在 data 参数中将 type 显式设置为 "password" 即可。前端会将其自动渲染为一个带密码掩码隐藏并支持“眼睛”图标切换显隐的输入框:

result = await __event_call__(
    {
        "type": "input",
        "data": {
            "title": "请输入您的第三方 API 密钥",
            "message": "该服务需要您的私有 Token 以便发起计费请求。",
            "placeholder": "请输入 sk-...",
            "type": "password"
        }
    }
)
提示

这与用户在配置插件阀门 (Valves) 密码字段时所使用的 SensitiveInput 敏感词输入组件在底层完全一致,拥有让用户倍感亲切的显隐图标切换体验。


execute(同时支持 __event_call____event_emitter__

在用户的浏览器沙箱环境中,直接呼叫执行一段 JavaScript 代码。

与其他交互式事件截然不同,execute 拥有能够同时适配这两个助手函数的强大能力:

助手函数运行表现机制最适合的黄金场景
__event_call__异步运行 JS 代码,并一直等待 JS 块的 return 结果返回给 Python 后端(双向通信)。需要将浏览器端数据回传给 Python (如抓取浏览器存储的 localStorage、检测客户端设备指纹、分辨率状态等)。
__event_emitter__向前端抛出 JS 源码单向发射,不管任何回传(单向通信)。无需任何返回值,只希望客户端进行一些操作(如触发一次二进制文件下载、静默操控 DOM 树等)。

双向通信示例(使用 __event_call__

result = await __event_call__(
    {
        "type": "execute",
        "data": {
            "code": "return document.title;",  # 将当前浏览器的标签页标题返回
        }
    }
)

await __event_emitter__(
    {
        "type": "notification",
        "data": {
            "type": "info",
            "content": f"系统在后端检测到您的浏览器标题为:{result}"
        }
    }
)

单向发射示例(使用 __event_emitter__

# 异步触发一次内存中的 blob 网页下载 —— 无需任何回传
try:
    await __event_emitter__(
        {
            "type": "execute",
            "data": {
                "code": """
                    (function() {
                        const blob = new Blob([data], {type: 'application/octet-stream'});
                        const url = URL.createObjectURL(blob);
                        const a = document.createElement('a');
                        a.href = url;
                        a.download = 'data_export.bin';
                        document.body.appendChild(a);
                        a.click();
                        URL.revokeObjectURL(url);
                        a.remove();
                    })();
                """
            }
        }
    )
except Exception:
    pass
iOS PWA 应用兼容性提示

在苹果 iOS 系统的 Safari 浏览器中(尤其是当用户将 Open WebUI “添加到主屏幕”作为 PWA 桌面独立应用运行时),如果使用双向挂起的 __event_call__ 来触发二进制下载,由于浏览器在拉起系统下载保存提示时有可能会截断 WebSocket 的双向响应通道,有可能会触发 "TypeError: Load failed" 的通道中断报错。这种情况下,使用单向发射的 __event_emitter__ (即发即弃)是完美的良药,因为它完全避开了对回传通道的依赖。

因此,如果您的 JS 逻辑需要触发浏览器下载,且 Python 不需要获得任何返回值,请优先选用 __event_emitter__ 以建立极致的跨平台兼容性。

工作原理

execute 事件在前端底层会使用 JavaScript 原生的 new Function()在浏览器主页面的全局上下文(Context)中直接运行代码。这意味着:

  • 运行该 JS 时,拥有对浏览器主 DOM 树、Cookies、localStorage 及 Session 的完整操作和读写权限
  • 不受任何 iframe 安全沙箱的限制 —— 它是纯原生的无隔离运行环境。
  • 它可以肆无忌惮地修改和操作 Open WebUI 平台的整个前端样式与交互行为(动态显示/隐藏任何元素、填入表单、强行拉起下载等)。
  • 该代码会作为一个 async 异步函数执行,因此只要您使用的是 __event_call__ 挂起,您便能在 JS 源码中自由调用 await 并将最终结论 return 回传给后端 Python。
前端自动化

由于 execute 运行在主页面上下文且拥有不受任何限制的 DOM 树访问权,您甚至可以使用它来对 Open WebUI 网页前端执行任何形式的自动化操作:代替用户点击特定的按钮、自动填写输入框表单、控制页面路由跳往别处、动态改变模型选择器配置、乃至偷偷帮用户点击发送新消息等。你可以将 execute 视为手里的前端遥控器 —— 凡是用户能在屏幕上手工点击完成的交互,您都能用它以写代码的方式自动化搞定。

示例:在前端弹出一个高度定制的交互式输入表单

result = await __event_call__(
    {
        "type": "execute",
        "data": {
            "code": """
                return new Promise((resolve) => {
                    const overlay = document.createElement('div');
                    overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:9999';
                    overlay.innerHTML = `
                        <div style="background:white;padding:24px;border-radius:12px;min-width:300px">
                            <h3 style="margin:0 0 12px;color:#333;">请输入您的资料</h3>
                            <input id="exec-name" placeholder="请输入姓名" style="width:100%;padding:8px;margin:4px 0;border:1px solid #ccc;border-radius:6px"/>
                            <input id="exec-email" placeholder="请输入电子邮箱" style="width:100%;padding:8px;margin:4px 0;border:1px solid #ccc;border-radius:6px"/>
                            <button id="exec-submit" style="margin-top:12px;padding:8px 16px;background:#333;color:white;border:none;border-radius:6px;cursor:pointer">提交表单</button>
                        </div>
                    `;
                    document.body.appendChild(overlay);
                    document.getElementById('exec-submit').onclick = () => {
                        const name = document.getElementById('exec-name').value;
                        const email = document.getElementById('exec-email').value;
                        overlay.remove();
                        resolve({ name, email });
                    };
                });
            """
        }
    }
)
# result 字典将稳稳获取到前端回传的数据:{"name": "古德白", "email": "goodbye@example.com"}

execute 与 Rich UI 嵌入的对比

使用 execute 发送 JS 与使用 Rich UI 卡片嵌入 是重塑前后台极客交互的两条相辅相成的完美路径:

属性维度execute 事件机制Rich UI 卡片嵌入 (Embeds)
运行运行所在环境前端主页面上下文(无沙箱物理隔离,为所欲为)沙箱隔离的安全 iframe 组件中
可持久化能力纯属瞬时性表现 —— 网页刷新或重新跳往别处即烟消云散持久化 —— 卡片状态会被妥善保存在会话历史中
主 DOM 访问权完整畅通(DOM、Cookies、localStorage、路由等)默认被沙箱彻底隔离屏蔽,不可访问父级
表单填写功能完全且自由支持(没有任何沙箱局限)需要管理员在系统设置中明确勾选 allowForms 放行
最佳适合场景瞬时性、一次性的强交互(如拉起确认框、动态下载、动态操控 DOM 或获取客户端系统信息)。持久化展现的视觉资产卡片(如数据分析图表、长期渲染的精美数据卡片看板)。

遇到瞬时或一次性动作(二次确认、密码录入、文件下载、获取浏览器变量)时请选用 execute;遇到需要长效沉淀在会话历史中供日后查看的炫酷多媒体数据组件时,请选择 Rich UI 嵌入机制。

注意

由于 execute 允许在用户的浏览器中无隔离运行任意 JavaScript,它拥有极高的本地操作权限。请务必仅在您百分之百信任的内部自研函数中调用此机制 —— 严禁将来自外界不可信的或用户自行灌入的代码通过此事件在平台前端发射运行,否则会埋下严重的安全隐患。


🏗️ 在何时何处使用事件

  • 可以在 Open WebUI 任何工具 (Tools) 或 函数 (Functions) 中进行声明和调用。
  • 用它来流式吐出正文、汇报后台复杂的处理进度、向用户索要凭证输入、重构网页外观或附加额外的输出文件与数据。
  • await __event_emitter__ 代表单向通信(即发即忘,毫无心理包袱)。
  • await __event_call__ 代表双向通信,适用于需要挂起 Python 逻辑以获得前端反馈(如用户点击二次确认框、手动录入值、或获取 JS 执行返回值)的进阶流程。
  • 特立独行的 execute 能够极佳地同时适配这两种助手方法。需要捕获 JS 返回值时请使用 __event_call__,只希望在前端触发某些动作而不必等待回复(如拉起本地下载)时,请使用 __event_emitter__
管道插件:返回值与事件的冲突

如果您正在编写 Pipe(管道)类型插件,请务必当心,切勿将不同的文字输出手段混为一谈。如果您的 pipe() 脚本以普通的 return "字符串" 结尾,该字符串会成为该对话最终被写入数据库的正文内容。如果它采用 yield 形式(生成器),那么每次 yield 产出的分块都会被打字机流式输出在聊天中。在此期间,如果您还多此一举地向前端发射了 chat:message:deltachat:message 事件,会导致事件追加的内容与返回值/生成器吐出的正文拼在一块发生严重的排版冲突。

黄金开发建议:请始终将标准的 return 或 yield 作为您输出助手正文内容的唯一核心手段。在整个运行期间,发射像 status(任务状态条)、source(检索引用源)、files(附加文件)或 notification(气泡提示)这类的辅助型事件是完全安全且被极力推崇的;但请务必坚决避免将 chat:message:deltachat:message 事件作为您向管道返回主要消息正文内容的手段

为什么对于管道插件而言,纯依赖事件来传递消息内容极其脆弱?:当管道处理流程宣告结束时,Open WebUI 前端会将整个本地缓存的历史记录整体发起一次全量同步,写入后台数据库中。这次全量覆写会粗暴覆盖掉此前由后端事件发射器(Event Emitter)零零碎碎同步过去的消息字段。如果您的 pipe 函数最终 return 了一个 None 或者空字符串,而全指望在逻辑里发送 type: "message" 事件来为用户投递文本,当逻辑执行完毕触发最后一次前端向数据库覆写时,由于前端认为模型的最终输出是一个空值,这会把您之前费尽心机发射成功的正文内容直接抹除掉,变成一片空白。

# ❌ 极度脆弱:完全依靠 message 事件来投递回复正文 —— 会在最终保存时被覆盖抹除
async def pipe(self, body: dict, __event_emitter__=None):
    await __event_emitter__({"type": "message", "data": {"content": "你好!"}})
    # 最终没有 return 任何值(隐式返回 None) —— 前端发起同步时,会把空内容写入 DB,冲掉上方的“你好!”

# ✅ 正确优雅:直接 return 最终文本正文,在整个执行期间使用其他事件完成进度条汇报
async def pipe(self, body: dict, __event_emitter__=None):
    await __event_emitter__({"type": "status", "data": {"description": "大模型在后台冥思苦想中...", "done": False}})
    result = "这是最终呈现给用户的文字回复。"
    await __event_emitter__({"type": "status", "data": {"description": "思考完毕!", "done": True}})
    return result

# ✅ 同样正确:使用 yield 流式吐出字词,配合 status 事件辅助状态传达
async def pipe(self, body: dict, __event_emitter__=None):
    await __event_emitter__({"type": "status", "data": {"description": "打字机开始流式输出了...", "done": False}})
    for chunk in ["欢迎", "来到", "Open ", "WebUI", "!"]:
        yield chunk
    await __event_emitter__({"type": "status", "data": {"description": "输出完毕", "done": True}})

💡 提示与高级注意事项

  • 支持多事件共存拼接:在单次助手回答周期内,您完全可以在前半段不断发射 status 事件刷新进度,中途发射 files 追加一些参考图片,随后采用 yield 流式吐出正文,并在最末尾投递一个精美的 embeds 可交互卡片 —— 一切配合得天衣无缝。
  • 自定义特殊事件:虽然上面列出的是官方标准支持列表,但您也可以声明自己个性的 type 字符串,并在您自研的特殊前端魔改代码中去拦截和表达这些事件。
  • 向未来的兼容性:Open WebUI 的事件体系正处于极速繁荣发展期,请随时关注 Open WebUI 官方代码仓库 以捕获最新的事件钩子和高级教程。

🧐 常见问题

Q: 如何为用户触发一条通知弹窗?

请使用 notification 事件类型:

await __event_emitter__({
    "type": "notification",
    "data": {"type": "success", "content": "任务已顺利达成!"}
})

Q: 如何提示用户输入内容并获取他们的回复?

请配合 __event_call__ 进行调用:

response = await __event_call__({
    "type": "input",
    "data": {
        "title": "请问您的称呼是?",
        "message": "系统需要确认您的名字以便提供个性化问候:",
        "placeholder": "请输入名字"
    }
})

# response 挂起回传结果将接收到:{"value": "用户的录入文本"}

Q: 有哪些事件类型可以被 __event_call__ 调用?

  • "input":拉起单行文本录入模态框
  • "confirmation":拉起带“确定/取消”的二次选择对话框
  • "execute":在前端浏览器环境中执行特定 JS 并等待回传 return 结果(如果只想执行而不需要管回传,也可以用 __event_emitter__ 发射 —— 请参考前文说明)。

Q: 我可以更新附加在消息上的文件吗?

完全可以 —— 发送 "files""chat:message:files" 事件,并附带 {files: [...]} 对象数组即可。

Q: 我可以更新对话标题或标签吗?

没问题,按需发射 "chat:title""chat:tags" 即可在后台进行同步修改并立即刷屏。

Q: 我可以向用户流式输出消息(渐进式 Token)吗?

可以 —— 在循环处理逻辑中反复向前端推送 "chat:message:delta" 事件,并在退出前发送一次 "chat:message" 敲定最终态。(但在 Pipe 管道开发中,我们依然强烈建议您使用 Python 原生的 yield 生成器作为更稳定也更安全的实现方法。)


🌐 外部工具事件

对于托管在外部独立服务器上的外部工具(OpenAPI 和 MCP 服务),也可以通过平台暴露的 REST 接口直接向 Open WebUI 投递完全相同的丰富事件。这使得部署在内网其他机器上的数据处理逻辑,依然能够为前端推送进度条、气泡通知,或实现工具层面的流式回复。

前提条件

为了能从 incoming 请求中成功抓取到会话 ID 与消息 ID,您必须在您的 Open WebUI 系统部署中,开启以下环境变量以放行信息头转发:

ENABLE_FORWARD_USER_INFO_HEADERS=True

如果未开启该开关,Open WebUI 会出于安全合规要求在请求向外发送前将这些元数据头部全部过滤抹除,外部工具将无法获取会话 ID 并推送事件。

由 Open WebUI 提供的 HTTP 头信息

当放行转发配置就绪后,外部工具服务在接收到来自 Open WebUI 的调用时,能够在其请求 Header 中捕获以下参数:

请求头字段含义说明对应的环境变量重写参数
X-Open-WebUI-Chat-Id该工具被拉起时所在的聊天会话 IDFORWARD_SESSION_INFO_HEADER_CHAT_ID
X-Open-WebUI-Message-Id触发此外部工具调用的消息 IDFORWARD_SESSION_INFO_HEADER_MESSAGE_ID

事件推送接口端点

接口 URLPOST /api/v1/chats/{chat_id}/messages/{message_id}/event

身份校验方式:必须在请求 Header 中携带有效的 Open WebUI 管理员 API 密钥 (API Key) 或当前用户的 Session Token 凭证。

请求 JSON 体

{
  "type": "status",
  "data": {
    "description": "外部微服务已接管您的请求并正在紧急处理中...",
    "done": false
  }
}

支持的事件类型

外部微服务所能发射的事件类型与原生 Python 插件完全对等:

  • status – 在前端汇报任务执行步骤与进度
  • notification – 在屏幕上弹出 Toast 气泡弹窗
  • chat:message:delta / message – 增量拼接助手正文
  • chat:message / replace – 全量重写助手正文
  • files / chat:message:files – 为助手追加输出文件
  • embeds / chat:message:embeds – 为消息下方挂载富交互式 iframe 卡片;传入 data.replace: true 即可完全替换已有的卡片数组(详情请参阅 embeds 章节)。
  • source / citation – 为大模型注入引用文献
备注

凡是需要使用 __event_call__ 助手函数实现挂起等待的双向交互事件(如 inputconfirmation)在外部工具中是不被支持的,因为它们从根本上强制依赖双向的持久化 WebSocket 实时链路。外部工具同样无法通过 __event_call__ 发起 execute 调用;但如果您不需要任何返回值,单向发射的 execute(通过 __event_emitter__)是有可能正常运作的。

示例:Python 外部工具实现

import httpx

def my_tool_handler(request):
    # 1. 从入站的 Request 中解析出 Open WebUI 转发的元数据 Header
    chat_id = request.headers.get("X-Open-WebUI-Chat-Id")
    message_id = request.headers.get("X-Open-WebUI-Message-Id")
    api_key = "您的-open-webui-全局-API-密钥"
    
    # 2. 主动向 Open WebUI 发送一条进度条 status 事件
    httpx.post(
        f"http://your-open-webui-host/api/v1/chats/{chat_id}/messages/{message_id}/event",
        headers={"Authorization": f"Bearer {api_key}"},
        json={
            "type": "status",
            "data": {"description": "外部计算引擎已成功启动...", "done": False}
        }
    )
    
    # ... 执行复杂的后台微服务计算 ...
    
    # 3. 计算结束后,发送最终态 status 事件以停掉加载流光
    httpx.post(
        f"http://your-open-webui-host/api/v1/chats/{chat_id}/messages/{message_id}/event",
        headers={"Authorization": f"Bearer {api_key}"},
        json={
            "type": "status",
            "data": {"description": "微服务处理完毕!", "done": True}
        }
    )
    
    return {"result": "success"}

示例:JavaScript/Node.js 外部工具实现

async function myToolHandler(req) {
  // 1. 从 HTTP 请求中提取会话标识
  const chatId = req.headers['x-open-webui-chat-id'];
  const messageId = req.headers['x-open-webui-message-id'];
  const apiKey = '您的-open-webui-全局-API-密钥';
  
  // 2. 跨进程向平台推送一个 Toast 弹窗通知
  await fetch(
    `http://your-open-webui-host/api/v1/chats/${chatId}/messages/${messageId}/event`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        type: 'notification',
        data: { type: 'info', content: '外部 JavaScript 工具微服务正在后台全力计算中...' }
      })
    }
  );
  
  return { result: 'success' };
}

🔒 状态持久化与浏览器断连处理

一个在开发进阶插件时极其高频且重要的问题是:当某个工具、管道或动作正在后台艰苦跑数据时,如果用户突然把浏览器标签页关闭了,会发生什么灾难?

服务端代码将继续执行

在 Open WebUI 中,每当触发对话请求时,系统在后端拉起的是一个完全独立于当前 HTTP 物理通道或 Socket.IO 连接状态的后台 asyncio 异步执行协程任务。如果用户此时关闭了网页标签页:

  1. 浏览器端的 WebSocket 链接应声断开,平台后端的 Socket.IO 连接断连处理器(Disconnect Handler)被触发。
  2. 该断连处理器会例行公事地清理掉该会话的连接元数据,但绝对不会去强行取消或掐断任何正在后台跑数的 asyncio 协程任务
  3. 后端的异步协程依然会在服务器内存里不受任何打扰地一路狂奔,直到正常执行完毕。
  4. 在此期间,Python 脚本调用的 sio.emit() 动作依然能够静默执行成功 —— 这些事件会被无声抛入一个已经空无一人的 Socket 房间里并被丢弃,但不会引发任何 Python 报错。
  5. 最关键的是,针对支持数据持久化的事件类型,数据库改写操作依然会稳稳生效(参考下文列表)。
  6. 该协程会完美运行到函数逻辑的最末尾 return 处,或者直到代码内部自己报错退出,抑或是管理员在后台强制杀掉任务。
没有执行超时上限

在 Open WebUI 架构中,针对管道、工具或动作的执行没有任何默认的超时强杀时间上限。您的代码在后台跑几分钟甚至长达数个小时都是完全合规且被允许的 —— 系统底层不会有人在半路强行把您的协程掐断。能够中断一个后台任务的只有以下四种物理可能:

  • 您的函数顺利 return 返回,或者自身抛出了代码级别未捕获的 Exception 异常。
  • 具有管理员身份的用户,通过请求 POST /api/tasks/stop/{task_id} (或在前端 UI 界面上疯狂点击红色的中止任务按钮)发起了强制中断。
  • Open WebUI 后端物理服务进程被关闭或重启。

哪些事件类型会被持久化写入数据库?

无论此时浏览器是否在线,事件发射器对于以下特定的事件类型,总是会直接将内容安全、稳妥地持久化存入服务器底层的数据库中。这些写入完全独立于系统的 ENABLE_REALTIME_CHAT_SAVE 参数开关。

✅ 会持久化保存(无视网页关闭)

事件类型具体会被写入数据库的哪个位置
status被追加到数据库中该消息对应的 statusHistory 状态历史数组中
message被追加到该消息对应的 content 正文字段尾部
replace整体重写并替换该消息对应的 content 正文字段
embeds被追加到消息底层的 embeds 卡片数组中,或者当配置了 data.replace: true 时整体重写卡片数组。详细的 Payload 参数请参阅 embeds 章节。
files被追加到该消息底层的 files 文件关联数组中
source / citation被追加到该消息底层的 sources 参考检索源数组中

这 6 类事件在其对应的 Emitter 函数被呼叫时,底层就会同步向物理数据库发起存储调用,不管浏览器此时是不是断网或者关闭,只要协程在跑,它们就一定会写入 DB 并保存下来。

始终使用短名称以确保持久化

请务必当心,后端事件持久化器有且仅能识别上表所列的短名称!如果您在逻辑中发射了长名称 "chat:message:embeds",虽然用户的浏览器开着时能被前端正常捕获并渲染,但一旦浏览器关闭了,后端持久化器是无法识别这个别名并写入 DB 的。如果您的信息非常关键且要求无视断网绝对留存,请在代码中永远使用短名称("status", "message", "replace", "embeds", "files", "source")。

⚠️ 管道插件:后端持久化内容可能被覆盖

特别针对 Pipe(管道)插件开发者,使用 "message""replace" 事件所写入到数据库的正文文字,在协程执行完毕时,非常容易被前端发起的全量同步覆写直接抹去。当管道的 pipe() 执行结束返回时,前端会理所当然地把其本地维护的、当时还一片空白的助手正文发起全量 Save 同步覆盖。如果您的 pipe 函数最终 return 了一个 None 或者空字串,前端的这次覆写会用空字串粗暴洗掉您之前用 emitter 发射并保存好的全部文字正文。

但这一行为完全不会影响 Tools 工具、Actions 动作或者 Filters 过滤器,在那些场景下,事件只是起到辅助元数据的作用,不负责承载最终正文。此外,它也完全不会影响 "status", "files", "source", 或者是 "embeds" 事件,因为它们存放在数据库独立的位置上,不会被全量正文覆写所干扰。

管道开发者开发铁律:投递真正的助手回复正文,必须且只能使用 return 或 yield;发送状态条、检索源、文件以及富卡片,请使用与之配套的 emitter 辅助事件。

❌ 不会持久化保存(网页关闭即丢失)

事件类型为什么它们在网页关闭后就烟消云散了?
chat:completionLLM 的流式文本分块传输机制 —— 仅活在 Socket.IO 瞬时通道中
chat:message:delta纯属前端的渲染别名,后端不提供任何持久化逻辑
chat:message纯属前端的别名,后端持久化器不识别此名称
chat:message:files纯属前端的别名,后端持久化器不识别此名称
chat:message:embeds纯属前端的别名,后端持久化器不识别此名称
chat:message:error仅支持 Socket.IO 的瞬时报错汇报
chat:message:follow_ups仅支持 Socket.IO 通道的后续追问提示推送
chat:message:favorite仅支持 Socket.IO 通道的前端红心状态瞬时高亮刷新
chat:title仅支持 Socket.IO 前端标题瞬时重写
chat:tags仅支持 Socket.IO 前端标签瞬时刷新
notification用户屏幕上的气泡 Toast 弹框 —— 只有浏览器亮着时弹出来才有意义
流式大模型输出的另一种方案

如果您的 Pipe 管道插件或者 Tool 工具插件需要去请求一个极度漫长的 LLM 并希望其输出即使在浏览器关闭时也能被安全存储在 DB 中,建议直接在您的 Python 代码中,引入并运行 Open WebUI 后端底层的 generate_chat_completion 内置方法,而不是自己零零碎碎发射 chat:completion 事件。该方法会让整个对话流走系统正规的生成持久化流程,自动在服务端落库,用户随时重新打开网页它都完美都在。

⚠️ 强制依赖实时连接(网页关闭将报错)

事件类型为什么它们在网页关闭时会报错?
confirmation底层使用了 Socket 级的 sio.call() —— 强制挂起并挂钩等待前端反馈,浏览器关闭后由于等不到回传,最终会超时报错
input底层使用了 Socket 级的 sio.call() —— 同上,必将超时报错
execute (当使用 __event_call__ 时)底层使用了 Socket 级的 sio.call() —— 同上,必将超时报错
execute (当使用 __event_emitter__ 时)即发即弃 —— 永远不会报错,但如果没有浏览器在线听它,那段 JS 代码实际上就直接石沉大海无法运行了

由于 "confirmation""input" 从物理层面上强制需要用户做出交互,因此必须在 live 状态下运行。如果此时网页被关闭了,挂起的 sio.call() 会在等待达到超时上限后(可通过 WEBSOCKET_EVENT_CALLER_TIMEOUT 调整),在您的 Python 协程代码里抛出一个未捕获的 TimeoutException 异常中断执行。

execute 则机敏得多:当您使用的是 __event_emitter__ (即发即改)时,它在向 Socket 房间丢出 JS 源码后就立刻退出了,因此绝对不会在后台引发超时中断,即便此时根本没有浏览器在听。这使得 __event_emitter__ 成为在 iOS PWA 应用上由于系统下载容易掐断 WebSocket 双向通道时,保障文件平滑下载的唯一安全方案。

函数返回值持久化

无论浏览器是否断连,当您的 Python 异步协程跑完并成功执行 return 或 yield 完最后一个 chunk 时,该返回值必定会被百分之百安全持久化写入底层的数据库中

管道 (Pipes)

当 Pipe 管道的 pipe() 顺利返回(或生成器完成 yield 完最后一个字)时,系统的流式处理器会自动执行终版存储:

  • 如果启用了 ENABLE_REALTIME_CHAT_SAVE:在流式生成中途就会细粒度地将字词同步写入 DB 中。
  • 如果禁用了 ENABLE_REALTIME_CHAT_SAVE:则会在生成完毕时一次性将完整内容合并落库。

不管采用哪种配置,最终的助手文本都是绝对会安全存盘的。用户在任何时间重新进来,答复都在。

警告

请再次牢记:最终返回值永远拥有最高落库优先级。如果您的 Pipe 频繁发射 "message" 事件写入正文,但在最后的 pipe() 却 return 了一个 None,前端在终审保存时会强行把一串空内容写入 DB,抹除掉您之前靠事件辛辛苦苦写好的全部内容。因此,投递消息正文,请务必直接 return 或 yield 它。

工具 (Tools)

函数 return 值的类型系统底层的具体处理行为是否落库持久化?
HTMLResponse (且带有了 Content-Disposition: inline 响应头)自动将 HTML 主体提取出来 → 装入 embeds 数组中 → 以 "embeds" 事件名发射出去✅ 是
HTMLResponse (不带上述 inline 响应头)自动将 Body 内容解码为纯文本,作为工具执行的普通文本出参✅ 是
str (字符串) / dict (字典)直接作为工具运行出来的标准文本结论✅ 是
list (MCP 规范的列表)自动将其中的文本连接起来,并将包含的图片流式转化为系统底层文件✅ 是

动作 (Actions)

Action 动作插件的返回值处理逻辑在系统底层与 Tool 工具完全共享。因此其持久化表现也完全一致:

函数 return 值的类型系统底层的具体处理行为是否落库持久化?
HTMLResponse (且带有了 Content-Disposition: inline 响应头)自动提取 HTML 体 → 装入 embeds 数组 → 发射 "embeds" 动态卡片✅ 是
HTMLResponse (不带 inline 头)将 Body 解码为纯文本,作为该 Action 的文本返回结论✅ 是
str / dict作为 Action 动作的普通文本返回结果✅ 是

过滤器 (Filters)

Filter 过滤器是专门在管道处理链中,对入参或出参的 form_data 数据字典进行结构性过滤改造的插件 —— 它们一般不直接向用户返回可视内容。但过滤器依然可以接收 __event_emitter__ 助手,并允许随时向前端发射类似于 "status", "embeds", "message" 等持久化类型的事件。

插件类型能力对比矩阵

关联扩展能力维度工具 (Tools)动作 (Actions)管道 (Pipes)过滤器 (Filters)
是否可以使用 __event_emitter__
是否可以使用 __event_call__
返回值是否能被直接处理为用户可见的回复正文❌ (其返回值用于改写 form_data)
能否直接通过返回 HTMLResponse 挂载富卡片

实战开发建议总结

如果您希望您的插件输出能够无惧任何网页关闭或网络瞬断,绝对、稳稳地保存在历史会话中,请严格遵守以下开发四铁律:

  1. 始终通过 returnyield 返回您最终的主文本答案 —— 函数的最终返回值拥有最高的数据库写入安全保障。
  2. 在发射事件时,始终且仅使用官方认识的短名称"status", "message", "embeds", "files", "source")以获得后端底层的物理数据库自动持久化支持。
  3. 避免在核心业务流程中过度依赖 "notification", "confirmation", "input", 或 "execute" 这类必须要求浏览器开着才能工作的瞬时性事件。
  4. Rich UI 富网页卡片嵌入(无论是通过 "embeds" 事件发射,还是通过 HTMLResponse 返回)都是会被持久化保存的,它们能在用户日后任何时间重新打开网页时完美且复原渲染出来。

📝 总结

事件 (Events) 系统 赋予了您直接在 Open WebUI 前端与用户之间进行极致交互的超能力。它们能让您的后端代码汇报进度、拉起强交互弹窗、向用户索要敏感凭证、流式吐字、运行前端 JS 自动化,进而极其丝滑地将您的后端 AI 智慧,以现代化的前端视觉呈现在聊天会话里。

  • 推荐使用 __event_emitter__ 来完成单向的任务进度、事件状态与媒体卡片的发射汇报。
  • 推荐使用 __event_call__ 来拉起诸如密码输入、OK/Cancel 确认、或需要同步获取浏览器 JS 返回值的高阶双向交互。

随时查阅本手册以梳理各种标准的事件参数与数据 Payload 结构,并在探索 Open WebUI 不断生机勃勃演进的插件生态中尽情挥洒您的无限创意!


祝您在 Open WebUI 事件驱动插件的极客开发之旅中斩获佳绩! 🚀

This content is for informational purposes only and does not constitute a warranty, guarantee, or contractual commitment. Open WebUI is provided "as is." See your license for applicable terms.