107 lines
4.9 KiB
JavaScript
107 lines
4.9 KiB
JavaScript
|
|
const express = require('express');
|
||
|
|
const { exec, execSync } = require('child_process');
|
||
|
|
const util = require('util');
|
||
|
|
const execPromise = util.promisify(exec);
|
||
|
|
const fs = require('fs');
|
||
|
|
const crypto = require('crypto');
|
||
|
|
|
||
|
|
const ADMIN_TOKEN = 'zy-gk-3d89564db9d3d50a';
|
||
|
|
const app = express();
|
||
|
|
app.use(express.json());
|
||
|
|
|
||
|
|
const USERS = {
|
||
|
|
awen: { pass: 'hololake2026', role: 'admin', display: 'Awen', repos: ['personal/hololake-awen','team/hololake-team'] },
|
||
|
|
hauer: { pass: 'hololake2026', role: 'member', display: 'Hauer', repos: ['team/hauer'] },
|
||
|
|
juzi: { pass: 'hololake2026', role: 'member', display: 'Juzi', repos: ['team/juzi'] },
|
||
|
|
yeye: { pass: 'hololake2026', role: 'member', display: 'Yeye', repos: ['team/yeye'] },
|
||
|
|
feimao: { pass: 'hololake2026', role: 'member', display: 'Feimao', repos: ['team/feimao'] }
|
||
|
|
};
|
||
|
|
const SESSIONS = {};
|
||
|
|
|
||
|
|
app.post('/api/login', (req, res) => {
|
||
|
|
const { username, password } = req.body;
|
||
|
|
const user = USERS[username];
|
||
|
|
if (!user || user.pass !== password) return res.status(401).json({ error: 'error' });
|
||
|
|
const token = crypto.randomBytes(16).toString('hex');
|
||
|
|
SESSIONS[token] = { username, ...user };
|
||
|
|
res.json({ token, user: { username, display: user.display, role: user.role, repos: user.repos } });
|
||
|
|
});
|
||
|
|
|
||
|
|
app.get('/api/session', (req, res) => {
|
||
|
|
const s = SESSIONS[req.headers['x-session-token']];
|
||
|
|
if (!s) return res.status(401).json({ error: 'unauth' });
|
||
|
|
res.json({ user: { username: s.username, display: s.display, role: s.role, repos: s.repos } });
|
||
|
|
});
|
||
|
|
|
||
|
|
app.get('/api/status', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { stdout: cr } = await execPromise("top -bn1 | grep Cpu | awk '{print $2}' | cut -d% -f1");
|
||
|
|
const cpu = parseFloat(cr.trim()) || 0;
|
||
|
|
const { stdout: mr } = await execPromise("free -m | grep Mem | awk '{print $3,$2,$7}'");
|
||
|
|
const [used, total, avail] = mr.trim().split(' ').map(Number);
|
||
|
|
const { stdout: dr } = await execPromise("df -h / | tail -1 | awk '{print $5,$2,$4}'");
|
||
|
|
const [du, dt, df] = dr.trim().split(' ');
|
||
|
|
res.json({ timestamp: new Date().toISOString(),
|
||
|
|
cpu: { usage: cpu.toFixed(1), status: cpu>80?'warn':cpu>50?'normal':'good' },
|
||
|
|
memory: { used, total, available: avail, percent: ((used/total)*100).toFixed(1), status: (used/total)>0.8?'warn':(used/total)>0.5?'normal':'good' },
|
||
|
|
disk: { used: du, total: dt, free: df, status: parseInt(du)>80?'warn':'good' } });
|
||
|
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
||
|
|
});
|
||
|
|
|
||
|
|
app.get('/api/processes', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { stdout } = await execPromise('ps aux --sort=-%mem | head -20');
|
||
|
|
const procs = stdout.trim().split('\n').slice(1).map(line => {
|
||
|
|
const p = line.trim().split(/\s+/);
|
||
|
|
return { name: p[10], pid: p[1], mem: p[3], cpu: p[2] };
|
||
|
|
});
|
||
|
|
res.json({ procs });
|
||
|
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
||
|
|
});
|
||
|
|
|
||
|
|
app.get('/api/services', async (req, res) => {
|
||
|
|
const svc = [];
|
||
|
|
try { execSync('ss -tlnp | grep :80', {timeout:2000}); svc.push({name:'Nginx',status:'running'}); } catch(e) { svc.push({name:'Nginx',status:'stopped'}); }
|
||
|
|
try { execSync('ss -tlnp | grep :3000', {timeout:2000}); svc.push({name:'Dify AI',status:'running'}); } catch(e) { svc.push({name:'Dify AI',status:'stopped'}); }
|
||
|
|
try { const p = JSON.parse(execSync('pm2 jlist 2>/dev/null', {timeout:2000}).toString()); p.forEach(x => svc.push({name:'PM2:'+x.name, status: x.pm2_env.status})); } catch(e) {}
|
||
|
|
res.json({ services: svc });
|
||
|
|
});
|
||
|
|
|
||
|
|
app.get('/api/repo-activity', async (req, res) => {
|
||
|
|
const acts = [];
|
||
|
|
for (const dir of ['personal','team']) {
|
||
|
|
const fd = '/opt/zhuyuan/repos/' + dir;
|
||
|
|
if (!fs.existsSync(fd)) continue;
|
||
|
|
for (const repo of fs.readdirSync(fd)) {
|
||
|
|
const rp = fd + '/' + repo;
|
||
|
|
if (!fs.statSync(rp).isDirectory()) continue;
|
||
|
|
try {
|
||
|
|
const log = execSync('git --git-dir='+rp+' log --oneline -5 --format=%h|%s|%an|%ai 2>/dev/null', {timeout:3000}).toString().trim();
|
||
|
|
if (log) log.split('\n').forEach(l => { const [h,m,a,d] = l.split('|'); acts.push({repo:dir+'/'+repo,hash:h,msg:m,author:a,date:d}); });
|
||
|
|
} catch(e) {}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
acts.sort((a,b) => new Date(b.date) - new Date(a.date));
|
||
|
|
res.json({ activities: acts.slice(0,20) });
|
||
|
|
});
|
||
|
|
|
||
|
|
app.get('/api/heartbeat', (req, res) => {
|
||
|
|
res.json({
|
||
|
|
servers: [{ id:'HL-SG-001', name:'硅谷', ip:'170.106.72.246', status:'online' },{ id:'HL-CN-001', name:'国内', ip:'43.139.207.172', status:'offline' }],
|
||
|
|
gatekeeper: { status:'disconnected' }
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
app.use(express.static('/opt/zhuyuan/console/public'));
|
||
|
|
|
||
|
|
app.post('/admin/exec', (req, res) => {
|
||
|
|
if (req.headers['x-admin-token'] !== ADMIN_TOKEN) return res.status(403).json({ error: 'unauthorized' });
|
||
|
|
const cmd = req.body.command;
|
||
|
|
if (!cmd || cmd.length > 5000) return res.status(400).json({ error: 'bad command' });
|
||
|
|
exec(cmd, { timeout: 60000, maxBuffer: 1048576 }, (e, stdout, stderr) => {
|
||
|
|
res.json({ output: stdout + (stderr || ''), error: e ? e.message : null });
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
app.listen(3920, '127.0.0.1', () => console.log('OK:3920'));
|