跳到主要内容

🚚 迁移指南:升级到 Open WebUI 0.9.0

本指南涵盖升级到 Open WebUI 0.9.0 时,Tools、Functions、Pipes、Filters 与 Actions 所涉及的破坏性变更及必要更新。


🧐 发生了什么变化,为什么?

Open WebUI 0.9.0 对内部结构进行了大规模重构:后端数据层已从上到下由同步切换为异步。Users、Chats、Files、Models、Functions、Tools、Knowledge、Memories、Groups、Folders、Messages、Feedback 等几乎所有模型类上的数据库相关方法,现在都变成了 async def,必须通过 await 调用。

在存储层,SQLAlchemy 现在以异步模式工作:session 为 AsyncSession,查询通过 await db.execute(select(...)) 发出,而不是过去的 db.query(...).first()。同步引擎仍保留在内部,但仅供启动阶段任务使用(如配置加载、Alembic / peewee 迁移、健康检查)——所有运行时代码(包括插件)都必须使用异步引擎

📌 为什么要做这次变更?

  • 并发能力: 事件循环中的阻塞式数据库调用,是高负载下最明显的瓶颈。将数据层改为异步后,FastAPI 可以在不耗尽线程池的情况下处理更多并发对话
  • 一致性: 请求处理器本来就是异步的,而数据层过去却是同步的,这迫使整个代码库引入笨重的 run_in_threadpool 包装
  • 面向未来: SQLAlchemy 2.0 的异步 API 才是后续受支持的正式路径

⚠️ 破坏性变更

如果你的插件会访问 Open WebUI 的数据库或模型——或者会调用任何最终会访问它们的辅助函数——你就需要更新代码。由于 open_webui.utils.* 中大量工具函数以及各种 router helper 最终都会读写模型,“异步影响范围”远比表面看到的 Users. / Chats. 调用要更广。具体来说:

  1. 所有模型方法现在都是 coroutine。 任何类似 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,你拿到的将是 coroutine object,而不是实际数据
  2. 任何依赖模型调用的下游 helper 也都变成异步。 你从 open_webui.utils.*open_webui.retrieval.*、router helper、权限校验等模块里导入的函数,只要它们直接或间接访问了数据库,现在几乎都已升级为 async def。请重新检查插件里所有 open_webui.* import
  3. 数据库 session 现在是 AsyncSession 请使用 get_async_db_context(或不共享 session 的 get_async_db)替代旧的 get_db_context。同步 helper(如 get_dbSessionLocal)仍存在,但仅供启动阶段代码使用——如果在请求处理器或插件中调用,会阻塞事件循环并与异步连接池竞争。所有原始查询也需要改写为 SQLAlchemy 2.0 的异步风格
  4. 任何等待上述调用的插件 helper,本身也必须是 async def 一旦调用链中某个函数变成 coroutine,它的所有调用者都必须逐级异步化,直到插件入口

插件入口本身(pipeinletoutletstreamaction 以及 Tool 方法)在 0.5+ 中本来就是异步的,因此签名通常无需变动——需要更新的是函数体。

部署说明——数据库驱动

Open WebUI 现在要求使用与你数据库匹配的异步驱动。标准部署会自动处理:

  • SQLite 使用 aiosqlite(运行时会把同步 sqlite:// URL 改写为 sqlite+aiosqlite://
  • PostgreSQL 使用 asyncpgpostgresql://postgresql+psycopg2://postgres:// 会被改写为 postgresql+asyncpg://
  • SQLCiphersqlite+sqlcipher://)没有异步驱动,因此在 0.9.0 中不受支持。如果你依赖 SQLCipher 做数据库加密,请停留在 0.8.x,或在升级前改用普通 SQLite / Postgres

这主要是运维层面的注意事项,但如果你的插件自己固定了 DB URL,也必须遵循同样的改写规则。


✅ 逐步迁移

🔄 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

请注意,这个 helper 本身也变成了 async def。它的调用者同样必须改成 await,异步会沿着调用链一路向上传播。


🗄️ 2. 用 get_async_db_context 替换 get_db_context

如果你的插件会自己打开数据库 session(虽然不常见,但某些 Tools 会这么做),那么旧的同步 helper 已不再适用,请切换到异步版本。

之前(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) 不再适用——请改用 sqlalchemyselect(Model)await db.execute(...)
  • .first() / .all() / .count() 现在通常要分别改为 .scalars().first().scalars().all().scalar_one(),并作用在 execute() 返回的 Result

如果你使用类型标注,session 类型现在是 sqlalchemy.ext.asyncio.AsyncSession,而不再是 sqlalchemy.orm.Session

将同步 helper 视为内部实现

SessionLocalget_dbget_session 以及同步版 save_config / reset_config 仍然存在,但它们只保留给 Open WebUI 启动和迁移代码使用——不要在插件、route handler 或任何运行于事件循环中的代码里使用它们。它们会从同步连接池获取连接,并在执行时阻塞事件循环,在并发负载下甚至可能与异步连接池互相卡住。运行时请始终优先使用异步版本: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)

在最简单的情况下,可能只是一行差异——但 inletoutletstreamaction 中所有模型调用都必须做同样处理。


🛠️ 4. Tools

Tool 方法可以声明为 async def(如果它会访问数据库,也应该这么做)。如果 Tool 只调用外部 API、完全不访问 Open WebUI 数据库,则无需调整。

之前(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 依赖与 session 类型

如果你的插件暴露了自己的 FastAPI route(少数高级 Tool 会这么做),并且声明了数据库 session 依赖,那么你还需要同时更新依赖函数与类型标注。

之前(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. Event emitters 的调用方式不变

内部构造器 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——它们现在都返回 coroutine
  • 对所有基于数据库的 open_webui.* helper 也一样处理(包括大量 utils、retrieval、权限控制、router helper)。如果它过去是同步的,又会碰数据,那么现在基本已经异步化
  • 任何等待这些 helper 的函数,本身也必须是 async def——异步会沿着你的整个调用链向上传播
  • get_async_db_context 替换 get_db_context(若不需要共享 session,则可用 get_async_db),并把原始查询改成 SQLAlchemy 2.0 的异步写法(select(...) + await db.execute(...)
  • Session 类型现在是 AsyncSession,不是 Session。对于 FastAPI routes,请依赖 get_async_session 而不是 get_session
  • 把同步 helper(SessionLocalget_dbget_session、同步 save_config / reset_config)视为仅供启动阶段使用的内部实现,不要在插件代码或事件循环中使用它们
  • 插件入口签名通常不需要变——真正要改的是函数体内部代码。Event emitters(__event_emitter____event_call__)的使用方式完全不变
  • 不要阻塞事件循环;若无法避免同步调用,就用 anyio.to_thread.run_sync 包起来
  • 运维侧还要确认数据库 URL 已指向异步驱动(aiosqlite / asyncpg)。SQLCipher 加密 SQLite 在 0.9.0 中不受支持

通常来说,做一次机械式全局排查(给所有 Users. / Chats. / Files. / Models. / Functions. / Tools. / Knowledges. 调用前加 await,然后逐级把调用者改成 async),就能解决绝大多数插件迁移问题。


💬 问题或反馈? 如果你在迁移中遇到问题,或有改进建议,欢迎提交 GitHub issue,也可以在社区中提问。

本内容仅供参考,不构成任何保证、担保或合同承诺。Open WebUI 按“现状”提供。请参阅您的许可协议 以了解适用条款。