ForcePilot/backend/package/yuxi/channel/common/http.py
Kris bab30f2715
Some checks failed
Deploy VitePress site to Pages / build (push) Has been cancelled
Ruff Format Check / Ruff Format & Lint (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
feat:0715
2026-07-15 12:30:58 +08:00

152 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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