refactor: 统一代理配置优先级链(key>provider>系统默认)并复用 HTTP 连接池

- 引入 resolve_proxy_param / build_proxy_client_kwargs 工具函数,统一
  httpx 客户端的代理+SSL+超时配置,替换各模块中零散的 get_ssl_context() 调用
- 所有涉及上游请求的模块(provider_query, usage replay, endpoint check,
  model fetch, OAuth, Vertex Auth, Gemini Files/Video 等)改用
  resolve_effective_proxy 按 key > provider > 系统默认优先级解析代理
- 流式请求改用 HTTPClientPool.get_upstream_client 复用连接池,移除各处
  http_client.aclose() 避免关闭共享客户端
- StreamProcessor._cleanup 不再关闭池中客户端,仅清理响应上下文
- 前端 EndpointFormDialog 增加 body_rules 帮助说明 Popover
- Mock handler 补充 OAuth 字段、endpoint extras 及新增 mock 路由
This commit is contained in:
fawney19
2026-02-11 03:16:53 +08:00
parent cd0acf1e6b
commit bda03d187e
22 changed files with 749 additions and 322 deletions

View File

@@ -1,13 +1,12 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
from urllib.parse import urlencode, urlparse, urlunparse
import httpx
from src.services.auth.oauth.models import OAuthToken, OAuthUserInfo
from src.utils.ssl_utils import get_ssl_context
if TYPE_CHECKING:
from src.models.database import OAuthProvider
@@ -98,9 +97,8 @@ class OAuthProviderBase(ABC):
timeout_seconds: float = 5.0,
headers: dict[str, str] | None = None,
) -> httpx.Response:
async with httpx.AsyncClient(
timeout=httpx.Timeout(timeout_seconds), verify=get_ssl_context()
) as client:
client_kwargs = self._build_http_client_kwargs(timeout_seconds)
async with httpx.AsyncClient(**client_kwargs) as client:
return await client.post(url, data=data, headers=headers)
async def _http_get(
@@ -110,7 +108,12 @@ class OAuthProviderBase(ABC):
timeout_seconds: float = 5.0,
headers: dict[str, str] | None = None,
) -> httpx.Response:
async with httpx.AsyncClient(
timeout=httpx.Timeout(timeout_seconds), verify=get_ssl_context()
) as client:
client_kwargs = self._build_http_client_kwargs(timeout_seconds)
async with httpx.AsyncClient(**client_kwargs) as client:
return await client.get(url, headers=headers)
@staticmethod
def _build_http_client_kwargs(timeout_seconds: float = 5.0) -> dict[str, Any]:
from src.services.proxy_node.resolver import build_proxy_client_kwargs
return build_proxy_client_kwargs(timeout=httpx.Timeout(timeout_seconds))

View File

