feat(aether-proxy): 网络层全面优化与代理耗时追踪

aether-proxy:
- 新增 20+ 可配置参数:Aether API/delegate/CONNECT 各阶段超时、连接池、TCP keepalive/nodelay
- AetherClient 支持指数退避重试(可配置次数/延迟)和 HTTP/2
- 新增 DNS 缓存(TTL + 容量限制)避免重复解析
- 新增并发连接数限制(Semaphore,默认基于硬件估算)
- CONNECT 隧道增加建连超时和升级超时
- TLS 增加握手超时、会话缓存(session ticket + memory cache)、ALPN 协商
- 新增 ProxyMetrics 收集请求计数和延迟,通过心跳上报
- delegate 响应注入 X-Proxy-Timing 头(dns_ms/upstream_ms/total_ms)

后端:
- StreamContext 和 handler 提取 X-Proxy-Timing 写入 proxy_info 追踪数据

前端:
- 请求时间线展示代理分阶段耗时(DNS/上游)
- Endpoint 添加按钮改为文字按钮样式
This commit is contained in:
fawney19
2026-02-11 11:30:29 +08:00
parent 68f5e7e502
commit ed3f208dda
20 changed files with 832 additions and 73 deletions

View File

@@ -40,7 +40,11 @@ from src.api.handlers.base.base_handler import (
from src.api.handlers.base.parsers import get_parser_for_format
from src.api.handlers.base.request_builder import PassthroughRequestBuilder, get_provider_auth
from src.api.handlers.base.response_parser import ResponseParser
from src.api.handlers.base.stream_context import StreamContext, is_format_converted
from src.api.handlers.base.stream_context import (
StreamContext,
extract_proxy_timing,
is_format_converted,
)
from src.api.handlers.base.stream_processor import StreamProcessor
from src.api.handlers.base.stream_telemetry import StreamTelemetryRecorder
from src.api.handlers.base.upstream_stream_bridge import (
@@ -975,6 +979,7 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
ctx.status_code = resp.status_code
ctx.response_headers = dict(resp.headers)
ctx.set_proxy_timing(ctx.response_headers)
if envelope:
envelope.on_http_status(base_url=ctx.selected_base_url, status_code=ctx.status_code)
@@ -1005,6 +1010,7 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
resp = await http_client.post(**_pkw)
ctx.status_code = resp.status_code
ctx.response_headers = dict(resp.headers)
ctx.set_proxy_timing(ctx.response_headers)
if envelope:
envelope.on_http_status(
base_url=ctx.selected_base_url, status_code=ctx.status_code
@@ -1168,6 +1174,7 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
ctx.status_code = stream_response.status_code
ctx.response_headers = dict(stream_response.headers)
ctx.set_proxy_timing(ctx.response_headers)
if envelope:
envelope.on_http_status(
base_url=ctx.selected_base_url,
@@ -1643,6 +1650,7 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
status_code = stream_resp.status_code
response_headers = dict(stream_resp.headers)
extract_proxy_timing(sync_proxy_info, response_headers)
if envelope:
envelope.on_http_status(
@@ -1695,6 +1703,7 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
status_code = resp.status_code
response_headers = dict(resp.headers)
extract_proxy_timing(sync_proxy_info, response_headers)
if envelope:
envelope.on_http_status(base_url=selected_base_url_cached, status_code=status_code)

View File

@@ -43,7 +43,11 @@ from src.api.handlers.base.request_builder import PassthroughRequestBuilder, get
from src.api.handlers.base.response_parser import (
ResponseParser,
)
from src.api.handlers.base.stream_context import StreamContext, is_format_converted
from src.api.handlers.base.stream_context import (
StreamContext,
extract_proxy_timing,
is_format_converted,
)
from src.api.handlers.base.upstream_stream_bridge import (
aggregate_upstream_stream_to_internal_response,
)
@@ -90,7 +94,7 @@ from src.utils.sse_parser import SSEEventParser
from src.utils.timeout import read_first_chunk_with_ttfb_timeout
# ==============================================================================
# SSE 行解析辅助函数
# 辅助函数
# ==============================================================================
@@ -946,6 +950,7 @@ class CliMessageHandlerBase(BaseMessageHandler):
ctx.status_code = resp.status_code
ctx.response_headers = dict(resp.headers)
ctx.set_proxy_timing(ctx.response_headers)
if envelope:
envelope.on_http_status(base_url=ctx.selected_base_url, status_code=ctx.status_code)
@@ -978,6 +983,7 @@ class CliMessageHandlerBase(BaseMessageHandler):
ctx.set_ttfb_ms(int((time.monotonic() - _connect_start) * 1000))
ctx.status_code = resp.status_code
ctx.response_headers = dict(resp.headers)
ctx.set_proxy_timing(ctx.response_headers)
if envelope:
envelope.on_http_status(
base_url=ctx.selected_base_url, status_code=ctx.status_code
@@ -1142,6 +1148,7 @@ class CliMessageHandlerBase(BaseMessageHandler):
ctx.status_code = stream_response.status_code
ctx.response_headers = dict(stream_response.headers)
ctx.set_proxy_timing(ctx.response_headers)
logger.debug(f" └─ 收到响应: status={stream_response.status_code}")
@@ -3079,6 +3086,7 @@ class CliMessageHandlerBase(BaseMessageHandler):
status_code = stream_resp.status_code
response_headers = dict(stream_resp.headers)
extract_proxy_timing(sync_proxy_info, response_headers)
if envelope:
envelope.on_http_status(
@@ -3131,6 +3139,7 @@ class CliMessageHandlerBase(BaseMessageHandler):
status_code = resp.status_code
response_headers = dict(resp.headers)
extract_proxy_timing(sync_proxy_info, response_headers)
if envelope:
envelope.on_http_status(base_url=selected_base_url_cached, status_code=status_code)

View File

@@ -10,6 +10,7 @@
from __future__ import annotations
import json
import time
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any
@@ -18,6 +19,21 @@ if TYPE_CHECKING:
from src.core.api_format.conversion.stream_state import StreamState
def extract_proxy_timing(proxy_info: dict[str, Any] | None, headers: dict[str, str]) -> None:
"""从响应头中提取代理分阶段耗时X-Proxy-Timing写入 proxy_info"""
if proxy_info is None:
return
timing_raw = headers.get("x-proxy-timing")
if not timing_raw:
return
try:
timing = json.loads(timing_raw)
if isinstance(timing, dict):
proxy_info["timing"] = timing
except (json.JSONDecodeError, TypeError):
pass
def is_format_converted(
provider_api_format: str | None,
client_api_format: str | None,
@@ -272,6 +288,10 @@ class StreamContext:
if self.proxy_info is not None:
self.proxy_info["ttfb_ms"] = ms
def set_proxy_timing(self, headers: dict[str, str]) -> None:
"""从代理响应头中提取分阶段耗时信息X-Proxy-Timing"""
extract_proxy_timing(self.proxy_info, headers)
def build_response_body(self, response_time_ms: int) -> dict[str, Any]:
"""
构建响应体元数据