178 lines
5.6 KiB
Python
178 lines
5.6 KiB
Python
|
|
from typing import Any
|
|||
|
|
|
|||
|
|
from yuxi.channel.exceptions import (
|
|||
|
|
ChannelErrorClassification,
|
|||
|
|
ChannelPermanentError,
|
|||
|
|
ChannelRateLimitedError,
|
|||
|
|
ChannelRetryableError,
|
|||
|
|
ChannelValidationError,
|
|||
|
|
)
|
|||
|
|
from yuxi.channel.plugins.protocol import ChannelMeta, ChannelPlugin, InboundMedia, InboundMessage, OutboundMessage
|
|||
|
|
from yuxi.channel.plugins.registry import get_registry
|
|||
|
|
from yuxi.channel.ports import (
|
|||
|
|
AuthMixin,
|
|||
|
|
BindingMixin,
|
|||
|
|
ConfigMixin,
|
|||
|
|
InboundMixin,
|
|||
|
|
LifecycleMixin,
|
|||
|
|
MetaMixin,
|
|||
|
|
OutboundMixin,
|
|||
|
|
SecurityMixin,
|
|||
|
|
SessionMixin,
|
|||
|
|
StatusMixin,
|
|||
|
|
ThreadingMixin,
|
|||
|
|
TransportMixin,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
_META_FIELDS = {
|
|||
|
|
"aliases",
|
|||
|
|
"capabilities",
|
|||
|
|
"delivery_mode",
|
|||
|
|
"transport_type",
|
|||
|
|
"ui_hints",
|
|||
|
|
"sort_weight",
|
|||
|
|
"icon",
|
|||
|
|
"description",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
class _BaseChannelPlugin(
|
|||
|
|
MetaMixin,
|
|||
|
|
ConfigMixin,
|
|||
|
|
InboundMixin,
|
|||
|
|
OutboundMixin,
|
|||
|
|
TransportMixin,
|
|||
|
|
SecurityMixin,
|
|||
|
|
StatusMixin,
|
|||
|
|
LifecycleMixin,
|
|||
|
|
SessionMixin,
|
|||
|
|
AuthMixin,
|
|||
|
|
BindingMixin,
|
|||
|
|
ThreadingMixin,
|
|||
|
|
):
|
|||
|
|
"""提供 ChannelPlugin Protocol 的默认实现,允许通过适配器覆盖指定方法。"""
|
|||
|
|
|
|||
|
|
def __init__(self, meta: ChannelMeta, adapters: dict[str, Any]) -> None:
|
|||
|
|
MetaMixin.__init__(self, meta)
|
|||
|
|
for name, impl in adapters.items():
|
|||
|
|
if callable(impl):
|
|||
|
|
setattr(self, name, impl)
|
|||
|
|
|
|||
|
|
# === 动作交互(可选扩展点) ===
|
|||
|
|
def describe_actions(self, config: dict, inbound: InboundMessage) -> list[dict]:
|
|||
|
|
"""描述当前消息可触发的动作;默认无动作。"""
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
def handle_action(self, action: dict, config: dict, inbound: InboundMessage) -> dict | None:
|
|||
|
|
"""执行动作;默认返回 None 表示未处理。"""
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
# === 扫码配对(可选扩展点) ===
|
|||
|
|
def supports_scan_pairing(self, config: dict | None = None) -> bool:
|
|||
|
|
return bool(config.get("pairing", {}).get("mode") in ("qr", "both")) if config else False
|
|||
|
|
|
|||
|
|
async def normalize_scan_event(
|
|||
|
|
self,
|
|||
|
|
request: Any,
|
|||
|
|
config: dict,
|
|||
|
|
account_id: str,
|
|||
|
|
) -> InboundMessage | None:
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
async def build_pairing_qr_reply(
|
|||
|
|
self,
|
|||
|
|
pairing_code: str,
|
|||
|
|
qr_content: str,
|
|||
|
|
config: dict,
|
|||
|
|
account_id: str,
|
|||
|
|
) -> OutboundMessage:
|
|||
|
|
return OutboundMessage(
|
|||
|
|
content=f"请扫描下方二维码完成绑定:{pairing_code}",
|
|||
|
|
content_type="text",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# === 媒体附件 ===
|
|||
|
|
async def download_attachment(self, inbound: InboundMessage, media: InboundMedia) -> tuple[bytes, str] | None:
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
# === Webhook 管理(可选扩展点) ===
|
|||
|
|
async def setup_webhook(self, config: dict, callback_url: str) -> bool:
|
|||
|
|
"""向渠道平台注册 Webhook;默认返回 False,表示未实现。"""
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
async def delete_webhook(self, config: dict) -> bool:
|
|||
|
|
"""向渠道平台注销 Webhook;默认返回 False,表示未实现。"""
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
# === 消息编辑与删除(可选扩展点) ===
|
|||
|
|
async def edit_message(
|
|||
|
|
self,
|
|||
|
|
channel_session_id: str,
|
|||
|
|
channel_message_id: str,
|
|||
|
|
new_payload: dict,
|
|||
|
|
config: dict | None = None,
|
|||
|
|
) -> bool:
|
|||
|
|
"""编辑已发送消息;默认返回 False,表示未实现。"""
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
async def delete_message(
|
|||
|
|
self,
|
|||
|
|
channel_session_id: str,
|
|||
|
|
channel_message_id: str,
|
|||
|
|
config: dict | None = None,
|
|||
|
|
) -> bool:
|
|||
|
|
"""删除已发送消息;默认返回 False,表示未实现。"""
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
# === 用户资料解析(可选扩展点) ===
|
|||
|
|
async def resolve_user_profile(self, sender_id: str, config: dict | None = None) -> dict | None:
|
|||
|
|
"""解析发送者资料;默认返回 None,表示未实现。"""
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
# === 入站/出站转换钩子(可选扩展点) ===
|
|||
|
|
async def transform_inbound(self, raw_event: dict, config: dict | None = None) -> dict:
|
|||
|
|
"""在 normalize_inbound 之前转换原始事件;默认原样返回。"""
|
|||
|
|
return raw_event
|
|||
|
|
|
|||
|
|
# === 投递异常分类(可选扩展点) ===
|
|||
|
|
def classify_error(
|
|||
|
|
self,
|
|||
|
|
exc: Exception,
|
|||
|
|
payload: dict | None = None,
|
|||
|
|
) -> tuple[ChannelErrorClassification, int | None]:
|
|||
|
|
"""对投递异常进行分类;默认按异常基类推断。"""
|
|||
|
|
if isinstance(exc, ChannelValidationError):
|
|||
|
|
return ChannelErrorClassification.PERMANENT, None
|
|||
|
|
if isinstance(exc, ChannelPermanentError):
|
|||
|
|
return ChannelErrorClassification.PERMANENT, None
|
|||
|
|
if isinstance(exc, ChannelRateLimitedError):
|
|||
|
|
return ChannelErrorClassification.RATE_LIMITED, exc.retry_after
|
|||
|
|
if isinstance(exc, ChannelRetryableError):
|
|||
|
|
return ChannelErrorClassification.RETRYABLE, None
|
|||
|
|
# 未知异常保守视为可重试
|
|||
|
|
return ChannelErrorClassification.RETRYABLE, None
|
|||
|
|
|
|||
|
|
|
|||
|
|
def create_channel_plugin_base(
|
|||
|
|
channel_type: str,
|
|||
|
|
display_name: str,
|
|||
|
|
config_schema: dict,
|
|||
|
|
**adapters: Any,
|
|||
|
|
) -> ChannelPlugin:
|
|||
|
|
meta_kwargs = {k: adapters.pop(k) for k in list(adapters.keys()) if k in _META_FIELDS}
|
|||
|
|
meta = ChannelMeta(
|
|||
|
|
channel_type=channel_type,
|
|||
|
|
display_name=display_name,
|
|||
|
|
config_schema=config_schema,
|
|||
|
|
**meta_kwargs,
|
|||
|
|
)
|
|||
|
|
return _BaseChannelPlugin(meta=meta, adapters=adapters)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def define_channel_plugin_entry(plugin: ChannelPlugin, register_mode: str = "full") -> None:
|
|||
|
|
registry = get_registry()
|
|||
|
|
if register_mode == "discovery":
|
|||
|
|
registry.register_primary(plugin)
|
|||
|
|
return
|
|||
|
|
registry.register(plugin)
|