ForcePilot/backend/package/yuxi/channel/plugins/registry.py
Kris dd1857221c feat(channel/plugins): 新增渠道插件系统完整实现
实现了渠道插件的全生命周期管理能力,包括:
1. 插件清单的加载、解析与存储
2. 依赖校验与拓扑排序安装/卸载
3. 插件发现与安全沙箱执行环境
4. 插件注册中心与目录管理能力
2026-05-21 10:27:58 +08:00

106 lines
3.1 KiB
Python

from __future__ import annotations
import logging
import threading
from typing import Any, Iterator
from yuxi.channel.protocols import MetaProtocol
logger = logging.getLogger(__name__)
class _RegistryMeta(type):
def __len__(cls) -> int:
return len(cls._plugins)
def __iter__(cls) -> Iterator[Any]:
return iter(
sorted(
cls._plugins.values(),
key=lambda p: (getattr(p, "order", 0), getattr(p, "id", "")),
)
)
def __contains__(cls, plugin_id: str) -> bool:
return plugin_id in cls._plugins
def __repr__(cls) -> str:
if not cls._plugins:
return "<ChannelPluginRegistry empty>"
ids = sorted(cls._plugins.keys())
preview = ", ".join(ids[:5])
tail = "..." if len(ids) > 5 else ""
return f"<ChannelPluginRegistry {len(cls._plugins)} plugins: {preview}{tail}>"
class ChannelPluginRegistry(metaclass=_RegistryMeta):
_plugins: dict[str, Any] = {}
_lock: threading.Lock | None = None
@classmethod
def _ensure_lock(cls) -> threading.Lock:
if cls._lock is None:
cls._lock = threading.Lock()
return cls._lock
@classmethod
def register(cls, plugin: Any) -> Any:
if not isinstance(plugin, MetaProtocol):
raise ValueError("Channel plugin must satisfy MetaProtocol (id, name, order)")
plugin_id = getattr(plugin, "id", None)
if not plugin_id or not isinstance(plugin_id, str) or not plugin_id.strip():
raise ValueError("Channel plugin must have a non-empty string 'id' attribute")
with cls._ensure_lock():
if plugin_id in cls._plugins:
raise ValueError(f"Channel plugin '{plugin_id}' is already registered")
cls._plugins[plugin_id] = plugin
logger.info("Registered channel plugin: %s", plugin_id)
return plugin
@classmethod
def get(cls, plugin_id: str) -> Any:
return cls._plugins.get(plugin_id)
@classmethod
def list_plugins(cls) -> list[str]:
return cls.list_ids()
@classmethod
def list_all(cls) -> list[Any]:
return sorted(
cls._plugins.values(),
key=lambda p: (getattr(p, "order", 0), getattr(p, "id", "")),
)
@classmethod
def list_ids(cls) -> list[str]:
return sorted(cls._plugins.keys())
@classmethod
def all(cls) -> list[Any]:
return cls.list_all()
@classmethod
def unregister(cls, plugin_id: str) -> Any | None:
with cls._ensure_lock():
plugin = cls._plugins.pop(plugin_id, None)
if plugin is not None:
logger.info("Unregistered channel plugin: %s", plugin_id)
return plugin
@classmethod
def retire(cls, plugin_id: str) -> Any | None:
return cls.unregister(plugin_id)
@classmethod
def clear(cls) -> None:
with cls._ensure_lock():
cls._plugins.clear()
@classmethod
def has(cls, plugin_id: str) -> bool:
return plugin_id in cls._plugins
@classmethod
def is_registered(cls, plugin_id: str) -> bool:
return cls.has(plugin_id)