🚚 迁移指南:升级到 Open WebUI 0.9.0
本指南涵盖了在升级到 Open WebUI 0.9.0 时,Tools、Functions、Pipes、Filters 和 Actions 等插件所涉及的重大破坏性变更(breaking changes)以及所需的更新项。
🧐 发生了哪些改变,为什么?
Open WebUI 0.9.0 引入了大规模的内部重构 :后端数据层已从上至下由同步(synchronous)彻底改为了异步(asynchronous)。现在,几乎每一个由数据库支持的、定义在模型类(包括 Users、Chats、Files、Models、Functions、Tools、Knowledge、Memories、Groups、Folders、Messages、Feedback 等)之上的方法,都被声明为了 async def,因此必须使用 await。
在存储层,SQLAlchemy 现已被配置为异步运行模式:会话(sessions)为 AsyncSession 实例,并且查询是通过 await db.execute(select(...)) 发起的,取代了以往传统的 db.query(...).first() 风格。虽然系统内部仍保留着一个同步引擎,但仅被限制用于在启动时执行的专属任务(配置加载、Alembic / peewee 迁移、健康检查) — 所有运行时代码,包括第三方或自定义插件,都必须改用异步引擎。
📌 我们为什么要做出这项改变?
- 高并发:在高负载情况下,阻塞在事件循环(event loop)中的数据库调用是最大的性能瓶颈。将数据层全面改为异步,能让 FastAPI 在没有线程池饱和的情况下处理多得多的并发聊天请求。
- 一致性:请求处理器本身早已是异步的;而旧数据层的同步设计,迫使整个代码库中到处充斥着令人尴尬的
run_in_threadpool包装代码。 - 着眼未来:SQLAlchemy 2.0 的异步 API 是未来唯一持续受支持的发展方向。
⚠️ 不兼容的变更
如果您的插件触及了 Open WebUI 的数据库或数据 模型 — 或者调用了最终会触及它们的任何辅助方法 — 您将需要对其进行更新。因为几乎 open_webui.utils.* 中的每个实用工具以及每个路由器函数最终都会通过某个模型来读取或写入数据,这导致“异步的触角”远远超出了显而易见的 Users. / Chats. 范围。具体而言:
- 所有的模型方法现在都是协程(coroutines)。 以前同步的任何调用,比如
Users.get_user_by_id(...)、Chats.get_chat_by_id(...)、Files.get_file_by_id(...)、Models.get_model_by_id(...)、Functions.get_function_by_id(...)、Tools.get_tool_by_id(...)、Knowledges.get_knowledge_by_id(...),现在都会返回一个可等待(awaitable)对象。调用它们时不使用await,返回的是协程对象,而不是您所期望的数据。 - 任何模型调用的下游方法现在也都是异步的。 从
open_webui.utils.*、open_webui.retrieval.*导入的实用辅助方法、路由器辅助方法、访问控制检查等 — 只要它们直接或间接地触及数据库,现在都已被提升为async def。以往您在调用它们时无需使用await的同步辅助工具,现在几乎都必然需要加await。请仔细核对您插件中从open_webui.*引入的每一项导入。 - 数据库会话现在都是
AsyncSession。 请使用get_async_db_context(或者非共享变体get_async_db),以取代老旧的get_db_context。虽然同步的辅助工具(get_db、SessionLocal)依然存在,但仅供启动时初始化代码专用 — 在请求处理器或插件中调用它们会阻塞事件循环,并与异步连接池产 生冲突。任何直接的查询代码都需要按照 SQLAlchemy 2.0 异步风格进行重写。 - 等待(await)上述任何方法的插件辅助函数本身也必须被声明为
async def。 异步会沿着您的调用链向上延伸:一旦链路中的一个函数变为了协程,那么它的每一个调用者也随之改变,并一直传导至您插件的主入口点。
Pipes、Filters、Actions 以及 Tool 的入口点方法本身在 0.5 及之后版本中就已经设计为异步的了,所以您在定义签名时无需更改参数 — 唯独需要重写的是它们的函数体内部。
✅ 逐步迁移
🔄 1. 对每一个模型方法使用 Await
这是最普遍需要修改的地方。凡是您的插件通过模型类进行读取或写入数据的任何位置,一律添加 await。
以前 (0.8.x):
from open_webui.models.users import Users
from open_webui.models.chats import Chats
def resolve_user(user_id: str):
user = Users.get_user_by_id(user_id)
chats = Chats.get_chat_list_by_user_id(user_id)
return user, chats现在 (0.9.0):
from open_webui.models.users import Users
from open_webui.models.chats import Chats
async def resolve_user(user_id: str):
user = await Users.get_user_by_id(user_id)
chats = await Chats.get_chat_list_by_user_id(user_id)
return user, chats请注意,该辅助函数本身也变为了 async def。它的调用者随之也必须依次 await 调用它 — 异步在您的代码中会往上逐层 传递。
🗄️ 2. 用 get_async_db_context 替换 get_db_context
如果您的插件需要开启自己专属的数据库会话(这在 Tools 中相对罕见,但有些自定义查询的 Tools 会这样做),原来的同步辅助工具已经被废弃。请更换为异步变体。
以前 (0.8.x):
from open_webui.internal.db import get_db_context
from open_webui.models.users import User
def count_active_users():
with get_db_context() as db:
return db.query(User).filter_by(is_active=True).count()现在 (0.9.0):
from sqlalchemy import select, func
from open_webui.internal.db import get_async_db_context
from open_webui.models.users import User
async def count_active_users():
async with get_async_db_context() as db:
result = await db.execute(
select(func.count()).select_from(User).where(User.is_active == True)
)
return result.scalar_one()关键区别:
with变为了async with。- 不再支持使用
db.query(Model)— 请从sqlalchemy导入并使用select(Model),并调用await db.execute(...)。 - 作用于
execute返回的Result之上的.first()/.all()/.count(),分别变为了.scalars().first()、.scalars().all()和.scalar_one()。
如果您依赖类型注解(type hints),会话类型现在是 sqlalchemy.ext.asyncio.AsyncSession,而不再是原来的 sqlalchemy.orm.Session。
SessionLocal、get_db、get_session 以及同步的 save_config / reset_config 路径依然存在,但它们仅被保留用于 Open WebUI 自身的系统启动及数据库迁移代码 — 绝不要在插件、路由处理器或事件循环中运行的任何位置去调用它们。它们会从同步连接池中获取数据库连接,阻塞整个事件循环的运行,且在并发高负载情况下,极易与异步池发生死锁。在系统启动后运行的任何代码中,请始终优先选择异步变体(get_async_db、get_async_db_context、get_async_session、async_save_config、async_reset_config)。
🧩 3. Pipes, Filters 和 Actions
插件的主入口点自 0.5 版本起就已经是异步的了,所以它们的函数签名依然如故。改变的地方仅在于,您在它们内部如何与模型进行数据交互。
以前 (0.8.x) — 一个负责查找调用者信息的 Pipe:
from pydantic import BaseModel
from fastapi import Request
from open_webui.models.users import Users
from open_webui.utils.chat import generate_chat_completion
class Pipe:
async def pipe(
self,
body: dict,
__user__: dict,
__request__: Request,
) -> str:
full_user = Users.get_user_by_id(__user__["id"])
body["model"] = "llama3.2:latest"
return await generate_chat_completion(__request__, body, full_user)现在 (0.9.0):
from pydantic import BaseModel
from fastapi import Request
from open_webui.models.users import Users
from open_webui.utils.chat import generate_chat_completion
class Pipe:
async def pipe(
self,
body: dict,
__user__: dict,
__request__: Request,
) -> str:
full_user = await Users.get_user_by_id(__user__["id"])
body["model"] = "llama3.2:latest"
return await generate_chat_completion(__request__, body, full_user)在这个最简案例中仅仅是一行代码的变更 — 但所有定义在 inlet、outlet、stream 以及 action 中的模型方法调用,都必须进行同样的改造。
🛠️ 4. Tools
Tool 里的方法现在可以被声明为 async def(且如果需要访问数据库,就应当这样做)。对于仅仅调用外部 API 而从不查询 Open WebUI 数据库的 Tools,无需做出任何改动。
以前 (0.8.x) — 一个用于读取文件的 Tool:
from open_webui.models.files import Files
class Tools:
def get_file_preview(self, file_id: str, __user__: dict) -> str:
file = Files.get_file_by_id_and_user_id(file_id, __user__["id"])
return file.data.get("content", "") if file else ""现在 (0.9.0):
from open_webui.models.files import Files
class Tools:
async def get_file_preview(self, file_id: str, __user__: dict) -> str:
file = await Files.get_file_by_id_and_user_id(file_id, __user__["id"])
return file.data.get("content", "") if file else ""