refactor(plugin): converge runtime ownership and compatibility

This commit is contained in:
jxxghp
2026-08-29 12:55:46 +08:00
parent 27c316c665
commit fa40f29dfd
51 changed files with 7403 additions and 4827 deletions

View 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,
)