mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
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:
@@ -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))
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 构建
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user