Files
vps-management-bot/projects/status-panel/server.js

130 lines
3.6 KiB
JavaScript
Raw Normal View History

2026-03-21 01:10:53 +08:00
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}`);
});