ForcePilot/backend/package/yuxi/channel/extensions/dingtalk/media.py
Kris 472afc4d1e feat(dingtalk): 新增钉钉渠道插件完整实现
该提交实现了完整的钉钉聊天渠道插件,包含:
1. 基础配置、账号管理与凭证校验
2. WebSocket长连接网关与消息去重
3. 消息接收/解析/分发与安全校验
4. 媒体文件上传下载与缓存
5. 互动卡片流式更新与回调处理
6. 群管理、命令支持与诊断工具
7. 完整的插件元数据与依赖声明
2026-05-21 10:44:03 +08:00

115 lines
3.0 KiB
Python

from __future__ import annotations
import hashlib
import logging
import os
import tempfile
import httpx
logger = logging.getLogger(__name__)
MEDIA_UPLOAD_URL = "https://oapi.dingtalk.com/media/upload"
MESSAGE_FILES_DOWNLOAD_URL = "https://api.dingtalk.com/v1.0/robot/messageFiles/download"
async def upload_media(
http: httpx.AsyncClient,
token: str,
file_path: str,
media_type: str,
) -> str | None:
if file_path.startswith("file://"):
file_path = file_path[7:]
if file_path.startswith(("http://", "https://")):
file_path = await _download_to_tmp(http, file_path)
if not os.path.exists(file_path):
logger.error("Media file not found: %s", file_path)
return None
with open(file_path, "rb") as f:
resp = await http.post(
MEDIA_UPLOAD_URL,
params={"access_token": token, "type": media_type},
files={"media": (os.path.basename(file_path), f)},
timeout=60.0,
)
data = resp.json()
if data.get("errcode") == 0:
return data.get("media_id")
logger.error("Media upload failed: %s", data)
return None
async def download_image(
http: httpx.AsyncClient,
token: str,
download_code: str,
robot_code: str,
save_dir: str | None = None,
) -> str | None:
resp = await http.post(
MESSAGE_FILES_DOWNLOAD_URL,
headers={
"x-acs-dingtalk-access-token": token,
"Content-Type": "application/json",
},
json={
"downloadCode": download_code,
"robotCode": robot_code,
},
timeout=30.0,
)
data = resp.json()
download_url = data.get("downloadUrl")
if not download_url:
logger.error("Failed to get download URL: %s", data)
return None
img_resp = await http.get(download_url, timeout=30.0)
img_resp.raise_for_status()
img_data = img_resp.content
ext = _guess_extension(img_data)
filename = f"{hashlib.md5(img_data).hexdigest()}{ext}"
if save_dir:
os.makedirs(save_dir, exist_ok=True)
save_path = os.path.join(save_dir, filename)
else:
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=ext)
save_path = tmp.name
tmp.close()
with open(save_path, "wb") as f:
f.write(img_data)
logger.debug("Image downloaded: %s -> %s", download_code, save_path)
return save_path
async def _download_to_tmp(http: httpx.AsyncClient, url: str) -> str:
resp = await http.get(url, timeout=60.0)
resp.raise_for_status()
data = resp.content
ext = _guess_extension(data)
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=ext)
tmp.write(data)
tmp.close()
return tmp.name
def _guess_extension(data: bytes) -> str:
if data[:4] == b"\x89PNG":
return ".png"
if data[:2] == b"\xff\xd8":
return ".jpg"
if data[:4] == b"GIF8":
return ".gif"
if data[:4] == b"RIFF" and data[8:12] == b"WEBP":
return ".webp"
return ".bin"