ForcePilot/backend/package/yuxi/channel/extensions/douyin/media.py
Kris 4552bde837 feat(douyin): 新增抖音渠道插件,支持私信收发与相关能力
该提交实现了完整的抖音开放平台IM渠道插件,包含:
1. 基础配置与账号管理能力
2. 消息去重、安全策略校验
3. 流式回复、多媒体消息发送
4. Webhook回调处理与事件解析
5. 配对认证与流量限流机制
2026-05-21 10:45:37 +08:00

70 lines
2.4 KiB
Python

import asyncio
import logging
import httpx
logger = logging.getLogger(__name__)
DOWNLOAD_URL = "https://open.douyin.com/api/apps/v1/developer_toolbox/image_material/download/"
RETRY_MAX = 3
RETRY_BASE_DELAY = 2
class DouyinMedia:
def __init__(self, token_provider):
self._token_provider = token_provider
self._http: httpx.AsyncClient | None = None
async def _client(self) -> httpx.AsyncClient:
if self._http is None:
self._http = httpx.AsyncClient(timeout=30.0)
return self._http
async def download(self, media_id: str) -> dict:
token = self._token_provider()
if not token:
return {"success": False, "error": "no access_token"}
url = f"{DOWNLOAD_URL}?access_token={token}&media_id={media_id}"
for attempt in range(RETRY_MAX):
try:
client = await self._client()
resp = await client.get(url)
content_type = resp.headers.get("content-type", "")
if "application/json" in content_type or resp.text.startswith("{"):
data = resp.json()
if data.get("data", {}).get("error_code", -1) != 0:
logger.warning(
"Douyin media download failed (attempt %d): error_code=%s",
attempt + 1,
data.get("data", {}).get("error_code"),
)
await asyncio.sleep(RETRY_BASE_DELAY * (2**attempt))
continue
return {"success": False, "error": "unexpected json response"}
return {
"success": True,
"data": resp.content,
"content_type": content_type,
}
except Exception as e:
logger.warning(
"Douyin media download exception (attempt %d/%d): %s",
attempt + 1,
RETRY_MAX,
e,
)
await asyncio.sleep(RETRY_BASE_DELAY * (2**attempt))
logger.error("Douyin media download exhausted retries for media_id=%s", media_id)
return {"success": False, "error": "download retries exhausted"}
async def close(self):
if self._http:
await self._http.aclose()
self._http = None