feat: 新增设备巡检模块实现完整的巡检告警功能

新增设备巡检模块核心功能,包括:
- 巡检数据存储与调度系统
- 华为/H3C交换机及Linux服务器采集器
- 设备扩展配置与告警规则管理
- 主机详情页巡检配置入口
- 产品名称自定义设置功能
- 网络设备SSH exec通道兼容处理
This commit is contained in:
2026-10-09 10:37:48 +08:00
parent 9202506c46
commit c64e00d835
75 changed files with 12087 additions and 446 deletions

View File

@@ -0,0 +1,95 @@
"use strict";
/**
* 采集器注册表
*
* 自动注册 adapters 目录下的所有采集器,供调度器使用。
*
* 使用方式:
* const adapterRegistry = require("./adapterRegistry.cjs");
* const adapter = adapterRegistry.createAdapter("LinuxServerAdapter", connInfo, options);
*/
const fs = require("fs");
const path = require("path");
const BaseAdapter = require("./adapters/baseAdapter.cjs");
const ADAPTERS_DIR = path.join(__dirname, "adapters");
const registry = {};
/**
* 扫描 adapters 目录,自动注册所有采集器类
* (只注册已实现的文件,不存在的适配器不报错,留作后续扩展)
*/
function loadAdapters() {
if (!fs.existsSync(ADAPTERS_DIR)) return;
const files = fs.readdirSync(ADAPTERS_DIR);
for (const file of files) {
if (!file.endsWith(".cjs")) continue;
if (file === "baseAdapter.cjs") continue;
try {
const modulePath = path.join(ADAPTERS_DIR, file);
const AdapterClass = require(modulePath);
// 验证是否继承自 BaseAdapter
if (
typeof AdapterClass === "function" &&
AdapterClass.prototype instanceof BaseAdapter
) {
console.log(`[Inspection] 注册采集器: ${AdapterClass.name} (${file})`);
registry[AdapterClass.name] = AdapterClass;
} else {
console.warn(`[Inspection] ${file} 不是 BaseAdapter 子类,导出类型: ${typeof AdapterClass}`);
}
} catch (e) {
// 加载失败跳过(比如引用了不存在的模块)
console.error(`[Inspection] 加载采集器失败: ${file}`, e.message, e.stack);
}
}
}
// 启动时自动加载
loadAdapters();
/**
* 获取已注册的采集器列表
* @returns {string[]} 采集器类名列表
*/
function getRegisteredAdapters() {
return Object.keys(registry);
}
/**
* 检查采集器是否已注册
* @param {string} className
* @returns {boolean}
*/
function hasAdapter(className) {
return !!registry[className];
}
/**
* 创建采集器实例
* @param {string} className - 采集器类名(如 "LinuxServerAdapter")
* @param {object} connectionInfo - SSH 连接信息
* @param {object} [options] - 配置项
* @returns {BaseAdapter|null}
*/
function createAdapter(className, connectionInfo, options = {}) {
const AdapterClass = registry[className];
if (!AdapterClass) {
console.warn(`[Inspection] 未找到采集器: ${className}`);
return null;
}
return new AdapterClass(connectionInfo, options);
}
module.exports = {
loadAdapters,
getRegisteredAdapters,
hasAdapter,
createAdapter,
};

View 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;

View 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;

View 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;

View 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;

View File

