ForcePilot/backend/package/yuxi/channel/infrastructure/plugin/discovery.py
Kris 9e503becd3
Some checks failed
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
feat(plugin): 实现完整的插件注册管理系统
新增了插件相关的完整领域模型、应用服务、基础设施实现,包括:
1. 插件状态、注册模式、来源等基础枚举和数据结构
2. 插件清单解析、发现、加载工具类
3. 插件注册表领域服务和内存存储实现
4. 插件相关的命令、查询、事件定义
5. 插件REST API接口和DTO映射
6. 集成了原有通道适配器到插件系统
7. 新增内置插件注册和自动发现能力
2026-05-31 16:44:13 +08:00

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