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)
|