@@ -0,0 +1,280 @@
"use strict";
/**
* 告警记录存储
*
* 告警记录结构:
* {
* id: "alert_xxx",
* rule_id: "rule_cpu_critical",
* rule_name: "CPU 使用率过高",
* device_id: "host_xxx",
* device_name: "核心交换机-01",
* metric: "cpu_usage",
* metric_value: "85",
* threshold: "80",
* level: "critical",
* message: "核心交换机-01 CPU 使用率 85%,超过阈值 80%",
* triggered_at: timestamp,
* status: "active",
* acknowledged_by: null,
* acknowledged_at: null,
* resolved_at: null,
* resolved_reason: null,
* note: null,
* }
*/
const { STORAGE_KEY_ALERT_RECORDS, STORAGE_KEY_ALERT_COUNTERS } = require("./storageKeys.cjs");
const { ALERT_RECORD_STATUS, ALERT_LEVELS } = require("./types.cjs");
const MAX_ALERTS = 2000;
let _alerts = null;
let _counters = null; // 防抖动计数器 { "deviceId_ruleId": count }
function _ensureLoaded(readFn) {
if (_alerts === null) {
const raw = readFn(STORAGE_KEY_ALERT_RECORDS);
_alerts = raw ? _safeParse(raw, []) : [];
}
if (_counters === null) {
const raw = readFn(STORAGE_KEY_ALERT_COUNTERS);
_counters = raw ? _safeParse(raw, {}) : {};
}
}
function _safeParse(text, def) {
try { return JSON.parse(text); }
catch (e) { return def; }
}
function _saveAlerts(writeFn) {
if (_alerts.length > MAX_ALERTS) {
// 只保留最近的
_alerts = _alerts.sort((a, b) => b.triggered_at - a.triggered_at).slice(0, MAX_ALERTS);
}
writeFn(STORAGE_KEY_ALERT_RECORDS, JSON.stringify(_alerts));
}
function _saveCounters(writeFn) {
writeFn(STORAGE_KEY_ALERT_COUNTERS, JSON.stringify(_counters));
}
// ---------- 告警触发(带防抖动) ----------
/**
* 处理告警:检查是否满足持续次数,满足则创建/更新告警记录
* @param {object} device - 设备信息 { device_id, device_name }
* @param {object} rule - 告警规则
* @param {number|string} metricValue - 当前指标值
* @param {boolean} isTriggered - 是否触发(指标值 > 阈值)
* @returns {object|null} 新创建或恢复的告警记录,无变化则返回 null
*/
function processAlert(readFn, writeFn, device, rule, metricValue, isTriggered) {
_ensureLoaded(readFn);
const counterKey = `${device.device_id}_${rule.id}`;
const activeAlert = _findActiveAlert(device.device_id, rule.id);
if (isTriggered) {
// 触发:计数 +1
_counters[counterKey] = (_counters[counterKey] || 0) + 1;
_saveCounters(writeFn);
// 达到持续次数且没有活跃告警 → 创建告警
if (_counters[counterKey] >= rule.duration && !activeAlert) {
const alert = _createAlert(device, rule, metricValue);
_alerts.push(alert);
_saveAlerts(writeFn);
return { ...alert, action: "triggered" };
}
// 已有活跃告警 → 更新指标值
if (activeAlert) {
activeAlert.metric_value = String(metricValue);
activeAlert.message = _buildMessage(device.device_name, rule, metricValue);
_saveAlerts(writeFn);
return { ...activeAlert, action: "updated" };
}
return null;
} else {
// 未触发:重置计数
if (_counters[counterKey]) {
delete _counters[counterKey];
_saveCounters(writeFn);
}
// 有活跃告警 → 自动恢复
if (activeAlert && activeAlert.status === ALERT_RECORD_STATUS.ACTIVE) {
activeAlert.status = ALERT_RECORD_STATUS.RESOLVED;
activeAlert.resolved_at = Date.now();
activeAlert.resolved_reason = "auto_recover";
_saveAlerts(writeFn);
return { ...activeAlert, action: "resolved" };
}
return null;
}
}
function _findActiveAlert(deviceId, ruleId) {
return _alerts.find(
(a) =>
a.device_id === deviceId &&
a.rule_id === ruleId &&
(a.status === ALERT_RECORD_STATUS.ACTIVE ||
a.status === ALERT_RECORD_STATUS.ACKNOWLEDGED)
);
}
function _createAlert(device, rule, metricValue) {
return {
id: `alert_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
rule_id: rule.id,
rule_name: rule.name,
device_id: device.device_id,
device_name: device.device_name || device.device_id,
metric: rule.metric,
metric_value: String(metricValue),
threshold: rule.threshold,
level: rule.level,
message: _buildMessage(device.device_name || device.device_id, rule, metricValue),
triggered_at: Date.now(),
status: ALERT_RECORD_STATUS.ACTIVE,
acknowledged_by: null,
acknowledged_at: null,
resolved_at: null,
resolved_reason: null,
note: null,
};
}
function _buildMessage(deviceName, rule, metricValue) {
const unit = rule.metric.includes("_usage") ? "%" : "";
return `${deviceName} ${rule.name},当前值 ${metricValue}${unit},阈值 ${rule.threshold}${unit}`;
}
// ---------- 告警查询 ----------
function listAlerts(readFn, options = {}) {
_ensureLoaded(readFn);
let result = [..._alerts];
if (options.status) {
result = result.filter((a) => a.status === options.status);
}
if (options.level) {
result = result.filter((a) => a.level === options.level);
}
if (options.device_id) {
result = result.filter((a) => a.device_id === options.device_id);
}
return result.sort((a, b) => b.triggered_at - a.triggered_at).slice(0, options.limit || 100);
}
function getActiveAlerts(readFn) {
return listAlerts(readFn, { status: ALERT_RECORD_STATUS.ACTIVE });
}
function getAlert(readFn, alertId) {
_ensureLoaded(readFn);
return _alerts.find((a) => a.id === alertId) || null;
}
// ---------- 告警处理 ----------
function acknowledgeAlert(readFn, writeFn, alertId, operator = "system") {
_ensureLoaded(readFn);
const alert = _alerts.find((a) => a.id === alertId);
if (!alert) return null;
alert.status = ALERT_RECORD_STATUS.ACKNOWLEDGED;
alert.acknowledged_by = operator;
alert.acknowledged_at = Date.now();
_saveAlerts(writeFn);
return { ...alert };
}
function resolveAlert(readFn, writeFn, alertId, reason = "manual", operator = "system") {
_ensureLoaded(readFn);
const alert = _alerts.find((a) => a.id === alertId);
if (!alert) return null;
alert.status = ALERT_RECORD_STATUS.RESOLVED;
alert.resolved_at = Date.now();
alert.resolved_reason = reason;
alert.acknowledged_by = alert.acknowledged_by || operator;
alert.acknowledged_at = alert.acknowledged_at || Date.now();
_saveAlerts(writeFn);
return { ...alert };
}
function closeAlert(readFn, writeFn, alertId, note = "") {
_ensureLoaded(readFn);
const alert = _alerts.find((a) => a.id === alertId);
if (!alert) return null;
alert.status = ALERT_RECORD_STATUS.CLOSED;
alert.note = note;
if (!alert.resolved_at) {
alert.resolved_at = Date.now();
alert.resolved_reason = "closed";
}
_saveAlerts(writeFn);
return { ...alert };
}
function updateNote(readFn, writeFn, alertId, note) {
_ensureLoaded(readFn);
const alert = _alerts.find((a) => a.id === alertId);
if (!alert) return null;
alert.note = note;
_saveAlerts(writeFn);
return { ...alert };
}
// ---------- 统计 ----------
function getAlertStats(readFn) {
_ensureLoaded(readFn);
const active = _alerts.filter((a) => a.status === ALERT_RECORD_STATUS.ACTIVE);
return {
total: _alerts.length,
active: active.length,
active_critical: active.filter((a) => a.level === ALERT_LEVELS.CRITICAL).length,
active_warning: active.filter((a) => a.level === ALERT_LEVELS.WARNING).length,
active_info: active.filter((a) => a.level === ALERT_LEVELS.INFO).length,
acknowledged: _alerts.filter((a) => a.status === ALERT_RECORD_STATUS.ACKNOWLEDGED).length,
resolved: _alerts.filter((a) => a.status === ALERT_RECORD_STATUS.RESOLVED).length,
closed: _alerts.filter((a) => a.status === ALERT_RECORD_STATUS.CLOSED).length,
};
}
// ---------- 重置(测试用) ----------
function _resetForTest() {
_alerts = null;
_counters = null;
}
module.exports = {
// 告警触发
processAlert,
// 查询
listAlerts,
getActiveAlerts,
getAlert,
// 处理
acknowledgeAlert,
resolveAlert,
closeAlert,
updateNote,
// 统计
getAlertStats,
// 内部
_resetForTest,
};

View File

@@ -0,0 +1,543 @@
"use strict";
/**
* 告警规则数据存储
*
* 告警规则结构:
* {
* id: "rule_xxx",
* name: "CPU 使用率过高",
* metric: "cpu_usage",
* operator: ">",
* threshold: "80",
* level: "critical",
* duration: 1, // 连续超标次数(防抖动)
* scope_type: "all", // all / tier / device / catalog
* scope_value: null, // 范围值(tier_key 或 hostId 数组 或 catalog_id)
* enabled: true,
* created_at: timestamp,
* updated_at: timestamp,
* }
*/
const { STORAGE_KEY_ALERT_RULES } = require("./storageKeys.cjs");
const { OPERATORS, ALERT_LEVELS } = require("./types.cjs");
let _rules = null;
function _ensureLoaded(readFn) {
if (_rules === null) {
const raw = readFn(STORAGE_KEY_ALERT_RULES);
_rules = raw ? _safeParse(raw, []) : _getDefaultRules();
}
}
function _safeParse(text, def) {
try { return JSON.parse(text); }
catch (e) { return def; }
}
function _save(writeFn) {
writeFn(STORAGE_KEY_ALERT_RULES, JSON.stringify(_rules));
}
/**
* 默认告警规则(预置)
*/
function _getDefaultRules() {
return [
{
id: "rule_cpu_critical",
name: "CPU 使用率过高",
metric: "cpu_usage",
operator: OPERATORS.GT,
threshold: "80",
level: ALERT_LEVELS.CRITICAL,
duration: 1,
scope_type: "all",
scope_value: null,
enabled: true,
preset: true,
created_at: Date.now(),
updated_at: Date.now(),
},
{
id: "rule_cpu_warning",
name: "CPU 使用率异常",
metric: "cpu_usage",
operator: OPERATORS.GT,
threshold: "70",
level: ALERT_LEVELS.WARNING,
duration: 2,
scope_type: "all",
scope_value: null,
enabled: true,
preset: true,
created_at: Date.now(),
updated_at: Date.now(),
},
{
id: "rule_memory_critical",
name: "内存使用率过高",
metric: "memory_usage",
operator: OPERATORS.GT,
threshold: "85",
level: ALERT_LEVELS.CRITICAL,
duration: 1,
scope_type: "all",
scope_value: null,
enabled: true,
preset: true,
created_at: Date.now(),
updated_at: Date.now(),
},
{
id: "rule_memory_warning",
name: "内存使用率异常",
metric: "memory_usage",
operator: OPERATORS.GT,
threshold: "75",
level: ALERT_LEVELS.WARNING,
duration: 2,
scope_type: "all",
scope_value: null,
enabled: true,
preset: true,
created_at: Date.now(),
updated_at: Date.now(),
},
{
id: "rule_offline",
name: "设备离线",
metric: "online",
operator: OPERATORS.EQ,
threshold: "false",
level: ALERT_LEVELS.CRITICAL,
duration: 1,
scope_type: "all",
scope_value: null,
enabled: true,
preset: true,
created_at: Date.now(),
updated_at: Date.now(),
},
// ---------- 磁盘使用率 ----------
{
id: "rule_disk_critical",
name: "磁盘使用率过高",
metric: "disk_usage",
operator: OPERATORS.GT,
threshold: "90",
level: ALERT_LEVELS.CRITICAL,
duration: 1,
scope_type: "all",
scope_value: null,
enabled: true,
preset: true,
created_at: Date.now(),
updated_at: Date.now(),
},
{
id: "rule_disk_warning",
name: "磁盘使用率异常",
metric: "disk_usage",
operator: OPERATORS.GT,
threshold: "80",
level: ALERT_LEVELS.WARNING,
duration: 2,
scope_type: "all",
scope_value: null,
enabled: true,
preset: true,
created_at: Date.now(),
updated_at: Date.now(),
},
// ---------- 网络设备通用(交换机/防火墙) ----------
{
id: "rule_netdev_temp_critical",
name: "设备温度过高",
metric: "temperature",
operator: OPERATORS.GT,
threshold: "60",
level: ALERT_LEVELS.CRITICAL,
duration: 2,
scope_type: "catalog",
scope_value: "huawei_h3c_switch",
enabled: true,
preset: true,
created_at: Date.now(),
updated_at: Date.now(),
},
{
id: "rule_netdev_power_critical",
name: "电源模块故障",
metric: "power_failure_count",
operator: OPERATORS.GT,
threshold: "0",
level: ALERT_LEVELS.CRITICAL,
duration: 1,
scope_type: "catalog",
scope_value: "huawei_h3c_switch",
enabled: true,
preset: true,
created_at: Date.now(),
updated_at: Date.now(),
},
{
id: "rule_netdev_fan_warning",
name: "风扇模块故障",
metric: "fan_failure_count",
operator: OPERATORS.GT,
threshold: "0",
level: ALERT_LEVELS.WARNING,
duration: 1,
scope_type: "catalog",
scope_value: "huawei_h3c_switch",
enabled: true,
preset: true,
created_at: Date.now(),
updated_at: Date.now(),
},
{
id: "rule_netdev_portdown_warning",
name: "端口 Down 数过多",
metric: "port_down_count",
operator: OPERATORS.GT,
threshold: "2",
level: ALERT_LEVELS.WARNING,
duration: 2,
scope_type: "catalog",
scope_value: "huawei_h3c_switch",
enabled: true,
preset: true,
created_at: Date.now(),
updated_at: Date.now(),
},
{
id: "rule_netdev_cpu_warning",
name: "交换机 CPU 使用率过高",
metric: "cpu_usage",
operator: OPERATORS.GT,
threshold: "75",
level: ALERT_LEVELS.WARNING,
duration: 2,
scope_type: "catalog",
scope_value: "huawei_h3c_switch",
enabled: true,
preset: true,
created_at: Date.now(),
updated_at: Date.now(),
},
{
id: "rule_netdev_memory_warning",
name: "交换机内存使用率过高",
metric: "memory_usage",
operator: OPERATORS.GT,
threshold: "80",
level: ALERT_LEVELS.WARNING,
duration: 2,
scope_type: "catalog",
scope_value: "huawei_h3c_switch",
enabled: true,
preset: true,
created_at: Date.now(),
updated_at: Date.now(),
},
// ---------- Cisco 交换机 ----------
{
id: "rule_cisco_temp_critical",
name: "Cisco 交换机温度过高",
metric: "temperature",
operator: OPERATORS.GT,
threshold: "60",
level: ALERT_LEVELS.CRITICAL,
duration: 2,
scope_type: "catalog",
scope_value: "cisco_switch",
enabled: true,
preset: true,
created_at: Date.now(),
updated_at: Date.now(),
},
{
id: "rule_cisco_power_critical",
name: "Cisco 电源模块故障",
metric: "power_failure_count",
operator: OPERATORS.GT,
threshold: "0",
level: ALERT_LEVELS.CRITICAL,
duration: 1,
scope_type: "catalog",
scope_value: "cisco_switch",
enabled: true,
preset: true,
created_at: Date.now(),
updated_at: Date.now(),
},
{
id: "rule_cisco_fan_warning",
name: "Cisco 风扇模块故障",
metric: "fan_failure_count",
operator: OPERATORS.GT,
threshold: "0",
level: ALERT_LEVELS.WARNING,
duration: 1,
scope_type: "catalog",
scope_value: "cisco_switch",
enabled: true,
preset: true,
created_at: Date.now(),
updated_at: Date.now(),
},
// ---------- 华为防火墙 ----------
{
id: "rule_hwfw_temp_critical",
name: "华为防火墙温度过高",
metric: "temperature",
operator: OPERATORS.GT,
threshold: "60",
level: ALERT_LEVELS.CRITICAL,
duration: 2,
scope_type: "catalog",
scope_value: "huawei_firewall",
enabled: true,
preset: true,
created_at: Date.now(),
updated_at: Date.now(),
},
{
id: "rule_hwfw_power_critical",
name: "华为防火墙电源故障",
metric: "power_failure_count",
operator: OPERATORS.GT,
threshold: "0",
level: ALERT_LEVELS.CRITICAL,
duration: 1,
scope_type: "catalog",
scope_value: "huawei_firewall",
enabled: true,
preset: true,
created_at: Date.now(),
updated_at: Date.now(),
},
{
id: "rule_hwfw_fan_warning",
name: "华为防火墙风扇故障",
metric: "fan_failure_count",
operator: OPERATORS.GT,
threshold: "0",
level: ALERT_LEVELS.WARNING,
duration: 1,
scope_type: "catalog",
scope_value: "huawei_firewall",
enabled: true,
preset: true,
created_at: Date.now(),
updated_at: Date.now(),
},
// ---------- 飞塔防火墙 ----------
{
id: "rule_fortinet_temp_critical",
name: "飞塔防火墙温度过高",
metric: "temperature",
operator: OPERATORS.GT,
threshold: "60",
level: ALERT_LEVELS.CRITICAL,
duration: 2,
scope_type: "catalog",
scope_value: "fortinet_firewall",
enabled: true,
preset: true,
created_at: Date.now(),
updated_at: Date.now(),
},
{
id: "rule_fortinet_power_critical",
name: "飞塔防火墙电源故障",
metric: "power_failure_count",
operator: OPERATORS.GT,
threshold: "0",
level: ALERT_LEVELS.CRITICAL,
duration: 1,
scope_type: "catalog",
scope_value: "fortinet_firewall",
enabled: true,
preset: true,
created_at: Date.now(),
updated_at: Date.now(),
},
{
id: "rule_fortinet_fan_warning",
name: "飞塔防火墙风扇故障",
metric: "fan_failure_count",
operator: OPERATORS.GT,
threshold: "0",
level: ALERT_LEVELS.WARNING,
duration: 1,
scope_type: "catalog",
scope_value: "fortinet_firewall",
enabled: true,
preset: true,
created_at: Date.now(),
updated_at: Date.now(),
},
];
}
// ---------- CRUD ----------
function listRules(readFn) {
_ensureLoaded(readFn);
return [..._rules];
}
function getRule(readFn, ruleId) {
_ensureLoaded(readFn);
return _rules.find((r) => r.id === ruleId) || null;
}
function createRule(readFn, writeFn, data) {
_ensureLoaded(readFn);
const rule = {
id: `rule_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
name: data.name,
metric: data.metric,
operator: data.operator,
threshold: String(data.threshold),
level: data.level,
duration: data.duration || 1,
scope_type: data.scope_type || "all",
scope_value: data.scope_value || null,
enabled: data.enabled !== false,
preset: false,
created_at: Date.now(),
updated_at: Date.now(),
};
_rules.push(rule);
_save(writeFn);
return { ...rule };
}
function updateRule(readFn, writeFn, ruleId, updates) {
_ensureLoaded(readFn);
const idx = _rules.findIndex((r) => r.id === ruleId);
if (idx === -1) return null;
_rules[idx] = {
..._rules[idx],
...updates,
updated_at: Date.now(),
};
_save(writeFn);
return { ..._rules[idx] };
}
function deleteRule(readFn, writeFn, ruleId) {
_ensureLoaded(readFn);
const idx = _rules.findIndex((r) => r.id === ruleId);
if (idx === -1) return false;
_rules.splice(idx, 1);
_save(writeFn);
return true;
}
function toggleRule(readFn, writeFn, ruleId, enabled) {
return updateRule(readFn, writeFn, ruleId, { enabled });
}
// ---------- 告警判断 ----------
/**
* 判断指标值是否触发规则
* @param {number|string} value - 指标值
* @param {string} operator - 比较符
* @param {string} threshold - 阈值
* @returns {boolean}
*/
function evaluateRule(value, operator, threshold) {
if (value === null || value === undefined) return false;
// disk_usage 是数组,检查任意挂载点是否超标
if (Array.isArray(value)) {
const numThreshold = parseFloat(threshold);
if (isNaN(numThreshold)) return false;
// 数组中每个元素是 { filesystem, usage_percent, mount }
return value.some((d) => {
const usage = typeof d === "object" ? d.usage_percent : parseFloat(d);
return !isNaN(usage) && usage > numThreshold;
});
}
// 数字比较
const numValue = parseFloat(value);
const numThreshold = parseFloat(threshold);
if (!isNaN(numValue) && !isNaN(numThreshold)) {
switch (operator) {
case OPERATORS.GT: return numValue > numThreshold;
case OPERATORS.LT: return numValue < numThreshold;
case OPERATORS.GTE: return numValue >= numThreshold;
case OPERATORS.LTE: return numValue <= numThreshold;
case OPERATORS.EQ: return numValue === numThreshold;
case OPERATORS.NE: return numValue !== numThreshold;
}
}
// 字符串比较
const strValue = String(value);
const strThreshold = String(threshold);
switch (operator) {
case OPERATORS.EQ: return strValue === strThreshold;
case OPERATORS.NE: return strValue !== strThreshold;
default: return false;
}
}
/**
* 获取设备适用的所有规则
* @param {object} deviceExt - 设备扩展信息
* @returns {Array} 适用的规则列表
*/
function getRulesForDevice(readFn, deviceExt) {
_ensureLoaded(readFn);
return _rules.filter((rule) => {
if (!rule.enabled) return false;
switch (rule.scope_type) {
case "all":
return true;
case "tier":
return deviceExt.tier && deviceExt.tier === rule.scope_value;
case "device":
return Array.isArray(rule.scope_value) && rule.scope_value.includes(deviceExt.host_id);
case "catalog":
return deviceExt.catalog_id === rule.scope_value;
default:
return false;
}
});
}
// ---------- 重置(测试用) ----------
function _resetForTest() {
_rules = null;
}
module.exports = {
listRules,
getRule,
createRule,
updateRule,
deleteRule,
toggleRule,
evaluateRule,
getRulesForDevice,
_resetForTest,
};

