From 83244c82475a35ca0eac8ba65900de0c808fa8c8 Mon Sep 17 00:00:00 2001 From: chaoszhu Date: Fri, 25 Jul 2025 00:18:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E7=89=88=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E8=BD=AE=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/app/server.js | 2 + server/app/socket/server-status.js | 934 ++++++++++++++++++ web/package.json | 1 + web/src/assets/scss/element/dark.scss | 10 +- web/src/plugins/tools.js | 11 +- web/src/store/index.js | 2 +- .../terminal/components/server-status.vue | 905 +++++++++++++++++ .../terminal/components/terminal-wrapper.vue | 17 +- yarn.lock | 12 + 9 files changed, 1876 insertions(+), 18 deletions(-) create mode 100644 server/app/socket/server-status.js create mode 100644 web/src/views/terminal/components/server-status.vue diff --git a/server/app/server.js b/server/app/server.js index e0a9cdc..72b2ff9 100644 --- a/server/app/server.js +++ b/server/app/server.js @@ -10,6 +10,7 @@ const wsSftpV2 = require('./socket/sftp-v2') const wsDocker = require('./socket/docker') const wsClientInfo = require('./socket/clients') const wsOnekey = require('./socket/onekey') +const wsServerStatus = require('./socket/server-status') const { throwError } = require('./utils/tools') const httpServer = () => { @@ -32,6 +33,7 @@ function serverHandler(app, server) { wsDocker(server) // docker wsOnekey(server) // 一键指令 wsClientInfo(server) // 客户端信息 + wsServerStatus(server) // 服务器状态监控 app.context.throwError = throwError // 常用方法挂载全局ctx上 app.use(compose(middlewares)) // 捕获error.js模块抛出的服务错误 diff --git a/server/app/socket/server-status.js b/server/app/socket/server-status.js new file mode 100644 index 0000000..cddfa7c --- /dev/null +++ b/server/app/socket/server-status.js @@ -0,0 +1,934 @@ +const { Server } = require('socket.io') +const { Client: SSHClient } = require('ssh2') +const { verifyAuthSync } = require('../utils/verify-auth') +const { isAllowedIp } = require('../utils/tools') +const { createTerminal } = require('./terminal') + +module.exports = (httpServer) => { + const serverIo = new Server(httpServer, { + path: '/server-status', + cors: { + origin: '*' + } + }) + + let connectionCount = 0 + + serverIo.on('connection', (socket) => { + connectionCount++ + consola.success(`server-status websocket 已连接 - 当前连接数: ${ connectionCount }`) + let requestIP = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address + if (!isAllowedIp(requestIP)) { + socket.emit('ip_forbidden', 'IP地址不在白名单中') + socket.disconnect() + return + } + + let targetSSHClient = null + let jumpSshClients = [] + let monitorTimer = null + let sendDataTimer = null + let lastNetStats = null // 用于计算网络速率 + let serverError = false // 服务器错误标志 + let previousCpuStats = null // 用于计算CPU使用率 + let defaultNetInterface = null // 默认网络接口 + + // 系统信息缓存(只获取一次的信息) + let staticSystemInfo = { + cpuCount: null, + cpuModel: null, + osInfo: null + } + + // 服务器状态数据(外层维护,提升及时性) + let statusData = { + connect: false, + cpuInfo: {}, + memInfo: {}, + swapInfo: {}, + driveInfo: {}, + netstatInfo: {}, + osInfo: {} + } + + socket.on('ws_server_status', async ({ hostId, token }) => { + try { + const { code } = await verifyAuthSync(token, requestIP) + if (code !== 1) { + socket.emit('token_verify_fail') + socket.disconnect() + return + } + + targetSSHClient = new SSHClient() + let { jumpSshClients: statusJumpSshClients } = await createTerminal(hostId, socket, targetSSHClient, false) + jumpSshClients.push(...statusJumpSshClients || []) + + // 开始监控 + startMonitoring() + } catch (error) { + consola.error('ws_server_status 事件处理失败:', error.message) + socket.emit('server_status_error', `连接失败: ${ error.message }`) + } + }) + + // 检查是否是关键的服务器错误 + const isServerCriticalError = (errorMessage) => { + const criticalErrors = [ + 'free: command not found', // free命令不存在 + '/proc/uptime: No such file or directory', // /proc/uptime文件不存在 + '/proc/net/dev: No such file or directory', // /proc/net/dev文件不存在 + '/proc/stat: No such file or directory', // /proc/stat文件不存在 + '/proc/cpuinfo: No such file or directory' // /proc/cpuinfo文件不存在 + ] + + return criticalErrors.some(error => errorMessage.includes(error)) + } + + // 设置服务器错误状态并停止监控 + const setServerError = (reason) => { + if (serverError) return // 已经设置过错误状态 + + serverError = true + consola.error(`检测到服务器关键错误,停止监控: ${ reason }`) + + // 停止监控定时器 + if (monitorTimer) { + clearInterval(monitorTimer) + monitorTimer = null + } + + // 发送错误状态给前端 + socket.emit('server_status_data', { + connect: false, + error: true, + errorReason: reason, + cpuInfo: {}, + memInfo: {}, + swapInfo: {}, + driveInfo: {}, + netstatInfo: {}, + osInfo: {} + }) + } + + // 执行SSH命令的通用函数(带重试机制) + const executeCommand = (command, maxRetries = 2) => { + return new Promise((resolve, reject) => { + const attemptExecution = (retryCount) => { + if (!targetSSHClient || !targetSSHClient._sock || !targetSSHClient._sock.writable) { + reject(new Error('SSH 连接已断开')) + return + } + + targetSSHClient.exec(command, (err, stream) => { + if (err) { + if (err.message.includes('Channel open failure') && retryCount < maxRetries) { + // 通道打开失败,等待一段时间后重试 + setTimeout(() => { + attemptExecution(retryCount + 1) + }, 100 * (retryCount + 1)) // 递增延迟 + return + } + reject(err) + return + } + + let stdout = '' + let stderr = '' + + stream + .on('data', (data) => { + stdout += data.toString() + }) + .stderr.on('data', (data) => { + stderr += data.toString() + }) + .on('close', (code) => { + if (code !== 0 && stderr) { + reject(new Error(stderr)) + } else { + resolve(stdout.trim()) + } + }) + .on('error', (error) => { + reject(error) + }) + }) + } + + attemptExecution(0) + }) + } + + // 获取静态CPU信息(CPU核心数和型号,只获取一次) + const getStaticCpuInfo = async () => { + if (staticSystemInfo.cpuCount !== null && staticSystemInfo.cpuModel !== null) { + return { + cpuCount: staticSystemInfo.cpuCount, + cpuModel: staticSystemInfo.cpuModel + } + } + + let cpuCount = 0 + let cpuModel = 'Unknown' + + // 获取CPU核心数 + try { + const cpuCountResult = await executeCommand('nproc') + cpuCount = parseInt(cpuCountResult) || 0 + + // 如果nproc失败,尝试其他方法 + if (cpuCount === 0) { + const lscpuResult = await executeCommand('lscpu | grep "^CPU(s):" | head -1') + const cpuCountMatch = lscpuResult.match(/CPU\(s\):\s*(\d+)/) + cpuCount = cpuCountMatch ? parseInt(cpuCountMatch[1]) : 0 + } + } catch (error) { + consola.warn('获取CPU核心数失败:', error.message) + } + + // 获取CPU型号 + try { + const cpuModelResult = await executeCommand('cat /proc/cpuinfo | grep "model name" | head -1') + const cpuModelMatch = cpuModelResult.match(/model name\s*:\s*(.+)/) + if (cpuModelMatch) { + cpuModel = cpuModelMatch[1].trim() + } else { + // 如果/proc/cpuinfo失败,尝试lscpu + const lscpuResult = await executeCommand('lscpu | grep "Model name:" | head -1') + const lscpuModelMatch = lscpuResult.match(/Model name:\s*(.+)/) + cpuModel = lscpuModelMatch ? lscpuModelMatch[1].trim() : 'Unknown' + } + } catch (error) { + consola.warn('获取CPU型号失败:', error.message) + + // 检查是否是关键错误 + if (isServerCriticalError(error.message)) { + setServerError(`获取CPU型号失败: ${ error.message }`) + return { cpuCount: 0, cpuModel: 'Unknown' } + } + } + + // 缓存结果 + staticSystemInfo.cpuCount = cpuCount + staticSystemInfo.cpuModel = cpuModel + + return { cpuCount, cpuModel } + } + + // 解析/proc/stat输出 + const parseProcStat = (procStatOutput) => { + try { + const firstLine = procStatOutput.split('\n')[0] + const values = firstLine.split(' ').slice(2).map(Number) + const idle = values[3] || 0 + const total = values.reduce((a, b) => a + b, 0) + return { idle, total } + } catch (error) { + return null + } + } + + // 获取CPU信息(优化的CPU使用率计算) + const getCpuInfo = async () => { + let cpuUsage = 0 + + try { + const procStatOutput = await executeCommand('cat /proc/stat') + const currentCpuStats = parseProcStat(procStatOutput) + const now = Date.now() + + if (currentCpuStats) { + if (previousCpuStats && previousCpuStats.timestamp < now) { + const totalDiff = currentCpuStats.total - previousCpuStats.total + const idleDiff = currentCpuStats.idle - previousCpuStats.idle + const timeDiffMs = now - previousCpuStats.timestamp + + // 确保时间差>500ms以获得更准确的计算,避免除零错误 + if (totalDiff > 0 && timeDiffMs > 500) { + const usageRatio = 1.0 - (idleDiff / totalDiff) + // 限制在0-100范围内 + cpuUsage = Math.max(0, Math.min(100, usageRatio * 100)) + } else { + // 时间间隔太短或没有变化,保持上次的值或设为0 + cpuUsage = 0 + } + } else { + // 第一次运行,无法计算使用率 + cpuUsage = 0 + } + + // 存储当前统计信息供下次使用 + previousCpuStats = { ...currentCpuStats, timestamp: now } + } + } catch (error) { + consola.warn('获取CPU使用率失败:', error.message) + + // 检查是否是关键错误 + if (isServerCriticalError(error.message)) { + setServerError(`获取CPU使用率失败: ${ error.message }`) + return { cpuUsage: 0, cpuCount: 0, cpuModel: 'Unknown' } + } + } + + // 获取静态CPU信息 + const { cpuCount, cpuModel } = await getStaticCpuInfo() + + // 获取负载平均值 + let loadAvg = [0, 0, 0] + try { + const uptimeOutput = await executeCommand('uptime') + const match = uptimeOutput.match(/load average(?:s)?:\s*([\d.]+)[, ]?\s*([\d.]+)[, ]?\s*([\d.]+)/) + if (match) { + loadAvg = [parseFloat(match[1]), parseFloat(match[2]), parseFloat(match[3])] + } + } catch (error) { + // 静默处理负载平均值获取失败 + } + + return { + cpuUsage: parseFloat(cpuUsage.toFixed(2)), + cpuCount, + cpuModel, + loadAvg + } + } + + // 获取内存和交换空间信息(统一处理,支持BusyBox) + const getMemoryInfo = async () => { + const defaultReturn = { + memInfo: { totalMemMb: 0, usedMemMb: 0, freeMemMb: 0, usedMemPercentage: 0, freeMemPercentage: 0 }, + swapInfo: { swapTotal: 0, swapUsed: 0, swapFree: 0, swapPercentage: '0' } + } + + try { + // 检查是否为BusyBox环境 + let freeCommand = 'free -m' + let isBusyBox = false + + try { + const busyboxCheck = await executeCommand('busybox --help') + if (busyboxCheck.includes('BusyBox')) { + freeCommand = 'free' + isBusyBox = true + } + } catch (err) { + // 如果检查失败,默认使用 free -m + } + + const freeOutput = await executeCommand(freeCommand) + const lines = freeOutput.split('\n') + + // 使用更鲁棒的方式查找内存和交换空间行 + const memLine = lines.find(line => line.trim().startsWith('Mem:')) + const swapLine = lines.find(line => line.trim().startsWith('Swap:')) + + let memInfo = defaultReturn.memInfo + let swapInfo = defaultReturn.swapInfo + + // 处理内存信息 + if (memLine) { + const parts = memLine.trim().split(/\s+/) + if (parts.length >= 3) { + let totalVal = parseInt(parts[1], 10) + let usedVal = parseInt(parts[2], 10) + let freeVal = parts[3] ? parseInt(parts[3], 10) : (totalVal - usedVal) + + // BusyBox环境下需要从KB转换为MB + if (isBusyBox) { + if (!isNaN(totalVal)) totalVal = Math.round(totalVal / 1024) + if (!isNaN(usedVal)) usedVal = Math.round(usedVal / 1024) + if (!isNaN(freeVal)) freeVal = Math.round(freeVal / 1024) + } + + if (!isNaN(totalVal) && !isNaN(usedVal)) { + const usedMemPercentage = totalVal > 0 ? parseFloat(((usedVal / totalVal) * 100).toFixed(2)) : 0 + const freeMemPercentage = totalVal > 0 ? parseFloat(((freeVal / totalVal) * 100).toFixed(2)) : 0 + + memInfo = { + totalMemMb: totalVal, + usedMemMb: usedVal, + freeMemMb: freeVal, + usedMemPercentage, + freeMemPercentage + } + } + } + } + + // 处理交换空间信息 + if (swapLine) { + const parts = swapLine.trim().split(/\s+/) + if (parts.length >= 3) { + let totalVal = parseInt(parts[1], 10) + let usedVal = parseInt(parts[2], 10) + let freeVal = parts[3] ? parseInt(parts[3], 10) : (totalVal - usedVal) + + // BusyBox环境下需要从KB转换为MB + if (isBusyBox) { + if (!isNaN(totalVal)) totalVal = Math.round(totalVal / 1024) + if (!isNaN(usedVal)) usedVal = Math.round(usedVal / 1024) + if (!isNaN(freeVal)) freeVal = Math.round(freeVal / 1024) + } + + if (!isNaN(totalVal) && !isNaN(usedVal)) { + const swapPercentage = totalVal > 0 ? ((usedVal / totalVal) * 100).toFixed(1) : '0' + + swapInfo = { + swapTotal: totalVal, + swapUsed: usedVal, + swapFree: freeVal, + swapPercentage + } + } + } + } else { + // 没有交换空间 + swapInfo = { swapTotal: 0, swapUsed: 0, swapFree: 0, swapPercentage: '0' } + } + + return { memInfo, swapInfo } + + } catch (error) { + consola.error('获取内存信息失败:', error.message) + + // 检查是否是关键错误 + if (isServerCriticalError(error.message)) { + setServerError(`获取内存信息失败: ${ error.message }`) + } + + return defaultReturn + } + } + + // 获取内存信息(兼容性包装函数) + const getMemInfo = async () => { + const { memInfo } = await getMemoryInfo() + return memInfo + } + + // 获取Swap信息(兼容性包装函数) + const getSwapInfo = async () => { + const { swapInfo } = await getMemoryInfo() + return swapInfo + } + + // 获取磁盘信息 + const getDriveInfo = async () => { + try { + const driveInfo = await executeCommand('df -kP / | tail -1') + const driveLine = driveInfo.split(/\s+/) + + const totalKb = parseInt(driveLine[1]) + const usedKb = parseInt(driveLine[2]) + const freeKb = parseInt(driveLine[3]) + const usedPercentage = parseFloat(driveLine[4].replace('%', '')) + + return { + totalGb: (totalKb / 1024 / 1024).toFixed(1), + usedGb: (usedKb / 1024 / 1024).toFixed(1), + freeGb: (freeKb / 1024 / 1024).toFixed(1), + usedPercentage: usedPercentage.toFixed(1), + freePercentage: (100 - usedPercentage).toFixed(1) + } + } catch (error) { + consola.error('获取磁盘信息失败:', error.message) + return { totalGb: '0', usedGb: '0', freeGb: '0', usedPercentage: '0', freePercentage: '0' } + } + } + + // 获取默认网络接口 + const getDefaultInterface = async () => { + if (defaultNetInterface) { + return defaultNetInterface + } + + try { + // 尝试通过路由表获取默认接口 + const routeOutput = await executeCommand('ip route get 1') + const match = routeOutput.match(/dev\s+(\w+)/) + if (match) { + defaultNetInterface = match[1] + return defaultNetInterface + } + } catch (error) { + // 静默处理,继续尝试其他方法 + } + + try { + // 备用方法:获取第一个非lo接口 + const netDev = await executeCommand('cat /proc/net/dev') + const lines = netDev.split('\n').slice(2) + for (const line of lines) { + const parts = line.trim().split(/\s+/) + if (parts.length >= 9 && parts[0]) { + const interfaceName = parts[0].replace(':', '') + if (interfaceName !== 'lo') { + defaultNetInterface = interfaceName + return defaultNetInterface + } + } + } + } catch (error) { + // 静默处理 + } + + return null + } + + // 解析网络设备统计信息 + const parseNetworkStats = async () => { + try { + const netDev = await executeCommand('cat /proc/net/dev') + const lines = netDev.split('\n').slice(2) + const stats = {} + + lines.forEach(line => { + const parts = line.trim().split(/\s+/) + if (parts.length >= 9 && parts[0]) { + const interfaceName = parts[0].replace(':', '') + const rxBytes = parseInt(parts[1]) || 0 + const txBytes = parseInt(parts[9]) || 0 + stats[interfaceName] = { rxBytes, txBytes } + } + }) + + return stats + } catch (error) { + return null + } + } + + // 获取网络信息(优化版) + const getNetstatInfo = async () => { + try { + const currentStats = await parseNetworkStats() + if (!currentStats) { + return { total: { inputMb: 0, outputMb: 0 } } + } + + const defaultInterface = await getDefaultInterface() + const timestamp = Date.now() + let netstatInfo = {} + + // 计算总流量(所有接口) + let totalInput = 0 + let totalOutput = 0 + Object.values(currentStats).forEach(stat => { + totalInput += stat.rxBytes + totalOutput += stat.txBytes + }) + + if (lastNetStats && lastNetStats.timestamp < timestamp) { + const timeDiffSeconds = (timestamp - lastNetStats.timestamp) / 1000 + + if (timeDiffSeconds > 0.1) { + // 总流量速率 + netstatInfo.total = { + inputMb: ((totalInput - lastNetStats.totalInput) / timeDiffSeconds / 1024 / 1024).toFixed(3), + outputMb: ((totalOutput - lastNetStats.totalOutput) / timeDiffSeconds / 1024 / 1024).toFixed(3) + } + + // 默认接口的速率 + if (defaultInterface && currentStats[defaultInterface] && lastNetStats.interfaces[defaultInterface]) { + const currentRx = currentStats[defaultInterface].rxBytes + const currentTx = currentStats[defaultInterface].txBytes + const lastRx = lastNetStats.interfaces[defaultInterface].rxBytes + const lastTx = lastNetStats.interfaces[defaultInterface].txBytes + + netstatInfo.default = { + interface: defaultInterface, + inputMb: ((currentRx - lastRx) / timeDiffSeconds / 1024 / 1024).toFixed(3), + outputMb: ((currentTx - lastTx) / timeDiffSeconds / 1024 / 1024).toFixed(3) + } + } + + // 各个接口的速率 + Object.keys(currentStats).forEach(interfaceName => { + if (lastNetStats.interfaces[interfaceName]) { + const currentInterface = currentStats[interfaceName] + const lastInterface = lastNetStats.interfaces[interfaceName] + + netstatInfo[interfaceName] = { + inputMb: ((currentInterface.rxBytes - lastInterface.rxBytes) / timeDiffSeconds / 1024 / 1024).toFixed(3), + outputMb: ((currentInterface.txBytes - lastInterface.txBytes) / timeDiffSeconds / 1024 / 1024).toFixed(3) + } + } + }) + } else { + // 时间间隔太短,设为0 + netstatInfo.total = { inputMb: '0.000', outputMb: '0.000' } + if (defaultInterface) { + netstatInfo.default = { + interface: defaultInterface, + inputMb: '0.000', + outputMb: '0.000' + } + } + } + } else { + // 第一次运行,初始化为0 + netstatInfo.total = { inputMb: '0.000', outputMb: '0.000' } + if (defaultInterface) { + netstatInfo.default = { + interface: defaultInterface, + inputMb: '0.000', + outputMb: '0.000' + } + } + } + + // 保存当前状态 + lastNetStats = { + totalInput, + totalOutput, + interfaces: currentStats, + timestamp + } + + return netstatInfo + } catch (error) { + consola.error('获取网络信息失败:', error.message) + + // 检查是否是关键错误 + if (isServerCriticalError(error.message)) { + setServerError(`获取网络信息失败: ${ error.message }`) + return { total: { inputMb: '0.000', outputMb: '0.000' } } + } + + return { total: { inputMb: '0.000', outputMb: '0.000' } } + } + } + + // 获取静态系统信息(只获取一次的信息) + const getStaticOsInfo = async () => { + if (staticSystemInfo.osInfo !== null) { + return staticSystemInfo.osInfo + } + + const defaultStaticInfo = { + hostname: 'Unknown', + type: 'Linux', + platform: 'linux', + release: 'Unknown', + arch: 'Unknown' + } + + try { + // 串行执行命令避免并发问题 + let hostname = 'Unknown' + try { + hostname = await executeCommand('hostname') || 'Unknown' + } catch (e) { + consola.warn('获取hostname失败:', e.message) + } + + // 通过读取 /etc/os-release 文件获取操作系统信息 + let type = 'Linux' + let release = 'Unknown' + let arch = 'Unknown' + + try { + // 读取 /etc/os-release 文件获取系统信息 + const osReleaseContent = await executeCommand('cat /etc/os-release') + const osReleaseLines = osReleaseContent.split('\n') + + // 解析系统名称和版本 + osReleaseLines.forEach(line => { + if (line.startsWith('NAME=')) { + type = line.replace('NAME=', '').replace(/"/g, '') || 'Linux' + } else if (line.startsWith('VERSION=')) { + release = line.replace('VERSION=', '').replace(/"/g, '') || 'Unknown' + } else if (line.startsWith('PRETTY_NAME=')) { + // 如果有PRETTY_NAME,可以作为更友好的显示名称 + const prettyName = line.replace('PRETTY_NAME=', '').replace(/"/g, '') + if (prettyName) { + type = prettyName.split(' ')[0] // 取第一个词作为系统类型 + // 提取版本信息 + const versionMatch = prettyName.match(/(\d+\.?\d*\.?\d*)/) + if (versionMatch) { + release = versionMatch[1] + } + } + } + }) + } catch (e) { + consola.warn('读取 /etc/os-release 失败,尝试使用 uname 命令:', e.message) + // 如果读取 /etc/os-release 失败,回退到 uname 命令 + try { + type = await executeCommand('uname -s') || 'Linux' + } catch (e2) { + consola.warn('获取系统类型失败:', e2.message) + } + + try { + release = await executeCommand('uname -r') || 'Unknown' + } catch (e2) { + consola.warn('获取系统版本失败:', e2.message) + } + } + + // 获取系统架构 + try { + arch = await executeCommand('uname -m') || 'Unknown' + } catch (e) { + consola.warn('获取系统架构失败:', e.message) + } + + const staticInfo = { + hostname: hostname.trim(), + type: type.trim(), + platform: 'linux', + release: release.trim(), + arch: arch.trim() + } + + // 缓存结果 + staticSystemInfo.osInfo = staticInfo + return staticInfo + } catch (error) { + consola.error('获取静态系统信息失败:', error.message) + staticSystemInfo.osInfo = defaultStaticInfo + return defaultStaticInfo + } + } + + // 获取系统信息(结合静态信息和动态信息) + const getOsInfo = async () => { + try { + // 获取静态信息 + const staticInfo = await getStaticOsInfo() + + // 获取动态信息:系统运行时间 + let uptime = 0 + try { + const uptimeStr = await executeCommand('cat /proc/uptime | cut -d" " -f1') || '0' + uptime = parseFloat(uptimeStr) || 0 + } catch (e) { + consola.warn('获取系统运行时间失败:', e.message) + + // 检查是否是关键错误 + if (isServerCriticalError(e.message)) { + setServerError(`获取系统运行时间失败: ${ e.message }`) + return { ...staticInfo, uptime: 0 } + } + } + + return { + ...staticInfo, + uptime + } + } catch (error) { + consola.error('获取系统信息失败:', error.message) + return { + hostname: 'Unknown', + type: 'Linux', + platform: 'linux', + release: 'Unknown', + arch: 'Unknown', + uptime: 0 + } + } + } + + // 更新CPU信息到全局statusData + const updateCpuInfo = async () => { + try { + const cpuInfo = await getCpuInfo() + statusData.cpuInfo = cpuInfo + statusData.connect = true // 标记连接状态 + } catch (error) { + consola.error('更新CPU信息失败:', error.message) + statusData.cpuInfo = {} + } + } + + // 更新内存信息到全局statusData + const updateMemoryInfo = async () => { + try { + const { memInfo, swapInfo } = await getMemoryInfo() + statusData.memInfo = memInfo + statusData.swapInfo = swapInfo + statusData.connect = true + } catch (error) { + consola.error('更新内存信息失败:', error.message) + statusData.memInfo = {} + statusData.swapInfo = {} + } + } + + // 更新磁盘信息到全局statusData + const updateDriveInfo = async () => { + try { + const driveInfo = await getDriveInfo() + statusData.driveInfo = driveInfo + statusData.connect = true + } catch (error) { + consola.error('更新磁盘信息失败:', error.message) + statusData.driveInfo = {} + } + } + + // 更新网络信息到全局statusData + const updateNetworkInfo = async () => { + try { + const netstatInfo = await getNetstatInfo() + statusData.netstatInfo = netstatInfo + statusData.connect = true + } catch (error) { + consola.error('更新网络信息失败:', error.message) + statusData.netstatInfo = {} + } + } + + // 更新系统信息到全局statusData + const updateOsInfo = async () => { + try { + const osInfo = await getOsInfo() + statusData.osInfo = osInfo + statusData.connect = true + } catch (error) { + consola.error('更新系统信息失败:', error.message) + statusData.osInfo = {} + } + } + + // 更新所有服务器状态信息(改为增量更新模式) + const updateServerStatus = async () => { + try { + // 并行执行一些不冲突的数据获取,每完成一个就立即更新statusData + const updateTasks = [ + updateCpuInfo(), + updateMemoryInfo(), + updateDriveInfo(), + updateNetworkInfo(), + updateOsInfo() + ] + + // 使用Promise.allSettled确保即使某个更新失败,其他更新仍能继续 + const results = await Promise.allSettled(updateTasks) + + // 检查是否有任何成功的更新 + const hasSuccessfulUpdate = results.some(result => result.status === 'fulfilled') + if (!hasSuccessfulUpdate) { + // 如果所有更新都失败,标记为断开连接 + statusData.connect = false + } + + } catch (error) { + consola.error('更新服务器状态过程中出错:', error.message) + statusData.connect = false + } + } + + // 开始监控(解耦数据收集和数据发送,提升及时性) + let isCollecting = false + const startMonitoring = () => { + // 数据收集函数 - 每2秒执行一次 + const collectData = async () => { + try { + if (isCollecting) return + isCollecting = true + // 如果已经检测到服务器错误,停止监控 + if (serverError) { + if (monitorTimer) { + clearInterval(monitorTimer) + monitorTimer = null + } + if (sendDataTimer) { + clearInterval(sendDataTimer) + sendDataTimer = null + } + consola.info('服务器存在关键错误,已停止监控') + return + } + + // 更新全局statusData + await updateServerStatus() + } catch (error) { + consola.error('数据收集过程中出错:', error.message) + } finally { + isCollecting = false + } + } + + // 数据发送函数 - 每1秒执行一次 + const sendData = () => { + try { + // 如果检测到服务器错误,停止发送 + if (serverError) { + if (sendDataTimer) { + clearInterval(sendDataTimer) + sendDataTimer = null + } + return + } + + // 发送当前statusData给前端 + socket.emit('server_status_data', statusData) + } catch (error) { + consola.error('数据发送过程中出错:', error.message) + } + } + + // 立即执行一次数据收集 + collectData() + + // 启动定时器(如果没有错误) + if (!serverError) { + // 每2秒收集一次数据 + monitorTimer = setInterval(collectData, 3000) + // 每1秒发送一次数据(1秒后开始,避免与首次收集冲突) + if (!serverError) { + sendDataTimer = setInterval(sendData, 1500) + } + } + } + + socket.on('disconnect', (reason) => { + connectionCount-- + + // 清理所有定时器 + if (monitorTimer) { + clearInterval(monitorTimer) + monitorTimer = null + } + if (sendDataTimer) { + clearInterval(sendDataTimer) + sendDataTimer = null + } + + targetSSHClient && targetSSHClient.end() + jumpSshClients?.forEach(sshClient => sshClient && sshClient.end()) + + // 清理所有状态变量 + targetSSHClient = null + jumpSshClients = null + lastNetStats = null + previousCpuStats = null + defaultNetInterface = null + serverError = false + + // 清理系统信息缓存 + staticSystemInfo = { + cpuCount: null, + cpuModel: null, + osInfo: null + } + + // 重置状态数据 + statusData = { + connect: false, + cpuInfo: {}, + memInfo: {}, + swapInfo: {}, + driveInfo: {}, + netstatInfo: {}, + osInfo: {} + } + + consola.info(`server-status websocket 连接断开: ${ reason } - 当前连接数: ${ connectionCount }`) + }) + }) +} \ No newline at end of file diff --git a/web/package.json b/web/package.json index fd09347..5617671 100644 --- a/web/package.json +++ b/web/package.json @@ -32,6 +32,7 @@ "@xterm/xterm": "^5.5.0", "ant-design-x-vue": "^1.0.7", "axios": "^1.8.4", + "chart.js": "^4.5.0", "codemirror": "^6.0.1", "crypto-js": "^4.2.0", "csv-parse": "^5.6.0", diff --git a/web/src/assets/scss/element/dark.scss b/web/src/assets/scss/element/dark.scss index b5f8cea..7b564a2 100644 --- a/web/src/assets/scss/element/dark.scss +++ b/web/src/assets/scss/element/dark.scss @@ -59,12 +59,10 @@ html.dark { } } } - .info_box { - border-right: 1px solid #454242; - .el-progress-bar__innerText { - span { - color: #fff !important; - } + .el-progress-bar__innerText { + span { + color: #ffffff !important; // 强制白色文字,确保在各种背景下都能看清 + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); // 添加文字阴影增强可读性 } } diff --git a/web/src/plugins/tools.js b/web/src/plugins/tools.js index b3245ef..4e32341 100644 --- a/web/src/plugins/tools.js +++ b/web/src/plugins/tools.js @@ -5,11 +5,18 @@ export default { value = Number(value) return isNaN(value) ? '--' : value.toFixed(count) }, - formatTime(second = 0) { + formatTime(second = 0, target = 'day') { let day = Math.floor(second / 60 / 60 / 24) let hour = Math.floor(second / 60 / 60 % 24) let minute = Math.floor(second / 60 % 60) - return `${ day }天${ hour }时${ minute }分` + if (target === 'day') { + return `${ day }天` + } else if (target === 'hour') { + return `${ day }天${ hour }时` + } else if (target === 'minute') { + return `${ day }天${ hour }时${ minute }分` + } + return `${ day }天${ hour }时${ minute }分${ second }秒` }, formatNetSpeed(netSpeedMB) { netSpeedMB = Number(netSpeedMB) || 0 diff --git a/web/src/store/index.js b/web/src/store/index.js index 05706c0..6dba5f3 100644 --- a/web/src/store/index.js +++ b/web/src/store/index.js @@ -83,7 +83,7 @@ const useStore = defineStore('global', { await this.getPlusInfo() this.getAIConfig() this.getChatHistory() - this.wsClientsStatus() + // this.wsClientsStatus() }, async getHostList() { let { data: newHostList } = await $api.getHostList() diff --git a/web/src/views/terminal/components/server-status.vue b/web/src/views/terminal/components/server-status.vue new file mode 100644 index 0000000..4441065 --- /dev/null +++ b/web/src/views/terminal/components/server-status.vue @@ -0,0 +1,905 @@ + + + + + + + diff --git a/web/src/views/terminal/components/terminal-wrapper.vue b/web/src/views/terminal/components/terminal-wrapper.vue index f08b3ee..6656b09 100644 --- a/web/src/views/terminal/components/terminal-wrapper.vue +++ b/web/src/views/terminal/components/terminal-wrapper.vue @@ -282,20 +282,18 @@ direction="ltr" class="mobile_menu_drawer" > -
-