Files
Aether/_deprecated_py_src/clients/http_client.py
fawney19 1d9c77522a refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/
- 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层
- 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构
- 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations)
- 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image
- 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
2026-04-03 16:26:16 +08:00

759 lines
29 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.
"""
全局HTTP客户端池管理
避免每次请求都创建新的AsyncClient,提高性能
性能优化说明:
1. 默认客户端:无代理场景,全局复用单一客户端
2. 代理客户端缓存:相同代理配置复用同一客户端,避免重复创建
3. 连接池复用Keep-alive 连接减少 TCP 握手开销
"""
from __future__ import annotations
import asyncio
import os
import time
from contextlib import asynccontextmanager
from typing import Any
import httpx
from src.config import config
from src.core.logger import logger
from src.services.proxy_node.resolver import (
build_proxy_url_async,
compute_proxy_cache_key,
get_system_proxy_config_async,
make_proxy_param,
resolve_delegate_config_async,
)
from src.utils.ssl_utils import get_ssl_context, get_ssl_context_for_profile
# 模块级锁,避免类属性延迟初始化的竞态条件
_proxy_clients_lock = asyncio.Lock()
_default_client_lock = asyncio.Lock()
def _get_int_env(name: str, default: int, minimum: int) -> int:
"""Read positive integer env value with bounds and fallback."""
raw = os.getenv(name)
if raw is None:
return default
try:
value = int(raw)
except ValueError:
logger.warning("环境变量 {} 不是有效整数: {}, 使用默认值 {}", name, raw, default)
return default
return max(minimum, value)
class HTTPClientPool:
"""
全局HTTP客户端池单例
管理可重用的httpx.AsyncClient实例,避免频繁创建/销毁连接
性能优化:
1. 默认客户端:无代理场景复用
2. 代理客户端缓存:相同代理配置复用同一客户端
3. LRU 淘汰:代理客户端超过上限时淘汰最久未使用的
"""
_instance: HTTPClientPool | None = None
_default_client: httpx.AsyncClient | None = None
_clients: dict[str, httpx.AsyncClient] = {}
_max_named_clients: int = 20
# 代理客户端缓存:{cache_key: (client, last_used_time)}
_proxy_clients: dict[str, tuple[httpx.AsyncClient, float]] = {}
# 代理客户端缓存上限(避免内存泄漏)
_max_proxy_clients: int = 50
# Tunnel 客户端缓存:{node_id: (client, last_used_time)}
_tunnel_clients: dict[str, tuple[httpx.AsyncClient, float]] = {}
_max_tunnel_clients: int = 30
# 后台清理任务引用集合(防止被 GC 回收)
_background_tasks: set[asyncio.Task[None]] = set()
def __new__(cls) -> "HTTPClientPool":
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
@classmethod
async def get_default_client_async(cls) -> httpx.AsyncClient:
"""
获取默认的HTTP客户端异步线程安全版本
用于大多数HTTP请求,具有合理的默认配置
"""
if cls._default_client is not None:
return cls._default_client
async with _default_client_lock:
# 双重检查,避免重复创建
if cls._default_client is None:
cls._default_client = httpx.AsyncClient(
http2=config.enable_http2,
verify=get_ssl_context(), # 使用 certifi 证书
timeout=httpx.Timeout(
connect=config.http_connect_timeout,
read=config.http_read_timeout,
write=config.http_write_timeout,
pool=config.http_pool_timeout,
),
limits=httpx.Limits(
max_connections=config.http_max_connections,
max_keepalive_connections=config.http_keepalive_connections,
keepalive_expiry=config.http_keepalive_expiry,
),
follow_redirects=True, # 跟随重定向
)
logger.info(
"全局HTTP客户端池已初始化: max_connections={}, keepalive={}, keepalive_expiry={}s",
config.http_max_connections,
config.http_keepalive_connections,
config.http_keepalive_expiry,
)
return cls._default_client
@classmethod
def get_default_client(cls) -> httpx.AsyncClient:
"""
获取默认的HTTP客户端同步版本向后兼容
⚠️ 注意:此方法在高并发首次调用时可能存在竞态条件,
推荐使用 get_default_client_async() 异步版本。
"""
if cls._default_client is None:
cls._default_client = httpx.AsyncClient(
http2=config.enable_http2,
verify=get_ssl_context(), # 使用 certifi 证书
timeout=httpx.Timeout(
connect=config.http_connect_timeout,
read=config.http_read_timeout,
write=config.http_write_timeout,
pool=config.http_pool_timeout,
),
limits=httpx.Limits(
max_connections=config.http_max_connections,
max_keepalive_connections=config.http_keepalive_connections,
keepalive_expiry=config.http_keepalive_expiry,
),
follow_redirects=True, # 跟随重定向
)
logger.info(
"全局HTTP客户端池已初始化: max_connections={}, keepalive={}, keepalive_expiry={}s",
config.http_max_connections,
config.http_keepalive_connections,
config.http_keepalive_expiry,
)
return cls._default_client
@classmethod
def get_client(cls, name: str, **kwargs: Any) -> httpx.AsyncClient:
"""
获取或创建命名的HTTP客户端
用于需要特定配置的场景(如不同的超时设置、代理等)
Args:
name: 客户端标识符
**kwargs: httpx.AsyncClient的配置参数
"""
if name in cls._clients:
# 命中缓存:移到末尾以维护 LRU 顺序
cls._clients[name] = cls._clients.pop(name)
return cls._clients[name]
# 淘汰最久未使用的客户端dict 头部即 LRU
if len(cls._clients) >= cls._max_named_clients:
oldest_name = next(iter(cls._clients))
old_client = cls._clients.pop(oldest_name)
try:
asyncio.get_running_loop().create_task(old_client.aclose())
except RuntimeError:
pass
logger.debug("淘汰命名HTTP客户端: {}", oldest_name)
# 合并默认配置和自定义配置
default_config = {
"http2": config.enable_http2,
"verify": get_ssl_context(),
"timeout": httpx.Timeout(
connect=config.http_connect_timeout,
read=config.http_read_timeout,
write=config.http_write_timeout,
pool=config.http_pool_timeout,
),
"follow_redirects": True,
}
default_config.update(kwargs)
cls._clients[name] = httpx.AsyncClient(**default_config) # type: ignore[arg-type]
logger.debug("创建命名HTTP客户端: {}", name)
return cls._clients[name]
@classmethod
def _get_proxy_clients_lock(cls) -> asyncio.Lock:
"""获取代理客户端缓存锁(模块级单例,避免竞态条件)"""
return _proxy_clients_lock
@classmethod
async def _evict_lru_proxy_client(cls) -> None:
"""淘汰最久未使用的代理客户端"""
if len(cls._proxy_clients) < cls._max_proxy_clients:
return
# 找到最久未使用的客户端
oldest_key = min(cls._proxy_clients.keys(), key=lambda k: cls._proxy_clients[k][1])
old_client, _ = cls._proxy_clients.pop(oldest_key)
# 异步关闭旧客户端
try:
await old_client.aclose()
logger.debug("淘汰代理客户端: {}", oldest_key)
except Exception as e:
logger.warning("关闭代理客户端失败: {}", e)
@classmethod
async def get_proxy_client(
cls,
proxy_config: dict[str, Any] | None = None,
tls_profile: str | None = None,
) -> httpx.AsyncClient:
"""
获取代理客户端(带缓存复用)
相同代理配置会复用同一个客户端,大幅减少连接建立开销。
当 proxy_config 为 None 时,自动回退到系统默认代理节点。
注意:返回的客户端使用默认超时配置,如需自定义超时请在请求时传递 timeout 参数。
Args:
proxy_config: 代理配置字典,为 None 时使用系统默认代理
Returns:
可复用的 httpx.AsyncClient 实例
"""
# 无特定代理时,回退到系统默认代理
if not proxy_config:
proxy_config = await get_system_proxy_config_async()
delegate_cfg = await resolve_delegate_config_async(proxy_config)
if delegate_cfg and delegate_cfg.get("tunnel"):
return await cls._get_tunnel_client(delegate_cfg["node_id"])
cache_key = compute_proxy_cache_key(proxy_config)
tls_profile_key = str(tls_profile or "").strip().lower()
if tls_profile_key:
cache_key = f"{cache_key}::tls:{tls_profile_key}"
# 无代理时返回默认客户端
if cache_key == "__no_proxy__":
return await cls.get_default_client_async()
lock = cls._get_proxy_clients_lock()
async with lock:
# 检查缓存
if cache_key in cls._proxy_clients:
client, _ = cls._proxy_clients[cache_key]
# 健康检查:如果客户端已关闭,移除并重新创建
if client.is_closed:
del cls._proxy_clients[cache_key]
logger.debug("代理客户端已关闭,将重新创建: {}", cache_key)
else:
# 更新最后使用时间
cls._proxy_clients[cache_key] = (client, time.time())
if tls_profile_key:
logger.debug(
"复用代理客户端 TLS profile={} key={}", tls_profile_key, cache_key
)
return client
# 淘汰旧客户端(如果超过上限)
await cls._evict_lru_proxy_client()
# 添加代理配置
proxy_url = await build_proxy_url_async(proxy_config) if proxy_config else None
# curl_cffi Transport: real TLS fingerprint impersonation.
# Supports:
# - "claude_code_nodejs" (legacy alias, uses default chrome120 impersonate)
# - direct chrome impersonate profile names (e.g. "chrome124")
use_curl_cffi_tls = False
if tls_profile_key:
from src.services.provider.fingerprint import KNOWN_IMPERSONATE_PROFILES
use_curl_cffi_tls = (
tls_profile_key == "claude_code_nodejs"
or tls_profile_key in KNOWN_IMPERSONATE_PROFILES
)
if use_curl_cffi_tls:
from src.clients.curl_cffi_transport import (
CURL_CFFI_AVAILABLE,
CurlCffiTransport,
)
if CURL_CFFI_AVAILABLE:
transport_kwargs: dict[str, Any] = {"proxy": proxy_url}
if tls_profile_key != "claude_code_nodejs":
transport_kwargs["impersonate"] = tls_profile_key
transport = CurlCffiTransport(**transport_kwargs)
client = httpx.AsyncClient(
transport=transport,
follow_redirects=True,
timeout=httpx.Timeout(
connect=config.http_connect_timeout,
read=config.http_read_timeout,
write=config.http_write_timeout,
pool=config.http_pool_timeout,
),
)
cls._proxy_clients[cache_key] = (client, time.time())
logger.info(
"创建 curl_cffi TLS 指纹客户端: profile={}, proxy={}",
tls_profile_key,
proxy_url or "direct",
)
return client
else:
logger.warning(
"curl_cffi 不可用,回退到 best-effort TLS 配置 (profile={})",
tls_profile_key,
)
# 创建新客户端(使用默认超时,请求时可覆盖)
client_config: dict[str, Any] = {
"http2": config.enable_http2,
"verify": get_ssl_context_for_profile(tls_profile),
"follow_redirects": True,
"limits": httpx.Limits(
max_connections=config.http_max_connections,
max_keepalive_connections=config.http_keepalive_connections,
keepalive_expiry=config.http_keepalive_expiry,
),
"timeout": httpx.Timeout(
connect=config.http_connect_timeout,
read=config.http_read_timeout,
write=config.http_write_timeout,
pool=config.http_pool_timeout,
),
}
proxy_param = make_proxy_param(proxy_url)
if proxy_param:
client_config["proxy"] = proxy_param
client = httpx.AsyncClient(**client_config) # type: ignore[arg-type]
cls._proxy_clients[cache_key] = (client, time.time())
proxy_label = "none"
if proxy_config:
proxy_label = str(
proxy_config.get("node_id") or proxy_config.get("url") or "unknown"
)
logger.debug(
"创建代理客户端(缓存): {}, 缓存数量: {}", proxy_label, len(cls._proxy_clients)
)
if tls_profile_key:
logger.debug("创建代理客户端 TLS profile={} key={}", tls_profile_key, cache_key)
return client
@classmethod
async def close_all(cls) -> None:
"""关闭所有HTTP客户端"""
if cls._default_client is not None:
await cls._default_client.aclose()
cls._default_client = None
logger.info("默认HTTP客户端已关闭")
for name, client in cls._clients.items():
await client.aclose()
logger.debug("命名HTTP客户端已关闭: {}", name)
cls._clients.clear()
# 关闭代理客户端缓存
for cache_key, (client, _) in cls._proxy_clients.items():
try:
await client.aclose()
logger.debug("代理客户端已关闭: {}", cache_key)
except Exception as e:
logger.warning("关闭代理客户端失败: {}", e)
cls._proxy_clients.clear()
# 关闭 tunnel 客户端缓存
for nid, (client, _) in cls._tunnel_clients.items():
try:
await client.aclose()
logger.debug("tunnel 客户端已关闭: {}", nid)
except Exception as e:
logger.warning("关闭 tunnel 客户端失败: {}", e)
cls._tunnel_clients.clear()
# 关闭 curl_cffi session 缓存
try:
from src.clients.curl_cffi_transport import CURL_CFFI_AVAILABLE, close_all_sessions
if CURL_CFFI_AVAILABLE:
await close_all_sessions()
logger.debug("curl_cffi sessions 已关闭")
except Exception as e:
logger.debug("关闭 curl_cffi sessions 失败: {}", e)
logger.info("所有HTTP客户端已关闭")
@classmethod
async def cleanup_idle_clients(
cls,
max_idle_seconds: int | None = None,
) -> dict[str, int]:
"""清理空闲的代理/Tunnel 客户端并关闭连接池资源。"""
idle_seconds = max_idle_seconds
if idle_seconds is None:
idle_seconds = _get_int_env("HTTP_CLIENT_IDLE_CLEANUP_MAX_SECONDS", 600, minimum=60)
now = time.time()
stale_proxy_clients: list[tuple[str, httpx.AsyncClient]] = []
stale_tunnel_clients: list[tuple[str, httpx.AsyncClient]] = []
removed_closed_proxy = 0
removed_closed_tunnel = 0
lock = cls._get_proxy_clients_lock()
async with lock:
for cache_key, (client, last_used) in list(cls._proxy_clients.items()):
if client.is_closed:
cls._proxy_clients.pop(cache_key, None)
removed_closed_proxy += 1
continue
if now - last_used > idle_seconds:
entry = cls._proxy_clients.pop(cache_key, None)
if entry is not None:
stale_proxy_clients.append((cache_key, entry[0]))
for node_id, (client, last_used) in list(cls._tunnel_clients.items()):
if client.is_closed:
cls._tunnel_clients.pop(node_id, None)
removed_closed_tunnel += 1
continue
if now - last_used > idle_seconds:
entry = cls._tunnel_clients.pop(node_id, None)
if entry is not None:
stale_tunnel_clients.append((node_id, entry[0]))
proxy_closed = 0
tunnel_closed = 0
for cache_key, client in stale_proxy_clients:
try:
await client.aclose()
proxy_closed += 1
except Exception as e:
logger.warning("关闭空闲代理客户端失败(key={}): {}", cache_key, e)
for node_id, client in stale_tunnel_clients:
try:
await client.aclose()
tunnel_closed += 1
except Exception as e:
logger.warning("关闭空闲 Tunnel 客户端失败(node_id={}): {}", node_id, e)
if proxy_closed or tunnel_closed or removed_closed_proxy or removed_closed_tunnel:
logger.info(
"HTTP 客户端空闲清理完成: proxy_closed={}, tunnel_closed={}, "
"proxy_already_closed={}, tunnel_already_closed={}, idle_seconds={}",
proxy_closed,
tunnel_closed,
removed_closed_proxy,
removed_closed_tunnel,
idle_seconds,
)
return {
"proxy_closed": proxy_closed,
"tunnel_closed": tunnel_closed,
"proxy_already_closed": removed_closed_proxy,
"tunnel_already_closed": removed_closed_tunnel,
}
@classmethod
@asynccontextmanager
async def get_temp_client(cls, **kwargs: Any) -> Any:
"""
获取临时HTTP客户端(上下文管理器)
用于一次性请求,使用后自动关闭
用法:
async with HTTPClientPool.get_temp_client() as client:
response = await client.get('https://example.com')
"""
default_config = {
"http2": config.enable_http2,
"verify": get_ssl_context(),
"timeout": httpx.Timeout(
connect=config.http_connect_timeout,
read=config.http_read_timeout,
write=config.http_write_timeout,
pool=config.http_pool_timeout,
),
}
default_config.update(kwargs)
client = httpx.AsyncClient(**default_config) # type: ignore[arg-type]
try:
yield client
finally:
await client.aclose()
@classmethod
async def _reset_default_client(cls) -> bool:
"""Atomically replace the shared default client with a fresh instance.
The old client is kept open briefly so that in-flight requests can
finish on their existing HTTP/2 streams; it is closed asynchronously
after a short grace period.
"""
async with _default_client_lock:
old_client = cls._default_client
if old_client is None:
return False
# Create a new client before discarding the old one
cls._default_client = httpx.AsyncClient(
http2=config.enable_http2,
verify=get_ssl_context(),
timeout=httpx.Timeout(
connect=config.http_connect_timeout,
read=config.http_read_timeout,
write=config.http_write_timeout,
pool=config.http_pool_timeout,
),
limits=httpx.Limits(
max_connections=config.http_max_connections,
max_keepalive_connections=config.http_keepalive_connections,
keepalive_expiry=config.http_keepalive_expiry,
),
follow_redirects=True,
)
# Close old client after a grace period so in-flight requests can drain
async def _close_old() -> None:
await asyncio.sleep(5)
try:
await old_client.aclose()
except Exception as exc:
logger.warning("关闭旧默认客户端失败: {}", exc)
task = asyncio.create_task(_close_old())
cls._background_tasks.add(task)
task.add_done_callback(cls._background_tasks.discard)
logger.warning("默认HTTP客户端已重建(HTTP/2 流容量恢复)")
return True
@classmethod
async def reset_upstream_client(
cls,
delegate_cfg: dict[str, Any] | None,
proxy_config: dict[str, Any] | None = None,
tls_profile: str | None = None,
) -> bool:
"""Reset cached upstream client for the given proxy/tunnel route.
Returns True when a cached client was closed and removed.
For the shared no-proxy default client, atomically replaces it with a
new instance so that subsequent requests get a fresh HTTP/2 connection
while in-flight requests on the old client can finish naturally.
"""
if delegate_cfg and delegate_cfg.get("tunnel"):
node_id = str(delegate_cfg.get("node_id") or "")
if not node_id:
return False
lock = cls._get_proxy_clients_lock()
async with lock:
entry = cls._tunnel_clients.pop(node_id, None)
if entry is None:
return False
client, _ = entry
try:
await client.aclose()
except Exception as exc:
logger.warning("关闭 Tunnel 客户端失败(node_id={}): {}", node_id, exc)
return True
if not proxy_config:
proxy_config = await get_system_proxy_config_async()
base_cache_key = compute_proxy_cache_key(proxy_config)
if base_cache_key == "__no_proxy__":
return await cls._reset_default_client()
cache_key_prefixes = [base_cache_key]
tls_profile_key = str(tls_profile or "").strip().lower()
if tls_profile_key:
cache_key_prefixes = [f"{base_cache_key}::tls:{tls_profile_key}"]
lock = cls._get_proxy_clients_lock()
async with lock:
keys_to_remove = [
key
for key in list(cls._proxy_clients.keys())
if any(
key == prefix
or key.startswith(f"{prefix}::")
or key.startswith(f"{prefix}::tls:")
for prefix in cache_key_prefixes
)
]
clients = [cls._proxy_clients.pop(key)[0] for key in keys_to_remove]
for client in clients:
try:
await client.aclose()
except Exception as exc:
logger.warning("关闭上游代理客户端失败: {}", exc)
return bool(clients)
@classmethod
async def get_upstream_client(
cls,
delegate_cfg: dict[str, Any] | None,
proxy_config: dict[str, Any] | None = None,
tls_profile: str | None = None,
) -> httpx.AsyncClient:
"""
获取可复用的上游请求客户端(自动选择 tunnel/代理模式)
tunnel 模式(delegate_cfg.tunnel=True):返回 TunnelTransport 客户端
直连/代理模式:返回代理客户端(含系统默认代理回退)
"""
if delegate_cfg and delegate_cfg.get("tunnel"):
return await cls._get_tunnel_client(delegate_cfg["node_id"])
return await cls.get_proxy_client(proxy_config=proxy_config, tls_profile=tls_profile)
@classmethod
async def create_upstream_stream_client(
cls,
delegate_cfg: dict[str, Any] | None,
proxy_config: dict[str, Any] | None = None,
timeout: httpx.Timeout | None = None,
tls_profile: str | None = None,
) -> httpx.AsyncClient:
"""
创建上游流式请求客户端(自动选择 tunnel/代理模式)
调用者需负责关闭返回的客户端。
"""
if delegate_cfg and delegate_cfg.get("tunnel"):
return await cls._get_tunnel_client(delegate_cfg["node_id"], timeout=timeout)
client_config: dict[str, Any] = {
"http2": config.enable_http2,
"verify": get_ssl_context_for_profile(tls_profile),
"follow_redirects": True,
}
if timeout:
client_config["timeout"] = timeout
else:
client_config["timeout"] = httpx.Timeout(
connect=config.http_connect_timeout,
read=config.http_read_timeout,
write=config.http_write_timeout,
pool=config.http_pool_timeout,
)
resolved_proxy_config = proxy_config
if resolved_proxy_config is None:
resolved_proxy_config = await get_system_proxy_config_async()
proxy_url = (
await build_proxy_url_async(resolved_proxy_config) if resolved_proxy_config else None
)
proxy_param = make_proxy_param(proxy_url)
if proxy_param:
client_config["proxy"] = proxy_param
logger.debug(
"创建带代理的HTTP客户端(一次性): {}",
resolved_proxy_config.get("url", "unknown") if resolved_proxy_config else "unknown",
)
return httpx.AsyncClient(**client_config)
@classmethod
async def _get_tunnel_client(
cls,
node_id: str,
timeout: httpx.Timeout | None = None,
) -> httpx.AsyncClient:
"""获取使用 TunnelTransport 的 httpx 客户端
当 timeout 为 None 时(非流式请求),返回按 node_id 缓存的 client
调用方不应关闭此 client其生命周期由 HTTPClientPool 管理。
当 timeout 非 None 时(流式请求),每次创建新 client由调用方负责关闭。
"""
from src.services.proxy_node.tunnel_transport import create_tunnel_transport
t = timeout or httpx.Timeout(
connect=config.http_connect_timeout,
read=config.http_read_timeout,
write=config.http_write_timeout,
pool=config.http_pool_timeout,
)
timeout_secs = t.read if isinstance(t, httpx.Timeout) else 60.0
# 流式请求:每次创建新 client调用方负责关闭
if timeout is not None:
transport = create_tunnel_transport(node_id, timeout=timeout_secs or 60.0)
return httpx.AsyncClient(transport=transport, timeout=t)
# 非流式请求:复用缓存的 client加锁与 proxy_clients 保持一致)
lock = cls._get_proxy_clients_lock()
async with lock:
entry = cls._tunnel_clients.get(node_id)
if entry is not None:
existing, _ = entry
if not existing.is_closed:
cls._tunnel_clients[node_id] = (existing, time.time())
return existing
del cls._tunnel_clients[node_id]
# 淘汰最久未使用的 tunnel 客户端
if len(cls._tunnel_clients) >= cls._max_tunnel_clients:
oldest_nid = min(cls._tunnel_clients, key=lambda k: cls._tunnel_clients[k][1])
old_client, _ = cls._tunnel_clients.pop(oldest_nid)
try:
await old_client.aclose()
except Exception:
pass
logger.debug("淘汰 tunnel 客户端: {}", oldest_nid)
transport = create_tunnel_transport(node_id, timeout=timeout_secs or 60.0)
client = httpx.AsyncClient(transport=transport, timeout=t)
cls._tunnel_clients[node_id] = (client, time.time())
return client
@classmethod
def get_pool_stats(cls) -> dict[str, Any]:
"""获取连接池统计信息"""
return {
"default_client_active": cls._default_client is not None,
"named_clients_count": len(cls._clients),
"proxy_clients_count": len(cls._proxy_clients),
"max_proxy_clients": cls._max_proxy_clients,
"tunnel_clients_count": len(cls._tunnel_clients),
}
# 便捷访问函数
def get_http_client() -> httpx.AsyncClient:
"""获取默认HTTP客户端的便捷函数"""
return HTTPClientPool.get_default_client()
async def close_http_clients() -> None:
"""关闭所有HTTP客户端的便捷函数"""
await HTTPClientPool.close_all()