View File

@@ -0,0 +1,189 @@
"use strict";
/**
* 设备类型数据模型 + 预置数据
*
* 设备类型(device_catalog):定义一类设备的采集能力和默认配置。
* 预置 6 种设备类型,用户可在界面上新增。
*/
const { DEVICE_CATALOG_TYPES, ALERT_LEVELS, DEVICE_TIERS } = require("./types.cjs");
/**
* 默认告警阈值 - 用于设备类型的预置配置
* 格式:{ metric, operator, threshold, level, duration }
*/
const DEFAULT_THRESHOLDS = {
linux: [
{ metric: "cpu_usage", operator: ">", threshold: "80", level: ALERT_LEVELS.CRITICAL, duration: 1 },
{ metric: "cpu_usage", operator: ">", threshold: "70", level: ALERT_LEVELS.WARNING, duration: 2 },
{ metric: "memory_usage", operator: ">", threshold: "85", level: ALERT_LEVELS.CRITICAL, duration: 1 },
{ metric: "memory_usage", operator: ">", threshold: "75", level: ALERT_LEVELS.WARNING, duration: 2 },
{ metric: "online", operator: "==", threshold: "false", level: ALERT_LEVELS.CRITICAL, duration: 1 },
],
windows: [
{ metric: "cpu_usage", operator: ">", threshold: "80", level: ALERT_LEVELS.CRITICAL, duration: 1 },
{ metric: "cpu_usage", operator: ">", threshold: "70", level: ALERT_LEVELS.WARNING, duration: 2 },
{ metric: "memory_usage", operator: ">", threshold: "85", level: ALERT_LEVELS.CRITICAL, duration: 1 },
{ metric: "memory_usage", operator: ">", threshold: "75", level: ALERT_LEVELS.WARNING, duration: 2 },
{ metric: "online", operator: "==", threshold: "false", level: ALERT_LEVELS.CRITICAL, duration: 1 },
],
huawei_h3c_switch: [
{ metric: "cpu_usage", operator: ">", threshold: "80", level: ALERT_LEVELS.CRITICAL, duration: 1 },
{ metric: "cpu_usage", operator: ">", threshold: "70", level: ALERT_LEVELS.WARNING, duration: 2 },
{ metric: "memory_usage", operator: ">", threshold: "85", level: ALERT_LEVELS.CRITICAL, duration: 1 },
{ metric: "memory_usage", operator: ">", threshold: "75", level: ALERT_LEVELS.WARNING, duration: 2 },
{ metric: "online", operator: "==", threshold: "false", level: ALERT_LEVELS.CRITICAL, duration: 1 },
],
cisco_switch: [
{ metric: "cpu_usage", operator: ">", threshold: "80", level: ALERT_LEVELS.CRITICAL, duration: 1 },
{ metric: "cpu_usage", operator: ">", threshold: "70", level: ALERT_LEVELS.WARNING, duration: 2 },
{ metric: "memory_usage", operator: ">", threshold: "85", level: ALERT_LEVELS.CRITICAL, duration: 1 },
{ metric: "memory_usage", operator: ">", threshold: "75", level: ALERT_LEVELS.WARNING, duration: 2 },
{ metric: "online", operator: "==", threshold: "false", level: ALERT_LEVELS.CRITICAL, duration: 1 },
],
huawei_firewall: [
{ metric: "cpu_usage", operator: ">", threshold: "80", level: ALERT_LEVELS.CRITICAL, duration: 1 },
{ metric: "memory_usage", operator: ">", threshold: "85", level: ALERT_LEVELS.CRITICAL, duration: 1 },
{ metric: "online", operator: "==", threshold: "false", level: ALERT_LEVELS.CRITICAL, duration: 1 },
],
fortinet_firewall: [
{ metric: "cpu_usage", operator: ">", threshold: "80", level: ALERT_LEVELS.CRITICAL, duration: 1 },
{ metric: "memory_usage", operator: ">", threshold: "85", level: ALERT_LEVELS.CRITICAL, duration: 1 },
{ metric: "online", operator: "==", threshold: "false", level: ALERT_LEVELS.CRITICAL, duration: 1 },
],
};
/**
* 预置设备类型列表
* 每条记录对应一条 device_catalog 数据
*/
const PRESET_DEVICE_CATALOG = [
{
id: "linux",
type_key: DEVICE_CATALOG_TYPES.LINUX,
name: "Linux 服务器",
vendor: "Linux",
adapter_class: "LinuxServerAdapter",
supported_metrics: [
"cpu_usage",
"memory_usage",
"disk_usage",
"load_avg",
"uptime",
"process_count",
"online",
],
default_thresholds: DEFAULT_THRESHOLDS.linux,
default_interval: 5, // 默认 5 分钟
description: "通用 Linux 服务器,支持 Ubuntu/CentOS/Debian/Alpine 等发行版",
preset: true,
},
{
id: "windows",
type_key: DEVICE_CATALOG_TYPES.WINDOWS,
name: "Windows 服务器",
vendor: "Microsoft",
adapter_class: "WindowsServerAdapter",
supported_metrics: [
"cpu_usage",
"memory_usage",
"disk_usage",
"uptime",
"process_count",
"service_status",
"online",
],
default_thresholds: DEFAULT_THRESHOLDS.windows,
default_interval: 5,
description: "Windows Server 2012/2016/2019/2022,通过 SSH + PowerShell 采集",
preset: true,
},
{
id: "huawei_h3c_switch",
type_key: DEVICE_CATALOG_TYPES.HUAWEI_H3C_SWITCH,
name: "华为/H3C 交换机",
vendor: "华为/H3C",
adapter_class: "HuaweiH3CSwitchAdapter",
supported_metrics: [
"cpu_usage",
"memory_usage",
"port_status",
"power_status",
"fan_status",
"device_info",
"online",
],
default_thresholds: DEFAULT_THRESHOLDS.huawei_h3c_switch,
default_interval: 10, // 交换机默认 10 分钟
description: "华为 S3000/S5000/S7000/S9000 系列、H3C S5100/S5500 等交换机",
preset: true,
},
{
id: "cisco_switch",
type_key: DEVICE_CATALOG_TYPES.CISCO_SWITCH,
name: "Cisco 交换机",
vendor: "Cisco",
adapter_class: "CiscoSwitchAdapter",
supported_metrics: [
"cpu_usage",
"memory_usage",
"port_status",
"power_status",
"fan_status",
"online",
],
default_thresholds: DEFAULT_THRESHOLDS.cisco_switch,
default_interval: 10,
description: "Cisco Catalyst 2960/3650/3850/4500/6500 等交换机",
preset: true,
},
{
id: "huawei_firewall",
type_key: DEVICE_CATALOG_TYPES.HUAWEI_FIREWALL,
name: "华为防火墙",
vendor: "华为",
adapter_class: "HuaweiFirewallAdapter",
supported_metrics: [
"cpu_usage",
"memory_usage",
"session_count",
"online",
],
default_thresholds: DEFAULT_THRESHOLDS.huawei_firewall,
default_interval: 5, // 防火墙重要,默认 5 分钟
description: "华为 USG 系列防火墙",
preset: true,
},
{
id: "fortinet_firewall",
type_key: DEVICE_CATALOG_TYPES.FORTINET_FIREWALL,
name: "飞塔防火墙",
vendor: "Fortinet",
adapter_class: "FortinetFirewallAdapter",
supported_metrics: [
"cpu_usage",
"memory_usage",
"session_count",
"online",
],
default_thresholds: DEFAULT_THRESHOLDS.fortinet_firewall,
default_interval: 5,
description: "Fortinet FortiGate 系列防火墙",
preset: true,
},
];
/**
* 获取预置设备类型数据
* @returns {Array}
*/
function getPresetCatalog() {
return JSON.parse(JSON.stringify(PRESET_DEVICE_CATALOG));
}
module.exports = {
PRESET_DEVICE_CATALOG,
DEFAULT_THRESHOLDS,
getPresetCatalog,
};

View File

