from __future__ import annotations from typing import Any class FeishuApiClient: """飞书 Open API HTTP 客户端,封装通用请求逻辑。""" DEFAULT_TIMEOUT_S = 30.0 def __init__(self, lark_client, http_client: Any | None = None, timeout: float | None = None): self._lark = lark_client self._http = http_client self._timeout = timeout or self.DEFAULT_TIMEOUT_S @property def domain(self) -> str: return getattr(self._lark, "domain", "https://open.feishu.cn") async def _get_token(self) -> str: resp = self._lark.auth.tenant_access_token_internal() if not resp.success(): raise RuntimeError(f"Token acquisition failed: {resp.msg}") return resp.token async def get(self, path: str, **params: Any) -> dict: import httpx token = await self._get_token() client = self._http or httpx.AsyncClient(timeout=httpx.Timeout(self._timeout)) try: headers = {"Authorization": f"Bearer {token}"} url = f"{self.domain}{path}" resp = await client.get(url, headers=headers, params=params) if resp.status_code != 200: raise RuntimeError(f"HTTP {resp.status_code}: {resp.text[:300]}") return resp.json() if resp.text else {} finally: if self._http is None: await client.aclose() async def post(self, path: str, body: dict | None = None) -> dict: import httpx token = await self._get_token() client = self._http or httpx.AsyncClient(timeout=httpx.Timeout(self._timeout)) try: headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", } url = f"{self.domain}{path}" resp = await client.post(url, headers=headers, json=body) if resp.status_code != 200: raise RuntimeError(f"HTTP {resp.status_code}: {resp.text[:300]}") return resp.json() if resp.text else {} finally: if self._http is None: await client.aclose() async def patch(self, path: str, body: dict) -> dict: import httpx token = await self._get_token() client = self._http or httpx.AsyncClient(timeout=httpx.Timeout(self._timeout)) try: headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", } url = f"{self.domain}{path}" resp = await client.patch(url, headers=headers, json=body) if resp.status_code != 200: raise RuntimeError(f"HTTP {resp.status_code}: {resp.text[:300]}") return resp.json() if resp.text else {} finally: if self._http is None: await client.aclose() async def delete(self, path: str) -> dict: import httpx token = await self._get_token() client = self._http or httpx.AsyncClient(timeout=httpx.Timeout(self._timeout)) try: headers = {"Authorization": f"Bearer {token}"} url = f"{self.domain}{path}" resp = await client.delete(url, headers=headers) if resp.status_code != 200: raise RuntimeError(f"HTTP {resp.status_code}: {resp.text[:300]}") return resp.json() if resp.text else {} finally: if self._http is None: await client.aclose()