- 前端 React+TypeScript (源码+构建产物) - 后端 Python FastAPI - 数据库迁移 001-015 - Docker部署配置 - 微信ClawBot API频道绑定 - 项目文档 README.md
126 lines
4.5 KiB
Python
126 lines
4.5 KiB
Python
"""
|
|
钉钉平台适配器
|
|
"""
|
|
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}},
|
|
}
|