diff --git a/app/api/dependencies/workflow.py b/app/api/dependencies/workflow.py index 4d1896552..087f8f76d 100644 --- a/app/api/dependencies/workflow.py +++ b/app/api/dependencies/workflow.py @@ -8,7 +8,7 @@ from sqlalchemy.orm import Session from app.adapters.external.server import MoviePilotServerHelper from app.api.context import get_async_session, get_host_runtime, get_sync_session -from app.application.scheduling import Scheduler +from app.application.scheduling import get_scheduler from app.application.workflow import ( WorkflowCachePort, WorkflowDefinitionCommand, @@ -25,7 +25,7 @@ def get_workflow_mutation_command( runtime: HostRuntime = Depends(get_host_runtime), ) -> WorkflowMutationCommand: """组装请求级工作流写用例和提交后的调度副作用。""" - scheduler = Scheduler() + scheduler = get_scheduler() workflow_manager = get_workflow_manager() system_config = cast(WorkflowCachePort, runtime.workflow.system_config()) return WorkflowMutationCommand( diff --git a/app/api/endpoints/auth.py b/app/api/endpoints/auth.py index 7b81c39e3..2a5227faf 100644 --- a/app/api/endpoints/auth.py +++ b/app/api/endpoints/auth.py @@ -7,7 +7,7 @@ from app.schemas.token import Token as _SchemaToken from app.schemas.user import AuthProviderInfo as _SchemaAuthProviderInfo from app.api.response import RAW_RESPONSE_OPENAPI_KEY, ResponseAPIRouter from app.application.security.auth import AuthService, consume_plugin_auth_ticket -from app.application.plugin.runtime import get_plugin_manager as PluginManager +from app.application.plugin.runtime import get_plugin_manager from app.api.dependencies.auth import get_auth_service router = ResponseAPIRouter() @@ -52,7 +52,7 @@ def auth_providers(service: AuthService = Depends(get_auth_service)) -> list[dic :return: 认证提供方摘要列表 """ providers = _system_auth_providers(service) - providers.extend(PluginManager().get_plugin_auth_providers()) + providers.extend(get_plugin_manager().get_plugin_auth_providers()) return [provider for provider in providers if provider.get("enabled", True)] diff --git a/app/api/endpoints/dashboard.py b/app/api/endpoints/dashboard.py index 410546339..ef2eba3d1 100644 --- a/app/api/endpoints/dashboard.py +++ b/app/api/endpoints/dashboard.py @@ -24,7 +24,7 @@ from app.api.dependencies.history import get_dashboard_query_service from app.application.dashboard import DashboardQueryService from app.schemas.types import StorageAction from app.application.directory import DirectoryHelper -from app.application.scheduling import Scheduler +from app.application.scheduling import get_scheduler from app.adapters.system.host import SystemUtils router = ResponseAPIRouter() @@ -180,7 +180,7 @@ async def schedule(_: Any = Depends(get_current_active_superuser)) -> Any: 查询后台服务信息 """ # 同步 list() 内含同步进度读取,放到线程池执行避免阻塞事件循环 - return await run_in_threadpool(Scheduler().list) + return await run_in_threadpool(get_scheduler().list) @router.get( @@ -195,7 +195,7 @@ async def schedule_progress( 查询指定后台服务的执行进度。 """ # 异步进度后端读取,避免同步 Redis 调用阻塞事件循环 - progress = await Scheduler().aget_progress(job_id) + progress = await get_scheduler().aget_progress(job_id) if not progress: return _SchemaResponse(success=False, message="后台服务不存在") return _SchemaResponse(success=True, data=progress.model_dump()) @@ -211,7 +211,7 @@ async def schedule2(_: Annotated[str, Depends(verify_apitoken)]) -> Any: 查询下载器信息 API_TOKEN认证(?token=xxx) """ # 同步 list() 内含同步进度读取,放到线程池执行避免阻塞事件循环 - return await run_in_threadpool(Scheduler().list) + return await run_in_threadpool(get_scheduler().list) @router.get( @@ -226,7 +226,7 @@ async def schedule_progress2( 查询指定后台服务的执行进度 API_TOKEN认证(?token=xxx) """ # 异步进度后端读取,避免同步 Redis 调用阻塞事件循环 - progress = await Scheduler().aget_progress(job_id) + progress = await get_scheduler().aget_progress(job_id) if not progress: return _SchemaResponse(success=False, message="后台服务不存在") return _SchemaResponse(success=True, data=progress.model_dump()) diff --git a/app/api/endpoints/plugin.py b/app/api/endpoints/plugin.py index 41223df79..98bdd96fe 100644 --- a/app/api/endpoints/plugin.py +++ b/app/api/endpoints/plugin.py @@ -35,7 +35,7 @@ from app.application.commands import init_commands from app.application.scheduling import remove_plugin_job, update_plugin_job from app.runtime.cache import async_fresh from app.application.configuration import get_api_runtime_config_snapshot -from app.application.plugin.runtime import get_plugin_manager as PluginManager +from app.application.plugin.runtime import PluginRuntime, get_plugin_manager from app.runtime.extensions.plugin.contracts import ( PluginDashboardError, PluginNotFoundError, @@ -71,7 +71,7 @@ _plugin_release_refresh_tasks: set[asyncio.Task] = set() async def _get_market_plugin_from_repo( - plugin_manager: PluginManager, + plugin_manager: PluginRuntime, plugin_id: str, repo_url: str, force: bool, @@ -175,7 +175,7 @@ def _is_plugin_auth_remote_file(plugin_id: str, filepath: str) -> bool: """ path = filepath.lstrip("/") normalized_plugin_id = plugin_id.lower() - plugin_manager = PluginManager() + plugin_manager = get_plugin_manager() for provider in plugin_manager.get_plugin_auth_providers(): remote = provider.get("remote") or {} if str(remote.get("id") or "").lower() != normalized_plugin_id: @@ -214,7 +214,7 @@ async def _get_plugin_history_detail( """ 按需获取插件远端元数据,避免插件列表加载时批量访问网络。 """ - plugin_manager = PluginManager() + plugin_manager = get_plugin_manager() installed_plugin = next( ( plugin @@ -266,7 +266,7 @@ async def all_plugins( 查询所有插件清单,包括本地插件和在线插件,插件状态:installed, market, all """ # 本地插件 - plugin_manager = PluginManager() + plugin_manager = get_plugin_manager() local_plugins = plugin_manager.get_local_plugins() # 已安装插件 installed_plugins = [plugin for plugin in local_plugins if plugin.installed] @@ -332,7 +332,7 @@ async def runtime_status( _: ApiPrincipal = Depends(get_current_active_superuser_async), ) -> _SchemaPluginRuntimeSummary: """返回插件页轮询所需的轻量状态摘要。""" - plugin_manager = PluginManager() + plugin_manager = get_plugin_manager() statuses = plugin_manager.get_plugin_runtime_statuses() pending = { _SchemaPluginRuntimeStatus.SOURCE_MISSING, @@ -394,7 +394,7 @@ async def plugin_releases( "items": [], } - plugin_manager = PluginManager() + plugin_manager = get_plugin_manager() market_plugin = await _get_market_plugin_from_repo( plugin_manager, plugin_id, repo_url, force ) @@ -523,7 +523,7 @@ def reload_plugin( """ 重新加载插件 """ - plugin_manager = PluginManager() + plugin_manager = get_plugin_manager() try: with plugin_manager.mutation(f"重载插件 {plugin_id}"): # 重新加载插件 @@ -557,7 +557,7 @@ async def install( """ plugin_helper = PluginHelper() package_manager = PluginPackageManager(plugin_helper) - plugin_manager = PluginManager() + plugin_manager = get_plugin_manager() async def save_installed_plugins(plugin_ids: List[str]) -> object: """保存安装用例确认后的插件列表。""" @@ -582,7 +582,9 @@ async def install( async def reload_runtime(target_id: str) -> object: """在线程池中重建源插件及其全部虚拟实例。""" - return await run_in_threadpool(PluginManager().reload_plugin_tree, target_id) + return await run_in_threadpool( + get_plugin_manager().reload_plugin_tree, target_id + ) async def refresh_registrations(target_id: str) -> object: """在线程池中刷新源插件及其虚拟实例的全部宿主注册。""" @@ -594,7 +596,7 @@ async def install( SystemConfigKey.UserInstalledPlugins ) or [], installed_plugins_writer=save_installed_plugins, - plugin_ids_provider=lambda: PluginManager().get_plugin_ids(), + plugin_ids_provider=lambda: get_plugin_manager().get_plugin_ids(), compatibility_checker=plugin_helper.async_get_plugin_system_version_check_message, package_installer=install_package, package_checkpointer=package_manager.async_checkpoint, @@ -633,7 +635,7 @@ async def remotes(token: str) -> Any: """ if token != "moviepilot": raise HTTPException(status_code=403, detail="Forbidden") - return PluginManager().get_plugin_remotes() + return get_plugin_manager().get_plugin_remotes() @router.get( @@ -645,7 +647,7 @@ def plugin_sidebar_nav(_: _SchemaTokenPayload = Depends(verify_token)) -> Any: """ 聚合已启用 Vue 插件声明的侧栏入口(get_sidebar_nav),供前端主界面侧栏展示。 """ - return PluginManager().get_plugin_sidebar_nav() + return get_plugin_manager().get_plugin_sidebar_nav() @router.get( @@ -659,7 +661,7 @@ def plugin_form( """ 根据插件ID获取插件配置表单或Vue组件URL """ - plugin_manager = PluginManager() + plugin_manager = get_plugin_manager() plugin_instance = plugin_manager.running_plugins.get(plugin_id) if not plugin_instance: raise HTTPException( @@ -695,7 +697,7 @@ def plugin_page( """ 根据插件ID获取插件数据页面 """ - plugin_instance = PluginManager().running_plugins.get(plugin_id) + plugin_instance = get_plugin_manager().running_plugins.get(plugin_id) if not plugin_instance: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -723,7 +725,7 @@ def plugin_dashboard_meta( """ 获取所有插件仪表板元信息 """ - return PluginManager().get_plugin_dashboard_meta() + return get_plugin_manager().get_plugin_dashboard_meta() @router.get("/dashboard/{plugin_id}/{key}", summary="获取插件仪表板配置") @@ -737,7 +739,7 @@ def plugin_dashboard_by_key( 根据插件ID获取插件仪表板 """ try: - return PluginManager().get_plugin_dashboard(plugin_id, key, user_agent) + return get_plugin_manager().get_plugin_dashboard(plugin_id, key, user_agent) except PluginNotFoundError as error: raise HTTPException(status_code=404, detail=str(error)) from error except PluginDashboardError as error: @@ -804,7 +806,7 @@ async def plugin_static_file( ) raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden") - source_plugin_id = PluginManager().get_plugin_source_id(plugin_id) + source_plugin_id = get_plugin_manager().get_plugin_source_id(plugin_id) plugin_base_dir = ( AsyncPath(get_api_runtime_config_snapshot().root_path) / "app" @@ -897,7 +899,7 @@ async def save_plugin_folders( 保存插件文件夹分组配置 """ try: - with PluginManager().mutation("保存插件文件夹配置"): + with get_plugin_manager().mutation("保存插件文件夹配置"): await get_configured_system_config().async_set( SystemConfigKey.PluginFolders, folders, @@ -920,7 +922,7 @@ async def create_plugin_folder( 创建新的插件文件夹 """ try: - with PluginManager().mutation(f"创建插件文件夹 {folder_name}"): + with get_plugin_manager().mutation(f"创建插件文件夹 {folder_name}"): folders = ( get_configured_system_config().get(SystemConfigKey.PluginFolders) or {} ) @@ -951,7 +953,7 @@ async def delete_plugin_folder( 删除插件文件夹 """ try: - with PluginManager().mutation(f"删除插件文件夹 {folder_name}"): + with get_plugin_manager().mutation(f"删除插件文件夹 {folder_name}"): folders = ( get_configured_system_config().get(SystemConfigKey.PluginFolders) or {} ) @@ -986,7 +988,7 @@ async def update_folder_plugins( 更新指定文件夹中的插件列表 """ try: - with PluginManager().mutation(f"更新插件文件夹 {folder_name}"): + with get_plugin_manager().mutation(f"更新插件文件夹 {folder_name}"): folders = ( get_configured_system_config().get(SystemConfigKey.PluginFolders) or {} ) @@ -1014,7 +1016,7 @@ def clone_plugin( """ 创建插件分身 """ - plugin_manager = PluginManager() + plugin_manager = get_plugin_manager() try: with plugin_manager.mutation(f"创建插件 {plugin_id} 分身"): success, message = plugin_manager.clone_plugin( @@ -1049,7 +1051,7 @@ async def plugin_config( """ 根据插件ID获取插件配置信息 """ - return PluginManager().get_plugin_config(plugin_id) + return get_plugin_manager().get_plugin_config(plugin_id) @router.put("/{plugin_id}", summary="更新插件配置", response_model=_SchemaResponse[None]) @@ -1073,7 +1075,7 @@ def uninstall_plugin( """ 卸载插件 """ - plugin_manager = PluginManager() + plugin_manager = get_plugin_manager() try: with plugin_manager.mutation(f"卸载插件 {plugin_id}"): virtual_instance = plugin_manager.get_plugin_instance(plugin_id) diff --git a/app/api/endpoints/site.py b/app/api/endpoints/site.py index 126fb3d28..8360ecace 100644 --- a/app/api/endpoints/site.py +++ b/app/api/endpoints/site.py @@ -22,7 +22,7 @@ from app.api.endpoints.plugin import register_plugin_api from app.chain.site import SiteChain from app.chain.torrents import TorrentsChain from app.application.commands import init_commands -from app.application.plugin.runtime import get_plugin_manager as PluginManager +from app.application.plugin.runtime import get_plugin_manager from app.adapters.web.security.access import verify_token from app.api.principal import ApiPrincipal from app.application.configuration import get_configured_system_config @@ -39,7 +39,7 @@ from app.api.dependencies.site import ( ) from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module from app.runtime.log import logger -from app.application.scheduling import Scheduler +from app.application.scheduling import get_scheduler from app.schemas.types import SystemConfigKey, MediaType from app.domain import site as site_rules from app.api.context import get_background_task_registry, resolve_background_task_registry @@ -178,7 +178,7 @@ async def cookie_cloud_sync( 运行CookieCloud同步站点信息 """ resolve_background_task_registry(task_registry).create_sync( - Scheduler().start, job_id="cookiecloud", owner="api.site.cookiecloud_sync" + get_scheduler().start, job_id="cookiecloud", owner="api.site.cookiecloud_sync" ) return _SchemaResponse(success=True, message="CookieCloud同步任务已启动!") @@ -196,7 +196,7 @@ async def reset( await get_configured_system_config().async_set(SystemConfigKey.IndexerSites, []) await get_configured_system_config().async_set(SystemConfigKey.RssSites, []) resolve_background_task_registry(task_registry).create_sync( - Scheduler().start, + get_scheduler().start, job_id="cookiecloud", owner="api.site.reset", manual=True, @@ -567,8 +567,8 @@ def auth_site( status, msg = SitesHelper().check_user(auth_info.site, auth_info.params) get_configured_system_config().set(SystemConfigKey.UserSiteAuthParams, auth_info.model_dump()) # 认证成功后,重新初始化插件 - PluginManager().init_config() - Scheduler().init_plugin_jobs() + get_plugin_manager().init_config() + get_scheduler().init_plugin_jobs() init_commands() register_plugin_api() return _SchemaResponse(success=status, message=msg) diff --git a/app/api/endpoints/subscribe.py b/app/api/endpoints/subscribe.py index 077570dc3..8269b9e79 100644 --- a/app/api/endpoints/subscribe.py +++ b/app/api/endpoints/subscribe.py @@ -55,7 +55,7 @@ from app.api.dependencies.subscription import ( get_subscription_sync_mutation_service, ) from app.adapters.external.server import MoviePilotServerHelper -from app.application.scheduling import Scheduler +from app.application.scheduling import get_scheduler from app.runtime.tasks import TaskRegistry from app.schemas.event import SubscribeModifiedEventData from app.schemas.types import ( @@ -376,7 +376,7 @@ def refresh_subscribes( """ if not current_user.is_superuser: return _SchemaResponse(success=False, message="订阅不存在") - Scheduler().start("subscribe_refresh") + get_scheduler().start("subscribe_refresh") return _SchemaResponse(success=True) @@ -418,7 +418,7 @@ def check_subscribes( """ if not current_user.is_superuser: return _SchemaResponse(success=False, message="订阅不存在") - Scheduler().start("subscribe_tmdb") + get_scheduler().start("subscribe_tmdb") return _SchemaResponse(success=True) diff --git a/app/api/endpoints/system.py b/app/api/endpoints/system.py index 197e08ddd..3ec1ef365 100644 --- a/app/api/endpoints/system.py +++ b/app/api/endpoints/system.py @@ -38,7 +38,7 @@ from app.chain.system import SystemChain from app.runtime.config import global_vars from app.runtime.events import eventmanager from app.domain.metainfo import MetaInfo -from app.application.module import ModuleManager +from app.application.module import get_module_manager from app.adapters.web.security.access import verify_apitoken, verify_resource_token, verify_token from app.api.principal import ApiPrincipal from app.application.configuration import ( @@ -66,7 +66,7 @@ from app.application.rules import RuleHelper from app.adapters.external.server import MoviePilotServerHelper from app.runtime.state import SystemHelper from app.runtime.log import logger -from app.application.scheduling import Scheduler +from app.application.scheduling import get_scheduler from app.schemas.event import ConfigChangeEventData from app.schemas.exception import PluginMutationRejectedError from app.schemas.types import SystemConfigKey, EventType @@ -1449,7 +1449,7 @@ def modulelist(_: _SchemaTokenPayload = Depends(verify_token)): 查询已加载的模块ID列表 """ modules = [] - for spec in ModuleManager().list_specs(): + for spec in get_module_manager().list_specs(): module_id = spec.id name = str(spec.metadata["name"]) modules.append( @@ -1473,7 +1473,7 @@ def moduletest(moduleid: str, _: _SchemaTokenPayload = Depends(verify_token)): """ 模块可用性测试接口 """ - state, errmsg = ModuleManager().test(moduleid) + state, errmsg = get_module_manager().test(moduleid) return _SchemaResponse(success=state, message=errmsg) @@ -1514,9 +1514,9 @@ def run_scheduler(jobid: str, _: ApiPrincipal = Depends(get_current_active_super if not jobid: return _SchemaResponse(success=False, message="命令不能为空!") if jobid in {"recommend_refresh", "cookiecloud"}: - Scheduler().start(jobid, manual=True) + get_scheduler().start(jobid, manual=True) else: - Scheduler().start(jobid) + get_scheduler().start(jobid) return _SchemaResponse(success=True) @@ -1531,7 +1531,7 @@ def run_scheduler2(jobid: str, _: Annotated[str, Depends(verify_apitoken)]): return _SchemaResponse(success=False, message="命令不能为空!") if jobid in {"recommend_refresh", "cookiecloud"}: - Scheduler().start(jobid, manual=True) + get_scheduler().start(jobid, manual=True) else: - Scheduler().start(jobid) + get_scheduler().start(jobid) return _SchemaResponse(success=True) diff --git a/app/api/endpoints/workflow.py b/app/api/endpoints/workflow.py index 023737d96..cf08837ae 100644 --- a/app/api/endpoints/workflow.py +++ b/app/api/endpoints/workflow.py @@ -16,7 +16,7 @@ from app.application.workflow import ( get_workflow_manager, ) from app.chain.workflow import WorkflowChain -from app.application.plugin.runtime import get_plugin_manager as PluginManager +from app.application.plugin.runtime import get_plugin_manager from app.api.dependencies.auth import ( get_current_active_manage_user, get_current_active_manage_user_async, @@ -66,7 +66,7 @@ def list_plugin_actions( """ 获取所有动作 """ - return PluginManager().get_plugin_actions(plugin_id) + return get_plugin_manager().get_plugin_actions(plugin_id) @router.get( diff --git a/app/command.py b/app/command.py index 755f6844b..abc822b79 100644 --- a/app/command.py +++ b/app/command.py @@ -12,10 +12,10 @@ from app.chain.subscribe import SubscribeChain from app.chain.system import SystemChain from app.chain.transfer import TransferChain from app.runtime.events import Event as ManagerEvent, eventmanager, Event -from app.application.plugin.runtime import get_plugin_manager as PluginManager +from app.application.plugin.runtime import get_plugin_manager from app.application.messaging.message import MessageHelper from app.application.messaging.skill import SkillInteractionHandler -from app.application.scheduling import Scheduler +from app.application.scheduling import get_scheduler from app.runtime.thread import ThreadHelper from app.runtime.log import logger from app.schemas.message import Message @@ -147,9 +147,9 @@ class Command(metaclass=Singleton): # 初始化锁 self._rlock = threading.RLock() # 插件管理 - self.pluginmanager = PluginManager() + self.pluginmanager = get_plugin_manager() # 定时服务管理 - self.scheduler = Scheduler() + self.scheduler = get_scheduler() # 消息管理器 self.messagehelper = MessageHelper() # 初始化命令 diff --git a/app/factory.py b/app/factory.py index c5e43844f..c17da72c5 100644 --- a/app/factory.py +++ b/app/factory.py @@ -14,7 +14,7 @@ from app.adapters.observability.otel import build_observation_port from app.adapters.web.plugin.routes import FastAPIDynamicRouteRegistry from app.adapters.web.health import install_health_routes from app.application.plugin.routes import configure_plugin_routes -from app.application.plugin.runtime import get_plugin_manager as PluginManager +from app.application.plugin.runtime import get_plugin_manager from app.schemas.exception import ( PersistenceUnavailableError, ) @@ -381,8 +381,8 @@ def create_app() -> FastAPI: # 统一经服务完成,避免 api.endpoints 反向依赖本模块。 configure_plugin_routes(FastAPIDynamicRouteRegistry( app=_app, - plugin_ids=lambda: PluginManager().get_running_plugin_ids(), - plugin_apis=lambda plugin_id: PluginManager().get_plugin_apis(plugin_id), + plugin_ids=lambda: get_plugin_manager().get_running_plugin_ids(), + plugin_apis=lambda plugin_id: get_plugin_manager().get_plugin_apis(plugin_id), verify_token=verify_token, verify_apikey=verify_apikey, prefix=f"{settings.API_V1_STR}/plugin", diff --git a/app/scheduler.py b/app/scheduler.py index ab59f3fa8..5fe0bd287 100644 --- a/app/scheduler.py +++ b/app/scheduler.py @@ -31,7 +31,7 @@ from app.runtime.events import Event, eventmanager from app.db.oper.agenttask import AgentTaskOper from app.application.database import get_database_governance from app.application.outbox import dispatch_pending_outbox -from app.application.plugin.runtime import get_plugin_manager as PluginManager +from app.application.plugin.runtime import get_plugin_manager from app.application.configuration import ( SchedulerRuntimeConfig, get_configured_system_config, @@ -551,7 +551,7 @@ class Scheduler(ConfigReloadMixin, metaclass=SingletonClass): JobSpec("random_wallpager", "壁纸缓存", WallpaperHelper().get_wallpapers, "image"), JobSpec("sitedata_refresh", "站点数据刷新", SiteChain().refresh_userdatas, "site"), JobSpec("recommend_refresh", "推荐缓存", RecommendChain().refresh_recommend, "recommend"), - JobSpec("plugin_market_refresh", "插件市场缓存", PluginManager().async_get_online_plugins, "plugin", kwargs={"force": True}), + JobSpec("plugin_market_refresh", "插件市场缓存", get_plugin_manager().async_get_online_plugins, "plugin", kwargs={"force": True}), JobSpec("subscribe_calendar_cache", "订阅日历缓存", SubscribeChain().cache_calendar, "subscription"), JobSpec("full_gc", "主动内存回收", self.full_gc, "runtime"), JobSpec("agent_heartbeat", "智能体定时任务", self.agent_heartbeat, "agent"), @@ -1608,7 +1608,7 @@ class Scheduler(ConfigReloadMixin, metaclass=SingletonClass): """ 初始化插件定时服务 """ - for pid in PluginManager().get_running_plugin_ids(): + for pid in get_plugin_manager().get_running_plugin_ids(): self.update_plugin_job(pid) @eventmanager.register(EventType.PluginReload) @@ -1684,7 +1684,7 @@ class Scheduler(ConfigReloadMixin, metaclass=SingletonClass): self._jobs.pop(job_id, None) if not jobs_to_remove: return - plugin_name = PluginManager().get_plugin_attr(pid, "plugin_name") + plugin_name = get_plugin_manager().get_plugin_attr(pid, "plugin_name") # 遍历移除任务 for job_id, service in jobs_to_remove: try: @@ -1758,7 +1758,7 @@ class Scheduler(ConfigReloadMixin, metaclass=SingletonClass): self.remove_plugin_job(pid) # 获取插件服务列表 with self._lock: - plugin_manager = PluginManager() + plugin_manager = get_plugin_manager() try: plugin_services = plugin_manager.get_plugin_services(pid=pid) except Exception as e: @@ -2047,7 +2047,7 @@ class Scheduler(ConfigReloadMixin, metaclass=SingletonClass): ) ) # 认证通过后重新初始化插件 - PluginManager().init_config() + get_plugin_manager().init_config() self.init_plugin_jobs() else: diff --git a/app/workflow/actions/invoke_plugin.py b/app/workflow/actions/invoke_plugin.py index a2e77b8ed..6a8dc873b 100644 --- a/app/workflow/actions/invoke_plugin.py +++ b/app/workflow/actions/invoke_plugin.py @@ -1,7 +1,7 @@ from pydantic import Field from app.workflow.actions import BaseAction -from app.application.plugin.runtime import get_plugin_manager as PluginManager +from app.application.plugin.runtime import get_plugin_manager from app.runtime.log import logger from app.schemas.workflow import ActionParams from app.schemas.workflow import ActionContext @@ -43,7 +43,7 @@ class InvokePluginAction(BaseAction): if not params.plugin_id or not params.action_id: return context try: - plugin_actions = PluginManager().get_plugin_actions(params.plugin_id) + plugin_actions = get_plugin_manager().get_plugin_actions(params.plugin_id) if not plugin_actions: logger.error(f"插件不存在: {params.plugin_id}") return context diff --git a/docs/refactor/backend-architecture-next-stage.md b/docs/refactor/backend-architecture-next-stage.md index ea3534cfa..02b9e07d2 100644 --- a/docs/refactor/backend-architecture-next-stage.md +++ b/docs/refactor/backend-architecture-next-stage.md @@ -6,7 +6,7 @@ > 审计范围:宿主后端;排除 `app/plugins/**` 运行时插件副本 > 规范优先级:`AGENTS.md` 与 `docs/rules/` 高于本文 > 相关文档:`docs/architecture-overview.md`、`docs/refactor/backend-architecture-governance.md`、`docs/refactor/backend-module-refactor-compatibility.md` -> 实施进度:阶段 0~6 的宿主架构能力已完成收口;API/Application 公共复杂度基线已清零,启动组合根的 SystemConfigOper 构造点已由 14 降至 1;API 进程内后台任务已完成首批统一登记,插件仓适配和 Outbox 外围扩展仍按风险切片推进。Model/Base 查询与写装饰器、legacy 隐式会话外壳均已清零,插件 SDK 也不再导出宿主 Model。2026-08-23 的长期整改阶段 0 已恢复宿主、启动性能、官方插件和 SDK 契约门禁的可信基线;阶段 1a 已补齐 TaskRegistry owner 零债务门禁和诚实的关停超时语义;阶段 1b1 已收口整理 worker、pending 回放、失败通知、进程内 AI 重试、插件监控与事件投递的生命周期所有权;2026-08-24 的阶段 2 已将 212 个已观察宿主模块方法的 legacy aggregation 清零,并补齐可执行 fanout 与下载器文件 DTO 边界;阶段 3 已将消息交互和远程命令的订阅删除统一到 Application/UoW/outbox,宿主不再调用裸线程统计入口;阶段 4 已统一七种消息渠道的宿主回环与后台执行边界;阶段 5 已补齐事件窗口聚合任务的生命周期所有权;阶段 6 已统一插件文件操作的取消完成语义;阶段 7 已统一插件协程补偿的终态等待;阶段 8 已统一宿主同步函数的异步线程池入口;阶段 9 已统一工作流运行时的宿主获取路径。 +> 实施进度:阶段 0~6 的宿主架构能力已完成收口;API/Application 公共复杂度基线已清零,启动组合根的 SystemConfigOper 构造点已由 14 降至 1;API 进程内后台任务已完成首批统一登记,插件仓适配和 Outbox 外围扩展仍按风险切片推进。Model/Base 查询与写装饰器、legacy 隐式会话外壳均已清零,插件 SDK 也不再导出宿主 Model。2026-08-23 的长期整改阶段 0 已恢复宿主、启动性能、官方插件和 SDK 契约门禁的可信基线;阶段 1a 已补齐 TaskRegistry owner 零债务门禁和诚实的关停超时语义;阶段 1b1 已收口整理 worker、pending 回放、失败通知、进程内 AI 重试、插件监控与事件投递的生命周期所有权;2026-08-24 的阶段 2 已将 212 个已观察宿主模块方法的 legacy aggregation 清零,并补齐可执行 fanout 与下载器文件 DTO 边界;阶段 3 已将消息交互和远程命令的订阅删除统一到 Application/UoW/outbox,宿主不再调用裸线程统计入口;阶段 4 已统一七种消息渠道的宿主回环与后台执行边界;阶段 5 已补齐事件窗口聚合任务的生命周期所有权;阶段 6 已统一插件文件操作的取消完成语义;阶段 7 已统一插件协程补偿的终态等待;阶段 8 已统一宿主同步函数的异步线程池入口;阶段 9 已统一工作流运行时的宿主获取路径;阶段 10 已统一模块、插件与调度运行时的显式 getter 调用。 ## 当前复核结论(2026-08-24) @@ -117,6 +117,18 @@ - 兼容边界不变:`app.workflow.WorkFlowManager` 的类路径、Singleton identity、公开方法、事件监听和 action 加载保持原样,旧插件仍可直接使用 concrete 类;本阶段只收口 canonical 宿主消费者。 +### 长期整改阶段 10:运行时 Facade 获取方式统一(2026-08-24) + +- API、CLI、Scheduler 和工作流动作原先同时存在显式 `get_*` 调用、Application 兼容类构造,以及 + `get_plugin_manager as PluginManager` 的类形别名。canonical 宿主消费者现统一显式调用 + `get_module_manager()`、`get_plugin_manager()` 和 `get_scheduler()`,不再把 Service Locator 伪装成 + concrete 类构造。 +- 架构门禁扫描全部宿主 Python 源码,禁止重新导入 `app.application.module.ModuleManager`、 + `app.application.scheduling.Scheduler`,或为插件 getter 建立别名。startup 仍负责创建 concrete + 管理器并注册 provider,运行时实例身份、初始化顺序和依赖图边均不改变。 +- 兼容边界不变:Application 的 `ModuleManager`、`Scheduler` 类形 Facade 和 concrete 插件管理器类路径 + 继续保留,旧插件、V1/V2/V3 索引加载及 SDK/Compat 映射无需迁移;本阶段仅统一宿主生产路径。 + ### 总体判断 当前架构总体合理,已经从跨层混合的遗留单体收敛为**边界清晰的模块化单体**: diff --git a/docs/rules/05-architecture.md b/docs/rules/05-architecture.md index b50c3b922..bad16c4e6 100644 --- a/docs/rules/05-architecture.md +++ b/docs/rules/05-architecture.md @@ -107,6 +107,12 @@ API dependencies must narrow that object to a domain runtime (for example, `AgentChatRuntime`) instead of adding a string key to a global service map. Legacy registries may delegate the same object while domains migrate, but they must not construct a second set of service instances. +Canonical host consumers of the process-wide module, plugin and scheduler +runtimes must call `get_module_manager()`, `get_plugin_manager()` and +`get_scheduler()` explicitly. The class-shaped `ModuleManager` and `Scheduler` +application facades, and concrete plugin manager class paths, remain compatibility +boundaries for plugins and startup composition; host code must not import those +facades or alias a getter back to a manager class name. API, Scheduler and Chain deployment values are exposed as frozen snapshots from `HostRuntime.configuration`; canonical callers must not add a fresh direct `settings` import when the required field belongs to an existing snapshot. diff --git a/tests/test_agent_scheduled_tasks.py b/tests/test_agent_scheduled_tasks.py index ad8456b7e..9b4855856 100644 --- a/tests/test_agent_scheduled_tasks.py +++ b/tests/test_agent_scheduled_tasks.py @@ -1,3 +1,5 @@ +# pylint: disable=no-name-in-module + import asyncio import json import threading @@ -801,7 +803,7 @@ async def test_dashboard_schedule_keeps_agent_tasks(monkeypatch) -> None: ) ] monkeypatch.setattr( - "app.api.endpoints.dashboard.Scheduler", + "app.api.endpoints.dashboard.get_scheduler", lambda: SimpleNamespace(list=lambda: scheduler_items), ) diff --git a/tests/test_api_authorization.py b/tests/test_api_authorization.py index 9686ae815..2b85f0d8b 100644 --- a/tests/test_api_authorization.py +++ b/tests/test_api_authorization.py @@ -229,7 +229,7 @@ def test_plugin_static_file_requires_resource_token_by_default(monkeypatch): """返回插件认证入口列表。""" return [] - monkeypatch.setattr(plugin_endpoint, "PluginManager", FakePluginManager) + monkeypatch.setattr(plugin_endpoint, "get_plugin_manager", FakePluginManager) monkeypatch.setattr(plugin_endpoint, "verify_resource_token", lambda token: calls.append(token)) plugin_endpoint._verify_plugin_static_file_access( @@ -259,7 +259,7 @@ def test_plugin_auth_remote_files_allow_anonymous_bootstrap(monkeypatch): } ] - monkeypatch.setattr(plugin_endpoint, "PluginManager", FakePluginManager) + monkeypatch.setattr(plugin_endpoint, "get_plugin_manager", FakePluginManager) monkeypatch.setattr(plugin_endpoint, "verify_resource_token", lambda token: calls.append(token)) plugin_endpoint._verify_plugin_static_file_access( diff --git a/tests/test_api_background_task_registry.py b/tests/test_api_background_task_registry.py index 5f64e6bd1..ca44dc433 100644 --- a/tests/test_api_background_task_registry.py +++ b/tests/test_api_background_task_registry.py @@ -134,7 +134,7 @@ def test_cookiecloud_sync_uses_task_registry(monkeypatch) -> None: """CookieCloud 手工同步应登记 Scheduler E1 任务而非 Starlette 后台回调。""" registry = _TaskRegistry() scheduler = SimpleNamespace(start=lambda **_kwargs: None) - monkeypatch.setattr(site, "Scheduler", lambda: scheduler) + monkeypatch.setattr(site, "get_scheduler", lambda: scheduler) response = asyncio.run(site.cookie_cloud_sync(registry, SimpleNamespace())) diff --git a/tests/test_architecture_dependencies.py b/tests/test_architecture_dependencies.py index dde44a258..2b54ff9ac 100644 --- a/tests/test_architecture_dependencies.py +++ b/tests/test_architecture_dependencies.py @@ -352,6 +352,40 @@ def test_host_code_does_not_import_legacy_roots(): assert violations == {} +def test_host_code_uses_explicit_runtime_facade_getters(): + """宿主消费者必须显式调用 getter,不得把兼容 Facade 当作新代码入口。""" + forbidden_imports = { + "app.application.module": {"ModuleManager"}, + "app.application.scheduling": {"Scheduler"}, + } + violations: list[str] = [] + for path in APP_ROOT.rglob("*.py"): + relative = path.relative_to(APP_ROOT) + if relative.parts[0] == "plugins" or relative.parts[:2] == ( + "runtime", + "compat", + ): + continue + tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path)) + for node in ast.walk(tree): + if not isinstance(node, ast.ImportFrom) or not node.module: + continue + forbidden_names = forbidden_imports.get(node.module, set()) + for alias in node.names: + class_shaped_plugin_getter = ( + node.module == "app.application.plugin.runtime" + and alias.name == "get_plugin_manager" + and alias.asname is not None + ) + if alias.name in forbidden_names or class_shaped_plugin_getter: + imported_name = alias.asname or alias.name + violations.append( + f"{relative.as_posix()}:{node.lineno}:{imported_name}" + ) + + assert violations == [] + + def test_plugin_components_do_not_reexport_legacy_abi_names(): """新插件组件只提供 canonical 能力,不得复制旧 Helper、Manager 或 Oper 导出。""" violations: list[str] = [] diff --git a/tests/test_plugin_endpoint.py b/tests/test_plugin_endpoint.py index 50da67f7d..c0b91f162 100644 --- a/tests/test_plugin_endpoint.py +++ b/tests/test_plugin_endpoint.py @@ -47,7 +47,7 @@ def test_plugin_history_merges_remote_metadata(): plugin_manager.get_local_repo_plugins.return_value = [] plugin_manager.async_get_online_plugins = AsyncMock(return_value=[market_plugin]) - with patch("app.api.endpoints.plugin.PluginManager", return_value=plugin_manager): + with patch("app.api.endpoints.plugin.get_plugin_manager", return_value=plugin_manager): result = asyncio.run(plugin_history("DemoPlugin", None, True)) assert result.repo_url == "https://github.com/demo/plugins" @@ -68,7 +68,7 @@ def test_runtime_status_reports_pending_and_terminal_counts(): plugin_manager.is_plugin_settling.return_value = True plugin_manager.get_plugin_runtime_generation.return_value = 7 - with patch("app.api.endpoints.plugin.PluginManager", return_value=plugin_manager): + with patch("app.api.endpoints.plugin.get_plugin_manager", return_value=plugin_manager): result = asyncio.run(runtime_status(None)) assert result.ready is False @@ -82,7 +82,7 @@ def test_reload_endpoint_reports_load_failure(monkeypatch): plugin_manager = MagicMock() plugin_manager.reload_plugin.return_value = PluginRuntimeStatus.LOAD_FAILED register = MagicMock() - monkeypatch.setattr(plugin_endpoint, "PluginManager", lambda: plugin_manager) + monkeypatch.setattr(plugin_endpoint, "get_plugin_manager", lambda: plugin_manager) monkeypatch.setattr(plugin_endpoint, "register_plugin", register) result = reload_plugin("DemoPlugin", None) @@ -107,7 +107,7 @@ def test_plugin_history_returns_installed_plugin_when_remote_missing(): plugin_manager.get_local_repo_plugins.return_value = [] plugin_manager.async_get_online_plugins = AsyncMock(return_value=[]) - with patch("app.api.endpoints.plugin.PluginManager", return_value=plugin_manager): + with patch("app.api.endpoints.plugin.get_plugin_manager", return_value=plugin_manager): result = asyncio.run(plugin_history("DemoPlugin", None, True)) assert result.id == "DemoPlugin" @@ -136,7 +136,7 @@ def test_plugin_history_uses_installed_repo_without_refreshing_all_markets(): plugin_manager.async_get_plugins_from_market = AsyncMock(return_value=[market_plugin]) plugin_manager.async_get_online_plugins = AsyncMock(return_value=[]) - with patch("app.api.endpoints.plugin.PluginManager", return_value=plugin_manager): + with patch("app.api.endpoints.plugin.get_plugin_manager", return_value=plugin_manager): result = asyncio.run(plugin_history("DemoPlugin", None, True)) assert result.history == {"v1.1.0": "- 新增更新说明"} @@ -167,7 +167,7 @@ def test_plugin_releases_returns_supported_versions_with_latest_and_current(monk ]) with ( - patch("app.api.endpoints.plugin.PluginManager", return_value=plugin_manager), + patch("app.api.endpoints.plugin.get_plugin_manager", return_value=plugin_manager), patch("app.api.endpoints.plugin.PluginHelper", return_value=plugin_helper), ): result = asyncio.run(plugin_releases("DemoPlugin", None, "https://github.com/demo/plugins", False)) @@ -206,7 +206,7 @@ def test_plugin_releases_does_not_mutate_cached_release_items(monkeypatch): plugin_helper.async_get_plugin_release_versions = AsyncMock(return_value=release_items) with ( - patch("app.api.endpoints.plugin.PluginManager", return_value=plugin_manager), + patch("app.api.endpoints.plugin.get_plugin_manager", return_value=plugin_manager), patch("app.api.endpoints.plugin.PluginHelper", return_value=plugin_helper), ): result = asyncio.run(plugin_releases("DemoPlugin", None, "https://github.com/demo/plugins", False)) @@ -236,7 +236,7 @@ def test_plugin_releases_falls_back_to_compatible_base_package(monkeypatch): plugin_helper.async_get_plugin_release_versions = AsyncMock(return_value=[]) with ( - patch("app.api.endpoints.plugin.PluginManager", return_value=plugin_manager), + patch("app.api.endpoints.plugin.get_plugin_manager", return_value=plugin_manager), patch("app.api.endpoints.plugin.PluginHelper", return_value=plugin_helper), ): result = asyncio.run( @@ -268,7 +268,7 @@ def test_plugin_releases_uses_force_refresh_for_market_metadata(monkeypatch): plugin_helper.async_get_plugin_release_versions = AsyncMock(return_value=[]) with ( - patch("app.api.endpoints.plugin.PluginManager", return_value=plugin_manager), + patch("app.api.endpoints.plugin.get_plugin_manager", return_value=plugin_manager), patch("app.api.endpoints.plugin.PluginHelper", return_value=plugin_helper), ): result = asyncio.run(plugin_releases("DemoPlugin", None, "https://github.com/demo/plugins", True)) @@ -319,7 +319,7 @@ def test_plugin_releases_force_uses_cached_release_response_and_schedules_refres scheduled.append((plugin_id, repo_url, task_registry)) with ( - patch("app.api.endpoints.plugin.PluginManager", return_value=plugin_manager), + patch("app.api.endpoints.plugin.get_plugin_manager", return_value=plugin_manager), patch("app.api.endpoints.plugin.PluginHelper", return_value=plugin_helper), patch.object(plugin_endpoint, "_schedule_plugin_release_refresh", fake_schedule), ): @@ -369,7 +369,7 @@ def test_plugin_releases_force_skips_background_refresh_without_release_cache(mo scheduled.append((plugin_id, repo_url)) with ( - patch("app.api.endpoints.plugin.PluginManager", return_value=plugin_manager), + patch("app.api.endpoints.plugin.get_plugin_manager", return_value=plugin_manager), patch("app.api.endpoints.plugin.PluginHelper", return_value=plugin_helper), patch.object(plugin_endpoint, "_schedule_plugin_release_refresh", fake_schedule), ): @@ -401,7 +401,7 @@ def test_plugin_releases_hides_items_when_market_plugin_does_not_enable_release( ]) with ( - patch("app.api.endpoints.plugin.PluginManager", return_value=plugin_manager), + patch("app.api.endpoints.plugin.get_plugin_manager", return_value=plugin_manager), patch("app.api.endpoints.plugin.PluginHelper", return_value=plugin_helper), ): result = asyncio.run(plugin_releases("DemoPlugin", None, "https://github.com/demo/plugins", False)) @@ -540,7 +540,7 @@ def test_virtual_instance_static_file_reads_from_source_directory(tmp_path, monk source_file.write_text("export default 'shared'", encoding="utf-8") plugin_manager = MagicMock() plugin_manager.get_plugin_source_id.return_value = "DemoPlugin" - monkeypatch.setattr(plugin_endpoint, "PluginManager", lambda: plugin_manager) + monkeypatch.setattr(plugin_endpoint, "get_plugin_manager", lambda: plugin_manager) monkeypatch.setattr( plugin_endpoint, "get_api_runtime_config_snapshot", @@ -570,7 +570,7 @@ def test_uninstall_virtual_instance_never_removes_source_package(monkeypatch): plugin_manager.get_plugin_source_instances.return_value = [] config = MagicMock() config.get.return_value = ["DemoPlugin"] - monkeypatch.setattr(plugin_endpoint, "PluginManager", lambda: plugin_manager) + monkeypatch.setattr(plugin_endpoint, "get_plugin_manager", lambda: plugin_manager) monkeypatch.setattr(plugin_endpoint, "get_configured_system_config", lambda: config) monkeypatch.setattr(plugin_endpoint, "remove_plugin_api", MagicMock()) monkeypatch.setattr(plugin_endpoint, "remove_plugin_job", MagicMock()) @@ -604,7 +604,7 @@ def test_sealed_http_uninstall_rejects_before_first_side_effect(monkeypatch): config_provider = MagicMock() remove_api = MagicMock() remove_job = MagicMock() - monkeypatch.setattr(plugin_endpoint, "PluginManager", lambda: plugin_manager) + monkeypatch.setattr(plugin_endpoint, "get_plugin_manager", lambda: plugin_manager) monkeypatch.setattr( plugin_endpoint, "get_configured_system_config", @@ -631,7 +631,7 @@ def test_sealed_http_clone_rejects_before_runtime_and_registration(monkeypatch): plugin_manager.mutation.side_effect = admission.hold register = MagicMock() add_to_folder = MagicMock() - monkeypatch.setattr(plugin_endpoint, "PluginManager", lambda: plugin_manager) + monkeypatch.setattr(plugin_endpoint, "get_plugin_manager", lambda: plugin_manager) monkeypatch.setattr(plugin_endpoint, "register_plugin", register) monkeypatch.setattr(plugin_endpoint, "_add_clone_to_plugin_folder", add_to_folder) @@ -655,7 +655,7 @@ def test_sealed_http_folder_update_rejects_before_config_access(monkeypatch): plugin_manager = MagicMock() plugin_manager.mutation.side_effect = admission.hold config_provider = MagicMock() - monkeypatch.setattr(plugin_endpoint, "PluginManager", lambda: plugin_manager) + monkeypatch.setattr(plugin_endpoint, "get_plugin_manager", lambda: plugin_manager) monkeypatch.setattr( plugin_endpoint, "get_configured_system_config", diff --git a/tests/test_plugin_local_sync.py b/tests/test_plugin_local_sync.py index 335354e0f..e863e5d88 100644 --- a/tests/test_plugin_local_sync.py +++ b/tests/test_plugin_local_sync.py @@ -727,7 +727,7 @@ def test_plugin_reload_refreshes_scheduler_services_idempotently(monkeypatch): } ] plugin_manager.get_plugin_attr.return_value = "测试插件" - monkeypatch.setattr("app.scheduler.PluginManager", lambda: plugin_manager) + monkeypatch.setattr("app.scheduler.get_plugin_manager", lambda: plugin_manager) backend = _FakeSchedulerBackend(["DemoPlugin_old"]) scheduler = _build_scheduler_for_plugin_reload( jobs={ diff --git a/tests/test_scheduler_cache_expiry.py b/tests/test_scheduler_cache_expiry.py index 6eb3c1190..3dfe66e94 100644 --- a/tests/test_scheduler_cache_expiry.py +++ b/tests/test_scheduler_cache_expiry.py @@ -86,7 +86,7 @@ def test_clear_cache_is_manual_only(monkeypatch): "TransferChain", "WallpaperHelper", "WorkflowChain", - "PluginManager", + "get_plugin_manager", ]: monkeypatch.setattr(scheduler_module, name, lambda: generic_chain) monkeypatch.setattr( diff --git a/tests/test_site_reset_scheduler.py b/tests/test_site_reset_scheduler.py index c12820284..15eca90dc 100644 --- a/tests/test_site_reset_scheduler.py +++ b/tests/test_site_reset_scheduler.py @@ -30,7 +30,7 @@ async def test_reset_submits_cookiecloud_after_site_transaction(monkeypatch): system_config = Mock() system_config.async_set = AsyncMock() - monkeypatch.setattr(site_endpoint, "Scheduler", Mock(return_value=scheduler)) + monkeypatch.setattr(site_endpoint, "get_scheduler", Mock(return_value=scheduler)) monkeypatch.setattr( site_endpoint, "get_configured_system_config", diff --git a/tests/test_subscribe_endpoint.py b/tests/test_subscribe_endpoint.py index 403e7136a..f5276058f 100644 --- a/tests/test_subscribe_endpoint.py +++ b/tests/test_subscribe_endpoint.py @@ -825,7 +825,7 @@ class SubscribeEndpointTest(TestCase): for endpoint in [refresh_subscribes, check_subscribes]: with self.subTest(endpoint=endpoint.__name__), patch( - "app.api.endpoints.subscribe.Scheduler" + "app.api.endpoints.subscribe.get_scheduler" ) as scheduler: response = endpoint(current_user=regular_user) @@ -838,7 +838,7 @@ class SubscribeEndpointTest(TestCase): (check_subscribes, "subscribe_tmdb"), ]: with self.subTest(endpoint=endpoint.__name__), patch( - "app.api.endpoints.subscribe.Scheduler" + "app.api.endpoints.subscribe.get_scheduler" ) as scheduler: response = endpoint(current_user=superuser) diff --git a/tests/test_system_i18n.py b/tests/test_system_i18n.py index 2f4237356..c29c668f0 100644 --- a/tests/test_system_i18n.py +++ b/tests/test_system_i18n.py @@ -22,7 +22,7 @@ class _FakeModuleManager: def test_system_modulelist_keeps_chinese_name_and_adds_i18n_name(): """模块列表接口应保留旧中文字段,并提供前端可用的多语言字段。""" token = LocaleHelper.set_current_locale("en-US") - with patch.object(system_endpoint, "ModuleManager", return_value=_FakeModuleManager()): + with patch.object(system_endpoint, "get_module_manager", return_value=_FakeModuleManager()): try: response = system_endpoint.modulelist(_="token") finally: @@ -38,7 +38,7 @@ def test_system_modulelist_keeps_chinese_name_and_adds_i18n_name(): def test_system_moduletest_localizes_message(): """模块测试接口应按当前请求语言直接返回翻译后的 message。""" token = LocaleHelper.set_current_locale("en-US") - with patch.object(system_endpoint, "ModuleManager", return_value=_FakeModuleManager()): + with patch.object(system_endpoint, "get_module_manager", return_value=_FakeModuleManager()): try: response = system_endpoint.moduletest("DoubanModule", _="token") finally: diff --git a/tests/test_uvicorn_entrypoint.py b/tests/test_uvicorn_entrypoint.py index 9479d391f..9109df165 100644 --- a/tests/test_uvicorn_entrypoint.py +++ b/tests/test_uvicorn_entrypoint.py @@ -17,7 +17,7 @@ PROJECT_ROOT = Path(__file__).parents[1] def test_create_app_does_not_start_plugin_manager_or_threads(monkeypatch): """ASGI factory 只构建应用结构,不得在创建阶段物化插件运行时。""" plugin_manager = MagicMock(side_effect=AssertionError("plugin runtime started")) - monkeypatch.setattr(factory, "PluginManager", plugin_manager) + monkeypatch.setattr(factory, "get_plugin_manager", plugin_manager) threads_before = threading.active_count() created = factory.create_app()