0740016ad8
Co-authored-by: Cursor <cursoragent@cursor.com>
272 lines
8.4 KiB
Python
272 lines
8.4 KiB
Python
"""节点授权订单。付款凭证先入库,管理员通过后才开通节点。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import secrets
|
|
|
|
from sqlalchemy import case, select
|
|
from sqlalchemy.exc import IntegrityError
|
|
from sqlalchemy.orm import Session, selectinload
|
|
|
|
from app.clock import utcnow
|
|
from app.config import settings
|
|
from app.errors import AppError
|
|
from app.models import LicenseOrder, OrderStatus
|
|
from app.money import money
|
|
from app.services.registry import register_node
|
|
|
|
|
|
def place_order(session: Session, user_id: int, tx_id: str) -> LicenseOrder:
|
|
digest = tx_id.strip()
|
|
if session.scalar(select(LicenseOrder.id).where(LicenseOrder.tx_id == digest)) is not None:
|
|
raise AppError("这笔交易已经提交过", 409)
|
|
order = LicenseOrder(
|
|
user_id=user_id,
|
|
amount=money(settings.license_price),
|
|
asset_symbol=settings.license_asset.strip().upper() or "USDT",
|
|
status=OrderStatus.PENDING,
|
|
tx_id=digest,
|
|
created_at=utcnow(),
|
|
)
|
|
session.add(order)
|
|
try:
|
|
session.flush()
|
|
except IntegrityError as exc:
|
|
session.rollback()
|
|
raise AppError("这笔交易已经提交过", 409) from exc
|
|
return order
|
|
|
|
|
|
def approve_order(session: Session, order_id: int) -> LicenseOrder:
|
|
order = _pending_order(session, order_id)
|
|
token = secrets.token_urlsafe(24)
|
|
node = register_node(
|
|
session,
|
|
order.user.wallet_address,
|
|
f"HN{order.id:06d}",
|
|
order.amount,
|
|
node_token=token,
|
|
)
|
|
order.status = OrderStatus.APPROVED
|
|
order.node = node
|
|
order.reviewed_at = utcnow()
|
|
session.flush()
|
|
return order
|
|
|
|
|
|
def reject_order(session: Session, order_id: int, note: str | None = None) -> LicenseOrder:
|
|
order = _pending_order(session, order_id)
|
|
order.status = OrderStatus.REJECTED
|
|
order.note = note
|
|
order.reviewed_at = utcnow()
|
|
session.flush()
|
|
return order
|
|
|
|
|
|
def list_orders(session: Session, user_id: int | None = None) -> list[LicenseOrder]:
|
|
pending_first = case((LicenseOrder.status == OrderStatus.PENDING, 0), else_=1)
|
|
query = select(LicenseOrder).options(selectinload(LicenseOrder.user), selectinload(LicenseOrder.node))
|
|
if user_id is not None:
|
|
query = query.where(LicenseOrder.user_id == user_id)
|
|
return list(session.scalars(query.order_by(pending_first, LicenseOrder.id.desc())).all())
|
|
|
|
|
|
_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__)
|
|
|
|
|
|
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(hardware()).encode()
|
|
request = urllib.request.Request(
|
|
CONFIG["base"] + "/api/v1/node/heartbeat",
|
|
data=body,
|
|
headers={"Content-Type": "application/json"},
|
|
)
|
|
with urllib.request.urlopen(request, timeout=20) as response:
|
|
print(response.read().decode())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
while True:
|
|
try:
|
|
beat()
|
|
except (urllib.error.URLError, TimeoutError, OSError) as exc:
|
|
print(exc)
|
|
time.sleep(60)
|
|
"""
|
|
|
|
|
|
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)
|
|
.options(selectinload(LicenseOrder.user), selectinload(LicenseOrder.node))
|
|
.where(LicenseOrder.id == order_id)
|
|
)
|
|
if order is None:
|
|
raise AppError("订单不存在", 404)
|
|
if order.status != OrderStatus.PENDING:
|
|
raise AppError("只有待审核的订单可以处理", 409)
|
|
return order
|