mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-02 05:17:23 +08:00
refactor(plugin): converge runtime ownership and compatibility
This commit is contained in:
324
app/runtime/extensions/plugin/runtime.py
Normal file
324
app/runtime/extensions/plugin/runtime.py
Normal file
@@ -0,0 +1,324 @@
|
||||
"""插件宿主运行时依赖聚合与构造。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, Protocol
|
||||
|
||||
from app.foundation.crypto import RSAUtils
|
||||
from app.foundation.version import compare_version
|
||||
from app.runtime.events import eventmanager
|
||||
from app.runtime.extensions.plugin.access import PluginAccessPolicy
|
||||
from app.runtime.extensions.plugin.admission import PluginMutationAdmission
|
||||
from app.runtime.extensions.plugin.catalog import PluginCatalogFacade
|
||||
from app.runtime.extensions.plugin.clone import PluginCloneService
|
||||
from app.runtime.extensions.plugin.dependency import PluginDependencyService
|
||||
from app.runtime.extensions.plugin.lifecycle import PluginLifecycle
|
||||
from app.runtime.extensions.plugin.loader import PluginLoader
|
||||
from app.runtime.extensions.plugin.metadata import PluginMetadataMapper
|
||||
from app.runtime.extensions.plugin.monitor import PluginMonitorController
|
||||
from app.runtime.extensions.plugin.paths import PluginPathResolver
|
||||
from app.runtime.extensions.plugin.projection import PluginProjection
|
||||
from app.runtime.extensions.plugin.registry import PluginRegistry
|
||||
from app.runtime.extensions.plugin.storage import (
|
||||
PluginConfigStore,
|
||||
PluginInstanceStore,
|
||||
PluginStorage,
|
||||
)
|
||||
from app.runtime.extensions.plugin.sync import (
|
||||
LocalPluginSyncService,
|
||||
PluginSyncService,
|
||||
)
|
||||
from app.runtime.extensions.plugin.system import PluginSystemServices
|
||||
from app.runtime.extensions.plugin.tools import PluginToolCatalog
|
||||
from app.schemas.types import SystemConfigKey
|
||||
|
||||
|
||||
class PluginRuntimeHost(Protocol):
|
||||
"""声明运行时 owner 回调宿主生命周期门面的最小合同。"""
|
||||
|
||||
def reload_plugin(self, plugin_id: str) -> Any:
|
||||
"""重载指定插件。"""
|
||||
...
|
||||
|
||||
def remove_plugin(self, plugin_id: str) -> Any:
|
||||
"""移除指定插件运行实例。"""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def get_plugin_remote_entry(plugin_id: str, page: str) -> str:
|
||||
"""构造插件远程页面入口。"""
|
||||
...
|
||||
|
||||
def _run_file_watcher(self) -> None:
|
||||
"""运行插件文件监控循环。"""
|
||||
...
|
||||
|
||||
def get_plugins_from_market(
|
||||
self,
|
||||
market: str,
|
||||
package_version: Optional[str] = None,
|
||||
force: bool = False,
|
||||
) -> Optional[list[Any]]:
|
||||
"""读取指定市场目录。"""
|
||||
...
|
||||
|
||||
async def async_get_plugins_from_market(
|
||||
self,
|
||||
market: str,
|
||||
package_version: Optional[str] = None,
|
||||
force: bool = False,
|
||||
) -> Optional[list[Any]]:
|
||||
"""异步读取指定市场目录。"""
|
||||
...
|
||||
|
||||
|
||||
PluginCatalogFactory = Callable[[Callable[..., Any]], Any]
|
||||
PluginImportService = Callable[..., None]
|
||||
PluginRemoteEntryBuilder = Callable[[str, str], str]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PluginRuntimeEnvironment:
|
||||
"""保存由组合根提供的插件运行时外部端口。"""
|
||||
|
||||
plugins_root: Path
|
||||
storage: Callable[[], PluginStorage]
|
||||
system: Callable[[], PluginSystemServices]
|
||||
catalog_factory: PluginCatalogFactory
|
||||
import_preparer: PluginImportService
|
||||
import_scanner: PluginImportService
|
||||
auth_level: Callable[[], int]
|
||||
remote_entry: PluginRemoteEntryBuilder
|
||||
development: Callable[[], bool]
|
||||
logger: Any
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PluginRuntime:
|
||||
"""聚合一个 PluginManager 生命周期内唯一的职责 owner。"""
|
||||
|
||||
registry: PluginRegistry
|
||||
instances: PluginInstanceStore
|
||||
configs: PluginConfigStore
|
||||
access: PluginAccessPolicy
|
||||
catalog: PluginCatalogFacade
|
||||
paths: PluginPathResolver
|
||||
local_sync: LocalPluginSyncService
|
||||
monitor: PluginMonitorController
|
||||
admission: PluginMutationAdmission
|
||||
dependencies: PluginDependencyService
|
||||
loader: PluginLoader
|
||||
tools: PluginToolCatalog
|
||||
lifecycle: PluginLifecycle
|
||||
metadata: PluginMetadataMapper
|
||||
sync: PluginSyncService
|
||||
clone: PluginCloneService
|
||||
projection: PluginProjection
|
||||
recent_local_sync: dict[str, float]
|
||||
system: Callable[[], PluginSystemServices]
|
||||
|
||||
|
||||
def build_plugin_runtime(
|
||||
host: PluginRuntimeHost,
|
||||
environment: PluginRuntimeEnvironment,
|
||||
*,
|
||||
tool_build_max_attempts: int,
|
||||
) -> PluginRuntime:
|
||||
"""按依赖顺序构造唯一插件运行时,各业务能力仍由对应 owner 实现。"""
|
||||
registry = PluginRegistry()
|
||||
instances = PluginInstanceStore(storage=environment.storage)
|
||||
configs = PluginConfigStore(
|
||||
storage=environment.storage,
|
||||
plugin_exists=lambda plugin_id: bool(registry.classes.get(plugin_id)),
|
||||
)
|
||||
access = PluginAccessPolicy(
|
||||
auth_level=environment.auth_level,
|
||||
verify_keys=RSAUtils.verify_rsa_keys,
|
||||
log=environment.logger,
|
||||
)
|
||||
loader = PluginLoader(
|
||||
plugins_root=environment.plugins_root,
|
||||
import_preparer=environment.import_preparer,
|
||||
import_scanner=environment.import_scanner,
|
||||
log=environment.logger,
|
||||
)
|
||||
tools = PluginToolCatalog(max_attempts=tool_build_max_attempts)
|
||||
|
||||
def load_plugins(
|
||||
plugin_id: Optional[str],
|
||||
installed_plugins: list[str],
|
||||
validator: Callable[[Any], bool],
|
||||
) -> list[Any]:
|
||||
"""加载物理插件或虚拟实例,并保持持久化实例顺序。"""
|
||||
if plugin_id:
|
||||
instance = instances.get(plugin_id)
|
||||
if instance:
|
||||
return loader.load_instance(instance, validator)
|
||||
return loader.load(plugin_id, installed_plugins, validator)
|
||||
plugins = loader.load(None, installed_plugins, validator)
|
||||
for instance in instances.all().values():
|
||||
plugins.extend(loader.load_instance(instance, validator))
|
||||
return plugins
|
||||
|
||||
lifecycle = PluginLifecycle(
|
||||
classes=registry.classes,
|
||||
running=registry.running,
|
||||
load_plugins=load_plugins,
|
||||
installed_plugins=lambda: environment.storage().read(
|
||||
SystemConfigKey.UserInstalledPlugins
|
||||
) or [],
|
||||
plugin_config=configs.read,
|
||||
auth_checker=lambda plugin: access.check(plugin),
|
||||
clear_modules=loader.clear_modules,
|
||||
clear_tools=tools.clear,
|
||||
enable_events=eventmanager.enable_event_handler,
|
||||
disable_events=eventmanager.disable_event_handler,
|
||||
runtime_status_writer=registry.set_runtime_status,
|
||||
log=environment.logger,
|
||||
event_sender=eventmanager.send_event,
|
||||
)
|
||||
metadata = PluginMetadataMapper(
|
||||
plugin_instance=registry.instance,
|
||||
plugin_class=registry.plugin_class,
|
||||
annotate_system_version=lambda info: environment.system().annotate_system_version(
|
||||
info
|
||||
),
|
||||
is_package_compatible=lambda info, version: environment.system().is_package_compatible(
|
||||
info,
|
||||
version,
|
||||
),
|
||||
auth_checker=lambda plugin, source: access.check(plugin, source),
|
||||
version_compare=lambda source, comparison, target: (
|
||||
compare_version(source, comparison, target) is True
|
||||
),
|
||||
log=environment.logger,
|
||||
)
|
||||
catalog = PluginCatalogFacade(
|
||||
classes=lambda: registry.classes,
|
||||
running=lambda: registry.running,
|
||||
storage=environment.storage,
|
||||
system=environment.system,
|
||||
market_catalog=lambda: environment.catalog_factory(metadata.map),
|
||||
market_loader=lambda market, package_version=None, force=False: (
|
||||
host.get_plugins_from_market(market, package_version, force)
|
||||
),
|
||||
async_market_loader=lambda market, package_version=None, force=False: (
|
||||
host.async_get_plugins_from_market(
|
||||
market,
|
||||
package_version,
|
||||
force,
|
||||
)
|
||||
),
|
||||
map_plugin=lambda **kwargs: metadata.map(
|
||||
plugin_id=kwargs["pid"],
|
||||
plugin_info=kwargs["plugin_info"],
|
||||
market=kwargs["market"],
|
||||
installed_plugins=kwargs["installed_apps"],
|
||||
add_time=kwargs["add_time"],
|
||||
package_version=kwargs.get("package_version"),
|
||||
),
|
||||
auth_checker=lambda **kwargs: access.check(**kwargs),
|
||||
plugin_attr=lambda plugin_id, attribute: getattr(
|
||||
registry.instance(plugin_id),
|
||||
attribute,
|
||||
None,
|
||||
),
|
||||
plugin_instance=instances.get,
|
||||
plugin_instances=instances.all,
|
||||
runtime_status=registry.runtime_status,
|
||||
log=environment.logger,
|
||||
)
|
||||
paths = PluginPathResolver(
|
||||
runtime_root=environment.plugins_root,
|
||||
running=lambda: registry.running,
|
||||
system=environment.system,
|
||||
strict_system_version=lambda: not environment.development(),
|
||||
log=environment.logger,
|
||||
)
|
||||
recent_local_sync: dict[str, float] = {}
|
||||
local_sync = LocalPluginSyncService(
|
||||
installed_plugins=lambda: environment.storage().read(
|
||||
SystemConfigKey.UserInstalledPlugins
|
||||
) or [],
|
||||
candidate=lambda plugin_id: environment.system().local_candidate(plugin_id),
|
||||
system=environment.system,
|
||||
recent_sync=recent_local_sync,
|
||||
log=environment.logger,
|
||||
)
|
||||
dependencies = PluginDependencyService(
|
||||
system=environment.system,
|
||||
instances=instances.all,
|
||||
registry=registry,
|
||||
log=environment.logger,
|
||||
)
|
||||
sync = PluginSyncService(
|
||||
frozen=lambda: environment.system().is_frozen(),
|
||||
installed_plugins=lambda: environment.storage().read(
|
||||
SystemConfigKey.UserInstalledPlugins
|
||||
) or [],
|
||||
online_plugins=catalog.online,
|
||||
local_plugins=catalog.local_repository,
|
||||
merge_plugins=lambda higher, base, _markets: catalog.merge(higher, base),
|
||||
plugin_exists=catalog.exists,
|
||||
install=lambda plugin_id, repo_url, force, startup_token: environment.system().install_plugin(
|
||||
plugin_id=plugin_id,
|
||||
repo_url=repo_url,
|
||||
force=force,
|
||||
startup_token=startup_token,
|
||||
),
|
||||
log=environment.logger,
|
||||
)
|
||||
def source_plugin_id(plugin_id: str) -> str:
|
||||
"""把虚拟实例归一到持久化的物理源码插件。"""
|
||||
instance = instances.get(plugin_id)
|
||||
return instance.source_plugin_id if instance else plugin_id
|
||||
|
||||
clone = PluginCloneService(
|
||||
plugin_class=registry.plugin_class,
|
||||
plugin_exists=catalog.exists,
|
||||
source_plugin_id=source_plugin_id,
|
||||
save_instance=instances.save,
|
||||
delete_instance=instances.delete,
|
||||
read_config=configs.read,
|
||||
save_config=lambda plugin_id, config: configs.write(
|
||||
plugin_id,
|
||||
config,
|
||||
force=True,
|
||||
),
|
||||
delete_config=lambda plugin_id: configs.delete(plugin_id, force=True),
|
||||
reload_plugin=host.reload_plugin,
|
||||
remove_plugin=host.remove_plugin,
|
||||
log=environment.logger,
|
||||
)
|
||||
projection = PluginProjection(
|
||||
registry.running,
|
||||
environment.logger,
|
||||
environment.remote_entry,
|
||||
)
|
||||
return PluginRuntime(
|
||||
registry=registry,
|
||||
instances=instances,
|
||||
configs=configs,
|
||||
access=access,
|
||||
catalog=catalog,
|
||||
paths=paths,
|
||||
local_sync=local_sync,
|
||||
monitor=PluginMonitorController(
|
||||
runner=host._run_file_watcher,
|
||||
log=environment.logger,
|
||||
),
|
||||
admission=PluginMutationAdmission(),
|
||||
dependencies=dependencies,
|
||||
loader=loader,
|
||||
tools=tools,
|
||||
lifecycle=lifecycle,
|
||||
metadata=metadata,
|
||||
sync=sync,
|
||||
clone=clone,
|
||||
projection=projection,
|
||||
recent_local_sync=recent_local_sync,
|
||||
system=environment.system,
|
||||
)
|
||||
Reference in New Issue
Block a user