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; 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'; // Map raw process commands to friendly names const PROCESS_ALIASES: Record = { 'mysqld': 'MySQL 8.0', 'redis-server': 'Redis 7', 'qdrant': 'Qdrant', 'nginx': 'Nginx', 'gitea': 'Gitea', 'node dist/src/main.js': 'NestJS API', 'node dist/main.js': 'NestJS API', 'act_runner': 'Gitea Runner', 'zhixi-worker': 'RAG Worker', 'python3': 'Python Worker', 'docker': 'Docker Daemon', 'containerd': 'containerd', 'hermes': 'Hermes Agent', 'certbot': 'Certbot', }; function friendlyName(cmd: string): string { for (const [pattern, name] of Object.entries(PROCESS_ALIASES)) { if (cmd.includes(pattern)) return name; } // Trim common paths return cmd.replace(/^\/usr\/bin\//, '').replace(/^\/opt\/.*?\//, '').replace(/^\/usr\/lib\//, '').slice(0, 30); } async function getDisks(mounts: string[]): Promise { const results: DiskInfo[] = []; for (const m of mounts) { try { const { stdout } = await execAsync(`df -h ${m} | tail -1 | awk '{print $2,$3,$4,$5}'`); const parts = stdout.trim().split(/\s+/); results.push({ mount: m, total: parts[0] || '-', used: parts[1] || '-', free: parts[2] || '-', percent: parseInt(parts[3]) || 0 }); } catch { results.push({ mount: m, total: '-', used: '-', free: '-', percent: 0 }) } } return results; } async function getProcesses(): Promise { try { const { stdout } = await execAsync("ps aux --sort=-%mem --no-headers | head -10 | awk '{print $2,$3,$4,$11}'"); return stdout.trim().split('\n').filter(Boolean).map(line => { const [pid, cpu, mem, ...cmd] = line.trim().split(/\s+/); const command = (cmd || []).join(' '); return { pid: parseInt(pid), cpu: cpu + '%', mem: mem + '%', name: friendlyName(command), command }; }); } catch { return [] } } @Injectable() export class AdminServersService { private readonly logger = new Logger(AdminServersService.name); async getLocalMetrics(): Promise { const cpus = os.cpus(); const totalMem = os.totalmem(); const freeMem = os.freemem(); const usedMem = totalMem - freeMem; const cpuUsage = Math.min(100, Math.round((os.loadavg()[0] / cpus.length) * 100)); const [disks, processes] = await Promise.all([ getDisks(['/', '/data']), getProcesses(), ]); const nets = os.networkInterfaces(); const privateIp = Object.values(nets).flat().find(n => n?.family === 'IPv4' && !n.internal)?.address || '172.21.0.4'; const d = Math.floor(os.uptime() / 86400); const h = Math.floor((os.uptime() % 86400) / 3600); const m = Math.floor((os.uptime() % 3600) / 60); return { name: '蜂驰云 8核32G', role: '生产核心', hostname: os.hostname(), cpu: { model: cpus[0]?.model || '', cores: cpus.length, usagePercent: cpuUsage }, memory: { total: (totalMem / 1e9).toFixed(1) + 'G', used: (usedMem / 1e9).toFixed(1) + 'G', free: (freeMem / 1e9).toFixed(1) + 'G', percent: Math.round((usedMem / totalMem) * 100) }, disks, uptime: `${d}d ${h}h ${m}m`, processes, network: { publicIp: '120.53.227.155', privateIp, domains: ['api.longde.cloud', 'admin.longde.cloud'] }, }; } async getRemoteMetrics(): Promise { try { const base = `ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -i ${SSH_KEY_PATH} ubuntu@${REMOTE_HOST}`; const execSSH = (cmd: string) => execAsync(`${base} "${cmd.replace(/"/g, '\\"')}"`, { timeout: 5000 }).then(r => r.stdout.trim()).catch(() => ''); const [hostname, privateIp, load, cores, memRaw, diskRoot, diskData, uptimeStr] = await Promise.all([ execSSH('hostname'), execSSH("hostname -I | awk '{print $1}'"), execSSH("cat /proc/loadavg | awk '{print $1}'"), execSSH('cat /proc/cpuinfo | grep processor | wc -l'), execSSH("free -m | grep Mem | awk '{print $2,$3,$4}'"), execSSH("df -h / | tail -1 | awk '{print $2,$3,$4,$5}'"), execSSH("df -h /data 2>/dev/null | tail -1 | awk '{print $2,$3,$4,$5}'"), execSSH("uptime -p | sed 's/up //'"), ]); // Get remote process list let processes: ProcessInfo[] = []; try { const procRaw = await execAsync(`${base} "ps aux --sort=-%mem --no-headers | head -10 | awk '{print \\$2,\\$3,\\$4,\\$11}'"`, { timeout: 5000 }); processes = procRaw.stdout.trim().split('\n').filter(Boolean).map(line => { const [pid, cpu, mem, ...cmd] = line.trim().split(/\s+/); const command = (cmd || []).join(' '); return { pid: parseInt(pid), cpu: cpu + '%', mem: mem + '%', name: friendlyName(command), command }; }); } catch {} const load1 = parseFloat(load) || 0; const cpuCores = parseInt(cores) || 4; const cpuUsage = Math.min(100, Math.round((load1 / cpuCores) * 100)); const memParts = memRaw.split(/\s+/); const memPer = memParts[0] && memParts[1] ? Math.round((parseInt(memParts[1]) / parseInt(memParts[0])) * 100) : 0; const rootParts = diskRoot.split(/\s+/); const dataParts = diskData.split(/\s+/); const disks: DiskInfo[] = [ { mount: '/', total: rootParts[0] || '-', used: rootParts[1] || '-', free: rootParts[2] || '-', percent: parseInt(rootParts[3]) || 0 }, ]; if (dataParts.length >= 3) { disks.push({ mount: '/data', total: dataParts[0], used: dataParts[1], free: dataParts[2], percent: parseInt(dataParts[3]) || 0 }); } return { name: '轻量云 4核4G', role: '工具/辅助', hostname: hostname || 'remote', cpu: { model: 'Intel Xeon (Lighthouse)', cores: cpuCores, usagePercent: cpuUsage }, memory: { total: memParts[0] ? (parseInt(memParts[0]) / 1024).toFixed(1) + 'G' : '-', used: memParts[1] ? (parseInt(memParts[1]) / 1024).toFixed(1) + 'G' : '-', free: memParts[2] ? (parseInt(memParts[2]) / 1024).toFixed(1) + 'G' : '-', percent: memPer }, disks, uptime: uptimeStr || '-', processes, network: { publicIp: '81.70.187.179', privateIp: privateIp || '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()]); const servers = [local]; if (remote) servers.push(remote); return { servers }; } }