feat: ProxyNode 代理节点管理系统与 OpenAI Responses API 解析增强

ProxyNode 系统:新增 aether-proxy(Rust)海外 VPS 代理组件,后端实现节点注册/心跳/
HMAC 认证/健康检测调度器/模块化集成,前端新增代理节点管理页面。ProxyConfig 支持
node_id 模式,http_client 支持 HMAC 签名代理 URL 构建与 TTL 缓存。

OpenAI CLI 解析器:适配 Responses API 格式,支持 input_tokens/output_tokens 提取、
output[].content[].text 文本解析、response.completed 流式事件 usage 嵌套结构。
This commit is contained in:
fawney19
2026-02-07 12:24:42 +08:00
parent 62f852b851
commit 1180634269
40 changed files with 4761 additions and 11 deletions

View File

@@ -0,0 +1,5 @@
"""Proxy node services."""
from .health_scheduler import ProxyNodeHealthScheduler, get_proxy_node_health_scheduler
__all__ = ["ProxyNodeHealthScheduler", "get_proxy_node_health_scheduler"]

View File

@@ -0,0 +1,103 @@
"""
ProxyNode 心跳检测调度器
定期检查 proxy_nodes 的 last_heartbeat_at更新节点状态
- elapsed > interval * 3 -> unhealthy
- elapsed > interval * 10 -> offline
"""
from __future__ import annotations
from datetime import datetime, timezone
from typing import Any
from src.core.logger import logger
from src.database import create_session
from src.models.database import ProxyNode, ProxyNodeStatus
from src.services.system.scheduler import get_scheduler
class ProxyNodeHealthScheduler:
"""代理节点心跳检测调度器"""
def __init__(self) -> None:
self.running = False
async def start(self) -> Any:
if self.running:
logger.warning("ProxyNodeHealthScheduler already running")
return
self.running = True
logger.info("ProxyNodeHealthScheduler started")
scheduler = get_scheduler()
scheduler.add_interval_job(
self._scheduled_check,
seconds=30,
job_id="proxy_node_health_check",
name="代理节点心跳检测",
)
# 启动时立即执行一次
await self._check_heartbeats()
async def stop(self) -> Any:
if not self.running:
return
self.running = False
logger.info("ProxyNodeHealthScheduler stopped")
async def _scheduled_check(self) -> None:
await self._check_heartbeats()
async def _check_heartbeats(self) -> None:
db = create_session()
try:
now = datetime.now(timezone.utc)
nodes = db.query(ProxyNode).filter(ProxyNode.status != ProxyNodeStatus.OFFLINE).all()
if not nodes:
return
changed = 0
for node in nodes:
interval = int(node.heartbeat_interval or 30)
last = node.last_heartbeat_at
if last is None:
new_status = ProxyNodeStatus.OFFLINE
else:
elapsed = (now - last).total_seconds()
if elapsed > interval * 10:
new_status = ProxyNodeStatus.OFFLINE
elif elapsed > interval * 3:
new_status = ProxyNodeStatus.UNHEALTHY
else:
new_status = ProxyNodeStatus.ONLINE
if node.status != new_status:
node.status = new_status
node.updated_at = now
changed += 1
if changed:
db.commit()
logger.info("ProxyNode 心跳状态已更新: {} 个节点", changed)
except Exception as e:
try:
db.rollback()
except Exception:
pass
logger.exception("ProxyNode 心跳检测失败: {}", e)
finally:
db.close()
_proxy_node_health_scheduler: ProxyNodeHealthScheduler | None = None
def get_proxy_node_health_scheduler() -> ProxyNodeHealthScheduler:
global _proxy_node_health_scheduler
if _proxy_node_health_scheduler is None:
_proxy_node_health_scheduler = ProxyNodeHealthScheduler()
return _proxy_node_health_scheduler

View File

@@ -704,6 +704,7 @@ class TaskService:
from src.core.exceptions import (
ConcurrencyLimitError,
EmbeddedErrorException,
ProxyNodeUnavailableError,
ThinkingSignatureException,
UpstreamClientException,
)
@@ -744,6 +745,20 @@ class TaskService:
)
return "break"
if isinstance(cause, ProxyNodeUnavailableError):
# ProxyNode 不可用属于“配置明确指定但不可达/不可用”的情况,
# 在当前候选上重试通常没有意义,直接切换到下一个候选更合理。
logger.warning(" [{}] 代理节点不可用,切换候选: {}", request_id, str(cause))
RequestCandidateService.mark_candidate_failed(
db=self.db,
candidate_id=candidate_record_id,
error_type=type(cause).__name__,
error_message=extract_error_message(cause),
latency_ms=elapsed_ms,
concurrent_requests=captured_key_concurrent,
)
return "break"
if isinstance(cause, EmbeddedErrorException):
error_message = cause.error_message or ""
embedded_status = cause.error_code or 200