@@ -0,0 +1,276 @@
"use strict";
/**
* 设备扩展数据存储(巡检相关配置)
*
* 关联 NetMesh 的主机数据,扩展巡检相关字段:
* - 设备类型(catalog_id)
* - 设备层级(core/aggregation/access)
* - 巡检周期
* - 启用状态
* - 自定义告警阈值
* - 上次巡检信息
*
* 周期优先级(从高到低):
* 1. 设备独立设置 inspection_interval
* 2. 设备层级的默认周期 DEVICE_TIER_DEFAULT_INTERVAL
* 3. 系统默认周期 SYS_CONFIG_DEFAULTS.inspection_default_interval
*/
const {
STORAGE_KEY_DEVICE_EXTENSION,
STORAGE_KEY_SYS_CONFIG,
} = require("./storageKeys.cjs");
const {
DEVICE_TIERS,
DEVICE_TIER_LABELS,
DEVICE_TIER_DEFAULT_INTERVAL,
SYS_CONFIG_DEFAULTS,
} = require("./types.cjs");
const { getPresetCatalog } = require("./deviceCatalogStore.cjs");
// ---------- 内存缓存 ----------
let _extensions = null; // { [hostId]: { ... } }
let _sysConfig = null; // { inspection_default_interval: 5, ... }
let _catalog = null; // 设备类型列表
/**
* 从存储加载数据(首次调用时加载,之后用缓存)
* @param {function} readFn - 读取函数 (key) => rawString
*/
function _ensureLoaded(readFn) {
if (_extensions === null) {
const raw = readFn(STORAGE_KEY_DEVICE_EXTENSION);
_extensions = raw ? _safeParse(raw, {}) : {};
}
if (_sysConfig === null) {
const raw = readFn(STORAGE_KEY_SYS_CONFIG);
_sysConfig = raw ? _safeParse(raw, {}) : { ...SYS_CONFIG_DEFAULTS };
// 合并默认值
for (const k of Object.keys(SYS_CONFIG_DEFAULTS)) {
if (_sysConfig[k] === undefined) _sysConfig[k] = SYS_CONFIG_DEFAULTS[k];
}
}
if (_catalog === null) {
_catalog = getPresetCatalog();
}
}
function _safeParse(text, def) {
try { return JSON.parse(text); }
catch (e) { return def; }
}
function _save(writeFn, key, data) {
writeFn(key, JSON.stringify(data));
}
// ---------- 系统配置 ----------
/**
* 获取系统配置
*/
function getSysConfig(readFn) {
_ensureLoaded(readFn);
return { ..._sysConfig };
}
/**
* 更新系统配置
*/
function updateSysConfig(readFn, writeFn, updates) {
_ensureLoaded(readFn);
_sysConfig = { ..._sysConfig, ...updates };
_save(writeFn, STORAGE_KEY_SYS_CONFIG, _sysConfig);
return { ..._sysConfig };
}
// ---------- 设备扩展 ----------
/**
* 获取所有设备扩展配置
*/
function getAllExtensions(readFn) {
_ensureLoaded(readFn);
return { ..._extensions };
}
/**
* 获取单个设备的扩展配置
*/
function getExtension(readFn, hostId) {
_ensureLoaded(readFn);
return _extensions[hostId] || null;
}
/**
* 创建/更新设备扩展配置
* @param {string} hostId - NetMesh 主机 ID
* @param {object} data - 扩展配置
* @param {string} data.catalog_id - 设备类型 ID
* @param {string} [data.tier] - 设备层级 core/aggregation/access
* @param {number} [data.inspection_interval] - 巡检周期(分钟),null 表示继承
* @param {boolean} [data.inspection_enabled] - 是否启用巡检
* @param {object} [data.custom_thresholds] - 自定义告警阈值
* @param {string[]} [data.inspection_tags] - 巡检标签
* @param {string} [data.hostname] - 主机名(IP 或域名),用于调度器关联 vault host SSH 配置
*/
function upsertExtension(readFn, writeFn, hostId, data) {
_ensureLoaded(readFn);
const existing = _extensions[hostId] || {
host_id: hostId,
hostname: null,
catalog_id: "",
tier: null,
inspection_interval: null,
inspection_enabled: false,
custom_thresholds: [],
inspection_tags: [],
last_scan_at: null,
last_scan_status: null,
last_scan_error: null,
};
_extensions[hostId] = {
...existing,
...data,
updated_at: Date.now(),
};
_save(writeFn, STORAGE_KEY_DEVICE_EXTENSION, _extensions);
return { ..._extensions[hostId] };
}
/**
* 删除设备扩展配置
*/
function deleteExtension(readFn, writeFn, hostId) {
_ensureLoaded(readFn);
if (_extensions[hostId]) {
delete _extensions[hostId];
_save(writeFn, STORAGE_KEY_DEVICE_EXTENSION, _extensions);
return true;
}
return false;
}
/**
* 计算设备的实际巡检周期(考虑三级继承)
* @param {string} hostId
* @returns {number} 周期(分钟)
*/
function getEffectiveInterval(readFn, hostId) {
_ensureLoaded(readFn);
const ext = _extensions[hostId];
if (!ext) return _sysConfig.inspection_default_interval;
// 1. 设备独立设置
if (typeof ext.inspection_interval === "number" && ext.inspection_interval > 0) {
return ext.inspection_interval;
}
// 2. 设备层级默认
if (ext.tier && DEVICE_TIER_DEFAULT_INTERVAL[ext.tier]) {
return DEVICE_TIER_DEFAULT_INTERVAL[ext.tier];
}
// 3. 系统默认
return _sysConfig.inspection_default_interval;
}
/**
* 获取所有启用了巡检的设备列表(带有效周期)
*/
function getEnabledDevices(readFn) {
_ensureLoaded(readFn);
const result = [];
for (const hostId of Object.keys(_extensions)) {
const ext = _extensions[hostId];
if (ext.inspection_enabled) {
result.push({
...ext,
effective_interval: getEffectiveInterval(readFn, hostId),
});
}
}
return result;
}
/**
* 更新上次巡检信息
*/
function updateLastScan(readFn, writeFn, hostId, status, error = null) {
_ensureLoaded(readFn);
if (!_extensions[hostId]) return false;
_extensions[hostId].last_scan_at = Date.now();
_extensions[hostId].last_scan_status = status;
_extensions[hostId].last_scan_error = error;
_save(writeFn, STORAGE_KEY_DEVICE_EXTENSION, _extensions);
return true;
}
// ---------- 设备类型 ----------
/**
* 获取设备类型列表
*/
function getCatalog() {
if (_catalog === null) {
_catalog = getPresetCatalog();
}
return [..._catalog];
}
/**
* 根据 ID 获取设备类型
*/
function getCatalogById(catalogId) {
const list = getCatalog();
return list.find((c) => c.id === catalogId) || null;
}
// ---------- 设备层级 ----------
/**
* 获取设备层级列表(带中文名称和默认周期)
*/
function getTiers() {
return [
{ key: DEVICE_TIERS.CORE, label: DEVICE_TIER_LABELS.core, default_interval: DEVICE_TIER_DEFAULT_INTERVAL.core },
{ key: DEVICE_TIERS.AGGREGATION, label: DEVICE_TIER_LABELS.aggregation, default_interval: DEVICE_TIER_DEFAULT_INTERVAL.aggregation },
{ key: DEVICE_TIERS.ACCESS, label: DEVICE_TIER_LABELS.access, default_interval: DEVICE_TIER_DEFAULT_INTERVAL.access },
];
}
// ---------- 重置(测试用) ----------
function _resetForTest() {
_extensions = null;
_sysConfig = null;
_catalog = null;
}
module.exports = {
// 系统配置
getSysConfig,
updateSysConfig,
// 设备扩展
getAllExtensions,
getExtension,
upsertExtension,
deleteExtension,
getEffectiveInterval,
getEnabledDevices,
updateLastScan,
// 设备类型
getCatalog,
getCatalogById,
// 设备层级
getTiers,
// 内部
_resetForTest,
};

View File

