ForcePilot/backend/package/yuxi/channel/extensions/xiaohongshu/media.py
Kris 5946478772 feat(channel): 添加小红书、XMPP、元宝和 Zalo 渠道扩展
新增小红书、XMPP、元宝、Zalo 四个渠道扩展。

小红书渠道扩展主要模块:config, gateway, webhook, outbound, streaming, pairing, security, dedupe, media, status, window

XMPP 渠道扩展主要模块:plugin, config, gateway, outbound, streaming, pairing, security, dedupe, accounts, commands, muc, rate_limiter, stanza_utils, status, monitor

元宝渠道扩展主要模块:plugin, client, config_schema, gateway, outbound(chunk/queue/transport), inbound(dispatcher), streaming, pairing, security, accounts, actions, commands, codec(biz/conn), session, shared, utils

Zalo 渠道扩展主要模块:api, config, gateway, webhook, outbound, pairing, security, session, polling, monitor, status
2026-05-21 12:04:05 +08:00

71 lines
2.3 KiB
Python

import asyncio
import logging
import httpx
logger = logging.getLogger(__name__)
DOWNLOAD_URL = "https://open.xiaohongshu.com/api/material/download"
RETRY_MAX = 3
RETRY_BASE_DELAY = 2
class XiaohongshuMedia:
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("code") != 0:
logger.warning(
"Xiaohongshu media download failed (attempt %d): code=%s",
attempt + 1,
data.get("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(
"Xiaohongshu media download exception (attempt %d/%d): %s",
attempt + 1,
RETRY_MAX,
e,
)
await asyncio.sleep(RETRY_BASE_DELAY * (2 ** attempt))
logger.error("Xiaohongshu 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