"""消息适配器 ABC — 所有渠道适配器的基类。 与 KBAdapter 的 authenticate()/close() 生命周期方法对齐: verify_signature() 对应 authenticate()。 """ from __future__ import annotations import abc from dataclasses import dataclass, field from enum import Enum from typing import Any import httpx class ChannelType(str, Enum): """支持的消息平台渠道。""" FEISHU = "feishu" DINGTALK = "dingtalk" WECOM = "wecom" SLACK = "slack" class MessageDirection(str, Enum): """消息流向。""" INBOUND = "inbound" OUTBOUND = "outbound" @dataclass class IncomingMessage: """标准化入站消息 — 所有渠道适配器将平台特定格式转换为此结构。""" channel: ChannelType platform_message_id: str user_id: str # 平台用户 ID chat_id: str # 群组/会话 ID content: str # 消息文本 raw_event: dict[str, Any] = field(default_factory=dict) # 原始事件 timestamp: str = "" @dataclass class OutgoingMessage: """标准化出站消息。""" channel: ChannelType chat_id: str content: str reply_to_message_id: str | None = None class URLVerificationChallenge(Exception): """URL 验证事件 — webhook 端点需返回 ``{"challenge": ...}`` 响应。 飞书 / Slack 在配置 webhook 时发送一次 ``url_verification`` 事件, 要求服务端原样返回 ``challenge`` 字段以验证 URL 可达。 """ def __init__(self, challenge: str) -> None: super().__init__(f"URL verification challenge: {challenge}") self.challenge = challenge class MessageAdapter(abc.ABC): """消息适配器 ABC。 生命周期: __init__ → verify_signature() → receive_message() → send_message() → close() 子类必须实现 verify_signature / receive_message / send_message 抽象方法。 ``_get_client`` / ``close`` 已在基类提供(懒构造 httpx 客户端 + 释放资源), 子类只需在 ``__init__`` 中调用 ``super().__init__()``。 verify_signature 失败时调用方应拒绝处理(webhook 端点 fail-closed: Redis 不可用或签名校验失败均返回 503/401,不可跳过 nonce dedup 直接处理消息)。 """ def __init__(self) -> None: # 懒加载 httpx 客户端 — 避免未使用的适配器持有连接池 self._client: httpx.AsyncClient | None = None def _get_client(self) -> httpx.AsyncClient: """懒构造 httpx 客户端(子类共享此实现)。""" if self._client is None: self._client = httpx.AsyncClient(timeout=10.0) return self._client @abc.abstractmethod async def verify_signature(self, headers: dict[str, str], body: bytes) -> bool: """验证平台签名/token。返回 True 表示请求可信。""" @abc.abstractmethod async def receive_message(self, headers: dict[str, str], body: bytes) -> IncomingMessage: """从 webhook 请求中解析标准化消息。""" @abc.abstractmethod async def send_message(self, message: OutgoingMessage) -> bool: """向平台发送消息。返回 True 表示发送成功。""" async def close(self) -> None: """关闭 httpx 客户端(如已创建)。子类可覆盖以释放额外资源。""" if self._client is not None: await self._client.aclose() self._client = None # --------------------------------------------------------------------------- # 辅助函数 # --------------------------------------------------------------------------- def header_get(headers: dict[str, str], name: str) -> str | None: """大小写不敏感的 header 查找。""" # 直接命中 if name in headers: return headers[name] lower = name.lower() for k, v in headers.items(): if k.lower() == lower: return v return None