新增了插件相关的完整领域模型、应用服务、基础设施实现,包括: 1. 插件状态、注册模式、来源等基础枚举和数据结构 2. 插件清单解析、发现、加载工具类 3. 插件注册表领域服务和内存存储实现 4. 插件相关的命令、查询、事件定义 5. 插件REST API接口和DTO映射 6. 集成了原有通道适配器到插件系统 7. 新增内置插件注册和自动发现能力
213 lines
8.1 KiB
Python
213 lines
8.1 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
from dataclasses import dataclass
|
|
|
|
from yuxi.channel.domain.exception.plugin_exception import PluginRegistrationException
|
|
from yuxi.channel.domain.model.plugin_registry.plugin_record import PluginRecord, PluginRecordId
|
|
from yuxi.channel.domain.model.plugin_registry.plugin_registration_mode import PluginRegistrationMode
|
|
from yuxi.channel.domain.model.plugin_registry.plugin_registry import PluginRegistry
|
|
from yuxi.channel.domain.model.shared.channel_capabilities import ChannelCapabilities
|
|
from yuxi.channel.domain.port.channel_adapter_port import ChannelAdapterPort
|
|
from yuxi.channel.infrastructure.plugin.api_guard import ApiGuard
|
|
from yuxi.channel.infrastructure.plugin.discovery import PluginCandidate, PluginDiscovery
|
|
from yuxi.channel.infrastructure.plugin.loader import PluginLoader
|
|
from yuxi.channel.infrastructure.plugin.manifest_parser import ManifestParser
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_registry_instance: PluginRegistryImpl | None = None
|
|
|
|
|
|
def get_registry() -> PluginRegistryImpl | None:
|
|
return _registry_instance
|
|
|
|
|
|
def set_registry(instance: PluginRegistryImpl | None) -> None:
|
|
global _registry_instance
|
|
_registry_instance = instance
|
|
|
|
|
|
@dataclass
|
|
class PluginRegistryImplConfig:
|
|
workspace_dir: str = "plugins"
|
|
installed_dir: str = "installed/plugins"
|
|
|
|
|
|
class PluginRegistryImpl:
|
|
def __init__(
|
|
self,
|
|
config: PluginRegistryImplConfig | None = None,
|
|
manifest_parser: ManifestParser | None = None,
|
|
loader: PluginLoader | None = None,
|
|
discovery: PluginDiscovery | None = None,
|
|
channel_config: object | None = None,
|
|
sse_endpoint: object | None = None,
|
|
):
|
|
self._config = config or PluginRegistryImplConfig()
|
|
self._registry = PluginRegistry()
|
|
self._manifest_parser = manifest_parser or ManifestParser()
|
|
self._loader = loader or PluginLoader()
|
|
self._discovery = discovery or PluginDiscovery(self._manifest_parser)
|
|
self._api_guard = ApiGuard()
|
|
self._channel_config = channel_config
|
|
self._sse_endpoint = sse_endpoint
|
|
|
|
set_registry(self)
|
|
|
|
@property
|
|
def registry(self) -> PluginRegistry:
|
|
return self._registry
|
|
|
|
async def register_plugin(
|
|
self,
|
|
candidate: PluginCandidate,
|
|
mode: PluginRegistrationMode = PluginRegistrationMode.FULL,
|
|
) -> PluginRecord:
|
|
manifest = await self._manifest_parser.parse_file(candidate.manifest_path) if candidate.manifest_path else None
|
|
if manifest is None and candidate.manifest_data:
|
|
manifest = self._manifest_parser.parse(candidate.manifest_data)
|
|
|
|
if manifest is None:
|
|
raise PluginRegistrationException(candidate.plugin_id)
|
|
|
|
record = PluginRecord(
|
|
record_id=PluginRecordId.generate(),
|
|
plugin_id=manifest.manifest.id,
|
|
name=manifest.manifest.name,
|
|
version=manifest.manifest.version,
|
|
channel_type=manifest.manifest.channel_type,
|
|
source=candidate.source,
|
|
manifest=manifest.manifest,
|
|
capabilities=manifest.manifest.capabilities,
|
|
config_schema=manifest.manifest.config_schema,
|
|
)
|
|
|
|
snapshot = self._registry.snapshot()
|
|
|
|
try:
|
|
if candidate.source.entry_file:
|
|
module = await self._loader.load(candidate.source.entry_file)
|
|
adapter_cls = self._loader.extract_adapter_class(module)
|
|
if adapter_cls:
|
|
record.set_adapter_class(adapter_cls)
|
|
|
|
api = self._api_guard.create(record.plugin_id)
|
|
await self._registry.register(record, api)
|
|
api.close()
|
|
|
|
if mode == PluginRegistrationMode.FULL and candidate.source.enabled:
|
|
config = self._resolve_config(record)
|
|
record.set_config(config)
|
|
from yuxi.channel.domain.service.plugin_registry_domain_service import PluginRegistryDomainService
|
|
|
|
PluginRegistryDomainService.transition_to_configured(record)
|
|
await self._registry.activate(record.plugin_id, **config)
|
|
|
|
except Exception as exc:
|
|
self._registry.restore(snapshot)
|
|
record.mark_error(str(exc))
|
|
raise PluginRegistrationException(record.plugin_id) from exc
|
|
|
|
return record
|
|
|
|
def register_builtin_adapter(self, name: str, adapter_cls: type[ChannelAdapterPort]) -> None:
|
|
existing = self._registry.get_record(name)
|
|
if existing:
|
|
return
|
|
|
|
from yuxi.channel.domain.model.plugin_registry.plugin_registry_factory import PluginRegistryFactory
|
|
|
|
caps = extract_capabilities(adapter_cls)
|
|
record = PluginRegistryFactory.create_builtin(
|
|
plugin_id=name,
|
|
name=name.capitalize(),
|
|
channel_type=name,
|
|
adapter_class=adapter_cls,
|
|
capabilities=caps,
|
|
)
|
|
self._registry.register_builtin(record)
|
|
|
|
def get_registered_adapter_classes(self) -> dict[str, type[ChannelAdapterPort]]:
|
|
result: dict[str, type[ChannelAdapterPort]] = {}
|
|
for record in self._registry.list_records():
|
|
if record.adapter_class:
|
|
result[record.channel_type] = record.adapter_class
|
|
return result
|
|
|
|
async def discover_and_register_all(
|
|
self,
|
|
mode: PluginRegistrationMode = PluginRegistrationMode.DISCOVERY,
|
|
) -> list[PluginRecord]:
|
|
candidates = await self._discovery.discover_all()
|
|
records = []
|
|
for candidate in candidates:
|
|
try:
|
|
record = await self.register_plugin(candidate, mode=mode)
|
|
records.append(record)
|
|
except Exception as exc:
|
|
logger.warning("failed to register plugin %s: %s", candidate.plugin_id, exc)
|
|
return records
|
|
|
|
def resolve_config(self, record: PluginRecord) -> dict:
|
|
return self._resolve_config(record)
|
|
|
|
def _resolve_config(self, record: PluginRecord) -> dict:
|
|
from yuxi.channel.channels._registry import get_channel_meta
|
|
|
|
default_config = record.get_default_config()
|
|
yaml_override = {}
|
|
if self._channel_config and hasattr(self._channel_config, "get_channel_config"):
|
|
yaml_override = self._channel_config.get_channel_config(record.channel_type)
|
|
merged = {**default_config, **yaml_override}
|
|
|
|
meta = get_channel_meta(record.channel_type)
|
|
|
|
for dep in meta.get("infra_dependencies", []):
|
|
if dep == "sse_push" and self._sse_endpoint and "sse_push" not in merged:
|
|
merged["sse_push"] = self._sse_endpoint
|
|
if dep == "hook_mappings" and self._channel_config:
|
|
hook_mappings = getattr(self._channel_config, "hook_mappings", None)
|
|
if hook_mappings and "mappings" not in merged:
|
|
merged["mappings"] = hook_mappings
|
|
|
|
env_mapping = meta.get("env_mapping", {})
|
|
if env_mapping:
|
|
env_overrides = self._apply_env_overrides_generic(env_mapping)
|
|
merged.update(env_overrides)
|
|
|
|
return merged
|
|
|
|
@staticmethod
|
|
def _apply_env_overrides_generic(env_mapping: dict[str, str]) -> dict:
|
|
overrides: dict = {}
|
|
for env_var, config_key in env_mapping.items():
|
|
value = os.getenv(env_var, "")
|
|
if value:
|
|
overrides[config_key] = value
|
|
return overrides
|
|
|
|
|
|
def extract_capabilities(adapter_cls: type) -> ChannelCapabilities:
|
|
from yuxi.channel.channels._registry import get_channel_meta
|
|
|
|
meta = get_channel_meta(adapter_cls.__name__.lower().replace("adapter", ""))
|
|
config_cls = meta.get("config_cls")
|
|
if config_cls:
|
|
try:
|
|
instance = config_cls()
|
|
if hasattr(instance, "capabilities"):
|
|
return instance.capabilities
|
|
except Exception:
|
|
pass
|
|
try:
|
|
instance = adapter_cls.__new__(adapter_cls)
|
|
if hasattr(instance, "capabilities"):
|
|
caps = instance.capabilities
|
|
if isinstance(caps, ChannelCapabilities):
|
|
return caps
|
|
except Exception:
|
|
pass
|
|
return ChannelCapabilities()
|