Download app/core/captcha_client.py from bg5/zai: direct link, hf CLI and curl.
- Browser
- Download file 7.15 kB
-
https://huggingface.co/spaces/bg5/zai/resolve/main/app/core/captcha_client.py
- Command line
-
hf download hf://spaces/bg5/zai/app/core/captcha_client.py
-
curl -L -o captcha_client.py https://huggingface.co/spaces/bg5/zai/resolve/main/app/core/captcha_client.py
7.15 kB
| #!/usr/bin/env python | |
| # -*- coding: utf-8 -*- | |
| """Captcha 服务客户端。 | |
| 通过 HTTP 调用独立的 Captcha Token Service 获取 captcha_verify_param。 | |
| 支持重试、超时、连接错误处理,与项目 SharedHttpClients 模式一致。 | |
| """ | |
| import asyncio | |
| from typing import Optional | |
| import httpx | |
| from app.core.config import settings | |
| from app.core.http_client import build_timeout, get_proxy_config | |
| from app.utils.logger import logger | |
| class CaptchaServiceError(Exception): | |
| """Captcha 服务不可用(连接失败、超时、非 200 响应)。""" | |
| class CaptchaTokenError(Exception): | |
| """Captcha token 获取失败(服务返回错误)。""" | |
| class CaptchaClient: | |
| """Captcha Token Service 的 HTTP 客户端。 | |
| 按需调用远端 captcha 服务获取验证码 token, | |
| 内置重试和指数退避。 | |
| """ | |
| def __init__( | |
| self, | |
| service_url: Optional[str] = None, | |
| timeout: Optional[float] = None, | |
| ) -> None: | |
| self._service_url = (service_url or settings.CAPTCHA_SERVICE_URL).rstrip("/") | |
| self._timeout = timeout or settings.CAPTCHA_SERVICE_TIMEOUT | |
| self._client: Optional[httpx.AsyncClient] = None | |
| self._max_retries = 3 | |
| def token_url(self) -> str: | |
| return f"{self._service_url}/token" | |
| def health_url(self) -> str: | |
| return f"{self._service_url}/health" | |
| async def _get_client(self) -> httpx.AsyncClient: | |
| if self._client is None or self._client.is_closed: | |
| self._client = httpx.AsyncClient( | |
| timeout=build_timeout(read_timeout=self._timeout), | |
| proxy=get_proxy_config(), | |
| trust_env=False, | |
| ) | |
| return self._client | |
| async def get_token(self, jwt_token: str) -> str: | |
| """获取一个新鲜的 captcha_verify_param。 | |
| Args: | |
| jwt_token: 用户的 Z.ai JWT token。 | |
| Returns: | |
| captcha_verify_param 字符串(base64 编码的 captcha 数据)。 | |
| Raises: | |
| CaptchaServiceError: 服务不可用或超时。 | |
| CaptchaTokenError: 服务返回错误。 | |
| """ | |
| last_error: Optional[Exception] = None | |
| for attempt in range(self._max_retries): | |
| try: | |
| client = await self._get_client() | |
| response = await client.post( | |
| self.token_url, | |
| json={"jwt_token": jwt_token}, | |
| ) | |
| if response.status_code == 200: | |
| data = response.json() | |
| token = data.get("captcha_verify_param") | |
| if token: | |
| logger.info( | |
| "[captcha] token obtained certify_id=%s attempt=%d", | |
| data.get("certify_id", "unknown"), | |
| attempt + 1, | |
| ) | |
| return token | |
| raise CaptchaTokenError( | |
| f"Captcha service returned empty token: {data}" | |
| ) | |
| error_text = response.text[:500] | |
| if response.status_code == 503: | |
| if attempt < self._max_retries - 1: | |
| wait = 2 ** attempt | |
| logger.warning( | |
| "[captcha] service unavailable (503), retry in %ds (attempt %d/%d): %s", | |
| wait, attempt + 1, self._max_retries, error_text, | |
| ) | |
| await asyncio.sleep(wait) | |
| last_error = CaptchaServiceError( | |
| f"Captcha service unavailable: {error_text}" | |
| ) | |
| continue | |
| raise CaptchaServiceError( | |
| f"Captcha service unavailable after {self._max_retries} attempts: {error_text}" | |
| ) | |
| raise CaptchaTokenError( | |
| f"Captcha service returned {response.status_code}: {error_text}" | |
| ) | |
| except (httpx.ConnectError, httpx.ConnectTimeout, httpx.ReadTimeout) as e: | |
| if attempt < self._max_retries - 1: | |
| wait = 2 ** attempt | |
| logger.warning( | |
| "[captcha] connection failed, retry in %ds (attempt %d/%d): %s", | |
| wait, attempt + 1, self._max_retries, e, | |
| ) | |
| await asyncio.sleep(wait) | |
| last_error = e | |
| continue | |
| raise CaptchaServiceError( | |
| f"Cannot reach captcha service at {self._service_url}: {e}" | |
| ) from e | |
| except (CaptchaServiceError, CaptchaTokenError): | |
| raise | |
| except Exception as e: | |
| if attempt < self._max_retries - 1: | |
| wait = 2 ** attempt | |
| logger.warning( | |
| "[captcha] unexpected error, retry in %ds (attempt %d/%d): %s", | |
| wait, attempt + 1, self._max_retries, e, | |
| ) | |
| await asyncio.sleep(wait) | |
| last_error = e | |
| continue | |
| raise CaptchaServiceError(f"Captcha client error: {e}") from e | |
| raise CaptchaServiceError( | |
| f"Failed to get captcha token after {self._max_retries} attempts" | |
| ) from last_error | |
| async def health(self) -> dict: | |
| """检查 captcha 服务健康状态。""" | |
| try: | |
| client = await self._get_client() | |
| response = await client.get(self.health_url) | |
| return response.json() if response.status_code == 200 else {"status": "error"} | |
| except Exception: | |
| return {"status": "unreachable"} | |
| async def close(self) -> None: | |
| if self._client and not self._client.is_closed: | |
| await self._client.aclose() | |
| # Module-level singleton | |
| _captcha_client: Optional[CaptchaClient] = None | |
| def get_captcha_client() -> Optional[CaptchaClient]: | |
| """获取 captcha 客户端单例。未初始化时返回 None。""" | |
| return _captcha_client | |
| def create_captcha_client( | |
| service_url: Optional[str] = None, | |
| timeout: Optional[float] = None, | |
| ) -> CaptchaClient: | |
| """创建并注册 captcha 客户端单例。""" | |
| global _captcha_client | |
| _captcha_client = CaptchaClient(service_url=service_url, timeout=timeout) | |
| logger.info( | |
| "[captcha] client initialized service_url=%s timeout=%s", | |
| _captcha_client._service_url, | |
| _captcha_client._timeout, | |
| ) | |
| return _captcha_client | |
| async def close_captcha_client() -> None: | |
| """关闭 captcha 客户端连接。""" | |
| global _captcha_client | |
| if _captcha_client: | |
| await _captcha_client.close() | |
| _captcha_client = None | |
| logger.info("[captcha] client closed") | |