铸渊 ICE-GL-ZY001 80bfaca576 feat: push-heartbeat 推送模式 — 各服主动上报非扫描
- 新增 push-heartbeat.js: 每台服务器部署,采集本机数据POST到主控台
- server.js v3.5: 新增 /api/heartbeat 接收端点 + /api/heartbeat-summary
- 架构改进: 冰朔指出扫描模式像攻击 → 改为push模式
2026-05-26 02:40:56 +08:00

347 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// # 光湖服务器主控台 API v3.4
// # Guanghu Server Console v3.4 — 12台(冰6+企3+之2+页1)
// # 部署: guanghulab.com/console/ · GZ-006:3920(主) + SG-001:3920(备)
// # 版权: 国作登字-2026-A-00037559 · TCS-0002∞
const express = require('express');
const http = require('http');
const crypto = require('crypto');
const path = require('path');
const fs = require('fs');
const app = express();
app.use(express.json());
app.use(express.static(__dirname));
const DATA_DIR = '/opt/zhuyuan/data';
try { fs.mkdirSync(DATA_DIR, { recursive: true }); } catch(e) {}
// 全服务器配置 — 冰朔6台 + 企业3台 + 之之2台 + 页页1台 = 12台
const SERVERS = [
{ 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: "ZZ-SV-001", name: "之之 · 硅谷", ip: "43.173.121.48", key: "zy_gtw_06151d40b1313956b069f3c13d0c1b8a70405ae201f5e7b2", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 59 } },
{ code: "ZZ-GZ-001", name: "之之 · 广州", ip: "193.112.126.174", key: "zy_gtw_77c779183284bf39d58080b4f5377d74ec6abc0f1f2218d6", port: 3910, spec: { cpu: 2, mem_gb: 2, disk_gb: 50 } },
{ code: "YY-SV-001", name: "页页 · 硅谷", ip: "43.153.118.46", key: "zy_gtw_706430821f334302ca50d341ffe3ccc046513bbfe7f56aa3", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 60 } },
];
const tempKeys = {};
const authQueue = [];
const hwCache = {};
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 }); } });
});
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();
});
}
async function fetchHardware(srv) {
const now = Date.now();
const cached = hwCache[srv.code];
if (cached && (now - cached.ts) < 60000) return cached.data;
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;
}
}
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 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();
});
}));
const hwTasks = pingResults.filter(r => r.alive && withHw).map(async (r) => {
const srv = SERVERS.find(s => s.code === r.code);
r.last_seen = new Date().toISOString();
try { r.hardware = await fetchHardware(srv); } catch(e) { r.hardware = null; }
});
await Promise.all(hwTasks);
for (const r of pingResults) {
if (!r.last_seen) r.last_seen = new Date().toISOString();
}
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 });
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 };
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/exec', async (req, res) => {
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 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 }); }
});
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: [] }; }
function savePoolRegistry(r) { r.updated_at = new Date().toISOString(); try { fs.mkdirSync(DATA_DIR, { recursive: true }); } catch(e) {} fs.writeFileSync(POOL_REGISTRY_FILE, JSON.stringify(r, null, 2)); }
function loadJoinTokens() { try { if (fs.existsSync(JOIN_TOKENS_FILE)) return JSON.parse(fs.readFileSync(JOIN_TOKENS_FILE, 'utf-8')); } catch(e) {} return { tokens: [] }; }
function saveJoinTokens(d) { try { fs.mkdirSync(DATA_DIR, { recursive: true }); } catch(e) {} fs.writeFileSync(JOIN_TOKENS_FILE, JSON.stringify(d, null, 2)); }
function sha256(s) { return crypto.createHash('sha256').update(s).digest('hex'); }
app.post('/api/pool/join', (req, res) => {
const { join_token, node_code, node_name, side, team_id, operator_name, hardware, network } = req.body;
if (!join_token || !node_code || !side) return res.status(400).json({ ok: false, error: '缺少参数' });
const jt = loadJoinTokens(); const th = sha256(join_token); const mt = jt.tokens.find(t => t.token_hash === th);
if (!mt) return res.status(401).json({ ok: false, error: '加入令牌无效' });
if (mt.used >= (mt.max_uses || 1)) return res.status(401).json({ ok: false, error: '令牌已用' });
const r = loadPoolRegistry(); if (r.active[node_code]) return res.status(409).json({ ok: false, error: '节点已存在' });
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) => {
const r = loadPoolRegistry();
for (const [c, n] of Object.entries(r.active)) { if (n.registration_id === req.params.rid) return res.json({ ok: true, status: 'approved', node_code: c, pool_token: n.pool_token, report_endpoint: 'https://43.156.237.110:3920/api/pool/report', side: n.side }); }
for (const [c, n] of Object.entries(r.pending)) { if (n.registration_id === req.params.rid) return res.json({ ok: true, status: 'pending', node_code: c }); }
res.status(404).json({ ok: false });
});
app.post('/api/pool/approve', (req, res) => {
const { registration_id, action } = req.body; if (!registration_id || !action) return res.status(400).json({ ok: false });
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); 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); 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 });
});
// ═══════════════════════════════════════
// /api/heartbeat — 各服务器主动推送上报push模式非扫描
// ═══════════════════════════════════════
const pushHwCache = {}; // { code: { data, ts, name } }
app.post('/api/heartbeat', (req, res) => {
const code = req.headers['x-server-code'] || req.body.server_code;
const key = req.headers['x-server-key'] || req.body.server_key || '';
if (!code) return res.status(400).json({ ok: false, error: '缺少 server_code' });
const srv = SERVERS.find(s => s.code === code);
if (!srv) return res.status(404).json({ ok: false, error: '未注册的服务器: ' + code });
// 简单验证key匹配或白名单
const valid = (key === srv.key) || (key === 'push-heartbeat-v1');
if (!valid) return res.status(401).json({ ok: false, error: '密钥不匹配' });
const hw = req.body.hardware || {};
pushHwCache[code] = {
ts: Date.now(),
name: srv.name,
data: {
cpu_cores: hw.cpu_cores || srv.spec.cpu,
cpu_load: hw.cpu_load || hw.load_1min || 0,
mem_total_mb: hw.mem_total_mb || srv.spec.mem_gb * 1024,
mem_used_mb: hw.mem_used_mb || 0,
mem_used_pct: hw.mem_used_pct || 0,
uptime_h: hw.uptime_h || 0,
hostname: hw.hostname || '',
},
alive: true,
last_seen: new Date().toISOString(),
source: 'push',
};
res.json({ ok: true, code, received_at: new Date().toISOString() });
});
// 获取推送心跳汇总
app.get('/api/heartbeat-summary', (req, res) => {
const now = Date.now();
const result = SERVERS.map(s => {
const c = pushHwCache[s.code];
const alive = c && (now - c.ts) < 10 * 60 * 1000; // 10分钟内有过上报
return {
code: s.code,
name: s.name,
spec: s.spec,
alive,
last_seen: c?.last_seen || null,
hardware: c?.data || null,
source: c?.source || 'unknown',
};
});
const aliveCount = result.filter(r => r.alive).length;
res.json({
ok: true,
total: SERVERS.length,
alive: aliveCount,
servers: result,
timestamp: new Date().toISOString(),
note: 'push模式 — 各服务器主动上报,非扫描',
});
});
// ═══════════════════════════════════════
const PORT = process.env.CONSOLE_PORT || 3920;
app.listen(PORT, '127.0.0.1', () => {
console.log('光湖主控台 v3.5 · ' + SERVERS.length + '台(冰6+企3+之2+页1) | ' + PORT + ' | push-heartbeat');
console.log(' 硬件采集: push模式(各服自报) + 旧pull缓存(60s) · 操作日志: 100条上限');
});
setInterval(() => {
const now = Date.now();
for (const [code, entry] of Object.entries(hwCache)) {
if (now - entry.ts > 120000) delete hwCache[code];
}
// 15分钟无上报标记为掉线
for (const [code, entry] of Object.entries(pushHwCache)) {
if (now - entry.ts > 15 * 60 * 1000) delete pushHwCache[code];
}
}, 60000);