按显卡世代和创世编号结算回本,机房批量接入需审核网段并用硬件指纹防克隆。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-11 00:30:46 +08:00
parent 7d29be8b17
commit 0740016ad8
22 changed files with 1554 additions and 96 deletions
+43 -1
View File
@@ -13,6 +13,9 @@ from app.config import settings
from app.errors import AppError
from app.models import ComputeNode, NodeStatus, UptimeInterval
from app.services.common import get_node_by_sn
from app.services.datacenter import assert_fingerprint, assert_subnet_gpu
from app.services.ip_guard import assert_ip_available
from app.services.hardware import GENESIS_RECOVERY_DAYS, HardwareReport, classify_generation, is_genesis, score_hardware
def assert_node_token(node: ComputeNode, token: str | None) -> None:
@@ -24,14 +27,33 @@ def assert_node_token(node: ComputeNode, token: str | None) -> None:
raise AppError("节点 Token 不正确", 403)
def record_heartbeat(session: Session, sn: str, token: str | None = None, now: datetime | None = None) -> ComputeNode:
def record_heartbeat(
session: Session,
sn: str,
token: str | None = None,
now: datetime | None = None,
hardware: HardwareReport | None = None,
client_ip: str | None = None,
hardware_fingerprint: str | None = None,
) -> ComputeNode:
"""刷新最后活跃时间。离线节点收到心跳后重新打开一段在线区间。"""
moment = now or utcnow()
node = get_node_by_sn(session, sn.strip())
assert_node_token(node, token)
if node.license_expires_at <= moment:
raise AppError("节点授权已到期", 403)
bound_ip = (client_ip or "").strip() or None
if bound_ip:
assert_ip_available(session, bound_ip, node=node, now=moment)
reported_gpu = hardware.gpu_model if hardware is not None and hardware.gpu_model else node.gpu_model
assert_subnet_gpu(session, bound_ip, reported_gpu)
node.client_ip = bound_ip
fingerprint = assert_fingerprint(session, hardware_fingerprint, node.id)
if fingerprint:
node.hardware_fingerprint = fingerprint
if hardware is not None:
_apply_hardware(node, hardware)
node.last_heartbeat_at = moment
node.updated_at = moment
if node.status != NodeStatus.ONLINE:
@@ -66,6 +88,26 @@ def mark_stale_nodes(session: Session, now: datetime | None = None) -> list[str]
return changed
def _apply_hardware(node: ComputeNode, hardware: HardwareReport) -> None:
node.gpu_model = (hardware.gpu_model or "").strip()[:128] or None
node.vram_gb = hardware.vram_gb
node.cpu_count = hardware.cpu_count
node.memory_gb = hardware.memory_gb
node.hardware_weight = float(score_hardware(node.gpu_model, node.vram_gb))
if node.generation_locked or not node.gpu_model:
return
generation, recovery_days = classify_generation(node.gpu_model)
node.gpu_generation = generation
node.generation_locked = True
if is_genesis(node.node_index):
node.recovery_days = GENESIS_RECOVERY_DAYS
return
node.recovery_days = recovery_days
horizon = node.licensed_at + timedelta(days=recovery_days)
if horizon > node.license_expires_at:
node.license_expires_at = horizon
def _ensure_open_interval(session: Session, node_id: int, started_at: datetime) -> None:
opened = session.scalar(
select(UptimeInterval).where(