@@ -0,0 +1,502 @@
"use strict";
/**
* 巡检模块统一入口
*
* 提供 IPC handler 注册函数,供主进程注册。
* 渲染进程通过 IPC 调用巡检相关功能。
*/
const deviceStore = require("./deviceStore.cjs");
const alertRuleStore = require("./alertRuleStore.cjs");
const inspectionResultStore = require("./inspectionResultStore.cjs");
const alertRecordStore = require("./alertRecordStore.cjs");
const adapterRegistry = require("./adapterRegistry.cjs");
const InspectionScheduler = require("./inspectionScheduler.cjs");
const { execSshCommand } = require("./inspectionExec.cjs");
const { executeBoundedSshCommand } = require("../boundedSshExec.cjs");
const {
ALERT_LEVEL_LABELS,
DEVICE_TIER_LABELS,
INSPECTION_STATUS_LABELS,
ALERT_RECORD_STATUS_LABELS,
OPERATOR_LABELS,
DEVICE_CATALOG_LABELS,
METRIC_LABELS,
INSPECTION_STATUS,
} = require("./types.cjs");
let _scheduler = null;
/**
* 获取标签映射(前端展示用)
*/
function getLabels() {
return {
alert_levels: ALERT_LEVEL_LABELS,
device_tiers: DEVICE_TIER_LABELS,
inspection_status: INSPECTION_STATUS_LABELS,
alert_record_status: ALERT_RECORD_STATUS_LABELS,
operators: OPERATOR_LABELS,
device_catalog: DEVICE_CATALOG_LABELS,
metrics: METRIC_LABELS,
};
}
/**
* 注册巡检模块的 IPC handlers
*
* @param {object} ipcMain - Electron ipcMain
* @param {object} options
* @param {function} options.readStorage - 读取本地存储 (key) => string
* @param {function} options.writeStorage - 写入本地存储 (key, value) => void
*/
function registerInspectionHandlers(ipcMain, options) {
const { readStorage, writeStorage, sessions } = options;
const readFn = (key) => readStorage(key);
const writeFn = (key, val) => writeStorage(key, val);
// 根据主机地址查找会话(用于复用终端 SSH 连接)
function findSessionByHost(hostname) {
const DEBUG = process.env.INSPECTION_DEBUG === "1";
if (DEBUG) console.log("[Inspection] 查找会话,总会话数:", sessions?.size || 0);
if (!sessions) {
if (DEBUG) console.log("[Inspection] sessions 未初始化");
return null;
}
for (const session of sessions.values()) {
if (!session) continue;
// SSH 会话才有 conn
if (!session.conn) continue;
// 匹配主机地址
const sessionHost = session.hostname || session.host || "";
if (DEBUG) {
console.log("[Inspection] SSH会话:", sessionHost, "| 目标:", hostname);
}
if (sessionHost === hostname) {
if (DEBUG) console.log("[Inspection] 匹配到会话:", sessionHost);
return session;
}
}
return null;
}
// ---------- 标签映射 ----------
ipcMain.handle("inspection:getLabels", () => {
return getLabels();
});
// ---------- 系统配置 ----------
ipcMain.handle("inspection:getSysConfig", () => {
return deviceStore.getSysConfig(readFn);
});
ipcMain.handle("inspection:updateSysConfig", (_e, updates) => {
return deviceStore.updateSysConfig(readFn, writeFn, updates);
});
// ---------- 设备类型 ----------
ipcMain.handle("inspection:getCatalog", () => {
return deviceStore.getCatalog();
});
ipcMain.handle("inspection:getTiers", () => {
return deviceStore.getTiers();
});
ipcMain.handle("inspection:getAdapters", () => {
return adapterRegistry.getRegisteredAdapters();
});
// ---------- 设备扩展配置 ----------
ipcMain.handle("inspection:getAllExtensions", () => {
return deviceStore.getAllExtensions(readFn);
});
ipcMain.handle("inspection:getExtension", (_e, hostId) => {
return deviceStore.getExtension(readFn, hostId);
});
ipcMain.handle("inspection:upsertExtension", (_e, hostId, data) => {
return deviceStore.upsertExtension(readFn, writeFn, hostId, data);
});
ipcMain.handle("inspection:deleteExtension", (_e, hostId) => {
return deviceStore.deleteExtension(readFn, writeFn, hostId);
});
ipcMain.handle("inspection:getEffectiveInterval", (_e, hostId) => {
return deviceStore.getEffectiveInterval(readFn, hostId);
});
ipcMain.handle("inspection:getEnabledDevices", () => {
return deviceStore.getEnabledDevices(readFn);
});
// ---------- 告警规则 ----------
ipcMain.handle("inspection:listRules", () => {
return alertRuleStore.listRules(readFn);
});
ipcMain.handle("inspection:getRule", (_e, ruleId) => {
return alertRuleStore.getRule(readFn, ruleId);
});
ipcMain.handle("inspection:createRule", (_e, data) => {
return alertRuleStore.createRule(readFn, writeFn, data);
});
ipcMain.handle("inspection:updateRule", (_e, ruleId, updates) => {
return alertRuleStore.updateRule(readFn, writeFn, ruleId, updates);
});
ipcMain.handle("inspection:deleteRule", (_e, ruleId) => {
return alertRuleStore.deleteRule(readFn, writeFn, ruleId);
});
ipcMain.handle("inspection:toggleRule", (_e, ruleId, enabled) => {
return alertRuleStore.toggleRule(readFn, writeFn, ruleId, enabled);
});
// ---------- 巡检结果 ----------
ipcMain.handle("inspection:getLatestAll", () => {
return inspectionResultStore.getLatestAll(readFn);
});
ipcMain.handle("inspection:getLatestByDevice", (_e, hostId, limit) => {
return inspectionResultStore.getLatestByDevice(readFn, hostId, limit);
});
ipcMain.handle("inspection:getRecentResults", (_e, limit) => {
return inspectionResultStore.getRecent(readFn, limit);
});
// ---------- 单次巡检执行(前端传入完整主机配置) ----------
ipcMain.handle("inspection:runInspection", async (_e, payload) => {
const { hostId, hostConfig, catalogId } = payload || {};
if (!hostId || !hostConfig) {
return { success: false, error: "缺少主机配置" };
}
const DEBUG = process.env.INSPECTION_DEBUG === "1";
if (DEBUG) {
console.log("[Inspection] 巡检配置:", JSON.stringify({
host: hostConfig.hostname || hostConfig.address,
port: hostConfig.port || 22,
username: hostConfig.username,
hasPassword: !!hostConfig.password,
hasPrivateKey: !!hostConfig.privateKeyContent,
hasKeyId: !!hostConfig.identityFileId,
}));
}
const sshConfig = {
host: hostConfig.hostname || hostConfig.address,
port: hostConfig.port || 22,
username: hostConfig.username,
password: hostConfig.password,
privateKey: hostConfig.privateKeyContent,
passphrase: hostConfig.passphrase,
};
if (!sshConfig.host || !sshConfig.username) {
return { success: false, error: "主机地址或用户名缺失" };
}
const startTime = Date.now();
const result = {
id: `ins_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
host_id: hostId,
catalog_id: catalogId || "linux_server",
start_time: startTime,
end_time: 0,
status: INSPECTION_STATUS.SUCCESS,
metrics: {},
raw: {},
error: null,
alerts: [],
};
try {
// 构造 execFn — 优先复用终端会话的 SSH 连接
const execFn = async (command, options = {}) => {
const timeoutMs = options.timeoutMs || 60000;
const DEBUG = process.env.INSPECTION_DEBUG === "1";
// 1. 先尝试复用终端会话的连接
const session = findSessionByHost(sshConfig.host);
if (DEBUG) {
console.log("[Inspection] 查找会话:", sshConfig.host,
"| 找到会话:", session ? "是" : "否",
"| 有连接:", session?.conn ? "是" : "否");
}
if (session && session.conn) {
try {
const result = await executeBoundedSshCommand(session.conn, command, {
openingTimeoutMs: timeoutMs,
runTimeoutMs: timeoutMs,
maxOutputBytes: 100 * 1024,
});
if (DEBUG) console.log("[Inspection] 复用会话成功, stdout:", result.stdout?.slice(0, 100));
return result.stdout + (result.stderr ? "\n" + result.stderr : "");
} catch (e) {
// 终端连接失败,回退到新建连接
if (DEBUG) console.log("[Inspection] 复用会话失败,回退:", e.message);
}
}
// 2. 回退:新建 SSH 连接
return await execSshCommand(sshConfig, command, {
connectTimeoutMs: 10000,
execTimeoutMs: timeoutMs,
});
};
// 构造批量执行函数(一次连接执行多条命令)
const execCommandsFn = async (commands, options = {}) => {
const { execSshCommands } = require("./inspectionExec.cjs");
return await execSshCommands(sshConfig, commands, options);
};
// 获取设备类型对应的采集器类名
const catalogs = deviceStore.getCatalog();
const catalog = catalogs.find((c) => c.id === catalogId);
const adapterClassName = catalog?.adapter_class || "LinuxServerAdapter";
// 创建采集器实例(注入 execFn 和 execCommandsFn)
const adapter = adapterRegistry.createAdapter(adapterClassName, sshConfig, {
execFn,
execCommands: execCommandsFn,
timeoutMs: 20000,
});
if (!adapter) {
throw new Error("采集器加载失败: " + adapterClassName);
}
const collectResult = await adapter.collect();
result.metrics = collectResult.metrics || {};
result.raw = collectResult.raw || {};
if (!collectResult.online) {
result.status = collectResult.status || INSPECTION_STATUS.FAILED;
result.error = collectResult.error || "设备离线或连接失败";
}
} catch (err) {
result.status = INSPECTION_STATUS.FAILED;
result.error = err.message;
console.error("[Inspection] runInspection error:", err.message);
}
result.end_time = Date.now();
const duration = result.end_time - result.start_time;
// 构造设备信息(用于告警记录)
const deviceInfo = {
device_id: hostId,
device_name: hostConfig.label || hostConfig.hostname || hostId,
catalog_id: catalogId || "linux_server",
};
// 保存巡检结果
try {
inspectionResultStore.addResult(readFn, writeFn, {
...result,
device_id: hostId,
online: result.status === INSPECTION_STATUS.SUCCESS,
duration_ms: duration,
error_message: result.error,
});
} catch (e) {
console.warn("[Inspection] 保存巡检结果失败:", e.message);
}
// 告警判断(成功/失败都要判断,特别是 online 指标)
const triggeredAlerts = [];
try {
const deviceExt = deviceStore.getExtension(readFn, hostId);
const rules = alertRuleStore.getRulesForDevice(readFn, deviceExt || { host_id: hostId, catalog_id: catalogId });
const isOnline = result.status === INSPECTION_STATUS.SUCCESS;
for (const rule of rules) {
// online 指标 — 成功/失败都要判断(设备离线告警)
if (rule.metric === "online") {
const shouldAlert = rule.threshold === "false" ? !isOnline : isOnline;
console.log(`[Inspection] 离线规则: name=${rule.name} enabled=${rule.enabled} isOnline=${isOnline} shouldAlert=${shouldAlert}`);
const alertResult = alertRecordStore.processAlert(
readFn,
writeFn,
deviceInfo,
rule,
isOnline ? "在线" : "离线",
shouldAlert
);
if (alertResult) {
console.log(`[Inspection] 告警触发:`, JSON.stringify(alertResult));
triggeredAlerts.push(alertResult);
}
continue;
}
// 普通指标 — 只在成功时判断(失败时没有指标数据)
if (isOnline) {
const metricValue = result.metrics[rule.metric];
if (metricValue === undefined || metricValue === null) continue;
const isTriggered = alertRuleStore.evaluateRule(metricValue, rule.operator, rule.threshold);
const alertResult = alertRecordStore.processAlert(
readFn,
writeFn,
deviceInfo,
rule,
metricValue,
isTriggered
);
if (alertResult) {
triggeredAlerts.push(alertResult);
}
}
}
} catch (e) {
console.warn("[Inspection] 告警判断失败:", e.message);
}
return {
success: result.status === INSPECTION_STATUS.SUCCESS,
status: result.status,
metrics: result.metrics,
error: result.error,
duration,
alerts: triggeredAlerts,
};
});
// ---------- 告警记录 ----------
ipcMain.handle("inspection:listAlerts", (_e, options) => {
return alertRecordStore.listAlerts(readFn, options || {});
});
ipcMain.handle("inspection:getActiveAlerts", () => {
return alertRecordStore.getActiveAlerts(readFn);
});
ipcMain.handle("inspection:getAlertStats", () => {
return alertRecordStore.getAlertStats(readFn);
});
ipcMain.handle("inspection:acknowledgeAlert", (_e, alertId, operator) => {
return alertRecordStore.acknowledgeAlert(readFn, writeFn, alertId, operator);
});
ipcMain.handle("inspection:resolveAlert", (_e, alertId, reason, operator) => {
return alertRecordStore.resolveAlert(readFn, writeFn, alertId, reason, operator);
});
ipcMain.handle("inspection:closeAlert", (_e, alertId, note) => {
return alertRecordStore.closeAlert(readFn, writeFn, alertId, note);
});
ipcMain.handle("inspection:updateAlertNote", (_e, alertId, note) => {
return alertRecordStore.updateNote(readFn, writeFn, alertId, note);
});
// 存储前端传入的设备 SSH 配置(hostId → sshConfig)
let _hostConfigs = null;
// ---------- 调度器控制 ----------
ipcMain.handle("inspection:scheduler:start", (_e, options = {}) => {
// 前端可传入 { hostConfigs: { [hostId]: sshConfig } } 用于调度器执行 SSH 命令
if (options.hostConfigs) {
_hostConfigs = options.hostConfigs;
}
// 从 hostConfigs 提取 hostInfo(hostname/label)用于告警显示
const hostInfo = {};
if (_hostConfigs) {
for (const [hostId, config] of Object.entries(_hostConfigs)) {
hostInfo[hostId] = {
hostname: config.host || config.hostname || "",
label: config.label || "",
};
}
}
if (!_scheduler) {
_scheduler = new InspectionScheduler({
readFn,
writeFn,
execFn: async (hostId, command) => {
// 优先用前端传入的 hostConfigs
if (_hostConfigs && _hostConfigs[hostId]) {
const config = _hostConfigs[hostId];
const { execSshCommand: execCmd } = require("./inspectionExec.cjs");
return await execCmd(config, command, {
connectTimeoutMs: 10000,
execTimeoutMs: 30000,
});
}
// 兜底:尝试复用已有终端会话
const session = findSessionByHost(hostId);
if (session?.conn) {
const { executeBoundedSshCommand } = require("../boundedSshExec.cjs");
return await executeBoundedSshCommand(session.conn, command, {
timeoutMs: 30000,
});
}
throw new Error(`无法执行巡检:未找到主机 ${hostId} 的 SSH 配置`);
},
pollIntervalMs: 30 * 1000,
concurrency: 5,
timeoutMs: 30 * 1000,
hostInfo,
});
}
_scheduler.start();
return _scheduler.getStatus();
});
ipcMain.handle("inspection:scheduler:stop", () => {
if (_scheduler) {
_scheduler.stop();
return _scheduler.getStatus();
}
return { running: false };
});
ipcMain.handle("inspection:scheduler:status", () => {
return _scheduler ? _scheduler.getStatus() : { running: false };
});
ipcMain.handle("inspection:scheduler:scanNow", (_e, hostId) => {
if (!_scheduler) return { error: "调度器未启动" };
return _scheduler.scanDeviceNow(hostId);
});
ipcMain.handle("inspection:scheduler:scanAllNow", () => {
if (!_scheduler) return { error: "调度器未启动" };
return _scheduler.scanAllNow();
});
console.log("[Inspection] IPC handlers 已注册");
return {
getScheduler: () => _scheduler,
createScheduler: (opts) => {
_scheduler = new InspectionScheduler({
readFn,
writeFn,
execFn: execCommand,
...opts,
});
return _scheduler;
},
};
}
module.exports = {
registerInspectionHandlers,
getLabels,
};

View File

@@ -0,0 +1,417 @@
"use strict";
/**
* 巡检执行器 - 用 ssh2 临时建立 SSH 连接执行命令
*
* 用于设备巡检的命令执行:每次巡检临时建立连接,执行完即释放,
* 不占用 terminalWorker 的连接池(巡检是低频后台任务)。
*/
const { Client: SSHClient } = require("ssh2");
const { buildAlgorithms } = require("../sshAlgorithms.cjs");
const DEFAULT_CONNECT_TIMEOUT_MS = 10_000;
const DEFAULT_EXEC_TIMEOUT_MS = 30_000;
const DEFAULT_MAX_OUTPUT_BYTES = 100 * 1024; // 100KB
/**
* 执行单条 SSH 命令(使用 pty shell 方式,兼容交换机等需要交互式 shell 的设备)
* @param {object} connConfig - ssh2 连接配置(host/port/username/password/privateKey 等)
* @param {string} command - 要执行的命令
* @param {object} [options]
* @param {number} [options.connectTimeoutMs]
* @param {number} [options.execTimeoutMs]
* @param {number} [options.maxOutputBytes]
* @returns {Promise<string>} 命令输出
*/
function execSshCommand(connConfig, command, options = {}) {
const connectTimeoutMs = options.connectTimeoutMs || DEFAULT_CONNECT_TIMEOUT_MS;
const execTimeoutMs = options.execTimeoutMs || DEFAULT_EXEC_TIMEOUT_MS;
const maxOutputBytes = options.maxOutputBytes || DEFAULT_MAX_OUTPUT_BYTES;
const DEBUG = process.env.INSPECTION_DEBUG === "1";
if (DEBUG) {
console.log("[InspectExec] SSH host:", connConfig.host, "| cmd:", command.slice(0, 100));
}
return new Promise((resolve, reject) => {
const client = new SSHClient();
let settled = false;
let stdout = "";
let execTimer = null;
let connectTimer = null;
let commandSent = false;
if (DEBUG) {
client.on("banner", (msg) => console.log("[InspectExec] banner:", msg.slice(0, 100)));
client.on("handshake", () => console.log("[InspectExec] handshake OK"));
client.on("error", (err) => console.error("[InspectExec] client error:", err.message));
client.on("close", () => console.log("[InspectExec] client closed"));
}
const cleanup = () => {
if (execTimer) clearTimeout(execTimer);
if (connectTimer) clearTimeout(connectTimer);
try { client.end(); } catch (e) { /* ignore */ }
};
const done = (err, result) => {
if (settled) return;
settled = true;
cleanup();
if (err) reject(err);
else resolve(result);
};
// 连接超时
connectTimer = setTimeout(() => {
console.error("[InspectExec] 连接超时,host:", connConfig.host);
done(new Error("SSH 连接超时(" + connectTimeoutMs / 1000 + "s)"));
}, connectTimeoutMs);
client.on("ready", () => {
if (DEBUG) console.log("[InspectExec] SSH ready,打开 pty shell...");
clearTimeout(connectTimer);
// 使用 pty shell(交互式)执行命令
client.shell({ term: "vt100", cols: 80, rows: 24 }, (err, stream) => {
if (err) {
console.error("[InspectExec] shell 失败:", err.message);
done(new Error("打开 shell 失败: " + err.message));
return;
}
// 执行超时
execTimer = setTimeout(() => {
console.error("[InspectExec] 命令执行超时(" + execTimeoutMs / 1000 + "s)");
done(new Error("命令执行超时"));
}, execTimeoutMs);
let outputBuffer = "";
let lastLogTime = 0;
stream.on("data", (data) => {
const chunk = data.toString("utf-8");
outputBuffer += chunk;
// 检测是否出现命令提示符
if (!commandSent) {
// 交换机提示符格式: <hostname> 或 hostname>
if (chunk.includes(">") || chunk.includes("<")) {
commandSent = true;
stream.write(command + "\n");
if (DEBUG) console.log("[InspectExec] 已发送命令:", command);
}
}
// 命令发送后,收集输出
if (commandSent) {
// 检测分页提示,发送 'q' 退出
if (outputBuffer.includes("-- More --") || outputBuffer.includes("---- More ----")) {
if (!outputBuffer.includes("__sent_q__")) {
stream.write("q");
outputBuffer += "__sent_q__"; // 避免重复发送
if (DEBUG) console.log("[InspectExec] 检测到分页,发送 q");
}
}
// 检测输出完成:出现新提示符
const lines = outputBuffer.split("\n");
if (lines.length > 5) {
// 检查倒数第二行是否是提示符(因为最后一行为空)
const lastContentLine = lines[lines.length - 2]?.trim() || "";
if (lastContentLine.includes(">") || lastContentLine.includes("#")) {
stdout = outputBuffer.replace(/__sent_q__/g, "");
clearTimeout(execTimer);
if (DEBUG) console.log("[InspectExec] 命令完成,行数:", lines.length);
done(null, stdout);
}
}
}
// 防止缓冲区过大
if (outputBuffer.length > maxOutputBytes * 2) {
stdout = outputBuffer.replace(/__sent_q__/g, "");
clearTimeout(execTimer);
if (DEBUG) console.log("[InspectExec] 缓冲区满,强制返回");
done(null, stdout);
}
});
stream.stderr.on("data", (data) => {
// stderr 通常为空
});
stream.on("close", () => {
clearTimeout(execTimer);
if (!commandSent) {
// 没等到提示符就关闭了,再试一次发送命令
commandSent = true;
stream.write(command + "\n");
} else if (stdout) {
// 已有输出,正常结束
done(null, stdout);
} else {
done(new Error("连接意外关闭"));
}
});
stream.on("error", (e) => {
console.error("[InspectExec] stream error:", e.message);
done(new Error("流错误: " + e.message));
});
});
});
client.on("error", (err) => {
clearTimeout(connectTimer);
console.error("[InspectExec] SSH 连接失败:", err.message);
done(new Error("SSH 连接失败: " + err.message));
});
try {
client.connect({
host: connConfig.host,
port: connConfig.port || 22,
username: connConfig.username,
password: connConfig.password || undefined,
privateKey: connConfig.privateKey || undefined,
passphrase: connConfig.passphrase || undefined,
readyTimeout: connectTimeoutMs,
// 跳过主机密钥验证
hostVerifier: () => true,
// 使用 NetMesh 的算法构建器,启用旧算法兼容,跳过 ECDSA 主机密钥
algorithms: buildAlgorithms(true, { skipEcdsaHostKey: true }),
});
} catch (e) {
clearTimeout(connectTimer);
done(e);
}
});
}
/**
* 批量执行多条 SSH 命令(一次连接,执行多条命令)
* @param {object} connConfig - ssh2 连接配置
* @param {string[]} commands - 命令数组
* @param {object} [options]
* @returns {Promise<object>} { outputs: { command, stdout }[] }
*/
function execSshCommands(connConfig, commands, options = {}) {
const connectTimeoutMs = options.connectTimeoutMs || DEFAULT_CONNECT_TIMEOUT_MS;
const totalTimeoutMs = options.totalTimeoutMs || 180000; // 总超时 3 分钟
const maxOutputBytes = options.maxOutputBytes || DEFAULT_MAX_OUTPUT_BYTES;
const DEBUG = process.env.INSPECTION_DEBUG === "1";
if (DEBUG) {
console.log("[InspectExec] 批量执行:", connConfig.host, "| 命令数:", commands.length);
}
return new Promise((resolve, reject) => {
const client = new SSHClient();
let settled = false;
let connectTimer = null;
let totalTimer = null;
let cmdTimeout = null;
let outputBuffer = "";
let commandIndex = 0;
let phase = "wait-login"; // wait-login → disable-paging → idle → waiting-output → done
let disableRetries = 0;
const results = [];
const cleanup = () => {
if (connectTimer) clearTimeout(connectTimer);
if (totalTimer) clearTimeout(totalTimer);
if (cmdTimeout) clearTimeout(cmdTimeout);
try { client.end(); } catch (e) { /* ignore */ }
};
const done = (err, result) => {
if (settled) return;
settled = true;
cleanup();
if (err) reject(err);
else resolve(result);
};
// 总超时
totalTimer = setTimeout(() => {
done(new Error("批量命令执行超时(" + totalTimeoutMs / 1000 + "s)"));
}, totalTimeoutMs);
// 匹配网络设备提示符: 取最后一行,检查是 <hostname> 或 hostname>/hostname# 格式
function hasPrompt(text) {
const lines = (text || '').split('\n');
// 从后往前找非空行
let lastLine = '';
for (let i = lines.length - 1; i >= 0; i--) {
const trimmed = lines[i].trimEnd();
if (trimmed.length > 0) {
lastLine = trimmed;
break;
}
}
if (DEBUG) {
console.log("[InspectExec] hasPrompt check:", JSON.stringify(lastLine.slice(-100)));
}
// <hostname> 格式(H3C/华为 VRP)
if (/<[\w\-./]+>\s*$/.test(lastLine)) return true;
// hostname> 或 hostname# 格式
if (/[\w\-./]+[>#]\s*$/.test(lastLine)) return true;
return false;
}
client.on("ready", () => {
if (DEBUG) console.log("[InspectExec] SSH ready,打开 shell...");
clearTimeout(connectTimer);
client.shell({ term: "vt100", cols: 200, rows: 24 }, (err, stream) => {
if (err) {
done(new Error("打开 shell 失败: " + err.message));
return;
}
let phase = "wait-login"; // wait-login → disable-paging → idle → waiting-output → done
let disableRetries = 0;
stream.on("data", (data) => {
const chunk = data.toString("utf-8");
outputBuffer += chunk;
if (DEBUG && outputBuffer.length % 1000 < 100) {
// 每隔约 1KB 打印一次状态(避免刷屏)
const lines = outputBuffer.split('\n');
console.log(`[InspectExec] phase=${phase} bufLen=${outputBuffer.length} lastLines=${JSON.stringify(lines.slice(-3))}`);
}
if (phase === "wait-login") {
if (hasPrompt(outputBuffer)) {
phase = "disable-paging";
outputBuffer = "";
if (DEBUG) console.log("[InspectExec] 登录成功,发送 screen-length 0 关闭分页");
stream.write("screen-length 0 temporary\n");
}
} else if (phase === "disable-paging") {
if (hasPrompt(outputBuffer)) {
phase = "idle";
outputBuffer = "";
if (DEBUG) console.log("[InspectExec] 分页已关闭");
sendNextCommand();
} else if (outputBuffer.length > 500 && disableRetries < 1) {
disableRetries++;
outputBuffer = "";
stream.write("screen-length 0 temporary\n");
if (DEBUG) console.log("[InspectExec] 重试关闭分页");
}
} else if (phase === "waiting-output") {
// 处理分页提示符:H3C/华为交换机可能在输出中途显示 "-- More --"
// 注意:如果 switch 不认识 screen-length 0,分页仍然会发生
const moreIdx = outputBuffer.indexOf("-- More --");
if (moreIdx !== -1 || outputBuffer.indexOf("---- More ----") !== -1) {
if (DEBUG) console.log("[InspectExec] 检测到分页,发送空格继续");
stream.write(" ");
// 把分页提示符从缓冲区移除,避免干扰
outputBuffer = outputBuffer.replace(/--\s*More\s*--/gi, "").replace(/----\s*More\s*----/gi, "");
return;
}
if (hasPrompt(outputBuffer)) {
// 命令输出完整,保存
const cmdOutput = cleanOutput(outputBuffer);
results[results.length - 1].output = cmdOutput;
outputBuffer = "";
phase = "idle";
if (DEBUG) console.log("[InspectExec] 命令输出完成,进入 idle");
sendNextCommand();
}
}
});
stream.on("close", () => {
if (phase === "done") {
// 主动 quit 关闭的,正常 resolve
if (!settled) done(null, { results });
return;
}
if (!settled) {
done(new Error("连接意外关闭"));
}
});
stream.on("error", (e) => {
done(new Error("流错误: " + e.message));
});
function sendNextCommand() {
if (commandIndex >= commands.length) {
if (DEBUG) console.log("[InspectExec] 所有命令执行完毕");
phase = "done";
stream.write("quit\n");
setTimeout(() => done(null, { results }), 500);
return;
}
const cmd = commands[commandIndex];
if (DEBUG) console.log("[InspectExec] 发送命令 " + (commandIndex + 1) + "/" + commands.length + ":", cmd.slice(0, 50));
stream.write(cmd + "\n");
results.push({ command: cmd, output: "" });
commandIndex++;
phase = "waiting-output";
// 每个命令 15 秒超时,防止卡住
if (cmdTimeout) clearTimeout(cmdTimeout);
cmdTimeout = setTimeout(() => {
if (phase === "waiting-output") {
if (DEBUG) console.log("[InspectExec] 命令超时,强制完成");
const cmdOutput = cleanOutput(outputBuffer);
results[results.length - 1].output = cmdOutput;
outputBuffer = "";
phase = "idle";
sendNextCommand();
}
}, 15000);
}
function cleanOutput(text) {
return (text || "")
.replace(/<[^>]+>/g, "")
.replace(/--\s*More\s*--/gi, "")
.replace(/----\s*More\s*----/gi, "")
.replace(/\r/g, "")
.trim();
}
});
});
client.on("error", (err) => {
clearTimeout(connectTimer);
done(new Error("SSH 连接失败: " + err.message));
});
try {
client.connect({
host: connConfig.host,
port: connConfig.port || 22,
username: connConfig.username,
password: connConfig.password || undefined,
privateKey: connConfig.privateKey || undefined,
passphrase: connConfig.passphrase || undefined,
readyTimeout: connectTimeoutMs,
hostVerifier: () => true,
algorithms: buildAlgorithms(true, { skipEcdsaHostKey: true }),
// 保活配置,防止交换机 VTY 超时断开
keepaliveInterval: 30000, // 每 30 秒发送一次心跳
keepaliveCountMax: 3, // 最多重试 3 次
});
} catch (e) {
done(e);
}
});
}
module.exports = {
execSshCommand,
execSshCommands,
DEFAULT_CONNECT_TIMEOUT_MS,
DEFAULT_EXEC_TIMEOUT_MS,
};

View File

@@ -0,0 +1,126 @@
"use strict";
/**
* 巡检结果存储
*
* 保存每次巡检的结果数据,支持按设备查询历史。
* 为避免数据量过大,仅保留最近 N 条记录(默认 1000 条)。
*/
const { STORAGE_KEY_INSPECTION_RESULTS } = require("./storageKeys.cjs");
const { INSPECTION_STATUS } = require("./types.cjs");
const MAX_RESULTS = 1000;
let _results = null;
function _ensureLoaded(readFn) {
if (_results === null) {
const raw = readFn(STORAGE_KEY_INSPECTION_RESULTS);
_results = raw ? _safeParse(raw, []) : [];
}
}
function _safeParse(text, def) {
try { return JSON.parse(text); }
catch (e) { return def; }
}
function _save(writeFn) {
if (_results.length > MAX_RESULTS) {
_results = _results.slice(-MAX_RESULTS);
}
writeFn(STORAGE_KEY_INSPECTION_RESULTS, JSON.stringify(_results));
}
// ---------- 增删改查 ----------
/**
* 新增一条巡检结果
*/
function addResult(readFn, writeFn, result) {
_ensureLoaded(readFn);
const record = {
id: `result_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
job_id: result.job_id || null,
device_id: result.device_id, // host_id
timestamp: Date.now(),
online: result.online,
status: result.status || INSPECTION_STATUS.SUCCESS,
duration_ms: result.duration_ms || 0,
metrics: result.metrics || {},
alerts: result.alerts || [],
error_message: result.error_message || null,
};
_results.push(record);
_save(writeFn);
return { ...record };
}
/**
* 获取指定设备的最近巡检结果
*/
function getLatestByDevice(readFn, hostId, limit = 20) {
_ensureLoaded(readFn);
return _results
.filter((r) => r.device_id === hostId)
.sort((a, b) => b.timestamp - a.timestamp)
.slice(0, limit);
}
/**
* 获取所有设备的最新一次巡检结果
*/
function getLatestAll(readFn) {
_ensureLoaded(readFn);
const latest = {};
for (const r of _results) {
if (!latest[r.device_id] || r.timestamp > latest[r.device_id].timestamp) {
latest[r.device_id] = r;
}
}
return Object.values(latest);
}
/**
* 获取最近 N 条结果
*/
function getRecent(readFn, limit = 50) {
_ensureLoaded(readFn);
return [..._results].sort((a, b) => b.timestamp - a.timestamp).slice(0, limit);
}
/**
* 根据时间范围查询
*/
function getByTimeRange(readFn, startTime, endTime) {
_ensureLoaded(readFn);
return _results.filter(
(r) => r.timestamp >= startTime && r.timestamp <= endTime
).sort((a, b) => b.timestamp - a.timestamp);
}
/**
* 清空结果(手动清理)
*/
function clearResults(readFn, writeFn) {
_results = [];
_save(writeFn);
}
// ---------- 重置(测试用) ----------
function _resetForTest() {
_results = null;
}
module.exports = {
addResult,
getLatestByDevice,
getLatestAll,
getRecent,
getByTimeRange,
clearResults,
_resetForTest,
};

View File

@@ -0,0 +1,383 @@
"use strict";
/**
* 巡检调度引擎
*
* 职责:
* 1. 定时扫描启用了巡检的设备
* 2. 根据各设备的巡检周期,判断是否需要执行
* 3. 并发执行多台设备的巡检
* 4. 采集指标 → 判断告警 → 写入结果和告警记录
*
* 设计说明:
* - 每 30 秒轮询一次(检查哪些设备到巡检时间了)
* - 并发数默认 5,可配置
* - 单设备超时默认 30 秒
* - 巡检结果通过回调通知上层(更新 UI 用)
*/
const deviceStore = require("./deviceStore.cjs");
const alertRuleStore = require("./alertRuleStore.cjs");
const inspectionResultStore = require("./inspectionResultStore.cjs");
const alertRecordStore = require("./alertRecordStore.cjs");
const adapterRegistry = require("./adapterRegistry.cjs");
const { INSPECTION_STATUS } = require("./types.cjs");
const DEFAULT_POLL_INTERVAL_MS = 30 * 1000; // 30 秒轮询一次
const DEFAULT_CONCURRENCY = 5;
const DEFAULT_DEVICE_TIMEOUT_MS = 30 * 1000;
class InspectionScheduler {
/**
* @param {object} options
* @param {function} options.readFn - 读取存储 (key) => string
* @param {function} options.writeFn - 写入存储 (key, value) => void
* @param {function} options.getConnectionFn - 获取 SSH 连接 (hostId) => connection 或 null
* @param {function} options.execFn - 执行命令 (hostId, command) => Promise<string>
* @param {function} [options.onResult] - 单设备巡检完成回调
* @param {number} [options.pollIntervalMs] - 轮询间隔
* @param {number} [options.concurrency] - 并发数
* @param {number} [options.timeoutMs] - 单设备超时
*/
constructor(options) {
this.readFn = options.readFn;
this.writeFn = options.writeFn;
this.getConnectionFn = options.getConnectionFn;
this.execFn = options.execFn;
this.onResult = options.onResult || (() => {});
this.pollIntervalMs = options.pollIntervalMs || DEFAULT_POLL_INTERVAL_MS;
this.concurrency = options.concurrency || DEFAULT_CONCURRENCY;
this.timeoutMs = options.timeoutMs || DEFAULT_DEVICE_TIMEOUT_MS;
// hostId → { hostname, label } 映射,用于告警记录显示设备名称
this._hostInfo = options.hostInfo || {};
this._timer = null;
this._running = false;
this._scanQueue = []; // 待巡检的设备队列
this._activeScans = 0; // 当前正在执行的巡检数
}
// ---------- 生命周期 ----------
start() {
if (this._running) return;
this._running = true;
console.log("[InspectionScheduler] 巡检调度器已启动");
// 启动时立即跑一轮(异步,不阻塞 start 返回)
setImmediate(() => this._tick());
}
stop() {
this._running = false;
if (this._timer) {
clearTimeout(this._timer);
this._timer = null;
}
console.log("[InspectionScheduler] 巡检调度器已停止");
}
isRunning() {
return this._running;
}
_scheduleNextTick() {
if (!this._running) return;
this._timer = setTimeout(() => this._tick(), this.pollIntervalMs);
}
// ---------- 主循环 ----------
async _tick() {
if (!this._running) return;
try {
// 1. 获取所有启用巡检的设备
const devices = deviceStore.getEnabledDevices(this.readFn);
// 2. 筛选出到期需要巡检的设备
const now = Date.now();
const dueDevices = devices.filter((dev) => {
const intervalMs = dev.effective_interval * 60 * 1000;
const lastScan = dev.last_scan_at || 0;
return now - lastScan >= intervalMs;
});
if (dueDevices.length > 0) {
console.log(`[InspectionScheduler] 到期设备 ${dueDevices.length} 台,加入巡检队列`);
// 加入队列
for (const dev of dueDevices) {
if (!this._scanQueue.find((d) => d.host_id === dev.host_id)) {
this._scanQueue.push(dev);
}
}
// 启动并发执行
this._processQueue();
}
} catch (e) {
console.error("[InspectionScheduler] 调度异常:", e.message);
}
// 3. 安排下一次轮询
this._scheduleNextTick();
}
_processQueue() {
while (this._activeScans < this.concurrency && this._scanQueue.length > 0) {
const device = this._scanQueue.shift();
this._activeScans++;
this._scanDevice(device).finally(() => {
this._activeScans--;
// 继续处理队列
this._processQueue();
});
}
}
// ---------- 单设备巡检 ----------
async _scanDevice(deviceExt) {
const startTime = Date.now();
const hostId = deviceExt.host_id;
try {
// 1. 获取设备类型信息
const catalog = deviceStore.getCatalogById(deviceExt.catalog_id);
if (!catalog) {
throw new Error(`未找到设备类型: ${deviceExt.catalog_id}`);
}
// 2. 创建采集器
const adapter = adapterRegistry.createAdapter(
catalog.adapter_class,
{ host: hostId },
{
execFn: async (cmd) => {
if (!this.execFn) throw new Error("execFn 未配置");
return this.execFn(hostId, cmd);
},
timeoutMs: this.timeoutMs,
}
);
if (!adapter) {
throw new Error(`采集器未注册: ${catalog.adapter_class}`);
}
// 3. 执行采集(带超时)
const result = await this._withTimeout(
adapter.collect(),
this.timeoutMs,
"巡检超时"
);
const durationMs = Date.now() - startTime;
// 4. 保存巡检结果
const record = inspectionResultStore.addResult(
this.readFn,
this.writeFn,
{
device_id: hostId,
online: result.online,
status: result.status,
duration_ms: durationMs,
metrics: result.metrics,
alerts: [],
error_message: result.error,
}
);
// 5. 更新设备最后巡检状态
deviceStore.updateLastScan(
this.readFn,
this.writeFn,
hostId,
result.status,
result.error
);
// 6. 判断告警
const triggeredAlerts = this._evaluateAlerts(deviceExt, catalog, result.metrics);
record.alerts = triggeredAlerts;
// 7. 回调通知
this.onResult({
device_id: hostId,
result: record,
alerts: triggeredAlerts,
});
return record;
} catch (e) {
const durationMs = Date.now() - startTime;
// 失败也要记录
const record = inspectionResultStore.addResult(
this.readFn,
this.writeFn,
{
device_id: hostId,
online: false,
status: e.message.includes("超时")
? INSPECTION_STATUS.TIMEOUT
: INSPECTION_STATUS.FAILED,
duration_ms: durationMs,
metrics: {},
alerts: [],
error_message: e.message,
}
);
deviceStore.updateLastScan(
this.readFn,
this.writeFn,
hostId,
e.message.includes("超时") ? INSPECTION_STATUS.TIMEOUT : INSPECTION_STATUS.FAILED,
e.message
);
// 离线告警(设备离线也要触发告警)
const triggeredAlerts = this._evaluateAlerts(deviceExt, null, { online: false });
record.alerts = triggeredAlerts;
this.onResult({
device_id: hostId,
result: record,
alerts: triggeredAlerts,
error: e.message,
});
return record;
}
}
// ---------- 告警判断 ----------
_evaluateAlerts(deviceExt, catalog, metrics) {
const triggered = [];
try {
// 获取该设备适用的规则
const rules = alertRuleStore.getRulesForDevice(this.readFn, deviceExt);
const hostInfo = this._hostInfo[deviceExt.host_id] || {};
const deviceName = hostInfo.label || hostInfo.hostname || deviceExt.host_id;
console.log(`[InspectionScheduler] 告警评估 device=${deviceName}(${deviceExt.host_id}) online=${metrics.online} 规则数=${rules.length}`);
for (const rule of rules) {
// 特殊处理:online 指标
if (rule.metric === "online") {
const isOnline = metrics.online !== false;
// threshold 为 "false" 表示离线时告警
const shouldAlert = rule.threshold === "false" ? !isOnline : isOnline;
console.log(`[InspectionScheduler] 离线规则: name=${rule.name} enabled=${rule.enabled} threshold=${rule.threshold} isOnline=${isOnline} shouldAlert=${shouldAlert}`);
const alertResult = alertRecordStore.processAlert(
this.readFn,
this.writeFn,
{ device_id: deviceExt.host_id, device_name: deviceName },
rule,
isOnline ? "在线" : "离线",
shouldAlert
);
if (alertResult) {
console.log(`[InspectionScheduler] 告警触发:`, JSON.stringify(alertResult));
triggered.push(alertResult);
}
continue;
}
// 普通数值指标
const value = this._getMetricValue(metrics, rule.metric);
if (value === null || value === undefined) continue;
const isTriggered = alertRuleStore.evaluateRule(value, rule.operator, rule.threshold);
const hostInfo = this._hostInfo[deviceExt.host_id] || {};
const deviceName = hostInfo.label || hostInfo.hostname || deviceExt.host_id;
const alertResult = alertRecordStore.processAlert(
this.readFn,
this.writeFn,
{ device_id: deviceExt.host_id, device_name: deviceName },
rule,
value,
isTriggered
);
if (alertResult) triggered.push(alertResult);
}
} catch (e) {
console.error("[InspectionScheduler] 告警判断异常:", e.message);
}
return triggered;
}
_getMetricValue(metrics, metricKey) {
if (!metrics) return null;
// 直接取
if (metrics[metricKey] !== undefined) return metrics[metricKey];
// 磁盘最高使用率
if (metricKey === "disk_usage" && Array.isArray(metrics.disk_usage)) {
return Math.max(...metrics.disk_usage.map((d) => d.usage_percent || 0));
}
return null;
}
// ---------- 手动巡检 ----------
/**
* 手动触发单设备巡检
*/
async scanDeviceNow(hostId) {
const ext = deviceStore.getExtension(this.readFn, hostId);
if (!ext) throw new Error(`设备未配置巡检: ${hostId}`);
const interval = deviceStore.getEffectiveInterval(this.readFn, hostId);
const device = { ...ext, effective_interval: interval };
// 加入队列头部
this._scanQueue.unshift(device);
this._processQueue();
return { queued: true, host_id: hostId };
}
/**
* 手动触发所有启用巡检的设备
*/
async scanAllNow() {
const devices = deviceStore.getEnabledDevices(this.readFn);
let count = 0;
for (const dev of devices) {
if (!this._scanQueue.find((d) => d.host_id === dev.host_id)) {
this._scanQueue.push(dev);
count++;
}
}
this._processQueue();
return { queued: count };
}
// ---------- 工具 ----------
_withTimeout(promise, ms, message) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error(message || "超时")), ms);
promise.then(
(v) => { clearTimeout(timer); resolve(v); },
(e) => { clearTimeout(timer); reject(e); }
);
});
}
getStatus() {
return {
running: this._running,
queue_size: this._scanQueue.length,
active_scans: this._activeScans,
concurrency: this.concurrency,
};
}
}
module.exports = InspectionScheduler;

