ForcePilot/backend/package/yuxi/channels/plugin.py
Kris ede29b1809 refactor(channel): 完成频道模块大重构与功能扩展
本次提交对频道模块进行了全面重构并新增多项核心功能:
1.  优化适配器状态获取逻辑,修复状态返回空值问题
2.  新增4种频道异常类型,完善错误处理体系
3.  大幅精简Mixin类,移除冗余的抽象方法定义
4.  重构适配器注册系统,统一注册入口并新增内置适配器加载方法
5.  扩展插件系统,新增更多元数据配置项支持
6.  新增线程类型、会话范围等模型定义,扩展事件类型枚举
7.  优化用户映射逻辑,使用PostgreSQL upsert避免重复创建
8.  新增历史消息注入模块,支持多格式历史格式化与缓存管理
9.  新增线程能力配置与各平台预置适配配置
10. 新增线程绑定管理器,支持多类型线程绑定生命周期管理
11. 重构__init__.py,整理导出模块与类型
12. 扩展基础适配器类,新增凭证解析、状态存储等核心方法
13. 重写消息路由器,支持按频道加载策略、安全校验与多命令处理
14. 新增/history、/context、/summary等交互命令实现
15. 优化消息记录与统计逻辑,完善路由调度链路
2026-05-13 16:41:11 +08:00

129 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 _register_builtin
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]
_register_builtin(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"),
pairing={"auto_pair": True},
conversation_bindings={"max_bindings": 5},
agent_prompt="You are a Telegram bot",
)
@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]
if self.channel_type is not None:
cls.channel_type = self.channel_type # type: ignore[attr-defined]
if self.pairing:
cls.pairing = self.pairing # type: ignore[attr-defined]
if self.conversation_bindings:
cls.conversation_bindings = self.conversation_bindings # type: ignore[attr-defined]
if self.agent_prompt is not None:
cls.agent_prompt = self.agent_prompt # type: ignore[attr-defined]
if self.messaging:
cls.messaging = self.messaging # type: ignore[attr-defined]
if self.directory is not None:
cls.directory = self.directory # type: ignore[attr-defined]
_register_builtin(self.channel_id, cls)
return cls