2026-10-15 13:53:04 +08:00
"use strict" ;
/ * *
* 巡检模块统一入口
*
* 提供 IPC handler 注册函数 , 供主进程注册 。
* 渲染进程通过 IPC 调用巡检相关功能 。
* /
const deviceStore = require ( "./deviceStore.cjs" ) ;
const alertRuleStore = require ( "./alertRuleStore.cjs" ) ;
const inspectionResultStore = require ( "./inspectionResultStore.cjs" ) ;
const alertRecordStore = require ( "./alertRecordStore.cjs" ) ;
const adapterRegistry = require ( "./adapterRegistry.cjs" ) ;
const InspectionScheduler = require ( "./inspectionScheduler.cjs" ) ;
const { execSshCommand , execSshCommands } = require ( "./inspectionExec.cjs" ) ;
const reportCommands = require ( "./reportCommands.cjs" ) ;
const reportStore = require ( "./reportStore.cjs" ) ;
const reportScheduleStore = require ( "./reportScheduleStore.cjs" ) ;
const dailyReportStore = require ( "./dailyReportStore.cjs" ) ;
const reportScheduler = require ( "./reportScheduler.cjs" ) ;
const reportExport = require ( "./reportExport.cjs" ) ;
const { executeBoundedSshCommand } = require ( "../boundedSshExec.cjs" ) ;
const {
ALERT _LEVEL _LABELS ,
DEVICE _TIER _LABELS ,
INSPECTION _STATUS _LABELS ,
ALERT _RECORD _STATUS _LABELS ,
OPERATOR _LABELS ,
DEVICE _CATALOG _LABELS ,
METRIC _LABELS ,
INSPECTION _STATUS ,
} = require ( "./types.cjs" ) ;
let _scheduler = null ;
let _isSnapshotRunning = false ; // 巡检执行状态( 主进程持有, renderer 可查询)
// 读报告产品名( PDF 巡检报告标题专用)
2026-10-19 21:11:57 +08:00
// 优先级: _report_product_name( 新) > _product_name( 新) > report_product_name_v1( 旧带前缀) > product_name_v1( 旧带前缀) > 默认值
2026-10-15 13:53:04 +08:00
function _readProductName ( readFn ) {
2026-10-19 21:11:57 +08:00
// 新 key( IPC save 写的)
try {
const raw = readFn ( "_report_product_name" ) ;
if ( typeof raw === "string" && raw . trim ( ) ) return raw . trim ( ) ;
} catch ( _ ) { }
try {
const raw = readFn ( "_product_name" ) ;
if ( typeof raw === "string" && raw . trim ( ) ) return raw . trim ( ) ;
} catch ( _ ) { }
// 旧带前缀 key( 可能 JSON.stringify 过)
2026-10-15 13:53:04 +08:00
try {
const raw = readFn ( "netcatty_report_product_name_v1" ) ;
2026-10-19 21:11:57 +08:00
const name = raw ? ( typeof raw === "string" ? JSON . parse ( raw ) : raw ) : null ;
2026-10-15 13:53:04 +08:00
if ( typeof name === "string" && name . trim ( ) ) return name . trim ( ) ;
} catch ( _ ) { }
try {
const raw = readFn ( "netcatty_product_name_v1" ) ;
2026-10-19 21:11:57 +08:00
const name = raw ? ( typeof raw === "string" ? JSON . parse ( raw ) : raw ) : null ;
2026-10-15 13:53:04 +08:00
if ( typeof name === "string" && name . trim ( ) ) return name . trim ( ) ;
} catch ( _ ) { }
return process . env . NETCATTY _PRODUCT _NAME || "NetMesh 智能巡检系统" ;
}
// broadcastAlertChanged 去抖: 1 秒内多次调用合并为一次广播
let _broadcastTimer = null ;
let _pendingBroadcast = false ;
function broadcastAlertChanged ( ) {
_pendingBroadcast = true ;
if ( _broadcastTimer ) return ;
_broadcastTimer = setTimeout ( ( ) => {
_broadcastTimer = null ;
if ( ! _pendingBroadcast ) return ;
_pendingBroadcast = false ;
try {
const { webContents } = require ( "electron" ) ;
for ( const wc of webContents . getAllWebContents ( ) ) {
wc . send ( "inspection:alertChanged" ) ;
}
} catch ( _ ) { }
} , 1000 ) ;
}
// 规则变化广播(立即,不去抖)
function broadcastRuleChanged ( ) {
try {
const { webContents } = require ( "electron" ) ;
for ( const wc of webContents . getAllWebContents ( ) ) {
wc . send ( "inspection:ruleChanged" ) ;
}
} catch ( _ ) { }
}
// AI 汇总状态广播(主进程主动推送 → renderer loadSnapshots 刷新)
function _broadcastReportAiStatus ( snapshotId , status , error ) {
try {
const { webContents } = require ( "electron" ) ;
for ( const wc of webContents . getAllWebContents ( ) ) {
wc . send ( "inspection:reportAiStatusChanged" , { snapshotId , status , error } ) ;
}
} catch ( _ ) { }
}
// ─── AI 汇总 helpers( 主进程 CJS 版本)──────────────────────
const _AI _SYSTEM _PROMPT = ` 你是一名资深运维工程师,负责汇总服务器/交换机/防火墙的定期巡检报告。
【 核心原则 — 必须先读这三条 】
❶ 只关注 "运行时参数异常" , 不关注 "硬件结构性事实" 。 结构性事实指 : 单电源 、 无 RAID 、 无风扇备份 、 主板型号 、 dmidecode 显示的内存条数量 、 磁盘品牌等 — — 这些是设备固有属性 , 每台设备每次巡检都一样 , 提它没有运维价值 。
❷ 跨平台命令失败不算异常 。 Linux 服务器跑交换机命令 ( show version / display vlan ) 报 command not found , 或交换机跑 Linux 命令 ( free / df / iostat ) 报未识别 , 这是巡检框架 fallback 机制的自然结果 , 不算命令失败 、 不进入 issues 、 不触发 hidden _risks 。 只有 "本 catalog 应该支持的命令" ( 如 linux _server 组的 free 、 huawei _h3c _switch 组的 display cpu ) 失败才值得关注 。
❸ hidden _risks 必须是 "参数偏离正常范围" 的信号 。 正常交换机温度 40 - 55 ° C 、 服务器 CPU 使用率 < 70 % 、 内存使用率 < 80 % 、 磁盘使用率 < 85 % 、 swap 使用 < 30 % , 这些范围内的数值不报 。 只有超阈值才提 。
❹ 内网环境 , 严禁输出任何外部 URL / 厂商官网链接 / 技术文档链接 ( 如 management . h3c . com 、 www . kylinos . cn 、 cisco . com / wiki 等 ) , 严禁引用厂商文档名称和章节号 , 严禁推荐访问外网的工具或步骤 。 设备固件版本 、 硬件型号等信息必须从巡检命令的 stdout 输出中提取 , 不得脑补 。
输出 JSON :
{
"health_summary" : "一句话整体评估( X 台在线 / Y 台离线,命令成功率 %,关键异常一句话,无异常则写" 本轮巡检全部正常 ") " ,
"offline_devices" : [ { "device" : "hostname" , "reason" : "SSH连接超时|认证失败|ping不通" } ] ,
"device_status" : [
{
"device" : "hostname" ,
"catalog" : "huawei_h3c_switch | linux_server | cisco_switch | unknown" ,
"running_ok" : true ,
"highlights" : [ "这台设备本轮关键正常参数,如 CPU 23% / 温度 48°C / 无 error 日志" ] ,
"issues" : [ "本 catalog 应有但失败的命令(排除 command not found) " ]
}
] ,
"hidden_risks" : [
{
"device" : "hostname" ,
"observation" : "具体数值 + 异常点描述(如 temperature 63°C 超阈值 60°C, 或 CPU 持续 91%) " ,
"severity" : "critical | warning | info"
}
] ,
"recommendations" : [ "按优先级排序的处置建议,必须具体到设备和操作,如:重启 host-a 的 nginx 进程、联系机房检查 host-b 散热" ]
}
【 hidden _risks 触发条件 — 只在满足以下情况时输出 】
✅ 温度超阈值 : 交换机 > 60 ° C , 服务器 CPU > 80 ° C , 磁盘 SMART 警告
✅ 资源超阈值 : CPU load > 核数 × 1.5 持续存在 、 内存使用率 > 85 % 、 磁盘使用率 > 90 % 、 inode > 90 % 、 swap 使用率 > 40 % 、 fd 使用率 > 80 %
✅ 网络异常 : 端口 CRC error / input error / output drop 非零 、 网卡 RX / TX error 包数持续增长 、 默认网关 ping 丢包
✅ NTP 漂移 : stratum 频繁切换 、 offset > 100 ms 、 last _sync 超过 30 min
✅ 日志异常 : kernel panic / oops / segfault ( critical ) 、 OOM killed process ( critical ) 、 JOURNAL _ERR error 级 ( warning ) 、 JOURNAL _WARN 周期性重复 warning ( warning ) 、 SSH 5 min 内 > 3 次失败密码 ( warning , 暴力破解嫌疑 ) 、 audit denied ( info / warning ) 、 sudo 非运维时段非常规命令 ( warning ) 、 failed systemd service ( warning )
✅ 安全异常 : 挖矿 / 反弹 shell 可疑进程 、 非标准 root 进程路径 、 异常 SUID 文件
❌ 以下情况不要输出 hidden _risks :
❌ 硬件结构性 : 无 RAID 、 无冗余电源 、 无冗余风扇 、 内存条数量 、 磁盘品牌 / 型号
❌ 平台差异 : 交换机上 free / df / iostat 报 command not found 、 Linux 上 show version / display vlan 报未识别
❌ 正常波动 : CPU 短时尖峰 ( < 30 s ) 、 内存 buffer / cache 正常回收 、 磁盘使用率 85 - 89 % ( 还没到 90 % ) 、 温度在阈值边缘抖动 ( 如 58 ° C / 阈值 60 ° C )
severity 分级 :
- critical : kernel panic / OOM / segfault 、 设备离线 、 SSH 连接超时 、 磁盘即将写满 > 95 % 、 温度 > 85 ° C
- warning : 资源超阈值 ( CPU > 85 % / MEM >90% / DISK > 90 % ) 、 端口 CRC error 、 SSH 暴力破解嫌疑 、 failed systemd service 、 周期性 error 日志
- info : 偶发 warning 日志 、 swap 偏高但还没用 、 inode > 85 % 、 fd > 70 %
严格输出合法 JSON , 不要额外解释 ` ;
function _classifyCmd ( cmdName ) {
const n = ( cmdName || "" ) . toLowerCase ( ) ;
if ( /version|device|hostname|uptime|uname|show version/ . test ( n ) ) return "base" ;
if ( /memory|cpu|temperature|fan|power|free -m|vmstat|top -bn1/ . test ( n ) ) return "resource" ;
if ( /interface|arp|vlan|stp|vrrp|ntp|route|neigh|ip -br|ip link|ip addr|display ip/ . test ( n ) ) return "network" ;
// 日志类: journalctl( 包括 -p warning/err/alert/-k/_COMM) 、logbuffer、dmesg、syslog、messages、audit、ausearch、sshd、sudo、lastb
if ( /logbuffer|journalctl|dmesg|messages|syslog|show log|audit|ausearch|sshd|sudo|lastb|failed_auth|warn|kernel_panic|oom|recent_30min/ . test ( n ) ) return "log" ;
// "log" 关键字兜底( purpose 里带 LOG 的命令也归入 log 类)
if ( /log|日志|audit|auth/ . test ( n ) ) return "log" ;
return "network" ;
}
function _truncateStdout ( cmdName , stdout ) {
if ( ! stdout ) return "" ;
const cls = _classifyCmd ( cmdName ) ;
if ( cls === "log" ) {
// 日志首尾都看:前 20 行 + 后 60 行,中间 4000 字符心跳重复日志跳过
const lines = stdout . split ( "\n" ) ;
if ( lines . length <= 80 ) return stdout ;
const head = lines . slice ( 0 , 20 ) . join ( "\n" ) ;
const tail = lines . slice ( - 60 ) . join ( "\n" ) ;
return ` ${ head } \n ...[中间 ${ lines . length - 80 } 行已跳过]... \n ${ tail } ` ;
}
if ( cls === "network" ) return stdout . slice ( 0 , 800 ) ;
return stdout . slice ( 0 , 2000 ) ;
}
function _buildSnapshotPrompt ( snapshot ) {
const offlineDevices = [ ] ;
const onlineDevices = [ ] ;
for ( const d of snapshot . devices || [ ] ) {
const cmds = d . commands || [ ] ;
const allFail = cmds . every ( c => ! c . ok ) ;
const isConnectionErr = cmds . every ( c =>
/Connection refused|ETIMEDOUT|ECONNREFUSED|connect failed|authentication failed|SSH.*failed/i . test ( ( c . stderr || c . stdout || "" ) )
) ;
const isOffline = ! d . ok || ( allFail && isConnectionErr ) ;
if ( isOffline ) {
offlineDevices . push ( {
device : d . hostname ,
2026-10-19 21:11:57 +08:00
reason : ( cmds [ 0 ] ? . stderr || cmds [ 0 ] ? . stdout || "SSH连接失败" ) . slice ( 0 , 150 ) ,
2026-10-15 13:53:04 +08:00
} ) ;
} else {
2026-10-19 21:11:57 +08:00
// 大幅压缩:只保留失败命令 + 最多 5 条关键资源命令,输出截断到 200 字符
2026-10-15 13:53:04 +08:00
const detailCmds = [ ] ;
let hasFail = false ;
2026-10-19 21:11:57 +08:00
let resourceCount = 0 ;
const MAX _RESOURCE = 5 ;
2026-10-15 13:53:04 +08:00
for ( const c of cmds ) {
if ( ! c . ok ) {
hasFail = true ;
detailCmds . push ( {
2026-10-19 21:11:57 +08:00
cmd : c . cmd , status : "fail" ,
output : ( c . stdout || c . stderr || "" ) . slice ( 0 , 200 ) ,
2026-10-15 13:53:04 +08:00
} ) ;
continue ;
}
const cls = _classifyCmd ( c . cmd ) ;
2026-10-19 21:11:57 +08:00
// 只保留 resource 类型( cpu/mem/磁盘/端口等关键指标),跳过 log 类型
if ( cls === "resource" && resourceCount < MAX _RESOURCE ) {
resourceCount ++ ;
detailCmds . push ( { cmd : c . cmd , status : "ok" , output : ( c . stdout || "" ) . slice ( 0 , 200 ) } ) ;
2026-10-15 13:53:04 +08:00
}
}
onlineDevices . push ( {
device : d . hostname ,
catalog : d . catalog _id ,
status : hasFail ? "partial_fail" : "ok" ,
command _total : cmds . length ,
command _failed : cmds . filter ( c => ! c . ok ) . length ,
commands : detailCmds ,
} ) ;
}
}
const totalCmds = onlineDevices . reduce ( ( a , d ) => a + d . command _total , 0 ) ;
const failCmds = onlineDevices . reduce ( ( a , d ) => a + d . command _failed , 0 ) ;
2026-10-19 21:11:57 +08:00
// 压缩 JSON: 用 null 替代缩进,减少 30-40% 体积
2026-10-15 13:53:04 +08:00
return JSON . stringify ( {
2026-10-19 21:11:57 +08:00
offline : offlineDevices ,
online : onlineDevices ,
summary : {
ts : new Date ( snapshot . timestamp ) . toISOString ( ) ,
trigger : snapshot . trigger ,
dur _ms : snapshot . duration _ms ,
offline _n : offlineDevices . length ,
online _n : onlineDevices . length ,
total _cmd : totalCmds ,
fail _cmd : failCmds ,
} ,
} ) ;
2026-10-15 13:53:04 +08:00
}
function _resolveAiStyle ( provider ) {
const lower = ( provider . baseURL || provider . id || "" ) . toLowerCase ( ) ;
if ( /anthropic|claude/ . test ( lower ) ) return "anthropic" ;
if ( /google|gemini/ . test ( lower ) ) return "google" ;
return "openai" ;
}
function _buildAiUrl ( provider ) {
const base = ( provider . baseURL || "" ) . replace ( /\/$/ , "" ) ;
const style = _resolveAiStyle ( provider ) ;
const suffix = style === "anthropic" ? "/v1/messages"
: style === "google" ? ` /v1beta/models/ ${ provider . defaultModel || "gemini-pro" } :generateContent `
: "/chat/completions" ;
return base + suffix ;
}
function _buildAiBody ( provider , userContent ) {
const style = _resolveAiStyle ( provider ) ;
const systemPrompt = _AI _SYSTEM _PROMPT ;
const base = { model : provider . defaultModel || "gpt-4o-mini" , temperature : 0.3 } ;
if ( style === "google" ) {
return { ... base , contents : [ { role : "user" , parts : [ { text : ` ${ systemPrompt } \n \n ${ userContent } ` } ] } ] } ;
}
if ( style === "anthropic" ) {
return { ... base , system : systemPrompt , messages : [ { role : "user" , content : userContent } ] , max _tokens : 2048 } ;
}
return { ... base , messages : [ { role : "system" , content : systemPrompt } , { role : "user" , content : userContent } ] } ;
}
function _parseAiResponse ( provider , rawData ) {
let data ;
try { data = typeof rawData === "string" ? JSON . parse ( rawData ) : rawData ; } catch { return "" ; }
const style = _resolveAiStyle ( provider ) ;
if ( style === "google" ) return data ? . candidates ? . [ 0 ] ? . content ? . parts ? . [ 0 ] ? . text || "" ;
if ( style === "anthropic" ) return data ? . content ? . [ 0 ] ? . text || "" ;
return data ? . choices ? . [ 0 ] ? . message ? . content || "" ;
}
function _extractJson ( text ) {
if ( ! text ) return null ;
let t = text . trim ( ) . replace ( /^```json\s*/i , "" ) . replace ( /\s*```\s*$/ , "" ) ;
const m = t . match ( /\{[\s\S]*\}/ ) ;
if ( ! m ) return null ;
try { return JSON . parse ( m [ 0 ] ) ; } catch { return null ; }
}
/ * *
* Node . js http / https fetch ( 类似 aiBridge 的 doFetch , 但不需要 redirect 处理 )
* /
function _doHttpFetch ( urlStr , headers , body , opts ) {
return new Promise ( ( resolve ) => {
const http = require ( "http" ) ;
const https = require ( "https" ) ;
try {
const parsed = new URL ( urlStr ) ;
const lib = parsed . protocol === "https:" ? https : http ;
const timeoutMs = opts ? . timeoutMs || 60000 ;
const skipTLS = ! ! opts ? . skipTLS ;
const req = lib . request ( parsed , {
method : "POST" ,
headers : { ... headers , "Content-Length" : Buffer . byteLength ( body || "" ) } ,
timeout : timeoutMs ,
rejectUnauthorized : skipTLS ? false : undefined ,
} , ( res ) => {
let data = "" ;
res . on ( "data" , ( chunk ) => data += chunk . toString ( ) ) ;
res . on ( "end" , ( ) => {
resolve ( {
ok : res . statusCode >= 200 && res . statusCode < 300 ,
status : res . statusCode ,
data ,
error : res . statusCode >= 300 ? ` HTTP ${ res . statusCode } ` : null ,
} ) ;
} ) ;
} ) ;
req . on ( "error" , ( err ) => resolve ( { ok : false , status : 0 , data : "" , error : err . message } ) ) ;
req . on ( "timeout" , ( ) => { req . destroy ( ) ; resolve ( { ok : false , status : 0 , data : "" , error : "Request timeout" } ) ; } ) ;
if ( body ) req . write ( body ) ;
req . end ( ) ;
} catch ( err ) {
resolve ( { ok : false , status : 0 , data : "" , error : err . message } ) ;
}
} ) ;
}
/ * *
* 获取标签映射 ( 前端展示用 )
* /
function getLabels ( ) {
return {
alert _levels : ALERT _LEVEL _LABELS ,
device _tiers : DEVICE _TIER _LABELS ,
inspection _status : INSPECTION _STATUS _LABELS ,
alert _record _status : ALERT _RECORD _STATUS _LABELS ,
operators : OPERATOR _LABELS ,
device _catalog : DEVICE _CATALOG _LABELS ,
metrics : METRIC _LABELS ,
} ;
}
/ * *
* 注册巡检模块的 IPC handlers
*
* @ param { object } ipcMain - Electron ipcMain
* @ param { object } options
* @ param { function } options . readStorage - 读取本地存储 ( key ) => string
* @ param { function } options . writeStorage - 写入本地存储 ( key , value ) => void
* /
function registerInspectionHandlers ( ipcMain , options ) {
const { readStorage , writeStorage , sessions } = options ;
const readFn = ( key ) => readStorage ( key ) ;
const writeFn = ( key , val ) => writeStorage ( key , val ) ;
// 根据主机地址查找会话(用于复用终端 SSH 连接)
function findSessionByHost ( hostname ) {
const DEBUG = process . env . INSPECTION _DEBUG === "1" ;
if ( DEBUG ) console . log ( "[Inspection] 查找会话,总会话数:" , sessions ? . size || 0 ) ;
if ( ! sessions ) {
if ( DEBUG ) console . log ( "[Inspection] sessions 未初始化" ) ;
return null ;
}
for ( const session of sessions . values ( ) ) {
if ( ! session ) continue ;
// SSH 会话才有 conn
if ( ! session . conn ) continue ;
// 匹配主机地址
const sessionHost = session . hostname || session . host || "" ;
if ( DEBUG ) {
console . log ( "[Inspection] SSH会话:" , sessionHost , "| 目标:" , hostname ) ;
}
if ( sessionHost === hostname ) {
if ( DEBUG ) console . log ( "[Inspection] 匹配到会话:" , sessionHost ) ;
return session ;
}
}
return null ;
}
// ---------- 标签映射 ----------
ipcMain . handle ( "inspection:getLabels" , ( ) => {
return getLabels ( ) ;
} ) ;
// ---------- 系统配置 ----------
ipcMain . handle ( "inspection:getSysConfig" , ( ) => {
return deviceStore . getSysConfig ( readFn ) ;
} ) ;
ipcMain . handle ( "inspection:updateSysConfig" , ( _e , updates ) => {
return deviceStore . updateSysConfig ( readFn , writeFn , updates ) ;
} ) ;
// ---------- 设备类型 ----------
ipcMain . handle ( "inspection:getCatalog" , ( ) => {
return deviceStore . getCatalog ( ) ;
} ) ;
ipcMain . handle ( "inspection:getTiers" , ( ) => {
return deviceStore . getTiers ( ) ;
} ) ;
ipcMain . handle ( "inspection:getAdapters" , ( ) => {
return adapterRegistry . getRegisteredAdapters ( ) ;
} ) ;
// ---------- 设备扩展配置 ----------
ipcMain . handle ( "inspection:getAllExtensions" , ( ) => {
return deviceStore . getAllExtensions ( readFn ) ;
} ) ;
ipcMain . handle ( "inspection:getExtension" , ( _e , hostId ) => {
return deviceStore . getExtension ( readFn , hostId ) ;
} ) ;
ipcMain . handle ( "inspection:upsertExtension" , ( _e , hostId , data ) => {
return deviceStore . upsertExtension ( readFn , writeFn , hostId , data ) ;
} ) ;
2026-10-19 21:11:57 +08:00
// 批量设置所有设备的 inspection_enabled
ipcMain . handle ( "inspection:batchSetInspectionEnabled" , ( _e , enabled ) => {
try {
const all = deviceStore . getAllExtensions ( readFn ) ;
let count = 0 ;
for ( const ext of Object . values ( all ) ) {
deviceStore . upsertExtension ( readFn , writeFn , ext . host _id , {
inspection _enabled : ! ! enabled ,
} ) ;
count ++ ;
}
console . log ( ` [Inspection] 批量设置巡检状态: ${ enabled ? "启用" : "停止" } ${ count } 台设备 ` ) ;
return { ok : true , count , enabled : ! ! enabled } ;
} catch ( e ) {
console . error ( "[Inspection] 批量设置巡检状态失败:" , e ) ;
return { ok : false , error : e . message } ;
}
} ) ;
2026-10-15 13:53:04 +08:00
ipcMain . handle ( "inspection:deleteExtension" , ( _e , hostId ) => {
return deviceStore . deleteExtension ( readFn , writeFn , hostId ) ;
} ) ;
ipcMain . handle ( "inspection:getEffectiveInterval" , ( _e , hostId ) => {
return deviceStore . getEffectiveInterval ( readFn , hostId ) ;
} ) ;
ipcMain . handle ( "inspection:getEnabledDevices" , ( ) => {
return deviceStore . getEnabledDevices ( readFn ) ;
} ) ;
// ---------- 告警规则 ----------
ipcMain . handle ( "inspection:listRules" , ( ) => {
return alertRuleStore . listRules ( readFn ) ;
} ) ;
ipcMain . handle ( "inspection:getRule" , ( _e , ruleId ) => {
return alertRuleStore . getRule ( readFn , ruleId ) ;
} ) ;
ipcMain . handle ( "inspection:createRule" , ( _e , data ) => {
const result = alertRuleStore . createRule ( readFn , writeFn , data ) ;
broadcastRuleChanged ( ) ;
return result ;
} ) ;
ipcMain . handle ( "inspection:updateRule" , ( _e , ruleId , updates ) => {
const result = alertRuleStore . updateRule ( readFn , writeFn , ruleId , updates ) ;
broadcastRuleChanged ( ) ;
return result ;
} ) ;
ipcMain . handle ( "inspection:deleteRule" , ( _e , ruleId ) => {
const result = alertRuleStore . deleteRule ( readFn , writeFn , ruleId ) ;
broadcastRuleChanged ( ) ;
return result ;
} ) ;
ipcMain . handle ( "inspection:toggleRule" , ( _e , ruleId , enabled ) => {
const result = alertRuleStore . toggleRule ( readFn , writeFn , ruleId , enabled ) ;
broadcastRuleChanged ( ) ;
return result ;
} ) ;
// ---------- 巡检结果 ----------
ipcMain . handle ( "inspection:getLatestAll" , ( ) => {
return inspectionResultStore . getLatestAll ( readFn ) ;
} ) ;
ipcMain . handle ( "inspection:getLatestByDevice" , ( _e , hostId , limit ) => {
return inspectionResultStore . getLatestByDevice ( readFn , hostId , limit ) ;
} ) ;
ipcMain . handle ( "inspection:getRecentResults" , ( _e , limit ) => {
return inspectionResultStore . getRecent ( readFn , limit ) ;
} ) ;
// ---------- 单次巡检执行(前端传入完整主机配置) ----------
ipcMain . handle ( "inspection:runInspection" , async ( _e , payload ) => {
const { hostId , hostConfig , catalogId } = payload || { } ;
if ( ! hostId || ! hostConfig ) {
return { success : false , error : "缺少主机配置" } ;
}
const DEBUG = process . env . INSPECTION _DEBUG === "1" ;
if ( DEBUG ) {
console . log ( "[Inspection] 巡检配置:" , JSON . stringify ( {
host : hostConfig . hostname || hostConfig . address ,
port : hostConfig . port || 22 ,
username : hostConfig . username ,
hasPassword : ! ! hostConfig . password ,
hasPrivateKey : ! ! hostConfig . privateKeyContent ,
hasKeyId : ! ! hostConfig . identityFileId ,
} ) ) ;
}
const sshConfig = {
host : hostConfig . hostname || hostConfig . address ,
port : hostConfig . port || 22 ,
username : hostConfig . username ,
password : hostConfig . password ,
privateKey : hostConfig . privateKeyContent ,
passphrase : hostConfig . passphrase ,
} ;
if ( ! sshConfig . host || ! sshConfig . username ) {
return { success : false , error : "主机地址或用户名缺失" } ;
}
const startTime = Date . now ( ) ;
const result = {
id : ` ins_ ${ Date . now ( ) } _ ${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` ,
host _id : hostId ,
catalog _id : catalogId || "linux_server" ,
start _time : startTime ,
end _time : 0 ,
status : INSPECTION _STATUS . SUCCESS ,
metrics : { } ,
raw : { } ,
error : null ,
alerts : [ ] ,
} ;
try {
// 构造 execFn — 优先复用终端会话的 SSH 连接
const execFn = async ( command , options = { } ) => {
const timeoutMs = options . timeoutMs || 60000 ;
const DEBUG = process . env . INSPECTION _DEBUG === "1" ;
// 1. 先尝试复用终端会话的连接
const session = findSessionByHost ( sshConfig . host ) ;
if ( DEBUG ) {
console . log ( "[Inspection] 查找会话:" , sshConfig . host ,
"| 找到会话:" , session ? "是" : "否" ,
"| 有连接:" , session ? . conn ? "是" : "否" ) ;
}
if ( session && session . conn ) {
try {
const result = await executeBoundedSshCommand ( session . conn , command , {
openingTimeoutMs : timeoutMs ,
runTimeoutMs : timeoutMs ,
maxOutputBytes : 100 * 1024 ,
} ) ;
if ( DEBUG ) console . log ( "[Inspection] 复用会话成功, stdout:" , result . stdout ? . slice ( 0 , 100 ) ) ;
return result . stdout + ( result . stderr ? "\n" + result . stderr : "" ) ;
} catch ( e ) {
// 终端连接失败,回退到新建连接
if ( DEBUG ) console . log ( "[Inspection] 复用会话失败,回退:" , e . message ) ;
}
}
// 2. 回退:新建 SSH 连接( connectTimeoutMs 提高到 20s — 公网服务器握手需要更长时间)
return await execSshCommand ( sshConfig , command , {
connectTimeoutMs : 20000 ,
execTimeoutMs : timeoutMs ,
} ) ;
} ;
// 构造批量执行函数(一次连接执行多条命令)
const execCommandsFn = async ( commands , options = { } ) => {
const { execSshCommands } = require ( "./inspectionExec.cjs" ) ;
return await execSshCommands ( sshConfig , commands , options ) ;
} ;
// 获取设备类型对应的采集器类名
const catalogs = deviceStore . getCatalog ( ) ;
const catalog = catalogs . find ( ( c ) => c . id === catalogId ) ;
const adapterClassName = catalog ? . adapter _class || "LinuxServerAdapter" ;
// 创建采集器实例(注入 execFn 和 execCommandsFn)
const adapter = adapterRegistry . createAdapter ( adapterClassName , sshConfig , {
execFn ,
execCommands : execCommandsFn ,
timeoutMs : 20000 ,
} ) ;
if ( ! adapter ) {
throw new Error ( "采集器加载失败: " + adapterClassName ) ;
}
const collectResult = await adapter . collect ( ) ;
result . metrics = collectResult . metrics || { } ;
result . raw = collectResult . raw || { } ;
if ( ! collectResult . online ) {
result . status = collectResult . status || INSPECTION _STATUS . FAILED ;
result . error = collectResult . error || "设备离线或连接失败" ;
}
} catch ( err ) {
result . status = INSPECTION _STATUS . FAILED ;
result . error = err . message ;
console . error ( "[Inspection] runInspection error:" , err . message ) ;
}
result . end _time = Date . now ( ) ;
const duration = result . end _time - result . start _time ;
// 构造设备信息(用于告警记录)
const deviceInfo = {
device _id : hostId ,
device _name : hostConfig . label || hostConfig . hostname || hostId ,
catalog _id : catalogId || "linux_server" ,
} ;
// 保存巡检结果
try {
inspectionResultStore . addResult ( readFn , writeFn , {
... result ,
device _id : hostId ,
online : result . status === INSPECTION _STATUS . SUCCESS ,
duration _ms : duration ,
error _message : result . error ,
} ) ;
} catch ( e ) {
console . warn ( "[Inspection] 保存巡检结果失败:" , e . message ) ;
}
// 更新设备 last_scan_at —— 关键! renderer 侧 runDeviceInspection 不更新这个字段,
// 主进程 InspectionScheduler 读这个字段判断过期,如果这里不更新会导致重复巡检
try {
deviceStore . updateLastScan (
readFn ,
writeFn ,
hostId ,
result . status ,
result . error || null
) ;
} catch ( e ) {
console . warn ( "[Inspection] updateLastScan 失败:" , e . message ) ;
}
// 告警判断(成功/失败都要判断,特别是 online 指标)
const triggeredAlerts = [ ] ;
try {
const deviceExt = deviceStore . getExtension ( readFn , hostId ) ;
const rules = alertRuleStore . getRulesForDevice ( readFn , deviceExt || { host _id : hostId , catalog _id : catalogId } ) ;
const isOnline = result . status === INSPECTION _STATUS . SUCCESS ;
for ( const rule of rules ) {
// online 指标 — 成功/失败都要判断(设备离线告警)
if ( rule . metric === "online" ) {
const shouldAlert = rule . threshold === "false" ? ! isOnline : isOnline ;
const alertResult = alertRecordStore . processAlert (
readFn ,
writeFn ,
deviceInfo ,
rule ,
isOnline ? "在线" : "离线" ,
shouldAlert
) ;
if ( alertResult ) triggeredAlerts . push ( alertResult ) ;
continue ;
}
// 普通指标 — 只在成功时判断(失败时没有指标数据)
if ( isOnline ) {
const metricValue = result . metrics [ rule . metric ] ;
if ( metricValue === undefined || metricValue === null ) continue ;
const alertResult = alertRecordStore . processAlert (
readFn ,
writeFn ,
deviceInfo ,
rule ,
metricValue ,
alertRuleStore . evaluateRule ( metricValue , rule . operator , rule . threshold )
) ;
if ( alertResult ) triggeredAlerts . push ( alertResult ) ;
}
}
} catch ( e ) {
console . warn ( "[Inspection] 告警判断失败:" , e . message ) ;
}
// 执行错误告警 —— timeout/failed 也要进告警中心
// 放在 processAlert 之后执行:如果 processAlert 已经创建了 rule_offline,
// createExecutionErrorAlert 会用更有意义的错误信息( SSH连接失败/超时等)覆盖 message
if ( result . status !== INSPECTION _STATUS . SUCCESS ) {
alertRecordStore . createExecutionErrorAlert (
readFn ,
writeFn ,
deviceInfo ,
result . status ,
result . error || "巡检执行失败"
) ;
broadcastAlertChanged ( ) ;
}
// 告警变化后广播(触发/更新/恢复都通知)
if ( triggeredAlerts . length > 0 ) {
broadcastAlertChanged ( ) ;
}
return {
success : result . status === INSPECTION _STATUS . SUCCESS ,
status : result . status ,
metrics : result . metrics ,
error : result . error ,
duration ,
alerts : triggeredAlerts ,
} ;
} ) ;
// ---------- AI RCA 单条命令执行(接收完整 hostConfig, 独立 SSH) ----------
// 供 RCA agent 用: rcaRunner 在 renderer 侧解密 vault hosts+keys 构建 hostConfig,
// 传入这里让主进程用 execSshCommand 独立建 SSH 连设备执行一条命令。
ipcMain . handle ( "inspection:runSingleCommand" , async ( _e , payload ) => {
const { hostConfig , command , timeoutMs = 15000 } = payload || { } ;
console . log ( "[Inspection:runSingleCommand] payload:" , JSON . stringify ( {
hostname : hostConfig ? . hostname ,
port : hostConfig ? . port ,
username : hostConfig ? . username ,
hasPassword : ! ! hostConfig ? . password ,
hasPrivateKey : ! ! hostConfig ? . privateKeyContent ,
command ,
timeoutMs ,
} ) ) ;
if ( ! hostConfig ? . hostname || ! hostConfig ? . username || ! command ) {
return { ok : false , error : "缺少 hostConfig 或 command" } ;
}
const sshConfig = {
host : hostConfig . hostname ,
port : hostConfig . port || 22 ,
username : hostConfig . username ,
password : hostConfig . password ,
privateKey : hostConfig . privateKeyContent ,
passphrase : hostConfig . passphrase ,
} ;
const start = Date . now ( ) ;
try {
console . log ( "[Inspection:runSingleCommand] 正在 SSH 连接" , sshConfig . host , "..." ) ;
// 自动拆分分号命令 —— bash/sh 支持 "cmd1; cmd2" 一条 exec 里跑多条,
// 但华为 CLI / Cisco IOS / 防火墙 VTY 不认分号拼接,需要拆成多条顺序执行。
let commands = command
. split ( /;\s*(?=(?:[^"']*"[^"']*")*[^"']*$)/ ) // 不拆引号内的分号
. map ( c => c . trim ( ) )
. filter ( Boolean ) ;
// 关闭分页命令兜底替换: AI 有时会给出错误的命令,自动修正
// terminal length 0 → screen-length 0 temporary( 华为/H3C 用户视图)
// terminal length 0 line → terminal length 0 line (华为 system-view)
// terminal length 100 → screen-length 100 temporary
commands = commands . map ( c => {
// 华为 CLI 常见的分页关闭命令替换(用户视图下有效)
if ( /^terminal\s+length\s+0(\s+line)?$/i . test ( c ) ) return "screen-length 0 temporary" ;
if ( /^terminal\s+length\s+\d+$/i . test ( c ) ) return c . replace ( /^terminal\s+length\s+(\d+)/ , "screen-length $1 temporary" ) ;
// Cisco IOS
if ( /^terminal\s+length\s+0$/i . test ( c ) ) return "terminal length 0" ; // IOS 这条是对的,不替换
return c ;
} ) ;
console . log ( "[Inspection:runSingleCommand] 拆分后命令数:" , commands . length , commands ) ;
let allStdout = "" ;
let allOk = true ;
// connectTimeout 也尊重短 timeoutMs( autoDetectCatalog 传 8000 时,连接超时也用 8s)
const connectTimeoutMs = Math . min ( 20000 , Math . max ( 3000 , timeoutMs ) ) ;
if ( commands . length === 1 ) {
// 单条命令,直接 execSshCommand
allStdout = await execSshCommand ( sshConfig , commands [ 0 ] , {
connectTimeoutMs ,
execTimeoutMs : timeoutMs ,
} ) ;
} else {
// 多条命令,顺序执行,拼接 stdout
const result = await execSshCommands ( sshConfig , commands , {
connectTimeoutMs ,
execTimeoutMs : Math . floor ( timeoutMs / Math . max ( commands . length , 1 ) ) ,
} ) ;
allStdout = ( result ? . results || [ ] ) . map ( r => r ? . output || "" ) . join ( "\n" ) ;
// 只要有一条命令输出里含错误关键字就算 ok=false
const ERROR _PATTERNS = /unrecognized command|command found at|Unknown command|invalid input|Error:|failed/i ;
if ( ( result ? . results || [ ] ) . some ( r => ERROR _PATTERNS . test ( r ? . output || "" ) ) ) {
allOk = false ;
}
}
// ANSI 控制序列 / 光标移动 / More 提示 — 统一过滤
// ( execSshCommand 单条路径没有 cleanOutput, handler 兜底)
function stripControl ( text ) {
return ( text || "" )
. replace ( /\x1B\[[0-9;]*[A-Za-z]/g , "" ) // CSI 序列
. replace ( /\x1B\][^\x07\x1B]*(\x07|\x1B\\)/g , "" ) // OSC 序列
. replace ( /\x1B[PX][^\x1B]*\x1B\\/g , "" ) // PM/APC 序列
. replace ( /\x1B[@-_][0-9;]*[ -\/]*[@-~]/g , "" ) // 其他 ESC 序列
. replace ( /[\x00-\x08\x0B\x0C\x0E-\x1F]/g , "" ) // 剩余单控制字符
. replace ( /\r/g , "" )
. trim ( ) ;
}
allStdout = stripControl ( allStdout ) ;
console . log ( "[Inspection:runSingleCommand] 成功, stdout 长度:" , allStdout ? . length || 0 ) ;
return {
ok : allOk ,
stdout : typeof allStdout === "string" ? allStdout : String ( allStdout ? ? "" ) ,
stderr : "" ,
exitCode : 0 ,
durationMs : Date . now ( ) - start ,
} ;
} catch ( e ) {
console . error ( "[Inspection:runSingleCommand] 失败:" , e ? . message || String ( e ) ) ;
return {
ok : false ,
stdout : "" ,
stderr : e ? . message || String ( e ) ,
exitCode : - 1 ,
durationMs : Date . now ( ) - start ,
error : e ? . message || String ( e ) ,
} ;
}
} ) ;
2026-10-19 21:11:57 +08:00
// ---------- AI RCA 完整实现(主进程版) ----------
// 用 _hostConfigs 直接拿 SSH 凭据,不需要 renderer 解密
// 支持 IPC handler( Electron 本地)和 remoteHttpServer 直接调用
const bridge = require ( "../aiBridge.cjs" ) ;
// 从 AI 响应中提取 JSON( 剥离 <think>...</think>、```json 包裹、找第一个配对的 {...})
function _extractJson ( text ) {
let cleaned = ( text || "" )
. replace ( /<think>[\s\S]*?<\/think>/gi , "" ) // 剥离思维链
. replace ( /```json\s*/gi , "" )
. replace ( /```\s*/g , "" )
. trim ( ) ;
// 直接尝试 parse
try { return JSON . parse ( cleaned ) ; } catch { }
// 找第一个配对的 {...}
const openIdx = cleaned . indexOf ( "{" ) ;
if ( openIdx >= 0 ) {
let depth = 0 , closeIdx = - 1 ;
for ( let i = openIdx ; i < cleaned . length ; i ++ ) {
if ( cleaned [ i ] === "{" ) depth ++ ;
else if ( cleaned [ i ] === "}" ) { depth -- ; if ( depth === 0 ) { closeIdx = i ; break ; } }
}
if ( closeIdx > openIdx ) {
const jsonStr = cleaned . slice ( openIdx , closeIdx + 1 ) ;
try { return JSON . parse ( jsonStr ) ; } catch { }
}
}
throw new Error ( "无法从 AI 响应中提取 JSON, 原始响应前 200 字: " + cleaned . slice ( 0 , 200 ) ) ;
}
const PHASE1 _SYSTEM = [
` 你是 NetMesh 运维平台的 AI 诊断规划专家。 ` ,
` ## 设备 Catalog 与命令族 ` ,
` | catalogId | 设备类型 | Shell | 推荐诊断命令 | ` ,
` |---|---|---|---| ` ,
` | linux | Linux 服务器 | bash/sh | top -bn1 | head -20, vmstat 1 2, free -m, cat /proc/meminfo | head -20, df -h, ps aux --sort=-%cpu | head -10, iostat -x 1 1 | ` ,
` | huawei_h3c_switch | 华为/H3C 交换机 | HUAWEI CLI | screen-length 0 temporary, display version, display device, display memory, display power, display fan, display logbuffer level warning size 512 | ` ,
` | cisco_switch | Cisco 交换机 | IOS CLI | terminal length 0, show version, show processes cpu sorted, show memory summary, show interfaces brief, show log tail 50 | ` ,
` ## 输出格式(严格 JSON) ` ,
` { "commands": [{ "command": "xxx", "purpose": "xxx" }], "reasoning": "一句话说明" } ` ,
` ## 规则 ` ,
` - 命令必须严格匹配 catalogId, 不得臆造 ` ,
` - 禁止 display cpu-usage( 有刷新动画抓不到完整输出) ` ,
` - 华为/H3C 必须先加 screen-length 0 temporary 关闭分页 ` ,
` - 不超过 6 条命令 ` ,
` - 禁止输出外网 URL / 厂商文档链接 ` ,
] . join ( "\n" ) ;
const PHASE2 _SYSTEM = [
` 你是 NetMesh 智慧运维平台的 AI 根因分析专家。 ` ,
` 你收到了告警 + 设备巡检 metrics + 多条诊断命令执行输出。 ` ,
` ## 输出格式(严格 JSON) ` ,
` { ` ,
` "confidence": 85, ` ,
` "rootCauses": ["最可能根因", "次要可能性"], ` ,
` "summary": "2-3 句摘要", ` ,
` "recommendations": [ ` ,
` { "priority": "immediate|short_term|long_term", "action": "要做什么", "command": "可选命令", "risk": "low|medium|high" } ` ,
` ] ` ,
` } ` ,
` ## 规则 ` ,
` - confidence 基于证据充分性 ` ,
` - recommendations 至少 2 条最多 5 条,按紧急程度排序 ` ,
` - 严禁输出外网 URL / 厂商文档 / 外部工具推荐 ` ,
] . join ( "\n" ) ;
async function _callAiForRCA ( providerConfigsOrNull , systemPrompt , userContent , maxTokens = 2000 ) {
if ( ! providerConfigsOrNull || ! providerConfigsOrNull . length ) {
throw new Error ( "AI provider 未配置" ) ;
}
const provider = providerConfigsOrNull . find ( p => p . enabled !== false ) ;
if ( ! provider ) throw new Error ( "没有启用的 AI provider" ) ;
const resolved = bridge . resolveProviderApiKey ( provider . id ) ;
if ( ! resolved ? . apiKey ) throw new Error ( "AI provider API key 未解密" ) ;
const url = provider . endpoint || "https://api.minimaxi.com/v1/chat/completions" ;
const model = provider . model || "MiniMax-M1" ;
const patched = bridge . injectApiKeyIntoRequest ( url , { } , provider . id ) ;
const http = require ( "http" ) , https = require ( "https" ) ;
const lib = patched . url . startsWith ( "https" ) ? https : http ;
const u = new URL ( patched . url ) ;
const bodyStr = JSON . stringify ( {
model ,
messages : [
{ role : "system" , content : systemPrompt } ,
{ role : "user" , content : userContent } ,
] ,
temperature : 0.3 ,
max _tokens : maxTokens ,
stream : false ,
} ) ;
return await new Promise ( ( resolve , reject ) => {
const req = lib . request ( {
hostname : u . hostname ,
port : u . port || ( u . protocol === "https:" ? 443 : 80 ) ,
path : u . pathname + u . search ,
method : "POST" ,
headers : {
"Content-Type" : "application/json" ,
"Authorization" : ` Bearer ${ resolved . apiKey } ` ,
} ,
timeout : 90000 ,
} , res => {
let chunks = [ ] ;
res . on ( "data" , c => chunks . push ( c ) ) ;
res . on ( "end" , ( ) => {
try {
const text = Buffer . concat ( chunks ) . toString ( ) ;
const parsed = JSON . parse ( text ) ;
const content = parsed . choices ? . [ 0 ] ? . message ? . content
|| parsed . content ? . [ 0 ] ? . text
|| parsed . candidates ? . [ 0 ] ? . content ? . parts ? . [ 0 ] ? . text
|| "" ;
resolve ( content . trim ( ) ) ;
} catch ( e ) {
reject ( new Error ( "AI 响应解析失败: " + e . message ) ) ;
}
} ) ;
} ) ;
req . on ( "error" , reject ) ;
req . on ( "timeout" , ( ) => { req . destroy ( ) ; reject ( new Error ( "AI 请求超时" ) ) ; } ) ;
req . write ( bodyStr ) ;
req . end ( ) ;
} ) ;
}
async function runAlertRCA ( alertId , providerConfigsOverride ) {
const start = Date . now ( ) ;
const results = { toolInvocations : [ ] } ;
function resetCaches ( ) {
try { deviceStore . _resetForTest ? . ( ) ; } catch { }
try { alertRecordStore . _resetForTest ? . ( ) ; } catch { }
try { inspectionResultStore . _resetForTest ? . ( ) ; } catch { }
try { alertRuleStore . _resetForTest ? . ( ) ; } catch { }
}
// 1. 找告警
resetCaches ( ) ;
const alerts = alertRecordStore . listAlerts ( readFn , { limit : 500 } ) ;
const alert = alerts . find ( a => a . id === alertId ) ;
if ( ! alert ) return { success : false , error : "告警不存在" , durationMs : 0 , toolInvocations : [ ] } ;
// 2. 找 hostConfig
let hostConfig = null ;
if ( _hostConfigs && _hostConfigs [ alert . device _id ] ) {
const hc = _hostConfigs [ alert . device _id ] ;
hostConfig = {
hostname : hc . host || hc . hostname ,
port : hc . port || 22 ,
username : hc . username ,
password : hc . password ,
privateKeyContent : hc . privateKey || hc . privateKeyContent ,
passphrase : hc . passphrase ,
} ;
}
// 3. 找 catalog_id + 巡检 metrics
let catalogId = "" ;
const extMap = deviceStore . getAllExtensions ( readFn ) ;
if ( extMap [ alert . device _id ] ? . catalog _id ) catalogId = extMap [ alert . device _id ] . catalog _id ;
let metrics = { } ;
const latest = inspectionResultStore . getLatestAll ( readFn ) ;
const result = latest . find ( r => r ? . device _id === alert . device _id ) ;
if ( result ) metrics = result . metrics || { } ;
const alertInfo = {
device _name : alert . device _name ,
device _id : alert . device _id ,
level : alert . level ,
metric : alert . metric ,
metric _value : alert . metric _value ,
threshold : alert . threshold ,
rule _name : alert . rule _name ,
triggered _at : alert . triggered _at ,
message : alert . message ,
catalog _id : catalogId || "unknown" ,
online : ! ! result ? . online ,
} ;
const providerConfigs = providerConfigsOverride || bridge . providerConfigs ;
// 4. Phase 1 — AI 规划诊断命令
let phase1Plan = { commands : [ ] } ;
try {
const phase1Input = JSON . stringify ( {
alert : alertInfo ,
metrics ,
catalog _id : catalogId ,
device _context : ` 设备名: ${ alert . device _name } , 设备ID: ${ alert . device _id } ` ,
} ) ;
const phase1Text = await _callAiForRCA ( providerConfigs , PHASE1 _SYSTEM , phase1Input , 1000 ) ;
const js = _extractJson ( phase1Text ) ;
phase1Plan = js ;
console . log ( "[RCA] Phase 1 规划:" , phase1Plan . commands ? . length , "条命令" ) ;
} catch ( e ) {
console . warn ( "[RCA] Phase 1 失败,跳过 SSH 诊断:" , e . message ) ;
results . toolInvocations . push ( { tool : "phase1_planning" , ok : false , durationMs : Date . now ( ) - start , error : e . message } ) ;
}
// 5. SSH 执行诊断命令(仅当 hostConfig 可用时)
const evidence = [ ] ;
let allCmdOutputs = "" ;
if ( hostConfig && phase1Plan . commands ? . length ) {
console . log ( "[RCA] 开始 SSH 诊断,目标:" , hostConfig . hostname , "命令数:" , phase1Plan . commands . length ) ;
for ( const cmd of phase1Plan . commands ) {
const cmdStart = Date . now ( ) ;
try {
const sshRes = await execSshCommand ( {
host : hostConfig . hostname ,
port : hostConfig . port || 22 ,
username : hostConfig . username ,
password : hostConfig . password ,
privateKey : hostConfig . privateKeyContent ,
passphrase : hostConfig . passphrase ,
} , cmd . command , { connectTimeoutMs : 15000 , execTimeoutMs : 20000 } ) ;
const output = ( sshRes || "" ) . toString ( ) ;
const truncated = output . length > 2000 ? output . slice ( - 2000 ) : output ;
allCmdOutputs += ` \n ### ${ cmd . command } \n ${ truncated } \n ` ;
evidence . push ( {
tool : "ssh_command" ,
summary : ` ${ cmd . command } : ${ output . length } 字节 ` ,
keyFindings : [ truncated . slice ( 0 , 300 ) ] ,
} ) ;
results . toolInvocations . push ( { tool : cmd . command , ok : true , durationMs : Date . now ( ) - cmdStart } ) ;
} catch ( e ) {
results . toolInvocations . push ( { tool : cmd . command , ok : false , durationMs : Date . now ( ) - cmdStart , error : e . message } ) ;
evidence . push ( { tool : "ssh_command" , summary : ` ${ cmd . command } : 失败 - ${ e . message } ` , keyFindings : [ ] } ) ;
}
}
} else if ( ! hostConfig ) {
console . warn ( "[RCA] 无 hostConfig, 跳过 SSH 诊断" ) ;
}
// 6. Phase 2 — AI 综合根因分析
try {
const phase2Input = JSON . stringify ( {
alert : alertInfo ,
metrics ,
catalog _id : catalogId ,
command _outputs : allCmdOutputs . slice ( 0 , 8000 ) ,
} ) ;
const phase2Text = await _callAiForRCA ( providerConfigs , PHASE2 _SYSTEM , phase2Input , 2000 ) ;
const phase2Json = _extractJson ( phase2Text ) ;
const rcaPayload = JSON . stringify ( {
_ _rca _json : true ,
generated _at : Date . now ( ) ,
source : "ai_full" ,
summary : phase2Json . summary ,
confidence : phase2Json . confidence ,
rootCauses : phase2Json . rootCauses ,
evidence ,
recommendations : phase2Json . recommendations ,
} ) ;
// 7. 写回告警 note
try {
resetCaches ( ) ;
const rawAlerts = readFn ( "netcatty_inspection_alert_records_v1" ) ;
let alertArr = typeof rawAlerts === "string" ? JSON . parse ( rawAlerts || "[]" ) : ( rawAlerts || [ ] ) ;
alertArr = alertArr . map ( a => a . id === alertId ? { ... a , note : "[RCA_JSON]" + rcaPayload } : a ) ;
writeFn ( "netcatty_inspection_alert_records_v1" , JSON . stringify ( alertArr ) ) ;
console . log ( "[RCA] 结果已写回告警 note" ) ;
} catch ( e ) {
console . error ( "[RCA] 写回告警失败:" , e . message ) ;
}
return {
success : true ,
rca : {
alertId ,
deviceId : alert . device _id ,
deviceName : alert . device _name ,
confidence : phase2Json . confidence ? ? 0 ,
rootCauses : phase2Json . rootCauses || [ ] ,
evidence ,
recommendations : phase2Json . recommendations || [ ] ,
summary : phase2Json . summary || "" ,
} ,
source : "ai" ,
durationMs : Date . now ( ) - start ,
toolInvocations : results . toolInvocations ,
} ;
} catch ( e ) {
console . error ( "[RCA] Phase 2 综合失败:" , e . message ) ;
return {
success : false ,
error : "Phase 2 AI 综合失败: " + e . message ,
rca : null ,
durationMs : Date . now ( ) - start ,
toolInvocations : results . toolInvocations ,
} ;
}
}
// 注册为 IPC handler( Electron 本地备用,主进程直接调用)
ipcMain . handle ( "inspection:runAlertRCA" , async ( _e , alertId ) => {
try {
const result = await runAlertRCA ( alertId ) ;
return { ok : result . success , ... result } ;
} catch ( e ) {
return { ok : false , error : e ? . message || String ( e ) } ;
}
} ) ;
// 导出给 remoteHttpServer 直接调用(同一个主进程,共享 _hostConfigs 和存储)
module . exports . runAlertRCA = runAlertRCA ;
2026-10-15 13:53:04 +08:00
// ---------- 巡检报告快照 ----------
// runReportSnapshot: renderer 侧解密 vault 构建 hostConfig, 传入主进程执行报告命令组
// payload: {
// deviceTargets: [
// { device_id, hostname, username, password?, privateKeyContent?, passphrase?, port?, catalog_id? }
// ],
// trigger: "manual" | "scheduled"
// }
// ERROR_PATTERNS + stripControl 抽成模块内共享
// 注意: ERROR_PATTERNS 必须精确匹配真正的运行时命令失败标记,
// 不能用 "failed" "fail" 之类单词,因为正常输出里会大量包含
// 不能把 command not found 算失败 —— 巡检命令都有 2>/dev/null || echo 'no_xxx' 兜底,
// 跨平台混跑( unknown fallback 组)时没被 2>/dev/null 吞掉的 command not found 不算真正问题
const _SNAPSHOT _ERROR _PATTERNS = /^.*\b(unrecognized command|invalid input|wrong parameter|error command|unknown command|Unknown command)\b.*$|^Error.*$|% Error:|Command failed|timed out|ETIMEDOUT|ECONNREFUSED|authentication failed/i ;
function _stripControl ( text ) {
return ( text || "" )
. 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 , "" )
. replace ( /[\x00-\x08\x0B\x0C\x0E-\x1F]/g , "" )
. replace ( /\r/g , "" )
. trim ( ) ;
}
// 过滤厂商 banner URL / 欢迎语 / 版权等 AI 不需要的噪音
// 麒麟/华为/Ubuntu/Cisco/RedHat 登录 banner 里带的 Management URL 一律删除
function _stripVendorNoise ( text ) {
if ( ! text ) return text ;
let out = text ;
// 删除 banner 里的厂商 URL( http/https + www. + 厂商域名)
out = out . replace ( /https?:\/\/[^\s|,]+/g , "" ) ;
// 单独处理 www.kylinos / www.huawei / www.cisco / www.redhat / www.ubuntu 等没带 http 的
out = out . replace ( /\bwww\.[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b/gi , "" ) ;
// 删除常见 banner 标语行(整行删除,不影响命令输出)
out = out . split ( "\n" )
. filter ( line => {
const t = line . trim ( ) ;
if ( ! t ) return true ;
// 麒麟 / Ubuntu / RedHat 欢迎 banner
if ( /Welcome to (Kylin|Ubuntu|CentOS|Red Hat|RedHat|Debian|SUSE|openSUSE|Fedora|AlmaLinux|Rocky Linux)/i . test ( t ) ) return false ;
// 华为/H3C 登录 banner 第一行("Huawei Versatile Routing Platform Software" / "H3C Comware Platform Software")
if ( /^(Huawei|H3C|Cisco|Ruijie|ZTE|锐捷|中兴|华为)/ . test ( t ) && /(Platform|Software|Operating System|Comware|VRP)/i . test ( t ) ) return false ;
// Management URL 行("Management: http://..." / "Management IP: ..." 但 URL 已在上文删过,这里删整行)
if ( /^\s*\*?\s*Management\s*[:: ]/i . test ( t ) ) return false ;
// * Copyright / * Trademark / * All Rights Reserved / * For Internal Use Only
if ( /^\s*\*?\s*(Copyright|Trademark|All Rights Reserved|For Internal Use|Unauthorized)/i . test ( t ) ) return false ;
// 纯 * 号行( banner 装饰线)
if ( /^\s*\*{5,}\s*$/ . test ( t ) ) return false ;
return true ;
} )
. join ( "\n" ) ;
// 多个空行压缩成最多一个
out = out . replace ( /\n{3,}/g , "\n\n" ) ;
return out . trim ( ) ;
}
// 内部函数: 执行一次快照( IPC handler + reportScheduler 共用)
async function _executeSnapshot ( deviceTargets , trigger = "manual" ) {
const t0 = Date . now ( ) ;
2026-10-19 21:11:57 +08:00
const DEBUG = process . env . INSPECTION _DEBUG === "1" ;
2026-10-15 15:30:31 +08:00
// 打印唯一调用栈前几帧 —— 一眼看出是谁调的
const stack = new Error ( ) . stack ? . split ( "\n" ) . slice ( 2 , 5 ) . join ( "\n " ) || "" ;
console . error ( ` [EXEC_SNAPSHOT] ════════════════════════════════════ ` ) ;
console . error ( ` [EXEC_SNAPSHOT] 路径: ${ trigger } | 设备数: ${ deviceTargets . length } ` ) ;
console . error ( ` [EXEC_SNAPSHOT] 调用栈: \n ${ stack } ` ) ;
console . error ( ` [EXEC_SNAPSHOT] ════════════════════════════════════ ` ) ;
2026-10-15 13:53:04 +08:00
console . log ( "[Inspection:_executeSnapshot] 目标设备数:" , deviceTargets . length , "trigger:" , trigger ) ;
const allDevices = [ ] ;
for ( const target of deviceTargets ) {
// catalogId: payload 有就用,没有从 device extension 兜底
let catalogId = target . catalog _id ;
if ( ! catalogId ) {
try {
const ext = deviceStore . getExtension ( readFn , target . device _id ) ;
catalogId = ext ? . catalog _id || undefined ;
} catch ( _ ) { }
}
const cmds = reportCommands . getReportCommands ( catalogId ) ;
const catalogCmdDefs = cmds . commands ; // 保留原始对象(含 altCmd / purpose)
// 传给 execSshCommands 的纯字符串数组( preCmd 单独标记)
const orderedCmdStrings = [ ] ;
if ( cmds . preCmd ) orderedCmdStrings . push ( cmds . preCmd ) ;
orderedCmdStrings . push ( ... catalogCmdDefs . map ( c => c . cmd ) ) ;
const sshConfig = {
host : target . hostname ,
port : target . port || 22 ,
username : target . username ,
password : target . password ,
privateKey : target . privateKeyContent ,
passphrase : target . passphrase ,
keepaliveCountMax : 0 ,
keepaliveInterval : 0 ,
} ;
const deviceCmds = [ ] ;
let deviceOk = true ;
// 先把所有主命令跑一遍
let sshResult ;
try {
sshResult = await execSshCommands ( sshConfig , orderedCmdStrings , {
connectTimeoutMs : 15000 ,
execTimeoutMs : 30000 ,
} ) ;
} catch ( e ) {
// 整台设备 SSH 失败
deviceOk = false ;
for ( const cmd of orderedCmdStrings ) {
deviceCmds . push ( {
cmd , purpose : "" , ok : false , duration _ms : 0 ,
stdout : "" , stderr : String ( e ? . message || e ) ,
} ) ;
}
allDevices . push ( {
device _id : target . device _id ,
hostname : target . hostname ,
catalog _id : catalogId || "unknown" ,
ok : false ,
duration _ms : 0 ,
commands : deviceCmds ,
} ) ;
continue ;
}
const results = sshResult ? . results || [ ] ;
// 逐命令评估 —— 失败的如果有 altCmd 就单独重试一次
for ( let i = 0 ; i < orderedCmdStrings . length ; i ++ ) {
const cmd = orderedCmdStrings [ i ] ;
const isPreCmd = cmds . preCmd && cmd === cmds . preCmd ;
const def = isPreCmd ? null : catalogCmdDefs . find ( c => c . cmd === cmd ) ;
const purpose = isPreCmd ? "关闭分页(前置)" : ( def ? . purpose || "" ) ;
const r = results [ i ] || { output : "" , ok : true , durationMs : 0 } ;
let rawOutput = _stripVendorNoise ( _stripControl ( r . output || "" ) ) ;
let isOk = ! _SNAPSHOT _ERROR _PATTERNS . test ( rawOutput ) && ( r . ok !== false ) ;
let finalCmd = cmd ;
// 主命令失败 → 有 altCmd → 重试一次(仅针对 catalog 业务命令, preCmd 不重试)
if ( ! isOk && ! isPreCmd && def ? . altCmd ) {
if ( DEBUG ) console . log ( ` [Inspection:_executeSnapshot] 命令失败,尝试 altCmd: ${ cmd } → ${ def . altCmd } ` ) ;
try {
const altResult = await execSshCommand ( sshConfig , def . altCmd , {
connectTimeoutMs : 15000 ,
execTimeoutMs : 20000 ,
} ) ;
const altOutput = _stripVendorNoise ( _stripControl ( altResult ? . output || "" ) ) ;
const altOk = ! _SNAPSHOT _ERROR _PATTERNS . test ( altOutput ) && ( altResult ? . ok !== false ) ;
if ( altOk ) {
rawOutput = altOutput ;
isOk = true ;
finalCmd = def . altCmd ;
// duration 累计:主命令 + altCmd
}
} catch ( altErr ) {
if ( DEBUG ) console . log ( ` [Inspection:_executeSnapshot] altCmd 也失败: ${ altErr ? . message } ` ) ;
}
}
if ( ! isOk ) deviceOk = false ;
deviceCmds . push ( {
cmd : finalCmd ,
purpose ,
ok : isOk ,
duration _ms : r . durationMs || 0 ,
stdout : rawOutput ,
stderr : "" ,
} ) ;
}
allDevices . push ( {
device _id : target . device _id ,
hostname : target . hostname ,
catalog _id : catalogId || "unknown" ,
ok : deviceOk ,
duration _ms : 0 ,
commands : deviceCmds ,
} ) ;
console . log ( "[Inspection:_executeSnapshot] 设备" , target . hostname ,
"→ ok:" , deviceOk , "命令数:" , deviceCmds . length ) ;
}
const totalDuration = Date . now ( ) - t0 ;
for ( const dev of allDevices ) {
const lastCmd = dev . commands [ dev . commands . length - 1 ] ;
dev . duration _ms = lastCmd ? . duration _ms || 0 ;
}
const snapshot = reportStore . addSnapshot ( readFn , writeFn , {
timestamp : Date . now ( ) ,
duration _ms : totalDuration ,
trigger ,
devices : allDevices ,
} ) ;
console . log ( "[Inspection:_executeSnapshot] 完成,耗时:" , totalDuration , "ms, 快照 id:" , snapshot . id ) ;
// 广播快照完成事件 —— renderer 即使切换页面也能收到,切回后自动刷新列表
try {
const { webContents } = require ( "electron" ) ;
for ( const wc of webContents . getAllWebContents ( ) ) {
wc . send ( "inspection:reportSnapshotCompleted" , {
snapshotId : snapshot . id ,
timestamp : snapshot . timestamp ,
trigger : snapshot . trigger ,
deviceCount : snapshot . devices . length ,
okCount : snapshot . devices . filter ( d => d . ok ) . length ,
duration _ms : totalDuration ,
} ) ;
}
} catch ( _ ) { }
// 自动触发 AI 分析 —— 不阻塞快照返回,后台独立执行
const bridge = require ( "../aiBridge.cjs" ) ;
2026-10-19 21:11:57 +08:00
const activeProviders = bridge . providerConfigs || [ ] ;
2026-10-15 13:53:04 +08:00
const defaultProvider = activeProviders . find ( p => p ? . selected ) || activeProviders [ 0 ] ;
const defaultProviderId = defaultProvider ? . id ;
setImmediate ( ( ) => {
// 直接调用原来 handler 里的完整逻辑(避免重复代码)
_runReportAiSummaryInternal ( snapshot . id , defaultProviderId )
. catch ( err => console . error ( "[Inspection] 自动 AI 分析失败:" , err ? . message || String ( err ) ) ) ;
} ) ;
return snapshot ;
}
/** AI 内部执行函数 —— 自动触发和 IPC handler 共用同一套逻辑 */
async function _runReportAiSummaryInternal ( snapshotId , providerId ) {
2026-10-19 21:11:57 +08:00
console . log ( "[ReportAiSummaryInternal] 🚀 开始执行 snapshotId=" , snapshotId , "providerId=" , providerId ) ;
2026-10-15 13:53:04 +08:00
const snapshot = reportStore . getSnapshot ( readFn , snapshotId ) ;
2026-10-19 21:11:57 +08:00
if ( ! snapshot ) {
console . log ( "[ReportAiSummaryInternal] ❌ 快照不存在:" , snapshotId ) ;
return ;
}
console . log ( "[ReportAiSummaryInternal] ✅ 快照找到, devices=" , snapshot . devices ? . length || 0 ) ;
2026-10-15 13:53:04 +08:00
reportStore . setSnapshotAiStatus ( readFn , writeFn , snapshotId , "running" ) ;
_broadcastReportAiStatus ( snapshotId , "running" ) ;
try {
const bridge = require ( "../aiBridge.cjs" ) ;
2026-10-19 21:11:57 +08:00
const activeProviders = bridge . providerConfigs ;
console . log ( "[ReportAiSummaryInternal] activeProviders=" , activeProviders . length ) ;
2026-10-15 13:53:04 +08:00
const provider = activeProviders . find ( p => p . id === providerId )
|| activeProviders . find ( p => p . enabled !== false ) ;
if ( ! provider ) throw new Error ( "AI provider 未配置" ) ;
2026-10-19 21:11:57 +08:00
console . log ( "[ReportAiSummaryInternal] ✅ provider=" , provider . id , provider . name ) ;
const resolved = bridge . resolveProviderApiKey ( provider . id ) ;
2026-10-15 13:53:04 +08:00
if ( ! resolved ? . apiKey ) throw new Error ( "AI provider API key 未解密" ) ;
2026-10-19 21:11:57 +08:00
console . log ( "[ReportAiSummaryInternal] ✅ API key 已解密, 长度=" , resolved . apiKey . length ) ;
2026-10-15 13:53:04 +08:00
const userContent = _buildSnapshotPrompt ( snapshot ) ;
2026-10-19 21:11:57 +08:00
// 兜底:如果压缩后仍超过 50KB, 硬截断设备数量
let finalContent = userContent ;
if ( userContent . length > 50000 ) {
try {
const parsed = JSON . parse ( userContent ) ;
if ( parsed . online && parsed . online . length > 12 ) {
parsed . online = parsed . online . slice ( 0 , 12 ) ;
parsed . summary . online _n = 12 ;
finalContent = JSON . stringify ( parsed ) ;
}
} catch { }
}
2026-10-15 13:53:04 +08:00
const url = _buildAiUrl ( provider ) ;
const headers = { "Content-Type" : "application/json" , "Authorization" : "Bearer __IPC_SECURED__" } ;
2026-10-19 21:11:57 +08:00
const body = JSON . stringify ( _buildAiBody ( provider , finalContent ) ) ;
console . log ( "[ReportAiSummaryInternal] 准备请求 AI, url=" , url ? . slice ( 0 , 80 ) , "body.length=" , body . length ) ;
const patched = bridge . injectApiKeyIntoRequest ( url , headers , provider . id ) ;
const skipTLS = bridge . shouldSkipTLSVerify ( provider . id ) ;
2026-10-15 13:53:04 +08:00
2026-10-19 21:11:57 +08:00
console . log ( "[ReportAiSummaryInternal] 发送 HTTP 请求..." ) ;
2026-10-15 13:53:04 +08:00
const result = await _doHttpFetch ( patched . url , patched . headers , body , { skipTLS , timeoutMs : 60000 } ) ;
2026-10-19 21:11:57 +08:00
console . log ( "[ReportAiSummaryInternal] HTTP 返回 status=" , result . status , "ok=" , result . ok , "error=" , result . error ) ;
2026-10-15 13:53:04 +08:00
if ( ! result . ok ) throw new Error ( result . error || ` HTTP ${ result . status } ` ) ;
const raw = _parseAiResponse ( provider , result . data ) ;
2026-10-19 21:11:57 +08:00
console . log ( "[ReportAiSummaryInternal] 解析 AI 响应长度=" , raw ? . length || 0 ) ;
2026-10-15 13:53:04 +08:00
const parsed = _extractJson ( raw ) ;
const summary = parsed || { health _summary : raw , offline _devices : [ ] , hidden _risks : [ ] , recommendations : [ ] } ;
reportStore . updateSnapshotAiSummary ( readFn , writeFn , snapshotId , summary ) ;
_broadcastReportAiStatus ( snapshotId , "done" ) ;
2026-10-19 21:11:57 +08:00
console . log ( "[ReportAiSummaryInternal] ✅ AI 分析完成!" ) ;
2026-10-15 13:53:04 +08:00
} catch ( err ) {
const msg = err ? . message || String ( err ) ;
console . error ( "[runReportAiSummary] 失败:" , msg ) ;
2026-10-19 21:11:57 +08:00
console . error ( "[runReportAiSummary] stack:" , err ? . stack ? . split ( '\n' ) . slice ( 0 , 5 ) . join ( '\n' ) ) ;
2026-10-15 13:53:04 +08:00
reportStore . updateSnapshotAiSummary ( readFn , writeFn , snapshotId , null , msg ) ;
_broadcastReportAiStatus ( snapshotId , "failed" , msg ) ;
}
}
ipcMain . handle ( "inspection:runReportSnapshot" , async ( _e , payload ) => {
const { deviceTargets = [ ] , trigger = "manual" } = payload || { } ;
2026-10-21 17:13:21 +08:00
// 防重入:快照正在执行中就拒绝(用户连点按钮或定时 tick 并发)
if ( _isSnapshotRunning ) {
console . warn ( "[Inspection:runReportSnapshot] 快照正在执行中,拒绝并发请求" ) ;
return { ok : false , error : "快照正在执行中,请稍候" } ;
}
2026-10-15 13:53:04 +08:00
_isSnapshotRunning = true ;
// 广播 running 状态(页面切换后回来的 renderer 也能收到)
try {
const { webContents } = require ( "electron" ) ;
for ( const wc of webContents . getAllWebContents ( ) ) {
wc . send ( "inspection:reportSnapshotStatusChanged" , { running : true } ) ;
}
} catch ( _ ) { }
try {
const snapshot = await _executeSnapshot ( deviceTargets , trigger ) ;
return { ok : true , snapshot } ;
} catch ( e ) {
console . error ( "[Inspection:runReportSnapshot] 失败:" , e ) ;
return { ok : false , error : e ? . message || String ( e ) } ;
} finally {
_isSnapshotRunning = false ;
2026-10-15 15:30:31 +08:00
// 手动巡检也更新 lastRunAt —— 避免手动跑完后定时 tick 还到点触发
try {
const cfg = reportScheduleStore . getSchedule ( readFn ) ;
reportScheduleStore . saveSchedule ( writeFn , { ... cfg , lastRunAt : Date . now ( ) } ) ;
} catch ( _ ) { }
2026-10-15 13:53:04 +08:00
try {
const { webContents } = require ( "electron" ) ;
for ( const wc of webContents . getAllWebContents ( ) ) {
wc . send ( "inspection:reportSnapshotStatusChanged" , { running : false } ) ;
}
} catch ( _ ) { }
}
} ) ;
ipcMain . handle ( "inspection:isReportSnapshotRunning" , ( ) => {
return { running : _isSnapshotRunning } ;
} ) ;
ipcMain . handle ( "inspection:listReportSnapshots" , ( _e , limit ) => {
return reportStore . listSnapshots ( readFn , limit || 50 ) ;
} ) ;
ipcMain . handle ( "inspection:deleteReportSnapshot" , ( _e , id ) => {
return reportStore . deleteSnapshot ( readFn , writeFn , id ) ;
} ) ;
ipcMain . handle ( "inspection:clearReportSnapshots" , ( ) => {
reportStore . clearAll ( readFn , writeFn ) ;
return { ok : true } ;
} ) ;
ipcMain . handle ( "inspection:saveReportAiSummary" , ( _e , { snapshotId , aiSummary } ) => {
if ( ! snapshotId ) return { ok : false , error : "snapshotId required" } ;
const ok = reportStore . updateSnapshotAiSummary ( readFn , writeFn , snapshotId , aiSummary ) ;
return { ok } ;
} ) ;
2026-10-19 21:11:57 +08:00
// 拓扑位置持久化( Electron 拖拽保存 → 远程浏览器可同步读取)
ipcMain . handle ( "inspection:saveTopologyPositions" , ( _e , positions ) => {
if ( ! positions || typeof positions !== "object" ) return { ok : false } ;
try {
if ( Object . keys ( positions ) . length === 0 ) {
const existing = readStorage ( "_topology_positions" ) ;
if ( existing && typeof existing === "object" && Object . keys ( existing ) . length > 0 ) {
console . info ( "[inspection] saveTopologyPositions skip empty" ) ;
return { ok : true , skipped : true } ;
}
}
writeStorage ( "_topology_positions" , positions ) ;
return { ok : true } ;
} catch ( e ) {
return { ok : false , error : e ? . message } ;
}
} ) ;
ipcMain . handle ( "inspection:getTopologyPositions" , ( ) => {
try {
return readStorage ( "_topology_positions" ) || { } ;
} catch { return { } ; }
} ) ;
// 监控大屏标题同步( Electron localStorage → 远程浏览器可读)
ipcMain . handle ( "inspection:saveDashboardTitle" , ( _e , title ) => {
if ( ! title || typeof title !== "string" ) return { ok : false } ;
try {
const RENDERER _DEFAULT = "云启Net智能巡检监测系统" ;
const existing = readStorage ( "_dashboard_title" ) ;
if ( title === RENDERER _DEFAULT && existing && existing !== RENDERER _DEFAULT ) {
console . info ( "[inspection] saveDashboardTitle skip default:" , title , "vs:" , existing ) ;
return { ok : true , skipped : true } ;
}
writeStorage ( "_dashboard_title" , title ) ;
return { ok : true } ;
} catch ( e ) {
return { ok : false , error : e ? . message } ;
}
} ) ;
// 产品名称同步(侧边栏显示名,远程浏览器读)
ipcMain . handle ( "inspection:saveProductName" , ( _e , name ) => {
if ( ! name || typeof name !== "string" ) return { ok : false } ;
try {
const RENDERER _DEFAULT = "云启智能运维平台" ;
const existing = readStorage ( "_product_name" ) ;
if ( name === RENDERER _DEFAULT && existing && existing !== RENDERER _DEFAULT ) {
console . info ( "[inspection] saveProductName skip default:" , name , "vs:" , existing ) ;
return { ok : true , skipped : true } ;
}
writeStorage ( "_product_name" , name ) ;
return { ok : true } ;
} catch ( e ) { return { ok : false , error : e ? . message } ; }
} ) ;
ipcMain . handle ( "inspection:getProductName" , ( ) => {
try { return readStorage ( "_product_name" ) || "" ; } catch { return "" ; }
} ) ;
// 报告产品名同步
ipcMain . handle ( "inspection:saveReportProductName" , ( _e , name ) => {
if ( typeof name !== "string" ) return { ok : false } ;
try {
if ( ( name === "" || name === null ) && name !== undefined ) {
const existing = readStorage ( "_report_product_name" ) ;
if ( existing && existing . length > 0 ) {
console . info ( "[inspection] saveReportProductName skip empty, existing:" , existing ) ;
return { ok : true , skipped : true } ;
}
}
writeStorage ( "_report_product_name" , name ) ;
return { ok : true } ;
} catch ( e ) { return { ok : false , error : e ? . message } ; }
} ) ;
ipcMain . handle ( "inspection:getReportProductName" , ( ) => {
try { return readStorage ( "_report_product_name" ) || "" ; } catch { return "" ; }
} ) ;
// 完整 vault hosts 同步(前端加载后发给主进程,远程浏览器 API 读)
ipcMain . handle ( "inspection:saveVaultHosts" , ( _e , hosts ) => {
if ( ! Array . isArray ( hosts ) ) { console . info ( "[inspection] saveVaultHosts called but not array:" , typeof hosts ) ; return { ok : false } ; }
try {
console . info ( "[inspection] saveVaultHosts called, count:" , hosts . length , "sample keys:" , hosts [ 0 ] ? Object . keys ( hosts [ 0 ] ) . join ( ',' ) : 'empty' ) ;
// 守卫: renderer 首次启动 Local Storage 空时会传 [],
// 但 inspectionStorage 可能已有完整数据(含凭证)→ 不要覆盖!
if ( hosts . length === 0 ) {
const existing = readStorage ( "_vault_hosts" ) ;
if ( Array . isArray ( existing ) && existing . length > 0 ) {
console . info ( "[inspection] saveVaultHosts 跳过空覆盖" ) ;
return { ok : true , count : existing . length , skipped : true } ;
}
}
// 保存完整 hosts( 含 username/password/port/identityFileId 等凭证字段)
const mapped = hosts . map ( h => ( {
id : h . id ,
label : h . label || h . name || h . hostname || "" ,
hostname : h . hostname || h . name || "" ,
port : h . port ,
username : h . username ,
password : h . password ,
identityFileId : h . identityFileId ,
privateKey : h . privateKey ,
passphrase : h . passphrase ,
2026-10-21 11:50:00 +08:00
tags : h . tags || [ ] ,
os : h . os || "linux" ,
2026-10-19 21:11:57 +08:00
type : h . type || "" ,
distro : h . distro || h . catalogId || "" ,
group : h . group || "" ,
2026-10-21 11:50:00 +08:00
legacyAlgorithms : h . legacyAlgorithms ,
2026-10-19 21:11:57 +08:00
} ) ) ;
writeStorage ( "_vault_hosts" , mapped ) ;
return { ok : true , count : mapped . length } ;
} catch ( e ) {
return { ok : false , error : e ? . message } ;
}
} ) ;
ipcMain . handle ( "inspection:getVaultHosts" , ( ) => {
try {
return readStorage ( "_vault_hosts" ) || [ ] ;
} catch { return [ ] ; }
} ) ;
// Vault 通用数据同步( renderer localStorage → 主进程 inspection-data.json → 远程 HTTP API 读)
// 覆盖所有子页面:钥匙串/代理/端口转发/脚本/笔记/已知主机/日志/分组/身份
const _VAULT _DATA _KEYS = [ "hosts" , "keys" , "proxies" , "port_forwards" , "snippets" , "notes" , "known_hosts" , "connection_logs" , "groups" , "identities" ] ;
_VAULT _DATA _KEYS . forEach ( vk => {
const sk = "_vault_" + vk ;
// renderer → 主进程
// 守卫: renderer 首次启动 Local Storage 空时会传 [],
// 但 inspectionStorage 可能已有从开发版拷过来的完整数据 → 不要覆盖!
ipcMain . handle ( "inspection:syncVault:" + vk , ( _e , data ) => {
try {
const incomingEmpty = data === null || data === undefined
|| ( Array . isArray ( data ) && data . length === 0 )
|| ( typeof data === "object" && ! Array . isArray ( data ) && Object . keys ( data ) . length === 0 ) ;
const existing = readStorage ( sk ) ;
const existingNonEmpty = existing !== null && existing !== undefined
&& ( ( Array . isArray ( existing ) && existing . length > 0 )
|| ( typeof existing === "object" && ! Array . isArray ( existing ) && Object . keys ( existing ) . length > 0 ) ) ;
if ( incomingEmpty && existingNonEmpty ) {
console . info ( ` [inspection] syncVault: ${ vk } 跳过: renderer 传空,但 inspectionStorage 已有 ${ ( Array . isArray ( existing ) ? existing . length : Object . keys ( existing ) . length ) } 条 ` ) ;
return { ok : true , skipped : true } ;
}
writeStorage ( sk , data ? ? ( Array . isArray ( data ) ? [ ] : { } ) ) ;
return { ok : true } ;
} catch ( e ) {
return { ok : false , error : e ? . message } ;
}
} ) ;
// 主进程内部读取( remoteHttpServer 也会直接 require inspectionStorage 读)
ipcMain . handle ( "inspection:getVault:" + vk , ( ) => {
try { return readStorage ( sk ) ; } catch { return null ; }
} ) ;
} ) ;
// 拓扑连线同步( Electron localStorage → 主进程 → 远程浏览器 API 读)
ipcMain . handle ( "inspection:getTopologyLinks" , ( ) => {
try { const v = readStorage ( "_topology_links" ) ; return Array . isArray ( v ) ? v : [ ] ; } catch { return [ ] ; }
} ) ;
ipcMain . handle ( "inspection:saveTopologyLinks" , ( _e , links ) => {
try {
const arr = Array . isArray ( links ) ? links : [ ] ;
if ( arr . length === 0 ) {
const existing = readStorage ( "_topology_links" ) ;
if ( Array . isArray ( existing ) && existing . length > 0 ) {
console . info ( "[inspection] saveTopologyLinks skip empty" ) ;
return { ok : true , count : existing . length , skipped : true } ;
}
}
writeStorage ( "_topology_links" , arr ) ;
return { ok : true , count : arr . length } ;
} catch ( e ) {
return { ok : false , error : e ? . message } ;
}
} ) ;
ipcMain . handle ( "inspection:getDashboardTitle" , ( ) => {
try {
return readStorage ( "_dashboard_title" ) || "" ;
} catch { return "" ; }
} ) ;
2026-10-15 13:53:04 +08:00
ipcMain . handle ( "inspection:clearReportAiSummary" , ( _e , { snapshotId } ) => {
if ( ! snapshotId ) return { ok : false , error : "snapshotId required" } ;
const ok = reportStore . clearSnapshotAiSummary ( readFn , writeFn , snapshotId ) ;
return { ok } ;
} ) ;
ipcMain . handle ( "inspection:runReportAiSummary" , async ( _e , { snapshotId , providerId } ) => {
if ( ! snapshotId ) return { ok : false , error : "snapshotId required" } ;
2026-10-19 21:11:57 +08:00
console . log ( "[ReportAiSummary] ⚡ 收到 IPC 请求 snapshotId=" , snapshotId , "providerId=" , providerId ) ;
console . log ( "[ReportAiSummary] providerConfigs=" , require ( "../aiBridge.cjs" ) . aiBridgeContext ? . providerConfigs ? . length || 0 , "个" ) ;
2026-10-15 13:53:04 +08:00
// 后台独立执行 —— 不阻塞 IPC 返回
setImmediate ( ( ) => {
2026-10-19 21:11:57 +08:00
console . log ( "[ReportAiSummary] ⏱️ setImmediate 触发执行..." ) ;
2026-10-15 13:53:04 +08:00
_runReportAiSummaryInternal ( snapshotId , providerId )
. catch ( err => console . error ( "[runReportAiSummary] 异常:" , err ? . message || String ( err ) ) ) ;
} ) ;
return { ok : true } ;
} ) ;
// ---------- 巡检报告调度配置 ----------
// 注意:定时快照执行需要 vault SSH 配置(仅 renderer 可解密),
// reportScheduler 目前只负责日报告 HH:MM 定时聚合;
// 定时快照由 renderer 侧( ReportPage useEffect + setInterval) 调 runReportSnapshot 传入解密后的 hostConfig
ipcMain . handle ( "inspection:getReportSchedule" , ( ) => {
return reportScheduleStore . getSchedule ( readFn ) ;
} ) ;
ipcMain . handle ( "inspection:saveReportSchedule" , ( _e , config ) => {
const saved = reportScheduleStore . saveSchedule ( writeFn , config ) ;
// 配置变更后如果 scheduler 没启动但 enabled=true, 则启动( 负责 dailyReport 定时聚合 + 主进程定时快照)
if ( saved . enabled && ! reportScheduler . isRunning ( ) ) {
reportScheduler . start ( {
readStorage : options . readStorage ,
writeStorage : options . writeStorage ,
runSnapshotFn : ( deviceIds , trigger ) => _runScheduledSnapshot ( deviceIds , trigger ) ,
2026-10-15 14:15:16 +08:00
getAllDeviceIdsFn : ( ) => Object . keys ( _hostConfigs || { } ) ,
2026-10-15 13:53:04 +08:00
} ) ;
}
return saved ;
} ) ;
ipcMain . handle ( "inspection:toggleReportScheduler" , ( _e , enabled ) => {
const cfg = reportScheduleStore . getSchedule ( readFn ) ;
cfg . enabled = ! ! enabled ;
reportScheduleStore . saveSchedule ( writeFn , cfg ) ;
if ( cfg . enabled && ! reportScheduler . isRunning ( ) ) {
reportScheduler . start ( {
readStorage : options . readStorage ,
writeStorage : options . writeStorage ,
runSnapshotFn : ( deviceIds , trigger ) => _runScheduledSnapshot ( deviceIds , trigger ) ,
2026-10-15 14:15:16 +08:00
getAllDeviceIdsFn : ( ) => Object . keys ( _hostConfigs || { } ) ,
2026-10-15 13:53:04 +08:00
} ) ;
} else if ( ! cfg . enabled && reportScheduler . isRunning ( ) ) {
reportScheduler . stop ( ) ;
}
return { ok : true , running : reportScheduler . isRunning ( ) } ;
} ) ;
ipcMain . handle ( "inspection:reportSchedulerStatus" , ( ) => {
return {
running : reportScheduler . isRunning ( ) ,
schedule : reportScheduleStore . getSchedule ( readFn ) ,
} ;
} ) ;
// ---------- 日运行报告 ----------
ipcMain . handle ( "inspection:listDailyReports" , ( _e , limit ) => {
return dailyReportStore . listDailyReports ( readFn , limit || 30 ) ;
} ) ;
ipcMain . handle ( "inspection:generateDailyReport" , async ( _e , options ) => {
const { mode = "provisional" , trigger = "manual" } = options || { } ;
try {
const report = await reportScheduler . triggerDailyReport ( readFn , writeFn , { mode , trigger } ) ;
return { ok : true , report } ;
} catch ( e ) {
console . error ( "[Inspection:generateDailyReport] 失败:" , e ) ;
return { ok : false , error : e ? . message || String ( e ) } ;
}
} ) ;
ipcMain . handle ( "inspection:deleteDailyReport" , ( _e , id ) => {
return dailyReportStore . deleteDailyReport ( readFn , writeFn , id ) ;
} ) ;
ipcMain . handle ( "inspection:clearDailyReports" , ( ) => {
dailyReportStore . clearAll ( readFn , writeFn ) ;
return { ok : true } ;
} ) ;
// ---------- PDF 导出 ----------
ipcMain . handle ( "inspection:exportReportSnapshotPdf" , async ( _e , payload ) => {
try {
console . log ( "[ExportPDF] 开始 payload=" , payload ) ;
const snapshotId = typeof payload === "string" ? payload : payload ? . snapshotId ;
const aiSummary = typeof payload === "object" && payload !== null ? payload . aiSummary : undefined ;
const customPath = typeof payload === "object" && payload !== null ? payload . customPath : undefined ;
const systemName = ( typeof payload === "object" && payload !== null ? payload . systemName : undefined ) || _readProductName ( readFn ) ;
console . log ( "[ExportPDF] snapshotId=" , snapshotId , "customPath=" , customPath , "systemName=" , systemName ) ;
const snap = reportStore . getSnapshot ( readFn , snapshotId ) ;
if ( ! snap ) {
console . log ( "[ExportPDF] 快照不存在 snapshotId=" , snapshotId ) ;
return { ok : false , error : "快照不存在" } ;
}
console . log ( "[ExportPDF] 找到快照 devices=" , snap . devices ? . length , "aiSummary存在=" , ! ! aiSummary ) ;
const app = require ( "electron" ) . app ;
const filePath = await reportExport . exportSnapshotPdf ( app , snap , aiSummary , customPath , systemName ) ;
console . log ( "[ExportPDF] 完成 filePath=" , filePath ) ;
return { ok : true , filePath } ;
} catch ( e ) {
console . error ( "[ExportPDF] 失败:" , e ) ;
console . error ( "[ExportPDF] 堆栈:" , e ? . stack ) ;
return { ok : false , error : e ? . message || String ( e ) } ;
}
} ) ;
ipcMain . handle ( "inspection:previewReportSnapshotPdf" , async ( _e , { snapshotId , systemName } ) => {
try {
console . log ( "[PreviewPDF] 开始 snapshotId=" , snapshotId , "systemName=" , systemName ) ;
const snap = reportStore . getSnapshot ( readFn , snapshotId ) ;
if ( ! snap ) return { ok : false , error : "快照不存在" } ;
const app = require ( "electron" ) . app ;
const aiSummary = snap . ai _summary || undefined ;
const effectiveName = systemName || _readProductName ( readFn ) ;
console . log ( "[PreviewPDF] aiSummary 存在:" , ! ! aiSummary , "effectiveName:" , effectiveName ) ;
const result = await reportExport . previewSnapshotPdf ( app , snap , aiSummary , effectiveName ) ;
console . log ( "[PreviewPDF] 完成 ok=" , result . ok , "filePath=" , result . filePath ) ;
return result ;
} catch ( e ) {
console . error ( "[PreviewPDF] 失败:" , e ) ;
console . error ( "[PreviewPDF] 堆栈:" , e ? . stack ) ;
return { ok : false , error : e ? . message || String ( e ) } ;
}
} ) ;
ipcMain . handle ( "inspection:previewDailyReportPdf" , async ( _e , { reportId , systemName } ) => {
try {
console . log ( "[PreviewDailyPDF] 开始 reportId=" , reportId , "systemName=" , systemName ) ;
const reports = dailyReportStore . listDailyReports ( readFn , 500 ) ;
const report = reports . find ( r => r . id === reportId ) ;
if ( ! report ) return { ok : false , error : "日报告不存在" } ;
const app = require ( "electron" ) . app ;
const allSnaps = reportStore . listSnapshots ( readFn , 500 ) ;
const pad = ( n ) => n . toString ( ) . padStart ( 2 , "0" ) ;
const daySnaps = allSnaps . filter ( s => {
const d = new Date ( s . timestamp ) ;
return ` ${ d . getFullYear ( ) } - ${ pad ( d . getMonth ( ) + 1 ) } - ${ pad ( d . getDate ( ) ) } ` === report . date ;
} ) ;
const effectiveName = systemName || _readProductName ( readFn ) ;
console . log ( "[PreviewDailyPDF] report.date:" , report . date , "daySnaps:" , daySnaps . length , "effectiveName:" , effectiveName ) ;
const result = await reportExport . previewDailyReportPdf ( app , report , daySnaps , effectiveName ) ;
console . log ( "[PreviewDailyPDF] 完成 ok=" , result . ok , "filePath=" , result . filePath ) ;
return result ;
} catch ( e ) {
console . error ( "[PreviewDailyPDF] 失败:" , e ) ;
console . error ( "[PreviewDailyPDF] 堆栈:" , e ? . stack ) ;
return { ok : false , error : e ? . message || String ( e ) } ;
}
} ) ;
ipcMain . handle ( "inspection:exportDailyReportPdf" , async ( _e , payload ) => {
try {
const reportId = typeof payload === "string" ? payload : payload ? . reportId ;
const customPath = typeof payload === "object" && payload !== null ? payload . customPath : undefined ;
const systemName = ( typeof payload === "object" && payload !== null ? payload . systemName : undefined ) || _readProductName ( readFn ) ;
const reports = dailyReportStore . listDailyReports ( readFn , 500 ) ;
const report = reports . find ( r => r . id === reportId ) ;
if ( ! report ) return { ok : false , error : "日报告不存在" } ;
const app = require ( "electron" ) . app ;
const allSnaps = reportStore . listSnapshots ( readFn , 500 ) ;
const pad = ( n ) => n . toString ( ) . padStart ( 2 , "0" ) ;
const daySnaps = allSnaps . filter ( s => {
const d = new Date ( s . timestamp ) ;
return ` ${ d . getFullYear ( ) } - ${ pad ( d . getMonth ( ) + 1 ) } - ${ pad ( d . getDate ( ) ) } ` === report . date ;
} ) ;
const filePath = await reportExport . exportDailyReportPdf ( app , report , customPath , daySnaps , systemName ) ;
return { ok : true , filePath } ;
} catch ( e ) {
console . error ( "[Inspection:exportDailyReportPdf] 失败:" , e ) ;
return { ok : false , error : e ? . message || String ( e ) } ;
}
} ) ;
// ---------- 告警记录 ----------
ipcMain . handle ( "inspection:listAlerts" , ( _e , options ) => {
return alertRecordStore . listAlerts ( readFn , options || { } ) ;
} ) ;
ipcMain . handle ( "inspection:getActiveAlerts" , ( ) => {
return alertRecordStore . getActiveAlerts ( readFn ) ;
} ) ;
ipcMain . handle ( "inspection:getAlertStats" , ( ) => {
return alertRecordStore . getAlertStats ( readFn ) ;
} ) ;
ipcMain . handle ( "inspection:acknowledgeAlert" , ( _e , alertId , operator ) => {
const r = alertRecordStore . acknowledgeAlert ( readFn , writeFn , alertId , operator ) ;
broadcastAlertChanged ( ) ;
return r ;
} ) ;
ipcMain . handle ( "inspection:resolveAlert" , ( _e , alertId , reason , operator ) => {
const r = alertRecordStore . resolveAlert ( readFn , writeFn , alertId , reason , operator ) ;
broadcastAlertChanged ( ) ;
return r ;
} ) ;
ipcMain . handle ( "inspection:closeAlert" , ( _e , alertId , note ) => {
const r = alertRecordStore . closeAlert ( readFn , writeFn , alertId , note ) ;
broadcastAlertChanged ( ) ;
return r ;
} ) ;
ipcMain . handle ( "inspection:updateAlertNote" , ( _e , alertId , note ) => {
return alertRecordStore . updateNote ( readFn , writeFn , alertId , note ) ;
} ) ;
// 存储前端传入的设备 SSH 配置( hostId → sshConfig)
let _hostConfigs = null ;
2026-10-19 21:11:57 +08:00
let _loggedHostCreds = false ;
2026-10-15 13:53:04 +08:00
// ---------- 调度器控制 ----------
ipcMain . handle ( "inspection:scheduler:start" , ( _e , options = { } ) => {
// 前端可传入 { hostConfigs: { [hostId]: sshConfig } } 用于调度器执行 SSH 命令
if ( options . hostConfigs ) {
_hostConfigs = options . hostConfigs ;
}
// 从 hostConfigs 提取 hostInfo( hostname/label) 用于告警显示
const hostInfo = { } ;
if ( _hostConfigs ) {
for ( const [ hostId , config ] of Object . entries ( _hostConfigs ) ) {
hostInfo [ hostId ] = {
hostname : config . host || config . hostname || "" ,
label : config . label || "" ,
} ;
}
}
2026-10-19 21:11:57 +08:00
// 持久化 hostInfo 到 inspection-data.json, 供远程 HTTP Server 读取补全设备名
try {
writeFn ( "_host_info_cache" , hostInfo ) ;
} catch { }
2026-10-15 13:53:04 +08:00
if ( ! _scheduler ) {
_scheduler = new InspectionScheduler ( {
readFn ,
writeFn ,
execFn : async ( hostId , command ) => {
if ( _hostConfigs && _hostConfigs [ hostId ] ) {
const config = _hostConfigs [ hostId ] ;
2026-10-19 21:11:57 +08:00
if ( ! _loggedHostCreds ) {
console . log ( "[Inspection] hostConfigs 凭据快照:" , JSON . stringify (
Object . fromEntries ( Object . entries ( _hostConfigs ) . map ( ( [ id , c ] ) => [ id , {
host : c . host ,
username : c . username ,
hasPassword : ! ! c . password ,
hasPrivateKey : ! ! c . privateKey ,
} ] ) )
) ) ;
_loggedHostCreds = true ;
}
2026-10-15 13:53:04 +08:00
const { execSshCommand : execCmd } = require ( "./inspectionExec.cjs" ) ;
return await execCmd ( config , command , {
connectTimeoutMs : 20000 ,
execTimeoutMs : 30000 ,
} ) ;
}
const session = findSessionByHost ( hostId ) ;
if ( session ? . conn ) {
const { executeBoundedSshCommand } = require ( "../boundedSshExec.cjs" ) ;
return await executeBoundedSshCommand ( session . conn , command , {
timeoutMs : 30000 ,
} ) ;
}
throw new Error ( ` 无法执行巡检:未找到主机 ${ hostId } 的 SSH 配置 ` ) ;
} ,
2026-10-19 21:11:57 +08:00
// 批量执行多条命令(一次连接执行多条,华为/H3C 交换机采集器需要)
// 闭包和 execFn 一样——动态读 _hostConfigs 最新值
execCommandsFn : async ( hostId , commands , options = { } ) => {
if ( _hostConfigs && _hostConfigs [ hostId ] ) {
const config = _hostConfigs [ hostId ] ;
if ( ! _loggedHostCreds ) {
console . log ( "[Inspection] execCommandsFn 凭据快照:" , JSON . stringify (
Object . fromEntries ( Object . entries ( _hostConfigs ) . map ( ( [ id , c ] ) => [ id , {
host : c . host ,
username : c . username ,
hasPassword : ! ! c . password ,
hasPrivateKey : ! ! c . privateKey ,
} ] ) )
) ) ;
_loggedHostCreds = true ;
}
const { execSshCommands } = require ( "./inspectionExec.cjs" ) ;
return await execSshCommands ( config , commands , {
connectTimeoutMs : 20000 ,
execTimeoutMs : options . execTimeoutMs || 30000 ,
... options ,
} ) ;
}
throw new Error ( ` 无法批量执行巡检:未找到主机 ${ hostId } 的 SSH 配置 ` ) ;
} ,
2026-10-15 13:53:04 +08:00
pollIntervalMs : 30 * 1000 ,
concurrency : 5 ,
timeoutMs : 30 * 1000 ,
hostInfo ,
onAlertChanged : broadcastAlertChanged ,
} ) ;
2026-10-19 21:11:57 +08:00
} else {
// scheduler 已存在:更新 hostInfo( 告警显示 device_name 用)
_scheduler . _hostInfo = hostInfo ;
console . log ( "[Inspection] 更新 hostConfigs:" , Object . keys ( _hostConfigs || { } ) . length , "台设备" ) ;
2026-10-15 13:53:04 +08:00
}
2026-10-21 17:00:12 +08:00
// start() 幂等:已 running 时直接返回。
// 不再每次 start 都 scanAllNow —— renderer 可能在多个页面 mount 时反复调 start
// ( MonitoringPage、ReportPage、useReportAutoScheduler 各调一次),每次都 scanAllNow
// 会导致短时间内同一批设备被巡检 N 次。
// InspectionScheduler 本身有 30s tick, 到点自然会把到期设备加入队列。
2026-10-19 21:11:57 +08:00
const wasRunning = _scheduler . isRunning ( ) ;
2026-10-15 13:53:04 +08:00
_scheduler . start ( ) ;
2026-10-21 17:00:12 +08:00
if ( ! wasRunning ) {
console . log ( "[Inspection] scheduler 已启动,首次 hostConfigs=" , Object . keys ( _hostConfigs || { } ) . length ) ;
2026-10-19 21:11:57 +08:00
}
2026-10-15 13:53:04 +08:00
return _scheduler . getStatus ( ) ;
} ) ;
ipcMain . handle ( "inspection:scheduler:stop" , ( ) => {
if ( _scheduler ) {
_scheduler . stop ( ) ;
return _scheduler . getStatus ( ) ;
}
return { running : false } ;
} ) ;
ipcMain . handle ( "inspection:scheduler:status" , ( ) => {
return _scheduler ? _scheduler . getStatus ( ) : { running : false } ;
} ) ;
2026-10-19 21:11:57 +08:00
// ─── 全局自动巡检一键启用/停止 ───
ipcMain . handle ( "inspection:scheduler:getAutoEnabled" , ( ) => {
const raw = readFn ( "inspection-auto-enabled" ) ;
return raw !== "false" ; // 默认 true
} ) ;
ipcMain . handle ( "inspection:scheduler:setAutoEnabled" , ( _e , enabled ) => {
const value = ! ! enabled ;
writeFn ( "inspection-auto-enabled" , value ? "true" : "false" ) ;
console . log ( ` [InspectionScheduler] 全局自动巡检 ${ value ? "已启用" : "已停止" } ` ) ;
if ( ! value && _scheduler ) {
// 停止时清空队列,避免积压
_scheduler . _scanQueue = [ ] ;
}
return { enabled : value } ;
} ) ;
2026-10-15 13:53:04 +08:00
ipcMain . handle ( "inspection:scheduler:scanNow" , ( _e , hostId ) => {
if ( ! _scheduler ) return { error : "调度器未启动" } ;
return _scheduler . scanDeviceNow ( hostId ) ;
} ) ;
ipcMain . handle ( "inspection:scheduler:scanAllNow" , ( ) => {
if ( ! _scheduler ) return { error : "调度器未启动" } ;
return _scheduler . scanAllNow ( ) ;
} ) ;
console . log ( "[Inspection] IPC handlers 已注册" ) ;
// ─── Electron 启动时自动启动 reportScheduler ───
// 负责: per-device 定时快照 tick( 主进程自身 SSH 执行) + dailyReport HH:MM 定时聚合
async function _runScheduledSnapshot ( deviceIds , trigger = "scheduled" ) {
2026-10-21 17:13:21 +08:00
// 与 IPC 手动触发路径互斥:防止定时 tick 和用户手动按钮并发跑快照
if ( _isSnapshotRunning ) {
console . warn ( "[reportScheduler] _isSnapshotRunning=true( 可能是手动触发在执行) , 跳过本次定时快照" ) ;
return ;
}
2026-10-15 13:53:04 +08:00
if ( ! _hostConfigs || Object . keys ( _hostConfigs ) . length === 0 ) {
console . warn ( "[reportScheduler] _hostConfigs 未初始化,定时快照跳过(需先打开监控页面让 renderer 同步 SSH 凭据到主进程)" ) ;
return ;
}
const targets = [ ] ;
for ( const hostId of deviceIds ) {
const cfg = _hostConfigs [ hostId ] ;
if ( ! cfg ) {
console . warn ( ` [reportScheduler] hostConfigs 中找不到 ${ hostId } ,跳过 ` ) ;
continue ;
}
let catalogId = undefined ;
try {
const ext = deviceStore . getExtension ( readFn , hostId ) ;
catalogId = ext ? . catalog _id || cfg . catalog _id || undefined ;
} catch ( _ ) {
catalogId = cfg . catalog _id || undefined ;
}
targets . push ( {
device _id : hostId ,
hostname : cfg . host || cfg . hostname ,
port : cfg . port || 22 ,
username : cfg . username ,
password : cfg . password ,
privateKeyContent : cfg . privateKey || cfg . privateKeyContent ,
passphrase : cfg . passphrase ,
catalog _id : catalogId ,
} ) ;
}
if ( targets . length === 0 ) {
console . warn ( "[reportScheduler] 无有效目标设备,跳过快照" ) ;
return ;
}
console . log ( ` [reportScheduler] 主进程执行定时快照,目标 ${ targets . length } 台 ` ) ;
2026-10-21 17:13:21 +08:00
_isSnapshotRunning = true ;
2026-10-15 13:53:04 +08:00
try {
await _executeSnapshot ( targets , trigger ) ;
} catch ( e ) {
console . error ( "[reportScheduler] 定时快照执行失败:" , e ? . message || String ( e ) ) ;
2026-10-21 17:13:21 +08:00
} finally {
_isSnapshotRunning = false ;
2026-10-15 13:53:04 +08:00
}
}
2026-10-15 15:30:31 +08:00
// 自动启动 reportScheduler( Electron 进程生命周期内只启一次)
2026-10-15 13:53:04 +08:00
try {
const cfg = reportScheduleStore . getSchedule ( readFn ) ;
2026-10-15 15:30:31 +08:00
if ( cfg ? . enabled && ! reportScheduler . isRunning ( ) ) {
2026-10-15 13:53:04 +08:00
reportScheduler . start ( {
readStorage : readFn ,
writeStorage : writeFn ,
runSnapshotFn : ( deviceIds , trigger ) => _runScheduledSnapshot ( deviceIds , trigger ) ,
2026-10-15 14:15:16 +08:00
getAllDeviceIdsFn : ( ) => Object . keys ( _hostConfigs || { } ) ,
2026-10-15 13:53:04 +08:00
} ) ;
}
} catch ( e ) {
2026-10-15 15:30:31 +08:00
console . error ( "[Inspection] reportScheduler 自动启动失败:" , e ? . message || String ( e ) ) ;
2026-10-15 13:53:04 +08:00
}
2026-10-15 14:15:16 +08:00
return {
getScheduler : ( ) => _scheduler ,
createScheduler : ( opts ) => {
_scheduler = new InspectionScheduler ( {
readFn ,
writeFn ,
execFn : execCommand ,
onAlertChanged : broadcastAlertChanged ,
... opts ,
} ) ;
return _scheduler ;
} ,
} ;
2026-10-15 13:53:04 +08:00
}
module . exports = {
registerInspectionHandlers ,
getLabels ,
} ;