mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 20:17:13 +08:00
refactor: use explicit runtime facade getters
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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)]
|
||||
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
+27
-25
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
+4
-4
@@ -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()
|
||||
# 初始化命令
|
||||
|
||||
+3
-3
@@ -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",
|
||||
|
||||
+6
-6
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 映射无需迁移;本阶段仅统一宿主生产路径。
|
||||
|
||||
### 总体判断
|
||||
|
||||
当前架构总体合理,已经从跨层混合的遗留单体收敛为**边界清晰的模块化单体**:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()))
|
||||
|
||||
|
||||
@@ -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] = []
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user