Files
qyyfw 78ff261b5a 初始提交: Qihu-AIFlow v2.0.1 完整项目
- 前端 React+TypeScript (源码+构建产物)
- 后端 Python FastAPI
- 数据库迁移 001-015
- Docker部署配置
- 微信ClawBot API频道绑定
- 项目文档 README.md
2026-07-02 14:40:46 +08:00

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}},
}