diff --git a/_deploy/console-server/server.js b/_deploy/console-server/server.js index 877851a..6f1c972 100644 --- a/_deploy/console-server/server.js +++ b/_deploy/console-server/server.js @@ -1,5 +1,5 @@ -// # 光湖服务器主控台 API -// # Guanghu Server Console v3.2 — with Pool Registry API +// # 光湖服务器主控台 API v3.3 +// # Guanghu Server Console v3.3 — 硬件采集 + 操作日志 + 实时监控 // # 部署: guanghulab.com/console/ · SG-001:3920 // # 版权: 国作登字-2026-A-00037559 · TCS-0002∞ @@ -16,68 +16,224 @@ app.use(express.static(path.join(__dirname, 'public'))); const DATA_DIR = '/opt/zhuyuan/data'; try { fs.mkdirSync(DATA_DIR, { recursive: true }); } catch(e) {} -// 全服务器配置 — 冰朔6台 + 企业3台 +// 全服务器配置 — 冰朔6台 + 企业3台 = 11台 const SERVERS = [ - { code: "BS-GZ-006", name: "广州 · 代码仓库", ip: "43.139.217.141", key: "zy_gtw_4a155446b7acc09fe46a2a29972c0ca5d9954da19c35dc23", port: 3910 }, - { code: "BS-SG-001", name: "新加坡 · 铸渊大脑", ip: "43.156.237.110", key: "zy_gtw_74d30f54fa8b5581514569ee7874def57861a31ccb2be8c9", port: 3911 }, - { code: "BS-SG-002", name: "新加坡 · 面孔", ip: "43.134.16.246", key: "zy_gtw_d1f6d2b8cb4ea44292bd036e4dd03a70745ea502d4a3ae40", port: 3910 }, - { code: "BS-SG-003", name: "新加坡 · 中继", ip: "43.153.193.169", key: "zy_gtw_d7c033d8ae992ffc9dd3e0dd5fe332dace0b644c0ae7c847", port: 3910 }, - { code: "ZY-SG-006", name: "新加坡 · 语料", ip: "43.153.203.105", key: "zy_gtw_fff861a2fe40cbdc366ee21f218023be8b1c182f66c852d3", port: 3910 }, - { code: "BS-SH-005", name: "上海 · 国内节点", ip: "124.223.10.33", key: "zy_gtw_b1045de5ddfd7832e3c53349d9c896f054b85a4a9ece22f9", port: 3910 }, - { code: "AW-GZ-001", name: "企业主服务器 · 广州", ip: "43.139.251.175", key: "zy_gtw_cab20e8c1b957aaad7507bf542231521bba30665461aa540", port: 3910 }, - { code: "AW-SH-002", name: "企业灾备 · 上海", ip: "124.222.54.198", key: "zy_gtw_242f1dd3b14c1260fb01196083abde3dcb85e53532d27666", port: 3910 }, - { code: "AW-GZ-003", name: "企业灾备 · 广州", ip: "119.29.181.132", key: "zy_gtw_8c9a8b439af823690ae6ad57f52734771ff39493ab986032", port: 3910 }, + { code: "BS-GZ-006", name: "广州 · 代码仓库", ip: "43.139.217.141", key: "zy_gtw_4a155446b7acc09fe46a2a29972c0ca5d9954da19c35dc23", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 60 } }, + { code: "BS-SG-001", name: "新加坡 · 铸渊大脑", ip: "43.156.237.110", key: "zy_gtw_74d30f54fa8b5581514569ee7874def57861a31ccb2be8c9", port: 3911, spec: { cpu: 4, mem_gb: 8, disk_gb: 80 } }, + { code: "BS-SG-002", name: "新加坡 · 面孔", ip: "43.134.16.246", key: "zy_gtw_d1f6d2b8cb4ea44292bd036e4dd03a70745ea502d4a3ae40", port: 3910, spec: { cpu: 2, mem_gb: 2, disk_gb: 30 } }, + { code: "BS-SG-003", name: "新加坡 · 中继", ip: "43.153.193.169", key: "zy_gtw_d7c033d8ae992ffc9dd3e0dd5fe332dace0b644c0ae7c847", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 80 } }, + { code: "ZY-SG-006", name: "新加坡 · 语料", ip: "43.153.203.105", key: "zy_gtw_fff861a2fe40cbdc366ee21f218023be8b1c182f66c852d3", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 50 } }, + { code: "BS-SH-005", name: "上海 · 国内节点", ip: "124.223.10.33", key: "zy_gtw_b1045de5ddfd7832e3c53349d9c896f054b85a4a9ece22f9", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 30 } }, + { code: "AW-GZ-001", name: "企业主服务器 · 广州", ip: "43.139.251.175", key: "zy_gtw_cab20e8c1b957aaad7507bf542231521bba30665461aa540", port: 3910, spec: { cpu: 4, mem_gb: 16, disk_gb: 80 } }, + { code: "AW-SH-002", name: "企业灾备 · 上海", ip: "124.222.54.198", key: "zy_gtw_242f1dd3b14c1260fb01196083abde3dcb85e53532d27666", port: 3910, spec: { cpu: 4, mem_gb: 4, disk_gb: 40 } }, + { code: "AW-GZ-003", name: "企业灾备 · 广州", ip: "119.29.181.132", key: "zy_gtw_8c9a8b439af823690ae6ad57f52734771ff39493ab986032", port: 3910, spec: { cpu: 2, mem_gb: 8, disk_gb: 50 } }, + // 页页 · 硅谷(待接入) + // { code: "YY-SV-001", name: "页页 · 硅谷", ip: "TBD", key: "TBD", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 50 } }, + // 之之 · 待定 + // { code: "ZZ-XX-001", name: "之之 · 待定", ip: "TBD", key: "TBD", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 50 } }, ]; const tempKeys = {}; const authQueue = []; -async function pingServer(srv) { - const start = Date.now(); - try { - const data = JSON.stringify({}); - const opts = { hostname: srv.ip, port: srv.port, path: '/health', method: 'POST', headers: { 'Authorization': 'Bearer ' + srv.key, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }, timeout: 5000 }; - const result = await new Promise((resolve) => { - const req = http.request(opts, (res) => { let body = ''; res.on('data', c => body += c); res.on('end', () => resolve({ ok: true, status: res.statusCode, body })); }); - req.on('error', (e) => resolve({ ok: false, error: e.message })); - req.on('timeout', () => { req.destroy(); resolve({ ok: false, error: 'timeout' }); }); - req.write(data); req.end(); +// ── 硬件缓存 (60秒TTL) ── +const hwCache = {}; // { code: { data, ts } } + +// ── Gatekeeper exec 通用函数 ── +async function gatekeeperExec(srv, cmd, timeoutMs) { + timeoutMs = timeoutMs || 10000; + const d = JSON.stringify({ cmd }); + const opts = { + hostname: srv.ip, port: srv.port, path: '/exec', method: 'POST', + headers: { 'Authorization': 'Bearer ' + srv.key, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(d) }, + timeout: timeoutMs + }; + return new Promise((resolve) => { + const req = http.request(opts, (res) => { + let b = ''; res.on('data', c => b += c); + res.on('end', () => { try { resolve({ ok: res.statusCode === 200, body: JSON.parse(b) }); } catch(e) { resolve({ ok: false, body: b, error: e.message }); } }); }); - return { code: srv.code, name: srv.name, ip: srv.ip, alive: result.ok && result.status === 200, latency: Date.now() - start, error: result.error || null, last_seen: new Date().toISOString() }; - } catch (e) { return { code: srv.code, name: srv.name, ip: srv.ip, alive: false, latency: 0, error: e.message, last_seen: new Date().toISOString() }; } + req.on('error', (e) => resolve({ ok: false, error: e.message })); + req.on('timeout', () => { req.destroy(); resolve({ ok: false, error: 'timeout' }); }); + req.write(d); req.end(); + }); } -function checkAgentAuth(req) { const a = req.headers.authorization || ''; const t = a.replace('Bearer ', ''); if (!t) return false; try { const d = fs.readFileSync('/tmp/zhuyuan-keys.json', 'utf-8'); return JSON.parse(d || '[]').some(k => k.value === t); } catch(e) { return false; } } +// ── 硬件采集(通过 Gatekeeper exec) ── +async function fetchHardware(srv) { + const now = Date.now(); + const cached = hwCache[srv.code]; + if (cached && (now - cached.ts) < 60000) return cached.data; // 60s cache + + try { + const cmd = "echo 'CPU_CORES:'$(nproc);echo 'UPTIME_S:'$(awk '{print int($1)}' /proc/uptime 2>/dev/null||echo 0);free -m|awk '/^Mem:/{printf \"MEM_TOTAL:%d\\nMEM_USED:%d\\nMEM_AVAIL:%d\\n\",$2,$3,$7}';cat /proc/loadavg 2>/dev/null|awk '{printf \"LOAD1:%.2f\\nLOAD5:%.2f\\nLOAD15:%.2f\\n\",$1,$2,$3}';df -h / 2>/dev/null|awk 'NR==2{printf \"DISK_TOTAL:%s\\nDISK_USED:%s\\nDISK_AVAIL:%s\\nDISK_PCT:%s\\n\",$2,$3,$4,$5}'"; + const r = await gatekeeperExec(srv, cmd, 8000); + + if (!r.ok || !r.body || !r.body.stdout) { + const fb = { cpu_cores: srv.spec.cpu || 2, uptime_h: 0, mem_total_mb: (srv.spec.mem_gb || 4) * 1024, mem_used_mb: 0, mem_avail_mb: (srv.spec.mem_gb || 4) * 1024, load_1min: 0, load_5min: 0, load_15min: 0, disk_total: (srv.spec.disk_gb || 50) + 'G', disk_used: '0G', disk_avail: (srv.spec.disk_gb || 50) + 'G', disk_pct: '0%', source: 'spec' }; + hwCache[srv.code] = { data: fb, ts: now }; + return fb; + } + + const stdout = r.body.stdout || ''; + const hw = { source: 'live' }; + + const m = (re) => { const match = stdout.match(re); return match ? match[1] : null; }; + + hw.cpu_cores = parseInt(m(/CPU_CORES:(\d+)/)) || srv.spec.cpu || 2; + hw.uptime_h = Math.floor((parseInt(m(/UPTIME_S:(\d+)/)) || 0) / 3600); + hw.mem_total_mb = parseInt(m(/MEM_TOTAL:(\d+)/)) || (srv.spec.mem_gb || 4) * 1024; + hw.mem_used_mb = parseInt(m(/MEM_USED:(\d+)/)) || 0; + hw.mem_avail_mb = parseInt(m(/MEM_AVAIL:(\d+)/)) || hw.mem_total_mb; + hw.load_1min = parseFloat(m(/LOAD1:([\d.]+)/)) || 0; + hw.load_5min = parseFloat(m(/LOAD5:([\d.]+)/)) || 0; + hw.load_15min = parseFloat(m(/LOAD15:([\d.]+)/)) || 0; + hw.disk_total = m(/DISK_TOTAL:(\S+)/) || (srv.spec.disk_gb || 50) + 'G'; + hw.disk_used = m(/DISK_USED:(\S+)/) || '0G'; + hw.disk_avail = m(/DISK_AVAIL:(\S+)/) || hw.disk_total; + hw.disk_pct = m(/DISK_PCT:(\S+)/) || '0%'; + + hw.mem_pct = hw.mem_total_mb > 0 ? Math.round(hw.mem_used_mb / hw.mem_total_mb * 100) : 0; + hw.cpu_pct = hw.cpu_cores > 0 ? Math.min(Math.round(hw.load_1min / hw.cpu_cores * 100), 100) : 0; + hw.disk_used_pct = parseInt(hw.disk_pct) || 0; + + hwCache[srv.code] = { data: hw, ts: now }; + return hw; + } catch (e) { + const fb = { cpu_cores: srv.spec.cpu || 2, uptime_h: 0, mem_total_mb: (srv.spec.mem_gb || 4) * 1024, mem_used_mb: 0, mem_avail_mb: (srv.spec.mem_gb || 4) * 1024, load_1min: 0, load_5min: 0, load_15min: 0, disk_total: (srv.spec.disk_gb || 50) + 'G', disk_used: '0G', disk_avail: (srv.spec.disk_gb || 50) + 'G', disk_pct: '0%', source: 'error', error: e.message }; + hwCache[srv.code] = { data: fb, ts: now }; + return fb; + } +} + +// ── 操作日志(滚动记录,最多100条) ── +const opLogs = []; +function addOpLog(code, name, action, detail, level) { + opLogs.unshift({ time: new Date().toISOString(), code, name, action, detail, level: level || 'info' }); + if (opLogs.length > 100) opLogs.length = 100; +} // ========== 基础 API ========== -app.get('/api/servers', async (req, res) => { const r = await Promise.all(SERVERS.map(s => pingServer(s))); res.json({ ok: true, servers: r, timestamp: new Date().toISOString() }); }); + +app.get('/api/servers', async (req, res) => { + const withHw = req.query.hw !== '0'; + const pingResults = await Promise.all(SERVERS.map(s => { + const base = { code: s.code, name: s.name, ip: s.ip, spec: s.spec }; + const start = Date.now(); + const data = JSON.stringify({}); + const opts = { + hostname: s.ip, port: s.port, path: '/health', method: 'POST', + headers: { 'Authorization': 'Bearer ' + s.key, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }, + timeout: 5000 + }; + return new Promise((resolve) => { + const req = http.request(opts, (res2) => { let b = ''; res2.on('data', c => b += c); res2.on('end', () => resolve({ ...base, alive: res2.statusCode === 200, latency: Date.now() - start, error: null })); }); + req.on('error', (e) => resolve({ ...base, alive: false, latency: 0, error: e.message })); + req.on('timeout', () => { req.destroy(); resolve({ ...base, alive: false, latency: 0, error: 'timeout' }); }); + req.write(data); req.end(); + }); + })); + + for (const r of pingResults) { + const srv = SERVERS.find(s => s.code === r.code); + r.last_seen = new Date().toISOString(); + if (r.alive && withHw) { + try { r.hardware = await fetchHardware(srv); } catch(e) { r.hardware = null; } + } + } + + res.json({ ok: true, servers: pingResults, timestamp: new Date().toISOString(), total: SERVERS.length }); +}); + +app.get('/api/logs', (req, res) => { + const limit = parseInt(req.query.limit) || 50; + const level = req.query.level; + let logs = opLogs.slice(0, limit); + if (level) logs = logs.filter(l => l.level === level); + res.json({ ok: true, logs, total: opLogs.length }); +}); + +app.get('/api/hardware/:code', async (req, res) => { + const s = SERVERS.find(s => s.code === req.params.code); + if (!s) return res.status(404).json({ ok: false, error: '未找到服务器' }); + try { const hw = await fetchHardware(s); res.json({ ok: true, code: s.code, hardware: hw }); } + catch (e) { res.status(500).json({ ok: false, error: e.message }); } +}); + +app.get('/api/summary', async (req, res) => { + const pingResults = await Promise.all(SERVERS.map(s => { + const start = Date.now(); + const data = JSON.stringify({}); + const opts = { + hostname: s.ip, port: s.port, path: '/health', method: 'POST', + headers: { 'Authorization': 'Bearer ' + s.key, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }, + timeout: 4000 + }; + return new Promise((resolve) => { + const req = http.request(opts, (res2) => { let b = ''; res2.on('data', c => b += c); res2.on('end', () => resolve({ code: s.code, alive: res2.statusCode === 200, latency: Date.now() - start })); }); + req.on('error', () => resolve({ code: s.code, alive: false, latency: 0 })); + req.on('timeout', () => { req.destroy(); resolve({ code: s.code, alive: false, latency: 0 }); }); + req.write(data); req.end(); + }); + })); + + const aliveCount = pingResults.filter(r => r.alive).length; + const totalCpu = SERVERS.reduce((sum, s) => sum + (s.spec.cpu || 2), 0); + const totalMem = SERVERS.reduce((sum, s) => sum + (s.spec.mem_gb || 4), 0); + const totalDisk = SERVERS.reduce((sum, s) => sum + (s.spec.disk_gb || 50), 0); + const alertCount = opLogs.filter(l => l.level === 'error').length; + + res.json({ + ok: true, total_servers: SERVERS.length, alive: aliveCount, + offline: SERVERS.length - aliveCount, alerts: alertCount, + total_cpu_cores: totalCpu, total_mem_gb: totalMem, total_disk_gb: totalDisk, + timestamp: new Date().toISOString() + }); +}); app.post('/api/knock', (req, res) => { const { target, reason } = req.body; if (!target) return res.status(400).json({ error: '缺少 target' }); const s = SERVERS.find(s => s.code === target || s.name.includes(target)); if (!s) return res.status(404).json({ error: '未找到' }); - const rid = 'req_' + crypto.randomBytes(8).toString('hex'); authQueue.push({ id: rid, target: s.code, target_name: s.name, reason: reason || '未说明', status: 'pending', created_at: new Date().toISOString(), temp_key: null }); + const rid = 'req_' + crypto.randomBytes(8).toString('hex'); + authQueue.push({ id: rid, target: s.code, target_name: s.name, reason: reason || '未说明', status: 'pending', created_at: new Date().toISOString(), temp_key: null }); + addOpLog(s.code, s.name, 'KNOCK', reason || '未说明', 'info'); res.json({ ok: true, request_id: rid, message: '等待冰朔确认' }); }); app.get('/api/pending', (req, res) => { res.json({ ok: true, requests: authQueue.filter(r => r.status === 'pending') }); }); app.post('/api/authorize', (req, res) => { - const { request_id } = req.body; const e = authQueue.find(r => r.id === request_id && r.status === 'pending'); if (!e) return res.status(404).json({ error: '未找到' }); - const tk = 'tmp_' + crypto.randomBytes(24).toString('hex'); e.status = 'approved'; e.temp_key = tk; e.approved_at = new Date().toISOString(); - tempKeys[tk] = { server: e.target, expires_at: Date.now() + 5*60*1000, request_id }; res.json({ ok: true, temp_key: tk, expires_in: '5分钟' }); + const { request_id } = req.body; + const e = authQueue.find(r => r.id === request_id && r.status === 'pending'); + if (!e) return res.status(404).json({ error: '未找到' }); + const tk = 'tmp_' + crypto.randomBytes(24).toString('hex'); + e.status = 'approved'; e.temp_key = tk; e.approved_at = new Date().toISOString(); + tempKeys[tk] = { server: e.target, expires_at: Date.now() + 5*60*1000, request_id }; + addOpLog(e.target, e.target_name, 'AUTHORIZED', '授权操作', 'info'); + res.json({ ok: true, temp_key: tk, expires_in: '5分钟' }); +}); +app.post('/api/reject', (req, res) => { + const e = authQueue.find(r => r.id === req.body.request_id && r.status === 'pending'); + if (e) { e.status = 'rejected'; addOpLog(e.target, e.target_name, 'REJECTED', '拒绝操作', 'warn'); } + res.json({ ok: true }); +}); +app.get('/api/verify-key/:key', (req, res) => { + const e = tempKeys[req.params.key]; + if (!e) return res.json({ ok: false, valid: false }); + if (Date.now() > e.expires_at) { delete tempKeys[req.params.key]; return res.json({ ok: false, valid: false }); } + res.json({ ok: true, valid: true, server: e.server }); }); -app.post('/api/reject', (req, res) => { const e = authQueue.find(r => r.id === req.body.request_id && r.status === 'pending'); if (e) e.status = 'rejected'; res.json({ ok: true }); }); -app.get('/api/verify-key/:key', (req, res) => { const e = tempKeys[req.params.key]; if (!e) return res.json({ ok: false, valid: false }); if (Date.now() > e.expires_at) { delete tempKeys[req.params.key]; return res.json({ ok: false, valid: false }); } res.json({ ok: true, valid: true, server: e.server }); }); app.post('/api/exec', async (req, res) => { - const { temp_key, target, cmd } = req.body; if (!temp_key || !target || !cmd) return res.status(400).json({ error: '缺少参数' }); + const { temp_key, target, cmd } = req.body; + if (!temp_key || !target || !cmd) return res.status(400).json({ error: '缺少参数' }); const ke = tempKeys[temp_key]; if (!ke || Date.now() > ke.expires_at) return res.status(401).json({ error: '密钥无效' }); const s = SERVERS.find(s => s.code === target); if (!s) return res.status(404).json({ error: '未找到' }); try { - const d = JSON.stringify({ cmd }); const o = { hostname: s.ip, port: s.port, path: '/exec', method: 'POST', headers: { 'Authorization': 'Bearer ' + s.key, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(d) }, timeout: 30000 }; - const r = await new Promise((resolve) => { const req = http.request(o, (r) => { let b = ''; r.on('data', c => b += c); r.on('end', () => { try { resolve(JSON.parse(b)); } catch(e) { resolve({ error: b }); } }); }); req.on('error', (e) => resolve({ error: e.message })); req.write(d); req.end(); }); + const r = await gatekeeperExec(s, cmd, 30000); + addOpLog(s.code, s.name, 'EXEC', cmd.slice(0, 60), 'info'); res.json({ ok: true, result: r }); } catch (e) { res.status(500).json({ error: e.message }); } }); -// ========== 节点池 API (v3.2) ========== +// ========== 节点池 API ========== const POOL_REGISTRY_FILE = DATA_DIR + '/pool-registry.json'; const JOIN_TOKENS_FILE = DATA_DIR + '/join-tokens.json'; function loadPoolRegistry() { try { if (fs.existsSync(POOL_REGISTRY_FILE)) return JSON.parse(fs.readFileSync(POOL_REGISTRY_FILE, 'utf-8')); } catch(e) {} return { active: {}, pending: {}, history: [] }; } @@ -96,6 +252,7 @@ app.post('/api/pool/join', (req, res) => { const rid = 'reg_' + crypto.randomBytes(8).toString('hex'); r.pending[node_code] = { registration_id: rid, node_code, node_name: node_name || node_code, side, team_id: team_id || null, operator_name: operator_name || '未知', hardware: hardware || {}, network: network || {}, status: 'pending', created_at: new Date().toISOString(), token_id: mt.token_id }; mt.used = (mt.used || 0) + 1; saveJoinTokens(jt); savePoolRegistry(r); + addOpLog(node_code, node_name || node_code, 'POOL_JOIN', side + '侧节点申请加入', 'info'); res.json({ ok: true, status: 'pending', registration_id: rid, node_code, poll_interval_seconds: 15 }); }); app.get('/api/pool/join-status/:rid', (req, res) => { @@ -109,13 +266,34 @@ app.post('/api/pool/approve', (req, res) => { const r = loadPoolRegistry(); let f = null, fc = null; for (const [c, n] of Object.entries(r.pending)) { if (n.registration_id === registration_id) { f = n; fc = c; break; } } if (!f) return res.status(404).json({ ok: false }); - if (action === 'approve') { const pt = 'zyp_' + crypto.randomBytes(16).toString('hex'); r.active[fc] = { node_code: fc, node_name: f.node_name, side: f.side, team_id: f.team_id, ip: f.network?.public_ip || '', pool_token: pt, joined_at: new Date().toISOString(), hardware: f.hardware || {} }; delete r.pending[fc]; savePoolRegistry(r); res.json({ ok: true, node_code: fc, pool_token: pt }); } + if (action === 'approve') { const pt = 'zyp_' + crypto.randomBytes(16).toString('hex'); r.active[fc] = { node_code: fc, node_name: f.node_name, side: f.side, team_id: f.team_id, ip: f.network?.public_ip || '', pool_token: pt, joined_at: new Date().toISOString(), hardware: f.hardware || {} }; delete r.pending[fc]; savePoolRegistry(r); addOpLog(fc, f.node_name, 'POOL_APPROVE', '加入算力池', 'info'); res.json({ ok: true, node_code: fc, pool_token: pt }); } else { r.history.push({ ...f, status: 'rejected', rejected_at: new Date().toISOString() }); delete r.pending[fc]; savePoolRegistry(r); res.json({ ok: true, action: 'rejected' }); } }); app.get('/api/pool/tokens', (req, res) => { const r = loadPoolRegistry(); res.json({ ok: true, nodes: Object.entries(r.active).map(([c, n]) => ({ node_code: c, node_name: n.node_name, side: n.side, team_id: n.team_id, hardware: n.hardware })) }); }); -app.delete('/api/pool/node/:code', (req, res) => { const r = loadPoolRegistry(); if (!r.active[req.params.code]) return res.status(404).json({ ok: false }); r.history.push({ ...r.active[req.params.code], status: 'removed', removed_at: new Date().toISOString() }); delete r.active[req.params.code]; savePoolRegistry(r); res.json({ ok: true }); }); - -let cachedPoolStatus = {}; +app.delete('/api/pool/node/:code', (req, res) => { const r = loadPoolRegistry(); if (!r.active[req.params.code]) return res.status(404).json({ ok: false }); r.history.push({ ...r.active[req.params.code], status: 'removed', removed_at: new Date().toISOString() }); delete r.active[req.params.code]; savePoolRegistry(r); addOpLog(req.params.code, 'POOL_REMOVE', '移出算力池', 'warn'); res.json({ ok: true }); }); +app.post('/api/pool/report', (req, res) => { + const { pool_token, node_code, hardware, tasks } = req.body; + if (!pool_token) return res.status(401).json({ ok: false, error: '缺少 pool_token' }); + const r = loadPoolRegistry(); + const n = r.active[node_code]; + if (!n || n.pool_token !== pool_token) return res.status(401).json({ ok: false, error: 'token 无效' }); + n.last_report = new Date().toISOString(); + if (hardware) n.hardware = { ...n.hardware, ...hardware }; + if (tasks) n.tasks = tasks; + savePoolRegistry(r); + res.json({ ok: true, node_code }); +}); +// ========== 启动 ========== const PORT = process.env.CONSOLE_PORT || 3920; -app.listen(PORT, '127.0.0.1', () => { console.log('光湖主控台 v3.2 · ' + SERVERS.length + '台 | SG-001:' + PORT); }); \ No newline at end of file +app.listen(PORT, '127.0.0.1', () => { + console.log('光湖主控台 v3.3 · ' + SERVERS.length + '台 | SG-001:' + PORT); + console.log(' 硬件采集: 60s缓存 · 操作日志: 100条上限'); +}); + +setInterval(() => { + const now = Date.now(); + for (const [code, entry] of Object.entries(hwCache)) { + if (now - entry.ts > 120000) delete hwCache[code]; + } +}, 60000); \ No newline at end of file