From 80bfaca576a6e371edb3e23f90e376e1e99c041d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=93=B8=E6=B8=8A=20ICE-GL-ZY001?= Date: Tue, 26 May 2026 02:40:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20push-heartbeat=20=E6=8E=A8=E9=80=81?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=20=E2=80=94=20=E5=90=84=E6=9C=8D=E4=B8=BB?= =?UTF-8?q?=E5=8A=A8=E4=B8=8A=E6=8A=A5=E9=9D=9E=E6=89=AB=E6=8F=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 push-heartbeat.js: 每台服务器部署,采集本机数据POST到主控台 - server.js v3.5: 新增 /api/heartbeat 接收端点 + /api/heartbeat-summary - 架构改进: 冰朔指出扫描模式像攻击 → 改为push模式 --- _deploy/console-server/push-heartbeat.js | 116 +++++++++++++++++++++++ _deploy/console-server/server.js | 75 ++++++++++++++- 2 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 _deploy/console-server/push-heartbeat.js diff --git a/_deploy/console-server/push-heartbeat.js b/_deploy/console-server/push-heartbeat.js new file mode 100644 index 0000000..224b083 --- /dev/null +++ b/_deploy/console-server/push-heartbeat.js @@ -0,0 +1,116 @@ +// 光湖 push-heartbeat v1.0 — 每台服务器主动上报本机状态 +// 部署: 各服务器 PM2 cron模式,每5分钟运行一次 +// 原理: 不扫描别人,只报自己。主控台(BS-GZ-006:3920)集中收。 +// 冰朔: "不要扫描,让每个服务器自己主动往外发" + +'use strict'; + +const http = require('http'); +const os = require('os'); +const fs = require('fs'); + +// ═══════════════════════════════════════ +// 配置 — 部署时修改 +// ═══════════════════════════════════════ + +const MASTER_HOST = process.env.MASTER_HOST || '43.139.217.141'; +const MASTER_PORT = parseInt(process.env.MASTER_PORT || '3920'); +const SERVER_CODE = process.env.SERVER_CODE || 'BS-UNKNOWN'; +const SERVER_NAME = process.env.SERVER_NAME || '未命名'; +const SERVER_KEY = process.env.SERVER_KEY || ''; +const INTERVAL_MS = 5 * 60 * 1000; // 5分钟 + +// ═══════════════════════════════════════ +// 硬件采集(本机,不连外网) +// ═══════════════════════════════════════ + +function collectHardware() { + const cpuCores = os.cpus().length; + const loadAvg = os.loadavg(); + const totalMem = os.totalmem(); + const freeMem = os.freemem(); + const usedMem = totalMem - freeMem; + const uptime = os.uptime(); + + return { + cpu_cores: cpuCores, + cpu_load: parseFloat(loadAvg[0].toFixed(2)), + cpu_load5: parseFloat(loadAvg[1].toFixed(2)), + cpu_load15: parseFloat(loadAvg[2].toFixed(2)), + mem_total_mb: Math.floor(totalMem / 1024 / 1024), + mem_used_mb: Math.floor(usedMem / 1024 / 1024), + mem_avail_mb: Math.floor(freeMem / 1024 / 1024), + mem_used_pct: parseFloat((usedMem / totalMem * 100).toFixed(1)), + uptime_h: Math.floor(uptime / 3600), + hostname: os.hostname(), + }; +} + +// ═══════════════════════════════════════ +// 上报到主控台(只发POST,不扫描) +// ═══════════════════════════════════════ + +function reportToMaster(data) { + return new Promise((resolve) => { + const payload = JSON.stringify(data); + const opts = { + hostname: MASTER_HOST, + port: MASTER_PORT, + path: '/api/heartbeat', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(payload), + 'X-Server-Code': SERVER_CODE, + 'X-Server-Key': SERVER_KEY, + }, + timeout: 8000, + }; + + const req = http.request(opts, (res) => { + let body = ''; + res.on('data', (c) => (body += c)); + res.on('end', () => resolve({ ok: res.statusCode === 200, 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(payload); + req.end(); + }); +} + +// ═══════════════════════════════════════ +// 主逻辑 +// ═══════════════════════════════════════ + +async function push() { + const hw = collectHardware(); + const data = { + server_code: SERVER_CODE, + server_name: SERVER_NAME, + timestamp: new Date().toISOString(), + hardware: hw, + }; + + console.log(`[${data.timestamp}] ${SERVER_CODE} → ${MASTER_HOST}:${MASTER_PORT}`); + + const result = await reportToMaster(data); + if (result.ok) { + console.log(` ✅ 上报成功 · CPU ${hw.cpu_load} · 内存 ${hw.mem_used_pct}% · 运行 ${hw.uptime_h}h`); + } else { + console.log(` ❌ 上报失败: ${result.error || result.status}`); + } +} + +// 立即发一次 +push(); + +// 定时发 +if (process.argv.includes('--once')) { + // 一次性模式 — PM2 cron 使用 +} else { + setInterval(push, INTERVAL_MS); +} diff --git a/_deploy/console-server/server.js b/_deploy/console-server/server.js index 0659857..bd02da2 100644 --- a/_deploy/console-server/server.js +++ b/_deploy/console-server/server.js @@ -262,10 +262,77 @@ app.post('/api/pool/report', (req, res) => { 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.4 · ' + SERVERS.length + '台(冰6+企3+之2+页1) | ' + PORT); - console.log(' 硬件采集: 并行+60s缓存 · 操作日志: 100条上限'); + console.log('光湖主控台 v3.5 · ' + SERVERS.length + '台(冰6+企3+之2+页1) | ' + PORT + ' | push-heartbeat'); + console.log(' 硬件采集: push模式(各服自报) + 旧pull缓存(60s) · 操作日志: 100条上限'); }); setInterval(() => { @@ -273,4 +340,8 @@ setInterval(() => { 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); \ No newline at end of file