const express = require('express'); const http = require('http'); const WebSocket = require('ws'); const Database = require('better-sqlite3'); const path = require('path'); const app = express(); const server = http.createServer(app); const wss = new WebSocket.Server({ server }); const db = new Database('status.db'); // 初始化数据库 db.exec(` CREATE TABLE IF NOT EXISTS nodes ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL, secret TEXT NOT NULL, last_seen INTEGER, created_at INTEGER DEFAULT (strftime('%s', 'now')) ); CREATE TABLE IF NOT EXISTS stats ( id INTEGER PRIMARY KEY AUTOINCREMENT, node_id INTEGER, cpu REAL, mem_used INTEGER, mem_total INTEGER, disk_used INTEGER, disk_total INTEGER, net_in INTEGER, net_out INTEGER, uptime INTEGER, load1 REAL, load5 REAL, load15 REAL, ts INTEGER DEFAULT (strftime('%s', 'now')), FOREIGN KEY (node_id) REFERENCES nodes(id) ); `); // 生成密钥 function genSecret() { return Math.random().toString(36).substr(2, 16); } // API: 获取所有节点状态 app.get('/api/nodes', (req, res) => { const nodes = db.prepare(` SELECT n.*, s.* FROM nodes n LEFT JOIN stats s ON n.id = s.node_id AND s.ts = (SELECT MAX(ts) FROM stats WHERE node_id = n.id) `).all(); res.json(nodes); }); // API: 添加节点 app.post('/api/nodes', express.json(), (req, res) => { const { name } = req.body; if (!name) return res.status(400).json({ error: 'name required' }); const secret = genSecret(); try { const result = db.prepare('INSERT INTO nodes (name, secret) VALUES (?, ?)').run(name, secret); res.json({ id: result.lastInsertRowid, name, secret }); } catch (e) { res.status(400).json({ error: 'name exists' }); } }); // API: 删除节点 app.delete('/api/nodes/:id', (req, res) => { db.prepare('DELETE FROM nodes WHERE id = ?').run(req.params.id); db.prepare('DELETE FROM stats WHERE node_id = ?').run(req.params.id); res.json({ ok: true }); }); // Agent 上报接口 app.post('/report', express.json(), (req, res) => { const { secret, cpu, mem_used, mem_total, disk_used, disk_total, net_in, net_out, uptime, load1, load5, load15 } = req.body; const node = db.prepare('SELECT * FROM nodes WHERE secret = ?').get(secret); if (!node) return res.status(403).json({ error: 'invalid secret' }); const now = Math.floor(Date.now() / 1000); db.prepare('UPDATE nodes SET last_seen = ? WHERE id = ?').run(now, node.id); db.prepare(` INSERT INTO stats (node_id, cpu, mem_used, mem_total, disk_used, disk_total, net_in, net_out, uptime, load1, load5, load15) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run(node.id, cpu, mem_used, mem_total, disk_used, disk_total, net_in, net_out, uptime, load1, load5, load15); // 清理7天前的数据 db.prepare('DELETE FROM stats WHERE ts < ?').run(now - 86400 * 7); // WebSocket 广播 broadcast({ type: 'update', node_id: node.id, data: req.body }); res.json({ ok: true }); }); // WebSocket 广播 function broadcast(data) { const msg = JSON.stringify(data); wss.clients.forEach(client => { if (client.readyState === WebSocket.OPEN) { client.send(msg); } }); } // Agent 脚本下载 app.get('/agent.sh', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'agent.sh')); }); // 静态文件 app.use(express.static(path.join(__dirname, 'public'))); // 首页 app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); const PORT = process.env.PORT || 3800; server.listen(PORT, () => { console.log(`Status panel running on http://0.0.0.0:${PORT}`); });