import { Injectable, Logger } from '@nestjs/common'; import * as os from 'os'; import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); export interface DiskInfo { mount: string; total: string; used: string; free: string; percent: number } export interface ProcessInfo { pid: number; cpu: string; mem: string; name: string; desc: string; command: string } export interface ServerInfo { name: string; role: string; hostname: string; cpu: { model: string; cores: number; usagePercent: number }; memory: { total: string; used: string; free: string; percent: number }; disks: DiskInfo[]; uptime: string; processes: ProcessInfo[]; network: { publicIp: string; privateIp: string; domains: string[] }; } const SSH_KEY_PATH = process.env.SSH_KEY_PATH || '/home/ubuntu/.ssh/wangdl.pem'; const REMOTE_HOST = '10.2.0.7'; const ALIASES: Record = { 'mysqld': { name: 'MySQL 8.0', desc: '业务数据库' }, 'redis-server': { name: 'Redis 7', desc: '缓存/队列' }, 'qdrant': { name: 'Qdrant', desc: '向量索引库' }, 'nginx': { name: 'Nginx', desc: '反向代理' }, 'gitea': { name: 'Gitea', desc: '代码仓库' }, 'dist/src/main.js': { name: 'NestJS API', desc: '知习后端' }, 'dist/main.js': { name: 'NestJS API', desc: '知习后端' }, 'act_runner': { name: 'Gitea Runner', desc: 'CI/CD' }, 'hermes': { name: 'Hermes Agent', desc: 'AI Agent' }, 'dockerd': { name: 'Docker', desc: '容器引擎' }, 'containerd': { name: 'containerd', desc: '容器管理' }, 'systemd-journald': { name: '日志服务', desc: '系统日志' }, 'snapd': { name: 'Snapd', desc: '包管理' }, 'multipathd': { name: '存储多路径', desc: '磁盘管理' }, 'YDEyes': { name: '腾讯云监控', desc: '安全监控' }, 'barad_agent': { name: '云监控上报', desc: '指标采集' }, }; function friendly(cmd: string): { name: string; desc: string } { for (const [p, info] of Object.entries(ALIASES)) if (cmd.includes(p)) return info; const s = cmd.split('/').pop()?.slice(0, 20) || cmd.slice(0, 18); return { name: s, desc: '' }; } function parsePsLine(line: string): ProcessInfo { const p = line.trim().split(/\s+/); const pid = parseInt(p[1]) || 0; // col 1 = PID const cpu = (p[2] || '0') + '%'; const mem = (p[3] || '0') + '%'; const cmd = p.slice(10).join(' '); const info = friendly(cmd); return { pid, cpu, mem, name: info.name, desc: info.desc, command: cmd.slice(0, 80) }; } function chineseUptime(sec: number): string { const d = Math.floor(sec / 86400), h = Math.floor((sec % 86400) / 3600), m = Math.floor((sec % 3600) / 60); return `${d}天${h}时${m}分`; } @Injectable() export class AdminServersService { private readonly logger = new Logger(AdminServersService.name); async getLocalMetrics(): Promise { const cpus = os.cpus(); const total = os.totalmem(), free = os.freemem(), used = total - free; const cpuPct = Math.min(100, Math.round((os.loadavg()[0] / cpus.length) * 100)); const disks: DiskInfo[] = []; for (const m of ['/', '/data']) { try { const { stdout } = await execAsync(`df -h ${m} | tail -1 | awk '{printf "%s,%s,%s,%d",$2,$3,$4,int($5)}'`); const parts = stdout.trim().split(','); if (parts.length >= 3) disks.push({ mount: m, total: parts[0], used: parts[1], free: parts[2], percent: parseInt(parts[3]) || 0 }); else disks.push({ mount: m, total: '-', used: '-', free: '-', percent: 0 }); } catch { disks.push({ mount: m, total: '-', used: '-', free: '-', percent: 0 }) } } let procs: ProcessInfo[] = []; try { const { stdout } = await execAsync("ps auxww --sort=-%mem --no-headers | head -10"); procs = stdout.trim().split('\n').filter(l => l).map(parsePsLine); } catch {} const nets = os.networkInterfaces(); const privIp = Object.values(nets).flat().find(n => n?.family === 'IPv4' && !n.internal)?.address || '172.21.0.4'; return { name: '蜂驰云 8核32G', role: '生产核心', hostname: os.hostname(), cpu: { model: cpus[0]?.model || '', cores: cpus.length, usagePercent: cpuPct }, memory: { total: (total / 1e9).toFixed(1) + 'G', used: (used / 1e9).toFixed(1) + 'G', free: (free / 1e9).toFixed(1) + 'G', percent: Math.round((used / total) * 100) }, disks, uptime: chineseUptime(os.uptime()), processes: procs, network: { publicIp: '120.53.227.155', privateIp: privIp, domains: ['api.longde.cloud', 'admin.longde.cloud'] }, }; } async getRemoteMetrics(): Promise { const run = (cmd: string) => execAsync(`ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -i ${SSH_KEY_PATH} ubuntu@${REMOTE_HOST} '${cmd.replace(/'/g, "'\\''")}'`, { timeout: 8000 }) .then(r => r.stdout.trim()).catch(() => ''); try { const [hostname, privIp, load, cores, memStr, diskRoot, diskData, uptimeRaw, procRaw] = await Promise.all([ run('hostname'), run("hostname -I | awk '{print $1}'"), run("cat /proc/loadavg | awk '{print $1}'"), run('cat /proc/cpuinfo | grep processor | wc -l'), run("free -m | awk '/Mem/{printf \"%.1fG,%.1fG,%.1fG,%d\",$2/1024,$3/1024,$4/1024,int($3/$2*100)}'"), run("df -h / | awk 'NR==2{printf \"%s,%s,%s,%d\",$2,$3,$4,int($5)}'"), run("df -h /data 2>/dev/null | awk 'NR==2{printf \"%s,%s,%s,%d\",$2,$3,$4,int($5)}'"), run("uptime -p | sed 's/up //'"), run("ps auxww --sort=-%mem --no-headers 2>/dev/null | head -8"), ]); const load1 = parseFloat(load) || 0; const cpuCores = parseInt(cores) || 4; const cpuPct = Math.min(100, Math.round((load1 / Math.max(cpuCores, 1)) * 100)); const memParts = memStr.split(','); const memPct = parseInt(memParts[3]) || 0; const drParts = diskRoot.split(','); const ddParts = diskData.split(','); const disks: DiskInfo[] = [{ mount: '/', total: drParts[0] || '-', used: drParts[1] || '-', free: drParts[2] || '-', percent: parseInt(drParts[3]) || 0 }]; if (ddParts.length >= 3) disks.push({ mount: '/data', total: ddParts[0], used: ddParts[1], free: ddParts[2], percent: parseInt(ddParts[3]) || 0 }); const procs: ProcessInfo[] = procRaw.split('\n').filter(l => /^\S+\s+\d+\s/.test(l)).map(parsePsLine); let up = uptimeRaw || ''; up = up.replace(/(\d+)\s+weeks?,?\s*/g, '$1周').replace(/(\d+)\s+days?,?\s*/g, '$1天').replace(/(\d+)\s+hours?,?\s*/g, '$1时').replace(/(\d+)\s+minutes?/g, '$1分'); return { name: '轻量云 4核4G', role: '工具/辅助', hostname: hostname || 'remote', cpu: { model: 'Intel Xeon (Lighthouse)', cores: cpuCores, usagePercent: cpuPct }, memory: { total: memParts[0] || '-', used: memParts[1] || '-', free: memParts[2] || '-', percent: memPct }, disks, uptime: up || '-', processes: procs, network: { publicIp: '81.70.187.179', privateIp: privIp || '10.2.0.7', domains: ['longde.cloud', 'git.longde.cloud'] }, }; } catch (err: any) { this.logger.warn('Remote metrics failed: ' + err.message); return null; } } async getAllMetrics() { const [local, remote] = await Promise.all([this.getLocalMetrics(), this.getRemoteMetrics()]); return { servers: [local, ...(remote ? [remote] : [])] }; } }