From 3034350f907d3fa6590d2605f63d1ceeb9bf9c0e Mon Sep 17 00:00:00 2001 From: yuWorm Date: Sun, 5 Apr 2026 14:11:44 +0800 Subject: [PATCH 1/6] Enhance plugin system with lifespan registry, setup hooks, and patching utilities - Add global LifespanRegistry for dynamic lifespan registration via decorator or direct call - Implement setup_plugins to auto-invoke plugin __init__.setup(app) with logging - Add patching module for runtime middleware and route replacement - Integrate lifespan registry and setup_plugins into app registrar Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/common/lifespan.py | 51 ++++++++++++++++++++++++++++++++++ backend/core/registrar.py | 10 +++++-- backend/plugin/core.py | 29 +++++++++++++++++++- backend/plugin/patching.py | 56 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 backend/common/lifespan.py create mode 100644 backend/plugin/patching.py diff --git a/backend/common/lifespan.py b/backend/common/lifespan.py new file mode 100644 index 000000000..c6ae60a94 --- /dev/null +++ b/backend/common/lifespan.py @@ -0,0 +1,51 @@ +from collections.abc import Callable +from contextlib import AbstractAsyncContextManager, asynccontextmanager +from typing import Any + +from fastapi import FastAPI + +LifespanFunc = Callable[[FastAPI], AbstractAsyncContextManager[dict[str, Any] | None]] + + +class LifespanRegistry: + """FastAPI lifespan 全局注册器""" + + def __init__(self) -> None: + self._lifespans: list[LifespanFunc] = [] + + def register(self, func: LifespanFunc) -> LifespanFunc: + """作为装饰器或直接调用,注册一个 lifespan 函数""" + self._lifespans.append(func) + return func + + def build(self) -> Callable[[FastAPI], AbstractAsyncContextManager[None]]: + """将所有注册的 lifespan 组合为一个,供 FastAPI 使用""" + lifespans = self._lifespans + + @asynccontextmanager + async def combined_lifespan(app: FastAPI): # noqa: ANN202 + exit_stack: list[AbstractAsyncContextManager[Any]] = [] + state: dict[str, Any] = {} + + try: + for lifespan_fn in lifespans: + ctx = lifespan_fn(app) + result = await ctx.__aenter__() + exit_stack.append(ctx) + if isinstance(result, dict): + state.update(result) + + for k, v in state.items(): + setattr(app.state, k, v) + + yield + + finally: + for ctx in reversed(exit_stack): + await ctx.__aexit__(None, None, None) + + return combined_lifespan + + +# 全局实例 +lifespan_registry = LifespanRegistry() diff --git a/backend/core/registrar.py b/backend/core/registrar.py index bc64058bd..f1713d6b9 100644 --- a/backend/core/registrar.py +++ b/backend/core/registrar.py @@ -12,12 +12,14 @@ from starlette.middleware.authentication import AuthenticationMiddleware from starlette.middleware.cors import CORSMiddleware from starlette.staticfiles import StaticFiles +from starlette.types import Lifespan from starlette_context.middleware import ContextMiddleware from starlette_context.plugins import RequestIdPlugin from backend import __version__ from backend.common.cache.pubsub import cache_pubsub_manager from backend.common.exception.exception_handler import register_exception +from backend.common.lifespan import lifespan_registry from backend.common.log import set_custom_logfile, setup_logging from backend.common.observability.otel import init_otel from backend.common.response.response_code import StandardResponseCode @@ -30,7 +32,7 @@ from backend.middleware.jwt_auth_middleware import JwtAuthMiddleware from backend.middleware.opera_log_middleware import OperaLogMiddleware from backend.middleware.state_middleware import StateMiddleware -from backend.plugin.core import build_final_router +from backend.plugin.core import build_final_router, setup_plugins from backend.utils.demo_mode import demo_site from backend.utils.openapi import ensure_unique_route_names, simplify_operation_ids from backend.utils.serializers import MsgSpecJSONResponse @@ -38,6 +40,7 @@ from backend.utils.trace_id import OtelTraceIdPlugin +@lifespan_registry.register @asynccontextmanager async def register_init(app: FastAPI) -> AsyncGenerator[None, None]: """ @@ -84,7 +87,7 @@ def register_app() -> FastAPI: redoc_url=settings.FASTAPI_REDOC_URL, openapi_url=settings.FASTAPI_OPENAPI_URL, default_response_class=MsgSpecJSONResponse, - lifespan=register_init, + lifespan=lifespan_registry.build(), ) # 注册组件 @@ -96,6 +99,9 @@ def register_app() -> FastAPI: register_page(app) register_exception(app) + # 初始化所有插件 + setup_plugins(app) + if settings.GRAFANA_METRICS_ENABLE: register_metrics(app) diff --git a/backend/plugin/core.py b/backend/plugin/core.py index cceb60de0..ff375eade 100644 --- a/backend/plugin/core.py +++ b/backend/plugin/core.py @@ -8,7 +8,7 @@ import anyio import rtoml -from fastapi import APIRouter, Depends, Request +from fastapi import APIRouter, Depends, FastAPI, Request from backend.common.enums import DataBaseType, PluginLevelType, PrimaryKeyType, StatusType from backend.common.exception import errors @@ -288,6 +288,33 @@ def build_final_router() -> APIRouter: return main_router +def setup_plugins(app: FastAPI) -> None: + """初始化插件""" + plugins = get_plugins() + + for plugin in plugins: + module_path = f'backend.plugin.{plugin}' + try: + module = import_module_cached(module_path) + except Exception as e: + log.warning('插件 {} 加载失败: {}', plugin, e) + continue + + setup_func = getattr(module, 'setup', None) + if setup_func is None: + continue + + if not callable(setup_func): + log.warning('插件 {} 的 setup 不是可调用对象,已跳过', plugin) + continue + + try: + setup_func(app) + log.info('插件 {} 初始化成功', plugin) + except Exception as e: + log.error('插件 {} 初始化失败: {}', plugin, e) + + class PluginStatusChecker: """插件状态检查器""" diff --git a/backend/plugin/patching.py b/backend/plugin/patching.py new file mode 100644 index 000000000..53399eec8 --- /dev/null +++ b/backend/plugin/patching.py @@ -0,0 +1,56 @@ +from collections.abc import Callable +from typing import Any + +from fastapi import FastAPI +from fastapi.routing import APIRoute +from starlette.middleware import Middleware + + +def replace_middleware( + app: FastAPI, # 目标 FastAPI 实例 + src: type, # 要被替换的中间件类 + target: type, # 替换后的中间件类 + **target_kwargs, # 传给 target 的初始化参数,完全重新构造,不继承 src 的原有 kwargs +) -> None: + """ + 将 app.user_middleware 中的 src 中间件替换为 target。 + + 应在首次请求到达前调用,此时 middleware_stack 尚未构建, + 替换结果即为最终编译态。若 src 不存在则抛出 ValueError。 + """ + for i, m in enumerate(app.user_middleware): + if m.cls is src: + app.user_middleware[i] = Middleware(target, **target_kwargs) + return + raise ValueError(f'{src.__name__} not found in app.user_middleware') + + +def replace_route( + app: FastAPI, # 目标 FastAPI 实例 + src_path: str, # 要被替换的路由路径,如 "/health" + target_endpoint: Callable[..., Any], # 替换后的 endpoint 函数 + methods: set[str] | None = None, # 限定匹配的 HTTP 方法,None 表示仅匹配路径 + **target_kwargs, # 透传给 APIRoute 的额外参数,如 dependencies +) -> bool: + """ + 将 app.router.routes 中匹配 src_path 的路由 endpoint 替换为 target_endpoint。 + + 路由列表无编译冻结,替换后立即对新请求生效。同时清除 openapi_schema + 缓存,确保 /docs 同步更新。未找到匹配路由时返回 False,找到并替换后返回 True。 + """ + for i, route in enumerate(app.router.routes): + if not isinstance(route, APIRoute): + continue + path_match = route.path == src_path + method_match = methods is None or route.methods == {m.upper() for m in methods} + if path_match and method_match: + app.router.routes[i] = APIRoute( + path=route.path, + endpoint=target_endpoint, + methods=methods or route.methods, + name=route.name, + **target_kwargs, + ) + app.openapi_schema = None # 清掉 OpenAPI 缓存,避免 /docs 显示旧路由 + return True + return False From 5823853d17e1bf06659eef383e132daef24e1d42 Mon Sep 17 00:00:00 2001 From: yuWorm Date: Sun, 5 Apr 2026 14:17:18 +0800 Subject: [PATCH 2/6] fix: remove registrar.py unused-import --- backend/core/registrar.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/core/registrar.py b/backend/core/registrar.py index f1713d6b9..e076a93c1 100644 --- a/backend/core/registrar.py +++ b/backend/core/registrar.py @@ -12,7 +12,6 @@ from starlette.middleware.authentication import AuthenticationMiddleware from starlette.middleware.cors import CORSMiddleware from starlette.staticfiles import StaticFiles -from starlette.types import Lifespan from starlette_context.middleware import ContextMiddleware from starlette_context.plugins import RequestIdPlugin From bf78f50c2d719ce8cdafa4102183800d7f5790f0 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Tue, 7 Apr 2026 23:29:15 +0800 Subject: [PATCH 3/6] Optimize implementations --- backend/common/lifespan.py | 49 +++++----- backend/core/registrar.py | 8 +- backend/plugin/core.py | 191 ++++++++++++++++++++++++++----------- backend/plugin/patching.py | 59 +++--------- 4 files changed, 180 insertions(+), 127 deletions(-) diff --git a/backend/common/lifespan.py b/backend/common/lifespan.py index c6ae60a94..534f104fc 100644 --- a/backend/common/lifespan.py +++ b/backend/common/lifespan.py @@ -1,5 +1,5 @@ from collections.abc import Callable -from contextlib import AbstractAsyncContextManager, asynccontextmanager +from contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager from typing import Any from fastapi import FastAPI @@ -7,45 +7,46 @@ LifespanFunc = Callable[[FastAPI], AbstractAsyncContextManager[dict[str, Any] | None]] -class LifespanRegistry: - """FastAPI lifespan 全局注册器""" +class LifespanManager: + """FastAPI lifespan 管理器""" def __init__(self) -> None: self._lifespans: list[LifespanFunc] = [] def register(self, func: LifespanFunc) -> LifespanFunc: - """作为装饰器或直接调用,注册一个 lifespan 函数""" - self._lifespans.append(func) + """ + 注册 lifespan hook + + :param func: lifespan hook + :return: + """ + if func not in self._lifespans: + self._lifespans.append(func) return func - def build(self) -> Callable[[FastAPI], AbstractAsyncContextManager[None]]: - """将所有注册的 lifespan 组合为一个,供 FastAPI 使用""" - lifespans = self._lifespans + def build(self) -> LifespanFunc: + """ + 构建组合后的 lifespan hook + + :return: + """ @asynccontextmanager async def combined_lifespan(app: FastAPI): # noqa: ANN202 - exit_stack: list[AbstractAsyncContextManager[Any]] = [] state: dict[str, Any] = {} - - try: - for lifespan_fn in lifespans: - ctx = lifespan_fn(app) - result = await ctx.__aenter__() - exit_stack.append(ctx) + async with AsyncExitStack() as exit_stack: + for lifespan_fn in self._lifespans: + result = await exit_stack.enter_async_context(lifespan_fn(app)) if isinstance(result, dict): state.update(result) - for k, v in state.items(): - setattr(app.state, k, v) - - yield + for key, value in state.items(): + setattr(app.state, key, value) - finally: - for ctx in reversed(exit_stack): - await ctx.__aexit__(None, None, None) + yield state or None return combined_lifespan -# 全局实例 -lifespan_registry = LifespanRegistry() +# 创建 lifespan_manager 单例 +lifespan_manager = LifespanManager() diff --git a/backend/core/registrar.py b/backend/core/registrar.py index e076a93c1..a3e464b52 100644 --- a/backend/core/registrar.py +++ b/backend/core/registrar.py @@ -18,7 +18,7 @@ from backend import __version__ from backend.common.cache.pubsub import cache_pubsub_manager from backend.common.exception.exception_handler import register_exception -from backend.common.lifespan import lifespan_registry +from backend.common.lifespan import lifespan_manager from backend.common.log import set_custom_logfile, setup_logging from backend.common.observability.otel import init_otel from backend.common.response.response_code import StandardResponseCode @@ -39,7 +39,7 @@ from backend.utils.trace_id import OtelTraceIdPlugin -@lifespan_registry.register +@lifespan_manager.register @asynccontextmanager async def register_init(app: FastAPI) -> AsyncGenerator[None, None]: """ @@ -86,7 +86,7 @@ def register_app() -> FastAPI: redoc_url=settings.FASTAPI_REDOC_URL, openapi_url=settings.FASTAPI_OPENAPI_URL, default_response_class=MsgSpecJSONResponse, - lifespan=lifespan_registry.build(), + lifespan=lifespan_manager.build(), ) # 注册组件 @@ -98,7 +98,7 @@ def register_app() -> FastAPI: register_page(app) register_exception(app) - # 初始化所有插件 + # 初始化插件 setup_plugins(app) if settings.GRAFANA_METRICS_ENABLE: diff --git a/backend/plugin/core.py b/backend/plugin/core.py index ff375eade..aa718d3dd 100644 --- a/backend/plugin/core.py +++ b/backend/plugin/core.py @@ -1,3 +1,4 @@ +import inspect import json import os import warnings @@ -12,6 +13,7 @@ from backend.common.enums import DataBaseType, PluginLevelType, PrimaryKeyType, StatusType from backend.common.exception import errors +from backend.common.lifespan import lifespan_manager from backend.common.log import log from backend.core.conf import settings from backend.core.path_conf import PLUGIN_DIR @@ -132,53 +134,131 @@ def load_plugin_config(plugin: str) -> dict[str, Any]: return rtoml.load(f) +def get_plugin_enable(plugin_info: str | None, default_status: int) -> str: + """ + 解析插件启用状态 + + :param plugin_info: 插件缓存信息 + :param default_status: 默认状态值 + :return: + """ + if not plugin_info: + return str(default_status) + + try: + return json.loads(plugin_info)['plugin']['enable'] + except Exception: + return str(default_status) + + +def get_enabled_plugins(plugins: tuple[str, ...] | None = None) -> set[str]: + """ + 获取已启用的插件列表 + + :param plugins: 插件名称列表 + :return: + """ + plugin_names = plugins or get_plugins() + enabled_plugins = set(plugin_names) + + current_redis_client = RedisCli() + run_await(current_redis_client.init)() + + try: + for plugin in plugin_names: + plugin_info = run_await(current_redis_client.get)(f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}') + if get_plugin_enable(plugin_info, StatusType.enable.value) != str(StatusType.enable.value): + enabled_plugins.discard(plugin) + finally: + run_await(current_redis_client.aclose)() + + return enabled_plugins + + +def register_plugin_lifespan_hook(plugin: str, module: Any) -> None: + """ + 注册插件 lifespan hook + + :param plugin: 插件名称 + :param module: 插件 hooks 模块 + :return: + """ + lifespan_hook = getattr(module, 'lifespan', None) + if lifespan_hook is None: + return + + if not callable(lifespan_hook): + log.warning(f'插件 {plugin} 的 lifespan 不是可调用对象,已跳过') + return + + lifespan_manager.register(lifespan_hook) + log.info(f'插件 {plugin} lifespan hook 注册成功') + + +def run_plugin_startup_hook(plugin: str, module: Any, app: FastAPI) -> None: + """ + 执行插件 startup hook + + :param plugin: 插件名称 + :param module: 插件 hooks 模块 + :param app: FastAPI 应用实例 + :return: + """ + setup_hook = getattr(module, 'setup', None) + if setup_hook is None: + return + + if not callable(setup_hook): + log.warning(f'插件 {plugin} 的 setup 不是可调用对象,已跳过') + return + + setup_result = setup_hook(app) + if inspect.isawaitable(setup_result): + run_await(lambda: setup_result)() # type: ignore + log.info(f'插件 {plugin} startup hook 执行成功') + + def parse_plugin_config() -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: - """解析插件配置""" + """ + 解析插件配置 + + :return: + """ extend_plugins = [] app_plugins = [] - plugins = get_plugins() # 使用独立连接 current_redis_client = RedisCli() run_await(current_redis_client.init)() - # 清理未知插件信息 - exclude_keys = [f'{settings.PLUGIN_REDIS_PREFIX}:{key}' for key in plugins] - run_await(current_redis_client.delete_prefix)( - settings.PLUGIN_REDIS_PREFIX, - exclude=exclude_keys, - ) + try: + # 清理未知插件信息 + exclude_keys = [f'{settings.PLUGIN_REDIS_PREFIX}:{key}' for key in plugins] + run_await(current_redis_client.delete_prefix)( + settings.PLUGIN_REDIS_PREFIX, + exclude=exclude_keys, + ) - for plugin in plugins: - data = load_plugin_config(plugin) - plugin_type = validate_plugin_config(plugin, data) - - if plugin_type == PluginLevelType.extend: - extend_plugins.append(data) - else: - app_plugins.append(data) - - # 补充插件信息 - data['plugin']['name'] = plugin - plugin_cache_key = f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}' - plugin_cache_info = run_await(current_redis_client.get)(plugin_cache_key) - if plugin_cache_info: - try: - data['plugin']['enable'] = json.loads(plugin_cache_info)['plugin']['enable'] - except Exception: - data['plugin']['enable'] = str(StatusType.enable.value) - else: - data['plugin']['enable'] = str(StatusType.enable.value) + for plugin in plugins: + plugin_config = load_plugin_config(plugin) + plugin_type = validate_plugin_config(plugin, plugin_config) - # 缓存最新插件信息 - run_await(current_redis_client.set)(plugin_cache_key, json.dumps(data, ensure_ascii=False)) + if plugin_type == PluginLevelType.extend: + extend_plugins.append(plugin_config) + else: + app_plugins.append(plugin_config) - # 重置插件变更状态 - run_await(current_redis_client.delete)(f'{settings.PLUGIN_REDIS_PREFIX}:changed') + plugin_config['plugin']['name'] = plugin + plugin_cache_key = f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}' + plugin_cache_info = run_await(current_redis_client.get)(plugin_cache_key) + plugin_config['plugin']['enable'] = get_plugin_enable(plugin_cache_info, StatusType.enable.value) - # 关闭连接 - run_await(current_redis_client.aclose)() + run_await(current_redis_client.set)(plugin_cache_key, json.dumps(plugin_config, ensure_ascii=False)) + + run_await(current_redis_client.delete)(f'{settings.PLUGIN_REDIS_PREFIX}:changed') + finally: + run_await(current_redis_client.aclose)() return extend_plugins, app_plugins @@ -289,30 +369,38 @@ def build_final_router() -> APIRouter: def setup_plugins(app: FastAPI) -> None: - """初始化插件""" + """ + 注册并执行插件 hooks + + :param app: FastAPI 应用实例 + :return: + """ plugins = get_plugins() + enabled_plugins = get_enabled_plugins(plugins) for plugin in plugins: - module_path = f'backend.plugin.{plugin}' - try: - module = import_module_cached(module_path) - except Exception as e: - log.warning('插件 {} 加载失败: {}', plugin, e) + if plugin not in enabled_plugins: + log.info(f'插件 {plugin} 未启用,已跳过 hooks 注册与执行') continue - setup_func = getattr(module, 'setup', None) - if setup_func is None: + module_path = f'backend.plugin.{plugin}.hooks' + try: + module = import_module_cached(module_path) + except ModuleNotFoundError as e: + if e.name == module_path: + # 未定义 hooks.py + continue + log.warning(f'插件 {plugin} hooks 模块加载失败: {e}') continue - - if not callable(setup_func): - log.warning('插件 {} 的 setup 不是可调用对象,已跳过', plugin) + except Exception as e: + log.warning(f'插件 {plugin} hooks 模块加载失败: {e}') continue try: - setup_func(app) - log.info('插件 {} 初始化成功', plugin) + register_plugin_lifespan_hook(plugin, module) + run_plugin_startup_hook(plugin, module, app) except Exception as e: - log.error('插件 {} 初始化失败: {}', plugin, e) + log.error(f'插件 {plugin} hooks 执行失败: {e}') class PluginStatusChecker: @@ -339,10 +427,5 @@ async def __call__(self, request: Request) -> None: log.error('插件状态未初始化或丢失,需重启服务自动修复') raise PluginInjectError('插件状态未初始化或丢失,请联系系统管理员') - try: - is_enabled = int(json.loads(plugin_info)['plugin']['enable']) - except Exception: - is_enabled = 0 - - if not is_enabled: + if get_plugin_enable(plugin_info, StatusType.disable.value) != str(StatusType.enable.value): raise errors.ServerError(msg=f'插件 {self.plugin} 未启用,请联系系统管理员') diff --git a/backend/plugin/patching.py b/backend/plugin/patching.py index 53399eec8..d876d278b 100644 --- a/backend/plugin/patching.py +++ b/backend/plugin/patching.py @@ -1,56 +1,25 @@ -from collections.abc import Callable -from typing import Any - from fastapi import FastAPI -from fastapi.routing import APIRoute from starlette.middleware import Middleware def replace_middleware( - app: FastAPI, # 目标 FastAPI 实例 - src: type, # 要被替换的中间件类 - target: type, # 替换后的中间件类 - **target_kwargs, # 传给 target 的初始化参数,完全重新构造,不继承 src 的原有 kwargs + app: FastAPI, + original_middleware_cls: type, + replacement_middleware_cls: type, + **replacement_kwargs, ) -> None: """ - 将 app.user_middleware 中的 src 中间件替换为 target。 + 替换中间件(应在插件的 startup hook 或 lifespan hook 中调用) - 应在首次请求到达前调用,此时 middleware_stack 尚未构建, - 替换结果即为最终编译态。若 src 不存在则抛出 ValueError。 + :param app: FastAPI 应用实例 + :param original_middleware_cls: 原始中间件类 + :param replacement_middleware_cls: 替换后的中间件类 + :param replacement_kwargs: 传给替换后中间件的初始化参数 + :return: """ - for i, m in enumerate(app.user_middleware): - if m.cls is src: - app.user_middleware[i] = Middleware(target, **target_kwargs) + for index, middleware in enumerate(app.user_middleware): + if middleware.cls is original_middleware_cls: + app.user_middleware[index] = Middleware(replacement_middleware_cls, **replacement_kwargs) return - raise ValueError(f'{src.__name__} not found in app.user_middleware') - -def replace_route( - app: FastAPI, # 目标 FastAPI 实例 - src_path: str, # 要被替换的路由路径,如 "/health" - target_endpoint: Callable[..., Any], # 替换后的 endpoint 函数 - methods: set[str] | None = None, # 限定匹配的 HTTP 方法,None 表示仅匹配路径 - **target_kwargs, # 透传给 APIRoute 的额外参数,如 dependencies -) -> bool: - """ - 将 app.router.routes 中匹配 src_path 的路由 endpoint 替换为 target_endpoint。 - - 路由列表无编译冻结,替换后立即对新请求生效。同时清除 openapi_schema - 缓存,确保 /docs 同步更新。未找到匹配路由时返回 False,找到并替换后返回 True。 - """ - for i, route in enumerate(app.router.routes): - if not isinstance(route, APIRoute): - continue - path_match = route.path == src_path - method_match = methods is None or route.methods == {m.upper() for m in methods} - if path_match and method_match: - app.router.routes[i] = APIRoute( - path=route.path, - endpoint=target_endpoint, - methods=methods or route.methods, - name=route.name, - **target_kwargs, - ) - app.openapi_schema = None # 清掉 OpenAPI 缓存,避免 /docs 显示旧路由 - return True - return False + raise ValueError(f'{original_middleware_cls.__name__} not found in app.user_middleware') From 402c18daec5277d8e1c68dcdfefed18ba7e1fc83 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Tue, 7 Apr 2026 23:34:04 +0800 Subject: [PATCH 4/6] Update function docs --- backend/plugin/core.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/backend/plugin/core.py b/backend/plugin/core.py index aa718d3dd..333cc1cfc 100644 --- a/backend/plugin/core.py +++ b/backend/plugin/core.py @@ -219,11 +219,7 @@ def run_plugin_startup_hook(plugin: str, module: Any, app: FastAPI) -> None: def parse_plugin_config() -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: - """ - 解析插件配置 - - :return: - """ + """解析插件配置""" extend_plugins = [] app_plugins = [] plugins = get_plugins() From dfaa5703d8c68211df99c4f5abeaa8b42ecf53c6 Mon Sep 17 00:00:00 2001 From: yuWorm Date: Wed, 8 Apr 2026 00:28:24 +0800 Subject: [PATCH 5/6] update replace_middleware func docs --- backend/plugin/patching.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/plugin/patching.py b/backend/plugin/patching.py index d876d278b..dc7477e59 100644 --- a/backend/plugin/patching.py +++ b/backend/plugin/patching.py @@ -9,7 +9,7 @@ def replace_middleware( **replacement_kwargs, ) -> None: """ - 替换中间件(应在插件的 startup hook 或 lifespan hook 中调用) + 替换中间件(应在插件的 startup hook 中调用) :param app: FastAPI 应用实例 :param original_middleware_cls: 原始中间件类 From aed579d347faa5e9a94582ab442293b8a86c2885 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Wed, 8 Apr 2026 10:31:41 +0800 Subject: [PATCH 6/6] Recover deleted comments --- backend/plugin/core.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/plugin/core.py b/backend/plugin/core.py index 333cc1cfc..132911d8c 100644 --- a/backend/plugin/core.py +++ b/backend/plugin/core.py @@ -245,13 +245,16 @@ def parse_plugin_config() -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: else: app_plugins.append(plugin_config) + # 补充插件信息 plugin_config['plugin']['name'] = plugin plugin_cache_key = f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}' plugin_cache_info = run_await(current_redis_client.get)(plugin_cache_key) plugin_config['plugin']['enable'] = get_plugin_enable(plugin_cache_info, StatusType.enable.value) + # 缓存最新插件信息 run_await(current_redis_client.set)(plugin_cache_key, json.dumps(plugin_config, ensure_ascii=False)) + # 重置插件变更状态 run_await(current_redis_client.delete)(f'{settings.PLUGIN_REDIS_PREFIX}:changed') finally: run_await(current_redis_client.aclose)()