feat: 新增设备巡检模块实现完整的巡检告警功能
新增设备巡检模块核心功能,包括: - 巡检数据存储与调度系统 - 华为/H3C交换机及Linux服务器采集器 - 设备扩展配置与告警规则管理 - 主机详情页巡检配置入口 - 产品名称自定义设置功能 - 网络设备SSH exec通道兼容处理
This commit is contained in:
127
electron/bridges/inspection/adapters/baseAdapter.cjs
Normal file
127
electron/bridges/inspection/adapters/baseAdapter.cjs
Normal file
@@ -0,0 +1,127 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* 采集器基类
|
||||
*
|
||||
* 所有设备采集器都继承此类,统一采集接口。
|
||||
* 采集器负责:
|
||||
* 1. 建立 SSH 连接
|
||||
* 2. 执行巡检命令
|
||||
* 3. 解析输出为结构化指标数据
|
||||
* 4. 关闭连接
|
||||
*
|
||||
* 子类需实现:
|
||||
* - getSupportedMetrics() 返回支持的指标列表
|
||||
* - collectMetrics() 执行所有采集命令并返回结构化指标
|
||||
*/
|
||||
|
||||
const { INSPECTION_STATUS } = require("../types.cjs");
|
||||
|
||||
class BaseAdapter {
|
||||
/**
|
||||
* @param {object} connectionInfo - SSH 连接信息
|
||||
* @param {string} connectionInfo.host - 主机 IP
|
||||
* @param {number} connectionInfo.port - 端口
|
||||
* @param {string} connectionInfo.username - 用户名
|
||||
* @param {string} [connectionInfo.password] - 密码
|
||||
* @param {string} [connectionInfo.privateKey] - 私钥
|
||||
* @param {object} [options]
|
||||
* @param {number} [options.timeoutMs] - 单条命令超时(毫秒)
|
||||
*/
|
||||
constructor(connectionInfo, options = {}) {
|
||||
this.connectionInfo = connectionInfo;
|
||||
this.timeoutMs = options.timeoutMs || 15000;
|
||||
this.conn = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立连接
|
||||
* 子类可以覆盖此方法实现不同的连接方式(如 Telnet)
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async connect() {
|
||||
// 基类不实现具体连接逻辑,由上层调度器负责 SSH 连接
|
||||
// 或者由子类自行实现
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行单条命令
|
||||
* 由子类或注入的 exec 函数实现
|
||||
* @param {string} command
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async execute(command) {
|
||||
throw new Error("execute() must be implemented by subclass or injected");
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭连接
|
||||
*/
|
||||
async disconnect() {
|
||||
// 子类可覆盖
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支持的指标列表
|
||||
* @returns {string[]}
|
||||
*/
|
||||
getSupportedMetrics() {
|
||||
throw new Error("getSupportedMetrics() must be implemented by subclass");
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行完整巡检,返回结构化指标数据
|
||||
* @returns {Promise<object>}
|
||||
* {
|
||||
* online: boolean,
|
||||
* metrics: { ... },
|
||||
* raw: { ... } // 原始输出(调试用)
|
||||
* }
|
||||
*/
|
||||
async collect() {
|
||||
const result = {
|
||||
online: false,
|
||||
metrics: {},
|
||||
raw: {},
|
||||
error: null,
|
||||
status: INSPECTION_STATUS.SUCCESS,
|
||||
};
|
||||
|
||||
try {
|
||||
const metrics = await this.collectMetrics();
|
||||
result.online = true;
|
||||
result.metrics = metrics;
|
||||
} catch (err) {
|
||||
result.error = err.message;
|
||||
result.status = err.message.includes("timeout")
|
||||
? INSPECTION_STATUS.TIMEOUT
|
||||
: INSPECTION_STATUS.FAILED;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 子类实现:具体的采集逻辑
|
||||
* @returns {Promise<object>} 结构化指标数据
|
||||
*/
|
||||
async collectMetrics() {
|
||||
throw new Error("collectMetrics() must be implemented by subclass");
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全地从字符串中提取数字
|
||||
* @param {string} text
|
||||
* @param {RegExp} regex
|
||||
* @returns {number|null}
|
||||
*/
|
||||
extractNumber(text, regex) {
|
||||
const match = (text || "").match(regex);
|
||||
if (!match) return null;
|
||||
const num = parseFloat(match[1]);
|
||||
return isNaN(num) ? null : num;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseAdapter;
|
||||
174
electron/bridges/inspection/adapters/huaweiH3CSwitchAdapter.cjs
Normal file
174
electron/bridges/inspection/adapters/huaweiH3CSwitchAdapter.cjs
Normal file
@@ -0,0 +1,174 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* 华为 / H3C 交换机采集器
|
||||
*
|
||||
* 采集指标:
|
||||
* - cpu_usage CPU 使用率(%)
|
||||
* - memory_usage 内存使用率(%)
|
||||
* - port_status 端口状态 { up_count, down_count }
|
||||
* - power_status 电源状态 { normal_count, abnormal_count }
|
||||
* - fan_status 风扇状态
|
||||
*
|
||||
* 实现说明:
|
||||
* 使用批量命令执行,一次连接执行所有命令,避免 VTY 超时
|
||||
*/
|
||||
|
||||
const BaseAdapter = require("./baseAdapter.cjs");
|
||||
|
||||
class HuaweiH3CSwitchAdapter extends BaseAdapter {
|
||||
constructor(connectionInfo, options = {}) {
|
||||
super(connectionInfo, options);
|
||||
this.adapterName = "huawei_h3c_switch";
|
||||
if (options.execCommands) {
|
||||
this.execCommands = options.execCommands;
|
||||
}
|
||||
}
|
||||
|
||||
async execCommands(commands, options = {}) {
|
||||
const { execSshCommands } = require("../inspectionExec.cjs");
|
||||
return await execSshCommands(this.connectionInfo, commands, options);
|
||||
}
|
||||
|
||||
getSupportedMetrics() {
|
||||
return [
|
||||
"cpu_usage",
|
||||
"memory_usage",
|
||||
"port_status",
|
||||
"power_status",
|
||||
"fan_status",
|
||||
];
|
||||
}
|
||||
|
||||
async collectMetrics() {
|
||||
const metrics = {};
|
||||
const DEBUG = process.env.INSPECTION_DEBUG === "1";
|
||||
|
||||
if (DEBUG) console.log("[HuaweiH3C] 开始采集交换机指标");
|
||||
|
||||
const commands = [
|
||||
"display cpu-usage",
|
||||
"display memory",
|
||||
"display interface brief",
|
||||
"display power",
|
||||
"display fan",
|
||||
];
|
||||
|
||||
try {
|
||||
const { results } = await this.execCommands(commands, { timeoutMs: 180000 });
|
||||
|
||||
if (DEBUG) {
|
||||
console.log("[HuaweiH3C] 原始输出:");
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
// 简化日志,不输出完整原始数据
|
||||
const out = results[i]?.output || "";
|
||||
console.log(`[HuaweiH3C] 命令${i + 1}: ${commands[i]} → ${out.length} 字符`);
|
||||
}
|
||||
}
|
||||
|
||||
if (results && results.length >= 5) {
|
||||
metrics.cpu_usage = this.parseCpuUsage(results[0]?.output || "");
|
||||
metrics.memory_usage = this.parseMemoryUsage(results[1]?.output || "");
|
||||
metrics.port_status = this.parsePorts(results[2]?.output || "");
|
||||
metrics.power_status = this.parsePower(results[3]?.output || "");
|
||||
metrics.fan_status = this.parseFan(results[4]?.output || "");
|
||||
}
|
||||
|
||||
if (DEBUG) console.log("[HuaweiH3C] Final:", JSON.stringify(metrics));
|
||||
} catch (e) {
|
||||
if (DEBUG) console.log("[HuaweiH3C] Error:", e.message);
|
||||
throw e;
|
||||
}
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
parseCpuUsage(output) {
|
||||
// H3C Comware 5/7: "7% in last 5 seconds"
|
||||
let m = output.match(/(\d+)\s*%\s*in\s+last\s+5\s+seconds/i);
|
||||
if (m) return parseFloat(m[1]);
|
||||
// 1 minute
|
||||
m = output.match(/(\d+)\s*%\s*in\s+last\s+1\s+minute/i);
|
||||
if (m) return parseFloat(m[1]);
|
||||
// 华为格式
|
||||
m = output.match(/CPU\s+Usage\s*:\s*(\d+)%/i);
|
||||
if (m) return parseFloat(m[1]);
|
||||
// 5 Seconds
|
||||
m = output.match(/5\s*Seconds\s*[::]\s*(\d+)\s*%/i);
|
||||
if (m) return parseFloat(m[1]);
|
||||
return null;
|
||||
}
|
||||
|
||||
parseMemoryUsage(output) {
|
||||
// H3C: "Used Rate: 45%"
|
||||
let m = output.match(/Used\s*Rate\s*[::]?\s*(\d+(?:\.\d+)?)\s*%/i);
|
||||
if (m) return parseFloat(m[1]);
|
||||
// 华为/其他: "Memory Using Percentage Is: 21%"
|
||||
m = output.match(/Memory\s+Using\s+Percentage\s+Is\s*[::]?\s*(\d+(?:\.\d+)?)\s*%/i);
|
||||
if (m) return parseFloat(m[1]);
|
||||
// H3C bytes 格式: "System Total Memory(bytes): 65546160" / "Total Used Memory(bytes): 29528544"
|
||||
const totalM = output.match(/System\s+Total\s+Memory\s*\(\s*bytes\s*\)\s*[::]?\s*(\d+)/i);
|
||||
const usedM = output.match(/Total\s+Used\s+Memory\s*\(\s*bytes\s*\)\s*[::]?\s*(\d+)/i);
|
||||
if (totalM && usedM) {
|
||||
const total = parseFloat(totalM[1]);
|
||||
const used = parseFloat(usedM[1]);
|
||||
if (total > 0) return Math.round((used / total) * 100 * 10) / 10;
|
||||
}
|
||||
// MB 格式: "Memory Size: xxx MB"
|
||||
const totalMB = output.match(/Memory\s*(?:Size|Total)\s*[::]?\s*(\d+)\s*MB/i);
|
||||
const usedMB = output.match(/Memory\s*Used\s*[::]?\s*(\d+)\s*MB/i);
|
||||
if (totalMB && usedMB) {
|
||||
const total = parseFloat(totalMB[1]);
|
||||
const used = parseFloat(usedMB[1]);
|
||||
if (total > 0) return Math.round((used / total) * 100 * 10) / 10;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
parsePorts(output) {
|
||||
const lines = output.split("\n");
|
||||
let upCount = 0, downCount = 0;
|
||||
const ports = [];
|
||||
for (const line of lines) {
|
||||
// H3C 格式: "GE1/0/1 UP 100M(a)"
|
||||
// 匹配 GE开头 + UP/DOWN
|
||||
const geMatch = line.match(/^(GE\d+\/\d+\/\d+)\s+(UP|DOWN|up|down)/i);
|
||||
if (geMatch) {
|
||||
const phy = geMatch[2].toLowerCase();
|
||||
ports.push({ name: geMatch[1], phy });
|
||||
if (phy === "up") upCount++;
|
||||
else downCount++;
|
||||
continue;
|
||||
}
|
||||
// 匹配 GigabitEthernet 全称
|
||||
const gigMatch = line.match(/^(GigabitEthernet\d+[\d\/]+)\s+(UP|DOWN|up|down)/i);
|
||||
if (gigMatch) {
|
||||
const phy = gigMatch[2].toLowerCase();
|
||||
ports.push({ name: gigMatch[1], phy });
|
||||
if (phy === "up") upCount++;
|
||||
else downCount++;
|
||||
}
|
||||
}
|
||||
return { up_count: upCount, down_count: downCount, ports };
|
||||
}
|
||||
|
||||
parsePower(output) {
|
||||
let normal = 0, abnormal = 0;
|
||||
for (const line of output.split("\n")) {
|
||||
if (/Normal|Supply|OK/i.test(line)) normal++;
|
||||
else if (/Absent|Fault|Error/i.test(line)) abnormal++;
|
||||
}
|
||||
return { normal_count: normal, abnormal_count: abnormal };
|
||||
}
|
||||
|
||||
parseFan(output) {
|
||||
let normal = 0, abnormal = 0;
|
||||
for (const line of output.split("\n")) {
|
||||
if (/Normal|Auto|Running|OK/i.test(line)) normal++;
|
||||
else if (/Fault|Absent|Error/i.test(line)) abnormal++;
|
||||
}
|
||||
return { normal_count: normal, abnormal_count: abnormal };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HuaweiH3CSwitchAdapter;
|
||||
102
electron/bridges/inspection/adapters/linuxServerAdapter.cjs
Normal file
102
electron/bridges/inspection/adapters/linuxServerAdapter.cjs
Normal file
@@ -0,0 +1,102 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Linux 服务器采集器
|
||||
*
|
||||
* 采集指标(告警相关):
|
||||
* - cpu_usage CPU 使用率(%)
|
||||
* - memory_usage 内存使用率(%)
|
||||
* - disk_usage 磁盘使用率(%,只显示超过 80% 的挂载点)
|
||||
*
|
||||
* 实现说明:
|
||||
* 直接使用 POSIX shell 命令,命令与 NetMesh System Manager 的概览一致。
|
||||
* 传入 exec 函数来执行远程命令(由调度器注入,复用 SSH 连接池)。
|
||||
*/
|
||||
|
||||
const BaseAdapter = require("./baseAdapter.cjs");
|
||||
|
||||
class LinuxServerAdapter extends BaseAdapter {
|
||||
constructor(connectionInfo, options = {}) {
|
||||
super(connectionInfo, options);
|
||||
this.adapterName = "linux_server";
|
||||
// 支持外部注入 exec 函数(用于复用 SSH 连接)
|
||||
if (options.execFn) {
|
||||
this.execute = options.execFn;
|
||||
}
|
||||
}
|
||||
|
||||
getSupportedMetrics() {
|
||||
return [
|
||||
"cpu_usage",
|
||||
"memory_usage",
|
||||
"disk_usage",
|
||||
];
|
||||
}
|
||||
|
||||
async collectMetrics() {
|
||||
const metrics = {};
|
||||
const raw = {};
|
||||
|
||||
// 1. CPU 使用率(采样 top)
|
||||
try {
|
||||
raw.cpu = await this.execute("top -bn1 2>/dev/null | head -n 5");
|
||||
const cpuMatch = (raw.cpu || "").match(/Cpu\(s\):\s*([\d.]+)\s*us,\s*([\d.]+)\s*sy/i);
|
||||
if (cpuMatch) {
|
||||
metrics.cpu_usage = Math.round(parseFloat(cpuMatch[1]) + parseFloat(cpuMatch[2]));
|
||||
}
|
||||
} catch (e) {
|
||||
throw e; // SSH 失败或其他错误必须向上传播
|
||||
}
|
||||
|
||||
// 2. 内存使用率
|
||||
try {
|
||||
raw.mem = await this.execute("free -k 2>/dev/null | head -n 2 | tail -n 1");
|
||||
// 支持中英文输出: "Mem:" 或 "内存:"
|
||||
const memMatch = (raw.mem || "").match(/(?:Mem:|内存[::])\s+(\d+)\s+(\d+)/);
|
||||
if (memMatch) {
|
||||
const total = parseInt(memMatch[1], 10);
|
||||
const used = parseInt(memMatch[2], 10);
|
||||
if (total > 0) {
|
||||
metrics.memory_usage = Math.min(100, Math.round((used / total) * 100));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
// 3. 磁盘使用率(显示所有挂载点)
|
||||
try {
|
||||
raw.disk = await this.execute("df -h 2>/dev/null | tail -n +2 | grep -v '/dev/loop'");
|
||||
const disks = this.parseDiskUsage(raw.disk);
|
||||
if (disks.length > 0) metrics.disk_usage = disks;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析磁盘使用率(df -h 输出格式),返回所有挂载点
|
||||
*/
|
||||
parseDiskUsage(text) {
|
||||
const disks = [];
|
||||
const lines = (text || "").split("\n");
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length < 6) continue;
|
||||
const usageStr = parts[4] || "";
|
||||
const usageMatch = usageStr.match(/^(\d+)%/);
|
||||
if (!usageMatch) continue;
|
||||
const usagePercent = parseInt(usageMatch[1], 10);
|
||||
disks.push({
|
||||
filesystem: parts[0],
|
||||
usage_percent: usagePercent,
|
||||
mount: parts[parts.length - 1],
|
||||
});
|
||||
}
|
||||
return disks;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LinuxServerAdapter;
|
||||
237
electron/bridges/inspection/adapters/windowsServerAdapter.cjs
Normal file
237
electron/bridges/inspection/adapters/windowsServerAdapter.cjs
Normal file
@@ -0,0 +1,237 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Windows 服务器采集器
|
||||
*
|
||||
* 采集指标:
|
||||
* - cpu_usage CPU 使用率(%)
|
||||
* - memory_usage 内存使用率(%)
|
||||
* - disk_usage 磁盘使用率(%)
|
||||
* - process_count 进程数
|
||||
* - uptime 运行时间
|
||||
* - service_status 关键服务状态
|
||||
*
|
||||
* 实现说明:
|
||||
* 使用 PowerShell Base64 EncodedCommand 方式执行,与 NetMesh System Manager 一致。
|
||||
* 传入 exec 函数执行远程命令(由调度器注入)。
|
||||
*/
|
||||
|
||||
const BaseAdapter = require("./baseAdapter.cjs");
|
||||
|
||||
class WindowsServerAdapter extends BaseAdapter {
|
||||
constructor(connectionInfo, options = {}) {
|
||||
super(connectionInfo, options);
|
||||
this.adapterName = "windows_server";
|
||||
if (options.execFn) {
|
||||
this.execute = options.execFn;
|
||||
}
|
||||
}
|
||||
|
||||
getSupportedMetrics() {
|
||||
return [
|
||||
"cpu_usage",
|
||||
"memory_usage",
|
||||
"disk_usage",
|
||||
"process_count",
|
||||
"uptime",
|
||||
"service_status",
|
||||
];
|
||||
}
|
||||
|
||||
async collectMetrics() {
|
||||
const metrics = {};
|
||||
const raw = {};
|
||||
const DEBUG = process.env.INSPECTION_DEBUG === "1";
|
||||
|
||||
// 1. CPU + 内存 + 运行时间(快速,独立执行)
|
||||
try {
|
||||
const script = this._buildFastStatsScript();
|
||||
const cmd = this._buildPowerShellCmd(script);
|
||||
if (DEBUG) console.log("[WinAdapter] 执行命令:", cmd);
|
||||
raw.stats = await this.execute(cmd);
|
||||
if (DEBUG) console.log("[WinAdapter] 原始输出:", raw.stats);
|
||||
const parsed = this._parseStats(raw.stats);
|
||||
if (DEBUG) console.log("[WinAdapter] 解析结果:", parsed);
|
||||
Object.assign(metrics, parsed);
|
||||
} catch (e) {
|
||||
raw.stats_error = e.message;
|
||||
if (DEBUG) console.error("[WinAdapter] 采集失败:", e.message);
|
||||
}
|
||||
|
||||
// 2. 磁盘使用率(较慢,单独执行,超时放宽到 30s)
|
||||
try {
|
||||
const diskScript = this._buildDiskScript();
|
||||
const diskCmd = this._buildPowerShellCmd(diskScript);
|
||||
if (DEBUG) console.log("[WinAdapter] 磁盘命令:", diskCmd);
|
||||
raw.disk = await this.execute(diskCmd);
|
||||
if (DEBUG) console.log("[WinAdapter] 磁盘原始输出:", raw.disk);
|
||||
metrics.disk_usage = this._parseDisk(raw.disk);
|
||||
} catch (e) {
|
||||
raw.disk_error = e.message;
|
||||
if (DEBUG) console.error("[WinAdapter] 磁盘采集失败:", e.message);
|
||||
}
|
||||
|
||||
// 3. 关键服务状态
|
||||
try {
|
||||
const serviceScript = this._buildServiceScript();
|
||||
const cmd = this._buildPowerShellCmd(serviceScript);
|
||||
if (DEBUG) console.log("[WinAdapter] 服务命令:", cmd);
|
||||
raw.services = await this.execute(cmd);
|
||||
if (DEBUG) console.log("[WinAdapter] 服务原始输出:", raw.services);
|
||||
metrics.service_status = this._parseServices(raw.services);
|
||||
} catch (e) {
|
||||
metrics.service_status = [];
|
||||
raw.services_error = e.message;
|
||||
if (DEBUG) console.error("[WinAdapter] 服务采集失败:", e.message);
|
||||
}
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速统计:CPU + 内存(使用更快的 WMI 查询)
|
||||
*/
|
||||
_buildFastStatsScript() {
|
||||
return `
|
||||
try { $cpu = (Get-WmiObject Win32_Processor).LoadPercentage; if (-not $cpu) { $cpu = 0 } } catch { $cpu = 0 }
|
||||
try { $os = Get-WmiObject Win32_OperatingSystem; $total = [math]::Round($os.TotalVisibleMemorySize/1MB,0); $free = [math]::Round($os.FreePhysicalMemory/1MB,0); $usage = if ($total -gt 0) { [math]::Round(($total-$free)/$total*100,1) } else { 0 } } catch { $total = 0; $usage = 0; $free = 0 }
|
||||
@{ cpu = $cpu; mem_usage = $usage; mem_total = $total; mem_free = $free } | ConvertTo-Json -Compress
|
||||
`.trim();
|
||||
}
|
||||
|
||||
/** 磁盘使用率(只查 C: 盘) */
|
||||
_buildDiskScript() {
|
||||
return `
|
||||
try {
|
||||
$disk = Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='C:'"
|
||||
if ($disk -and $disk.Size -gt 0) {
|
||||
$s = [math]::Round($disk.Size/1GB,1); $f = [math]::Round($disk.FreeSpace/1GB,1); $u = [math]::Round(($disk.Size-$disk.FreeSpace)/$disk.Size*100,1)
|
||||
@{ d='C:'; s=$s; f=$f; u=$u } | ConvertTo-Json -Compress
|
||||
} else { 'null' }
|
||||
} catch { 'null' }
|
||||
`.trim();
|
||||
}
|
||||
|
||||
_parseDisk(stdout) {
|
||||
try {
|
||||
const json = this._extractJson(stdout);
|
||||
if (!json || json === "null" || json === null) return [];
|
||||
return [{
|
||||
drive: json.d || "C:",
|
||||
size_gb: json.s || 0,
|
||||
free_gb: json.f || 0,
|
||||
usage_percent: json.u || 0,
|
||||
}];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 PowerShell 命令:UTF-16LE Base64 编码 + -EncodedCommand
|
||||
* Node.js Buffer.from(str, "utf16le") 是最可靠的编码方式
|
||||
*/
|
||||
_buildPowerShellCmd(script) {
|
||||
// 用 Node.js 原生 Buffer 进行 UTF-16LE 编码,再转 Base64
|
||||
// UTF-16LE 编码(不带 BOM),然后 Base64
|
||||
const utf16le = Buffer.from(script, "utf16le");
|
||||
const encoded = utf16le.toString("base64");
|
||||
return `powershell -NoProfile -NonInteractive -EncodedCommand ${encoded}`;
|
||||
}
|
||||
|
||||
_buildServiceScript() {
|
||||
return `
|
||||
$ErrorActionPreference = 'SilentlyContinue'
|
||||
|
||||
# 关键服务
|
||||
$services = @(
|
||||
"wuauserv", # Windows Update
|
||||
"Spooler", # Print Spooler
|
||||
"TermService", # Remote Desktop
|
||||
"DHCP", # DHCP Client
|
||||
"DNScache" # DNS Client
|
||||
)
|
||||
|
||||
$results = foreach ($svc in $services) {
|
||||
$s = Get-Service -Name $svc -ErrorAction SilentlyContinue
|
||||
if ($s) {
|
||||
[PSCustomObject]@{
|
||||
name = $s.Name
|
||||
display_name = $s.DisplayName
|
||||
status = $s.Status.ToString()
|
||||
start_type = $s.StartType.ToString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$results | ConvertTo-Json -Depth 3
|
||||
`.trim();
|
||||
}
|
||||
|
||||
_parseStats(stdout) {
|
||||
try {
|
||||
const json = this._extractJson(stdout);
|
||||
if (!json) return {};
|
||||
|
||||
const result = {};
|
||||
if (json.cpu !== undefined) result.cpu_usage = json.cpu;
|
||||
if (json.mem_usage !== undefined) result.memory_usage = json.mem_usage;
|
||||
if (json.mem_total !== undefined && json.mem_total > 0) {
|
||||
result.memory = {
|
||||
total_mb: json.mem_total,
|
||||
used_mb: Math.round((json.mem_usage / 100) * json.mem_total),
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
_parseServices(stdout) {
|
||||
try {
|
||||
const json = this._extractJson(stdout);
|
||||
if (!json) return [];
|
||||
return Array.isArray(json) ? json : [json];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 PowerShell 输出中提取 JSON(可能有多余的警告/文本)
|
||||
*/
|
||||
_extractJson(text) {
|
||||
if (!text) return null;
|
||||
const start = text.indexOf("{");
|
||||
const startArr = text.indexOf("[");
|
||||
const realStart = startArr === -1 ? start : (start === -1 ? startArr : Math.min(start, startArr));
|
||||
if (realStart === -1) return null;
|
||||
|
||||
let depth = 0;
|
||||
let end = -1;
|
||||
const open = text[realStart];
|
||||
const close = open === "{" ? "}" : "]";
|
||||
|
||||
for (let i = realStart; i < text.length; i++) {
|
||||
if (text[i] === open) depth++;
|
||||
else if (text[i] === close) {
|
||||
depth--;
|
||||
if (depth === 0) {
|
||||
end = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (end === -1) return null;
|
||||
try {
|
||||
return JSON.parse(text.substring(realStart, end + 1));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WindowsServerAdapter;
|
||||
Reference in New Issue
Block a user