View File

@@ -0,0 +1,89 @@
"use strict";
/**
* 巡检模块主进程存储适配器
*
* 使用 userData 目录下的 JSON 文件存储巡检相关数据(设备配置、告警规则、巡检结果等)。
* 提供与 localStorage 类似的 key-value 接口,方便注入到 store 模块。
*/
const fs = require("node:fs");
const path = require("node:path");
const STORAGE_FILENAME = "inspection-data.json";
let _storageCache = null;
let _filePath = null;
let _dirty = false;
let _saveTimer = null;
function init(userDataDir) {
_filePath = path.join(userDataDir, STORAGE_FILENAME);
_loadFromDisk();
}
function _loadFromDisk() {
if (!_filePath) {
_storageCache = {};
return;
}
try {
if (fs.existsSync(_filePath)) {
const raw = fs.readFileSync(_filePath, "utf-8");
_storageCache = JSON.parse(raw);
} else {
_storageCache = {};
}
} catch (e) {
console.error("[Inspection Storage] 加载失败:", e?.message);
_storageCache = {};
}
}
function _scheduleSave() {
if (_saveTimer) return;
_saveTimer = setTimeout(() => {
_saveTimer = null;
_flushToDisk();
}, 200);
}
function _flushToDisk() {
if (!_filePath || !_dirty) return;
try {
const dir = path.dirname(_filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(_filePath, JSON.stringify(_storageCache, null, 2), "utf-8");
_dirty = false;
} catch (e) {
console.error("[Inspection Storage] 保存失败:", e?.message);
}
}
function readStorage(key) {
if (!_storageCache) _loadFromDisk();
return _storageCache[key] ?? null;
}
function writeStorage(key, value) {
if (!_storageCache) _loadFromDisk();
if (value === null || value === undefined) {
delete _storageCache[key];
} else {
_storageCache[key] = value;
}
_dirty = true;
_scheduleSave();
}
function flushSync() {
_flushToDisk();
}
module.exports = {
init,
readStorage,
writeStorage,
flushSync,
};

View File

@@ -0,0 +1,173 @@
"use strict";
/**
* Prompt 解析器 - 从命令输出中提取指标
*
* 分层策略:
* 1. 正则匹配(快,简单场景够用)
* 2. 返回原始输出(解析失败时让用户看原始数据)
*/
/**
* 华为/H3C 交换机命令解析配置
*/
const HUAWEI_H3C_PATTERNS = {
cpu_usage: {
// CPU 使用率:多种可能的格式
patterns: [
/CPU\s+Usage\s*:\s*(\d+)%/i,
/CPU\s+utilization.*one\s+minute:\s*(\d+)%/i,
/CPU\s+Usage\s+:\s*(\d+)%/i,
],
default: null,
},
memory_usage: {
// 内存使用率:多种格式
patterns: [
/Memory\s+Using\s+Percentage\s+Is:\s*(\d+)%/i,
/Memory\s+utilization.*:\s*(\d+)%/i,
],
default: null,
},
};
/**
* 解析单行指标
* @param {string} output - 命令原始输出
* @param {object} config - { patterns: RegExp[], default: any }
* @returns {any} 解析结果
*/
function parseMetric(output, config) {
if (!output || !config?.patterns) return config?.default ?? null;
for (const pattern of config.patterns) {
const match = output.match(pattern);
if (match) {
return parseFloat(match[1]);
}
}
return config.default ?? null;
}
/**
* 解析端口状态
* @param {string} output - 命令输出
* @returns {object} { up_count, down_count, ports }
*/
function parseInterfaceBrief(output) {
const lines = output.split("\n");
const ports = [];
let upCount = 0;
let downCount = 0;
for (const line of lines) {
// 匹配端口行:GigabitEthernet0/0/1 up up
const portMatch = line.match(/^(GigabitEthernet|FastEthernet|Ethernet)(\S+)\s+(\w+)\s+(\w+)/);
if (portMatch) {
const phy = portMatch[3].toLowerCase();
const proto = portMatch[4].toLowerCase();
ports.push({
name: portMatch[1] + portMatch[2],
phy,
protocol: proto,
is_up: phy === "up" && proto === "up",
});
if (phy === "up") upCount++;
else downCount++;
}
}
return { up_count: upCount, down_count: downCount, ports };
}
/**
* 解析电源状态
* @param {string} output - 命令输出
* @returns {object} { normal_count, abnormal_count }
*/
function parsePowerStatus(output) {
const lines = output.split("\n");
let normalCount = 0;
let abnormalCount = 0;
for (const line of lines) {
// 匹配电源状态行
if (/Normal|Supply|OK/i.test(line)) {
normalCount++;
} else if (/Absent|Fault|Error/i.test(line)) {
abnormalCount++;
}
}
return { normal_count: normalCount, abnormal_count: abnormalCount };
}
/**
* 清理交换机输出
* @param {string} output - 原始输出
* @returns {string} 清理后的输出
*/
function cleanOutput(output) {
return output
// 移除提示符
.replace(/<\w+[-A-Za-z0-9]*>/g, "")
// 移除分页标记
.replace(/---- More ----/g, "")
// 移除命令回显
.replace(/^[^\r\n]*\r?\n/, "")
// 移除空行
.replace(/\n{3,}/g, "\n\n")
.trim();
}
/**
* 使用 prompt 风格解析交换机输出
* @param {string} output - 原始命令输出
* @param {object} context - { catalog_id, device_info }
* @returns {object} 结构化指标
*/
function parseSwitchOutput(output, context = {}) {
const cleaned = cleanOutput(output);
const lines = cleaned.split("\n");
const metrics = {
cpu_usage: null,
memory_usage: null,
port_status: [],
power_status: [],
fan_status: [],
};
// 解析 CPU
metrics.cpu_usage = parseMetric(cleaned, HUAWEI_H3C_PATTERNS.cpu_usage);
// 解析内存
metrics.memory_usage = parseMetric(cleaned, HUAWEI_H3C_PATTERNS.memory_usage);
// 解析端口
metrics.port_status = parseInterfaceBrief(cleaned);
// 解析电源
metrics.power_status = parsePowerStatus(cleaned);
// 原始输出(便于调试)
metrics._raw_output = cleaned;
metrics._parse_errors = [];
if (metrics.cpu_usage === null) {
metrics._parse_errors.push("cpu_usage: 无法解析");
}
if (metrics.memory_usage === null) {
metrics._parse_errors.push("memory_usage: 无法解析");
}
return metrics;
}
module.exports = {
parseMetric,
parseInterfaceBrief,
parsePowerStatus,
cleanOutput,
parseSwitchOutput,
};

View File

@@ -0,0 +1,35 @@
"use strict";
/**
* 巡检模块 - 存储键定义
*
* 所有巡检相关的 localStorage 键统一在此集中管理。
*/
const PREFIX = "netcatty_inspection_";
// ---------- 系统配置 ----------
exports.STORAGE_KEY_SYS_CONFIG = `${PREFIX}sys_config_v1`;
// ---------- 设备类型 ----------
exports.STORAGE_KEY_DEVICE_CATALOG = `${PREFIX}device_catalog_v1`;
// ---------- 设备扩展(巡检配置) ----------
exports.STORAGE_KEY_DEVICE_EXTENSION = `${PREFIX}device_extension_v1`;
// ---------- 告警规则 ----------
exports.STORAGE_KEY_ALERT_RULES = `${PREFIX}alert_rules_v1`;
// ---------- 巡检结果 ----------
exports.STORAGE_KEY_INSPECTION_RESULTS = `${PREFIX}inspection_results_v1`;
// ---------- 告警记录 ----------
exports.STORAGE_KEY_ALERT_RECORDS = `${PREFIX}alert_records_v1`;
// ---------- 告警持续计数(防抖动) ----------
exports.STORAGE_KEY_ALERT_COUNTERS = `${PREFIX}alert_counters_v1`;
// ---------- 调度器状态 ----------
exports.STORAGE_KEY_SCHEDULER_STATE = `${PREFIX}scheduler_state_v1`;
exports.STORAGE_KEY_PREFIX = PREFIX;

View File

@@ -0,0 +1,42 @@
"use strict";
/**
* 巡检模块 - 本地存储封装
*
* 使用 NetMesh 的 localStorageAdapter 机制存储巡检数据。
* 所有存储 key 统一加前缀 netcatty_inspection_。
*/
const { STORAGE_KEY_PREFIX } = require("./storageKeys.cjs");
/**
* 从 localStorage 读取数据(主进程侧通过 IPC 访问,这里提供 key 和默认值)
* @param {string} key - 存储键(不含前缀)
* @param {*} defaultValue - 默认值
* @returns {*}
*/
function getStore() {
// 主进程中,通过全局共享的 storage 方式
// 实际存储由 electron 的 localStorageAdapter 管理
// 这里提供纯函数式的读写接口,外层调用方负责实际 I/O
return {
getKey(key) {
return `${STORAGE_KEY_PREFIX}${key}`;
},
parse(raw, defaultValue) {
if (!raw) return defaultValue;
try {
return JSON.parse(raw);
} catch (e) {
return defaultValue;
}
},
stringify(value) {
return JSON.stringify(value);
},
};
}
module.exports = {
getStore,
};

View File

@@ -0,0 +1,143 @@
"use strict";
/**
* 巡检模块 - 基础类型定义
*
* 说明:
* - 数据库/代码中使用英文枚举键值
* - 中文展示名通过 LABELS 映射,界面层统一显示中文
*/
// ---------- 告警级别 ----------
const ALERT_LEVELS = {
CRITICAL: "critical",
WARNING: "warning",
INFO: "info",
};
const ALERT_LEVEL_LABELS = {
critical: "严重",
warning: "警告",
info: "提示",
};
// ---------- 设备层级 ----------
const DEVICE_TIERS = {
CORE: "core",
AGGREGATION: "aggregation",
ACCESS: "access",
};
const DEVICE_TIER_LABELS = {
core: "核心级",
aggregation: "重要级",
access: "一般",
};
// 各层级默认巡检周期(分钟)
const DEVICE_TIER_DEFAULT_INTERVAL = {
core: 5,
aggregation: 10,
access: 15,
};
// ---------- 巡检状态 ----------
const INSPECTION_STATUS = {
SUCCESS: "success",
FAILED: "failed",
TIMEOUT: "timeout",
};
const INSPECTION_STATUS_LABELS = {
success: "成功",
failed: "失败",
timeout: "超时",
};
// ---------- 告警记录状态 ----------
const ALERT_RECORD_STATUS = {
ACTIVE: "active",
ACKNOWLEDGED: "acknowledged",
RESOLVED: "resolved",
CLOSED: "closed",
};
const ALERT_RECORD_STATUS_LABELS = {
active: "活跃",
acknowledged: "已确认",
resolved: "已恢复",
closed: "已关闭",
};
// ---------- 比较操作符 ----------
const OPERATORS = {
GT: ">",
LT: "<",
EQ: "==",
NE: "!=",
GTE: ">=",
LTE: "<=",
};
const OPERATOR_LABELS = {
">": "大于",
"<": "小于",
"==": "等于",
"!=": "不等于",
">=": "大于等于",
"<=": "小于等于",
};
// ---------- 设备类型(采集器标识)----------
const DEVICE_CATALOG_TYPES = {
LINUX: "linux",
WINDOWS: "windows",
HUAWEI_H3C_SWITCH: "huawei_h3c_switch",
CISCO_SWITCH: "cisco_switch",
HUAWEI_FIREWALL: "huawei_firewall",
FORTINET_FIREWALL: "fortinet_firewall",
};
const DEVICE_CATALOG_LABELS = {
linux: "Linux 服务器",
windows: "Windows 服务器",
huawei_h3c_switch: "华为/H3C 交换机",
cisco_switch: "Cisco 交换机",
huawei_firewall: "华为防火墙",
fortinet_firewall: "飞塔防火墙",
};
// ---------- 指标中文名称 ----------
const METRIC_LABELS = {
cpu_usage: "CPU 使用率",
memory_usage: "内存使用率",
disk_usage: "磁盘使用率",
port_status: "端口状态",
power_status: "电源状态",
fan_status: "风扇状态",
online: "设备在线状态",
session_count: "会话数",
};
// ---------- 系统配置默认值 ----------
const SYS_CONFIG_DEFAULTS = {
inspection_default_interval: 5, // 全局默认巡检周期(分钟)
};
module.exports = {
ALERT_LEVELS,
ALERT_LEVEL_LABELS,
DEVICE_TIERS,
DEVICE_TIER_LABELS,
DEVICE_TIER_DEFAULT_INTERVAL,
INSPECTION_STATUS,
INSPECTION_STATUS_LABELS,
ALERT_RECORD_STATUS,
ALERT_RECORD_STATUS_LABELS,
OPERATORS,
OPERATOR_LABELS,
DEVICE_CATALOG_TYPES,
DEVICE_CATALOG_LABELS,
METRIC_LABELS,
SYS_CONFIG_DEFAULTS,
};