This commit is contained in:
Baobhan Sith
2025-04-20 21:53:17 +08:00
parent 7b00a111dd
commit b862e85ea5
7 changed files with 174 additions and 27 deletions
@@ -6,6 +6,8 @@ const FOCUS_SEQUENCE_KEY = 'focusSwitcherSequence'; // 焦点切换顺序设置
const NAV_BAR_VISIBLE_KEY = 'navBarVisible'; // 导航栏可见性设置键
const LAYOUT_TREE_KEY = 'layoutTree'; // 布局树设置键
const AUTO_COPY_ON_SELECT_KEY = 'autoCopyOnSelect'; // 终端选中自动复制设置键
const STATUS_MONITOR_INTERVAL_SECONDS_KEY = 'statusMonitorIntervalSeconds'; // 状态监控间隔设置键
const DEFAULT_STATUS_MONITOR_INTERVAL_SECONDS = 3; // 默认状态监控间隔
export const settingsService = {
/**
@@ -240,5 +242,54 @@ export const settingsService = {
console.error(`[Service] Error calling settingsRepository.setSetting for key ${AUTO_COPY_ON_SELECT_KEY}:`, error);
throw new Error('Failed to save auto copy on select setting.');
}
}, // *** 确保这里有逗号 ***
/**
* 获取状态监控轮询间隔 (秒)
* @returns 返回间隔秒数 (number),如果未设置或无效则返回默认值
*/
async getStatusMonitorIntervalSeconds(): Promise<number> {
console.log(`[Service] Attempting to get setting for key: ${STATUS_MONITOR_INTERVAL_SECONDS_KEY}`);
try {
const intervalStr = await settingsRepository.getSetting(STATUS_MONITOR_INTERVAL_SECONDS_KEY);
console.log(`[Service] Raw value from repository for ${STATUS_MONITOR_INTERVAL_SECONDS_KEY}:`, intervalStr);
if (intervalStr) {
const intervalNum = parseInt(intervalStr, 10);
// 验证是否为正整数
if (!isNaN(intervalNum) && intervalNum > 0) {
return intervalNum;
} else {
console.warn(`[Service] Invalid status monitor interval value found ('${intervalStr}'). Returning default.`);
}
} else {
console.log(`[Service] No status monitor interval found in settings. Returning default.`);
}
} catch (error) {
console.error(`[Service] Error getting status monitor interval setting (key: ${STATUS_MONITOR_INTERVAL_SECONDS_KEY}):`, error);
}
// 返回默认值
return DEFAULT_STATUS_MONITOR_INTERVAL_SECONDS;
}, // *** 确保这里有逗号 ***
/**
* 设置状态监控轮询间隔 (秒)
* @param interval 间隔秒数 (number)
*/
async setStatusMonitorIntervalSeconds(interval: number): Promise<void> {
console.log(`[Service] setStatusMonitorIntervalSeconds called with: ${interval}`);
// 验证输入是否为正整数
if (!Number.isInteger(interval) || interval <= 0) {
console.error(`[Service] Attempted to save invalid status monitor interval: ${interval}`);
throw new Error('Invalid interval value provided. Must be a positive integer.');
}
try {
const intervalStr = String(interval);
console.log(`[Service] Attempting to save setting. Key: ${STATUS_MONITOR_INTERVAL_SECONDS_KEY}, Value: ${intervalStr}`);
await settingsRepository.setSetting(STATUS_MONITOR_INTERVAL_SECONDS_KEY, intervalStr);
console.log(`[Service] Successfully saved setting for key: ${STATUS_MONITOR_INTERVAL_SECONDS_KEY}`);
} catch (error) {
console.error(`[Service] Error calling settingsRepository.setSetting for key ${STATUS_MONITOR_INTERVAL_SECONDS_KEY}:`, error);
throw new Error('Failed to save status monitor interval setting.');
}
} // *** 最后的方法后面不需要逗号 ***
};
@@ -1,6 +1,7 @@
import { Client } from 'ssh2';
import { WebSocket } from 'ws';
import { ClientState } from '../websocket'; // 导入统一的 ClientState
import { settingsService } from './settings.service'; // +++ 导入 settingsService +++
// 定义服务器状态的数据结构 (与前端 StatusMonitor.vue 匹配)
interface ServerStatus {
@@ -31,7 +32,7 @@ interface NetworkStats {
}
}
const DEFAULT_POLLING_INTERVAL = 1000; // 修改为 1 秒轮询间隔 (毫秒)
// const DEFAULT_POLLING_INTERVAL = 3000; // --- 移除常量,将从 settingsService 获取 ---
// 用于存储上一次的网络统计信息以计算速率
const previousNetStats = new Map<string, { rx: number, tx: number, timestamp: number }>();
@@ -45,9 +46,9 @@ export class StatusMonitorService {
/**
* 启动指定会话的状态轮询
* @param sessionId 会话 ID
* @param interval 轮询间隔 (毫秒),可选,默认为 DEFAULT_POLLING_INTERVAL
* @param interval 轮询间隔 (毫秒),可选,默认为 DEFAULT_POLLING_INTERVAL // --- 参数移除 ---
*/
startStatusPolling(sessionId: string, interval: number = DEFAULT_POLLING_INTERVAL): void {
async startStatusPolling(sessionId: string): Promise<void> { // --- 改为 async, 移除 interval 参数 ---
const state = this.clientStates.get(sessionId);
if (!state || !state.sshClient) {
//console.warn(`[StatusMonitor] 无法为会话 ${sessionId} 启动状态轮询:状态无效或 SSH 客户端不存在。`);
@@ -57,11 +58,23 @@ export class StatusMonitorService {
//console.warn(`[StatusMonitor] 会话 ${sessionId} 的状态轮询已在运行中。`);
return;
}
//console.warn(`[StatusMonitor] 为会话 ${sessionId} 启动状态轮询,间隔 ${interval}ms`);
// +++ 从 settingsService 获取轮询间隔 +++
let intervalMs: number;
try {
const intervalSeconds = await settingsService.getStatusMonitorIntervalSeconds();
intervalMs = intervalSeconds * 1000;
console.log(`[StatusMonitor ${sessionId}] 使用配置的轮询间隔: ${intervalSeconds} 秒 (${intervalMs}ms)`);
} catch (error) {
console.error(`[StatusMonitor ${sessionId}] 获取轮询间隔设置失败,将使用默认值 3000ms:`, error);
intervalMs = 3000; // 出错时回退到 3 秒
}
//console.warn(`[StatusMonitor] 为会话 ${sessionId} 启动状态轮询,间隔 ${intervalMs}ms`);
// 移除立即执行,让 setInterval 负责第一次调用,给连接更多准备时间
state.statusIntervalId = setInterval(() => {
this.fetchAndSendServerStatus(sessionId);
}, interval);
}, intervalMs); // --- 使用获取到的间隔 ---
}
/**
@@ -94,7 +107,8 @@ export class StatusMonitorService {
const status = await this.fetchServerStatus(state.sshClient, sessionId);
state.ws.send(JSON.stringify({ type: 'status_update', payload: { connectionId: state.dbConnectionId, status } }));
} catch (error: any) {
//console.warn(`[StatusMonitor] 获取会话 ${sessionId} 服务器状态失败:`, error);
// --- 移除 console.warn ---
// console.warn(`[StatusMonitor] 获取会话 ${sessionId} 服务器状态失败:`, error);
state.ws.send(JSON.stringify({ type: 'status_error', payload: { connectionId: state.dbConnectionId, message: `获取状态失败: ${error.message}` } }));
}
}
@@ -116,7 +130,7 @@ export class StatusMonitorService {
const osReleaseOutput = await this.executeSshCommand(sshClient, 'cat /etc/os-release');
const nameMatch = osReleaseOutput.match(/^PRETTY_NAME="?([^"]+)"?/m);
status.osName = nameMatch ? nameMatch[1] : (osReleaseOutput.match(/^NAME="?([^"]+)"?/m)?.[1] ?? 'Unknown');
} catch (err) { console.warn(`[StatusMonitor ${sessionId}] Failed to get OS name:`, err); }
} catch (err) { /* 静默处理 */ } // --- 移除 console.warn ---
// --- CPU Model (Try /proc/cpuinfo first, fallback to lscpu) ---
try {
@@ -126,13 +140,13 @@ export class StatusMonitorService {
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);
// console.warn(`[StatusMonitor ${sessionId}] Failed to get CPU model from /proc/cpuinfo, trying lscpu...`, procErr); // --- 移除 console.warn ---
// 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);
// console.warn(`[StatusMonitor ${sessionId}] Failed to get CPU model from lscpu as well:`, lscpuErr); // --- 移除 console.warn ---
}
}
// If still no model found after both attempts
@@ -140,7 +154,7 @@ export class StatusMonitorService {
status.cpuModel = 'Unknown';
}
} catch (err) { // Catch any unexpected error during the process
console.warn(`[StatusMonitor ${sessionId}] Error getting CPU model:`, err);
// console.warn(`[StatusMonitor ${sessionId}] Error getting CPU model:`, err); // --- 移除 console.warn ---
status.cpuModel = 'Unknown';
}
@@ -172,7 +186,7 @@ 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); }
} catch (err) { /* 静默处理 */ } // --- 移除 console.warn ---
// --- Disk Usage (Root Partition, POSIX format for compatibility) ---
try {
@@ -193,7 +207,7 @@ export class StatusMonitorService {
}
}
}
} catch (err) { console.warn(`[StatusMonitor ${sessionId}] Failed to get disk usage:`, err); }
} catch (err) { /* 静默处理 */ } // --- 移除 console.warn ---
// --- CPU Usage (Simplified from top) ---
try {
@@ -203,14 +217,14 @@ export class StatusMonitorService {
const idlePercent = parseFloat(idleMatch[1]);
status.cpuPercent = parseFloat((100 - idlePercent).toFixed(1));
}
} catch (err) { console.warn(`[StatusMonitor ${sessionId}] Failed to get CPU usage from top:`, err); }
} catch (err) { /* 静默处理 */ } // --- 移除 console.warn ---
// --- Load Average ---
try {
const uptimeOutput = await this.executeSshCommand(sshClient, 'uptime');
const match = uptimeOutput.match(/load average(?:s)?:\s*([\d.]+)[, ]?\s*([\d.]+)[, ]?\s*([\d.]+)/);
if (match) status.loadAvg = [parseFloat(match[1]), parseFloat(match[2]), parseFloat(match[3])];
} catch (err) { console.warn(`[StatusMonitor ${sessionId}] Failed to get uptime/load average:`, err); }
} catch (err) { /* 静默处理 */ } // --- 移除 console.warn ---
// --- Network Rates ---
try {
@@ -233,9 +247,9 @@ export class StatusMonitorService {
} else { status.netRxRate = 0; status.netTxRate = 0; } // First run or no time diff
previousNetStats.set(sessionId, { rx: currentRx, tx: currentTx, timestamp });
} else { console.warn(`[StatusMonitor ${sessionId}] Could not find stats for default interface ${defaultInterface}`); }
} else { /* 静默处理 */ } // --- 移除 console.warn ---
}
} catch (err) { console.warn(`[StatusMonitor ${sessionId}] Failed to get network stats:`, err); }
} catch (err) { /* 静默处理 */ } // --- 移除 console.warn ---
} catch (error) {
console.error(`[StatusMonitor ${sessionId}] General error fetching server status:`, error);
@@ -256,7 +270,7 @@ export class StatusMonitorService {
output = await this.executeSshCommand(sshClient, 'cat /proc/net/dev');
} catch (error) {
// 如果命令失败,记录警告并返回 null
console.warn("[StatusMonitor] Failed to execute 'cat /proc/net/dev':", error);
// console.warn("[StatusMonitor] Failed to execute 'cat /proc/net/dev':", error); // --- 移除 console.warn ---
return null;
}
// 如果命令成功,继续解析
@@ -276,7 +290,7 @@ export class StatusMonitorService {
return Object.keys(stats).length > 0 ? stats : null;
} catch (parseError) {
// 如果解析失败,记录错误并返回 null
console.error("[StatusMonitor] Error parsing /proc/net/dev output:", parseError);
// console.error("[StatusMonitor] Error parsing /proc/net/dev output:", parseError); // --- 移除 console.error ---
return null;
}
}
@@ -293,10 +307,10 @@ export class StatusMonitorService {
const interfaceName = output.trim();
if (interfaceName) return interfaceName;
// 如果 ip route 没返回有效接口名,也尝试 fallback
console.warn("[StatusMonitor] 'ip route' did not return a valid interface name. Falling back...");
// console.warn("[StatusMonitor] 'ip route' did not return a valid interface name. Falling back..."); // --- 移除 console.warn ---
} catch (error) {
console.warn("[StatusMonitor] Failed to get default interface using 'ip route', falling back:", error);
// console.warn("[StatusMonitor] Failed to get default interface using 'ip route', falling back:", error); // --- 移除 console.warn ---
// Fallback: 尝试查找第一个非 lo 接口
try {
const netDevOutput = await this.executeSshCommand(sshClient, 'cat /proc/net/dev');
@@ -308,7 +322,7 @@ export class StatusMonitorService {
}
}
} catch (fallbackError) {
console.error("[StatusMonitor] Failed to fallback to /proc/net/dev for interface:", fallbackError);
// console.error("[StatusMonitor] Failed to fallback to /proc/net/dev for interface:", fallbackError); // --- 移除 console.error ---
}
// Ensure null is returned if both primary and fallback fail within the outer catch
return null;
@@ -341,7 +355,8 @@ export class StatusMonitorService {
}).on('data', (data: Buffer) => {
output += data.toString('utf8');
}).stderr.on('data', (data: Buffer) => {
//console.warn(`[StatusMonitor] Command '${command}' stderr: ${data.toString('utf8').trim()}`);
// --- 移除 console.warn ---
// console.warn(`[StatusMonitor] Command '${command}' stderr: ${data.toString('utf8').trim()}`);
});
});
});
@@ -33,7 +33,8 @@ export const settingsController = {
const allowedSettingsKeys = [
'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration',
'showPopupFileEditor', 'shareFileEditorTabs', 'ipWhitelistEnabled',
'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand' // +++ 添加 Docker 设置键 +++
'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand',
'statusMonitorIntervalSeconds' // +++ 添加状态监控间隔键 +++
];
const filteredSettings: Record<string, string> = {};
for (const key in settingsToUpdate) {
+13
View File
@@ -643,6 +643,19 @@
"saveFailed": "Failed to save Docker settings.",
"invalidInterval": "Refresh interval must be a positive integer."
}
},
"statusMonitor": {
"title": "Status Monitor Settings",
"refreshIntervalLabel": "Status Refresh Interval (seconds):",
"refreshIntervalHint": "How often to fetch server CPU, memory, disk, etc. status (minimum 1).",
"saveButton": "Save Status Monitor Settings",
"success": {
"saved": "Status monitor settings saved successfully."
},
"error": {
"saveFailed": "Failed to save status monitor settings.",
"invalidInterval": "Refresh interval must be a positive integer."
}
}
},
"common": {
+13
View File
@@ -643,6 +643,19 @@
"saveFailed": "保存 Docker 设置失败。",
"invalidInterval": "刷新间隔必须是正整数。"
}
},
"statusMonitor": {
"title": "状态监控设置",
"refreshIntervalLabel": "状态刷新间隔 (秒):",
"refreshIntervalHint": "获取服务器 CPU、内存、磁盘等状态的频率(最小为 1)。",
"saveButton": "保存状态监控设置",
"success": {
"saved": "状态监控设置已成功保存。"
},
"error": {
"saveFailed": "保存状态监控设置失败。",
"invalidInterval": "刷新间隔必须是正整数。"
}
}
},
"common": {
+16 -3
View File
@@ -16,6 +16,7 @@ interface SettingsState {
autoCopyOnSelect?: string; // 'true' or 'false' - 终端选中自动复制
dockerStatusIntervalSeconds?: string; // NEW: Docker 状态刷新间隔 (秒)
dockerDefaultExpand?: string; // NEW: Docker 默认展开详情 'true' or 'false'
statusMonitorIntervalSeconds?: string; // NEW: 状态监控轮询间隔 (秒)
// Add other general settings keys here as needed
[key: string]: string | undefined; // Allow other string settings
}
@@ -74,7 +75,10 @@ export const useSettingsStore = defineStore('settings', () => {
if (settings.value.dockerDefaultExpand === undefined) {
settings.value.dockerDefaultExpand = 'false'; // 默认不展开
}
// NEW: Status Monitor interval default
if (settings.value.statusMonitorIntervalSeconds === undefined) {
settings.value.statusMonitorIntervalSeconds = '3'; // 默认 3 秒
}
// --- 语言设置 ---
const langFromSettings = settings.value.language;
@@ -124,7 +128,8 @@ export const useSettingsStore = defineStore('settings', () => {
const allowedKeys: Array<keyof SettingsState> = [
'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration',
'showPopupFileEditor', 'shareFileEditorTabs', 'ipWhitelistEnabled',
'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand' // +++ 添加 Docker 设置键 +++
'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand',
'statusMonitorIntervalSeconds' // +++ 添加状态监控间隔键 +++
];
if (!allowedKeys.includes(key)) {
console.error(`[SettingsStore] 尝试更新不允许的设置键: ${key}`);
@@ -157,7 +162,8 @@ export const useSettingsStore = defineStore('settings', () => {
const allowedKeys: Array<keyof SettingsState> = [
'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration',
'showPopupFileEditor', 'shareFileEditorTabs', 'ipWhitelistEnabled',
'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand' // +++ 添加 Docker 设置键 +++
'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand',
'statusMonitorIntervalSeconds' // +++ 添加状态监控间隔键 +++
];
const filteredUpdates: Partial<SettingsState> = {};
let languageUpdate: 'en' | 'zh' | undefined = undefined;
@@ -224,6 +230,12 @@ export const useSettingsStore = defineStore('settings', () => {
return settings.value.dockerDefaultExpand === 'true';
});
// NEW: Getter for Status Monitor interval, returning number
const statusMonitorIntervalSecondsNumber = computed(() => {
const val = parseInt(settings.value.statusMonitorIntervalSeconds || '3', 10);
return isNaN(val) || val <= 0 ? 3 : val; // Fallback to 3 if invalid
});
return {
settings, // 只包含通用设置
isLoading,
@@ -234,6 +246,7 @@ export const useSettingsStore = defineStore('settings', () => {
ipWhitelistEnabled, // 暴露 IP 白名单启用状态
autoCopyOnSelectBoolean,
dockerDefaultExpandBoolean, // +++ 暴露 Docker 默认展开 getter +++
statusMonitorIntervalSecondsNumber, // +++ 暴露状态监控间隔 getter +++
// 移除外观相关的 getters 和 actions
loadInitialSettings,
updateSetting,
+42 -1
View File
@@ -156,6 +156,21 @@
</div>
<!-- END: Docker Settings Section -->
<!-- NEW: Status Monitor Settings Section -->
<div class="settings-section">
<h2>{{ t('settings.statusMonitor.title') }}</h2>
<form @submit.prevent="handleUpdateStatusMonitorInterval">
<div class="form-group">
<label for="statusMonitorInterval">{{ t('settings.statusMonitor.refreshIntervalLabel') }}</label>
<input type="number" id="statusMonitorInterval" v-model.number="statusMonitorIntervalLocal" min="1" step="1" required>
<small>{{ t('settings.statusMonitor.refreshIntervalHint') }}</small>
</div>
<button type="submit" :disabled="statusMonitorLoading">{{ statusMonitorLoading ? $t('common.saving') : t('settings.statusMonitor.saveButton') }}</button>
<p v-if="statusMonitorMessage" :class="{ 'success-message': statusMonitorSuccess, 'error-message': !statusMonitorSuccess }">{{ statusMonitorMessage }}</p>
</form>
</div>
<!-- END: Status Monitor Settings Section -->
<div class="settings-section">
<h2>{{ $t('settings.ipWhitelist.title') }}</h2>
<p>{{ $t('settings.ipWhitelist.description') }}</p>
@@ -252,7 +267,7 @@ const { t } = useI18n();
// --- Reactive state from store ---
// 使 storeToRefs getter language
const { settings, isLoading: settingsLoading, error: settingsError, showPopupFileEditorBoolean, shareFileEditorTabsBoolean, autoCopyOnSelectBoolean, dockerDefaultExpandBoolean, language: storeLanguage } = storeToRefs(settingsStore); // +++ dockerDefaultExpandBoolean language getter +++
const { settings, isLoading: settingsLoading, error: settingsError, showPopupFileEditorBoolean, shareFileEditorTabsBoolean, autoCopyOnSelectBoolean, dockerDefaultExpandBoolean, statusMonitorIntervalSecondsNumber, language: storeLanguage } = storeToRefs(settingsStore); // +++ statusMonitorIntervalSecondsNumber getter +++
// --- Local state for forms ---
const ipWhitelistInput = ref('');
@@ -290,6 +305,10 @@ const dockerExpandDefault = ref(false); // 本地状态,用于 Docker 默认
const dockerSettingsLoading = ref(false);
const dockerSettingsMessage = ref('');
const dockerSettingsSuccess = ref(false);
const statusMonitorIntervalLocal = ref(3); // v-model
const statusMonitorLoading = ref(false);
const statusMonitorMessage = ref('');
const statusMonitorSuccess = ref(false);
// --- Watcher to sync local form state with store state ---
@@ -308,6 +327,7 @@ watch(settings, (newSettings, oldSettings) => {
autoCopyEnabled.value = autoCopyOnSelectBoolean.value; //
dockerInterval.value = parseInt(newSettings.dockerStatusIntervalSeconds || '2', 10); // Docker
dockerExpandDefault.value = dockerDefaultExpandBoolean.value; // Docker
statusMonitorIntervalLocal.value = statusMonitorIntervalSecondsNumber.value; //
}, { deep: true, immediate: true }); // immediate: true to run on initial load
@@ -398,6 +418,27 @@ const handleUpdateDockerSettings = async () => {
}
};
// --- Status Monitor interval setting method ---
const handleUpdateStatusMonitorInterval = async () => {
statusMonitorLoading.value = true;
statusMonitorMessage.value = '';
statusMonitorSuccess.value = false;
try {
const intervalValue = statusMonitorIntervalLocal.value;
if (isNaN(intervalValue) || intervalValue < 1 || !Number.isInteger(intervalValue)) {
throw new Error(t('settings.statusMonitor.error.invalidInterval')); //
}
await settingsStore.updateSetting('statusMonitorIntervalSeconds', String(intervalValue)); //
statusMonitorMessage.value = t('settings.statusMonitor.success.saved'); //
statusMonitorSuccess.value = true;
} catch (error: any) {
console.error('更新状态监控间隔失败:', error);
statusMonitorMessage.value = error.message || t('settings.statusMonitor.error.saveFailed'); //
statusMonitorSuccess.value = false;
} finally {
statusMonitorLoading.value = false;
}
};
// --- ---
const openStyleCustomizer = () => {