新增了插件相关的完整领域模型、应用服务、基础设施实现,包括: 1. 插件状态、注册模式、来源等基础枚举和数据结构 2. 插件清单解析、发现、加载工具类 3. 插件注册表领域服务和内存存储实现 4. 插件相关的命令、查询、事件定义 5. 插件REST API接口和DTO映射 6. 集成了原有通道适配器到插件系统 7. 新增内置插件注册和自动发现能力
85 lines
2.9 KiB
Python
85 lines
2.9 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
from yuxi.channel.domain.model.plugin_registry.plugin_source import PluginOrigin, PluginSource
|
|
from yuxi.channel.infrastructure.plugin.manifest_parser import ManifestParser
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class PluginCandidate:
|
|
plugin_id: str
|
|
source: PluginSource
|
|
manifest_path: str | None = None
|
|
manifest_data: dict | None = None
|
|
|
|
|
|
class PluginDiscovery:
|
|
MANIFEST_FILENAME = "yuxi.plugin.yaml"
|
|
|
|
def __init__(self, manifest_parser: ManifestParser | None = None):
|
|
self._parser = manifest_parser or ManifestParser()
|
|
|
|
async def discover_workspace(self, base_dir: str = "plugins") -> list[PluginCandidate]:
|
|
return await self._scan_directory(base_dir, PluginOrigin.WORKSPACE)
|
|
|
|
async def discover_installed(self, base_dir: str = "installed/plugins") -> list[PluginCandidate]:
|
|
return await self._scan_directory(base_dir, PluginOrigin.INSTALLED)
|
|
|
|
async def discover_all(self) -> list[PluginCandidate]:
|
|
candidates = []
|
|
candidates.extend(await self.discover_workspace())
|
|
candidates.extend(await self.discover_installed())
|
|
return candidates
|
|
|
|
async def find(self, plugin_id: str) -> PluginCandidate | None:
|
|
all_candidates = await self.discover_all()
|
|
for candidate in all_candidates:
|
|
if candidate.plugin_id == plugin_id:
|
|
return candidate
|
|
return None
|
|
|
|
async def _scan_directory(self, base_dir: str, origin: PluginOrigin) -> list[PluginCandidate]:
|
|
candidates = []
|
|
base_path = Path(base_dir)
|
|
if not base_path.exists():
|
|
return candidates
|
|
|
|
for plugin_dir in sorted(base_path.iterdir()):
|
|
if not plugin_dir.is_dir():
|
|
continue
|
|
if plugin_dir.name.startswith("_") or plugin_dir.name.startswith("."):
|
|
continue
|
|
|
|
manifest_path = plugin_dir / self.MANIFEST_FILENAME
|
|
if not manifest_path.exists():
|
|
logger.debug("skipping %s: no %s found", plugin_dir, self.MANIFEST_FILENAME)
|
|
continue
|
|
|
|
try:
|
|
result = await self._parser.parse_file(str(manifest_path))
|
|
manifest = result.manifest
|
|
|
|
source = PluginSource(
|
|
origin=origin,
|
|
root_dir=str(plugin_dir),
|
|
entry_file=str(plugin_dir / manifest.entry) if manifest.entry else "",
|
|
)
|
|
|
|
candidates.append(
|
|
PluginCandidate(
|
|
plugin_id=manifest.id,
|
|
source=source,
|
|
manifest_path=str(manifest_path),
|
|
manifest_data=result.raw,
|
|
)
|
|
)
|
|
except Exception as exc:
|
|
logger.warning("failed to parse manifest in %s: %s", plugin_dir, exc)
|
|
|
|
return candidates
|