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

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
+29
View File
@@ -193,7 +193,36 @@ input:focus { border-color: var(--gold); }
.pill.online, .pill.completed { color: var(--online); }
.pill.offline, .pill.rejected, .pill.failed { color: var(--offline); }
.pill.pending { color: var(--gold); }
.genesis-tag {
display: inline-flex;
width: fit-content;
margin: 12px 0 0;
padding: 6px 10px;
border: 1px solid rgba(224, 192, 122, 0.55);
border-radius: 999px;
color: #e0c07a;
background: #08090d;
font-size: 12px;
letter-spacing: 0.03em;
}
.network-line { margin: 18px 0 0; color: #e0c07a; font-size: 14px; letter-spacing: 0.04em; }
.whitelist-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 12px; }
.toggle { display: inline-flex; align-items: center; gap: 8px; color: var(--muted); font-size: 12px; letter-spacing: 0.06em; cursor: pointer; }
.toggle input { position: absolute; opacity: 0; width: 0; height: 0; }
.toggle span { position: relative; width: 42px; height: 24px; border-radius: 999px; border: 1px solid rgba(224, 192, 122, 0.4); background: #08090d; }
.toggle span::after { content: ""; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: #8a8478; transition: transform 0.15s ease, background 0.15s ease; }
.toggle input:checked + span { border-color: #e0c07a; }
.toggle input:checked + span::after { transform: translateX(18px); background: #e0c07a; }
.toggle input:focus-visible + span { outline: 1px solid #e0c07a; }
.whitelist-panel { margin-top: 8px; }
.ip-form { display: grid; grid-template-columns: 1.2fr 1fr auto; gap: 12px; align-items: end; }
.ip-form button { height: 42px; }
@media (max-width: 720px) { .ip-form { grid-template-columns: 1fr; } }
.forms { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 16px; align-items: start; }
.span-2 { grid-column: 1 / -1; }
textarea, select { width: 100%; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--line); background: rgba(8, 9, 13, 0.55); color: inherit; font: inherit; }
textarea:focus, select:focus { border-color: var(--gold); outline: none; }
textarea { min-height: 96px; resize: vertical; }
.panel { display: grid; gap: 12px; padding: 22px; }
.panel h2 { font-family: var(--serif); font-size: 28px; font-weight: 560; }
.table-wrap { overflow: auto; margin-top: 8px; }
+153 -5
View File
@@ -10,23 +10,30 @@ const nodes = ref([]);
const withdrawals = ref([]);
const orders = ref([]);
const hashes = ref({});
const whitelist = ref([]);
const applications = ref([]);
const ipForm = ref({ ip_address: "", note: "" });
const error = ref("");
const notice = ref("");
let timer = 0;
async function load() {
const [nextSummary, nextUsers, nextNodes, nextCash, nextOrders] = await Promise.all([
const [nextSummary, nextUsers, nextNodes, nextCash, nextOrders, nextIps, nextApps] = await Promise.all([
api("/admin/summary"),
api("/admin/users"),
api("/admin/nodes"),
api("/admin/withdrawals"),
api("/admin/orders"),
api("/admin/ip-whitelist"),
api("/admin/datacenter-applications"),
]);
summary.value = nextSummary;
users.value = nextUsers;
nodes.value = nextNodes;
withdrawals.value = nextCash;
orders.value = nextOrders;
whitelist.value = nextIps;
applications.value = nextApps;
}
async function approveOrder(row) {
@@ -34,7 +41,7 @@ async function approveOrder(row) {
notice.value = "";
try {
const result = await api(`/admin/orders/${row.id}/approve`, { method: "POST" });
notice.value = `订单 #${row.id} 已开通,节点 ${result.sn}`;
notice.value = `订单 #${row.id} 已开通,节点 ${result.display_node_id || result.sn}`;
await load();
} catch (err) {
error.value = err.message;
@@ -76,6 +83,82 @@ async function confirm(row) {
}
}
async function toggleWhitelist(node) {
error.value = "";
notice.value = "";
const next = !node.is_whitelist;
try {
await api(`/admin/nodes/${encodeURIComponent(node.sn)}/whitelist`, {
method: "POST",
body: JSON.stringify({ is_whitelist: next }),
});
notice.value = next
? `节点 ${node.display_node_id || node.sn} 已加入白名单`
: `节点 ${node.display_node_id || node.sn} 已取消白名单`;
await load();
} catch (err) {
error.value = err.message;
}
}
async function addIp() {
error.value = "";
notice.value = "";
try {
await api("/admin/ip-whitelist", {
method: "POST",
body: JSON.stringify({
ip_address: ipForm.value.ip_address.trim(),
note: ipForm.value.note.trim(),
}),
});
ipForm.value = { ip_address: "", note: "" };
notice.value = "白名单 IP 已添加";
await load();
} catch (err) {
error.value = err.message;
}
}
async function approveDatacenter(row) {
error.value = "";
notice.value = "";
try {
await api(`/admin/datacenter-applications/${row.id}/approve`, { method: "POST" });
notice.value = `机房申请 #${row.id} 已通过,网段 ${row.subnet_ip} 已加入白名单`;
await load();
} catch (err) {
error.value = err.message;
}
}
async function rejectDatacenter(row) {
error.value = "";
notice.value = "";
try {
await api(`/admin/datacenter-applications/${row.id}/reject`, {
method: "POST",
body: JSON.stringify({ note: "未达到机房门槛" }),
});
notice.value = `机房申请 #${row.id} 已拒绝`;
await load();
} catch (err) {
error.value = err.message;
}
}
async function removeIp(row) {
error.value = "";
notice.value = "";
try {
await api(`/admin/ip-whitelist/${row.id}`, { method: "DELETE" });
notice.value = `${row.ip_address} 已移出白名单`;
await load();
} catch (err) {
error.value = err.message;
}
}
async function reject(row) {
error.value = "";
notice.value = "";
@@ -118,7 +201,7 @@ onUnmounted(() => window.clearInterval(timer));
<em>已注册钱包</em>
</article>
<article class="metric">
<span>节点</span>
<span>全网节点</span>
<strong>{{ summary.nodes }}</strong>
<em>在线 {{ summary.online }} · 离线 {{ summary.offline }}</em>
</article>
@@ -147,7 +230,7 @@ onUnmounted(() => window.clearInterval(timer));
<td>{{ money(row.amount) }} {{ row.asset_symbol }}</td>
<td class="mono">{{ row.tx_id }}</td>
<td><span class="pill" :class="row.status">{{ statusText[row.status] }}</span></td>
<td>{{ row.sn || "—" }}</td>
<td>{{ row.display_node_id || row.sn || "—" }}</td>
<td class="actions" v-if="row.status === 'pending'">
<button class="gold" type="button" @click="approveOrder(row)">通过</button>
<button class="texty" type="button" @click="rejectOrder(row)">拒绝</button>
@@ -163,19 +246,84 @@ onUnmounted(() => window.clearInterval(timer));
<section class="board">
<article v-for="node in nodes" :key="node.sn" class="plate">
<header>
<strong>{{ node.sn }}</strong>
<strong>{{ node.display_node_id || node.sn }}</strong>
<span class="pill" :class="node.status"><i />{{ statusText[node.status] }}</span>
</header>
<div class="whitelist-row">
<span v-if="node.is_whitelist" class="pill pending">白名单</span>
<label class="toggle">
<input type="checkbox" :checked="node.is_whitelist" @change="toggleWhitelist(node)" />
<span></span>
白名单
</label>
</div>
<p v-if="node.genesis" class="genesis-tag">[Genesis Special Node - 30 Days Recovery]</p>
<p v-if="node.genesis" class="sub">创世特权节点 - 30天回本</p>
<p class="wallet">{{ node.wallet_address }}</p>
<p class="sub">{{ node.sn }}</p>
<p class="rate"><b :style="{ width: pct(node.online_rate) }" />{{ pct(node.online_rate) }} 今日在线率</p>
<dl>
<div><dt>今日积分</dt><dd>{{ money(node.today_points) }}</dd></div>
<div><dt>心跳</dt><dd>{{ when(node.last_heartbeat_at) }}</dd></div>
<div><dt>绑定 IP</dt><dd>{{ node.client_ip || "—" }}</dd></div>
</dl>
</article>
<p v-if="!nodes.length" class="empty">还没有节点。</p>
</section>
<h2 class="section-title">IP 白名单设置</h2>
<section class="panel whitelist-panel">
<p class="sub">可以填写单 IP,或整个机房网段,例如 203.0.113.0/24。网段内的地址都可以挂载多台设备。</p>
<form class="ip-form" @submit.prevent="addIp">
<label>豁免 IP / 网段<input v-model="ipForm.ip_address" required placeholder="203.0.113.0/24" /></label>
<label>备注<input v-model="ipForm.note" placeholder="自营机房出口" /></label>
<button class="gold" type="submit">添加</button>
</form>
<div class="table-wrap">
<table>
<thead>
<tr><th>IP</th><th>备注</th><th>添加时间</th><th></th></tr>
</thead>
<tbody>
<tr v-for="row in whitelist" :key="row.id">
<td class="mono">{{ row.ip_address }}</td>
<td>{{ row.note || "—" }}</td>
<td>{{ when(row.created_at) }}</td>
<td class="actions"><button class="texty" type="button" @click="removeIp(row)">删除</button></td>
</tr>
<tr v-if="!whitelist.length"><td colspan="4" class="empty">还没有豁免 IP。</td></tr>
</tbody>
</table>
</div>
</section>
<h2 class="section-title">机房大客户申请</h2>
<div class="table-wrap">
<table>
<thead>
<tr><th>单号</th><th>钱包</th><th>联系方式</th><th>节点数</th><th>网段</th><th>显卡</th><th>证明</th><th>状态</th><th></th></tr>
</thead>
<tbody>
<tr v-for="row in applications" :key="row.id">
<td>#{{ row.id }}</td>
<td class="mono">{{ row.wallet_address }}</td>
<td>{{ row.contact }}</td>
<td>{{ row.node_count }}</td>
<td class="mono">{{ row.subnet_ip }}</td>
<td>{{ row.gpu_generation }} 系</td>
<td>{{ row.hardware_proof }}</td>
<td><span class="pill" :class="row.status">{{ statusText[row.status] }}</span></td>
<td class="actions" v-if="row.status === 'pending'">
<button class="gold" type="button" @click="approveDatacenter(row)">通过</button>
<button class="texty" type="button" @click="rejectDatacenter(row)">拒绝</button>
</td>
<td v-else></td>
</tr>
<tr v-if="!applications.length"><td colspan="9" class="empty">还没有机房申请。</td></tr>
</tbody>
</table>
</div>
<h2 class="section-title">提现审核</h2>
<div class="table-wrap">
<table>
+68 -5
View File
@@ -9,6 +9,13 @@ const error = ref("");
const notice = ref("");
const txId = ref("");
const cashForm = ref({ amount: "", asset_symbol: "POINT" });
const datacenterForm = ref({
contact: "",
node_count: "5",
subnet_ip: "",
hardware_proof: "",
gpu_generation: "40",
});
let timer = 0;
async function load() {
@@ -27,6 +34,17 @@ async function run(work, done) {
}
}
function gpuText(node) {
if (!node.gpu_model) return "未上报";
return node.vram_gb ? `${node.gpu_model} · ${node.vram_gb} GB` : node.gpu_model;
}
function generationText(node) {
if (node.gpu_generation === "40") return "40系及以上";
if (node.gpu_generation === "30") return "30系";
return "10系及以下";
}
function placeOrder() {
return run(async () => {
await api("/me/orders", { method: "POST", body: JSON.stringify({ tx_id: txId.value.trim() }) });
@@ -48,6 +66,23 @@ function downloadClient(node) {
return run(() => download(`/me/nodes/${encodeURIComponent(node.sn)}/client`, `hypernode-${node.sn}.py`), "客户端已开始下载");
}
function applyDatacenter() {
return run(async () => {
await api("/me/datacenter-applications", {
method: "POST",
body: JSON.stringify({
contact: datacenterForm.value.contact.trim(),
node_count: Number(datacenterForm.value.node_count),
subnet_ip: datacenterForm.value.subnet_ip.trim(),
hardware_proof: datacenterForm.value.hardware_proof.trim(),
gpu_generation: datacenterForm.value.gpu_generation,
}),
});
datacenterForm.value.subnet_ip = "";
datacenterForm.value.hardware_proof = "";
}, "机房申请已提交,等待审核");
}
function requestCash() {
return run(async () => {
await api("/me/withdrawals", {
@@ -93,9 +128,9 @@ onUnmounted(() => window.clearInterval(timer));
<em>按当前在线实时估算</em>
</article>
<article class="metric">
<span>节点</span>
<strong>{{ board.nodes.length }}</strong>
<em>{{ board.nodes.filter((node) => node.status === "online").length }} 台在线</em>
<span>全网节点</span>
<strong>{{ board.network_nodes }}</strong>
<em>在线 {{ board.network_online }}</em>
</article>
<p class="fine">* 算力积分兑换汇率:1 积分 = 0.8 ~ 1.0 USDT(根据全网算力动态浮动,当前基准约 0.95 USDT)</p>
</section>
@@ -108,7 +143,7 @@ onUnmounted(() => window.clearInterval(timer));
</header>
<p class="wallet">{{ money(order.amount) }} {{ order.asset_symbol }} · {{ order.tx_id }}</p>
<p v-if="order.status === 'pending'" class="sub">审核通过前不能复制 Token,也不能下载客户端。</p>
<p v-else-if="order.sn" class="sub">节点 {{ order.sn }}</p>
<p v-else-if="order.display_node_id" class="sub">节点 {{ order.display_node_id }}</p>
<div v-if="order.status !== 'approved'" class="actions">
<button class="gold" type="button" disabled>复制 Token</button>
<button class="texty" type="button" disabled>下载客户端</button>
@@ -116,14 +151,21 @@ onUnmounted(() => window.clearInterval(timer));
</article>
<article v-for="node in board?.nodes || []" :key="node.sn" class="plate">
<header>
<strong>{{ node.sn }}</strong>
<strong>{{ node.display_node_id || node.sn }}</strong>
<span class="pill" :class="node.activated ? node.status : 'pending'"><i />{{ node.activated ? statusText[node.status] : "待审核" }}</span>
</header>
<p v-if="node.genesis" class="genesis-tag">[Genesis Special Node - 30 Days Recovery]</p>
<p v-if="node.genesis" class="sub">创世特权节点 - 30天回本</p>
<p class="rate"><b :style="{ width: pct(node.online_rate) }" />{{ pct(node.online_rate) }} 今日在线</p>
<dl>
<div><dt>购买金额</dt><dd>{{ money(node.purchase_amount) }}</dd></div>
<div><dt>今日积分</dt><dd>{{ money(node.today_points) }}</dd></div>
<div><dt>最近心跳</dt><dd>{{ when(node.last_heartbeat_at) }}</dd></div>
<div><dt>显卡</dt><dd>{{ gpuText(node) }}</dd></div>
<div><dt>硬件权重</dt><dd>{{ node.hardware_weight }}</dd></div>
<div><dt>显卡世代</dt><dd>{{ generationText(node) }}</dd></div>
<div><dt>回本周期</dt><dd>{{ node.recovery_days }} 天</dd></div>
<div><dt>待返还质押</dt><dd>{{ money(node.recoverable_stake_amount || 0) }}</dd></div>
</dl>
<label>挂机 Token<input class="token-box" :value="node.activated ? node.node_token : '审核通过后发放'" readonly /></label>
<div class="actions">
@@ -154,6 +196,27 @@ onUnmounted(() => window.clearInterval(timer));
</li>
</ul>
</form>
<form class="panel span-2" @submit.prevent="applyDatacenter">
<h2>机房白名单申请</h2>
<p class="sub">同一公网出口批量接入,需至少 5 台且全部为 30 系或 40 系显卡。审核通过后,该网段内硬件指纹不同的机器可以一起挂载。</p>
<label>联系方式<input v-model="datacenterForm.contact" required minlength="4" placeholder="Telegram / 邮箱 / 手机" /></label>
<label>预估节点数<input v-model="datacenterForm.node_count" required type="number" min="5" placeholder="5" /></label>
<label>出口网段<input v-model="datacenterForm.subnet_ip" required placeholder="203.0.113.0/24" /></label>
<label>显卡世代
<select v-model="datacenterForm.gpu_generation">
<option value="40">40 系及以上</option>
<option value="30">30 系</option>
</select>
</label>
<label>硬件配置证明<textarea v-model="datacenterForm.hardware_proof" required minlength="4" placeholder="例如 8 台 RTX 4090,附采购或机房照片说明" /></label>
<button class="gold" type="submit">提交机房申请</button>
<ul class="mini">
<li v-for="row in board?.datacenter_applications || []" :key="row.id">
<span>#{{ row.id }} · {{ row.subnet_ip }} · {{ row.node_count }} 台</span>
<span class="pill" :class="row.status">{{ statusText[row.status] }}</span>
</li>
</ul>
</form>
</section>
</Shell>
</template>
+14 -1
View File
@@ -1,5 +1,5 @@
<script setup>
import { ref } from "vue";
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { api, saveSession } from "../api";
@@ -15,6 +15,18 @@ const form = ref({
confirm: "",
});
const termsOpen = ref(false);
const networkNodes = ref(null);
const networkOnline = ref(null);
onMounted(async () => {
try {
const data = await api("/network");
networkNodes.value = data.nodes;
networkOnline.value = data.online;
} catch {
networkNodes.value = null;
}
});
async function submit() {
error.value = "";
@@ -68,6 +80,7 @@ function register() {
<p class="eyebrow">HyperNode</p>
<h1>连接全球 DePIN 分布式算力网络</h1>
<p>基于去中心化节点的推理验证与任务调度平台。连接您的算力节点,实时监控质押释放与算力收益。</p>
<p v-if="networkNodes !== null" class="network-line">全网节点 {{ networkNodes }} · 在线 {{ networkOnline }}</p>
</section>
<form class="gate-card" @submit.prevent="submit">
<div class="switch">