/** * 巡检模块 - 前端 API 层 * * 通过 window.electronAPI.inspection 调用主进程的巡检 IPC handlers。 */ // ---------- 类型定义 ---------- export type AlertLevel = "critical" | "warning" | "info"; export type DeviceTier = "core" | "aggregation" | "access"; export type InspectionStatus = "success" | "failed" | "timeout"; export type AlertRecordStatus = "active" | "acknowledged" | "resolved" | "closed"; export type Operator = ">" | "<" | "==" | "!=" | ">=" | "<="; export interface InspectionLabels { alert_levels: Record; device_tiers: Record; inspection_status: Record; alert_record_status: Record; operators: Record; device_catalog: Record; metrics: Record; } export interface DeviceCatalog { id: string; type_key: string; name: string; vendor: string; adapter_class: string; supported_metrics: string[]; default_thresholds: AlertRule[]; default_interval: number; description: string; preset: boolean; } export interface DeviceTierInfo { key: DeviceTier; label: string; default_interval: number; } export interface DeviceExtension { host_id: string; /** 主机名(IP 或域名),用于调度器匹配 vault SSH 配置 */ hostname?: string | null; catalog_id: string; tier: DeviceTier | null; inspection_interval: number | null; inspection_enabled: boolean; custom_thresholds: AlertRule[]; inspection_tags: string[]; last_scan_at: number | null; last_scan_status: InspectionStatus | null; last_scan_error: string | null; created_at: number; updated_at: number; effective_interval?: number; } export interface AlertRule { id: string; name: string; metric: string; operator: Operator; threshold: string; level: AlertLevel; duration: number; scope_type: "all" | "tier" | "device" | "catalog"; scope_value: string | string[] | null; enabled: boolean; preset?: boolean; created_at: number; updated_at: number; } export interface InspectionResult { id: string; job_id: string | null; device_id: string; timestamp: number; online: boolean; status: InspectionStatus; duration_ms: number; metrics: Record; alerts: any[]; error_message: string | null; } export interface AlertRecord { id: string; rule_id: string; rule_name: string; device_id: string; device_name: string; metric: string; metric_value: string; threshold: string; level: AlertLevel; message: string; triggered_at: number; status: AlertRecordStatus; acknowledged_by: string | null; acknowledged_at: number | null; resolved_at: number | null; resolved_reason: string | null; note: string | null; } export interface AlertStats { total: number; active: number; active_critical: number; active_warning: number; active_info: number; acknowledged: number; resolved: number; closed: number; } export interface SchedulerStatus { running: boolean; queue_size: number; active_scans: number; concurrency: number; } // ---------- API 调用 ---------- const api = (window as any).netcatty?.inspection; // 浏览器远程访问环境:通过 HTTP API fallback const IS_REMOTE_BROWSER = typeof window !== "undefined" && !!(window as any).__REMOTE_TOKEN__; const REMOTE_TOKEN = (window as any).__REMOTE_TOKEN__ || ""; const REMOTE_API_MAP: Record = { getLatestAll: { method: "GET", path: "/api/inspection/results", arrayField: "results" }, getLatestByDevice: { method: "GET", path: "/api/inspection/results", arrayField: "results" }, getRecentResults: { method: "GET", path: "/api/inspection/results/recent", arrayField: "results" }, getResultsByTimeRange: { method: "GET", path: "/api/inspection/results/range", arrayField: "results" }, getActiveAlerts: { method: "GET", path: "/api/alerts", arrayField: "alerts" }, listAlerts: { method: "GET", path: "/api/alerts", arrayField: "alerts" }, getAlertStats: { method: "GET", path: "/api/alert-stats" }, listRules: { method: "GET", path: "/api/alert-rules", arrayField: "rules" }, getRule: { method: "GET", path: "/api/alert-rules", arrayField: "rules" }, createRule: { method: "POST", path: "/api/alert-rule" }, updateRule: { method: "POST", path: "/api/alert-rule" }, deleteRule: { method: "POST", path: "/api/alert-rule" }, toggleRule: { method: "POST", path: "/api/alert-rule" }, getAllExtensions: { method: "GET", path: "/api/devices", arrayField: "devices" }, getEnabledDevices: { method: "GET", path: "/api/devices/enabled", arrayField: "devices" }, schedulerStatus: { method: "GET", path: "/api/inspection/scheduler-status" }, getCatalog: { method: "GET", path: "/api/inspection/catalog", arrayField: "catalog" }, getSysConfig: { method: "GET", path: "/api/inspection/sys-config" }, updateSysConfig: { method: "POST", path: "/api/inspection/sys-config" }, getLabels: { method: "GET", path: "/api/inspection/labels" }, listReportSnapshots: { method: "GET", path: "/api/report/snapshots", arrayField: "snapshots" }, isReportSnapshotRunning: { method: "GET", path: "/api/report/snapshot-status" }, getReportSchedule: { method: "GET", path: "/api/report/schedule" }, saveReportSchedule: { method: "POST", path: "/api/report/schedule" }, toggleReportScheduler: { method: "POST", path: "/api/report/scheduler-toggle" }, reportSchedulerStatus: { method: "GET", path: "/api/report/scheduler-status" }, listDailyReports: { method: "GET", path: "/api/daily-reports", arrayField: "reports" }, batchSetInspectionEnabled: { method: "POST", path: "/api/devices/batch-enable" }, runReportAiSummary: { method: "POST", path: "/api/run-report-ai-summary" }, generateDailyReport: { method: "POST", path: "/api/generate-daily-report" }, acknowledgeAlert: { method: "POST", path: "/api/alert/__ID__/acknowledge" }, resolveAlert: { method: "POST", path: "/api/alert/__ID__/resolve" }, closeAlert: { method: "POST", path: "/api/alert/__ID__/close" }, updateAlertNote: { method: "POST", path: "/api/alert/__ID__/note" }, }; // 某些 API 返回数组 —— 在 REMOTE_API_MAP 里用 arrayField 标记 // 找不到映射的 API 默认返回 null,让调用方自己兜底 const ARRAY_METHODS = new Set([ "listReportSnapshots", "listDailyReports", "listRules", "getLatestAll", "getLatestByDevice", "getRecentResults", "getResultsByTimeRange", "getActiveAlerts", "listAlerts", "getAllExtensions", "getEnabledDevices", "getCatalog", ]); async function remoteFetch(name: string, ...args: any[]): Promise { const mapping = REMOTE_API_MAP[name]; if (!mapping) { // 没有映射的方法:读场景返回空数组/空对象兜底,避免 .find 崩溃 console.warn(`[Inspection] remote fallback: no mapping for ${name}, returning safe default`); if (ARRAY_METHODS.has(name)) return []; if (name === "getSchedule" || name === "getReportSchedule") return {}; if (name === "getSysConfig") return {}; if (name === "getLabels") return { alert_levels: {}, device_tiers: {}, inspection_status: {}, alert_record_status: {}, operators: {}, device_catalog: {}, metrics: {} }; if (name === "schedulerStatus" || name === "isReportSnapshotRunning") return { running: false }; return null; } let url = mapping.path; // 告警状态变更:把 __ID__ 替换成实际 alertId if (url.includes("__ID__") && args[0]) { url = url.replace("__ID__", encodeURIComponent(args[0])); args = args.slice(1); // alertId 从参数里移除,剩下的是 payload } url += `?token=${encodeURIComponent(REMOTE_TOKEN)}`; if (name === "getLatestByDevice" && args[0]) { url += `&device_id=${encodeURIComponent(args[0])}`; } if (name === "getResultsByTimeRange" && args[0] !== undefined && args[1] !== undefined) { url += `&start=${encodeURIComponent(args[0])}&end=${encodeURIComponent(args[1])}`; } if ((name === "listAlerts" || name === "listReportSnapshots") && args[0] !== undefined) { const opts = typeof args[0] === "object" ? args[0] : { limit: args[0] }; if (opts.status) url += `&status=${opts.status}`; if (opts.level) url += `&level=${opts.level}`; if (opts.device_id) url += `&device_id=${opts.device_id}`; if (opts.limit) url += `&limit=${opts.limit}`; } else if (args[0] !== undefined && name !== "getRule") { url += `&limit=${encodeURIComponent(args[0])}`; } try { const fetchOpts: RequestInit = {}; if (mapping.method === "POST") { fetchOpts.method = "POST"; fetchOpts.headers = { "Content-Type": "application/json" }; if (name === "batchSetInspectionEnabled") { fetchOpts.body = JSON.stringify({ enabled: !!args[0] }); } else if (name === "runReportAiSummary") { // invoke("runReportAiSummary", snapshotId, providerId) fetchOpts.body = JSON.stringify({ snapshotId: args[0], providerId: args[1] }); } else if (name === "generateDailyReport") { // invoke("generateDailyReport", options) fetchOpts.body = JSON.stringify(args[0] || {}); } else if (name === "createRule") { fetchOpts.body = JSON.stringify({ action: "create", data: args[0] || {} }); } else if (name === "updateRule") { fetchOpts.body = JSON.stringify({ action: "update", id: args[0], updates: args[1] || {} }); } else if (name === "deleteRule") { fetchOpts.body = JSON.stringify({ action: "delete", id: args[0] }); } else if (name === "toggleRule") { fetchOpts.body = JSON.stringify({ action: "toggle", id: args[0], enabled: !!args[1] }); } else if (name === "toggleReportScheduler") { fetchOpts.body = JSON.stringify({ enabled: !!args[0] }); } else if (name === "acknowledgeAlert") { fetchOpts.body = JSON.stringify({ operator: args[0] || "remote" }); } else if (name === "resolveAlert") { fetchOpts.body = JSON.stringify({ reason: args[0] || "manual", operator: args[1] || "remote" }); } else if (name === "closeAlert") { fetchOpts.body = JSON.stringify({ note: args[0] || "" }); } else if (name === "updateAlertNote") { fetchOpts.body = JSON.stringify({ note: args[0] || "" }); } else { fetchOpts.body = JSON.stringify(args[0] !== undefined ? args[0] : {}); } } const res = await fetch(url, fetchOpts); const data = await res.json(); // 适配不同返回结构 if (data && typeof data === "object") { if (Array.isArray(data)) return data; // getAllExtensions:必须把 devices 数组转成 Record if (name === "getAllExtensions" && Array.isArray(data.devices)) { const map: Record = {}; for (const d of data.devices) { if (d?.host_id) map[d.host_id] = d; } return map; } // reportSchedulerStatus: { running, schedule } if (name === "reportSchedulerStatus" && "running" in data) return data; // 优先用 arrayField 约定的字段(getAllExtensions 已在上文处理 Record 转换) if (mapping.arrayField && Array.isArray(data[mapping.arrayField])) { return data[mapping.arrayField]; } if (data.alerts !== undefined) return data.alerts; if (data.devices !== undefined) return data.devices; if (data.rules !== undefined) return data.rules; if (data.results !== undefined) return data.results; if (data.snapshots !== undefined) return data.snapshots; if (data.reports !== undefined) return data.reports; if (data.schedule !== undefined && (name === "getReportSchedule" || name === "saveReportSchedule")) return data.schedule; if (data.ok === true && data.data !== undefined) return data.data; if ("running" in data) return data; // schedulerStatus / snapshot-status return data; } return null; } catch (e) { console.warn(`[Inspection] remote fetch ${name} failed:`, e); // fetch 失败也返回安全默认值,不要让 .find 崩 if (ARRAY_METHODS.has(name)) return []; return null; } } function isAvailable(): boolean { return !!api || IS_REMOTE_BROWSER; } function invoke(name: string, ...args: any[]): Promise { if (api && typeof api[name] === "function") { return api[name](...args); } // Electron bridge 不可用 → 浏览器远程访问环境走 HTTP fallback if (IS_REMOTE_BROWSER) { return remoteFetch(name, ...args); } console.warn(`[Inspection] api.${name} 不可用`); return Promise.resolve(null); } // ---------- 标签 ---------- export const inspectionLabelsApi = { isAvailable, getLabels: (): Promise => invoke("getLabels"), }; // ---------- 系统配置 ---------- export const inspectionSysConfigApi = { get: (): Promise> => invoke("getSysConfig"), update: (updates: Record) => invoke("updateSysConfig", updates), }; // ---------- 设备类型 ---------- export const inspectionCatalogApi = { list: (): Promise => invoke("getCatalog"), getTiers: (): Promise => invoke("getTiers"), getAdapters: (): Promise => invoke("getAdapters"), }; // ---------- 设备扩展 ---------- export const inspectionDeviceApi = { getAll: (): Promise> => invoke("getAllExtensions"), get: (hostId: string): Promise => invoke("getExtension", hostId), upsert: (hostId: string, data: Partial): Promise => invoke("upsertExtension", hostId, data), batchSetEnabled: (enabled: boolean): Promise<{ ok: boolean; count: number; enabled: boolean; error?: string }> => invoke("batchSetInspectionEnabled", enabled), remove: (hostId: string): Promise => invoke("deleteExtension", hostId), getEffectiveInterval: (hostId: string): Promise => invoke("getEffectiveInterval", hostId), getEnabled: (): Promise => invoke("getEnabledDevices"), }; // ---------- 告警规则 ---------- export const inspectionAlertRuleApi = { list: (): Promise => invoke("listRules"), get: (ruleId: string): Promise => invoke("getRule", ruleId), create: (data: Partial): Promise => invoke("createRule", data), update: (ruleId: string, updates: Partial): Promise => invoke("updateRule", ruleId, updates), remove: (ruleId: string): Promise => invoke("deleteRule", ruleId), toggle: (ruleId: string, enabled: boolean): Promise => invoke("toggleRule", ruleId, enabled), }; // ---------- 巡检结果 ---------- export const inspectionResultApi = { getLatestAll: (): Promise => invoke("getLatestAll"), getLatestByDevice: (hostId: string, limit = 20): Promise => invoke("getLatestByDevice", hostId, limit), getRecent: (limit = 50): Promise => invoke("getRecentResults", limit), getByTimeRange: (startTime: number, endTime: number): Promise => invoke("getResultsByTimeRange", startTime, endTime), }; // ---------- 告警记录 ---------- export const inspectionAlertApi = { list: (options?: { status?: AlertRecordStatus; level?: AlertLevel; device_id?: string; limit?: number; }): Promise => invoke("listAlerts", options), getActive: (): Promise => invoke("getActiveAlerts"), getStats: (): Promise => invoke("getAlertStats"), acknowledge: (alertId: string, operator = "system"): Promise => invoke("acknowledgeAlert", alertId, operator), resolve: ( alertId: string, reason = "manual", operator = "system", ): Promise => invoke("resolveAlert", alertId, reason, operator), close: (alertId: string, note = ""): Promise => invoke("closeAlert", alertId, note), updateNote: (alertId: string, note: string): Promise => invoke("updateAlertNote", alertId, note), }; // ---------- 调度器 ---------- // SSHConfig 是用于 SSH 连接的主机配置(hostname/port/username/password/privateKey 等) export type SshHostConfig = Record; export const inspectionSchedulerApi = { start: (hostConfigs?: Record): Promise => invoke("schedulerStart", hostConfigs ? { hostConfigs } : {}), stop: (): Promise => invoke("schedulerStop"), status: (): Promise => invoke("schedulerStatus"), scanNow: (hostId: string): Promise => invoke("scanNow", hostId), scanAllNow: (): Promise => invoke("scanAllNow"), getAutoEnabled: (): Promise => invoke("getAutoEnabled"), setAutoEnabled: (enabled: boolean): Promise<{ enabled: boolean }> => invoke("setAutoEnabled", enabled), }; // ---------- 单次巡检执行 ---------- export interface RunInspectionResult { success: boolean; status: string; metrics: Record; error: string | null; duration: number; alerts: any[]; } export interface HostConfigForInspection { id: string; label?: string; hostname: string; port?: number; username: string; password?: string; privateKeyContent?: string; passphrase?: string; } export const inspectionExecApi = { /** * 执行一次完整巡检(前端传入完整主机配置) * @param hostId 主机 ID * @param hostConfig 主机连接配置 * @param catalogId 设备类型 ID(采集器 ID) */ runInspection: ( hostId: string, hostConfig: HostConfigForInspection, catalogId: string ): Promise => invoke("runInspection", { hostId, hostConfig, catalogId }), }; // =============== 巡检报告快照 =============== export interface ReportCmdResult { cmd: string; purpose: string; ok: boolean; duration_ms: number; stdout: string; stderr: string; } export interface ReportDeviceResult { device_id: string; hostname: string; catalog_id: string; ok: boolean; duration_ms: number; commands: ReportCmdResult[]; } export interface ReportSnapshot { id: string; timestamp: number; duration_ms: number; trigger: "manual" | "scheduled"; devices: ReportDeviceResult[]; /** AI 汇总结果(主进程自动生成后持久化到 JSON) */ ai_summary?: any; /** AI 汇总生成状态:running / done / failed */ ai_summary_status?: "running" | "done" | "failed"; /** AI 汇总失败时的错误信息 */ ai_summary_error?: string; /** AI 汇总完成时间戳 */ ai_summary_updated_at?: number; } export interface ReportDeviceTarget { device_id: string; hostname: string; port?: number; username: string; password?: string; privateKeyContent?: string; passphrase?: string; catalog_id?: string; } export interface RunReportSnapshotResult { ok: boolean; snapshot?: ReportSnapshot; error?: string; } /** 从 renderer localStorage 读报告产品名(优先报告专用 key,fallback 通用产品名) * 注意:useStoredString 的 writeString 直接存纯字符串,不 JSON.stringify, * 所以这里不能 JSON.parse,直接判断值类型即可。 */ function _getReportSystemName(): string | undefined { try { const reportRaw = localStorage.getItem("netcatty_report_product_name_v1"); if (typeof reportRaw === "string" && reportRaw.trim()) return reportRaw.trim(); const productRaw = localStorage.getItem("netcatty_product_name_v1"); if (typeof productRaw === "string" && productRaw.trim()) return productRaw.trim(); } catch (_) {} return undefined; } export const inspectionReportApi = { /** * 跑一次巡检报告快照 —— 所有 deviceTargets 独立 SSH 连设备执行各自 catalog 的命令组 */ runSnapshot: ( deviceTargets: ReportDeviceTarget[], trigger: "manual" | "scheduled" = "manual" ): Promise => invoke("runReportSnapshot", { deviceTargets, trigger }), /** 查询主进程当前是否有快照正在执行(页面切换后查状态用) */ isSnapshotRunning: (): Promise<{ running: boolean }> => invoke("isReportSnapshotRunning"), /** * 列出最近 N 条快照(新的在前) */ listSnapshots: (limit = 50): Promise => invoke("listReportSnapshots", limit), /** * 删除一条快照 */ deleteSnapshot: (id: string): Promise => invoke("deleteReportSnapshot", id), /** * 清空所有快照 */ clearSnapshots: (): Promise<{ ok: boolean }> => invoke("clearReportSnapshots"), /** * 导出快照 PDF * @param options.snapshotId 快照 ID * @param options.aiSummary 可选 AI 汇总(有则 PDF 含 AI 分析) * @param options.customPath 可选用户指定保存路径(下载按钮用) */ exportSnapshotPdf: (options: { snapshotId: string; aiSummary?: any; customPath?: string; }): Promise<{ ok: boolean; filePath?: string; error?: string }> => invoke("exportReportSnapshotPdf", { ...options, systemName: _getReportSystemName() }), /** 内嵌预览(Electron BrowserWindow + 内置 PDF viewer) */ previewSnapshotPdf: (snapshotId: string): Promise<{ ok: boolean; filePath?: string; error?: string }> => invoke("previewReportSnapshotPdf", { snapshotId, systemName: _getReportSystemName() }), /** 保存 AI 汇总到快照(持久化,切换页面不丢) */ saveAiSummary: (snapshotId: string, aiSummary: any): Promise<{ ok: boolean }> => invoke("saveReportAiSummary", { snapshotId, aiSummary }), /** 清空快照的 AI 汇总(用于重试前) */ clearAiSummary: (snapshotId: string): Promise<{ ok: boolean }> => invoke("clearReportAiSummary", { snapshotId }), /** 主进程执行 AI 汇总 —— 独立进程,页面切换不影响 */ runAiSummary: (snapshotId: string, providerId?: string): Promise<{ ok: boolean }> => invoke("runReportAiSummary", snapshotId, providerId), }; // =============== 巡检报告调度配置 =============== export interface ReportSchedule { enabled: boolean; defaultIntervalHours: number; dailyReportTime: string; // "HH:MM" enabledDeviceIds: string[] | null; // null=全部;[]=一个都不选;[id1]=指定 perDeviceInterval: Record; aiSummaryEnabled: boolean; } export const inspectionReportScheduleApi = { get: (): Promise => invoke("getReportSchedule"), save: (config: Partial): Promise => invoke("saveReportSchedule", config), toggleScheduler: (enabled: boolean): Promise<{ ok: boolean; running: boolean }> => invoke("toggleReportScheduler", enabled), status: (): Promise<{ running: boolean; schedule: ReportSchedule }> => invoke("reportSchedulerStatus"), }; // =============== 日运行报告 =============== export interface DailyReportSummary { snapshotCount: number; deviceCount: number; totalCmds: number; okCmds: number; failCmds: number; failRate: number; deviceSuccessRate: Record; topFailedCommands: Array<{ device_id: string; cmd: string; count: number }>; } export interface DailyReport { id: string; date: string; // "YYYY-MM-DD" mode: "provisional" | "finalized"; timestamp: number; trigger: "manual" | "scheduled"; period: { start_ts: number; end_ts: number }; summary: DailyReportSummary; aiSummary: any | null; } export const inspectionDailyReportApi = { list: (limit = 30): Promise => invoke("listDailyReports", limit), generate: (options?: { mode?: "provisional" | "finalized"; trigger?: "manual" | "scheduled"; }): Promise<{ ok: boolean; report?: DailyReport; error?: string }> => invoke("generateDailyReport", options || {}), delete: (id: string): Promise => invoke("deleteDailyReport", id), clear: (): Promise<{ ok: boolean }> => invoke("clearDailyReports"), exportPdf: (reportId: string): Promise<{ ok: boolean; filePath?: string; error?: string }> => invoke("exportDailyReportPdf", { reportId, systemName: _getReportSystemName() }), /** 内嵌预览(Electron BrowserWindow + 内置 PDF viewer) */ previewPdf: (reportId: string): Promise<{ ok: boolean; filePath?: string; error?: string }> => invoke("previewDailyReportPdf", { reportId, systemName: _getReportSystemName() }), }; export const inspectionSnapshotExportApi = { exportPdf: (snapshotId: string, aiSummary?: any): Promise<{ ok: boolean; filePath?: string; error?: string }> => invoke("exportReportSnapshotPdf", aiSummary ? { snapshotId, aiSummary } : snapshotId), };