""" 飞书平台适配器 """ import hashlib import hmac import json import logging from typing import Any from ..platform_adapter import register_platform_adapter, BasePlatformAdapter, PlatformMetadata logger = logging.getLogger(__name__) @register_platform_adapter( name="feishu", display_name="飞书", description="飞书开放平台消息适配器,支持文本、图片、文件、富文本消息", support_streaming=True, support_image=True, support_file=True, support_voice=False, ) class FeishuPlatformAdapter(BasePlatformAdapter): """飞书平台适配器""" def get_metadata(self) -> PlatformMetadata: from ..platform_adapter import platform_registry return platform_registry["feishu"] def verify_signature(self, payload: dict, signature: str, timestamp: str) -> bool: """飞书签名验证: HMAC-SHA256""" if not signature or not timestamp: # 飞书首次URL验证(challenge)不需要签名验证 if payload.get("challenge"): return True return True # 宽松模式 app_secret = self.config.get("app_secret", "") if not app_secret: logger.warning("[Feishu] 未配置app_secret,跳过签名验证") return True # 飞书签名格式: timestamp + nonce + body try: body_str = json.dumps(payload, separators=(",", ":"), ensure_ascii=False) nonce = timestamp # 飞书用timestamp作为nonce的一部分 sign_str = f"{timestamp}\n{nonce}\n{body_str}" expected = hmac.new( app_secret.encode("utf-8"), sign_str.encode("utf-8"), hashlib.sha256 ).hexdigest() return hmac.compare_digest(signature, expected) except Exception as e: logger.error(f"[Feishu] 签名验证异常: {e}") return False def parse_webhook(self, payload: dict) -> dict: """解析飞书webhook payload为统一消息格式""" # 飞书URL验证challenge已在外部处理,这里处理正常消息 event = payload.get("event", {}) message = event.get("message", {}) msg_type = message.get("message_type", "text") raw_content = message.get("content", "{}") content = "" content_type = "text" sender_id = event.get("sender", {}).get("sender_id", {}).get("open_id", "") sender_name = event.get("sender", {}).get("sender_id", {}).get("open_id", "") message_id = event.get("header", {}).get("message_id", "") try: if isinstance(raw_content, str): content_obj = json.loads(raw_content) else: content_obj = raw_content if isinstance(raw_content, dict) else {} if msg_type == "text": content = content_obj.get("text", "") content_type = "text" elif msg_type == "image": image_key = message.get("image_key", "") content = f"[图片] image_key={image_key}" content_type = "image" elif msg_type == "file": file_key = message.get("file_key", "") file_name = content_obj.get("file_name", "unknown") content = f"[文件] {file_name} file_key={file_key}" content_type = "file" elif msg_type == "post": # 富文本消息,提取text内容 post_content = content_obj.get("content", []) text_parts = [] for paragraph in post_content: if isinstance(paragraph, list): for element in paragraph: if isinstance(element, dict) and element.get("tag") == "text": text_parts.append(element.get("text", "")) content = "".join(text_parts) content_type = "post" else: content = str(content_obj) content_type = msg_type except (json.JSONDecodeError, TypeError) as e: logger.warning(f"[Feishu] 解析content失败: {e}, raw={str(raw_content)[:200]}") content = str(raw_content) return { "content_type": content_type, "content": content, "sender_id": sender_id, "sender_name": sender_name, "message_id": message_id, "timestamp": event.get("header", {}).get("create_time", 0), "group_id": event.get("chat_id", None), "group_name": None, "raw": payload, } def build_reply(self, channel_config: dict, content: str, reply_to: str = None) -> dict: """构建飞书回复消息""" app_id = channel_config.get("app_id", "") app_secret = channel_config.get("app_secret", "") return { "api_url": "https://open.feishu.cn/open-apis/im/v1/messages", "headers": { "Content-Type": "application/json", "Authorization": f"Bearer {channel_config.get('tenant_access_token', '')}", }, "body": { "msg_type": "text", "content": json.dumps({"text": content}), }, }