Files
SenMeshNet/electron/bridges/mcpServerBridge/inspectionAgentHandlers.cjs
zhaolei 2f9f8da48e
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
Initial commit: NetMesh smart ops platform with 3D topology, inspection reports, RCA and AI ops assistant
2026-10-15 13:53:04 +08:00

520 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
/**
* Inspection domain handlers for AI agent tools (RCA + natural-language ops).
*
* Each handler matches a capability id declared in capabilities/catalog/inspection.cjs
* and is wired into the builtinRpcHandlerRegistry via registerInspectionHandlers().
*
* Storage:
* alertRecordStore and inspectionResultStore take (readFn, writeFn) — we resolve storage
* lazily on first call so this module can be require()d before the app fully initializes.
*/
const alertRecordStore = require("../inspection/alertRecordStore.cjs");
const inspectionResultStore = require("../inspection/inspectionResultStore.cjs");
const inspectionAlertRuleStore = require("../inspection/alertRuleStore.cjs");
const inspectionDeviceStore = require("../inspection/deviceStore.cjs");
// Lazy storage adapter — resolved from Electron's global bridge on first call.
let _storageBridge = null;
let _vaultBridge = null;
function getStorageBridge() {
if (!_storageBridge) {
try {
// 和 inspectionStorage.cjs 保持一致的文件名
const { app } = require("electron");
const path = require("path");
const fs = require("fs");
const userData = app.getPath("userData");
const storeFile = path.join(userData, "inspection-data.json");
_storageBridge = {
_storeFile: storeFile,
read: (key) => {
try {
if (!fs.existsSync(storeFile)) return null;
const raw = fs.readFileSync(storeFile, "utf-8");
const all = JSON.parse(raw || "{}");
return all[key] ?? null;
} catch (_) { return null; }
},
write: (key, value) => {
try {
let all = {};
if (fs.existsSync(storeFile)) {
all = JSON.parse(fs.readFileSync(storeFile, "utf-8") || "{}");
}
all[key] = value;
fs.writeFileSync(storeFile, JSON.stringify(all, null, 2), "utf-8");
} catch (e) {
console.error("[InspectionAgent] write failed:", e.message);
}
},
};
} catch (e) {
console.warn("[InspectionAgent] storage bridge not available:", e.message);
_storageBridge = { read: () => null, write: () => {} };
}
console.log("[InspectionAgent] storage file:", _storageBridge._storeFile);
}
return _storageBridge;
}
function getVaultBridge() {
if (!_vaultBridge) {
try {
const vaultStore = require("../../vault/hostStore.cjs");
_vaultBridge = vaultStore;
} catch (e) {
try {
_vaultBridge = require("../vault/vaultStore.cjs");
} catch (_) {
console.warn("[InspectionAgent] vault bridge not available");
_vaultBridge = null;
}
}
}
return _vaultBridge;
}
function readFn(key) { return getStorageBridge().read(key); }
function writeFn(key, val) { getStorageBridge().write(key, val); }
// ---------- Handlers ----------
function handleAlertDetail(params) {
const id = params?.alertId || params?.id;
if (!id) return { error: "alertId required" };
return alertRecordStore.getAlert(readFn, id);
}
function handleHostAlerts(params) {
const deviceId = params?.deviceId || params?.hostId;
if (!deviceId) return { error: "deviceId required" };
return alertRecordStore.listAlerts(readFn, {
device_id: deviceId,
limit: 50,
});
}
function handleHostMetrics(params) {
const deviceId = params?.deviceId || params?.hostId;
if (!deviceId) return { error: "deviceId required" };
const records = inspectionResultStore.getLatestByDevice(readFn, deviceId, 1);
if (!records || records.length === 0) return { device_id: deviceId, metrics: null, status: "no_data" };
const latest = records[0];
// catalogId 兜底:优先 result(新巡检存的),没有则从 device extension 拿
let catalogId = latest.catalog_id || undefined;
try {
const ext = inspectionDeviceStore.getExtension(readFn, deviceId);
console.log("[InspectionAgent] getExtension for", deviceId, "→", ext ? JSON.stringify({ host_id: ext.host_id, catalog_id: ext.catalog_id, hostname: ext.hostname }) : "null");
if (!catalogId) catalogId = ext?.catalog_id || undefined;
} catch (e) {
console.warn("[InspectionAgent] getExtension error:", e?.message);
}
console.log("[InspectionAgent] handleHostMetrics → result.catalog_id:", latest.catalog_id, "| final catalogId:", catalogId);
return {
device_id: deviceId,
device_name: latest.device_name,
catalogId,
timestamp: latest.timestamp,
metrics: latest.metrics || {},
online: latest.metrics?.online !== false,
};
}
function handleHostRecentResults(params) {
const deviceId = params?.deviceId || params?.hostId;
const limit = Math.min(params?.limit || 10, 50);
if (!deviceId) return { error: "deviceId required" };
return inspectionResultStore.getLatestByDevice(readFn, deviceId, limit);
}
function handleAllActiveAlerts() {
return alertRecordStore.getActiveAlerts(readFn);
}
function handleHostInfo(params) {
const deviceId = params?.deviceId || params?.hostId;
if (!deviceId) return { error: "deviceId required" };
// 先从 inspection device extension 拿 catalogId(这个不依赖 vault,始终可用)
let catalogId = undefined;
let hostname = undefined;
let username = undefined;
let port = undefined;
try {
const ext = inspectionDeviceStore.getExtension(readFn, deviceId);
console.log("[InspectionAgent] hostInfo getExtension for", deviceId, "→", ext ? JSON.stringify({ catalog_id: ext.catalog_id, hostname: ext.hostname }) : "null");
catalogId = ext?.catalog_id || undefined;
hostname = ext?.hostname || undefined;
} catch (_) {}
// 尝试 vault 拿更多设备信息
const vault = getVaultBridge();
if (vault) {
let host = null;
if (typeof vault.getHost === "function") host = vault.getHost(deviceId);
if (!host && typeof vault.getById === "function") host = vault.getById(deviceId);
if (!host && typeof vault.listHosts === "function") {
const hosts = vault.listHosts();
host = hosts.find((h) => h.id === deviceId) || null;
}
if (host) {
hostname = hostname || host.hostname || host.address;
username = host.username;
port = host.port || 22;
if (!catalogId) catalogId = host.catalog_id || host.catalogId || undefined;
}
}
console.log("[InspectionAgent] handleHostInfo → catalogId:", catalogId, "hostname:", hostname);
// 至少返回 catalogId(从 inspection extension 兜底)
return {
device_id: deviceId,
hostname,
username,
port,
catalogId,
};
}
function handleHostTopology(params) {
const deviceId = params?.deviceId || params?.hostId;
if (!deviceId) return { error: "deviceId required" };
const vault = getVaultBridge();
if (!vault) return { error: "vault not available" };
// Get host list + offline status for topology context
let hosts = [];
if (typeof vault.listHosts === "function") hosts = vault.listHosts();
else if (typeof vault.getAll === "function") hosts = vault.getAll();
const alerts = alertRecordStore.getActiveAlerts(readFn);
const offlineIds = new Set(
alerts.filter((a) => a.rule_id === "rule_offline" && a.status === "active").map((a) => a.device_id)
);
const criticalIds = new Set(
alerts.filter((a) => a.level === "critical" && a.status === "active" && a.rule_id !== "rule_offline").map((a) => a.device_id)
);
const peers = hosts
.filter((h) => h.id !== deviceId)
.map((h) => ({
id: h.id,
name: h.label || h.hostname,
offline: offlineIds.has(h.id),
critical: criticalIds.has(h.id),
}));
return {
device_id: deviceId,
peers_total: peers.length,
peers_offline: peers.filter((p) => p.offline).length,
peers_critical: peers.filter((p) => p.critical).length,
peers,
};
}
/**
* inspection.runCommand — execute a single shell command on a device via independent SSH connection.
* Used by RCA agent to run diagnosis commands on the target device.
*
* Params: { deviceId, command, timeoutMs? }
* Returns: { ok, stdout, stderr, exitCode, durationMs, error? }
*/
async function handleRunCommand(params) {
const deviceId = params?.deviceId;
const command = params?.command;
if (!deviceId) return { ok: false, error: "deviceId required" };
if (!command) return { ok: false, error: "command required" };
// 1. 优先从 sessions 按 hostname 复用已有 SSH 连接(最快、不用拿 vault)
const sessions = getSessions();
let targetHostname = null;
// 尝试从 inspection metrics 拿 hostname(通过 metrics 存储里的 catalog 扩展)
try {
const ext = inspectionDeviceStore.getExtension(readFn, deviceId);
targetHostname = ext?.hostname || null;
} catch (_) {}
// 尝试 vault(如果碰巧能拿到)
if (!targetHostname) {
const vault = getVaultBridge();
if (vault) {
let host = null;
if (typeof vault.getHost === "function") host = vault.getHost(deviceId);
if (!host && typeof vault.listHosts === "function") {
host = vault.listHosts().find((h) => h.id === deviceId);
}
if (host) targetHostname = host.hostname || host.address;
}
}
// 在 sessions Map 里按 hostname 匹配
let matchedSession = null;
if (sessions && targetHostname) {
for (const session of sessions.values()) {
if (!session || !session.conn) continue;
const sessionHost = session.hostname || session.host || "";
if (sessionHost === targetHostname) {
matchedSession = session;
break;
}
}
}
const start = Date.now();
if (matchedSession) {
// —— 复用已有 SSH 连接发 exec 命令 ——
return new Promise((resolve) => {
const timeoutMs = params.timeoutMs || 15000;
let timedOut = false;
const timer = setTimeout(() => {
timedOut = true;
resolve({
ok: false,
stdout: "",
stderr: "",
exitCode: -1,
durationMs: Date.now() - start,
error: `command timed out after ${timeoutMs}ms`,
});
}, timeoutMs);
try {
matchedSession.conn.exec(command, { pty: false }, (err, channel) => {
if (timedOut) return;
if (err) {
clearTimeout(timer);
resolve({
ok: false,
stdout: "",
stderr: err.message || String(err),
exitCode: -1,
durationMs: Date.now() - start,
error: err.message || String(err),
});
return;
}
let stdout = "";
let stderr = "";
channel.on("data", (d) => { stdout += d.toString(); });
channel.stderr?.on("data", (d) => { stderr += d.toString(); });
channel.on("close", (code) => {
if (timedOut) return;
clearTimeout(timer);
resolve({
ok: code === 0,
stdout,
stderr,
exitCode: code ?? 0,
durationMs: Date.now() - start,
error: code !== 0 ? `exit code ${code}` : undefined,
});
});
// 安全兜底
channel.on("error", (e) => {
if (timedOut) return;
clearTimeout(timer);
resolve({
ok: false,
stdout,
stderr: e.message || String(e),
exitCode: -1,
durationMs: Date.now() - start,
error: e.message || String(e),
});
});
});
} catch (e) {
clearTimeout(timer);
resolve({
ok: false,
stdout: "",
stderr: e?.message || String(e),
exitCode: -1,
durationMs: Date.now() - start,
error: e?.message || String(e),
});
}
});
}
// 2. 没有匹配的已有 SSH session — 无法独立连接(vault 凭据不可得)
// 告诉 RCA 在设备上打开一个终端 tab 后重试
return {
ok: false,
error: "目标设备没有活跃的 SSH 终端会话。请先在 Vault 里打开该设备的终端,让 RCA 复用连接执行诊断命令。",
};
}
/**
* inspection.runNow — synchronous immediate inspection.
* Requires the device to be reachable; uses the same SSH adapter as the scheduler.
* We can't re-use runInspection from index.cjs directly (it lives in Electron ipcMain scope
* and has side effects via broadcast). Instead we construct a fresh runner on demand.
*/
async function handleRunNow(params) {
const deviceId = params?.deviceId || params?.hostId;
if (!deviceId) return { error: "deviceId required" };
const vault = getVaultBridge();
if (!vault) return { error: "vault not available" };
let host = null;
if (typeof vault.getHost === "function") host = vault.getHost(deviceId);
if (!host && typeof vault.getById === "function") host = vault.getById(deviceId);
if (!host && typeof vault.listHosts === "function") {
host = vault.listHosts().find((h) => h.id === deviceId);
}
if (!host) return { error: `host ${deviceId} not found in Vault` };
const hostConfig = {
id: host.id,
label: host.label,
hostname: host.hostname || host.address,
port: host.port || 22,
username: host.username,
password: host.password,
privateKey: host.privateKey,
passphrase: host.passphrase,
};
let catalogId = "linux_server";
try {
const ext = inspectionDeviceStore.getExtension(readFn, deviceId);
if (ext?.catalog_id) catalogId = ext.catalog_id;
} catch (_) {}
try {
const { execSshCommand, execSshCommands } = require("../inspection/inspectionExec.cjs");
const { createAdapter } = require("../inspection/adapterRegistry.cjs");
const deviceStoreLocal = require("../inspection/deviceStore.cjs");
const sshConfig = {
host: hostConfig.hostname,
port: hostConfig.port,
username: hostConfig.username,
password: hostConfig.password,
privateKey: hostConfig.privateKey,
passphrase: hostConfig.passphrase,
};
const execFn = async (command, options = {}) => {
const timeoutMs = options.timeoutMs || 15000;
return await execSshCommand(sshConfig, command, {
connectTimeoutMs: 20000,
execTimeoutMs: timeoutMs,
});
};
const execCommandsFn = async (commands, options = {}) => {
return await execSshCommands(sshConfig, commands, options);
};
const catalogs = deviceStoreLocal.getCatalog();
const catalog = catalogs.find((c) => c.id === catalogId);
const adapterClassName = catalog?.adapter_class || "LinuxServerAdapter";
const adapter = createAdapter(adapterClassName, sshConfig, {
execFn,
execCommands: execCommandsFn,
timeoutMs: 20000,
});
if (!adapter) {
return { device_id: deviceId, status: "failed", error: "adapter " + adapterClassName + " not found" };
}
const start = Date.now();
const collectResult = await adapter.collect();
const durationMs = Date.now() - start;
// Save to result store
try {
inspectionResultStore.addResult(readFn, writeFn, {
id: `ins_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
device_id: deviceId,
device_name: host.label || host.hostname || deviceId,
catalog_id: catalogId,
start_time: start,
end_time: Date.now(),
duration_ms: durationMs,
status: collectResult.online ? "success" : "failed",
online: collectResult.online,
metrics: collectResult.metrics || {},
raw: collectResult.raw || {},
error_message: collectResult.error || null,
});
} catch (_) {}
return {
device_id: deviceId,
device_name: host.label || host.hostname,
status: collectResult.online ? "success" : "failed",
durationMs,
metrics: collectResult.metrics || null,
error: collectResult.error || null,
};
} catch (e) {
return {
device_id: deviceId,
status: "failed",
error: e.message,
metrics: null,
};
}
}
// Lazy storage adapter — resolved from Electron's global bridge on first call.
// (模块级变量已在文件顶部声明:_storageBridge / _vaultBridge)
let _sessions = null;
/**
* Context injector — called by main process bridge registration to supply
* shared dependencies. `sessions` is the main-process SSH session Map
* (sessionId → session with .conn ssh2.Client).
*/
function configure(ctx = {}) {
if (ctx.sessions) _sessions = ctx.sessions;
}
function getSessions() { return _sessions; }
/**
* Map capability id → handler function. Spread this into buildBuiltinRpcHandlerRegistry().
*/
function registerInspectionHandlers() {
return {
"inspection.alertDetail": handleAlertDetail,
"inspection.hostAlerts": handleHostAlerts,
"inspection.hostMetrics": handleHostMetrics,
"inspection.hostRecentResults": handleHostRecentResults,
"inspection.hostTopology": handleHostTopology,
"inspection.hostInfo": handleHostInfo,
"inspection.runNow": handleRunNow,
"inspection.runCommand": handleRunCommand,
"inspection.allActiveAlerts": handleAllActiveAlerts,
};
}
module.exports = {
registerInspectionHandlers,
configure,
getSessions,
// exported for testing / direct invocation
handleAlertDetail,
handleHostAlerts,
handleHostMetrics,
handleHostRecentResults,
handleHostTopology,
handleHostInfo,
handleAllActiveAlerts,
};