Initial commit: NetMesh smart ops platform with 3D topology, inspection reports, RCA and AI ops assistant
Some checks failed
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
Some checks failed
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
This commit is contained in:
462
infrastructure/inspection/inspectionApi.ts
Normal file
462
infrastructure/inspection/inspectionApi.ts
Normal file
@@ -0,0 +1,462 @@
|
||||
/**
|
||||
* 巡检模块 - 前端 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<AlertLevel, string>;
|
||||
device_tiers: Record<DeviceTier, string>;
|
||||
inspection_status: Record<InspectionStatus, string>;
|
||||
alert_record_status: Record<AlertRecordStatus, string>;
|
||||
operators: Record<Operator, string>;
|
||||
device_catalog: Record<string, string>;
|
||||
metrics: Record<string, string>;
|
||||
}
|
||||
|
||||
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<string, any>;
|
||||
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;
|
||||
|
||||
function isAvailable(): boolean {
|
||||
return !!api;
|
||||
}
|
||||
|
||||
function invoke(name: string, ...args: any[]): Promise<any> {
|
||||
if (!api || typeof api[name] !== "function") {
|
||||
console.warn(`[Inspection] api.${name} 不可用`);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return api[name](...args);
|
||||
}
|
||||
|
||||
// ---------- 标签 ----------
|
||||
export const inspectionLabelsApi = {
|
||||
isAvailable,
|
||||
getLabels: (): Promise<InspectionLabels> => invoke("getLabels"),
|
||||
};
|
||||
|
||||
// ---------- 系统配置 ----------
|
||||
export const inspectionSysConfigApi = {
|
||||
get: (): Promise<Record<string, any>> => invoke("getSysConfig"),
|
||||
update: (updates: Record<string, any>) => invoke("updateSysConfig", updates),
|
||||
};
|
||||
|
||||
// ---------- 设备类型 ----------
|
||||
export const inspectionCatalogApi = {
|
||||
list: (): Promise<DeviceCatalog[]> => invoke("getCatalog"),
|
||||
getTiers: (): Promise<DeviceTierInfo[]> => invoke("getTiers"),
|
||||
getAdapters: (): Promise<string[]> => invoke("getAdapters"),
|
||||
};
|
||||
|
||||
// ---------- 设备扩展 ----------
|
||||
export const inspectionDeviceApi = {
|
||||
getAll: (): Promise<Record<string, DeviceExtension>> => invoke("getAllExtensions"),
|
||||
get: (hostId: string): Promise<DeviceExtension | null> => invoke("getExtension", hostId),
|
||||
upsert: (hostId: string, data: Partial<DeviceExtension>): Promise<DeviceExtension> =>
|
||||
invoke("upsertExtension", hostId, data),
|
||||
remove: (hostId: string): Promise<boolean> => invoke("deleteExtension", hostId),
|
||||
getEffectiveInterval: (hostId: string): Promise<number> =>
|
||||
invoke("getEffectiveInterval", hostId),
|
||||
getEnabled: (): Promise<DeviceExtension[]> => invoke("getEnabledDevices"),
|
||||
};
|
||||
|
||||
// ---------- 告警规则 ----------
|
||||
export const inspectionAlertRuleApi = {
|
||||
list: (): Promise<AlertRule[]> => invoke("listRules"),
|
||||
get: (ruleId: string): Promise<AlertRule | null> => invoke("getRule", ruleId),
|
||||
create: (data: Partial<AlertRule>): Promise<AlertRule> => invoke("createRule", data),
|
||||
update: (ruleId: string, updates: Partial<AlertRule>): Promise<AlertRule | null> =>
|
||||
invoke("updateRule", ruleId, updates),
|
||||
remove: (ruleId: string): Promise<boolean> => invoke("deleteRule", ruleId),
|
||||
toggle: (ruleId: string, enabled: boolean): Promise<AlertRule | null> =>
|
||||
invoke("toggleRule", ruleId, enabled),
|
||||
};
|
||||
|
||||
// ---------- 巡检结果 ----------
|
||||
export const inspectionResultApi = {
|
||||
getLatestAll: (): Promise<InspectionResult[]> => invoke("getLatestAll"),
|
||||
getLatestByDevice: (hostId: string, limit = 20): Promise<InspectionResult[]> =>
|
||||
invoke("getLatestByDevice", hostId, limit),
|
||||
getRecent: (limit = 50): Promise<InspectionResult[]> =>
|
||||
invoke("getRecentResults", limit),
|
||||
};
|
||||
|
||||
// ---------- 告警记录 ----------
|
||||
export const inspectionAlertApi = {
|
||||
list: (options?: {
|
||||
status?: AlertRecordStatus;
|
||||
level?: AlertLevel;
|
||||
device_id?: string;
|
||||
limit?: number;
|
||||
}): Promise<AlertRecord[]> => invoke("listAlerts", options),
|
||||
getActive: (): Promise<AlertRecord[]> => invoke("getActiveAlerts"),
|
||||
getStats: (): Promise<AlertStats> => invoke("getAlertStats"),
|
||||
acknowledge: (alertId: string, operator = "system"): Promise<AlertRecord | null> =>
|
||||
invoke("acknowledgeAlert", alertId, operator),
|
||||
resolve: (
|
||||
alertId: string,
|
||||
reason = "manual",
|
||||
operator = "system",
|
||||
): Promise<AlertRecord | null> =>
|
||||
invoke("resolveAlert", alertId, reason, operator),
|
||||
close: (alertId: string, note = ""): Promise<AlertRecord | null> =>
|
||||
invoke("closeAlert", alertId, note),
|
||||
updateNote: (alertId: string, note: string): Promise<AlertRecord | null> =>
|
||||
invoke("updateAlertNote", alertId, note),
|
||||
};
|
||||
|
||||
// ---------- 调度器 ----------
|
||||
// SSHConfig 是用于 SSH 连接的主机配置(hostname/port/username/password/privateKey 等)
|
||||
export type SshHostConfig = Record<string, unknown>;
|
||||
|
||||
export const inspectionSchedulerApi = {
|
||||
start: (hostConfigs?: Record<string, SshHostConfig>): Promise<SchedulerStatus> =>
|
||||
invoke("schedulerStart", hostConfigs ? { hostConfigs } : {}),
|
||||
stop: (): Promise<SchedulerStatus> => invoke("schedulerStop"),
|
||||
status: (): Promise<SchedulerStatus> => invoke("schedulerStatus"),
|
||||
scanNow: (hostId: string): Promise<any> => invoke("scanNow", hostId),
|
||||
scanAllNow: (): Promise<any> => invoke("scanAllNow"),
|
||||
};
|
||||
|
||||
// ---------- 单次巡检执行 ----------
|
||||
export interface RunInspectionResult {
|
||||
success: boolean;
|
||||
status: string;
|
||||
metrics: Record<string, any>;
|
||||
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<RunInspectionResult> =>
|
||||
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<RunReportSnapshotResult> =>
|
||||
invoke("runReportSnapshot", { deviceTargets, trigger }),
|
||||
|
||||
/** 查询主进程当前是否有快照正在执行(页面切换后查状态用) */
|
||||
isSnapshotRunning: (): Promise<{ running: boolean }> =>
|
||||
invoke("isReportSnapshotRunning"),
|
||||
|
||||
/**
|
||||
* 列出最近 N 条快照(新的在前)
|
||||
*/
|
||||
listSnapshots: (limit = 50): Promise<ReportSnapshot[]> =>
|
||||
invoke("listReportSnapshots", limit),
|
||||
|
||||
/**
|
||||
* 删除一条快照
|
||||
*/
|
||||
deleteSnapshot: (id: string): Promise<boolean> =>
|
||||
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<string, number>;
|
||||
aiSummaryEnabled: boolean;
|
||||
}
|
||||
|
||||
export const inspectionReportScheduleApi = {
|
||||
get: (): Promise<ReportSchedule> => invoke("getReportSchedule"),
|
||||
save: (config: Partial<ReportSchedule>): Promise<ReportSchedule> =>
|
||||
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<string, number>;
|
||||
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<DailyReport[]> => 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<boolean> => 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),
|
||||
};
|
||||
329
infrastructure/inspection/reportSummary.ts
Normal file
329
infrastructure/inspection/reportSummary.ts
Normal file
@@ -0,0 +1,329 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* 巡检报告 AI 汇总
|
||||
*
|
||||
* 复用 RCA runner 的 AI provider 读取(localStorage netcatty_ai_providers_v1)
|
||||
* 通过 window.netcatty.aiFetch bridge 发 HTTP 请求到用户配置的 LLM endpoint。
|
||||
*
|
||||
* Prompt 四段式输出,JSON 结构化:
|
||||
* { health_summary, anomalies[], trend_notes[], recommendations[] }
|
||||
*/
|
||||
|
||||
type ProviderInfo = {
|
||||
providerId: string;
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
model: string;
|
||||
style: "openai" | "google" | "anthropic";
|
||||
};
|
||||
|
||||
const AI_PROVIDERS_KEY = "netcatty_ai_providers_v1";
|
||||
const AI_ACTIVE_PROVIDER_KEY = "netcatty_ai_active_provider_v1";
|
||||
|
||||
async function resolveProvider(): Promise<ProviderInfo | null> {
|
||||
let providers: any[] | null = null;
|
||||
let activeId: string | null = null;
|
||||
try {
|
||||
const raw = localStorage.getItem(AI_PROVIDERS_KEY);
|
||||
if (raw) providers = JSON.parse(raw);
|
||||
activeId = localStorage.getItem(AI_ACTIVE_PROVIDER_KEY);
|
||||
} catch (_) {}
|
||||
if (!Array.isArray(providers) || providers.length === 0) return null;
|
||||
|
||||
let primary: any = null;
|
||||
if (activeId) primary = providers.find((p) => p.id === activeId);
|
||||
if (!primary) primary = providers.find((p) => p.enabled !== false);
|
||||
if (!primary?.apiKey) return null;
|
||||
|
||||
// 同步 providers 到主进程(RCA 路径也需要)
|
||||
const api = (window as any).netcatty;
|
||||
if (api?.aiSyncProviders) {
|
||||
try { await api.aiSyncProviders(providers); } catch (_) {}
|
||||
}
|
||||
|
||||
const lower = (primary.baseURL || primary.providerId || "").toLowerCase();
|
||||
const style = /anthropic|claude/.test(lower) ? "anthropic"
|
||||
: /google|gemini/.test(lower) ? "google"
|
||||
: "openai";
|
||||
|
||||
const suffix = style === "anthropic" ? "/v1/messages"
|
||||
: style === "google" ? "/v1beta/models"
|
||||
: "/chat/completions";
|
||||
|
||||
return {
|
||||
providerId: primary.id || "default",
|
||||
apiKey: primary.apiKey,
|
||||
baseUrl: (primary.baseURL || "").replace(/\/$/, "") + suffix,
|
||||
model: primary.defaultModel || "gpt-4o-mini",
|
||||
style,
|
||||
};
|
||||
}
|
||||
|
||||
async function aiChatCompletion(
|
||||
provider: ProviderInfo,
|
||||
systemPrompt: string,
|
||||
userContent: string,
|
||||
): Promise<string> {
|
||||
const api = (window as any).netcatty;
|
||||
if (!api?.aiFetch) throw new Error("aiFetch bridge unavailable");
|
||||
|
||||
const b = (window as any).netcatty;
|
||||
if (b?.aiAllowlistAddHost) {
|
||||
try { await b.aiAllowlistAddHost(provider.baseUrl); } catch (_) {}
|
||||
}
|
||||
|
||||
let url = provider.baseUrl;
|
||||
const body: any = {
|
||||
model: provider.model,
|
||||
temperature: 0.3,
|
||||
};
|
||||
|
||||
// 用 placeholder — 主进程 injectApiKeyIntoRequest 会根据 providerId 解密注入真实 key
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer __IPC_SECURED__",
|
||||
};
|
||||
|
||||
if (provider.style === "google") {
|
||||
url = `${provider.baseUrl}/${provider.model}:generateContent`;
|
||||
delete headers.Authorization;
|
||||
headers["x-goog-api-key"] = "__IPC_SECURED__";
|
||||
body.contents = [{
|
||||
role: "user",
|
||||
parts: [{ text: `${systemPrompt}\n\n${userContent}` }],
|
||||
}];
|
||||
} else if (provider.style === "anthropic") {
|
||||
headers["x-api-key"] = "__IPC_SECURED__";
|
||||
headers["anthropic-version"] = "2023-06-01";
|
||||
delete headers.Authorization;
|
||||
body.model = provider.model;
|
||||
body.system = systemPrompt;
|
||||
body.messages = [{ role: "user", content: userContent }];
|
||||
body.max_tokens = 1024;
|
||||
} else {
|
||||
body.messages = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: userContent },
|
||||
];
|
||||
}
|
||||
|
||||
// timeoutMs=60s — prompt 已压缩(只给失败+resource/log 输出),正常 30s 内返回
|
||||
const resp: any = await api.aiFetch(url, "POST", headers, JSON.stringify(body), provider.providerId, true, false, false, 60000);
|
||||
if (!resp?.ok) {
|
||||
const msg = resp?.error ? `${resp.error} (HTTP ${resp.status})` : `HTTP ${resp?.status || "?"}`;
|
||||
throw new Error(`AI 调用失败: ${msg}`);
|
||||
}
|
||||
|
||||
const data = resp.data ? JSON.parse(resp.data) : {};
|
||||
if (provider.style === "google") {
|
||||
return data?.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
||||
} else if (provider.style === "anthropic") {
|
||||
return data?.content?.[0]?.text || "";
|
||||
}
|
||||
return data?.choices?.[0]?.message?.content || "";
|
||||
}
|
||||
|
||||
function extractJson(text: string): any {
|
||||
// 去掉 ```json ... ``` 包裹
|
||||
let t = text.trim();
|
||||
t = t.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; }
|
||||
}
|
||||
|
||||
// ─── Prompt ────────────────────────────────────
|
||||
|
||||
const SYSTEM_PROMPT = `你是一名资深运维工程师,负责汇总服务器/交换机/防火墙的巡检报告。
|
||||
数据中提供了每台设备执行各类巡检命令的原始输出,请观察这些细节数据,识别隐藏风险和运行状态。
|
||||
|
||||
输出 JSON:
|
||||
|
||||
{
|
||||
"health_summary": "一句话整体评估(X 台在线 / Y 台离线,命令成功率 %,关键发现一句话)",
|
||||
"offline_devices": [ { "device": "hostname", "reason": "SSH连接超时|认证失败" } ],
|
||||
"device_status": [
|
||||
{
|
||||
"device": "hostname",
|
||||
"catalog": "huawei_h3c_switch",
|
||||
"running_ok": true,
|
||||
"highlights": ["关键观察1", "关键观察2"],
|
||||
"issues": ["失败的命令或隐藏风险"]
|
||||
}
|
||||
],
|
||||
"hidden_risks": [
|
||||
{ "device": "hostname", "observation": "具体数值和异常点", "severity": "critical|warning|info" }
|
||||
],
|
||||
"recommendations": ["按优先级排序的处置建议,必须具体到设备和操作"]
|
||||
}
|
||||
|
||||
分析要点:
|
||||
- 观察温度数值:正常交换机 40-55°C,超过 60°C 值得关注
|
||||
- 观察 CPU/内存使用率:单台持续 >75% 要标 warning
|
||||
- 观察风扇转速:异常偏高(如 4500 RPM 但正常 3000)可能预示轴承磨损
|
||||
- 观察端口状态:CRC error、input error 非零 = 物理链路问题
|
||||
- 观察 NTP:stratum 变化、peer 切换频繁 = 时钟源不稳定
|
||||
- 观察 display logbuffer:找出 WARNING/CRITICAL 级别的日志条目作为隐藏风险
|
||||
- vrrp/stp 角色异常(非预期的 Master/Backup 切换、指定 STP 阻塞端口)= 网络拓扑变更
|
||||
- Linux 服务器的 load average / /proc/meminfo / ip link 状态也要关注
|
||||
- Linux journalctl -p warning 输出的是 warning 以上日志,逐条解读
|
||||
|
||||
hidden_risks 用于放那些"命令没失败但数值异常"的情况(如 display temperature 成功但读到 65°C)
|
||||
device_status 用于列出每台设备的简要运行概览(亮点 + 问题)
|
||||
严格输出合法 JSON,不要额外解释`;
|
||||
|
||||
/**
|
||||
* 按命令类型分层保留 stdout:
|
||||
* 基础信息/资源指标 → 完整保留(短,是核心)
|
||||
* 网络状态 → 前 800 字
|
||||
* 日志类 → 最后 30 行(tail)
|
||||
* 失败命令 → 完整保留错误输出
|
||||
*/
|
||||
function _classifyCmd(cmdName: string): "base" | "resource" | "network" | "log" {
|
||||
const n = cmdName.toLowerCase();
|
||||
if (/version|device|hostname|uptime|uname/.test(n)) return "base";
|
||||
if (/memory|cpu|temperature|fan|power|free -m|vmstat|top -bn1|journalctl -p/.test(n)) return "resource";
|
||||
if (/interface|arp|vlan|stp|vrrp|ntp|route|neigh|ip -br|ip link|ip addr/.test(n)) return "network";
|
||||
if (/logbuffer|journalctl|dmesg|messages|syslog|show log/.test(n)) return "log";
|
||||
return "network"; // 未知默认中等长度
|
||||
}
|
||||
|
||||
function _truncateStdout(cmdName: string, stdout: string): string {
|
||||
if (!stdout) return "";
|
||||
const cls = _classifyCmd(cmdName);
|
||||
if (cls === "log") {
|
||||
// 日志取最后 30 行
|
||||
const lines = stdout.split("\n");
|
||||
if (lines.length > 30) return lines.slice(-30).join("\n");
|
||||
return stdout;
|
||||
}
|
||||
if (cls === "network") {
|
||||
return stdout.slice(0, 800);
|
||||
}
|
||||
// base / resource 完整保留(一般 < 30 行)
|
||||
return stdout.slice(0, 2000); // 兜底防极端
|
||||
}
|
||||
|
||||
function buildSnapshotUserContext(snapshot: any): string {
|
||||
const offlineDevices: any[] = [];
|
||||
const onlineDevices: any[] = [];
|
||||
|
||||
for (const d of snapshot.devices) {
|
||||
const allFail = d.commands.every((c: any) => !c.ok);
|
||||
const isConnectionErr = d.commands.every((c: any) =>
|
||||
/Connection refused|ETIMEDOUT|ECONNREFUSED|connect failed|authentication failed|SSH.*failed/i.test(c.stderr || c.stdout || "")
|
||||
);
|
||||
const isOffline = !d.ok || (allFail && isConnectionErr);
|
||||
|
||||
if (isOffline) {
|
||||
const reason = d.commands[0]?.stderr?.slice(0, 200)
|
||||
|| d.commands[0]?.stdout?.slice(0, 200)
|
||||
|| "SSH连接失败";
|
||||
offlineDevices.push({ device: d.hostname, reason });
|
||||
} else {
|
||||
// 在线设备:成功设备只给概览,只给失败命令 + 资源/日志关键命令的截断 stdout
|
||||
const detailCmds: any[] = [];
|
||||
let hasFail = false;
|
||||
for (const c of d.commands) {
|
||||
if (!c.ok) {
|
||||
hasFail = true;
|
||||
detailCmds.push({
|
||||
cmd: c.cmd, purpose: c.purpose, status: "fail",
|
||||
output: (c.stdout || c.stderr || "").slice(0, 600),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
// 成功命令:只保留 resource(CPU/温度/风扇)和 log 类的截断输出
|
||||
const cls = _classifyCmd(c.cmd);
|
||||
if (cls === "resource") {
|
||||
detailCmds.push({
|
||||
cmd: c.cmd, purpose: c.purpose, status: "ok",
|
||||
output: (c.stdout || "").slice(0, 400),
|
||||
});
|
||||
} else if (cls === "log") {
|
||||
detailCmds.push({
|
||||
cmd: c.cmd, purpose: c.purpose, status: "ok",
|
||||
output: _truncateStdout(c.cmd, c.stdout || ""),
|
||||
});
|
||||
}
|
||||
// base / network 类成功命令 → **完全不塞**
|
||||
}
|
||||
|
||||
onlineDevices.push({
|
||||
device: d.hostname,
|
||||
catalog: d.catalog_id,
|
||||
status: hasFail ? "partial_fail" : "ok",
|
||||
command_total: d.commands.length,
|
||||
command_failed: d.commands.filter((c: any) => !c.ok).length,
|
||||
commands: detailCmds, // 只有失败命令 + resource/log 成功命令
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 总体统计
|
||||
const totalOnline = onlineDevices.length;
|
||||
const totalOffline = offlineDevices.length;
|
||||
const totalCmds = onlineDevices.reduce((a, d) => a + (d as any).command_total, 0);
|
||||
const failCmds = onlineDevices.reduce((a, d) => a + (d as any).command_failed, 0);
|
||||
|
||||
return JSON.stringify({
|
||||
timestamp: new Date(snapshot.timestamp).toISOString(),
|
||||
trigger: snapshot.trigger,
|
||||
duration_ms: snapshot.duration_ms,
|
||||
offline_count: totalOffline,
|
||||
online_count: totalOnline,
|
||||
total_commands: totalCmds,
|
||||
failed_commands: failCmds,
|
||||
offline_devices: offlineDevices,
|
||||
online_devices: onlineDevices,
|
||||
}, null, 2);
|
||||
}
|
||||
|
||||
function buildDailyUserContext(report: any): string {
|
||||
const s = report.summary;
|
||||
const failDevices = Object.entries(s.deviceSuccessRate)
|
||||
.filter(([, r]) => (r as number) < 0.95)
|
||||
.map(([id, r]) => ({ device: id, successRate: r }));
|
||||
return JSON.stringify({
|
||||
date: report.date,
|
||||
mode: report.mode,
|
||||
snapshotCount: s.snapshotCount,
|
||||
deviceCount: s.deviceCount,
|
||||
totalCmds: s.totalCmds,
|
||||
failRate: s.failRate,
|
||||
failingDevices: failDevices,
|
||||
topFailedCommands: s.topFailedCommands.slice(0, 10),
|
||||
}, null, 2);
|
||||
}
|
||||
|
||||
// ─── 主入口 ────────────────────────────────────
|
||||
|
||||
export async function summarizeSnapshot(snapshot: any): Promise<any> {
|
||||
const provider = await resolveProvider();
|
||||
if (!provider) {
|
||||
throw new Error("AI provider 未配置,请在设置→AI 页面添加 provider");
|
||||
}
|
||||
const content = buildSnapshotUserContext(snapshot);
|
||||
console.log("[ReportSummary] summarizeSnapshot:", {
|
||||
providerId: provider.providerId,
|
||||
model: provider.model,
|
||||
devices: snapshot?.devices?.length,
|
||||
contentLen: content.length,
|
||||
});
|
||||
const t0 = Date.now();
|
||||
const raw = await aiChatCompletion(provider, SYSTEM_PROMPT, content);
|
||||
console.log("[ReportSummary] AI 返回,耗时:", (Date.now() - t0) / 1000, "s,返回长度:", raw.length);
|
||||
return extractJson(raw) || { health_summary: raw, anomalies: [], trend_notes: [], recommendations: [] };
|
||||
}
|
||||
|
||||
export async function summarizeDailyReport(report: any): Promise<any> {
|
||||
const provider = await resolveProvider();
|
||||
if (!provider) {
|
||||
throw new Error("AI provider 未配置,请在设置→AI 页面添加 provider");
|
||||
}
|
||||
const content = buildDailyUserContext(report);
|
||||
const raw = await aiChatCompletion(provider, SYSTEM_PROMPT, content);
|
||||
return extractJson(raw) || { health_summary: raw, anomalies: [], trend_notes: [], recommendations: [] };
|
||||
}
|
||||
Reference in New Issue
Block a user