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

@@ -10,7 +10,7 @@ import re
from datetime import datetime
from typing import Any
from pydantic import BaseModel, Field, field_validator
from pydantic import BaseModel, Field, field_validator, model_validator
from src.core.enums import ProviderBillingType
@@ -18,18 +18,26 @@ from src.core.enums import ProviderBillingType
class ProxyConfig(BaseModel):
"""代理配置"""
url: str = Field(..., description="代理 URL (http://, https://, socks5://)")
# 模式 1: 手动配置代理 URL原有
url: str | None = Field(None, description="代理 URL (http://, https://, socks5://)")
username: str | None = Field(None, max_length=255, description="代理用户名")
password: str | None = Field(None, max_length=500, description="代理密码")
# 模式 2: ProxyNodeaether-proxy 注册的节点)
node_id: str | None = Field(None, description="代理节点 ID")
enabled: bool = Field(True, description="是否启用代理false 时保留配置但不使用)")
@field_validator("url")
@classmethod
def validate_proxy_url(cls, v: str) -> str:
def validate_proxy_url(cls, v: str | None) -> str | None:
"""验证代理 URL 格式"""
if v is None:
return None
from urllib.parse import urlparse
v = v.strip()
if not v:
return None
# 检查禁止的字符(防止注入)
if "\n" in v or "\r" in v:
@@ -50,6 +58,27 @@ class ProxyConfig(BaseModel):
return v
@field_validator("node_id")
@classmethod
def validate_node_id(cls, v: str | None) -> str | None:
if v is None:
return None
v = v.strip()
return v or None
@model_validator(mode="after")
def validate_proxy_mode(self) -> "ProxyConfig":
if not self.enabled:
return self
if not self.url and not self.node_id:
raise ValueError("启用代理时,必须提供 url 或 node_id")
if self.url and self.node_id:
raise ValueError("url 和 node_id 不能同时设置")
return self
class CreateProviderRequest(BaseModel):
"""创建 Provider 请求"""

View File

@@ -792,6 +792,63 @@ class ProviderEndpoint(Base):
)
class ProxyNodeStatus(PyEnum):
"""代理节点状态"""
ONLINE = "online"
UNHEALTHY = "unhealthy"
OFFLINE = "offline"
class ProxyNode(Base):
"""代理节点表(用于 aether-proxy 注册/心跳)"""
__tablename__ = "proxy_nodes"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
name = Column(String(100), nullable=False) # 节点名
ip = Column(String(45), nullable=False) # 公网 IPIPv6 最长 39 + 冗余)
port = Column(Integer, nullable=False) # 代理端口
region = Column(String(100), nullable=True) # 区域标签
status = Column(
Enum(
ProxyNodeStatus,
name="proxynodestatus",
create_type=False,
values_callable=lambda x: [e.value for e in x],
),
default=ProxyNodeStatus.ONLINE,
nullable=False,
)
registered_by = Column(
String(36),
ForeignKey("users.id", ondelete="SET NULL"),
nullable=True,
comment="注册该节点的管理员用户 ID可空",
)
last_heartbeat_at = Column(DateTime(timezone=True), nullable=True)
heartbeat_interval = Column(Integer, default=30, nullable=False)
# 性能指标(心跳上报)
active_connections = Column(Integer, default=0, nullable=False)
total_requests = Column(BigInteger, default=0, nullable=False)
avg_latency_ms = Column(Float, nullable=True)
created_at = Column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False
)
updated_at = Column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc),
nullable=False,
)
__table_args__ = (UniqueConstraint("ip", "port", name="uq_proxy_node_ip_port"),)
class GlobalModel(Base):
"""全局统一模型定义 - 包含价格和能力配置