跳到主要内容

🚚 迁移指南:升级到 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. 范围。具体而言:

  1. 所有的模型方法现在都是协程(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,返回的是协程对象,而不是您所期望的数据。
  2. 任何模型调用的下游方法现在也都是异步的。open_webui.utils.*open_webui.retrieval.* 导入的实用辅助方法、路由器辅助方法、访问控制检查等 — 只要它们直接或间接地触及数据库,现在都已被提升为 async def。以往您在调用它们时无需使用 await 的同步辅助工具,现在几乎都必然需要加 await。请仔细核对您插件中从 open_webui.* 引入的每一项导入。
  3. 数据库会话现在都是 AsyncSession 请使用 get_async_db_context(或者非共享变体 get_async_db),以取代老旧的 get_db_context。虽然同步的辅助工具(get_dbSessionLocal)依然存在,但仅供启动时初始化代码专用 — 在请求处理器或插件中调用它们会阻塞事件循环,并与异步连接池产生冲突。任何直接的查询代码都需要按照 SQLAlchemy 2.0 异步风格进行重写。
  4. 等待(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

始终将同步辅助工具视为内部机制

SessionLocalget_dbget_session 以及同步的 save_config / reset_config 路径依然存在,但它们仅被保留用于 Open WebUI 自身的系统启动及数据库迁移代码 — 绝不要在插件、路由处理器或事件循环中运行的任何位置去调用它们。它们会从同步连接池中获取数据库连接,阻塞整个事件循环的运行,且在并发高负载情况下,极易与异步池发生死锁。在系统启动后运行的任何代码中,请始终优先选择异步变体(get_async_dbget_async_db_contextget_async_sessionasync_save_configasync_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)

在这个最简案例中仅仅是一行代码的变更 — 但所有定义在 inletoutletstream 以及 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 ""

🧵 5. FastAPI 依赖项与会话类型

如果您的插件向外暴露了其专属的 FastAPI 路由(某些高级的 Tools 可能会通过 router 导入来实现此目的),并且声明了数据库会话依赖项,请将依赖项与类型注解进行同步替换。

以前 (0.8.x):

from fastapi import Depends
from sqlalchemy.orm import Session

from open_webui.internal.db import get_session

@router.get("/my-endpoint")
def my_endpoint(db: Session = Depends(get_session)):
    ...

现在 (0.9.0):

from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession

from open_webui.internal.db import get_async_session

@router.get("/my-endpoint")
async def my_endpoint(db: AsyncSession = Depends(get_async_session)):
    ...

📣 6. 事件发射器的调用方式依然相同

虽然本版本中,内部构造器 get_event_emitter / get_event_call 已被改造为异步的,但插件作者对此完全无感知。您在函数签名中拿到的,依然是已经构造完毕的 __event_emitter____event_call__ 可调用对象,并且您的 await 调用写法与之前一模一样:

await __event_emitter__({"type": "status", "data": {...}})
response = await __event_call__({"type": "input", "data": {...}})

在这里不需要修改任何插件代码。


🔁 7. 尚不支持 async 的第三方库

如果您依赖的某个库目前仅暴露出同步 API(例如某个老旧的 HTTP 客户端,或者某个自备阻塞 I/O 的类库),切勿直接调用它以防阻塞事件循环。请将此同步调用包装进单独的线程中:

import anyio

result = await anyio.to_thread.run_sync(legacy_client.fetch, url)

这样能保证在您的同步逻辑于后台工作线程(worker thread)中运行的同时,Open WebUI 的主事件循环能够保持敏捷与畅通。只要存在对应的异步原生替代方案(如 httpx.AsyncClientaiofiles 等),都应优先采用。


🌟 总结

  • 所有的模型方法调用前加 await — 它们现在都只返回协程。
  • 对任何坐落在数据库层之上的 open_webui.* 辅助工具进行相同的改造(包含 utils 的绝大多数模块、知识检索、权限访问控制、路由器辅助函数等)。只要它在以前是同步的且会碰触数据,现在都已变为了异步。
  • 任何等待(await)上述异步调用的函数本身也必须定义为 async def — 异步会穿过并传导至您的整个调用链路。
  • get_async_db_context 替换 get_db_context(在不需要会话共享时,则使用 get_async_db),并使用 SQLAlchemy 2.0 异步规范重写原生 SQL 查询代码(select(...) + await db.execute(...))。
  • 数据库会话类型目前是 AsyncSession,而不再是 Session。在 FastAPI 路由中,依赖注入项需改用 get_async_session 以替换 get_session
  • 视同步辅助工具(SessionLocalget_dbget_session、同步版的 save_config / reset_config)为系统启动和底层迁移专属的内部机制 — 绝不要在任何插件代码或运行于事件循环之中的任何位置调用它们。
  • 插件的入口点签名没有改变 — 改变的仅是内部实现的逻辑。事件发射器(__event_emitter____event_call__)的使用方法与以往完全一致。
  • 不要阻塞事件循环;对于无法避免的同步调用,请使用 anyio.to_thread.run_sync 进行包装。
  • 运维人员注意:确保您的数据库链接 URL 明确指向了异步驱动程序(aiosqlite / asyncpg)。0.9.0 版本中不再支持带有 SQLCipher 加密的 SQLite。

只需进行一次机械式的大扫除(即在每个 Users. / Chats. / Files. / Models. / Functions. / Tools. / Knowledges. 调用前加上 await,并把 async 传导到其所有上游函数中),便可让绝大多数插件完成升级过渡。


💬 有任何问题或反馈? 如果您在升级时遇到了任何麻烦或者有极好的建议,随时欢迎前往 GitHub issue 提交反馈,或者在社区论坛中发帖讨论!

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.