import asyncio import logging import httpx from yuxi.channel.extensions.jd.signature import build_signed_params from yuxi.channel.extensions.jd.types import JOS_API_GATEWAY, JOS_MAX_RETRIES, JOS_RETRY_BASE_DELAY, OutboundResult logger = logging.getLogger(__name__) class JOSClient: def __init__(self, app_key: str, app_secret: str, shop_id: str): self._app_key = app_key self._app_secret = app_secret self._shop_id = shop_id self._http = httpx.AsyncClient(timeout=15.0) async def close(self): await self._http.aclose() async def call(self, method: str, biz_params: dict, access_token: str) -> dict: params = build_signed_params( method=method, biz_params=biz_params, app_key=self._app_key, app_secret=self._app_secret, access_token=access_token, ) for attempt in range(JOS_MAX_RETRIES): try: resp = await self._http.post(JOS_API_GATEWAY, data=params) resp.raise_for_status() data = resp.json() error_response = data.get("error_response") if error_response: code = error_response.get("code") msg = error_response.get("zh_desc", "Unknown error") logger.error("JOS API error [%s]: %s", code, msg) raise ValueError(f"JOS API error {code}: {msg}") return data except (httpx.TimeoutException, httpx.NetworkError) as e: logger.warning("JOS API retry %d/%d: %s", attempt + 1, JOS_MAX_RETRIES, e) if attempt == JOS_MAX_RETRIES - 1: raise await asyncio.sleep(JOS_RETRY_BASE_DELAY * (2**attempt)) raise RuntimeError("JOS API max retries exceeded") async def send_msg( self, chat_id: str, to_id: str, msg_type: int, content: str, access_token: str, ) -> OutboundResult: biz_params = { "chat_id": chat_id, "from_id": self._shop_id, "to_id": to_id, "msg_type": msg_type, "content": content, } try: data = await self.call("jingdong.im.sendMsg", biz_params, access_token) result = data.get("jingdong_im_sendMsg_responce", {}).get("result", {}) return OutboundResult( message_id=result.get("msg_id", ""), success=True, ) except Exception as e: return OutboundResult( message_id="", success=False, error_code="JOS_ERROR", error_msg=str(e), )