""" 钉钉平台适配器 """ import hashlib import hmac import base64 import json import logging import time from typing import Any from ..platform_adapter import register_platform_adapter, BasePlatformAdapter, PlatformMetadata logger = logging.getLogger(__name__) @register_platform_adapter( name="dingtalk", display_name="钉钉", description="钉钉开放平台消息适配器,支持文本、图片、文件等消息类型", support_streaming=False, support_image=True, support_file=True, support_voice=False, ) class DingtalkPlatformAdapter(BasePlatformAdapter): """钉钉平台适配器""" def get_metadata(self) -> PlatformMetadata: from ..platform_adapter import platform_registry return platform_registry["dingtalk"] def verify_signature(self, payload: dict, signature: str, timestamp: str) -> bool: """钉钉签名验证: HMAC-SHA256 Base64""" if not signature or not timestamp: return True # 无签名默认通过 app_secret = self.config.get("app_secret", "") if not app_secret: logger.warning("[DingTalk] 未配置app_secret,跳过签名验证") return True try: # 钉钉签名格式: timestamp + "\n" + appSecret sign_str = f"{timestamp}\n{app_secret}" hmac_code = hmac.new( app_secret.encode("utf-8"), sign_str.encode("utf-8"), hashlib.sha256, ).digest() expected = base64.b64encode(hmac_code).decode("utf-8") return signature == expected except Exception as e: logger.error(f"[DingTalk] 签名验证异常: {e}") return False def parse_webhook(self, payload: dict) -> dict: """解析钉钉webhook payload为统一消息格式""" msg_type = payload.get("msgtype", "text") content = "" content_type = "text" sender_id = "" sender_name = "" message_id = "" if msg_type == "text": text_obj = payload.get("text", {}) content = text_obj.get("content", "") content_type = "text" elif msg_type == "image": image_obj = payload.get("image", {}) content = f"[图片] media_id={image_obj.get('mediaId', '')}" content_type = "image" elif msg_type == "file": file_obj = payload.get("file", {}) content = f"[文件] {file_obj.get('fileName', 'unknown')} media_id={file_obj.get('mediaId', '')}" content_type = "file" elif msg_type == "link": link_obj = payload.get("link", {}) content = f"[链接] {link_obj.get('title', '')} url={link_obj.get('messageUrl', '')}" content_type = "link" elif msg_type == "markdown": md_obj = payload.get("markdown", {}) content = md_obj.get("title", "") content_type = "markdown" elif msg_type == "actionCard": card = payload.get("actionCard", {}) content = f"[卡片] {card.get('title', '')}" content_type = "actionCard" else: content = json.dumps(payload, ensure_ascii=False) content_type = msg_type # 提取发送者信息 sender_id = payload.get("senderId", payload.get("senderStaffId", "")) sender_name = payload.get("senderNick", payload.get("senderCorpId", "")) message_id = str(payload.get("msgId", "")) # 群聊信息 group_id = payload.get("conversationId", payload.get("chatId", None)) group_name = payload.get("conversationTitle", None) return { "content_type": content_type, "content": content, "sender_id": str(sender_id), "sender_name": str(sender_name), "message_id": message_id, "timestamp": int(payload.get("createAt", time.time() * 1000)) // 1000, "group_id": group_id, "group_name": group_name, "raw": payload, } def build_reply(self, channel_config: dict, content: str, reply_to: str = None) -> dict: """构建钉钉回复消息""" webhook_url = channel_config.get("webhook_url", "") if not webhook_url: logger.warning("[DingTalk] 未配置webhook_url,无法构建回复") return {} return { "api_url": webhook_url, "headers": {"Content-Type": "application/json"}, "body": {"msgtype": "text", "text": {"content": content}}, }