新增小红书、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
71 lines
2.3 KiB
Python
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
|