1. 简化message_actions.py中获取适配器的逻辑 2. 新增适配器合法性校验工具方法 3. 新增会话映射过期清理功能 4. 重构渠道状态机与基础适配器实现 5. 统一渠道操作异常处理逻辑 6. 新增凭证状态查询与刷新接口 7. 优化健康检查与自动重连逻辑 8. 新增统计数据缓存与批量查询优化 9. 修复部分数据库操作的异常处理逻辑
263 lines
8.2 KiB
Python
263 lines
8.2 KiB
Python
from __future__ import annotations
|
|
|
|
import inspect
|
|
|
|
from yuxi.channels.base import BaseChannelAdapter
|
|
|
|
_BUILTIN_ADAPTERS: dict[str, type[BaseChannelAdapter]] = {}
|
|
_BUILTIN_ADAPTER_ALIASES: dict[str, str] = {}
|
|
|
|
|
|
def _register_builtin(channel_id: str, cls: type[BaseChannelAdapter], aliases: list[str] | None = None) -> None:
|
|
"""统一的内置适配器注册入口 — 所有注册路径最终都调用此函数"""
|
|
_BUILTIN_ADAPTERS[channel_id] = cls
|
|
if aliases:
|
|
for alias in aliases:
|
|
_BUILTIN_ADAPTER_ALIASES[alias] = channel_id
|
|
|
|
|
|
def register_builtin_adapter(cls: type[BaseChannelAdapter] | None = None, *, aliases: list[str] | None = None):
|
|
def _decorator(cls_inner: type[BaseChannelAdapter]) -> type[BaseChannelAdapter]:
|
|
_register_builtin(cls_inner.channel_id, cls_inner, aliases)
|
|
return cls_inner
|
|
|
|
if cls is None:
|
|
return _decorator
|
|
return _decorator(cls)
|
|
|
|
|
|
def _load_builtin_adapters():
|
|
if _BUILTIN_ADAPTERS:
|
|
return
|
|
try:
|
|
from yuxi.channels.adapters.telegram import TelegramAdapter
|
|
|
|
_BUILTIN_ADAPTERS["telegram"] = TelegramAdapter
|
|
except ImportError:
|
|
pass
|
|
try:
|
|
from yuxi.channels.adapters.slack import SlackAdapter
|
|
|
|
_BUILTIN_ADAPTERS["slack"] = SlackAdapter
|
|
except ImportError:
|
|
pass
|
|
try:
|
|
from yuxi.channels.adapters.matrix import MatrixAdapter
|
|
|
|
_BUILTIN_ADAPTERS["matrix"] = MatrixAdapter
|
|
except ImportError:
|
|
pass
|
|
try:
|
|
from yuxi.channels.adapters.mattermost import MattermostAdapter
|
|
|
|
_BUILTIN_ADAPTERS["mattermost"] = MattermostAdapter
|
|
except ImportError:
|
|
pass
|
|
try:
|
|
from yuxi.channels.adapters.nostr import NostrAdapter
|
|
|
|
_BUILTIN_ADAPTERS["nostr"] = NostrAdapter
|
|
except ImportError:
|
|
pass
|
|
try:
|
|
from yuxi.channels.adapters.twitch.adapter import TwitchAdapter
|
|
|
|
_BUILTIN_ADAPTERS["twitch"] = TwitchAdapter
|
|
except ImportError:
|
|
pass
|
|
try:
|
|
from yuxi.channels.adapters.imessage import IMessageAdapter
|
|
|
|
_BUILTIN_ADAPTERS["imessage"] = IMessageAdapter
|
|
except ImportError:
|
|
pass
|
|
try:
|
|
from yuxi.channels.adapters.whatsapp import WhatsAppAdapter
|
|
|
|
_BUILTIN_ADAPTERS["whatsapp"] = WhatsAppAdapter
|
|
except ImportError:
|
|
pass
|
|
try:
|
|
from yuxi.channels.adapters.discord import DiscordAdapter
|
|
|
|
_BUILTIN_ADAPTERS["discord"] = DiscordAdapter
|
|
except ImportError:
|
|
pass
|
|
try:
|
|
from yuxi.channels.adapters.irc import IRCAdapter
|
|
|
|
_BUILTIN_ADAPTERS["irc"] = IRCAdapter
|
|
except ImportError:
|
|
pass
|
|
try:
|
|
from yuxi.channels.adapters.dingding import DingDingChannelAdapter
|
|
|
|
_BUILTIN_ADAPTERS["dingding"] = DingDingChannelAdapter
|
|
except ImportError:
|
|
pass
|
|
try:
|
|
from yuxi.channels.adapters.feishu import FeishuAdapter
|
|
|
|
_BUILTIN_ADAPTERS["feishu"] = FeishuAdapter
|
|
except ImportError:
|
|
pass
|
|
try:
|
|
from yuxi.channels.adapters.msteams import MSTeamsAdapter
|
|
|
|
_BUILTIN_ADAPTERS["msteams"] = MSTeamsAdapter
|
|
except ImportError:
|
|
pass
|
|
try:
|
|
from yuxi.channels.adapters.bluebubbles import BlueBubblesAdapter
|
|
|
|
_BUILTIN_ADAPTERS["bluebubbles"] = BlueBubblesAdapter
|
|
except ImportError:
|
|
pass
|
|
try:
|
|
from yuxi.channels.adapters.synologychat import SynologyChatAdapter
|
|
|
|
_BUILTIN_ADAPTERS["synologychat"] = SynologyChatAdapter
|
|
except ImportError:
|
|
pass
|
|
try:
|
|
from yuxi.channels.adapters.nextcloudtalk import NextcloudTalkAdapter
|
|
|
|
_BUILTIN_ADAPTERS["nextcloud-talk"] = NextcloudTalkAdapter
|
|
except ImportError:
|
|
pass
|
|
try:
|
|
from yuxi.channels.adapters.googlechat import GoogleChatAdapter
|
|
|
|
_BUILTIN_ADAPTERS["googlechat"] = GoogleChatAdapter
|
|
except ImportError:
|
|
pass
|
|
try:
|
|
from yuxi.channels.adapters.line import LINEAdapter
|
|
|
|
_BUILTIN_ADAPTERS["line"] = LINEAdapter
|
|
except ImportError:
|
|
pass
|
|
try:
|
|
from yuxi.channels.adapters.qqbot import QQBotAdapter
|
|
|
|
_BUILTIN_ADAPTERS["qqbot"] = QQBotAdapter
|
|
except ImportError:
|
|
pass
|
|
try:
|
|
from yuxi.channels.adapters.signal.channel import SignalChannel
|
|
|
|
_BUILTIN_ADAPTERS["signal"] = SignalChannel
|
|
except ImportError:
|
|
pass
|
|
try:
|
|
from yuxi.channels.adapters.zalo_user import ZaloUserAdapter
|
|
|
|
_BUILTIN_ADAPTERS["zalo_user"] = ZaloUserAdapter
|
|
except ImportError:
|
|
pass
|
|
try:
|
|
from yuxi.channels.adapters.urbit import UrbitAdapter
|
|
|
|
_BUILTIN_ADAPTERS["urbit"] = UrbitAdapter
|
|
except ImportError:
|
|
pass
|
|
try:
|
|
from yuxi.channels.adapters.zalo_oa import ZaloOAAdapter
|
|
|
|
_BUILTIN_ADAPTERS["zalo_oa"] = ZaloOAAdapter
|
|
except ImportError:
|
|
pass
|
|
try:
|
|
from yuxi.channels.adapters.wechat import WeChatAdapter
|
|
|
|
_BUILTIN_ADAPTERS["wechat"] = WeChatAdapter
|
|
except ImportError:
|
|
pass
|
|
try:
|
|
from yuxi.channels.adapters.yuanbao.adapter import YuanbaoAdapter
|
|
|
|
_BUILTIN_ADAPTERS["yuanbao"] = YuanbaoAdapter
|
|
except ImportError:
|
|
pass
|
|
|
|
|
|
class ChannelRegistry:
|
|
def __init__(self):
|
|
self._adapters: dict[str, type[BaseChannelAdapter]] = {}
|
|
self._alias_map: dict[str, str] = {}
|
|
|
|
def _build_alias_map(self) -> None:
|
|
for alias, channel_id in _BUILTIN_ADAPTER_ALIASES.items():
|
|
self._alias_map[alias] = channel_id
|
|
for channel_id, adapter_cls in {**_BUILTIN_ADAPTERS, **self._adapters}.items():
|
|
meta = getattr(adapter_cls, "meta", None)
|
|
if meta and meta.aliases:
|
|
for alias in meta.aliases:
|
|
self._alias_map[alias] = channel_id
|
|
|
|
def get(self, channel_id: str) -> type[BaseChannelAdapter] | None:
|
|
if channel_id in self._adapters:
|
|
return self._adapters[channel_id]
|
|
if channel_id in _BUILTIN_ADAPTERS:
|
|
return _BUILTIN_ADAPTERS[channel_id]
|
|
if not self._alias_map:
|
|
self._build_alias_map()
|
|
resolved = self._alias_map.get(channel_id)
|
|
if resolved:
|
|
return self._adapters.get(resolved) or _BUILTIN_ADAPTERS.get(resolved)
|
|
return None
|
|
|
|
def register(self, channel_id: str, adapter_cls: type[BaseChannelAdapter]) -> None:
|
|
self._adapters[channel_id] = adapter_cls
|
|
|
|
def list_channels(self) -> list[str]:
|
|
return list({*self._adapters.keys(), *_BUILTIN_ADAPTERS.keys()})
|
|
|
|
def unregister(self, channel_id: str) -> None:
|
|
self._adapters.pop(channel_id, None)
|
|
|
|
def is_available(self, channel_id: str) -> bool:
|
|
return channel_id in self._adapters or channel_id in _BUILTIN_ADAPTERS
|
|
|
|
@staticmethod
|
|
def _validate_adapter(adapter_cls: type[BaseChannelAdapter]) -> list[str]:
|
|
CAPABILITY_METHOD_MAP: dict[str, str] = {
|
|
"reactions": "send_reaction",
|
|
"edit": "edit_message",
|
|
"unsend": "delete_message",
|
|
"media": "send_media",
|
|
}
|
|
|
|
caps = getattr(adapter_cls, "capabilities", None)
|
|
if caps is None:
|
|
return []
|
|
|
|
issues: list[str] = []
|
|
for cap_flag, method_name in CAPABILITY_METHOD_MAP.items():
|
|
if not getattr(caps, cap_flag, False):
|
|
continue
|
|
method = getattr(adapter_cls, method_name, None)
|
|
if method is None:
|
|
issues.append(f"Capability '{cap_flag}' declared but method '{method_name}' not found")
|
|
continue
|
|
try:
|
|
source = inspect.getsource(method)
|
|
except (OSError, TypeError):
|
|
continue
|
|
if "raise NotImplementedError" in source or "raise NotImplementedError()" in source:
|
|
issues.append(
|
|
f"Capability '{cap_flag}' declared but method '{method_name}' is not implemented "
|
|
f"(raises NotImplementedError)"
|
|
)
|
|
|
|
return issues
|
|
|
|
def load_builtins(self) -> None:
|
|
"""将 _BUILTIN_ADAPTERS 中的所有内置适配器加载到 self._adapters 中"""
|
|
_load_builtin_adapters()
|
|
for channel_id, adapter_cls in _BUILTIN_ADAPTERS.items():
|
|
self._adapters.setdefault(channel_id, adapter_cls)
|
|
|
|
|
|
BUILTIN_ADAPTERS = _BUILTIN_ADAPTERS
|