diff --git a/package-lock.json b/package-lock.json index b3a2616..055769f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -466,6 +466,15 @@ "node": ">=12" } }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz", + "integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==", + "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", + "engines": { + "node": ">=6" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -1014,6 +1023,42 @@ "@types/node": "*" } }, + "node_modules/@types/splitpanes": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@types/splitpanes/-/splitpanes-2.2.6.tgz", + "integrity": "sha512-3dV5sO1Ht74iER4jJU03mreL3f+Q2h47ZqXS6Sfbqc6hkCvsDrX1GA0NbYWRdNvZemPyTDzUoApWKeoGbALwkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "vue": "^2.0.0" + } + }, + "node_modules/@types/splitpanes/node_modules/@vue/compiler-sfc": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.16.tgz", + "integrity": "sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.23.5", + "postcss": "^8.4.14", + "source-map": "^0.6.1" + }, + "optionalDependencies": { + "prettier": "^1.18.2 || ^2.0.0" + } + }, + "node_modules/@types/splitpanes/node_modules/vue": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.16.tgz", + "integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==", + "deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-sfc": "2.7.16", + "csstype": "^3.1.0" + } + }, "node_modules/@types/sqlite3": { "version": "3.1.11", "resolved": "https://registry.npmjs.org/@types/sqlite3/-/sqlite3-3.1.11.tgz", @@ -4434,6 +4479,23 @@ "node": ">=10" } }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -5144,6 +5206,18 @@ "node": ">=0.10.0" } }, + "node_modules/splitpanes": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/splitpanes/-/splitpanes-4.0.3.tgz", + "integrity": "sha512-S/f1CoH2JroOib7kzQtTQNtQCa7VzNQ2qKOO5HNj/5EVVcNkfz1eX/sH+X3XKdBdDLihEKDekVGwrLADd2oirA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antoniandre" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -6274,11 +6348,13 @@ "name": "@nexus-terminal/frontend", "version": "0.1.0", "dependencies": { + "@fortawesome/fontawesome-free": "^6.7.2", "@simplewebauthn/browser": "^13.1.0", "axios": "^1.8.4", "monaco-editor": "^0.52.2", "pinia": "^3.0.2", "pinia-plugin-persistedstate": "^4.2.0", + "splitpanes": "^4.0.3", "vite-plugin-monaco-editor": "^1.1.0", "vue": "^3.3.0", "vue-i18n": "^9.14.4", @@ -6288,6 +6364,7 @@ "xterm-addon-web-links": "^0.9.0" }, "devDependencies": { + "@types/splitpanes": "^2.2.6", "@vitejs/plugin-vue": "^4.2.0", "typescript": "^5.0.0", "vite": "^4.4.0", diff --git a/packages/backend/src/services/status-monitor.service.ts b/packages/backend/src/services/status-monitor.service.ts index 26e863c..81f58d9 100644 --- a/packages/backend/src/services/status-monitor.service.ts +++ b/packages/backend/src/services/status-monitor.service.ts @@ -55,13 +55,13 @@ export class StatusMonitorService { } if (state.statusIntervalId) { //console.warn(`[StatusMonitor] 会话 ${sessionId} 的状态轮询已在运行中。`); - return; - } - //console.warn(`[StatusMonitor] 为会话 ${sessionId} 启动状态轮询,间隔 ${interval}ms`); - this.fetchAndSendServerStatus(sessionId); // 立即执行一次 - state.statusIntervalId = setInterval(() => { - this.fetchAndSendServerStatus(sessionId); - }, interval); + return; + } + //console.warn(`[StatusMonitor] 为会话 ${sessionId} 启动状态轮询,间隔 ${interval}ms`); + // 移除立即执行,让 setInterval 负责第一次调用,给连接更多准备时间 + state.statusIntervalId = setInterval(() => { + this.fetchAndSendServerStatus(sessionId); + }, interval); } /** @@ -118,11 +118,31 @@ export class StatusMonitorService { status.osName = nameMatch ? nameMatch[1] : (osReleaseOutput.match(/^NAME="?([^"]+)"?/m)?.[1] ?? 'Unknown'); } catch (err) { console.warn(`[StatusMonitor ${sessionId}] Failed to get OS name:`, err); } - // --- CPU Model --- + // --- CPU Model (Try /proc/cpuinfo first, fallback to lscpu) --- try { - const lscpuOutput = await this.executeSshCommand(sshClient, "lscpu | grep 'Model name:'"); - status.cpuModel = lscpuOutput.match(/Model name:\s+(.*)/)?.[1].trim() ?? 'Unknown'; - } catch (err) { console.warn(`[StatusMonitor ${sessionId}] Failed to get CPU model:`, err); } + let cpuModelOutput = ''; + try { + // Try /proc/cpuinfo first, common on many systems including Alpine + cpuModelOutput = await this.executeSshCommand(sshClient, "cat /proc/cpuinfo | grep 'model name' | head -n 1"); + status.cpuModel = cpuModelOutput.match(/model name\s*:\s*(.*)/i)?.[1].trim(); + } catch (procErr) { + console.warn(`[StatusMonitor ${sessionId}] Failed to get CPU model from /proc/cpuinfo, trying lscpu...`, procErr); + // Fallback to lscpu if /proc/cpuinfo fails + try { + cpuModelOutput = await this.executeSshCommand(sshClient, "lscpu | grep 'Model name:'"); + status.cpuModel = cpuModelOutput.match(/Model name:\s+(.*)/)?.[1].trim(); + } catch (lscpuErr) { + console.warn(`[StatusMonitor ${sessionId}] Failed to get CPU model from lscpu as well:`, lscpuErr); + } + } + // If still no model found after both attempts + if (!status.cpuModel) { + status.cpuModel = 'Unknown'; + } + } catch (err) { // Catch any unexpected error during the process + console.warn(`[StatusMonitor ${sessionId}] Error getting CPU model:`, err); + status.cpuModel = 'Unknown'; + } // --- Memory and Swap --- try { @@ -154,16 +174,23 @@ export class StatusMonitorService { } else { status.swapTotal = 0; status.swapUsed = 0; status.swapPercent = 0; } } catch (err) { console.warn(`[StatusMonitor ${sessionId}] Failed to get memory/swap usage:`, err); } - // --- Disk Usage (Root Partition) --- + // --- Disk Usage (Root Partition, POSIX format for compatibility) --- try { - const dfOutput = await this.executeSshCommand(sshClient, "df -k / | tail -n 1"); - const parts = dfOutput.split(/\s+/); - if (parts.length >= 5) { - const total = parseInt(parts[1], 10); const used = parseInt(parts[2], 10); - const percentMatch = parts[4].match(/(\d+)%/); - if (!isNaN(total) && !isNaN(used) && percentMatch) { - status.diskTotal = total; status.diskUsed = used; - status.diskPercent = parseFloat(percentMatch[1]); + // 使用 df -kP / 获取 POSIX 标准格式输出,更稳定 + const dfOutput = await this.executeSshCommand(sshClient, "df -kP /"); + const lines = dfOutput.split('\n'); + if (lines.length >= 2) { + const parts = lines[1].split(/\s+/); // 解析第二行 (数据行) + // POSIX 格式: Filesystem 1024-blocks Used Available Capacity Mounted on + // parts[1]=Total(KB), parts[2]=Used(KB), parts[4]=Capacity(%) + if (parts.length >= 5) { + const total = parseInt(parts[1], 10); + const used = parseInt(parts[2], 10); + const percentMatch = parts[4].match(/(\d+)%/); + if (!isNaN(total) && !isNaN(used) && percentMatch) { + status.diskTotal = total; status.diskUsed = used; + status.diskPercent = parseFloat(percentMatch[1]); + } } } } catch (err) { console.warn(`[StatusMonitor ${sessionId}] Failed to get disk usage:`, err); } diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 332183f..e067806 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -8,11 +8,13 @@ "preview": "vite preview" }, "dependencies": { + "@fortawesome/fontawesome-free": "^6.7.2", "@simplewebauthn/browser": "^13.1.0", "axios": "^1.8.4", "monaco-editor": "^0.52.2", "pinia": "^3.0.2", "pinia-plugin-persistedstate": "^4.2.0", + "splitpanes": "^4.0.3", "vite-plugin-monaco-editor": "^1.1.0", "vue": "^3.3.0", "vue-i18n": "^9.14.4", @@ -22,6 +24,7 @@ "xterm-addon-web-links": "^0.9.0" }, "devDependencies": { + "@types/splitpanes": "^2.2.6", "@vitejs/plugin-vue": "^4.2.0", "typescript": "^5.0.0", "vite": "^4.4.0", diff --git a/packages/frontend/src/App.vue b/packages/frontend/src/App.vue index ebc3930..a1876e9 100644 --- a/packages/frontend/src/App.vue +++ b/packages/frontend/src/App.vue @@ -1,10 +1,12 @@ + + + + diff --git a/packages/frontend/src/components/WorkspaceConnectionList.vue b/packages/frontend/src/components/WorkspaceConnectionList.vue index 255ff8a..72a3699 100644 --- a/packages/frontend/src/components/WorkspaceConnectionList.vue +++ b/packages/frontend/src/components/WorkspaceConnectionList.vue @@ -1,5 +1,5 @@ +