from pathlib import Path from app.runtime.compat.diagnostics import ( configure_legacy_import_diagnostics, scan_plugin_legacy_imports, ) from app.runtime.compat.resource_imports import scan_plugin_resource_imports from app.application.plugin.routes import register_plugin_api from app.runtime.config import global_vars from app.runtime.config import settings from app.runtime.extensions.plugin_manager import ( PluginManager, configure_plugin_catalog_factory, configure_plugin_install_reporter, configure_plugin_legacy_import_services, configure_plugin_resource_import_preparer, configure_site_auth_level_provider, ) from app.runtime.extensions.plugin.dependency import PluginDependencyInstallResult from app.application.plugin.catalog import PluginCatalogService from app.application.plugin.data import DeletePluginDataCommand from app.adapters.external.plugin.client import PluginMarketClient from app.runtime.extensions.plugin.storage import ( PluginStorage, configure_plugin_storage, ) from app.runtime.extensions.plugin.system import ( PluginSystemServices, configure_plugin_system, ) from app.runtime.managed_resources import acquire_managed_resource from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module from app.adapters.external.server import MoviePilotServerHelper from app.adapters.external.market import ( PluginHelper, VERSION_BACKWARD_COMPATIBLE_FLAGS, configure_installed_plugins_provider, ) from app.adapters.system.plugin.dependency import PluginDependencyInstaller from app.adapters.system.plugin.manifest import dependency_manifest_status from app.adapters.system.plugin.package import PluginPackageManager from app.adapters.system.host import SystemUtils from app.db.oper.plugindata import PluginDataOper from app.application.configuration import get_configured_system_config from app.db.session import SessionFactory from app.db.uow import SqlAlchemyUnitOfWork from app.runtime.log import logger from app.foundation.version import compare_version from app.schemas.plugin import PluginRuntimeStatus from app.schemas.types import SystemConfigKey async def _async_write_plugin_config(key, value): """通过数据库操作器异步保存插件运行时配置。""" return await get_configured_system_config().async_set(key, value) def _delete_plugin_data(plugin_id: str) -> None: """用独占同步会话执行插件重置的数据删除事务。""" session = SessionFactory() try: DeletePluginDataCommand( repository=PluginDataOper(session), unit_of_work=SqlAlchemyUnitOfWork(session), ).execute(plugin_id) finally: session.close() def _prepare_legacy_plugin_import(*, plugin_id: str, plugin_dir: Path) -> None: """在执行旧插件顶层代码前准备其静态导入所需的宿主资源。""" for capability_id in scan_plugin_resource_imports(plugin_id, plugin_dir): acquire_managed_resource( capability_id, reason="legacy_plugin_import", ) def configure_plugin_services() -> None: """把兼容诊断、远程上报和站点认证等级装配到插件管理器。""" plugin_helper = PluginHelper() market_client = PluginMarketClient(plugin_helper) configure_plugin_legacy_import_services( diagnostics_configurator=configure_legacy_import_diagnostics, import_scanner=scan_plugin_legacy_imports, ) configure_plugin_resource_import_preparer(_prepare_legacy_plugin_import) configure_plugin_install_reporter(MoviePilotServerHelper.install_plugin_reg) configure_site_auth_level_provider(lambda: SitesHelper().auth_level) configure_installed_plugins_provider( lambda: get_configured_system_config().get(SystemConfigKey.UserInstalledPlugins) or [] ) configure_plugin_catalog_factory(_build_plugin_catalog) configure_plugin_system(PluginSystemServices( market=market_client, package=PluginPackageManager(plugin_helper), dependency=PluginDependencyInstaller( plugin_helper, installed_plugins_provider=lambda: get_configured_system_config().get( SystemConfigKey.UserInstalledPlugins ) or [], plugin_dir=Path(settings.ROOT_PATH) / "app" / "plugins", ), dependency_manifest_status=dependency_manifest_status, compatible_flags=lambda flag: ( [flag] + VERSION_BACKWARD_COMPATIBLE_FLAGS.get(flag, []) if flag else [] ), frozen=SystemUtils.is_frozen, )) configure_plugin_storage(PluginStorage( read=lambda key: get_configured_system_config().get(key), write=lambda key, value: get_configured_system_config().set(key, value), async_write=_async_write_plugin_config, delete=lambda key: get_configured_system_config().delete(key), delete_data=_delete_plugin_data, )) def _build_plugin_catalog(manager: PluginManager) -> PluginCatalogService: """在组合根连接目录用例、市场客户端、持久化读取和插件 DTO 映射。""" client = PluginMarketClient() return PluginCatalogService( market_loader=client.get_plugins, async_market_loader=client.async_get_plugins, installed_plugins_provider=lambda: get_configured_system_config().get( SystemConfigKey.UserInstalledPlugins ) or [], plugin_mapper=manager._process_plugin_info, is_local_repo=PluginMarketClient.is_local_repo_url, version_compare=compare_version, warning=logger.warning, error=logger.error, ) async def sync_plugins() -> bool: """ 初始化安装插件,并动态注册后台任务及API """ plugin_manager = None try: configure_plugin_services() loop = global_vars.loop plugin_manager = PluginManager() plugin_manager.set_plugin_settling(True) sync_result = await execute_task(loop, plugin_manager.sync, "插件同步到本地") dependency_result = await execute_task( loop, plugin_manager.install_plugin_missing_dependencies_with_status, "缺失依赖项安装", ) if dependency_result is None: return False if not isinstance(dependency_result, PluginDependencyInstallResult): logger.error("缺失依赖项安装返回了无效结果,跳过插件重新初始化") return False previous_statuses = plugin_manager.get_plugin_runtime_statuses() classification = plugin_manager.classify_plugins() plugin_manager.apply_plugin_dependency_classification(classification) if not dependency_result.success: logger.error("缺失依赖项安装未完成,将继续激活当前已就绪插件") changed_ids = await execute_task( loop, lambda: _activate_ready_plugins( plugin_manager, classification.ready, sync_result or [], previous_statuses, ), "插件运行态激活", ) if changed_ids is None: return False if not changed_ids: logger.debug("没有新的插件进入可运行状态") return False for plugin_id in changed_ids: register_plugin_api(plugin_id) if dependency_result.success: logger.info(f"后台插件加载完成,共处理 {len(changed_ids)} 个插件") else: logger.warning( f"缺失依赖项仍未全部恢复,已激活 {len(changed_ids)} 个就绪插件" ) return True except Exception as e: logger.error(f"插件初始化过程中出现异常: {e}") return False def _activate_ready_plugins( plugin_manager: PluginManager, ready_ids: tuple[str, ...], synced_ids: list[str], previous_statuses: dict[str, PluginRuntimeStatus], ) -> list[str]: """在线程池中完成插件导入和初始化,避免阻塞 Web 事件循环。""" running_ids = set(plugin_manager.running_plugins) synced = set(synced_ids) changed_ids: list[str] = [] for plugin_id in ready_ids: dependency_recovered = ( previous_statuses.get(plugin_id) is PluginRuntimeStatus.DEPENDENCY_PENDING ) if plugin_id in running_ids and (plugin_id in synced or dependency_recovered): plugin_manager.reload_plugin(plugin_id) changed_ids.append(plugin_id) continue if plugin_id not in running_ids: plugin_manager.start(plugin_id) changed_ids.append(plugin_id) return changed_ids async def execute_task(loop, task_func, task_name): """ 执行后台任务 """ try: result = await loop.run_in_executor(None, task_func) if isinstance(result, PluginDependencyInstallResult): processed_count = len(result.missing) elif isinstance(result, list): processed_count = len(result) else: processed_count = 0 if processed_count: logger.debug(f"{task_name} 已完成,共处理 {processed_count} 个项目") else: logger.debug(f"没有新的 {task_name} 需要处理") return result except Exception as e: logger.error(f"{task_name} 时发生错误:{e}", exc_info=True) return None def init_plugins(): """ 初始化插件 """ configure_plugin_services() plugin_manager = PluginManager() classification = plugin_manager.classify_plugins() plugin_manager.apply_plugin_dependency_classification(classification) plugin_manager.set_plugin_settling(True) for plugin_id in classification.ready: plugin_manager.start(plugin_id) register_plugin_api() plugin_manager.start_monitor() logger.info( "插件启动分类:立即加载=%s,等待依赖=%s,等待源码=%s", len(classification.ready), len(classification.missing_dependencies), len(classification.missing_source), ) def stop_plugins(): """ 停止插件 """ try: plugin_manager = PluginManager() try: plugin_manager.stop_monitor() finally: plugin_manager.stop() except Exception as e: logger.error(f"停止插件时发生错误:{e}", exc_info=True)