ForcePilot/backend/package/yuxi/channels/plugin.py

114 lines
3.6 KiB
Python
Raw Normal View History

from __future__ import annotations
from typing import Any
from yuxi.channels.base import BaseChannelAdapter
from yuxi.channels.capabilities import ChannelCapabilities
from yuxi.channels.meta import ChannelMeta
from yuxi.channels.registry import _BUILTIN_ADAPTERS
def channel_plugin(
cls: type[BaseChannelAdapter] | None = None,
*,
channel_id: str | None = None,
capabilities: ChannelCapabilities | None = None,
meta: ChannelMeta | None = None,
order: int = 100,
) -> type[BaseChannelAdapter]:
"""渠道插件注册装饰器 — 组合入口, 替代 @register_builtin_adapter
用法:
@channel_plugin(
capabilities=ChannelCapabilities(...),
meta=ChannelMeta(id="telegram", label="Telegram"),
)
class TelegramAdapter(BaseChannelAdapter):
...
当用作无参装饰器时, 从类属性自动推断 channel_id:
@channel_plugin
class TelegramAdapter(BaseChannelAdapter):
channel_id = "telegram"
"""
def _decorate(_cls: type[BaseChannelAdapter]) -> type[BaseChannelAdapter]:
cid = channel_id or getattr(_cls, "channel_id", None)
if not cid:
raise ValueError(
f"channel_plugin requires channel_id for {_cls.__name__}. "
"Set channel_id as a ClassVar or pass it to the decorator."
)
if capabilities is not None:
_cls.capabilities = capabilities # type: ignore[attr-defined]
if meta is not None:
_cls.meta = meta # type: ignore[attr-defined]
_BUILTIN_ADAPTERS[cid] = _cls
return _cls
if cls is not None:
return _decorate(cls)
return _decorate
class ChannelPlugin:
"""渠道插件组合入口 — 聚合协议和能力声明的便利构造器
为渠道开发者提供声明式 API在适配器类上组合配置:
plugin = ChannelPlugin(
channel_id="telegram",
channel_type=ChannelType.TELEGRAM,
capabilities=ChannelCapabilities(
chat_types=["direct", "group", "thread"],
polls=True, reactions=True, edit=True,
supports_streaming=True,
streaming_modes=["off", "partial", "block", "progress"],
),
meta=ChannelMeta(id="telegram", label="Telegram"),
)
@plugin.register
class TelegramAdapter(BaseChannelAdapter):
...
"""
def __init__(
self,
*,
channel_id: str,
channel_type: Any = None,
capabilities: ChannelCapabilities | None = None,
meta: ChannelMeta | None = None,
order: int = 100,
pairing: dict | None = None,
conversation_bindings: dict | None = None,
agent_prompt: str | None = None,
messaging: dict | None = None,
directory: Any = None,
):
self.channel_id = channel_id
self.channel_type = channel_type
self.capabilities = capabilities
self.meta = meta or ChannelMeta(id=channel_id, label=channel_id)
self.order = order
self.pairing = pairing or {}
self.conversation_bindings = conversation_bindings or {}
self.agent_prompt = agent_prompt
self.messaging = messaging or {}
self.directory = directory
def register(self, cls: type[BaseChannelAdapter]) -> type[BaseChannelAdapter]:
if self.capabilities is not None:
cls.capabilities = self.capabilities # type: ignore[attr-defined]
if self.meta is not None:
cls.meta = self.meta # type: ignore[attr-defined]
_BUILTIN_ADAPTERS[self.channel_id] = cls
return cls