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

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
+160 -6
View File
@@ -72,23 +72,172 @@ def list_orders(session: Session, user_id: int | None = None) -> list[LicenseOrd
return list(session.scalars(query.order_by(pending_first, LicenseOrder.id.desc())).all())
def render_client(sn: str, token: str, base_url: str) -> str:
config = json.dumps({"sn": sn, "token": token, "base": base_url.rstrip("/")}, ensure_ascii=True)
return f"""# HyperNode 挂机客户端。这个文件里有节点 Token,不要发给别人。
_CLIENT = """# HyperNode 挂机客户端。这个文件里有节点 Token,不要发给别人。
import ctypes
import hashlib
import json
import os
import subprocess
import time
import urllib.error
import urllib.request
import uuid
CONFIG = json.loads({config!r})
CONFIG = json.loads(__CONFIG__)
def hardware():
gpu_model, vram_gb, gpu_uuid = _gpu()
return {
"sn": CONFIG["sn"],
"token": CONFIG["token"],
"gpu_model": gpu_model,
"vram_gb": vram_gb,
"cpu_count": os.cpu_count(),
"memory_gb": _memory_gb(),
"hardware_fingerprint": _fingerprint(gpu_uuid),
}
def _fingerprint(gpu_uuid):
board = _board_serial() or ""
mac = f"{uuid.getnode():012x}"
material = "|".join([gpu_uuid or "", board, mac])
if not material.strip("|"):
return None
return hashlib.sha256(material.encode()).hexdigest()
def _board_serial():
if os.name == "nt":
try:
output = subprocess.check_output(
["wmic", "baseboard", "get", "serialnumber"],
stderr=subprocess.DEVNULL,
timeout=8,
text=True,
)
except (OSError, subprocess.SubprocessError):
return None
lines = [line.strip() for line in output.splitlines() if line.strip() and "SerialNumber" not in line]
return lines[0] if lines else None
for path in ("/sys/class/dmi/id/board_serial", "/sys/class/dmi/id/product_uuid"):
try:
with open(path, encoding="utf-8") as handle:
value = handle.read().strip()
except OSError:
continue
if value and value.lower() not in {"none", "to be filled by o.e.m."}:
return value
return None
def _gpu():
probed = _pynvml()
if probed[0]:
return probed
return _nvidia_smi()
def _pynvml():
try:
import pynvml
except ImportError:
return None, None, None
try:
pynvml.nvmlInit()
best_name = None
best_bytes = -1
best_uuid = None
for index in range(pynvml.nvmlDeviceGetCount()):
handle = pynvml.nvmlDeviceGetHandleByIndex(index)
name = pynvml.nvmlDeviceGetName(handle)
if isinstance(name, bytes):
name = name.decode()
total = int(pynvml.nvmlDeviceGetMemoryInfo(handle).total)
if total > best_bytes:
uid = pynvml.nvmlDeviceGetUUID(handle)
if isinstance(uid, bytes):
uid = uid.decode()
best_name = str(name)
best_bytes = total
best_uuid = str(uid)
pynvml.nvmlShutdown()
except Exception:
return None, None, None
if not best_name or best_bytes < 0:
return None, None, None
return best_name, round(best_bytes / (1024 ** 3), 2), best_uuid
def _nvidia_smi():
try:
output = subprocess.check_output(
["nvidia-smi", "--query-gpu=name,memory.total,uuid", "--format=csv,noheader,nounits"],
stderr=subprocess.DEVNULL,
timeout=8,
text=True,
)
except (OSError, subprocess.SubprocessError):
return None, None, None
best_name = None
best_mib = -1.0
best_uuid = None
for line in output.splitlines():
parts = [part.strip() for part in line.split(",", 2)]
if len(parts) < 2:
continue
name, memory = parts[0], parts[1]
gpu_uuid = parts[2] if len(parts) > 2 else None
try:
mib = float(memory)
except ValueError:
continue
if mib > best_mib:
best_name = name or None
best_mib = mib
best_uuid = gpu_uuid or None
if best_name is None:
return None, None, None
return best_name, round(best_mib / 1024, 2), best_uuid
def _memory_gb():
if os.name == "nt":
class MemoryStatus(ctypes.Structure):
_fields_ = [
("dwLength", ctypes.c_ulong),
("dwMemoryLoad", ctypes.c_ulong),
("ullTotalPhys", ctypes.c_ulonglong),
("ullAvailPhys", ctypes.c_ulonglong),
("ullTotalPageFile", ctypes.c_ulonglong),
("ullAvailPageFile", ctypes.c_ulonglong),
("ullTotalVirtual", ctypes.c_ulonglong),
("ullAvailVirtual", ctypes.c_ulonglong),
("ullAvailExtendedVirtual", ctypes.c_ulonglong),
]
status = MemoryStatus()
status.dwLength = ctypes.sizeof(MemoryStatus)
if ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(status)):
return round(status.ullTotalPhys / (1024 ** 3), 2)
return None
try:
with open("/proc/meminfo", encoding="utf-8") as handle:
for line in handle:
if line.startswith("MemTotal:"):
return round(int(line.split()[1]) / 1024 / 1024, 2)
except OSError:
return None
return None
def beat():
body = json.dumps({{"sn": CONFIG["sn"], "token": CONFIG["token"]}}).encode()
body = json.dumps(hardware()).encode()
request = urllib.request.Request(
CONFIG["base"] + "/api/v1/node/heartbeat",
data=body,
headers={{"Content-Type": "application/json"}},
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(request, timeout=20) as response:
print(response.read().decode())
@@ -104,6 +253,11 @@ if __name__ == "__main__":
"""
def render_client(sn: str, token: str, base_url: str) -> str:
config = json.dumps({"sn": sn, "token": token, "base": base_url.rstrip("/")}, ensure_ascii=True)
return _CLIENT.replace("__CONFIG__", repr(config))
def _pending_order(session: Session, order_id: int) -> LicenseOrder:
order = session.scalar(
select(LicenseOrder)