// 光湖心跳Agent v1.0 — 采集各节点硬件数据并上报至Console Server // 部署: GZ-006 PM2 cron模式 // 用法: node heartbeat-agent.js 或 pm2 start heartbeat-agent.js const http = require('http'); const https = require('https'); const fs = require('fs'); const CONSOLE_HOST = '127.0.0.1'; const CONSOLE_PORT = 3920; const REPORT_INTERVAL = 300000; // 5分钟 — 2026-05-26修改: 30秒被腾讯云误判为对外攻击 // 全服务器配置 — 和 console-server 保持一致 const SERVERS = [ { code: "BS-GZ-006", name: "广州 · 代码仓库", ip: "43.139.217.141", key: "GATEKEEPER_TOKEN_REDACTED", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 60 } }, { code: "BS-SG-001", name: "新加坡 · 铸渊大脑", ip: "43.156.237.110", key: "GATEKEEPER_TOKEN_REDACTED", port: 3911, spec: { cpu: 4, mem_gb: 8, disk_gb: 80 } }, { code: "BS-SG-002", name: "新加坡 · 面孔", ip: "43.134.16.246", key: "GATEKEEPER_TOKEN_REDACTED", port: 3910, spec: { cpu: 2, mem_gb: 2, disk_gb: 30 } }, { code: "BS-SG-003", name: "新加坡 · 中继", ip: "43.153.193.169", key: "GATEKEEPER_TOKEN_REDACTED", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 80 } }, { code: "ZY-SG-006", name: "新加坡 · 语料", ip: "43.153.203.105", key: "GATEKEEPER_TOKEN_REDACTED", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 50 } }, { code: "BS-SH-005", name: "上海 · 国内节点", ip: "124.223.10.33", key: "GATEKEEPER_TOKEN_REDACTED", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 30 } }, { code: "AW-GZ-001", name: "企业主服务器 · 广州", ip: "43.139.251.175", key: "GATEKEEPER_TOKEN_REDACTED", port: 3910, spec: { cpu: 4, mem_gb: 16, disk_gb: 80 } }, { code: "AW-SH-002", name: "企业灾备 · 上海", ip: "124.222.54.198", key: "GATEKEEPER_TOKEN_REDACTED", port: 3910, spec: { cpu: 4, mem_gb: 4, disk_gb: 40 } }, { code: "AW-GZ-003", name: "企业灾备 · 广州", ip: "119.29.181.132", key: "GATEKEEPER_TOKEN_REDACTED", port: 3910, spec: { cpu: 2, mem_gb: 8, disk_gb: 50 } }, { code: "ZZ-SV-001", name: "之之 · 硅谷", ip: "43.173.121.48", key: "GATEKEEPER_TOKEN_REDACTED", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 59 } }, { code: "ZZ-GZ-001", name: "之之 · 广州", ip: "193.112.126.174", key: "GATEKEEPER_TOKEN_REDACTED", port: 3910, spec: { cpu: 2, mem_gb: 2, disk_gb: 50 } }, { code: "YY-SV-001", name: "页页 · 硅谷", ip: "43.153.118.46", key: "GATEKEEPER_TOKEN_REDACTED", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 60 } }, ]; // 心跳数据存储 const HB_FILE = '/opt/zhuyuan/data/heartbeat-data.json'; function loadHB() { try { if (fs.existsSync(HB_FILE)) return JSON.parse(fs.readFileSync(HB_FILE, 'utf-8')); } catch(e) {} return { nodes: {}, pool: {}, updated_at: null }; } function saveHB(data) { try { fs.mkdirSync('/opt/zhuyuan/data', { recursive: true }); fs.writeFileSync(HB_FILE, JSON.stringify(data, null, 2)); } catch(e) { console.error('保存心跳数据失败:', e.message); } } // 通过Gatekeeper执行命令 function gatekeeperExec(srv, cmd, timeoutMs) { timeoutMs = timeoutMs || 8000; 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 }); } }); }); req.on('error', () => resolve({ ok: false })); req.on('timeout', () => { req.destroy(); resolve({ ok: false }); }); req.write(d); req.end(); }); } // 健康检查 function healthCheck(srv) { 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 }; return new Promise((resolve) => { const start = Date.now(); const req = http.request(opts, (res) => { let b = ''; res.on('data', c => b += c); res.on('end', () => resolve({ ok: res.statusCode === 200, latency: Date.now() - start })); }); req.on('error', () => resolve({ ok: false, latency: 0 })); req.on('timeout', () => { req.destroy(); resolve({ ok: false, latency: 0 }); }); req.write(data); req.end(); }); } // 采集硬件数据 async function collectHardware(srv) { 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) return null; const stdout = r.body.stdout; const m = (re) => { const match = stdout.match(re); return match ? match[1] : null; }; const cpu_cores = parseInt(m(/CPU_CORES:(\d+)/)) || srv.spec.cpu; const uptime_h = Math.floor((parseInt(m(/UPTIME_S:(\d+)/)) || 0) / 3600); const mem_total_mb = parseInt(m(/MEM_TOTAL:(\d+)/)) || srv.spec.mem_gb * 1024; const mem_used_mb = parseInt(m(/MEM_USED:(\d+)/)) || 0; const mem_avail_mb = parseInt(m(/MEM_AVAIL:(\d+)/)) || mem_total_mb; const cpu_load = parseFloat(m(/LOAD1:([\d.]+)/)) || 0; const disk_used_pct = parseInt(m(/DISK_PCT:(\d+)/)) || 0; return { cpu_cores, uptime_h, mem_total_mb, mem_used_mb, mem_avail_mb, cpu_load, disk_used_pct, spec: srv.spec }; } // 主循环 async function heartbeat() { const hb = loadHB(); console.log('[' + new Date().toISOString() + '] 心跳采集开始...'); // 并行健康检查 const healthResults = await Promise.all(SERVERS.map(async (srv) => { const health = await healthCheck(srv); return { ...srv, alive: health.ok, latency: health.latency }; })); // 对在线节点采集硬件 const hwResults = await Promise.all(healthResults.map(async (srv) => { let hw = null; if (srv.alive) { hw = await collectHardware(srv); } return { ...srv, hardware: hw }; })); // 汇总数据 let totalCpu = 0, usedCpu = 0, totalMem = 0, usedMem = 0, onlineNodes = 0; for (const srv of hwResults) { const nodeData = { name: srv.name, online: srv.alive, latency: srv.latency, last_seen: new Date().toISOString(), spec: srv.spec }; if (srv.alive && srv.hardware) { nodeData.hardware = srv.hardware; nodeData.cpu_load = srv.hardware.cpu_load; nodeData.mem_total_mb = srv.hardware.mem_total_mb; nodeData.mem_avail_mb = srv.hardware.mem_avail_mb; nodeData.disk_used_pct = srv.hardware.disk_used_pct; nodeData.uptime_hours = srv.hardware.uptime_h; nodeData.tasks = 0; totalCpu += srv.spec.cpu; usedCpu += srv.hardware.cpu_load; totalMem += srv.hardware.mem_total_mb; usedMem += srv.hardware.mem_used_mb; onlineNodes++; } else if (srv.alive) { totalCpu += srv.spec.cpu; totalMem += srv.spec.mem_gb * 1024; onlineNodes++; } hb.nodes[srv.code] = nodeData; } // 池数据 hb.pool = { total_nodes: SERVERS.length, online_nodes: onlineNodes, cpu_total: totalCpu, cpu_used: parseFloat(usedCpu.toFixed(1)), cpu_available: parseFloat((totalCpu - usedCpu).toFixed(1)), mem_total_gb: parseFloat((totalMem / 1024).toFixed(1)), mem_used_gb: parseFloat((usedMem / 1024).toFixed(1)), mem_available_gb: parseFloat(((totalMem - usedMem) / 1024).toFixed(1)), active_tasks: 0 }; hb.total_spec = { cpu: SERVERS.reduce((s, n) => s + n.spec.cpu, 0), mem_gb: SERVERS.reduce((s, n) => s + n.spec.mem_gb, 0), disk_gb: SERVERS.reduce((s, n) => s + n.spec.disk_gb, 0) }; hb.updated_at = new Date().toISOString(); saveHB(hb); console.log(' 在线: ' + onlineNodes + '/' + SERVERS.length + ' | CPU: ' + hb.pool.cpu_used + '/' + hb.pool.cpu_total + '核' + ' | 内存: ' + hb.pool.mem_used_gb + '/' + hb.pool.mem_total_gb + 'GB'); } // 启动 console.log('光湖心跳Agent v1.0 启动'); console.log(' 节点数: ' + SERVERS.length); console.log(' 采集间隔: ' + (REPORT_INTERVAL / 1000) + '秒'); console.log(' 数据文件: ' + HB_FILE); // 立即执行一次 heartbeat(); // 定时执行 setInterval(heartbeat, REPORT_INTERVAL);