ForcePilot/backend/package/yuxi/channel/plugins/builders.py
Kris bab30f2715
Some checks failed
Deploy VitePress site to Pages / build (push) Has been cancelled
Ruff Format Check / Ruff Format & Lint (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
feat:0715
2026-07-15 12:30:58 +08:00

178 lines
5.6 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 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)