Files
2026-04-24 17:31:54 +08:00

169 lines
5.1 KiB
Python

"""
核心基础设施模块
提供HTTP客户端池管理
"""
import httpx
import os
from typing import Dict, Optional, Any, Tuple
import re
class HTTPClientPool:
"""
HTTP客户端池
管理持久化的 httpx.AsyncClient,支持连接复用
"""
_clients: Dict[Tuple[Any, ...], httpx.AsyncClient] = {}
_loop_id: Optional[int] = None # 记录创建客户端时的事件循环ID
@classmethod
def _check_loop_change(cls) -> bool:
"""
检测事件循环是否发生变化
返回: True = 循环已变化,需要清理旧客户端
"""
try:
import asyncio
current_loop = asyncio.get_running_loop()
current_loop_id = id(current_loop)
if cls._loop_id is None:
cls._loop_id = current_loop_id
return False
if cls._loop_id != current_loop_id:
# 事件循环已变化,清理所有旧客户端
cls._loop_id = current_loop_id
# 不能 await close,直接丢弃引用(让 GC 处理)
cls._clients.clear()
return True
return False
except RuntimeError:
# 没有运行中的事件循环,保守处理
return False
@classmethod
def get_client(
cls,
provider: str,
base_url: Optional[str] = None,
timeout: float = 60.0,
proxy: Optional[str] = None,
verify_ssl: bool = True,
**kwargs
) -> httpx.AsyncClient:
"""
获取或创建HTTP客户端(支持连接复用)
参数:
provider: 服务商标识(用于日志)
base_url: API基础URL,作为缓存的Key
timeout: 超时时间(秒)
proxy: 代理设置
verify_ssl: 是否验证SSL证书
"""
# 检测事件循环变化,必要时清理旧客户端
cls._check_loop_change()
# 智能判定是否为本地地址
is_local = False
if base_url:
is_local = any(host in str(base_url) for host in ['localhost', '127.0.0.1', '0.0.0.0', '[::1]'])
trust_env = not is_local
cache_key = (
base_url or provider,
float(timeout),
proxy or "",
bool(verify_ssl),
trust_env,
tuple(sorted((key, repr(value)) for key, value in kwargs.items())),
)
if cache_key in cls._clients:
client = cls._clients[cache_key]
if not client.is_closed:
return client
# 创建新客户端
client_kwargs = {
'timeout': httpx.Timeout(timeout, connect=10.0, read=timeout, write=60.0),
'verify': verify_ssl,
'follow_redirects': True,
'http2': False,
# 关键修复:根据目标地址智能控制系统代理配置
# 避免 HTTP_PROXY/HTTPS_PROXY 拦截本地请求,同时允许外部请求(如xflow, openai)使用代理
'trust_env': trust_env,
# 设置连接池保持连接
'limits': httpx.Limits(max_keepalive_connections=10, max_connections=20, keepalive_expiry=60.0)
}
if proxy:
client_kwargs['proxies'] = proxy
client_kwargs.update(kwargs)
client = httpx.AsyncClient(**client_kwargs)
cls._clients[cache_key] = client
return client
@classmethod
async def close_all(cls):
"""关闭所有已创建的客户端,彻底释放资源"""
for key in list(cls._clients.keys()):
client = cls._clients.pop(key)
try:
await client.aclose()
except:
pass
# Logger 类已移除,请直接从 ..utils.common 导入 log_prepare, log_complete, log_error 等函数使用。
class BaseAPIService:
"""
API服务抽象基类
所有服务(LLM, VLM, Baidu)的基础
"""
def __init__(self, http_client_pool: HTTPClientPool = None):
"""
初始化基类
参数:
http_client_pool: HTTP客户端池(可选,默认使用全局池)
"""
self.http_client_pool = http_client_pool or HTTPClientPool
def get_config(self) -> Dict[str, Any]:
"""
获取服务配置(子类必须实现)
返回:
Dict: 配置字典
"""
raise NotImplementedError("子类必须实现 get_config 方法")
async def handle_error(self, error: Exception, provider: str) -> Dict[str, Any]:
"""
统一错误处理
参数:
error: 异常对象
provider: 服务商标识
返回:
Dict: 错误响应
"""
from ..utils.common import format_api_error
error_message = format_api_error(error, provider)
return {
"success": False,
"error": error_message
}