refactor: use explicit runtime facade getters

This commit is contained in:
jxxghp
2026-08-24 03:56:54 +08:00
parent 9f76fc9dec
commit 4dd2d7fed2
25 changed files with 154 additions and 98 deletions
+2 -2
View File
@@ -8,7 +8,7 @@ from sqlalchemy.orm import Session
from app.adapters.external.server import MoviePilotServerHelper from app.adapters.external.server import MoviePilotServerHelper
from app.api.context import get_async_session, get_host_runtime, get_sync_session 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 ( from app.application.workflow import (
WorkflowCachePort, WorkflowCachePort,
WorkflowDefinitionCommand, WorkflowDefinitionCommand,
@@ -25,7 +25,7 @@ def get_workflow_mutation_command(
runtime: HostRuntime = Depends(get_host_runtime), runtime: HostRuntime = Depends(get_host_runtime),
) -> WorkflowMutationCommand: ) -> WorkflowMutationCommand:
"""组装请求级工作流写用例和提交后的调度副作用。""" """组装请求级工作流写用例和提交后的调度副作用。"""
scheduler = Scheduler() scheduler = get_scheduler()
workflow_manager = get_workflow_manager() workflow_manager = get_workflow_manager()
system_config = cast(WorkflowCachePort, runtime.workflow.system_config()) system_config = cast(WorkflowCachePort, runtime.workflow.system_config())
return WorkflowMutationCommand( return WorkflowMutationCommand(
+2 -2
View File
@@ -7,7 +7,7 @@ from app.schemas.token import Token as _SchemaToken
from app.schemas.user import AuthProviderInfo as _SchemaAuthProviderInfo from app.schemas.user import AuthProviderInfo as _SchemaAuthProviderInfo
from app.api.response import RAW_RESPONSE_OPENAPI_KEY, ResponseAPIRouter from app.api.response import RAW_RESPONSE_OPENAPI_KEY, ResponseAPIRouter
from app.application.security.auth import AuthService, consume_plugin_auth_ticket 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 from app.api.dependencies.auth import get_auth_service
router = ResponseAPIRouter() router = ResponseAPIRouter()
@@ -52,7 +52,7 @@ def auth_providers(service: AuthService = Depends(get_auth_service)) -> list[dic
:return: 认证提供方摘要列表 :return: 认证提供方摘要列表
""" """
providers = _system_auth_providers(service) 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)] return [provider for provider in providers if provider.get("enabled", True)]
+5 -5
View File
@@ -24,7 +24,7 @@ from app.api.dependencies.history import get_dashboard_query_service
from app.application.dashboard import DashboardQueryService from app.application.dashboard import DashboardQueryService
from app.schemas.types import StorageAction from app.schemas.types import StorageAction
from app.application.directory import DirectoryHelper 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 from app.adapters.system.host import SystemUtils
router = ResponseAPIRouter() router = ResponseAPIRouter()
@@ -180,7 +180,7 @@ async def schedule(_: Any = Depends(get_current_active_superuser)) -> Any:
查询后台服务信息 查询后台服务信息
""" """
# 同步 list() 内含同步进度读取,放到线程池执行避免阻塞事件循环 # 同步 list() 内含同步进度读取,放到线程池执行避免阻塞事件循环
return await run_in_threadpool(Scheduler().list) return await run_in_threadpool(get_scheduler().list)
@router.get( @router.get(
@@ -195,7 +195,7 @@ async def schedule_progress(
查询指定后台服务的执行进度。 查询指定后台服务的执行进度。
""" """
# 异步进度后端读取,避免同步 Redis 调用阻塞事件循环 # 异步进度后端读取,避免同步 Redis 调用阻塞事件循环
progress = await Scheduler().aget_progress(job_id) progress = await get_scheduler().aget_progress(job_id)
if not progress: if not progress:
return _SchemaResponse(success=False, message="后台服务不存在") return _SchemaResponse(success=False, message="后台服务不存在")
return _SchemaResponse(success=True, data=progress.model_dump()) 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 查询下载器信息 API_TOKEN认证(?token=xxx
""" """
# 同步 list() 内含同步进度读取,放到线程池执行避免阻塞事件循环 # 同步 list() 内含同步进度读取,放到线程池执行避免阻塞事件循环
return await run_in_threadpool(Scheduler().list) return await run_in_threadpool(get_scheduler().list)
@router.get( @router.get(
@@ -226,7 +226,7 @@ async def schedule_progress2(
查询指定后台服务的执行进度 API_TOKEN认证(?token=xxx 查询指定后台服务的执行进度 API_TOKEN认证(?token=xxx
""" """
# 异步进度后端读取,避免同步 Redis 调用阻塞事件循环 # 异步进度后端读取,避免同步 Redis 调用阻塞事件循环
progress = await Scheduler().aget_progress(job_id) progress = await get_scheduler().aget_progress(job_id)
if not progress: if not progress:
return _SchemaResponse(success=False, message="后台服务不存在") return _SchemaResponse(success=False, message="后台服务不存在")
return _SchemaResponse(success=True, data=progress.model_dump()) return _SchemaResponse(success=True, data=progress.model_dump())
+27 -25
View File
@@ -35,7 +35,7 @@ from app.application.commands import init_commands
from app.application.scheduling import remove_plugin_job, update_plugin_job from app.application.scheduling import remove_plugin_job, update_plugin_job
from app.runtime.cache import async_fresh from app.runtime.cache import async_fresh
from app.application.configuration import get_api_runtime_config_snapshot 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 ( from app.runtime.extensions.plugin.contracts import (
PluginDashboardError, PluginDashboardError,
PluginNotFoundError, PluginNotFoundError,
@@ -71,7 +71,7 @@ _plugin_release_refresh_tasks: set[asyncio.Task] = set()
async def _get_market_plugin_from_repo( async def _get_market_plugin_from_repo(
plugin_manager: PluginManager, plugin_manager: PluginRuntime,
plugin_id: str, plugin_id: str,
repo_url: str, repo_url: str,
force: bool, force: bool,
@@ -175,7 +175,7 @@ def _is_plugin_auth_remote_file(plugin_id: str, filepath: str) -> bool:
""" """
path = filepath.lstrip("/") path = filepath.lstrip("/")
normalized_plugin_id = plugin_id.lower() normalized_plugin_id = plugin_id.lower()
plugin_manager = PluginManager() plugin_manager = get_plugin_manager()
for provider in plugin_manager.get_plugin_auth_providers(): for provider in plugin_manager.get_plugin_auth_providers():
remote = provider.get("remote") or {} remote = provider.get("remote") or {}
if str(remote.get("id") or "").lower() != normalized_plugin_id: 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( installed_plugin = next(
( (
plugin plugin
@@ -266,7 +266,7 @@ async def all_plugins(
查询所有插件清单,包括本地插件和在线插件,插件状态:installed, market, all 查询所有插件清单,包括本地插件和在线插件,插件状态:installed, market, all
""" """
# 本地插件 # 本地插件
plugin_manager = PluginManager() plugin_manager = get_plugin_manager()
local_plugins = plugin_manager.get_local_plugins() local_plugins = plugin_manager.get_local_plugins()
# 已安装插件 # 已安装插件
installed_plugins = [plugin for plugin in local_plugins if plugin.installed] 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), _: ApiPrincipal = Depends(get_current_active_superuser_async),
) -> _SchemaPluginRuntimeSummary: ) -> _SchemaPluginRuntimeSummary:
"""返回插件页轮询所需的轻量状态摘要。""" """返回插件页轮询所需的轻量状态摘要。"""
plugin_manager = PluginManager() plugin_manager = get_plugin_manager()
statuses = plugin_manager.get_plugin_runtime_statuses() statuses = plugin_manager.get_plugin_runtime_statuses()
pending = { pending = {
_SchemaPluginRuntimeStatus.SOURCE_MISSING, _SchemaPluginRuntimeStatus.SOURCE_MISSING,
@@ -394,7 +394,7 @@ async def plugin_releases(
"items": [], "items": [],
} }
plugin_manager = PluginManager() plugin_manager = get_plugin_manager()
market_plugin = await _get_market_plugin_from_repo( market_plugin = await _get_market_plugin_from_repo(
plugin_manager, plugin_id, repo_url, force plugin_manager, plugin_id, repo_url, force
) )
@@ -523,7 +523,7 @@ def reload_plugin(
""" """
重新加载插件 重新加载插件
""" """
plugin_manager = PluginManager() plugin_manager = get_plugin_manager()
try: try:
with plugin_manager.mutation(f"重载插件 {plugin_id}"): with plugin_manager.mutation(f"重载插件 {plugin_id}"):
# 重新加载插件 # 重新加载插件
@@ -557,7 +557,7 @@ async def install(
""" """
plugin_helper = PluginHelper() plugin_helper = PluginHelper()
package_manager = PluginPackageManager(plugin_helper) package_manager = PluginPackageManager(plugin_helper)
plugin_manager = PluginManager() plugin_manager = get_plugin_manager()
async def save_installed_plugins(plugin_ids: List[str]) -> object: 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: 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: async def refresh_registrations(target_id: str) -> object:
"""在线程池中刷新源插件及其虚拟实例的全部宿主注册。""" """在线程池中刷新源插件及其虚拟实例的全部宿主注册。"""
@@ -594,7 +596,7 @@ async def install(
SystemConfigKey.UserInstalledPlugins SystemConfigKey.UserInstalledPlugins
) or [], ) or [],
installed_plugins_writer=save_installed_plugins, 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, compatibility_checker=plugin_helper.async_get_plugin_system_version_check_message,
package_installer=install_package, package_installer=install_package,
package_checkpointer=package_manager.async_checkpoint, package_checkpointer=package_manager.async_checkpoint,
@@ -633,7 +635,7 @@ async def remotes(token: str) -> Any:
""" """
if token != "moviepilot": if token != "moviepilot":
raise HTTPException(status_code=403, detail="Forbidden") raise HTTPException(status_code=403, detail="Forbidden")
return PluginManager().get_plugin_remotes() return get_plugin_manager().get_plugin_remotes()
@router.get( @router.get(
@@ -645,7 +647,7 @@ def plugin_sidebar_nav(_: _SchemaTokenPayload = Depends(verify_token)) -> Any:
""" """
聚合已启用 Vue 插件声明的侧栏入口(get_sidebar_nav),供前端主界面侧栏展示。 聚合已启用 Vue 插件声明的侧栏入口(get_sidebar_nav),供前端主界面侧栏展示。
""" """
return PluginManager().get_plugin_sidebar_nav() return get_plugin_manager().get_plugin_sidebar_nav()
@router.get( @router.get(
@@ -659,7 +661,7 @@ def plugin_form(
""" """
根据插件ID获取插件配置表单或Vue组件URL 根据插件ID获取插件配置表单或Vue组件URL
""" """
plugin_manager = PluginManager() plugin_manager = get_plugin_manager()
plugin_instance = plugin_manager.running_plugins.get(plugin_id) plugin_instance = plugin_manager.running_plugins.get(plugin_id)
if not plugin_instance: if not plugin_instance:
raise HTTPException( raise HTTPException(
@@ -695,7 +697,7 @@ def plugin_page(
""" """
根据插件ID获取插件数据页面 根据插件ID获取插件数据页面
""" """
plugin_instance = PluginManager().running_plugins.get(plugin_id) plugin_instance = get_plugin_manager().running_plugins.get(plugin_id)
if not plugin_instance: if not plugin_instance:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, 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="获取插件仪表板配置") @router.get("/dashboard/{plugin_id}/{key}", summary="获取插件仪表板配置")
@@ -737,7 +739,7 @@ def plugin_dashboard_by_key(
根据插件ID获取插件仪表板 根据插件ID获取插件仪表板
""" """
try: 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: except PluginNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error)) from error raise HTTPException(status_code=404, detail=str(error)) from error
except PluginDashboardError as error: except PluginDashboardError as error:
@@ -804,7 +806,7 @@ async def plugin_static_file(
) )
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden") 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 = ( plugin_base_dir = (
AsyncPath(get_api_runtime_config_snapshot().root_path) AsyncPath(get_api_runtime_config_snapshot().root_path)
/ "app" / "app"
@@ -897,7 +899,7 @@ async def save_plugin_folders(
保存插件文件夹分组配置 保存插件文件夹分组配置
""" """
try: try:
with PluginManager().mutation("保存插件文件夹配置"): with get_plugin_manager().mutation("保存插件文件夹配置"):
await get_configured_system_config().async_set( await get_configured_system_config().async_set(
SystemConfigKey.PluginFolders, SystemConfigKey.PluginFolders,
folders, folders,
@@ -920,7 +922,7 @@ async def create_plugin_folder(
创建新的插件文件夹 创建新的插件文件夹
""" """
try: try:
with PluginManager().mutation(f"创建插件文件夹 {folder_name}"): with get_plugin_manager().mutation(f"创建插件文件夹 {folder_name}"):
folders = ( folders = (
get_configured_system_config().get(SystemConfigKey.PluginFolders) or {} get_configured_system_config().get(SystemConfigKey.PluginFolders) or {}
) )
@@ -951,7 +953,7 @@ async def delete_plugin_folder(
删除插件文件夹 删除插件文件夹
""" """
try: try:
with PluginManager().mutation(f"删除插件文件夹 {folder_name}"): with get_plugin_manager().mutation(f"删除插件文件夹 {folder_name}"):
folders = ( folders = (
get_configured_system_config().get(SystemConfigKey.PluginFolders) or {} get_configured_system_config().get(SystemConfigKey.PluginFolders) or {}
) )
@@ -986,7 +988,7 @@ async def update_folder_plugins(
更新指定文件夹中的插件列表 更新指定文件夹中的插件列表
""" """
try: try:
with PluginManager().mutation(f"更新插件文件夹 {folder_name}"): with get_plugin_manager().mutation(f"更新插件文件夹 {folder_name}"):
folders = ( folders = (
get_configured_system_config().get(SystemConfigKey.PluginFolders) or {} get_configured_system_config().get(SystemConfigKey.PluginFolders) or {}
) )
@@ -1014,7 +1016,7 @@ def clone_plugin(
""" """
创建插件分身 创建插件分身
""" """
plugin_manager = PluginManager() plugin_manager = get_plugin_manager()
try: try:
with plugin_manager.mutation(f"创建插件 {plugin_id} 分身"): with plugin_manager.mutation(f"创建插件 {plugin_id} 分身"):
success, message = plugin_manager.clone_plugin( success, message = plugin_manager.clone_plugin(
@@ -1049,7 +1051,7 @@ async def plugin_config(
""" """
根据插件ID获取插件配置信息 根据插件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]) @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: try:
with plugin_manager.mutation(f"卸载插件 {plugin_id}"): with plugin_manager.mutation(f"卸载插件 {plugin_id}"):
virtual_instance = plugin_manager.get_plugin_instance(plugin_id) virtual_instance = plugin_manager.get_plugin_instance(plugin_id)
+6 -6
View File
@@ -22,7 +22,7 @@ from app.api.endpoints.plugin import register_plugin_api
from app.chain.site import SiteChain from app.chain.site import SiteChain
from app.chain.torrents import TorrentsChain from app.chain.torrents import TorrentsChain
from app.application.commands import init_commands 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.adapters.web.security.access import verify_token
from app.api.principal import ApiPrincipal from app.api.principal import ApiPrincipal
from app.application.configuration import get_configured_system_config 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.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module
from app.runtime.log import logger 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.schemas.types import SystemConfigKey, MediaType
from app.domain import site as site_rules from app.domain import site as site_rules
from app.api.context import get_background_task_registry, resolve_background_task_registry from app.api.context import get_background_task_registry, resolve_background_task_registry
@@ -178,7 +178,7 @@ async def cookie_cloud_sync(
运行CookieCloud同步站点信息 运行CookieCloud同步站点信息
""" """
resolve_background_task_registry(task_registry).create_sync( 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同步任务已启动!") 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.IndexerSites, [])
await get_configured_system_config().async_set(SystemConfigKey.RssSites, []) await get_configured_system_config().async_set(SystemConfigKey.RssSites, [])
resolve_background_task_registry(task_registry).create_sync( resolve_background_task_registry(task_registry).create_sync(
Scheduler().start, get_scheduler().start,
job_id="cookiecloud", job_id="cookiecloud",
owner="api.site.reset", owner="api.site.reset",
manual=True, manual=True,
@@ -567,8 +567,8 @@ def auth_site(
status, msg = SitesHelper().check_user(auth_info.site, auth_info.params) status, msg = SitesHelper().check_user(auth_info.site, auth_info.params)
get_configured_system_config().set(SystemConfigKey.UserSiteAuthParams, auth_info.model_dump()) get_configured_system_config().set(SystemConfigKey.UserSiteAuthParams, auth_info.model_dump())
# 认证成功后,重新初始化插件 # 认证成功后,重新初始化插件
PluginManager().init_config() get_plugin_manager().init_config()
Scheduler().init_plugin_jobs() get_scheduler().init_plugin_jobs()
init_commands() init_commands()
register_plugin_api() register_plugin_api()
return _SchemaResponse(success=status, message=msg) return _SchemaResponse(success=status, message=msg)
+3 -3
View File
@@ -55,7 +55,7 @@ from app.api.dependencies.subscription import (
get_subscription_sync_mutation_service, get_subscription_sync_mutation_service,
) )
from app.adapters.external.server import MoviePilotServerHelper 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.runtime.tasks import TaskRegistry
from app.schemas.event import SubscribeModifiedEventData from app.schemas.event import SubscribeModifiedEventData
from app.schemas.types import ( from app.schemas.types import (
@@ -376,7 +376,7 @@ def refresh_subscribes(
""" """
if not current_user.is_superuser: if not current_user.is_superuser:
return _SchemaResponse(success=False, message="订阅不存在") return _SchemaResponse(success=False, message="订阅不存在")
Scheduler().start("subscribe_refresh") get_scheduler().start("subscribe_refresh")
return _SchemaResponse(success=True) return _SchemaResponse(success=True)
@@ -418,7 +418,7 @@ def check_subscribes(
""" """
if not current_user.is_superuser: if not current_user.is_superuser:
return _SchemaResponse(success=False, message="订阅不存在") return _SchemaResponse(success=False, message="订阅不存在")
Scheduler().start("subscribe_tmdb") get_scheduler().start("subscribe_tmdb")
return _SchemaResponse(success=True) return _SchemaResponse(success=True)
+8 -8
View File
@@ -38,7 +38,7 @@ from app.chain.system import SystemChain
from app.runtime.config import global_vars from app.runtime.config import global_vars
from app.runtime.events import eventmanager from app.runtime.events import eventmanager
from app.domain.metainfo import MetaInfo 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.adapters.web.security.access import verify_apitoken, verify_resource_token, verify_token
from app.api.principal import ApiPrincipal from app.api.principal import ApiPrincipal
from app.application.configuration import ( from app.application.configuration import (
@@ -66,7 +66,7 @@ from app.application.rules import RuleHelper
from app.adapters.external.server import MoviePilotServerHelper from app.adapters.external.server import MoviePilotServerHelper
from app.runtime.state import SystemHelper from app.runtime.state import SystemHelper
from app.runtime.log import logger 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.event import ConfigChangeEventData
from app.schemas.exception import PluginMutationRejectedError from app.schemas.exception import PluginMutationRejectedError
from app.schemas.types import SystemConfigKey, EventType from app.schemas.types import SystemConfigKey, EventType
@@ -1449,7 +1449,7 @@ def modulelist(_: _SchemaTokenPayload = Depends(verify_token)):
查询已加载的模块ID列表 查询已加载的模块ID列表
""" """
modules = [] modules = []
for spec in ModuleManager().list_specs(): for spec in get_module_manager().list_specs():
module_id = spec.id module_id = spec.id
name = str(spec.metadata["name"]) name = str(spec.metadata["name"])
modules.append( 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) return _SchemaResponse(success=state, message=errmsg)
@@ -1514,9 +1514,9 @@ def run_scheduler(jobid: str, _: ApiPrincipal = Depends(get_current_active_super
if not jobid: if not jobid:
return _SchemaResponse(success=False, message="命令不能为空!") return _SchemaResponse(success=False, message="命令不能为空!")
if jobid in {"recommend_refresh", "cookiecloud"}: if jobid in {"recommend_refresh", "cookiecloud"}:
Scheduler().start(jobid, manual=True) get_scheduler().start(jobid, manual=True)
else: else:
Scheduler().start(jobid) get_scheduler().start(jobid)
return _SchemaResponse(success=True) return _SchemaResponse(success=True)
@@ -1531,7 +1531,7 @@ def run_scheduler2(jobid: str, _: Annotated[str, Depends(verify_apitoken)]):
return _SchemaResponse(success=False, message="命令不能为空!") return _SchemaResponse(success=False, message="命令不能为空!")
if jobid in {"recommend_refresh", "cookiecloud"}: if jobid in {"recommend_refresh", "cookiecloud"}:
Scheduler().start(jobid, manual=True) get_scheduler().start(jobid, manual=True)
else: else:
Scheduler().start(jobid) get_scheduler().start(jobid)
return _SchemaResponse(success=True) return _SchemaResponse(success=True)
+2 -2
View File
@@ -16,7 +16,7 @@ from app.application.workflow import (
get_workflow_manager, get_workflow_manager,
) )
from app.chain.workflow import WorkflowChain 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 ( from app.api.dependencies.auth import (
get_current_active_manage_user, get_current_active_manage_user,
get_current_active_manage_user_async, 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( @router.get(
+4 -4
View File
@@ -12,10 +12,10 @@ from app.chain.subscribe import SubscribeChain
from app.chain.system import SystemChain from app.chain.system import SystemChain
from app.chain.transfer import TransferChain from app.chain.transfer import TransferChain
from app.runtime.events import Event as ManagerEvent, eventmanager, Event 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.message import MessageHelper
from app.application.messaging.skill import SkillInteractionHandler 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.thread import ThreadHelper
from app.runtime.log import logger from app.runtime.log import logger
from app.schemas.message import Message from app.schemas.message import Message
@@ -147,9 +147,9 @@ class Command(metaclass=Singleton):
# 初始化锁 # 初始化锁
self._rlock = threading.RLock() self._rlock = threading.RLock()
# 插件管理 # 插件管理
self.pluginmanager = PluginManager() self.pluginmanager = get_plugin_manager()
# 定时服务管理 # 定时服务管理
self.scheduler = Scheduler() self.scheduler = get_scheduler()
# 消息管理器 # 消息管理器
self.messagehelper = MessageHelper() self.messagehelper = MessageHelper()
# 初始化命令 # 初始化命令
+3 -3
View File
@@ -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.plugin.routes import FastAPIDynamicRouteRegistry
from app.adapters.web.health import install_health_routes from app.adapters.web.health import install_health_routes
from app.application.plugin.routes import configure_plugin_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 ( from app.schemas.exception import (
PersistenceUnavailableError, PersistenceUnavailableError,
) )
@@ -381,8 +381,8 @@ def create_app() -> FastAPI:
# 统一经服务完成,避免 api.endpoints 反向依赖本模块。 # 统一经服务完成,避免 api.endpoints 反向依赖本模块。
configure_plugin_routes(FastAPIDynamicRouteRegistry( configure_plugin_routes(FastAPIDynamicRouteRegistry(
app=_app, app=_app,
plugin_ids=lambda: PluginManager().get_running_plugin_ids(), plugin_ids=lambda: get_plugin_manager().get_running_plugin_ids(),
plugin_apis=lambda plugin_id: PluginManager().get_plugin_apis(plugin_id), plugin_apis=lambda plugin_id: get_plugin_manager().get_plugin_apis(plugin_id),
verify_token=verify_token, verify_token=verify_token,
verify_apikey=verify_apikey, verify_apikey=verify_apikey,
prefix=f"{settings.API_V1_STR}/plugin", prefix=f"{settings.API_V1_STR}/plugin",
+6 -6
View File
@@ -31,7 +31,7 @@ from app.runtime.events import Event, eventmanager
from app.db.oper.agenttask import AgentTaskOper from app.db.oper.agenttask import AgentTaskOper
from app.application.database import get_database_governance from app.application.database import get_database_governance
from app.application.outbox import dispatch_pending_outbox 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 ( from app.application.configuration import (
SchedulerRuntimeConfig, SchedulerRuntimeConfig,
get_configured_system_config, get_configured_system_config,
@@ -551,7 +551,7 @@ class Scheduler(ConfigReloadMixin, metaclass=SingletonClass):
JobSpec("random_wallpager", "壁纸缓存", WallpaperHelper().get_wallpapers, "image"), JobSpec("random_wallpager", "壁纸缓存", WallpaperHelper().get_wallpapers, "image"),
JobSpec("sitedata_refresh", "站点数据刷新", SiteChain().refresh_userdatas, "site"), JobSpec("sitedata_refresh", "站点数据刷新", SiteChain().refresh_userdatas, "site"),
JobSpec("recommend_refresh", "推荐缓存", RecommendChain().refresh_recommend, "recommend"), 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("subscribe_calendar_cache", "订阅日历缓存", SubscribeChain().cache_calendar, "subscription"),
JobSpec("full_gc", "主动内存回收", self.full_gc, "runtime"), JobSpec("full_gc", "主动内存回收", self.full_gc, "runtime"),
JobSpec("agent_heartbeat", "智能体定时任务", self.agent_heartbeat, "agent"), 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) self.update_plugin_job(pid)
@eventmanager.register(EventType.PluginReload) @eventmanager.register(EventType.PluginReload)
@@ -1684,7 +1684,7 @@ class Scheduler(ConfigReloadMixin, metaclass=SingletonClass):
self._jobs.pop(job_id, None) self._jobs.pop(job_id, None)
if not jobs_to_remove: if not jobs_to_remove:
return 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: for job_id, service in jobs_to_remove:
try: try:
@@ -1758,7 +1758,7 @@ class Scheduler(ConfigReloadMixin, metaclass=SingletonClass):
self.remove_plugin_job(pid) self.remove_plugin_job(pid)
# 获取插件服务列表 # 获取插件服务列表
with self._lock: with self._lock:
plugin_manager = PluginManager() plugin_manager = get_plugin_manager()
try: try:
plugin_services = plugin_manager.get_plugin_services(pid=pid) plugin_services = plugin_manager.get_plugin_services(pid=pid)
except Exception as e: 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() self.init_plugin_jobs()
else: else:
+2 -2
View File
@@ -1,7 +1,7 @@
from pydantic import Field from pydantic import Field
from app.workflow.actions import BaseAction 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.runtime.log import logger
from app.schemas.workflow import ActionParams from app.schemas.workflow import ActionParams
from app.schemas.workflow import ActionContext from app.schemas.workflow import ActionContext
@@ -43,7 +43,7 @@ class InvokePluginAction(BaseAction):
if not params.plugin_id or not params.action_id: if not params.plugin_id or not params.action_id:
return context return context
try: 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: if not plugin_actions:
logger.error(f"插件不存在: {params.plugin_id}") logger.error(f"插件不存在: {params.plugin_id}")
return context return context
@@ -6,7 +6,7 @@
> 审计范围:宿主后端;排除 `app/plugins/**` 运行时插件副本 > 审计范围:宿主后端;排除 `app/plugins/**` 运行时插件副本
> 规范优先级:`AGENTS.md``docs/rules/` 高于本文 > 规范优先级:`AGENTS.md``docs/rules/` 高于本文
> 相关文档:`docs/architecture-overview.md``docs/refactor/backend-architecture-governance.md``docs/refactor/backend-module-refactor-compatibility.md` > 相关文档:`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 ## 当前复核结论(2026-08-24
@@ -117,6 +117,18 @@
- 兼容边界不变:`app.workflow.WorkFlowManager` 的类路径、Singleton identity、公开方法、事件监听和 - 兼容边界不变:`app.workflow.WorkFlowManager` 的类路径、Singleton identity、公开方法、事件监听和
action 加载保持原样,旧插件仍可直接使用 concrete 类;本阶段只收口 canonical 宿主消费者。 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 映射无需迁移;本阶段仅统一宿主生产路径。
### 总体判断 ### 总体判断
当前架构总体合理,已经从跨层混合的遗留单体收敛为**边界清晰的模块化单体**: 当前架构总体合理,已经从跨层混合的遗留单体收敛为**边界清晰的模块化单体**:
+6
View File
@@ -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. `AgentChatRuntime`) instead of adding a string key to a global service map.
Legacy registries may delegate the same object while domains migrate, but they Legacy registries may delegate the same object while domains migrate, but they
must not construct a second set of service instances. 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 API, Scheduler and Chain deployment values are exposed as frozen snapshots from
`HostRuntime.configuration`; canonical callers must not add a fresh direct `HostRuntime.configuration`; canonical callers must not add a fresh direct
`settings` import when the required field belongs to an existing snapshot. `settings` import when the required field belongs to an existing snapshot.
+3 -1
View File
@@ -1,3 +1,5 @@
# pylint: disable=no-name-in-module
import asyncio import asyncio
import json import json
import threading import threading
@@ -801,7 +803,7 @@ async def test_dashboard_schedule_keeps_agent_tasks(monkeypatch) -> None:
) )
] ]
monkeypatch.setattr( monkeypatch.setattr(
"app.api.endpoints.dashboard.Scheduler", "app.api.endpoints.dashboard.get_scheduler",
lambda: SimpleNamespace(list=lambda: scheduler_items), lambda: SimpleNamespace(list=lambda: scheduler_items),
) )
+2 -2
View File
@@ -229,7 +229,7 @@ def test_plugin_static_file_requires_resource_token_by_default(monkeypatch):
"""返回插件认证入口列表。""" """返回插件认证入口列表。"""
return [] 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)) monkeypatch.setattr(plugin_endpoint, "verify_resource_token", lambda token: calls.append(token))
plugin_endpoint._verify_plugin_static_file_access( 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)) monkeypatch.setattr(plugin_endpoint, "verify_resource_token", lambda token: calls.append(token))
plugin_endpoint._verify_plugin_static_file_access( plugin_endpoint._verify_plugin_static_file_access(
+1 -1
View File
@@ -134,7 +134,7 @@ def test_cookiecloud_sync_uses_task_registry(monkeypatch) -> None:
"""CookieCloud 手工同步应登记 Scheduler E1 任务而非 Starlette 后台回调。""" """CookieCloud 手工同步应登记 Scheduler E1 任务而非 Starlette 后台回调。"""
registry = _TaskRegistry() registry = _TaskRegistry()
scheduler = SimpleNamespace(start=lambda **_kwargs: None) 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())) response = asyncio.run(site.cookie_cloud_sync(registry, SimpleNamespace()))
+34
View File
@@ -352,6 +352,40 @@ def test_host_code_does_not_import_legacy_roots():
assert violations == {} 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(): def test_plugin_components_do_not_reexport_legacy_abi_names():
"""新插件组件只提供 canonical 能力,不得复制旧 Helper、Manager 或 Oper 导出。""" """新插件组件只提供 canonical 能力,不得复制旧 Helper、Manager 或 Oper 导出。"""
violations: list[str] = [] violations: list[str] = []
+17 -17
View File
@@ -47,7 +47,7 @@ def test_plugin_history_merges_remote_metadata():
plugin_manager.get_local_repo_plugins.return_value = [] plugin_manager.get_local_repo_plugins.return_value = []
plugin_manager.async_get_online_plugins = AsyncMock(return_value=[market_plugin]) 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)) result = asyncio.run(plugin_history("DemoPlugin", None, True))
assert result.repo_url == "https://github.com/demo/plugins" 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.is_plugin_settling.return_value = True
plugin_manager.get_plugin_runtime_generation.return_value = 7 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)) result = asyncio.run(runtime_status(None))
assert result.ready is False assert result.ready is False
@@ -82,7 +82,7 @@ def test_reload_endpoint_reports_load_failure(monkeypatch):
plugin_manager = MagicMock() plugin_manager = MagicMock()
plugin_manager.reload_plugin.return_value = PluginRuntimeStatus.LOAD_FAILED plugin_manager.reload_plugin.return_value = PluginRuntimeStatus.LOAD_FAILED
register = MagicMock() 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) monkeypatch.setattr(plugin_endpoint, "register_plugin", register)
result = reload_plugin("DemoPlugin", None) 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.get_local_repo_plugins.return_value = []
plugin_manager.async_get_online_plugins = AsyncMock(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)) result = asyncio.run(plugin_history("DemoPlugin", None, True))
assert result.id == "DemoPlugin" 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_plugins_from_market = AsyncMock(return_value=[market_plugin])
plugin_manager.async_get_online_plugins = AsyncMock(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)) result = asyncio.run(plugin_history("DemoPlugin", None, True))
assert result.history == {"v1.1.0": "- 新增更新说明"} assert result.history == {"v1.1.0": "- 新增更新说明"}
@@ -167,7 +167,7 @@ def test_plugin_releases_returns_supported_versions_with_latest_and_current(monk
]) ])
with ( 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("app.api.endpoints.plugin.PluginHelper", return_value=plugin_helper),
): ):
result = asyncio.run(plugin_releases("DemoPlugin", None, "https://github.com/demo/plugins", False)) 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) plugin_helper.async_get_plugin_release_versions = AsyncMock(return_value=release_items)
with ( 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("app.api.endpoints.plugin.PluginHelper", return_value=plugin_helper),
): ):
result = asyncio.run(plugin_releases("DemoPlugin", None, "https://github.com/demo/plugins", False)) 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=[]) plugin_helper.async_get_plugin_release_versions = AsyncMock(return_value=[])
with ( 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("app.api.endpoints.plugin.PluginHelper", return_value=plugin_helper),
): ):
result = asyncio.run( 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=[]) plugin_helper.async_get_plugin_release_versions = AsyncMock(return_value=[])
with ( 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("app.api.endpoints.plugin.PluginHelper", return_value=plugin_helper),
): ):
result = asyncio.run(plugin_releases("DemoPlugin", None, "https://github.com/demo/plugins", True)) 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)) scheduled.append((plugin_id, repo_url, task_registry))
with ( 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("app.api.endpoints.plugin.PluginHelper", return_value=plugin_helper),
patch.object(plugin_endpoint, "_schedule_plugin_release_refresh", fake_schedule), 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)) scheduled.append((plugin_id, repo_url))
with ( 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("app.api.endpoints.plugin.PluginHelper", return_value=plugin_helper),
patch.object(plugin_endpoint, "_schedule_plugin_release_refresh", fake_schedule), 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 ( 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("app.api.endpoints.plugin.PluginHelper", return_value=plugin_helper),
): ):
result = asyncio.run(plugin_releases("DemoPlugin", None, "https://github.com/demo/plugins", False)) 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") source_file.write_text("export default 'shared'", encoding="utf-8")
plugin_manager = MagicMock() plugin_manager = MagicMock()
plugin_manager.get_plugin_source_id.return_value = "DemoPlugin" 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( monkeypatch.setattr(
plugin_endpoint, plugin_endpoint,
"get_api_runtime_config_snapshot", "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 = [] plugin_manager.get_plugin_source_instances.return_value = []
config = MagicMock() config = MagicMock()
config.get.return_value = ["DemoPlugin"] 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, "get_configured_system_config", lambda: config)
monkeypatch.setattr(plugin_endpoint, "remove_plugin_api", MagicMock()) monkeypatch.setattr(plugin_endpoint, "remove_plugin_api", MagicMock())
monkeypatch.setattr(plugin_endpoint, "remove_plugin_job", 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() config_provider = MagicMock()
remove_api = MagicMock() remove_api = MagicMock()
remove_job = MagicMock() remove_job = MagicMock()
monkeypatch.setattr(plugin_endpoint, "PluginManager", lambda: plugin_manager) monkeypatch.setattr(plugin_endpoint, "get_plugin_manager", lambda: plugin_manager)
monkeypatch.setattr( monkeypatch.setattr(
plugin_endpoint, plugin_endpoint,
"get_configured_system_config", "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 plugin_manager.mutation.side_effect = admission.hold
register = MagicMock() register = MagicMock()
add_to_folder = 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, "register_plugin", register)
monkeypatch.setattr(plugin_endpoint, "_add_clone_to_plugin_folder", add_to_folder) 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 = MagicMock()
plugin_manager.mutation.side_effect = admission.hold plugin_manager.mutation.side_effect = admission.hold
config_provider = MagicMock() config_provider = MagicMock()
monkeypatch.setattr(plugin_endpoint, "PluginManager", lambda: plugin_manager) monkeypatch.setattr(plugin_endpoint, "get_plugin_manager", lambda: plugin_manager)
monkeypatch.setattr( monkeypatch.setattr(
plugin_endpoint, plugin_endpoint,
"get_configured_system_config", "get_configured_system_config",
+1 -1
View File
@@ -727,7 +727,7 @@ def test_plugin_reload_refreshes_scheduler_services_idempotently(monkeypatch):
} }
] ]
plugin_manager.get_plugin_attr.return_value = "测试插件" 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"]) backend = _FakeSchedulerBackend(["DemoPlugin_old"])
scheduler = _build_scheduler_for_plugin_reload( scheduler = _build_scheduler_for_plugin_reload(
jobs={ jobs={
+1 -1
View File
@@ -86,7 +86,7 @@ def test_clear_cache_is_manual_only(monkeypatch):
"TransferChain", "TransferChain",
"WallpaperHelper", "WallpaperHelper",
"WorkflowChain", "WorkflowChain",
"PluginManager", "get_plugin_manager",
]: ]:
monkeypatch.setattr(scheduler_module, name, lambda: generic_chain) monkeypatch.setattr(scheduler_module, name, lambda: generic_chain)
monkeypatch.setattr( monkeypatch.setattr(
+1 -1
View File
@@ -30,7 +30,7 @@ async def test_reset_submits_cookiecloud_after_site_transaction(monkeypatch):
system_config = Mock() system_config = Mock()
system_config.async_set = AsyncMock() 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( monkeypatch.setattr(
site_endpoint, site_endpoint,
"get_configured_system_config", "get_configured_system_config",
+2 -2
View File
@@ -825,7 +825,7 @@ class SubscribeEndpointTest(TestCase):
for endpoint in [refresh_subscribes, check_subscribes]: for endpoint in [refresh_subscribes, check_subscribes]:
with self.subTest(endpoint=endpoint.__name__), patch( with self.subTest(endpoint=endpoint.__name__), patch(
"app.api.endpoints.subscribe.Scheduler" "app.api.endpoints.subscribe.get_scheduler"
) as scheduler: ) as scheduler:
response = endpoint(current_user=regular_user) response = endpoint(current_user=regular_user)
@@ -838,7 +838,7 @@ class SubscribeEndpointTest(TestCase):
(check_subscribes, "subscribe_tmdb"), (check_subscribes, "subscribe_tmdb"),
]: ]:
with self.subTest(endpoint=endpoint.__name__), patch( with self.subTest(endpoint=endpoint.__name__), patch(
"app.api.endpoints.subscribe.Scheduler" "app.api.endpoints.subscribe.get_scheduler"
) as scheduler: ) as scheduler:
response = endpoint(current_user=superuser) response = endpoint(current_user=superuser)
+2 -2
View File
@@ -22,7 +22,7 @@ class _FakeModuleManager:
def test_system_modulelist_keeps_chinese_name_and_adds_i18n_name(): def test_system_modulelist_keeps_chinese_name_and_adds_i18n_name():
"""模块列表接口应保留旧中文字段,并提供前端可用的多语言字段。""" """模块列表接口应保留旧中文字段,并提供前端可用的多语言字段。"""
token = LocaleHelper.set_current_locale("en-US") 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: try:
response = system_endpoint.modulelist(_="token") response = system_endpoint.modulelist(_="token")
finally: finally:
@@ -38,7 +38,7 @@ def test_system_modulelist_keeps_chinese_name_and_adds_i18n_name():
def test_system_moduletest_localizes_message(): def test_system_moduletest_localizes_message():
"""模块测试接口应按当前请求语言直接返回翻译后的 message。""" """模块测试接口应按当前请求语言直接返回翻译后的 message。"""
token = LocaleHelper.set_current_locale("en-US") 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: try:
response = system_endpoint.moduletest("DoubanModule", _="token") response = system_endpoint.moduletest("DoubanModule", _="token")
finally: finally:
+1 -1
View File
@@ -17,7 +17,7 @@ PROJECT_ROOT = Path(__file__).parents[1]
def test_create_app_does_not_start_plugin_manager_or_threads(monkeypatch): def test_create_app_does_not_start_plugin_manager_or_threads(monkeypatch):
"""ASGI factory 只构建应用结构,不得在创建阶段物化插件运行时。""" """ASGI factory 只构建应用结构,不得在创建阶段物化插件运行时。"""
plugin_manager = MagicMock(side_effect=AssertionError("plugin runtime started")) 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() threads_before = threading.active_count()
created = factory.create_app() created = factory.create_app()