新增 Webex、微信 iLink、微信小程序三个渠道扩展。 Webex 渠道扩展功能模块: - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - media: 媒体资源处理 微信 iLink 渠道扩展功能模块: - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - context_store: 上下文存储 - aes_ecb: AES-ECB 加解密 - media: 媒体资源处理 - typing: 输入状态 微信小程序渠道扩展功能模块: - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - crypto: 加解密处理 - dedupe: 消息去重 - message: 消息处理 - passive_reply: 被动回复 - media: 媒体资源处理 - status: 会话状态管理
127 lines
4.4 KiB
Python
127 lines
4.4 KiB
Python
import logging
|
|
|
|
import httpx
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
UPLOAD_URL = "https://api.weixin.qq.com/cgi-bin/media/upload"
|
|
DOWNLOAD_URL = "https://api.weixin.qq.com/cgi-bin/media/get"
|
|
|
|
ALLOWED_MEDIA_TYPE_MAP = {
|
|
"image": {"image/jpeg", "image/jpg", "image/png"},
|
|
"voice": {"audio/amr", "audio/mp3", "audio/speex"},
|
|
"video": {"video/mp4"},
|
|
"thumb": {"image/jpeg", "image/jpg"},
|
|
}
|
|
|
|
MAX_BYTES_MAP = {
|
|
"image": 10 * 1024 * 1024,
|
|
"voice": 2 * 1024 * 1024,
|
|
"video": 10 * 1024 * 1024,
|
|
"thumb": 64 * 1024,
|
|
}
|
|
|
|
|
|
class MiniProgramMedia:
|
|
|
|
def __init__(self, access_token_provider, app_id: str = "", app_secret: str = ""):
|
|
self._token_provider = access_token_provider
|
|
self._http: httpx.AsyncClient | None = None
|
|
self._app_id = app_id
|
|
self._app_secret = app_secret
|
|
|
|
async def _client(self) -> httpx.AsyncClient:
|
|
if self._http is None:
|
|
self._http = httpx.AsyncClient(timeout=30.0)
|
|
return self._http
|
|
|
|
async def upload(
|
|
self,
|
|
file_data: bytes,
|
|
filename: str,
|
|
media_type: str,
|
|
content_type: str | None = None,
|
|
) -> dict:
|
|
if media_type not in ALLOWED_MEDIA_TYPE_MAP:
|
|
return {"success": False, "error": f"unknown media_type: {media_type}"}
|
|
|
|
max_bytes = MAX_BYTES_MAP.get(media_type, 0)
|
|
if len(file_data) > max_bytes:
|
|
return {"success": False, "error": f"file too large: {len(file_data)} bytes (max {max_bytes})"}
|
|
|
|
token = self._token_provider()
|
|
if not token:
|
|
return {"success": False, "error": "no access_token"}
|
|
|
|
url = f"{UPLOAD_URL}?access_token={token}&type={media_type}"
|
|
files = {"media": (filename, file_data, content_type or "application/octet-stream")}
|
|
|
|
try:
|
|
client = await self._client()
|
|
resp = await client.post(url, files=files)
|
|
data = resp.json()
|
|
|
|
if "media_id" in data:
|
|
return {"success": True, "media_id": data["media_id"], "type": data.get("type", media_type)}
|
|
if "errcode" in data:
|
|
logger.warning(
|
|
"MiniProgram media upload failed: errcode=%s errmsg=%s",
|
|
data.get("errcode"),
|
|
data.get("errmsg"),
|
|
)
|
|
return {"success": False, "error": data.get("errmsg", "upload failed"), "errcode": data["errcode"]}
|
|
return {"success": False, "error": "unknown response"}
|
|
except Exception as e:
|
|
logger.exception("MiniProgram media upload exception")
|
|
return {"success": False, "error": str(e)}
|
|
|
|
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}"
|
|
|
|
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 "errcode" in data and data["errcode"] != 0:
|
|
logger.warning(
|
|
"MiniProgram media download failed: errcode=%s errmsg=%s",
|
|
data.get("errcode"),
|
|
data.get("errmsg"),
|
|
)
|
|
return {
|
|
"success": False,
|
|
"error": data.get("errmsg", "download failed"),
|
|
"errcode": data["errcode"],
|
|
}
|
|
return {"success": False, "error": "unexpected json response"}
|
|
|
|
return {
|
|
"success": True,
|
|
"data": resp.content,
|
|
"content_type": content_type,
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.exception("MiniProgram media download exception")
|
|
return {"success": False, "error": str(e)}
|
|
|
|
async def download_image_base64(self, media_id: str) -> str:
|
|
result = await self.download(media_id)
|
|
if result.get("success") and result.get("data"):
|
|
import base64
|
|
|
|
return f"data:image/jpeg;base64,{base64.b64encode(result['data']).decode()}"
|
|
return ""
|
|
|
|
async def close(self):
|
|
if self._http:
|
|
await self._http.aclose()
|
|
self._http = None
|