Initial commit: NetMesh smart ops platform with 3D topology, inspection reports, RCA and AI ops assistant
Some checks failed
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
Some checks failed
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
This commit is contained in:
95
electron/bridges/inspection/adapterRegistry.cjs
Normal file
95
electron/bridges/inspection/adapterRegistry.cjs
Normal 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,
|
||||
};
|
||||
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;
|
||||
132
electron/bridges/inspection/adapters/linuxServerAdapter.cjs
Normal file
132
electron/bridges/inspection/adapters/linuxServerAdapter.cjs
Normal file
@@ -0,0 +1,132 @@
|
||||
"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 = {};
|
||||
const DEBUG = process.env.INSPECTION_DEBUG === "1";
|
||||
|
||||
// 1. CPU 使用率(采样 top)
|
||||
try {
|
||||
raw.cpu = await this.execute("top -bn1 2>/dev/null | head -n 5");
|
||||
if (DEBUG) console.log("[LinuxAdapter] CPU stdout:", JSON.stringify(raw.cpu?.slice(0, 200)));
|
||||
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]));
|
||||
} else {
|
||||
// 尝试 mpstat 兜底(部分系统 top 不支持 batch 模式)
|
||||
try {
|
||||
raw.cpuMpstat = await this.execute("mpstat 1 1 2>/dev/null | tail -n 1");
|
||||
if (DEBUG) console.log("[LinuxAdapter] mpstat stdout:", JSON.stringify(raw.cpuMpstat?.slice(0, 200)));
|
||||
const idleMatch = (raw.cpuMpstat || "").match(/(?:%?idle|idle)\s*[:=]?\s*([\d.]+)/i);
|
||||
if (idleMatch) metrics.cpu_usage = Math.round(100 - parseFloat(idleMatch[1]));
|
||||
} catch (_) { /* ignore */ }
|
||||
}
|
||||
} catch (e) {
|
||||
if (DEBUG) console.error("[LinuxAdapter] CPU 采集失败:", e.message);
|
||||
throw e; // SSH 失败或其他错误必须向上传播
|
||||
}
|
||||
|
||||
// 2. 内存使用率
|
||||
try {
|
||||
raw.mem = await this.execute("free -k 2>/dev/null | head -n 2 | tail -n 1");
|
||||
if (DEBUG) console.log("[LinuxAdapter] MEM stdout:", JSON.stringify(raw.mem?.slice(0, 200)));
|
||||
// 支持中英文输出: "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));
|
||||
}
|
||||
} else {
|
||||
// 兜底:/proc/meminfo
|
||||
try {
|
||||
raw.memInfo = await this.execute("cat /proc/meminfo 2>/dev/null | head -n 3");
|
||||
if (DEBUG) console.log("[LinuxAdapter] meminfo stdout:", JSON.stringify(raw.memInfo?.slice(0, 300)));
|
||||
const memInfo = raw.memInfo || "";
|
||||
const totalMatch = memInfo.match(/MemTotal:\s+(\d+)/);
|
||||
const availMatch = memInfo.match(/MemAvailable:\s+(\d+)/);
|
||||
if (totalMatch && availMatch) {
|
||||
const total = parseInt(totalMatch[1], 10);
|
||||
const avail = parseInt(availMatch[1], 10);
|
||||
metrics.memory_usage = Math.min(100, Math.round(((total - avail) / total) * 100));
|
||||
}
|
||||
} catch (_) { /* ignore */ }
|
||||
}
|
||||
} catch (e) {
|
||||
if (DEBUG) console.error("[LinuxAdapter] MEM 采集失败:", e.message);
|
||||
throw e;
|
||||
}
|
||||
|
||||
// 3. 磁盘使用率(显示所有挂载点)
|
||||
try {
|
||||
raw.disk = await this.execute("df -h 2>/dev/null | tail -n +2 | grep -v '/dev/loop'");
|
||||
if (DEBUG) console.log("[LinuxAdapter] DISK stdout:", JSON.stringify(raw.disk?.slice(0, 300)));
|
||||
const disks = this.parseDiskUsage(raw.disk);
|
||||
if (disks.length > 0) metrics.disk_usage = disks;
|
||||
} catch (e) {
|
||||
if (DEBUG) console.error("[LinuxAdapter] DISK 采集失败:", e.message);
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (DEBUG) console.log("[LinuxAdapter] 最终 metrics:", JSON.stringify(metrics));
|
||||
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;
|
||||
411
electron/bridges/inspection/alertRecordStore.cjs
Normal file
411
electron/bridges/inspection/alertRecordStore.cjs
Normal file
@@ -0,0 +1,411 @@
|
||||
"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) {
|
||||
// 如果告警之前被手动确认过(acknowledged)但设备继续超限,重置回活跃状态
|
||||
if (activeAlert.status === ALERT_RECORD_STATUS.ACKNOWLEDGED) {
|
||||
activeAlert.status = ALERT_RECORD_STATUS.ACTIVE;
|
||||
activeAlert.acknowledged_by = null;
|
||||
activeAlert.acknowledged_at = null;
|
||||
}
|
||||
activeAlert.metric_value = _stringifyMetric(metricValue);
|
||||
activeAlert.threshold = _stringifyMetric(rule.threshold);
|
||||
activeAlert.message = _buildMessage(device.device_name, rule, _stringifyMetric(metricValue), _stringifyMetric(rule.threshold));
|
||||
activeAlert.triggered_at = Date.now();
|
||||
_saveAlerts(writeFn);
|
||||
return { ...activeAlert, action: "updated" };
|
||||
}
|
||||
|
||||
return null;
|
||||
} else {
|
||||
// 未触发:重置计数
|
||||
if (_counters[counterKey]) {
|
||||
delete _counters[counterKey];
|
||||
_saveCounters(writeFn);
|
||||
}
|
||||
|
||||
// 有活跃告警 → 自动恢复(ACTIVE 和 ACKNOWLEDGED 都要恢复)
|
||||
if (activeAlert && (activeAlert.status === ALERT_RECORD_STATUS.ACTIVE || activeAlert.status === ALERT_RECORD_STATUS.ACKNOWLEDGED)) {
|
||||
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 _stringifyMetric(v) {
|
||||
if (v === null || v === undefined) return "";
|
||||
if (typeof v === "number" || typeof v === "string" || typeof v === "boolean") return String(v);
|
||||
if (Array.isArray(v)) {
|
||||
// 数组:数字数组 join 逗号,对象数组 JSON 然后截断
|
||||
if (v.length === 0) return "";
|
||||
if (v.every(x => typeof x === "number" || typeof x === "string" || typeof x === "boolean")) {
|
||||
return v.map(x => String(x)).join(", ");
|
||||
}
|
||||
try { return JSON.stringify(v).slice(0, 200); } catch { return ""; }
|
||||
}
|
||||
if (typeof v === "object") {
|
||||
// 对象:优先取 value / current / usage 等常见数值字段
|
||||
if (typeof v.value === "number" || typeof v.value === "string") return String(v.value);
|
||||
if (typeof v.current === "number" || typeof v.current === "string") return String(v.current);
|
||||
if (typeof v.usage === "number" || typeof v.usage === "string") return String(v.usage);
|
||||
if (typeof v.percent === "number" || typeof v.percent === "string") return String(v.percent);
|
||||
try { return JSON.stringify(v).slice(0, 200); } catch { return ""; }
|
||||
}
|
||||
return String(v);
|
||||
}
|
||||
|
||||
function _createAlert(device, rule, metricValue) {
|
||||
const mv = _stringifyMetric(metricValue);
|
||||
const tv = _stringifyMetric(rule.threshold);
|
||||
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: mv,
|
||||
threshold: tv,
|
||||
level: rule.level,
|
||||
message: _buildMessage(device.device_name || device.device_id, rule, mv, tv),
|
||||
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, metricValueStr, thresholdStr) {
|
||||
const unit = rule.metric.includes("_usage") || rule.metric.includes("_percent") ? "%" : "";
|
||||
return `${deviceName} ${rule.name},当前值 ${metricValueStr}${unit},阈值 ${thresholdStr}${unit}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建执行错误告警(timeout / failed 等采集器执行异常)
|
||||
* 每台设备 + rule_offline 全局唯一:
|
||||
* - 已存在任何非 RESOLVED 状态 → 复用,强制激活,覆盖 message/triggered_at
|
||||
* - 同设备多条残留(历史遗留)→ 自动合并:最新一条保留,其余降级为 RESOLVED
|
||||
* - 全 RESOLVED → 新建一条 ACTIVE
|
||||
*/
|
||||
function createExecutionErrorAlert(readFn, writeFn, device, status, errorMessage) {
|
||||
_ensureLoaded(readFn);
|
||||
|
||||
const offlineRuleId = "rule_offline";
|
||||
const offlineRuleName = "设备离线";
|
||||
|
||||
// 1. 强制去重:同 device + rule_offline 的非 RESOLVED 告警,只保留最新的一条
|
||||
const sameDeviceOffline = _alerts.filter(
|
||||
(a) => a.device_id === device.device_id && a.rule_id === offlineRuleId
|
||||
);
|
||||
const activeList = sameDeviceOffline.filter(
|
||||
(a) => a.status !== ALERT_RECORD_STATUS.RESOLVED && a.status !== ALERT_RECORD_STATUS.CLOSED
|
||||
);
|
||||
if (activeList.length > 1) {
|
||||
// 多条残留 → triggered_at 最新的保留,其余强制降级为 RESOLVED(清理历史重复)
|
||||
activeList.sort((a, b) => b.triggered_at - a.triggered_at);
|
||||
for (let i = 1; i < activeList.length; i++) {
|
||||
activeList[i].status = ALERT_RECORD_STATUS.RESOLVED;
|
||||
activeList[i].resolved_at = Date.now();
|
||||
activeList[i].resolved_reason = "dedup_execution_error";
|
||||
}
|
||||
_saveAlerts(writeFn);
|
||||
}
|
||||
|
||||
// 2. 复用唯一剩余的那条非 RESOLVED 告警(ACTIVE 或 ACKNOWLEDGED 都算)
|
||||
const existing = activeList[0];
|
||||
if (existing && existing.status !== ALERT_RECORD_STATUS.RESOLVED && existing.status !== ALERT_RECORD_STATUS.CLOSED) {
|
||||
if (existing.status === ALERT_RECORD_STATUS.ACKNOWLEDGED) {
|
||||
existing.status = ALERT_RECORD_STATUS.ACTIVE;
|
||||
existing.acknowledged_by = null;
|
||||
existing.acknowledged_at = null;
|
||||
}
|
||||
existing.metric_value = "false";
|
||||
existing.message = `${device.device_name || device.device_id} ${offlineRuleName}: ${errorMessage}`;
|
||||
existing.triggered_at = Date.now();
|
||||
_saveAlerts(writeFn);
|
||||
return { ...existing, action: "updated" };
|
||||
}
|
||||
|
||||
// 3. 没有 → 新建
|
||||
const alert = {
|
||||
id: `alert_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
rule_id: offlineRuleId,
|
||||
rule_name: offlineRuleName,
|
||||
device_id: device.device_id,
|
||||
device_name: device.device_name || device.device_id,
|
||||
metric: "online",
|
||||
metric_value: "false",
|
||||
threshold: "true",
|
||||
level: "critical",
|
||||
message: `${device.device_name || device.device_id} ${offlineRuleName}: ${errorMessage}`,
|
||||
triggered_at: Date.now(),
|
||||
status: ALERT_RECORD_STATUS.ACTIVE,
|
||||
acknowledged_by: null,
|
||||
acknowledged_at: null,
|
||||
resolved_at: null,
|
||||
resolved_reason: null,
|
||||
note: null,
|
||||
};
|
||||
|
||||
_alerts.push(alert);
|
||||
_saveAlerts(writeFn);
|
||||
return { ...alert, action: "triggered" };
|
||||
}
|
||||
|
||||
// ---------- 告警查询 ----------
|
||||
|
||||
function listAlerts(readFn, options = {}) {
|
||||
_ensureLoaded(readFn);
|
||||
let result = [..._alerts];
|
||||
|
||||
if (options.status) {
|
||||
result = result.filter((a) => a.status === options.status);
|
||||
}
|
||||
|
||||
// 读取时去重:同 device_id + rule_offline + (ACTIVE/ACKNOWLEDGED) 只留最新一条
|
||||
// 这是对历史遗留重复数据的保护,正常流程已由 createExecutionErrorAlert 保证唯一
|
||||
const offlineGroups = new Map();
|
||||
result = result.filter((a) => {
|
||||
if (a.rule_id === "rule_offline" && (a.status === "active" || a.status === "acknowledged")) {
|
||||
const key = `${a.device_id}::offline-active`;
|
||||
const prev = offlineGroups.get(key);
|
||||
if (prev) {
|
||||
// 已存在 → 保留触发时间新的那条
|
||||
if (a.triggered_at > prev.triggered_at) {
|
||||
offlineGroups.set(key, a);
|
||||
return true; // 这条更新,过滤掉 prev(但 prev 已经在 result 里了,下面处理)
|
||||
}
|
||||
return false; // 这条旧,丢弃
|
||||
}
|
||||
offlineGroups.set(key, a);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// 二次清理:把每个 group 里最旧的那条也去掉
|
||||
const bestOffline = new Set(offlineGroups.values().map((a) => a.id));
|
||||
result = result.filter(
|
||||
(a) => !(a.rule_id === "rule_offline" && (a.status === "active" || a.status === "acknowledged")) || bestOffline.has(a.id)
|
||||
);
|
||||
|
||||
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 || MAX_ALERTS);
|
||||
}
|
||||
|
||||
function getActiveAlerts(readFn) {
|
||||
return listAlerts(readFn, { status: ALERT_RECORD_STATUS.ACTIVE, limit: MAX_ALERTS });
|
||||
}
|
||||
|
||||
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,
|
||||
createExecutionErrorAlert,
|
||||
// 查询
|
||||
listAlerts,
|
||||
getActiveAlerts,
|
||||
getAlert,
|
||||
// 处理
|
||||
acknowledgeAlert,
|
||||
resolveAlert,
|
||||
closeAlert,
|
||||
updateNote,
|
||||
// 统计
|
||||
getAlertStats,
|
||||
// 内部
|
||||
_resetForTest,
|
||||
};
|
||||
543
electron/bridges/inspection/alertRuleStore.cjs
Normal file
543
electron/bridges/inspection/alertRuleStore.cjs
Normal 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,
|
||||
};
|
||||
289
electron/bridges/inspection/dailyReportStore.cjs
Normal file
289
electron/bridges/inspection/dailyReportStore.cjs
Normal file
@@ -0,0 +1,289 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* 日运行报告存储 + 聚合
|
||||
*
|
||||
* 每次生成日报告一条:
|
||||
* {
|
||||
* id, // "daily_<date>_<mode>"
|
||||
* date, // "2026-10-13"
|
||||
* mode, // "provisional" | "finalized"
|
||||
* timestamp, // 生成时间
|
||||
* trigger, // "manual" | "scheduled"
|
||||
* period: { start_ts, end_ts }, // 覆盖的数据时间范围
|
||||
* summary: {
|
||||
* snapshotCount,
|
||||
* deviceCount,
|
||||
* totalCmds, okCmds, failCmds,
|
||||
* failRate,
|
||||
* deviceSuccessRate: { host_a: 0.98, ... },
|
||||
* topFailedCommands: [ { cmd, device_id, count } ]
|
||||
* },
|
||||
* aiSummary: { ... } | null // AI 四段式汇总,可选
|
||||
* }
|
||||
*
|
||||
* 独立文件,不与 snapshots 混存。
|
||||
*/
|
||||
|
||||
const { STORAGE_KEY_DAILY_REPORTS } = require("./storageKeys.cjs");
|
||||
|
||||
const MAX_DAILY_REPORTS = 120; // 保留最近约 4 个月(每天 1 条)
|
||||
|
||||
function _safeParse(raw, def) {
|
||||
try { return JSON.parse(raw); }
|
||||
catch { return def; }
|
||||
}
|
||||
|
||||
let _reports = null;
|
||||
|
||||
function _ensureLoaded(readFn) {
|
||||
if (_reports === null) {
|
||||
const raw = readFn(STORAGE_KEY_DAILY_REPORTS);
|
||||
_reports = raw ? _safeParse(raw, []) : [];
|
||||
}
|
||||
}
|
||||
|
||||
function _save(writeFn) {
|
||||
if (_reports.length > MAX_DAILY_REPORTS) {
|
||||
_reports = _reports.slice(-MAX_DAILY_REPORTS);
|
||||
}
|
||||
writeFn(STORAGE_KEY_DAILY_REPORTS, JSON.stringify(_reports, null, 2));
|
||||
}
|
||||
|
||||
function _dayStart(date) {
|
||||
const d = new Date(date);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
return d.getTime();
|
||||
}
|
||||
|
||||
function _dayEnd(date) {
|
||||
const d = new Date(date);
|
||||
d.setHours(23, 59, 59, 999);
|
||||
return d.getTime();
|
||||
}
|
||||
|
||||
function _dateStr(ts) {
|
||||
const d = new Date(ts);
|
||||
const p = (n) => n.toString().padStart(2, "0");
|
||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从一组 snapshots 聚合出一份日运行报告数据
|
||||
*
|
||||
* @param {Array} snapshots - 快照数组
|
||||
* @param {number} startTs - 数据时间范围起点
|
||||
* @param {number} endTs - 数据时间范围终点
|
||||
*/
|
||||
function buildDailyReport(snapshots, startTs, endTs, { mode, trigger }) {
|
||||
const filtered = snapshots.filter(s => s.timestamp >= startTs && s.timestamp <= endTs);
|
||||
|
||||
const deviceMap = new Map(); // hostId -> { okCmds, totalCmds, okCount, totalCount }
|
||||
const cmdFailureMap = new Map(); // "host|cmd" -> count
|
||||
let totalCmds = 0, okCmds = 0;
|
||||
|
||||
for (const snap of filtered) {
|
||||
for (const dev of snap.devices) {
|
||||
const prev = deviceMap.get(dev.device_id) || { okCmds: 0, totalCmds: 0 };
|
||||
for (const cmd of dev.commands) {
|
||||
totalCmds++;
|
||||
if (cmd.ok) { okCmds++; prev.okCmds++; }
|
||||
prev.totalCmds++;
|
||||
if (!cmd.ok) {
|
||||
const key = `${dev.device_id}|${cmd.cmd}`;
|
||||
cmdFailureMap.set(key, (cmdFailureMap.get(key) || 0) + 1);
|
||||
}
|
||||
}
|
||||
deviceMap.set(dev.device_id, prev);
|
||||
}
|
||||
}
|
||||
|
||||
// 设备成功率
|
||||
const deviceSuccessRate = {};
|
||||
for (const [devId, v] of deviceMap) {
|
||||
deviceSuccessRate[devId] = v.totalCmds > 0
|
||||
? Number((v.okCmds / v.totalCmds).toFixed(3)) : 0;
|
||||
}
|
||||
|
||||
// Top 失败命令(最多 20 条)
|
||||
const topFailedCommands = Array.from(cmdFailureMap.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 20)
|
||||
.map(([key, count]) => {
|
||||
const [device_id, cmd] = key.split("|");
|
||||
return { device_id, cmd, count };
|
||||
});
|
||||
|
||||
const dateStr = _dateStr(endTs);
|
||||
|
||||
// ═══ 新增:从快照 AI 汇总构建故障时间线 ═══
|
||||
// 只有 ai_summary_status === "done" 的快照才贡献 timeline 事件
|
||||
const timeline = [];
|
||||
const perSnapshotHighlights = []; // 每个快照的健康摘要(按时间序)
|
||||
for (const snap of filtered) {
|
||||
const ts = snap.timestamp;
|
||||
const ai = snap.ai_summary;
|
||||
const status = snap.ai_summary_status;
|
||||
const timeStr = new Date(ts).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
|
||||
const dateTimeStr = new Date(ts).toLocaleString("zh-CN", { hour: "2-digit", minute: "2-digit", month: "2-digit", day: "2-digit" });
|
||||
|
||||
if (ai && status === "done") {
|
||||
// 每个快照的健康摘要(时间线串起来的基础)
|
||||
perSnapshotHighlights.push({
|
||||
timestamp: ts,
|
||||
time: timeStr,
|
||||
snapshot_id: snap.id,
|
||||
health_summary: ai.health_summary || "—",
|
||||
});
|
||||
|
||||
// 离线设备 → timeline event
|
||||
for (const dev of (ai.offline_devices || [])) {
|
||||
timeline.push({
|
||||
timestamp: ts,
|
||||
time: timeStr,
|
||||
snapshot_id: snap.id,
|
||||
type: "offline",
|
||||
severity: "critical",
|
||||
device: dev.device || dev.hostname || dev,
|
||||
detail: dev.reason || "SSH 连接失败",
|
||||
});
|
||||
}
|
||||
// 隐藏风险 → timeline event
|
||||
for (const risk of (ai.hidden_risks || [])) {
|
||||
timeline.push({
|
||||
timestamp: ts,
|
||||
time: timeStr,
|
||||
snapshot_id: snap.id,
|
||||
type: "risk",
|
||||
severity: risk.severity || "warning",
|
||||
device: risk.device || "未知设备",
|
||||
detail: risk.observation || risk.message || String(risk),
|
||||
});
|
||||
}
|
||||
// 命令异常(在 anomalies 数组里)→ timeline event
|
||||
for (const a of (ai.anomalies || [])) {
|
||||
timeline.push({
|
||||
timestamp: ts,
|
||||
time: timeStr,
|
||||
snapshot_id: snap.id,
|
||||
type: "anomaly",
|
||||
severity: "warning",
|
||||
device: a.device || "未知设备",
|
||||
detail: a.cmd || a.message || String(a),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 无 AI 汇总的快照(可能 AI 失败/未触发),也记录基础事实
|
||||
const offlineDevs = snap.devices.filter(d => !d.ok);
|
||||
if (offlineDevs.length > 0) {
|
||||
for (const d of offlineDevs) {
|
||||
timeline.push({
|
||||
timestamp: ts,
|
||||
time: timeStr,
|
||||
snapshot_id: snap.id,
|
||||
type: "offline",
|
||||
severity: "critical",
|
||||
device: d.hostname,
|
||||
detail: "本次巡检整体失败",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按时间升序排(时间线要串起来)
|
||||
timeline.sort((a, b) => a.timestamp - b.timestamp);
|
||||
|
||||
// 设备状态矩阵(哪些设备在今天的哪些时间点出问题)
|
||||
const deviceStatusMatrix = {}; // hostId → [{ time, snapshot_id, status, note }]
|
||||
for (const snap of filtered) {
|
||||
const ts = snap.timestamp;
|
||||
const timeStr = new Date(ts).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
|
||||
for (const dev of snap.devices) {
|
||||
if (!deviceStatusMatrix[dev.device_id]) {
|
||||
deviceStatusMatrix[dev.device_id] = [];
|
||||
}
|
||||
let status = "ok";
|
||||
let note = "";
|
||||
if (!dev.ok) { status = "offline"; note = "SSH 连接失败"; }
|
||||
else {
|
||||
const failCmds = dev.commands.filter(c => !c.ok);
|
||||
if (failCmds.length > 0) { status = "partial"; note = `${failCmds.length} 条命令失败`; }
|
||||
}
|
||||
deviceStatusMatrix[dev.device_id].push({
|
||||
time: timeStr, snapshot_id: snap.id, status, note,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const report = {
|
||||
id: `daily_${dateStr}_${mode}`,
|
||||
date: dateStr,
|
||||
mode,
|
||||
timestamp: Date.now(),
|
||||
trigger,
|
||||
period: { start_ts: startTs, end_ts: endTs },
|
||||
summary: {
|
||||
snapshotCount: filtered.length,
|
||||
deviceCount: deviceMap.size,
|
||||
totalCmds,
|
||||
okCmds,
|
||||
failCmds: totalCmds - okCmds,
|
||||
failRate: totalCmds > 0 ? Number(((totalCmds - okCmds) / totalCmds * 100).toFixed(1)) : 0,
|
||||
deviceSuccessRate,
|
||||
topFailedCommands,
|
||||
},
|
||||
timeline, // ═══ 新增:故障时间线 ═══
|
||||
perSnapshotHighlights, // ═══ 新增:每个快照的健康摘要(按时间)═══
|
||||
deviceStatusMatrix, // ═══ 新增:设备×时间点状态矩阵 ═══
|
||||
aiSummary: null, // 日报告自己的 AI 汇总(可选生成)
|
||||
};
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存一份日报告到存储,同一 date+mode 存在则覆盖
|
||||
*/
|
||||
function addDailyReport(readFn, writeFn, report) {
|
||||
_ensureLoaded(readFn);
|
||||
const idx = _reports.findIndex(r => r.date === report.date && r.mode === report.mode);
|
||||
if (idx >= 0) {
|
||||
_reports[idx] = report;
|
||||
} else {
|
||||
_reports.push(report);
|
||||
}
|
||||
_reports.sort((a, b) => b.timestamp - a.timestamp);
|
||||
_save(writeFn);
|
||||
return report;
|
||||
}
|
||||
|
||||
function listDailyReports(readFn, limit = 30) {
|
||||
_ensureLoaded(readFn);
|
||||
return _reports.slice(0, limit);
|
||||
}
|
||||
|
||||
function deleteDailyReport(readFn, writeFn, id) {
|
||||
_ensureLoaded(readFn);
|
||||
const idx = _reports.findIndex(r => r.id === id);
|
||||
if (idx < 0) return false;
|
||||
_reports.splice(idx, 1);
|
||||
_save(writeFn);
|
||||
return true;
|
||||
}
|
||||
|
||||
function clearAll(readFn, writeFn) {
|
||||
_reports = [];
|
||||
_save(writeFn);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
MAX_DAILY_REPORTS,
|
||||
buildDailyReport,
|
||||
addDailyReport,
|
||||
listDailyReports,
|
||||
deleteDailyReport,
|
||||
clearAll,
|
||||
_dayStart,
|
||||
_dayEnd,
|
||||
_dateStr,
|
||||
};
|
||||
189
electron/bridges/inspection/deviceCatalogStore.cjs
Normal file
189
electron/bridges/inspection/deviceCatalogStore.cjs
Normal 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,
|
||||
};
|
||||
290
electron/bridges/inspection/deviceStore.cjs
Normal file
290
electron/bridges/inspection/deviceStore.cjs
Normal file
@@ -0,0 +1,290 @@
|
||||
"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) {
|
||||
if (text === null || text === undefined) return def;
|
||||
let parsed;
|
||||
try { parsed = JSON.parse(text); }
|
||||
catch (e) { return def; }
|
||||
// 防御二次/三次 stringify:inspectionStorage.writeStorage 先 JSON.stringify 一次,
|
||||
// _save 又 JSON.stringify 一次 → 文件里值是 "\"{...}\"" 这种嵌套字符串
|
||||
// 连续 parse 直到拿到真正的对象/数组(最多 3 层避免死循环)
|
||||
let depth = 0;
|
||||
while (typeof parsed === "string" && parsed.length > 0 && depth < 3) {
|
||||
let next;
|
||||
try { next = JSON.parse(parsed); } catch { break; }
|
||||
if (typeof next === "string" && next === parsed) break;
|
||||
parsed = next;
|
||||
depth++;
|
||||
}
|
||||
return (parsed !== null && typeof parsed === "object") ? parsed : 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,
|
||||
};
|
||||
1511
electron/bridges/inspection/index.cjs
Normal file
1511
electron/bridges/inspection/index.cjs
Normal file
File diff suppressed because it is too large
Load Diff
506
electron/bridges/inspection/inspectionExec.cjs
Normal file
506
electron/bridges/inspection/inspectionExec.cjs
Normal file
@@ -0,0 +1,506 @@
|
||||
"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 sendCommandTimer = null;
|
||||
let outputDoneTimer = null;
|
||||
let commandSent = false;
|
||||
|
||||
// 匹配行尾是否是 shell prompt(同时兼容交换机和 Linux)
|
||||
function hasPromptTail(line) {
|
||||
// 交换机: <hostname> 结尾 或 hostname> / hostname# 结尾
|
||||
if (/<[\w\-./]+>\s*$/.test(line)) return true;
|
||||
if (/^[\w\-./]+[>#]\s*$/.test(line)) return true;
|
||||
// Linux bash/zsh: user@host...$ / user@host...# 结尾
|
||||
if (/[\w.-]+@[\w.-]+.*[$#]\s*$/.test(line)) return true;
|
||||
// Linux 路径 prompt: ~$ / /tmp# 等
|
||||
if (/[:~\/]\s*[$#]\s*$/.test(line)) return true;
|
||||
return 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);
|
||||
if (sendCommandTimer) clearTimeout(sendCommandTimer);
|
||||
if (outputDoneTimer) clearTimeout(outputDoneTimer);
|
||||
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 sendCommandTimer = null;
|
||||
let outputDoneTimer = null; // 命令发送后静默期兜底(输出完成检测)
|
||||
let promptStableCount = 0; // shell 稳定化:需要看到 prompt 连续出现 2 次才发命令
|
||||
|
||||
// shell 稳定化:先发空回车让交换机 VTY/Linux bash 进入稳定态
|
||||
// shell ready 静默期 500ms(比之前的 200ms 更宽松,给 banner 时间)
|
||||
const scheduleSendCommand = () => {
|
||||
if (commandSent || sendCommandTimer) return;
|
||||
clearTimeout(sendCommandTimer);
|
||||
sendCommandTimer = setTimeout(() => {
|
||||
commandSent = true;
|
||||
sendCommandTimer = null;
|
||||
stream.write(command + "\n");
|
||||
if (DEBUG) console.log("[InspectExec] shell 稳定,发送命令:", command);
|
||||
}, 500); // 500ms 给 shell 稳定时间
|
||||
};
|
||||
|
||||
// 命令发送后,2500ms 静默期 → 兜底认为输出完成(兼容 Linux bash $ 结尾 prompt)
|
||||
// 注意:top/df 等命令输出可能持续 1-2s,不能太短
|
||||
const scheduleOutputDone = () => {
|
||||
if (outputDoneTimer) clearTimeout(outputDoneTimer);
|
||||
outputDoneTimer = setTimeout(() => {
|
||||
outputDoneTimer = null;
|
||||
if (stdout) return; // 已完成
|
||||
// 命令回显可能被 prompt 包含了 —— 做基本判断后返回
|
||||
const buf = outputBuffer.replace(/__sent_q__/g, "");
|
||||
if (buf.trim().length > 0) {
|
||||
stdout = buf;
|
||||
clearTimeout(execTimer);
|
||||
if (DEBUG) console.log("[InspectExec] 输出静默期到,兜底完成,len:", stdout.length);
|
||||
done(null, stdout);
|
||||
} else {
|
||||
// 什么都没收到,让 execTimer 继续等
|
||||
if (DEBUG) console.log("[InspectExec] 输出静默期到但缓冲区为空,继续等待");
|
||||
}
|
||||
}, 2500);
|
||||
};
|
||||
|
||||
stream.on("data", (data) => {
|
||||
const chunk = data.toString("utf-8");
|
||||
outputBuffer += chunk;
|
||||
|
||||
// 未发送命令时:等待 shell prompt 出现后再给稳定时间
|
||||
if (!commandSent) {
|
||||
const lastLine = outputBuffer.split("\n").pop()?.trimEnd() || "";
|
||||
// 精确匹配交换机/路由器 prompt
|
||||
const swPrompt = /^<[\w\-./]+>\s*$/.test(lastLine)
|
||||
|| /^[\w\-./]+[>#]\s*$/.test(lastLine);
|
||||
if (swPrompt) {
|
||||
// 看到 prompt 了,但还要给 shell 稳定时间(可能需要 enable / system-view)
|
||||
// 不立即发命令 → 走 scheduleSendCommand 的 500ms 静默期逻辑
|
||||
scheduleSendCommand();
|
||||
} else {
|
||||
scheduleSendCommand();
|
||||
}
|
||||
}
|
||||
|
||||
// 命令发送后,收集输出
|
||||
if (commandSent) {
|
||||
// 重置输出静默期计时器(持续有数据就不触发兜底)
|
||||
scheduleOutputDone();
|
||||
|
||||
// 检测分页提示,发送 '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");
|
||||
}
|
||||
}
|
||||
|
||||
// 检测输出完成:匹配末尾 prompt(同时兼容交换机和 Linux)
|
||||
const lines = outputBuffer.split("\n");
|
||||
// 至少收到了命令本身的回显 + 一些数据才检查
|
||||
if (lines.length >= 2) {
|
||||
const lastContentLine = lines[lines.length - 1]?.trimEnd() || "";
|
||||
const prevContentLine = lines.length >= 2 ? lines[lines.length - 2]?.trimEnd() || "" : "";
|
||||
|
||||
// 匹配最后一行或倒数第二行的 prompt
|
||||
const promptEnd = lastContentLine.length > 0 && hasPromptTail(lastContentLine);
|
||||
const prevPromptEnd = prevContentLine.length > 0 && hasPromptTail(prevContentLine);
|
||||
if (promptEnd || prevPromptEnd) {
|
||||
stdout = outputBuffer.replace(/__sent_q__/g, "");
|
||||
clearTimeout(execTimer);
|
||||
clearTimeout(outputDoneTimer);
|
||||
if (DEBUG) console.log("[InspectExec] 命令完成,行数:", lines.length);
|
||||
done(null, stdout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 防止缓冲区过大
|
||||
if (outputBuffer.length > maxOutputBytes * 2) {
|
||||
stdout = outputBuffer.replace(/__sent_q__/g, "");
|
||||
clearTimeout(execTimer);
|
||||
clearTimeout(outputDoneTimer);
|
||||
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> 和 Linux bash/zsh)
|
||||
function hasPrompt(text) {
|
||||
const lines = (text || '').split('\n');
|
||||
// 从后往前找非空行(最后一行通常是空的,我们看倒数第一和倒数第二)
|
||||
const findLastLine = () => {
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const trimmed = lines[i].trimEnd();
|
||||
if (trimmed.length > 0) return trimmed;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
const lastLine = findLastLine();
|
||||
// 如果只有一行内容,再看前一行(有些 shell 会在输出末尾多一个空行)
|
||||
const prevLine = lines.length >= 2 ? lines[lines.length - 2].trimEnd() : '';
|
||||
|
||||
if (DEBUG) {
|
||||
console.log("[InspectExec] hasPrompt check lastLine:", JSON.stringify(lastLine.slice(-100)),
|
||||
"| prevLine:", JSON.stringify(prevLine.slice(-100)));
|
||||
}
|
||||
// 华为 H3C VRP prompt(尖括号包裹,可能带 # root 标记、-D slot 标记):
|
||||
// <hostname> — 用户视图
|
||||
// <hostname#> — 系统视图 / root prompt
|
||||
// <hostname#-D> — 指定 slot 的 root prompt
|
||||
// <hostname~> — SSH 登录提示等
|
||||
if (/<[\w\-./]+(?:[#~]\w*)*(?:-\w+)?>\s*$/.test(lastLine)) return true;
|
||||
if (/<[\w\-./]+(?:[#~]\w*)*(?:-\w+)?>\s*$/.test(prevLine)) return true;
|
||||
// Cisco IOS / 纯 hostname prompt(无尖括号): hostname> 或 hostname#
|
||||
if (/^[\w\-./]+[>#]\s*$/.test(lastLine)) return true;
|
||||
if (/^[\w\-./]+[>#]\s*$/.test(prevLine)) return true;
|
||||
// Linux bash: user@host:path$ 或 root@host:path#(必须有 @ 符号区分 "user@host" 和命令输出里的路径)
|
||||
if (/[\w.-]+@[\w.-]+[:\s].*[$#]\s*$/.test(lastLine)) return true;
|
||||
// 简单 Linux prompt 兜底: 行尾是 # 或 $ 且前面有路径特征(~/ / path)
|
||||
if (/[:~\/]\s*[$#]\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 → idle → waiting-output → done
|
||||
let promptCheckTimer = null; // debounced prompt check
|
||||
|
||||
// 延迟 150ms 再检查 prompt —— 给 vt100 pty 足够时间把一整行数据吐出来
|
||||
// 避免逐字节 stream 'data' 事件导致永远 split 不到 \n
|
||||
const schedulePromptCheck = () => {
|
||||
if (promptCheckTimer) clearTimeout(promptCheckTimer);
|
||||
promptCheckTimer = setTimeout(() => {
|
||||
promptCheckTimer = null;
|
||||
_runPromptCheck();
|
||||
}, 150);
|
||||
};
|
||||
|
||||
const _runPromptCheck = () => {
|
||||
if (phase === "wait-login") {
|
||||
if (hasPrompt(outputBuffer)) {
|
||||
phase = "idle";
|
||||
outputBuffer = "";
|
||||
if (DEBUG) console.log("[InspectExec] 登录成功,进入 idle,准备发送第一条命令(由 reportCommands 提供)");
|
||||
sendNextCommand();
|
||||
}
|
||||
} else if (phase === "waiting-output") {
|
||||
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, "");
|
||||
schedulePromptCheck(); // 分页处理完继续等
|
||||
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("data", (data) => {
|
||||
const chunk = data.toString("utf-8");
|
||||
outputBuffer += chunk;
|
||||
|
||||
if (DEBUG && outputBuffer.length % 1000 < 100) {
|
||||
const lines = outputBuffer.split('\n');
|
||||
console.log(`[InspectExec] phase=${phase} bufLen=${outputBuffer.length} lastLines=${JSON.stringify(lines.slice(-3))}`);
|
||||
}
|
||||
|
||||
// 每次收到数据都重置 debounce timer —— 持续有数据就不检查,
|
||||
// 等 vt100 pty 静默 150ms 后再统一跑 hasPrompt
|
||||
schedulePromptCheck();
|
||||
});
|
||||
|
||||
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 || "")
|
||||
// ANSI escape sequences(\x1B[... 系列 — 光标移动、颜色、进度条等)
|
||||
.replace(/\x1B\[[0-9;]*[A-Za-z]/g, "")
|
||||
.replace(/\x1B\][^\x07\x1B]*(\x07|\x1B\\)/g, "")
|
||||
.replace(/\x1B[PX][^\x1B]*\x1B\\/g, "")
|
||||
// 其他常见控制序列
|
||||
.replace(/\x1B[@-_][0-9;]*[ -\/]*[@-~]/g, "")
|
||||
// 华为 CLI 的 More 提示
|
||||
.replace(/--\s*More\s*--/gi, "")
|
||||
.replace(/----\s*More\s*----/gi, "")
|
||||
// 清理剩余可能的单控制字符
|
||||
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, "")
|
||||
.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,
|
||||
};
|
||||
127
electron/bridges/inspection/inspectionResultStore.cjs
Normal file
127
electron/bridges/inspection/inspectionResultStore.cjs
Normal file
@@ -0,0 +1,127 @@
|
||||
"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
|
||||
catalog_id: result.catalog_id || null,
|
||||
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,
|
||||
};
|
||||
399
electron/bridges/inspection/inspectionScheduler.cjs
Normal file
399
electron/bridges/inspection/inspectionScheduler.cjs
Normal file
@@ -0,0 +1,399 @@
|
||||
"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.onAlertChanged = options.onAlertChanged || (() => {});
|
||||
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. 保存巡检结果(带上 catalog_id 供后续 RCA 根因分析用)
|
||||
const record = inspectionResultStore.addResult(
|
||||
this.readFn,
|
||||
this.writeFn,
|
||||
{
|
||||
device_id: hostId,
|
||||
catalog_id: deviceExt.catalog_id || catalog.id || null,
|
||||
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;
|
||||
|
||||
if (triggeredAlerts.length > 0) this.onAlertChanged();
|
||||
|
||||
// 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
|
||||
);
|
||||
|
||||
// 执行错误告警 —— 统一归类为 rule_offline(与设备离线告警合并)
|
||||
// 不再额外调 _evaluateAlerts(online: false) 避免重复创建同规则告警
|
||||
const failStatus = e.message.includes("超时") ? "timeout" : "failed";
|
||||
alertRecordStore.createExecutionErrorAlert(
|
||||
this.readFn,
|
||||
this.writeFn,
|
||||
{
|
||||
device_id: hostId,
|
||||
device_name: deviceExt.label || deviceExt.hostname || hostId,
|
||||
catalog_id: deviceExt.catalog_id,
|
||||
},
|
||||
failStatus,
|
||||
e.message || "巡检执行失败"
|
||||
);
|
||||
this.onAlertChanged();
|
||||
|
||||
this.onResult({
|
||||
device_id: hostId,
|
||||
result: record,
|
||||
alerts: [],
|
||||
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;
|
||||
89
electron/bridges/inspection/inspectionStorage.cjs
Normal file
89
electron/bridges/inspection/inspectionStorage.cjs
Normal 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,
|
||||
};
|
||||
173
electron/bridges/inspection/promptParser.cjs
Normal file
173
electron/bridges/inspection/promptParser.cjs
Normal 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,
|
||||
};
|
||||
203
electron/bridges/inspection/reportCommands.cjs
Normal file
203
electron/bridges/inspection/reportCommands.cjs
Normal file
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* 巡检报告 — 每 catalog 的命令组定义
|
||||
*
|
||||
* 与告警巡检(每 5min)不同:
|
||||
* - 频率低(每 1h 一次)
|
||||
* - 命令多(15-20 条/台)
|
||||
* - 输出大(display logbuffer 可能几千字)
|
||||
* - 存独立文件 inspection-report.json(不碰 inspection-data.json)
|
||||
*
|
||||
* 每个条目: { cmd: string, purpose: string }
|
||||
* preCmd: 每种 catalog 前置命令(关闭分页等),只跑一次
|
||||
*/
|
||||
|
||||
const REORT_COMMANDS_BY_CATALOG = {
|
||||
|
||||
// ─── Linux 服务器(云主机 / 物理机通用)──────
|
||||
// 分层设计:硬件 → OS → 资源 → 网络 → 安全 → 服务 → 日志 → 虚拟化
|
||||
// 原则:只读、幂等、有兜底、输出可控
|
||||
linux_server: {
|
||||
shell: "bash",
|
||||
preCmd: null,
|
||||
commands: [
|
||||
// ═══ 1. 基础身份识别 ═══
|
||||
{ cmd: "echo '=== OS ==='; cat /etc/os-release 2>/dev/null | head -5; echo '=== KERNEL ==='; uname -a; echo '=== HOST ==='; hostname 2>/dev/null; echo '=== UPTIME ==='; uptime", purpose: "OS 发行版 + 内核 + 主机名 + 运行时长" },
|
||||
{ cmd: "echo '=== CPU ==='; lscpu 2>/dev/null | egrep 'Model name|CPU\\(s\\)|Thread|Core|Socket|MHz|Flags.*vmx|Flags.*svm' || cat /proc/cpuinfo | egrep 'model name|processor|cpu MHz' | head -15", purpose: "CPU 型号 / 核心数 / 虚拟化能力" },
|
||||
{ cmd: "echo '=== MEMINFO ==='; cat /proc/meminfo | grep -E 'MemTotal|MemAvailable|MemFree|Buffers|Cached|SwapTotal|SwapFree|Dirty|Writeback'; echo '=== FREE_H ==='; free -h 2>/dev/null; echo '=== DIMM ==='; dmidecode -t memory 2>/dev/null | egrep 'Size|Type|Speed|Manufacturer' | head -20 || echo 'dmidecode_not_available'", purpose: "内存原始数据(/proc/meminfo 精确字段 + free -h + DIMM)" },
|
||||
{ cmd: "echo '=== DISK ==='; lsblk -o NAME,SIZE,TYPE,ROTA,TEMPERATURE,MODEL 2>/dev/null || fdisk -l 2>/dev/null | head -30 || echo 'lsblk/fdisk_not_available'", purpose: "磁盘块设备 + SSD/HDD 区分 + 温度" },
|
||||
{ cmd: "df -hT -x tmpfs -x devtmpfs -x overlay 2>/dev/null | head -20", purpose: "文件系统使用率(排除临时系统)" },
|
||||
{ cmd: "df -i -x tmpfs -x devtmpfs 2>/dev/null | head -15", purpose: "inode 使用率" },
|
||||
{ cmd: "echo '=== SMART ==='; for d in $(ls /dev/sd* /dev/nvme* /dev/vd* 2>/dev/null | grep -v '[0-9]'); do echo \"-- $d --\"; smartctl -H $d 2>/dev/null || echo 'no_smartctl'; done | head -30", purpose: "磁盘健康(SMART)" },
|
||||
|
||||
// ═══ 2. 资源实时状态 ═══
|
||||
{ cmd: "echo '=== LOAD ==='; uptime; echo '=== CPU_TOP ==='; top -bn1 | head -12; echo '=== MEM_TOP ==='; ps aux --sort=-%mem | head -10", purpose: "负载 + CPU/内存 Top 进程快照" },
|
||||
{ cmd: "vmstat 1 3 2>/dev/null || vmstat 2 2>/dev/null | head -5", purpose: "CPU/内存/IO 实时活动" },
|
||||
{ cmd: "echo '=== IO ==='; iostat -x 1 2 2>/dev/null | tail -40 || iostat 2>/dev/null | head -20 || echo 'iostat_not_available'", purpose: "磁盘 IO 统计 + await 时间" },
|
||||
{ cmd: "echo '=== NET_STAT ==='; nstat -az 2>/dev/null | tail -20 || ss -s 2>/dev/null; echo '=== TCP_SOCK ==='; ss -tan state established 2>/dev/null | wc -l; echo '=== UDP_SOCK ==='; ss -uan 2>/dev/null | wc -l", purpose: "网络收发统计 + TCP/UDP 会话数" },
|
||||
|
||||
// ═══ 3. 硬件健康(传感器) ═══
|
||||
{ cmd: "echo '=== TEMP ==='; sensors 2>/dev/null | head -40 || (cat /sys/class/thermal/thermal_zone*/temp 2>/dev/null | while read t; do echo \"temp: $((t/1000))C\"; done) || echo 'no_sensors_data'", purpose: "CPU/主板温度" },
|
||||
{ cmd: "echo '=== DISK_TEMP ==='; hddtemp /dev/sd? 2>/dev/null || for d in /sys/class/scsi_disk/*/device; do bn=$(basename $(dirname $d)); temp=$(cat $d/../../block/$bn/queue/rotational 2>/dev/null | grep -q 1 && cat $d/../../../../../../devices/platform/*/ata*/host*/target*/*/scsi_disk/*/temperature 2>/dev/null); [ -n \"$temp\" ] && echo \"$bn: ${temp}C\"; done || echo 'disk_temp_not_available'", purpose: "磁盘温度(hddtemp 或 sysfs 兜底)" },
|
||||
{ cmd: "echo '=== RAID ==='; cat /proc/mdstat 2>/dev/null || echo 'no_mdraid'; echo '=== LVM ==='; pvs 2>/dev/null | head -10 || echo 'no_lvm'; echo '=== BTRFS ==='; btrfs filesystem show 2>/dev/null | head -10 || echo 'no_btrfs'", purpose: "阵列/卷管理状态" },
|
||||
{ cmd: "echo '=== UPS ==='; upsc 2>/dev/null | head -10 || echo 'ups_not_monitored'; echo '=== BATTERY ==='; cat /sys/class/power_supply/BAT*/status 2>/dev/null | head -5 || echo 'no_laptop_battery'", purpose: "电源/UPS/笔记本电池" },
|
||||
|
||||
// ═══ 4. 网络配置 ═══
|
||||
{ cmd: "echo '=== IP ==='; ip -br addr 2>/dev/null; echo '=== LINK ==='; ip -br link 2>/dev/null; echo '=== RX_ERROR ==='; ip -s link 2>/dev/null | egrep '(errors|dropped):|RX:|TX:' | head -30", purpose: "接口 IP + up/down + 网卡错误包统计" },
|
||||
{ cmd: "echo '=== ROUTE ==='; ip route 2>/dev/null; echo '=== GATEWAY_PING ==='; ping -c 2 -W 2 $(ip route show default 2>/dev/null | awk '{print $3; exit}') 2>/dev/null | tail -5 || echo 'no_default_gateway'", purpose: "路由表 + 默认网关连通性" },
|
||||
{ cmd: "echo '=== DNS ==='; cat /etc/resolv.conf 2>/dev/null | head -10; echo '=== DNS_TEST ==='; timeout 3 nslookup www.baidu.com 2>/dev/null | tail -5 || timeout 3 host www.baidu.com 2>/dev/null | tail -5 || echo 'no_dns_tool'", purpose: "DNS 配置 + 解析测试" },
|
||||
{ cmd: "echo '=== CONNTRACK ==='; conntrack -C 2>/dev/null || echo 'no_conntrack'; echo '=== SYNFLOOD ==='; sysctl net.ipv4.tcp_max_syn_backlog 2>/dev/null; echo '=== TIME_WAIT ==='; ss -tan state time-wait 2>/dev/null | wc -l; echo '=== ESTAB ==='; ss -tan state established 2>/dev/null | wc -l", purpose: "连接跟踪 + SYN backlog + 连接状态分布" },
|
||||
|
||||
// ═══ 5. 安全检查 ═══
|
||||
{ cmd: "echo '=== OPEN_PORTS ==='; ss -tulnp 2>/dev/null | head -30; echo '=== LISTEN_UNKNOWN_USER ==='; ss -tulnp 2>/dev/null | grep -v 'Users:' | head -10 || echo 'all_listen_have_owner'", purpose: "监听端口 + 异常无名端口" },
|
||||
{ cmd: "echo '=== LAST_LOGIN ==='; last -n 5 2>/dev/null | head -8; echo '=== FAILED_LOGIN ==='; lastb -n 10 2>/dev/null | head -12 || echo 'no_failed_logins_or_lastb'; echo '=== CURRENT ==='; who 2>/dev/null", purpose: "登录历史 + 失败登录 + 当前在线" },
|
||||
{ cmd: "echo '=== SUSPICIOUS_PROC ==='; ps aux 2>/dev/null | egrep 'miner|xmrig|cryptonight|stratum|nc .* -l|/dev/tcp|reverse shell|bash -i.*>&' | grep -v grep | head -10 || echo 'no_suspicious_process'; echo '=== ROOT_PROC ==='; ps aux 2>/dev/null | awk '$1==\"root\" && $11 !~ /^(\\[|/usr/sbin|/usr/bin|/bin|/sbin|systemd|ps\\b)/ && NR>1 {print $11}' | sort -u | head -20", purpose: "挖矿/反弹 shell 可疑进程 + root 运行的非标准路径进程" },
|
||||
{ cmd: "echo '=== SUID_FILES ==='; find / -perm -4000 -type f 2>/dev/null | xargs -r ls -la 2>/dev/null | awk '{print $NF, $1}' | head -20; echo '=== WORLD_WRITABLE ==='; find /etc -perm -o+w -type f 2>/dev/null | head -10 || echo 'no_world_writable_etc'", purpose: "SUID 文件(检查提权后门)+ /etc 下可写文件" },
|
||||
{ cmd: "echo '=== FIREWALL ==='; (iptables -L -n 2>/dev/null | head -30) || (ufw status 2>/dev/null) || (firewall-cmd --list-all 2>/dev/null) || echo 'no_firewall_active'; echo '=== SELINUX ==='; getenforce 2>/dev/null || echo 'selinux_not_available'; echo '=== APPARMOR ==='; aa-status 2>/dev/null | head -5 || echo 'apparmor_not_available'", purpose: "防火墙 + MAC 强制访问控制状态" },
|
||||
|
||||
// ═══ 6. 服务状态 ═══
|
||||
{ cmd: "echo '=== FAILED_SVC ==='; systemctl --failed --no-pager 2>/dev/null | head -15 || echo 'systemd_not_available'; echo '=== SVC_TOTAL ==='; systemctl list-units --type=service --no-pager 2>/dev/null | wc -l; echo '=== CRON ==='; crontab -l 2>/dev/null | head -15 || echo 'no_user_cron'; echo '=== AT ==='; atq 2>/dev/null | head -5 || echo 'no_at_jobs'", purpose: "失败 systemd 服务 + 定时任务" },
|
||||
{ cmd: "echo '=== KEY_SVC ==='; for svc in sshd ssh nginx httpd apache2 mysql mysqld postgres redis-server docker containerd kubelet; do systemctl is-active $svc 2>/dev/null | grep -q active && echo \"$svc: active\"; done; echo '=== DOCKER ==='; docker ps --format 'table {{.Names}}\\t{{.Status}}\\t{{.Ports}}' 2>/dev/null | head -15 || echo 'no_docker'", purpose: "关键服务状态 + Docker 容器" },
|
||||
|
||||
// ═══ 7. 系统日志(分层获取,AI 重点分析) ═══
|
||||
{ cmd: "echo '=== JOURNAL_ERR ==='; journalctl -p err..alert -n 80 --no-pager -q 2>/dev/null | tail -40 || (grep -i 'error\\|fail\\|critical\\|panic' /var/log/syslog /var/log/messages 2>/dev/null | tail -30) || echo 'no_error_log'", purpose: "最近 error/critical 级别(journal + syslog 兜底)" },
|
||||
{ cmd: "echo '=== JOURNAL_WARN ==='; journalctl -p warning..warning -n 80 --no-pager -q 2>/dev/null | tail -40 || (grep -i 'warning\\|warn\\|alert' /var/log/syslog /var/log/messages 2>/dev/null | tail -25) || echo 'no_warn_log'", purpose: "最近 warning 级别日志" },
|
||||
{ cmd: "echo '=== JOURNAL_30MIN ==='; journalctl --since '30 min ago' -n 100 --no-pager -q 2>/dev/null | tail -50 || echo 'no_recent_30min_log'", purpose: "最近 30 分钟全部日志(时间窗口过滤)" },
|
||||
{ cmd: "echo '=== KERNEL_LOG ==='; journalctl -k -n 60 --no-pager -q 2>/dev/null | tail -30 || (dmesg 2>/dev/null | tail -25) || echo 'no_kernel_log'; echo '=== OOM ==='; (dmesg 2>/dev/null | grep -i 'oom\\|out of memory\\|killed process' || journalctl -k 2>/dev/null | grep -i oom) | tail -10 || echo 'no_oom_event'; echo '=== KERNEL_PANIC ==='; (dmesg 2>/dev/null | grep -i 'panic\\|oops\\|general protection' || journalctl -k 2>/dev/null | grep -i 'panic\\|oops') | tail -10 || echo 'no_panic_oops'", purpose: "内核日志 + OOM + panic/oops(三类关键内核事件分开过滤)" },
|
||||
{ cmd: "echo '=== AUDIT_LOG ==='; (ausearch -ie -ts recent -30m 2>/dev/null | head -40) || (grep -i 'denied\\|avc:\\|audit.*denied\\|user=root' /var/log/audit/audit.log 2>/dev/null | tail -25) || echo 'no_audit_log'; echo '=== FAILED_AUTH ==='; (grep -i 'failed password\\|authentication failure\\|invalid user' /var/log/auth.log /var/log/secure 2>/dev/null | tail -20) || journalctl _COMM=sshd --since '30 min ago' 2>/dev/null | grep -i fail | tail -15 || echo 'no_failed_auth'", purpose: "审计日志 + SSH 失败登录" },
|
||||
{ cmd: "echo '=== AUTH_LAST ==='; lastb -n 15 2>/dev/null | head -18 || echo 'no_lastb'; echo '=== SUDO ==='; tail -20 /var/log/auth.log 2>/dev/null | grep -i sudo || tail -20 /var/log/secure 2>/dev/null | grep -i sudo || journalctl _COMM=sudo --since '1 hour ago' 2>/dev/null | tail -15 || echo 'no_sudo_log'", purpose: "lastb 失败登录 + sudo 执行记录" },
|
||||
|
||||
// ═══ 8. 虚拟化 / 云环境检测 ═══
|
||||
{ cmd: "echo '=== VIRT ==='; hostnamectl 2>/dev/null | grep -i virtual || systemd-detect-virt 2>/dev/null || echo 'no_virt_detect'; echo '=== CLOUD ==='; (curl -s --connect-timeout 1 http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null | head -1 && echo 'aws_detected') || echo 'not_aws'; (curl -s --connect-timeout 1 -H 'Metadata: true' http://169.254.168.254/metadata/instance?api-version=2021-02-01 2>/dev/null | grep -q 'compute' && echo 'azure_detected') || echo 'not_azure'; echo '=== SNAP ==='; snap list 2>/dev/null | head -5 || echo 'no_snap'; echo '=== FLAT ==='; flatpak list 2>/dev/null | head -5 || echo 'no_flatpak'", purpose: "虚拟化类型 + 云厂商 + 包管理器" },
|
||||
|
||||
// ═══ 9. 资源异常阈值(快速告警) ═══
|
||||
{ cmd: "echo '=== THRESHOLD ==='; echo 'CPU_LOAD_15:' $(awk '{print $1}' /proc/loadavg)/$(nproc 2>/dev/null || echo 1); echo 'MEM_USED_PCT:' $(awk '/MemTotal/{t=$2}/MemAvailable/{a=$2}END{printf \"%.1f\",(t-a)/t*100}' /proc/meminfo 2>/dev/null); echo 'SWAP_USED_PCT:' $(awk '/SwapTotal/{t=$2}/SwapFree/{f=$2}END{if(t>0) printf \"%.1f\",(t-f)/t*100; else print \"0.0\"}' /proc/meminfo 2>/dev/null); echo 'ROOT_USED_PCT:' $(df / 2>/dev/null | awk 'NR==2{print $5}' | tr -d %); echo 'INODE_ROOT_PCT:' $(df -i / 2>/dev/null | awk 'NR==2{print $5}' | tr -d %); echo 'PROC_COUNT:' $(ps aux --no-headers 2>/dev/null | wc -l); echo 'FD_PERCENT:' $(awk '{if($3>0) printf \"%.1f\",$1/$3*100; else print \"0.0\"}' /proc/sys/fs/file-nr 2>/dev/null)", purpose: "资源阈值快速快照(AI 异常识别主数据源,/proc/meminfo 精确计算)" },
|
||||
|
||||
// ═══ 10. 时间同步 ═══
|
||||
{ cmd: "echo '=== NTP_CHRONY ==='; chronyc tracking 2>/dev/null | head -10 || echo 'no_chronyc'; chronyc sources 2>/dev/null | head -10 || true; ntpq -p 2>/dev/null | head -10 || echo 'no_ntpq'; echo '=== DATE ==='; date -u; echo '=== HWCLK ==='; timedatectl 2>/dev/null | head -8 || echo 'no_timedatectl'", purpose: "NTP/Chrony 时间同步状态 + 硬件时钟" },
|
||||
|
||||
// ═══ 11. 最近变更(变更历史) ═══
|
||||
{ cmd: "echo '=== APT_DPKG ==='; test -f /var/log/apt/history.log && zgrep -E 'Start-Date|End-Date|Commandline|Install|Upgrade|Remove' /var/log/apt/history.log* 2>/dev/null | tail -25 || echo 'no_apt_log'; echo '=== DNF_YUM ==='; dnf history 2>/dev/null | head -12 || yum history 2>/dev/null | head -12 || echo 'no_dnf_yum'; echo '=== RECENT_PKG ==='; ls -lt /var/cache/apt/archives/ 2>/dev/null | head -8 || ls -lt /var/cache/yum/ 2>/dev/null | head -8 || echo 'no_pkg_cache'", purpose: "最近包安装/升级历史" },
|
||||
|
||||
// ═══ 12. Swap + 限制检查 ═══
|
||||
{ cmd: "echo '=== SWAP ==='; swapon --show 2>/dev/null || cat /proc/swaps 2>/dev/null; echo '=== SWAPPINESS ==='; cat /proc/sys/vm/swappiness 2>/dev/null; echo '=== ULIMIT ==='; ulimit -a 2>/dev/null | head -8; echo '=== FD_LIMIT ==='; cat /proc/sys/fs/file-max 2>/dev/null; echo '=== CURRENT_FD ==='; cat /proc/sys/fs/file-nr 2>/dev/null; echo '=== CORE_PATTERN ==='; cat /proc/sys/kernel/core_pattern 2>/dev/null", purpose: "Swap 策略 + fd/进程限制 + core dump 配置" },
|
||||
|
||||
// ═══ 13. 内核模块 + PCI ═══
|
||||
{ cmd: "echo '=== LSMOD ==='; lsmod 2>/dev/null | head -30 || cat /proc/modules 2>/dev/null | head -20 || echo 'no_module'; echo '=== PCI_ERR ==='; dmesg 2>/dev/null | grep -i 'pcie\\|mce\\|machine check\\|pci error' | tail -10 || echo 'no_pci_error'; echo '=== SYSCTL_CORE ==='; sysctl -a 2>/dev/null | egrep 'kernel\\.(panic|oops|modprobe|kptr)' | head -10 || echo 'no_sysctl'", purpose: "内核模块 + PCI 硬件错误 + 内核 panic/oops 策略" },
|
||||
|
||||
// ═══ 14. K8s / 容器编排探测 ═══
|
||||
{ cmd: "echo '=== KUBELET ==='; systemctl is-active kubelet 2>/dev/null || echo 'no_kubelet'; echo '=== KUBE_NODES ==='; kubectl get nodes --no-headers 2>/dev/null || echo 'no_kubectl_or_no_cluster'; echo '=== KUBE_PODS ==='; kubectl get pods -A --no-headers --field-selector=status.phase!=Running 2>/dev/null | head -15 || echo 'no_non_running_pod_or_no_kubectl'; echo '=== CRI ==='; crictl ps 2>/dev/null | head -10 || echo 'no_crictl'", purpose: "K8s 节点状态 + 非 Running Pod 列表 + CRI 容器" },
|
||||
],
|
||||
},
|
||||
|
||||
// ─── Huawei/H3C 交换机 ─────────────────────
|
||||
huawei_h3c_switch: {
|
||||
shell: "huawei_cli",
|
||||
preCmd: "screen-length 0 temporary",
|
||||
commands: [
|
||||
{ cmd: "display version", altCmd: null, purpose: "型号 + 固件 + 启动时长" },
|
||||
{ cmd: "display device", altCmd: "display device all", purpose: "所有板卡模块状态 (Master/Slave/Registered)" },
|
||||
{ cmd: "display temperature", altCmd: "display temperature all", purpose: "各板卡温度" },
|
||||
{ cmd: "display fan", altCmd: "display fan all", purpose: "风扇状态" },
|
||||
{ cmd: "display power", altCmd: "display power all", purpose: "电源状态" },
|
||||
{ cmd: "display memory", altCmd: "display memory-usage", purpose: "内存使用率" },
|
||||
{ cmd: "display cpu", altCmd: "display cpu-usage 5 1", purpose: "CPU 使用率快照" },
|
||||
{ cmd: "display interface brief", altCmd: "display ip interface brief", purpose: "所有端口 up/down + 收发包统计" },
|
||||
{ cmd: "display ip interface brief", altCmd: "display interface brief", purpose: "三层 Vlanif 接口 IP 状态" },
|
||||
{ cmd: "display arp", altCmd: "display arp all", purpose: "ARP 表" },
|
||||
{ cmd: "display logbuffer level warning size 200", altCmd: "display logbuffer size 200", purpose: "最近 warning 级别日志" },
|
||||
{ cmd: "display stp", altCmd: "display stp brief", purpose: "STP 角色 + 阻塞端口" },
|
||||
{ cmd: "display vrrp", altCmd: null, purpose: "VRRP 状态" },
|
||||
{ cmd: "display ntp status", altCmd: "display ntp-service status", purpose: "NTP peer + stratum" },
|
||||
{ cmd: "display vlan", altCmd: "display vlan brief", purpose: "VLAN 列表 + 端口归属" },
|
||||
],
|
||||
},
|
||||
|
||||
// ─── Cisco IOS 交换机 ─────────────────────
|
||||
cisco_switch: {
|
||||
shell: "cisco_ios",
|
||||
preCmd: "terminal length 0",
|
||||
commands: [
|
||||
{ cmd: "show version", purpose: "型号 + IOS 版本 + 启动时长" },
|
||||
{ cmd: "show inventory", purpose: "硬件模块 + 序列号" },
|
||||
{ cmd: "show environment status", purpose: "电源 + 风扇 + 温度" },
|
||||
{ cmd: "show processes cpu sorted 1min", purpose: "CPU Top 进程(1min 平均)" },
|
||||
{ cmd: "show memory summary", purpose: "内存总量/已用" },
|
||||
{ cmd: "show interfaces brief", purpose: "所有端口 up/down" },
|
||||
{ cmd: "show interfaces counters errors", purpose: "端口错误包(CRC/input error)" },
|
||||
{ cmd: "show ip interface brief", purpose: "三层接口 IP 状态" },
|
||||
{ cmd: "show arp", purpose: "ARP 表" },
|
||||
{ cmd: "show log tail 100", purpose: "最近 100 行日志" },
|
||||
{ cmd: "show spanning-tree summary", purpose: "STP 总览" },
|
||||
{ cmd: "show vlan brief", purpose: "VLAN 列表" },
|
||||
{ cmd: "show ip route summary", purpose: "路由表总览" },
|
||||
{ cmd: "show ntp status", purpose: "NTP peer + stratum" },
|
||||
],
|
||||
},
|
||||
|
||||
// ─── Huawei 防火墙 ─────────────────────────
|
||||
huawei_firewall: {
|
||||
shell: "huawei_cli",
|
||||
preCmd: "screen-length 0 temporary",
|
||||
commands: [
|
||||
{ cmd: "display version", purpose: "型号 + 固件" },
|
||||
{ cmd: "display device", purpose: "模块状态" },
|
||||
{ cmd: "display temperature", purpose: "温度" },
|
||||
{ cmd: "display fan", purpose: "风扇" },
|
||||
{ cmd: "display power", purpose: "电源" },
|
||||
{ cmd: "display memory", purpose: "内存" },
|
||||
{ cmd: "display firewall session table | include Total", purpose: "会话表统计" },
|
||||
{ cmd: "display security-policy rule all | head -20", purpose: "安全策略概览" },
|
||||
{ cmd: "display logbuffer level warning size 200", purpose: "最近 warning 日志" },
|
||||
{ cmd: "display ntp status", purpose: "NTP" },
|
||||
],
|
||||
},
|
||||
|
||||
// ─── Fortinet FortiOS 防火墙 ────────────────
|
||||
fortinet_firewall: {
|
||||
shell: "fortigate",
|
||||
preCmd: "set console length 0",
|
||||
commands: [
|
||||
{ cmd: "get system status", purpose: "型号 + FortiOS 版本 + 启动时长" },
|
||||
{ cmd: "get system performance cpu", purpose: "CPU 使用率" },
|
||||
{ cmd: "get system performance memory", purpose: "内存使用率" },
|
||||
{ cmd: "get system hardware", purpose: "硬件模块 + 温度 + 风扇" },
|
||||
{ cmd: "get session stat", purpose: "会话统计" },
|
||||
{ cmd: "get router info routing-table summary", purpose: "路由表" },
|
||||
{ cmd: "diag debug flow stat", purpose: "流量统计" },
|
||||
{ cmd: "get log report local-traffic", purpose: "最近流量日志" },
|
||||
{ cmd: "execute time", purpose: "系统时间 + NTP" },
|
||||
],
|
||||
},
|
||||
|
||||
// ─── 默认 fallback(catalog 未知)────────────
|
||||
// 自适应识别:先探测是什么类型的设备,再按类型跑对应的精简命令
|
||||
unknown: {
|
||||
shell: "unknown",
|
||||
preCmd: null,
|
||||
commands: [
|
||||
// 探测 1:能不能跑 bash?→ Linux
|
||||
{ cmd: "echo '=== DETECT ==='; uname -a 2>&1 | head -3; echo '=== TRY_LINUX ==='; ls /proc/version 2>/dev/null && echo IS_LINUX || echo NOT_LINUX; which bash 2>/dev/null; echo '=== TRY_SWITCH ==='; echo '=== OS_PROMPT_CHECK ==='; cat /etc/os-release 2>/dev/null | head -3 || true", purpose: "探测设备类型(Linux / 交换机 / 防火墙)" },
|
||||
|
||||
// 如果探测到 Linux,这些命令在 Linux 上有效;交换机上会报 command not found(被 ERROR_PATTERNS 过滤掉也没关系)
|
||||
{ cmd: "echo '=== BASIC ==='; hostname 2>/dev/null || host; uptime 2>/dev/null || echo 'no_uptime'; free -h 2>/dev/null || echo 'no_free'; df -h 2>/dev/null | head -10 || echo 'no_df'; whoami", purpose: "基础系统信息(Linux 优先)" },
|
||||
{ cmd: "echo '=== CPU ==='; cat /proc/cpuinfo 2>/dev/null | head -15 || echo 'no_cpuinfo'; echo '=== MEM ==='; cat /proc/meminfo 2>/dev/null | head -5 || echo 'no_meminfo'", purpose: "CPU/内存(Linux /proc 方式)" },
|
||||
{ cmd: "echo '=== NET ==='; ip addr 2>/dev/null | head -20 || ifconfig 2>/dev/null | head -20 || echo 'no_net_cmd'; ip route 2>/dev/null | head -5 || echo 'no_route'; ss -tulnp 2>/dev/null | head -15 || netstat -tulnp 2>/dev/null | head -15 || echo 'no_socket_cmd'", purpose: "网络接口 + 监听端口" },
|
||||
{ cmd: "echo '=== TOP ==='; top -bn1 2>/dev/null | head -15 || ps aux 2>/dev/null | head -15 || echo 'no_top'; echo '=== DISK ==='; lsblk 2>/dev/null || fdisk -l 2>/dev/null | head -20 || echo 'no_disk'; echo '=== TEMP ==='; sensors 2>/dev/null | head -15 || cat /sys/class/thermal/thermal_zone*/temp 2>/dev/null || echo 'no_temp'", purpose: "Top 进程 + 磁盘 + 温度" },
|
||||
{ cmd: "echo '=== FAILED_SVC ==='; systemctl --failed --no-pager 2>/dev/null | head -10 || echo 'no_systemd'; echo '=== ERR_LOG ==='; journalctl -p err..alert -n 20 --no-pager 2>/dev/null | tail -15 || (grep -i 'error\\|fail\\|critical' /var/log/syslog /var/log/messages 2>/dev/null | tail -10) || echo 'no_log'; echo '=== OOM ==='; dmesg 2>/dev/null | grep -i oom | tail -5 || echo 'no_oom'", purpose: "失败服务 + 错误日志" },
|
||||
{ cmd: "echo '=== SEC ==='; ps aux 2>/dev/null | egrep 'miner|xmrig|nc .* -l|bash -i.*>&' | grep -v grep | head -5 || echo 'no_suspicious'; last -n 5 2>/dev/null || echo 'no_last'; echo '=== THRESHOLD ==='; echo 'LOAD:' $(awk '{print $1}' /proc/loadavg 2>/dev/null || echo 0); echo 'MEM_PCT:' $(free 2>/dev/null | awk '/Mem:/{printf \"%.1f\",$3/$2*100}' || echo 0); echo 'DISK_ROOT:' $(df / 2>/dev/null | awk 'NR==2{print $5}' || echo 0)", purpose: "安全快速检查 + 资源阈值" },
|
||||
|
||||
// 如果探测到交换机/防火墙,这些 show/display 命令有效
|
||||
{ cmd: "echo '=== SHOW_VERSION ==='; show version 2>&1 | head -20 || echo 'show_version_failed'; echo '=== DISPLAY_VERSION ==='; display version 2>&1 | head -20 || echo 'display_version_failed'; echo '=== SHOW_ENV ==='; show environment 2>&1 | head -20 || display environment 2>&1 | head -20 || echo 'env_failed'", purpose: "版本/硬件状态(交换机/防火墙)" },
|
||||
{ cmd: "echo '=== SHOW_INT ==='; show interfaces brief 2>&1 | head -25 || display interface brief 2>&1 | head -25 || echo 'int_failed'; echo '=== SHOW_INV ==='; show inventory 2>&1 | head -20 || display device 2>&1 | head -20 || echo 'inv_failed'", purpose: "端口 + 板卡(交换机/防火墙)" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
function getReportCommands(catalogId) {
|
||||
return REORT_COMMANDS_BY_CATALOG[catalogId] || REORT_COMMANDS_BY_CATALOG.unknown;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
REORT_COMMANDS_BY_CATALOG,
|
||||
getReportCommands,
|
||||
};
|
||||
1184
electron/bridges/inspection/reportExport.cjs
Normal file
1184
electron/bridges/inspection/reportExport.cjs
Normal file
File diff suppressed because it is too large
Load Diff
78
electron/bridges/inspection/reportScheduleStore.cjs
Normal file
78
electron/bridges/inspection/reportScheduleStore.cjs
Normal file
@@ -0,0 +1,78 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* 巡检报告调度配置存储
|
||||
*
|
||||
* 存储内容(report-schedule.json):
|
||||
* {
|
||||
* enabled: true,
|
||||
* defaultIntervalHours: 1, // 全局默认巡检周期(小时)
|
||||
* dailyReportTime: "23:00", // 日运行报告定时触发时间 HH:MM
|
||||
* enabledDeviceIds: null | string[], // null/undefined = 全部;[] = 一个都不选;["id1","id2"] = 指定
|
||||
* perDeviceInterval: { host_a: 2 }, // 单设备周期覆盖(小时)
|
||||
* aiSummaryEnabled: true // AI 汇总分析开关
|
||||
* }
|
||||
*/
|
||||
|
||||
const { STORAGE_KEY_REPORT_SCHEDULE } = require("./storageKeys.cjs");
|
||||
|
||||
const DEFAULTS = {
|
||||
enabled: false,
|
||||
defaultIntervalHours: 1,
|
||||
dailyReportTime: "23:00",
|
||||
enabledDeviceIds: null, // null = 全部设备(向后兼容旧的 empty array 也按全部处理)
|
||||
perDeviceInterval: {},
|
||||
aiSummaryEnabled: true,
|
||||
};
|
||||
|
||||
function _safeParse(raw, def) {
|
||||
try { return JSON.parse(raw); }
|
||||
catch { return def; }
|
||||
}
|
||||
|
||||
function _normalize(cfg) {
|
||||
// 旧数据兼容:undefined enabledDeviceIds → null(全部)
|
||||
// 新约定:null = 全部;[] = 一个都不选;[id1,id2] = 指定 —— 不主动改动
|
||||
if (cfg.enabledDeviceIds === undefined) {
|
||||
cfg.enabledDeviceIds = null;
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
function getSchedule(readFn) {
|
||||
const raw = readFn(STORAGE_KEY_REPORT_SCHEDULE);
|
||||
if (!raw) return _normalize({ ...DEFAULTS });
|
||||
const parsed = _safeParse(raw, {});
|
||||
return _normalize({ ...DEFAULTS, ...parsed });
|
||||
}
|
||||
|
||||
function saveSchedule(writeFn, config) {
|
||||
const merged = _normalize({ ...DEFAULTS, ...config });
|
||||
writeFn(STORAGE_KEY_REPORT_SCHEDULE, JSON.stringify(merged, null, 2));
|
||||
return merged;
|
||||
}
|
||||
|
||||
function getIntervalHours(readFn, deviceId) {
|
||||
const cfg = getSchedule(readFn);
|
||||
if (cfg.perDeviceInterval && cfg.perDeviceInterval[deviceId]) {
|
||||
return Number(cfg.perDeviceInterval[deviceId]) || cfg.defaultIntervalHours;
|
||||
}
|
||||
return cfg.defaultIntervalHours;
|
||||
}
|
||||
|
||||
function isDeviceEnabled(readFn, deviceId) {
|
||||
const cfg = getSchedule(readFn);
|
||||
if (!cfg.enabled) return false;
|
||||
// null/undefined = 全部;空数组 = 都不选;非空数组 = 白名单
|
||||
if (!cfg.enabledDeviceIds) return true;
|
||||
if (cfg.enabledDeviceIds.length === 0) return false;
|
||||
return cfg.enabledDeviceIds.includes(deviceId);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULTS,
|
||||
getSchedule,
|
||||
saveSchedule,
|
||||
getIntervalHours,
|
||||
isDeviceEnabled,
|
||||
};
|
||||
157
electron/bridges/inspection/reportScheduler.cjs
Normal file
157
electron/bridges/inspection/reportScheduler.cjs
Normal file
@@ -0,0 +1,157 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* 巡检报告定时调度器
|
||||
*
|
||||
* 和 inspectionScheduler.cjs **物理隔离**:
|
||||
* - 独立 tick 循环(1min)
|
||||
* - 独立配置存储(reportScheduleStore)
|
||||
* - 独立触发路径(调 index.cjs 里 runReportSnapshot 的实现函数)
|
||||
* - 独立日报告聚合(dailyReportStore)
|
||||
*
|
||||
* 防止重蹈「两个调度器竞争同一巡检任务」的覆辙。
|
||||
*/
|
||||
|
||||
const reportScheduleStore = require("./reportScheduleStore.cjs");
|
||||
const reportStore = require("./reportStore.cjs");
|
||||
const dailyReportStore = require("./dailyReportStore.cjs");
|
||||
|
||||
let _tickTimer = null;
|
||||
let _dailyTimer = null;
|
||||
let _lastDailyTriggerKey = null; // 用于 HH:MM 日报告每日只触发一次
|
||||
let _deviceLastRun = new Map(); // deviceId → timestamp(用于 per-device interval)
|
||||
|
||||
/**
|
||||
* 启动 reportScheduler
|
||||
*
|
||||
* @param {object} deps
|
||||
* @param {function} deps.readStorage
|
||||
* @param {function} deps.writeStorage
|
||||
* @param {function} deps.runSnapshotFn - 定时快照执行函数(renderer 侧 Hook 实际负责执行,这里传空函数)
|
||||
* @param {function} [deps.onDailyReportReady] - 日报告聚合完成后回调 async (report) => void
|
||||
*/
|
||||
function start({ readStorage, writeStorage, runSnapshotFn, onDailyReportReady }) {
|
||||
if (_tickTimer) return; // 已启动
|
||||
|
||||
const readFn = (k) => readStorage(k);
|
||||
const writeFn = (k, v) => writeStorage(k, v);
|
||||
|
||||
const tick = async () => {
|
||||
const cfg = reportScheduleStore.getSchedule(readFn);
|
||||
// 临时验证:忽略 cfg.enabled
|
||||
// if (!cfg.enabled) return;
|
||||
|
||||
const now = Date.now();
|
||||
const dueTargets = [];
|
||||
|
||||
// null/undefined = 全部;空数组 = 不巡检;非空数组 = 指定设备
|
||||
let deviceIds = cfg.enabledDeviceIds;
|
||||
if (deviceIds === null || deviceIds === undefined) {
|
||||
// 全部设备 —— 从 deviceStore 拉所有设备 ID
|
||||
try {
|
||||
const deviceStore = require("./deviceStore.cjs");
|
||||
const all = deviceStore.listDevices(readFn);
|
||||
deviceIds = all.map(d => d.host_id || d.id).filter(Boolean);
|
||||
console.log("[reportScheduler] tick: enabledDeviceIds=null, deviceStore.listDevices →", all.length, "台 → ids=", JSON.stringify(deviceIds));
|
||||
} catch (e) {
|
||||
console.error("[reportScheduler] ❌ 读取全部设备失败:", e?.message, e?.stack?.split("\n").slice(0,3).join("\n"));
|
||||
return;
|
||||
}
|
||||
} else if (Array.isArray(deviceIds) && deviceIds.length === 0) {
|
||||
console.log("[reportScheduler] tick: enabledDeviceIds=[], 跳过(不巡检任何设备)");
|
||||
return;
|
||||
} else {
|
||||
console.log("[reportScheduler] tick: enabledDeviceIds 非空数组 →", JSON.stringify(deviceIds));
|
||||
}
|
||||
|
||||
for (const deviceId of deviceIds || []) {
|
||||
if (!reportScheduleStore.isDeviceEnabled(readFn, deviceId)) continue;
|
||||
// 临时验证:强制 2 分钟间隔 + 首次立即触发
|
||||
const intervalMs = 2 * 60 * 1000;
|
||||
const lastRun = _deviceLastRun.get(deviceId) || 0;
|
||||
if (now - lastRun >= intervalMs) {
|
||||
dueTargets.push(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[reportScheduler] tick: 总设备", deviceIds?.length || 0, "台, 到点", dueTargets.length, "台");
|
||||
if (dueTargets.length === 0) return;
|
||||
console.log(`[reportScheduler] tick: ${dueTargets.length} 台设备到点,interval=${cfg.defaultIntervalHours}h`);
|
||||
try {
|
||||
await runSnapshotFn(dueTargets, "scheduled");
|
||||
for (const id of dueTargets) _deviceLastRun.set(id, now);
|
||||
} catch (e) {
|
||||
console.error("[reportScheduler] runSnapshot 失败:", e?.message || String(e));
|
||||
}
|
||||
};
|
||||
|
||||
// 临时验证:启动后立刻执行一次 tick(不等间隔)
|
||||
console.log("[reportScheduler] 启动,立即执行首次 tick");
|
||||
tick().catch(e => console.error("[reportScheduler] 首次 tick 异常:", e));
|
||||
|
||||
// 临时验证:每 5 秒 tick 一次
|
||||
_tickTimer = setInterval(() => { tick().catch(e => console.error("[reportScheduler] tick 异常:", e)); }, 5 * 1000);
|
||||
_tickTimer.unref?.();
|
||||
|
||||
// 每秒检查是否到 dailyReportTime(HH:MM 精确匹配,每日只触发一次)
|
||||
const dailyCheck = () => {
|
||||
const cfg = reportScheduleStore.getSchedule(readFn);
|
||||
if (!cfg.enabled) return;
|
||||
const now = new Date();
|
||||
const p = (n) => n.toString().padStart(2, "0");
|
||||
const current = `${p(now.getHours())}:${p(now.getMinutes())}`;
|
||||
const key = `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}`;
|
||||
|
||||
if (current === cfg.dailyReportTime && _lastDailyTriggerKey !== key) {
|
||||
_lastDailyTriggerKey = key;
|
||||
console.log("[reportScheduler] 触发日运行报告:", key);
|
||||
triggerDailyReport(readFn, writeFn, { mode: "finalized", trigger: "scheduled" })
|
||||
.then(report => {
|
||||
if (onDailyReportReady) {
|
||||
onDailyReportReady(report).catch(e =>
|
||||
console.error("[reportScheduler] onDailyReportReady 异常:", e?.message || String(e))
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(e => console.error("[reportScheduler] 日报告生成异常:", e));
|
||||
}
|
||||
};
|
||||
_dailyTimer = setInterval(dailyCheck, 30 * 1000);
|
||||
_dailyTimer.unref?.();
|
||||
|
||||
console.log("[reportScheduler] 已启动");
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (_tickTimer) { clearInterval(_tickTimer); _tickTimer = null; }
|
||||
if (_dailyTimer) { clearInterval(_dailyTimer); _dailyTimer = null; }
|
||||
console.log("[reportScheduler] 已停止");
|
||||
}
|
||||
|
||||
function isRunning() {
|
||||
return !!_tickTimer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发日运行报告(立即执行,不等到 HH:MM)
|
||||
* mode: "provisional" — 取当天 0:00 ~ 此刻
|
||||
* "finalized" — 取当天 0:00 ~ 23:59
|
||||
*/
|
||||
async function triggerDailyReport(readFn, writeFn, { mode = "provisional", trigger = "manual" } = {}) {
|
||||
const now = Date.now();
|
||||
const dayStart = dailyReportStore._dayStart(now);
|
||||
const dayEnd = mode === "finalized" ? dailyReportStore._dayEnd(now) : now;
|
||||
|
||||
const allSnapshots = reportStore.listSnapshots(readFn, 1000);
|
||||
const report = dailyReportStore.buildDailyReport(allSnapshots, dayStart, dayEnd, { mode, trigger });
|
||||
dailyReportStore.addDailyReport(readFn, writeFn, report);
|
||||
console.log(`[reportScheduler] 日报告 ${report.id} 已生成 (mode=${mode})`);
|
||||
return report;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
stop,
|
||||
isRunning,
|
||||
triggerDailyReport,
|
||||
};
|
||||
230
electron/bridges/inspection/reportStore.cjs
Normal file
230
electron/bridges/inspection/reportStore.cjs
Normal file
@@ -0,0 +1,230 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* 巡检报告快照存储
|
||||
*
|
||||
* 每次 runReportSnapshot 生成一条快照:
|
||||
*
|
||||
* {
|
||||
* id, // "snap_<ts>_<rand>"
|
||||
* timestamp, // 快照生成时间
|
||||
* duration_ms, // 整体耗时
|
||||
* trigger, // "manual" | "scheduled"
|
||||
* devices: [ // 每台设备的报告
|
||||
* {
|
||||
* device_id,
|
||||
* hostname,
|
||||
* catalog_id,
|
||||
* ok, // 设备上所有命令都 ok = true
|
||||
* duration_ms,
|
||||
* commands: [ // 每一条命令的执行结果
|
||||
* { cmd, purpose, ok, duration_ms, stdout, stderr }
|
||||
* ]
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* 为避免 JSON 无限膨胀,MAX_SNAPSHOTS 条整体保留,每条设备 stdout 截断 MAX_CMD_STDOUT 字符。
|
||||
*/
|
||||
|
||||
const { STORAGE_KEY_REPORT_SNAPSHOTS } = require("./storageKeys.cjs");
|
||||
|
||||
const MAX_SNAPSHOTS = 200; // 保留最近 200 次快照
|
||||
const MAX_CMD_STDOUT = 4000; // 单条命令 stdout 保留前 4000 字符(display logbuffer 可能 10k+)
|
||||
|
||||
let _snapshots = null;
|
||||
|
||||
function _safeParse(text, def) {
|
||||
try { return JSON.parse(text); }
|
||||
catch (e) { return def; }
|
||||
}
|
||||
|
||||
function _ensureLoaded(readFn) {
|
||||
if (_snapshots === null) {
|
||||
const raw = readFn(STORAGE_KEY_REPORT_SNAPSHOTS);
|
||||
_snapshots = raw ? _safeParse(raw, []) : [];
|
||||
}
|
||||
}
|
||||
|
||||
function _save(writeFn) {
|
||||
if (_snapshots.length > MAX_SNAPSHOTS) {
|
||||
_snapshots = _snapshots.slice(-MAX_SNAPSHOTS);
|
||||
}
|
||||
writeFn(STORAGE_KEY_REPORT_SNAPSHOTS, JSON.stringify(_snapshots));
|
||||
}
|
||||
|
||||
function _truncate(text, max = MAX_CMD_STDOUT) {
|
||||
if (!text) return "";
|
||||
if (text.length <= max) return text;
|
||||
return text.slice(0, max) + `\n... [truncated ${text.length - max} chars]`;
|
||||
}
|
||||
|
||||
// ---------- 增删改查 ----------
|
||||
|
||||
/**
|
||||
* 新增一条快照。snapshot.devices[].commands[].stdout 会自动截断。
|
||||
*/
|
||||
function addSnapshot(readFn, writeFn, snapshot) {
|
||||
_ensureLoaded(readFn);
|
||||
|
||||
// 自动截断 stdout
|
||||
const cleanDevices = (snapshot.devices || []).map(d => ({
|
||||
...d,
|
||||
commands: (d.commands || []).map(c => ({
|
||||
...c,
|
||||
stdout: _truncate(c.stdout || ""),
|
||||
stderr: _truncate(c.stderr || "", 500),
|
||||
})),
|
||||
}));
|
||||
|
||||
const record = {
|
||||
id: snapshot.id || `snap_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
timestamp: snapshot.timestamp || Date.now(),
|
||||
duration_ms: snapshot.duration_ms || 0,
|
||||
trigger: snapshot.trigger || "manual",
|
||||
devices: cleanDevices,
|
||||
};
|
||||
|
||||
_snapshots.push(record);
|
||||
_save(writeFn);
|
||||
return { ...record };
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出快照(新的在前)
|
||||
*/
|
||||
function listSnapshots(readFn, limit = 50) {
|
||||
_ensureLoaded(readFn);
|
||||
return [..._snapshots].reverse().slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 id 查单条快照
|
||||
*/
|
||||
function getSnapshot(readFn, id) {
|
||||
_ensureLoaded(readFn);
|
||||
return _snapshots.find(s => s.id === id) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除一条快照
|
||||
*/
|
||||
function deleteSnapshot(readFn, writeFn, id) {
|
||||
_ensureLoaded(readFn);
|
||||
const before = _snapshots.length;
|
||||
_snapshots = _snapshots.filter(s => s.id !== id);
|
||||
if (_snapshots.length < before) {
|
||||
_save(writeFn);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有快照
|
||||
*/
|
||||
function clearAll(readFn, writeFn) {
|
||||
_ensureLoaded(readFn);
|
||||
_snapshots = [];
|
||||
_save(writeFn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计概览(用于日运行报告聚合)
|
||||
*/
|
||||
function summarize(readFn, sinceTs) {
|
||||
_ensureLoaded(readFn);
|
||||
const snaps = _snapshots.filter(s => s.timestamp >= sinceTs);
|
||||
|
||||
let totalDevices = 0;
|
||||
let totalCmds = 0;
|
||||
let okCmds = 0;
|
||||
let failedCmds = 0;
|
||||
const failedDevices = new Set();
|
||||
|
||||
for (const snap of snaps) {
|
||||
for (const dev of snap.devices || []) {
|
||||
totalDevices++;
|
||||
for (const cmd of dev.commands || []) {
|
||||
totalCmds++;
|
||||
if (cmd.ok) okCmds++;
|
||||
else { failedCmds++; failedDevices.add(dev.device_id); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
snapshot_count: snaps.length,
|
||||
total_device_checks: totalDevices,
|
||||
total_commands: totalCmds,
|
||||
ok_commands: okCmds,
|
||||
failed_commands: failedCmds,
|
||||
failed_device_ids: Array.from(failedDevices),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记快照 AI 分析状态(running 主进程在跑、done 成功、failed 失败)
|
||||
*/
|
||||
function setSnapshotAiStatus(readFn, writeFn, snapshotId, status, error) {
|
||||
_ensureLoaded(readFn);
|
||||
const snap = _snapshots.find(s => s.id === snapshotId);
|
||||
if (!snap) return false;
|
||||
snap.ai_summary_status = status; // "running" | "done" | "failed" | undefined
|
||||
if (error) snap.ai_summary_error = error;
|
||||
else delete snap.ai_summary_error;
|
||||
if (status === "running") {
|
||||
delete snap.ai_summary;
|
||||
delete snap.ai_summary_updated_at;
|
||||
}
|
||||
_save(writeFn);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新快照的 AI 汇总(AI 成功分析后存回,status 自动设为 done)
|
||||
*/
|
||||
function updateSnapshotAiSummary(readFn, writeFn, snapshotId, aiSummary, error) {
|
||||
_ensureLoaded(readFn);
|
||||
const snap = _snapshots.find(s => s.id === snapshotId);
|
||||
if (!snap) return false;
|
||||
if (error) {
|
||||
snap.ai_summary_status = "failed";
|
||||
snap.ai_summary_error = error;
|
||||
delete snap.ai_summary;
|
||||
} else {
|
||||
snap.ai_summary_status = "done";
|
||||
snap.ai_summary = aiSummary;
|
||||
snap.ai_summary_updated_at = Date.now();
|
||||
delete snap.ai_summary_error;
|
||||
}
|
||||
_save(writeFn);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空快照的 AI 汇总(用于重试前清除旧结果)
|
||||
*/
|
||||
function clearSnapshotAiSummary(readFn, writeFn, snapshotId) {
|
||||
_ensureLoaded(readFn);
|
||||
const snap = _snapshots.find(s => s.id === snapshotId);
|
||||
if (!snap) return false;
|
||||
delete snap.ai_summary;
|
||||
delete snap.ai_summary_updated_at;
|
||||
_save(writeFn);
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
MAX_SNAPSHOTS,
|
||||
MAX_CMD_STDOUT,
|
||||
addSnapshot,
|
||||
listSnapshots,
|
||||
getSnapshot,
|
||||
deleteSnapshot,
|
||||
clearAll,
|
||||
summarize,
|
||||
setSnapshotAiStatus,
|
||||
updateSnapshotAiSummary,
|
||||
clearSnapshotAiSummary,
|
||||
};
|
||||
44
electron/bridges/inspection/storageKeys.cjs
Normal file
44
electron/bridges/inspection/storageKeys.cjs
Normal file
@@ -0,0 +1,44 @@
|
||||
"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_REPORT_SNAPSHOTS = `${PREFIX}report_snapshots_v1`;
|
||||
|
||||
// ---------- 巡检报告调度配置 ----------
|
||||
exports.STORAGE_KEY_REPORT_SCHEDULE = `${PREFIX}report_schedule_v1`;
|
||||
|
||||
// ---------- 日运行报告 ----------
|
||||
exports.STORAGE_KEY_DAILY_REPORTS = `${PREFIX}daily_reports_v1`;
|
||||
|
||||
exports.STORAGE_KEY_PREFIX = PREFIX;
|
||||
42
electron/bridges/inspection/store.cjs
Normal file
42
electron/bridges/inspection/store.cjs
Normal 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,
|
||||
};
|
||||
143
electron/bridges/inspection/types.cjs
Normal file
143
electron/bridges/inspection/types.cjs
Normal 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,
|
||||
};
|
||||
Reference in New Issue
Block a user