from __future__ import annotations from collections.abc import Mapping from typing import Any import httpx from yuxi.utils.logging_config import logger # httpx 接受的 multipart 文件参数:字段名 -> 文件说明或 (文件名, 内容, MIME) 等 RequestFiles = list[tuple[str, Any]] | Mapping[str, Any] class ChannelHttpClient: """渠道网关统一的异步 HTTP 客户端。 封装 ``httpx.AsyncClient``,自动完成响应状态检查、JSON 解析与错误日志, 简化各渠道插件中的 ``send_message``、``health_check`` 与媒体上传实现。 """ def __init__(self, client: httpx.AsyncClient | None = None) -> None: self._own_client = client is None self._client = client or httpx.AsyncClient() async def __aenter__(self) -> ChannelHttpClient: return self async def __aexit__(self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: Any) -> None: if self._own_client: await self._client.aclose() async def request_json( self, method: str, url: str, *, json: dict | None = None, params: dict[str, Any] | None = None, headers: dict[str, str] | None = None, files: RequestFiles | None = None, timeout: float = 60.0, ) -> dict[str, Any]: try: resp = await self._client.request( method, url, json=json, params=params, headers=headers, files=files, timeout=timeout, ) resp.raise_for_status() return resp.json() except httpx.HTTPStatusError as exc: logger.error( "Channel HTTP %s %s failed: %d %s", method, url, exc.response.status_code, exc.response.text[:200], ) raise except httpx.RequestError as exc: logger.error( "Channel HTTP %s %s request error: %s", method, url, exc, ) raise except json.JSONDecodeError as exc: logger.error( "Channel HTTP %s %s returned invalid JSON: %s", method, url, exc, ) raise async def post_json( self, url: str, *, json: dict | None = None, params: dict[str, Any] | None = None, headers: dict[str, str] | None = None, files: RequestFiles | None = None, timeout: float = 60.0, ) -> dict[str, Any]: return await self.request_json( "POST", url, json=json, params=params, headers=headers, files=files, timeout=timeout, ) async def get_json( self, url: str, *, params: dict[str, Any] | None = None, headers: dict[str, str] | None = None, timeout: float = 60.0, ) -> dict[str, Any]: return await self.request_json( "GET", url, params=params, headers=headers, timeout=timeout, ) async def get_bytes( self, url: str, *, params: dict[str, Any] | None = None, headers: dict[str, str] | None = None, timeout: float = 60.0, follow_redirects: bool = True, ) -> bytes: """Execute a GET request and return the raw response body as bytes.""" try: resp = await self._client.get( url, params=params, headers=headers, timeout=timeout, follow_redirects=follow_redirects, ) resp.raise_for_status() return resp.content except httpx.HTTPStatusError as exc: logger.error( "Channel HTTP GET %s failed: %d %s", url, exc.response.status_code, exc.response.text[:200], ) raise except httpx.RequestError as exc: logger.error( "Channel HTTP GET %s request error: %s", url, exc, ) raise