mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
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:
@@ -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: ProxyNode(aether-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 请求"""
|
||||
|
||||
@@ -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) # 公网 IP(IPv6 最长 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):
|
||||
"""全局统一模型定义 - 包含价格和能力配置
|
||||
|
||||
|
||||
Reference in New Issue
Block a user