// komari 服务器监控拉取: 公开 API (无需 key) // GET /api/nodes 节点列表 // GET /api/recent/{uuid} 最近实时状态 (数组, 多个时间点) // GET /api/records/load?uuid=... 1 分钟粒度历史 (保留 24h) const mysql = require('mysql2/promise') const BASE = process.env.KOMARI_BASE || 'https://komari.honor3.com' const TIMEOUT = 20000 function parseUrl(url) { const u = new URL(url) return { host: u.hostname, port: Number(u.port) || 3306, user: decodeURIComponent(u.username), password: decodeURIComponent(u.password), database: u.pathname.replace(/^\//, '') } } async function jget(path) { const ctrl = new AbortController() setTimeout(() => ctrl.abort(), TIMEOUT) const r = await fetch(BASE + path, { headers: { 'User-Agent': 'iboard-monitor/1.0' }, signal: ctrl.signal }) if (!r.ok) throw new Error('komari ' + path + ' HTTP ' + r.status) const j = await r.json() if (j.status && j.status !== 'success') throw new Error('komari ' + path + ' status: ' + j.status + ' ' + (j.message || '')) return j.data } // 拉一次所有节点 + 最近状态 + 1 分钟历史, 入库 async function runOnce() { const conn = await mysql.createConnection(parseUrl(process.env.DATABASE_URL)) const stats = { nodes: 0, snapshots: 0, history: 0, failed: 0 } try { const nodes = await jget('/api/nodes') if (!Array.isArray(nodes)) throw new Error('nodes 格式异常') stats.nodes = nodes.length for (const n of nodes) { const uuid = n.uuid if (!uuid) continue try { const recent = await jget('/api/recent/' + uuid) let snap = null if (Array.isArray(recent) && recent.length) snap = recent[recent.length - 1] const recordedAt = snap ? new Date(snap.updated_at) : new Date() const ramTotal = snap && snap.ram ? Number(snap.ram.total) : Number(n.mem_total || 0) const diskTotal = snap && snap.disk ? Number(snap.disk.total) : Number(n.disk_total || 0) await conn.execute( 'INSERT INTO `ServerNode` (`uuid`,`name`,`cpuName`,`os`,`region`,`memTotal`,`diskTotal`,`cpuCores`,' + '`cpuUsage`,`ramUsed`,`swapUsed`,`load1`,`load5`,`load15`,`diskUsed`,`netIn`,`netOut`,`uptime`,`process`,`connections`,`status`,`recordedAt`,`createdAt`,`updatedAt`) ' + 'VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,NOW(3),NOW(3)) ' + 'ON DUPLICATE KEY UPDATE `name`=VALUES(`name`),`cpuName`=VALUES(`cpuName`),`os`=VALUES(`os`),`region`=VALUES(`region`),' + '`memTotal`=VALUES(`memTotal`),`diskTotal`=VALUES(`diskTotal`),`cpuCores`=VALUES(`cpuCores`),' + '`cpuUsage`=VALUES(`cpuUsage`),`ramUsed`=VALUES(`ramUsed`),`swapUsed`=VALUES(`swapUsed`),' + '`load1`=VALUES(`load1`),`load5`=VALUES(`load5`),`load15`=VALUES(`load15`),`diskUsed`=VALUES(`diskUsed`),' + '`netIn`=VALUES(`netIn`),`netOut`=VALUES(`netOut`),`uptime`=VALUES(`uptime`),' + '`process`=VALUES(`process`),`connections`=VALUES(`connections`),`status`=VALUES(`status`),' + '`recordedAt`=VALUES(`recordedAt`),`updatedAt`=NOW(3)', [ uuid, n.name || '', n.cpu_name || '', n.os || '', n.region || '', ramTotal, diskTotal, Number(n.cpu_cores || 0), snap ? Number(snap.cpu?.usage || 0) : 0, snap ? Number(snap.ram?.used || 0) : 0, snap ? Number(snap.swap?.used || 0) : 0, snap ? Number(snap.load?.load1 || 0) : 0, snap ? Number(snap.load?.load5 || 0) : 0, snap ? Number(snap.load?.load15 || 0) : 0, snap ? Number(snap.disk?.used || 0) : 0, snap ? Number(snap.network?.down || 0) : 0, snap ? Number(snap.network?.up || 0) : 0, snap ? Number(snap.uptime || 0) : 0, snap ? Number(snap.process || 0) : 0, snap ? Number(snap.connections?.tcp || 0) : 0, snap ? 'online' : 'unknown', recordedAt, ], ) stats.snapshots++ let rec = null try { rec = await jget('/api/records/load?uuid=' + uuid) } catch (e) { /* 节点未开启 record */ } if (rec && Array.isArray(rec.records)) { for (const r of rec.records) { const period = new Date(r.time) if (isNaN(period.getTime())) continue const [res] = await conn.execute( 'INSERT INTO `ServerNodeHistory` (`uuid`,`period`,`cpu`,`ram`,`load`,`disk`,`netIn`,`netOut`,`process`,`connections`,`createdAt`) ' + 'VALUES (?,?,?,?,?,?,?,?,?,?,NOW(3)) ' + 'ON DUPLICATE KEY UPDATE `cpu`=VALUES(`cpu`),`ram`=VALUES(`ram`),`load`=VALUES(`load`),`disk`=VALUES(`disk`),' + '`netIn`=VALUES(`netIn`),`netOut`=VALUES(`netOut`),`process`=VALUES(`process`),`connections`=VALUES(`connections`)', [ uuid, period, Number(r.cpu || 0), Number(r.ram || 0), Number(r.load || 0), Number(r.disk || 0), Number(r.net_in || 0), Number(r.net_out || 0), Number(r.process || 0), Number(r.connections || 0), ], ) if (res.affectedRows === 1) stats.history++ } } } catch (e) { stats.failed++ console.error('[komari] 节点 ' + uuid + ' 失败:', e.message) } } } finally { await conn.end() } return stats } // 清理 24h 之前的历史 async function cleanupOld() { const conn = await mysql.createConnection(parseUrl(process.env.DATABASE_URL)) try { const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000) const [r] = await conn.execute('DELETE FROM `ServerNodeHistory` WHERE `period` < ?', [cutoff]) return r.affectedRows } finally { await conn.end() } } module.exports = { runOnce, cleanupOld, BASE }