@@ -24,7 +24,17 @@ from src.services.auth.oauth.state import consume_oauth_state, create_oauth_stat
from src.services.auth.service import AuthService
from src.services.cache.user_cache import UserCacheService
from src.services.system.config import SystemConfigService
from src.utils.ssl_utils import get_ssl_context
def _build_oauth_client_kwargs(
timeout_seconds: float = 5.0, follow_redirects: bool = False
) -> dict[str, Any]:
"""构建 OAuth HTTP 客户端参数(含系统默认代理)"""
from src.services.proxy_node.resolver import build_proxy_client_kwargs
return build_proxy_client_kwargs(
timeout=httpx.Timeout(timeout_seconds), follow_redirects=follow_redirects
)
class OAuthService:
@@ -835,7 +845,7 @@ class OAuthService:
async def _reachable(url: str) -> bool:
try:
async with httpx.AsyncClient(
timeout=httpx.Timeout(5.0), follow_redirects=False, verify=get_ssl_context()
**_build_oauth_client_kwargs(5.0, follow_redirects=False)
) as client:
await client.get(url)
return True
@@ -851,9 +861,7 @@ class OAuthService:
if has_secret and client_secret:
# 使用无效 code 做一次 token 请求(仅做粗略判定)
try:
async with httpx.AsyncClient(
timeout=httpx.Timeout(5.0), verify=get_ssl_context()
) as client:
async with httpx.AsyncClient(**_build_oauth_client_kwargs(5.0)) as client:
resp = await client.post(
token_url,
data={
@@ -914,7 +922,7 @@ class OAuthService:
async def _reachable(url: str) -> bool:
try:
async with httpx.AsyncClient(
timeout=httpx.Timeout(5.0), follow_redirects=False, verify=get_ssl_context()
**_build_oauth_client_kwargs(5.0, follow_redirects=False)
) as client:
await client.get(url)
return True
@@ -930,9 +938,7 @@ class OAuthService:
if client_secret:
# 使用无效 code 做一次 token 请求(仅做粗略判定)
try:
async with httpx.AsyncClient(
timeout=httpx.Timeout(5.0), verify=get_ssl_context()
) as client:
async with httpx.AsyncClient(**_build_oauth_client_kwargs(5.0)) as client:
resp = await client.post(
token_url,
data={

View File

@@ -35,6 +35,7 @@ from src.services.model.upstream_fetcher import (
merge_upstream_metadata,
)
from src.services.provider.oauth_token import resolve_oauth_access_token
from src.services.proxy_node.resolver import resolve_effective_proxy
from src.services.system.scheduler import get_scheduler
# 从环境变量读取间隔,默认 1440 分钟1 天),限制在 60-10080 分钟之间
@@ -448,7 +449,9 @@ class ModelFetchScheduler:
encrypted_auth_config if isinstance(encrypted_auth_config, str) else None
),
format_to_endpoint=format_to_endpoint,
proxy_config=getattr(provider, "proxy", None),
proxy_config=resolve_effective_proxy(
getattr(provider, "proxy", None), getattr(key, "proxy", None)
),
)
async def _update_key_after_fetch(

View File

@@ -15,7 +15,6 @@ import httpx
from src.core.api_format import get_extra_headers_from_endpoint
from src.core.logger import logger
from src.utils.ssl_utils import get_ssl_context
# 并发请求限制
MAX_CONCURRENT_REQUESTS = 5
@@ -100,7 +99,7 @@ async def _fetch_models_default(
) -> tuple[list[dict], list[str], bool, dict[str, Any] | None]:
endpoint_configs = build_all_format_configs(ctx.api_key_value, ctx.format_to_endpoint)
models, errors, has_success = await fetch_models_from_endpoints(
endpoint_configs, timeout=timeout_seconds
endpoint_configs, timeout=timeout_seconds, proxy_config=ctx.proxy_config
)
return models, errors, has_success, None
@@ -230,6 +229,7 @@ def build_all_format_configs(
async def fetch_models_from_endpoints(
endpoint_configs: list[dict],
timeout: float = 30.0,
proxy_config: dict[str, Any] | None = None,
) -> tuple[list[dict], list[str], bool]:
"""
从多个端点并发获取模型
@@ -237,10 +237,13 @@ async def fetch_models_from_endpoints(
Args:
endpoint_configs: 端点配置列表,每个配置包含 api_key, base_url, api_format, extra_headers
timeout: 请求超时时间(秒)
proxy_config: 代理配置(可选),支持系统默认回退
Returns:
(模型列表, 错误列表, 是否有成功)
"""
from src.services.proxy_node.resolver import build_proxy_client_kwargs
all_models: list[dict] = []
errors: list[str] = []
has_success = False
@@ -279,7 +282,9 @@ async def fetch_models_from_endpoints(
logger.exception("获取 {} 模型出错", api_format)
return [], f"{api_format}: error", False
async with httpx.AsyncClient(timeout=timeout, verify=get_ssl_context()) as client:
async with httpx.AsyncClient(
**build_proxy_client_kwargs(proxy_config, timeout=timeout)
) as client:
results = await asyncio.gather(*[fetch_one(client, c) for c in endpoint_configs])
for models, error, success in results:
all_models.extend(models)

View File

@@ -292,6 +292,66 @@ def resolve_effective_proxy(
return provider_proxy
def resolve_proxy_param(
proxy_config: dict[str, Any] | None = None,
) -> str | httpx.Proxy | None:
"""
将代理配置解析为 httpx 可接受的代理参数(含系统默认回退)
优先级proxy_config -> 系统默认代理 -> None直连
Args:
proxy_config: 代理配置字典(通常来自 resolve_effective_proxy 的返回值)
Returns:
httpx 可接受的 proxy 参数,或 None
"""
url = build_proxy_url(proxy_config) if proxy_config else None
if not url:
sys_proxy = get_system_proxy_config()
if sys_proxy:
try:
url = build_proxy_url(sys_proxy)
except Exception as exc:
logger.warning("resolve_proxy_param: 构建系统默认代理 URL 失败: {}", exc)
url = None
return make_proxy_param(url)
def build_proxy_client_kwargs(
proxy_config: dict[str, Any] | None = None,
*,
timeout: float = 30.0,
verify: Any | None = None,
**extra: Any,
) -> dict[str, Any]:
"""
构建包含代理配置的 httpx.AsyncClient 初始化参数。
将 resolve_proxy_param + dict 构建 + 条件 proxy 赋值合并为一步,
减少调用方的样板代码。
Args:
proxy_config: 代理配置字典(通常来自 resolve_effective_proxy
timeout: 请求超时(秒)
verify: SSL 验证参数None 时自动使用 get_ssl_context()
**extra: 其他 httpx.AsyncClient 参数(如 follow_redirects
Returns:
可直接解包传给 httpx.AsyncClient 的参数字典
"""
if verify is None:
from src.utils.ssl_utils import get_ssl_context
verify = get_ssl_context()
kwargs: dict[str, Any] = {"timeout": timeout, "verify": verify, **extra}
proxy_param = resolve_proxy_param(proxy_config)
if proxy_param:
kwargs["proxy"] = proxy_param
return kwargs
# ---------------------------------------------------------------------------
# 代理 URL 构建
# ---------------------------------------------------------------------------

View File

@@ -175,14 +175,20 @@ class RequestExecutor:
)
else:
# 非流式请求:标记为 success 状态
from src.services.proxy_node.resolver import resolve_proxy_info
from src.services.proxy_node.resolver import (
resolve_effective_proxy,
resolve_proxy_info,
)
_eff_proxy = resolve_effective_proxy(
getattr(provider, "proxy", None), getattr(key, "proxy", None)
)
_extra: dict[str, Any] = {
"is_cached_user": is_cached_user,
"model_name": model_name,
"api_format": api_format,
}
_pi = resolve_proxy_info(getattr(provider, "proxy", None))
_pi = resolve_proxy_info(_eff_proxy)
if _pi:
_extra["proxy"] = _pi
RequestCandidateService.mark_candidate_success(

View File

@@ -708,11 +708,15 @@ class TaskService:
ThinkingSignatureException,
UpstreamClientException,
)
from src.services.proxy_node.resolver import resolve_proxy_info
from src.services.proxy_node.resolver import resolve_effective_proxy, resolve_proxy_info
from src.services.request.executor import ExecutionError
# 提前解析代理信息,写入候选记录的 extra_data用于链路追踪展示
_proxy_info = resolve_proxy_info(getattr(candidate.provider, "proxy", None))
_eff_proxy = resolve_effective_proxy(
getattr(candidate.provider, "proxy", None),
getattr(candidate.key, "proxy", None),
)
_proxy_info = resolve_proxy_info(_eff_proxy)
_proxy_extra: dict[str, Any] | None = {"proxy": _proxy_info} if _proxy_info else None
if not isinstance(exec_err, ExecutionError):