mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
feat(proxy): 支持远程推送升级与proxy元数据上报
- aether-proxy 注册和心跳时上报 proxy_metadata(含版本号) - 心跳 ACK 支持 upgrade_to 字段,proxy 收到后自动执行升级 - 重构 upgrade 逻辑,新增 perform_upgrade 用于远程触发的自动升级 - stream_handler 延迟统计改为仅记录连接建立延迟(DNS+TCP/TLS+TTFB) - 后端新增 proxy_metadata 数据库字段和批量升级 API - 远程配置支持下发 upgrade_to 版本指令 - 前端展示节点版本号,支持单节点和批量升级操作
This commit is contained in:
@@ -351,7 +351,7 @@ class HubConnectionManager:
|
||||
|
||||
def _sync_heartbeat() -> dict[str, object]:
|
||||
from src.database import create_session
|
||||
from src.services.proxy_node.service import ProxyNodeService
|
||||
from src.services.proxy_node.service import ProxyNodeService, build_heartbeat_ack
|
||||
|
||||
if not node_id:
|
||||
return {}
|
||||
@@ -367,12 +367,10 @@ class HubConnectionManager:
|
||||
failed_requests=data.get("failed_requests"),
|
||||
dns_failures=data.get("dns_failures"),
|
||||
stream_errors=data.get("stream_errors"),
|
||||
proxy_metadata=data.get("proxy_metadata"),
|
||||
proxy_version=data.get("proxy_version"),
|
||||
)
|
||||
result: dict[str, object] = {}
|
||||
if node.remote_config:
|
||||
result["remote_config"] = node.remote_config
|
||||
result["config_version"] = node.config_version or 0
|
||||
return result
|
||||
return build_heartbeat_ack(node)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ def node_to_dict(node: ProxyNode) -> dict[str, Any]:
|
||||
"failed_requests": node.failed_requests,
|
||||
"dns_failures": node.dns_failures,
|
||||
"stream_errors": node.stream_errors,
|
||||
"proxy_metadata": node.proxy_metadata,
|
||||
"hardware_info": node.hardware_info,
|
||||
"estimated_max_concurrency": node.estimated_max_concurrency,
|
||||
"remote_config": node.remote_config,
|
||||
@@ -95,6 +96,40 @@ def _sanitize_proxy_error(err: Exception) -> str:
|
||||
return re.sub(r"://[^@/]+@", "://***@", str(err))
|
||||
|
||||
|
||||
def _normalize_proxy_metadata(
|
||||
proxy_metadata: Any | None, proxy_version: str | None = None
|
||||
) -> dict[str, Any] | None:
|
||||
"""规范化 proxy 元数据,兼容旧版单独上报 proxy_version。"""
|
||||
normalized: dict[str, Any] = {}
|
||||
if isinstance(proxy_metadata, dict):
|
||||
normalized = {str(k): v for k, v in proxy_metadata.items() if k is not None}
|
||||
|
||||
version: str | None = None
|
||||
raw_version = normalized.pop("version", None)
|
||||
if isinstance(raw_version, str) and raw_version.strip():
|
||||
version = raw_version.strip()[:20]
|
||||
if proxy_version is not None and proxy_version.strip():
|
||||
version = proxy_version.strip()[:20]
|
||||
if version is not None:
|
||||
normalized["version"] = version
|
||||
|
||||
return normalized or None
|
||||
|
||||
|
||||
def build_heartbeat_ack(node: ProxyNode) -> dict[str, Any]:
|
||||
"""从心跳后的节点构建 ACK 响应 payload(供 hub_transport / tunnel_manager 使用)。"""
|
||||
result: dict[str, Any] = {}
|
||||
if not node.remote_config:
|
||||
return result
|
||||
result["remote_config"] = node.remote_config
|
||||
result["config_version"] = node.config_version or 0
|
||||
if isinstance(node.remote_config, dict):
|
||||
raw_upgrade = node.remote_config.get("upgrade_to")
|
||||
if isinstance(raw_upgrade, str) and raw_upgrade.strip():
|
||||
result["upgrade_to"] = raw_upgrade.strip()
|
||||
return result
|
||||
|
||||
|
||||
async def _test_proxy_connectivity(proxy_url: str) -> dict[str, Any]:
|
||||
"""通过代理 URL 测试连通性,返回标准化结果 dict"""
|
||||
import time as _time
|
||||
@@ -250,11 +285,14 @@ class ProxyNodeService:
|
||||
active_connections: int | None = None,
|
||||
total_requests: int | None = None,
|
||||
avg_latency_ms: float | None = None,
|
||||
proxy_metadata: dict[str, Any] | None = None,
|
||||
proxy_version: str | None = None,
|
||||
registered_by: str | None = None,
|
||||
) -> ProxyNode:
|
||||
"""注册或更新 aether-proxy 节点(tunnel 模式)"""
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
normalized_proxy_metadata = _normalize_proxy_metadata(proxy_metadata, proxy_version)
|
||||
|
||||
node = (
|
||||
db.query(ProxyNode)
|
||||
@@ -283,6 +321,8 @@ class ProxyNodeService:
|
||||
node.total_requests = total_requests
|
||||
if avg_latency_ms is not None:
|
||||
node.avg_latency_ms = avg_latency_ms
|
||||
if normalized_proxy_metadata is not None:
|
||||
node.proxy_metadata = normalized_proxy_metadata
|
||||
else:
|
||||
node = ProxyNode(
|
||||
id=str(uuid.uuid4()),
|
||||
@@ -298,6 +338,7 @@ class ProxyNodeService:
|
||||
active_connections=active_connections or 0,
|
||||
total_requests=total_requests or 0,
|
||||
avg_latency_ms=avg_latency_ms,
|
||||
proxy_metadata=normalized_proxy_metadata,
|
||||
hardware_info=hardware_info,
|
||||
estimated_max_concurrency=estimated_max_concurrency,
|
||||
tunnel_mode=True,
|
||||
@@ -322,6 +363,8 @@ class ProxyNodeService:
|
||||
failed_requests: int | None = None,
|
||||
dns_failures: int | None = None,
|
||||
stream_errors: int | None = None,
|
||||
proxy_metadata: dict[str, Any] | None = None,
|
||||
proxy_version: str | None = None,
|
||||
) -> ProxyNode:
|
||||
"""处理节点心跳(仅 tunnel 模式节点,更新指标并修正状态不一致)
|
||||
|
||||
@@ -357,6 +400,9 @@ class ProxyNodeService:
|
||||
values["active_connections"] = active_connections
|
||||
if avg_latency_ms is not None:
|
||||
values["avg_latency_ms"] = avg_latency_ms
|
||||
normalized_proxy_metadata = _normalize_proxy_metadata(proxy_metadata, proxy_version)
|
||||
if normalized_proxy_metadata is not None:
|
||||
values["proxy_metadata"] = normalized_proxy_metadata
|
||||
|
||||
# 区间增量指标 -- 使用数据库原子自增,避免并发心跳读改写丢增量
|
||||
if total_requests is not None and total_requests > 0:
|
||||
@@ -626,7 +672,11 @@ class ProxyNodeService:
|
||||
# Merge with existing config (so partial updates are preserved)
|
||||
# Copy to a new dict so SQLAlchemy detects the change on the JSON column
|
||||
existing = dict(node.remote_config) if node.remote_config else {}
|
||||
existing.update(config_updates)
|
||||
for key, value in config_updates.items():
|
||||
if key == "upgrade_to" and value is None:
|
||||
existing.pop("upgrade_to", None)
|
||||
continue
|
||||
existing[key] = value
|
||||
|
||||
node.remote_config = existing
|
||||
node.config_version = (node.config_version or 0) + 1
|
||||
@@ -635,3 +685,44 @@ class ProxyNodeService:
|
||||
db.commit()
|
||||
db.refresh(node)
|
||||
return node
|
||||
|
||||
@staticmethod
|
||||
def batch_upgrade_online_nodes(db: Session, *, version: str) -> dict[str, Any]:
|
||||
"""批量向在线 tunnel 节点下发 upgrade_to。"""
|
||||
normalized = version.strip()
|
||||
if not normalized:
|
||||
raise InvalidRequestException("version 不能为空")
|
||||
|
||||
nodes = (
|
||||
db.query(ProxyNode)
|
||||
.filter(
|
||||
ProxyNode.is_manual == False, # noqa: E712
|
||||
ProxyNode.tunnel_mode == True, # noqa: E712
|
||||
ProxyNode.status == ProxyNodeStatus.ONLINE,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
updated_node_ids: list[str] = []
|
||||
skipped = 0
|
||||
now = datetime.now(timezone.utc)
|
||||
for node in nodes:
|
||||
existing = dict(node.remote_config) if node.remote_config else {}
|
||||
if existing.get("upgrade_to") == normalized:
|
||||
skipped += 1
|
||||
continue
|
||||
existing["upgrade_to"] = normalized
|
||||
node.remote_config = existing
|
||||
node.config_version = (node.config_version or 0) + 1
|
||||
node.updated_at = now
|
||||
updated_node_ids.append(node.id)
|
||||
|
||||
if updated_node_ids:
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"version": normalized,
|
||||
"updated": len(updated_node_ids),
|
||||
"skipped": skipped,
|
||||
"node_ids": updated_node_ids,
|
||||
}
|
||||
|
||||
@@ -470,7 +470,7 @@ class TunnelManager:
|
||||
|
||||
def _sync_heartbeat() -> dict[str, Any]:
|
||||
from src.database import create_session
|
||||
from src.services.proxy_node.service import ProxyNodeService
|
||||
from src.services.proxy_node.service import ProxyNodeService, build_heartbeat_ack
|
||||
|
||||
db = create_session()
|
||||
try:
|
||||
@@ -483,12 +483,10 @@ class TunnelManager:
|
||||
failed_requests=data.get("failed_requests"),
|
||||
dns_failures=data.get("dns_failures"),
|
||||
stream_errors=data.get("stream_errors"),
|
||||
proxy_metadata=data.get("proxy_metadata"),
|
||||
proxy_version=data.get("proxy_version"),
|
||||
)
|
||||
result: dict[str, Any] = {}
|
||||
if node.remote_config:
|
||||
result["remote_config"] = node.remote_config
|
||||
result["config_version"] = node.config_version or 0
|
||||
return result
|
||||
return build_heartbeat_ack(node)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user