跳到主要内容

🚚 迁移指南:升级到 OPL 数据空间0.9.0

本页介绍升级到 OPL 数据空间0.9.0 时,Tools、Functions、Pipes、Filters、Actions 需要关注的破坏性变化。

发生了什么变化

OPL 数据空间0.9.0 的核心改动是:后端数据层从同步全面切换为异步

几乎所有基于数据库的模型方法都变成了 async def,包括但不限于:

  • Users
  • Chats
  • Files
  • Models
  • Functions
  • Tools
  • Knowledge
  • Memories
  • Groups
  • Folders
  • Messages
  • Feedback

现在这些方法都必须 await

在存储层,SQLAlchemy 也切换到了 async 模式:

  • session 现在是 AsyncSession
  • 查询应写成 await db.execute(select(...))
  • 旧的 db.query(...).first() 风格不再适合运行时插件代码
为什么要做这个改动
  • 为了更高并发
  • 为了让 request handler 与数据层的 async 设计保持一致
  • 为了跟上 SQLAlchemy 2.0 的长期支持路径

破坏性变化

如果你的插件:

  • 直接访问 OPL 数据空间数据模型
  • 或调用任何最终会访问数据模型的 open_webui.* helper

那么你就需要更新。

1. 所有模型方法都变成 coroutine

例如这些调用都必须加 await

  • 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(...)

不加 await,你拿到的不是数据,而是 coroutine 对象。

2. 下游 helper 也会连带变成 async

大量 open_webui.utils.*open_webui.retrieval.*、路由 helper、权限检查 helper,只要内部会碰数据库,也都被提升成了 async def

所以迁移时不要只搜 Users. / Chats.,也要重新审查你所有 open_webui.* import。

3. 数据库 session 变成 AsyncSession

如果你的插件自己打开数据库 session:

  • 不再使用 get_db_context
  • 改用 get_async_db_context

4. async 会一路向上传播

一旦某个 helper 需要 await,它的调用者也必须变成 async def,然后调用者的调用者也一样。最终整个调用链都会被 async 化。

插件入口方法本身在 0.5 以后通常已经是 async 了,所以签名通常不用改,主要改的是内部实现。

迁移步骤

1. 给所有模型方法调用加 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_db_context 改成 get_async_db_context

旧写法(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()

迁移要点:

  • withasync with
  • db.query(...)select(...) + await db.execute(...)
  • .count() / .first() / .all() 也要按 SQLAlchemy 2.0 async 风格重写
把同步 DB helper 当成内部启动代码

SessionLocalget_dbget_session 以及同步版 save_config / reset_config 仍然存在,但它们只应该用于启动阶段内部流程,不应该在插件、路由或事件循环中的运行时代码里继续使用。

3. Pipe / Filter / Action 的变化

这些入口方法的签名通常不用改,但方法体里凡是读取模型数据的地方,都要补 await

旧写法

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)

新写法

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)

4. Tool 的变化

如果 Tool 只调外部 API,不碰 OPL 数据空间数据库,可能几乎不用改。

但一旦读取文件、用户、知识库等本地模型数据,就应该改成 async def 并加 await

旧写法

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 ""

新写法

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 dependency 与 Session 类型

如果你的插件暴露了自己的 FastAPI route,并显式依赖数据库 session,那么类型和依赖函数都要改。

旧写法

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)):
    ...

新写法

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 emitter 用法不变

虽然内部 get_event_emitter / get_event_call 的构造逻辑变成了 async,但插件作者看到的 __event_emitter____event_call__ 用法没有变化:

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

这里不用做额外迁移。

7. 第三方库还不支持 async 怎么办

不要直接阻塞事件循环。用线程包装:

import anyio

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

如果有 async-native 替代库,例如 httpx.AsyncClientaiofiles,优先换掉。

总结

  • 所有模型方法调用都要补 await
  • 很多 open_webui.* helper 也会跟着变成 async
  • 一旦某个 helper 变 async,整条调用链都要一起 async 化
  • get_db_context 改成 get_async_db_context
  • 原始查询改写成 SQLAlchemy 2.0 async 风格
  • Session 类型从 Session 变成 AsyncSession
  • 插件入口签名通常不变,主要改内部逻辑
  • __event_emitter__ / __event_call__ 的使用方式不变
  • 避免在事件循环里跑阻塞同步代码

一个很实用的迁移策略是:先全局检索所有 Users. / Chats. / Files. / Models. / Functions. / Tools. / Knowledges. 调用,加上 await,再顺着报错把调用链逐步 async 化。

💬 如果迁移过程中遇到问题,可以到 GitHub 提 issue,或在社区讨论区提问。