feat: 巡检增强 / 拓扑大屏 / 远程 HTTP / portable 绿色打包
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

巡检模块:巡检调度器改为轮询驱动,新增每日报告存储、告警规则独立
Store、设备 Store 解耦,_executeSnapshot 补齐 DEBUG 声明,巡检中心
与告警中心页增强。

拓扑大屏:MonitoringTopologyView 扩展分组/布局控制,
useMonitoringData 重构数据流。

远程 HTTP:新增 electron/remoteHttpServer.cjs,preload/api.cjs 暴露
IPC,registerBridges 注册 netcatty:http:open 桥。

portable 绿色打包:
- electron-builder.config.cjs 恢复原始白名单(61MB app.asar),
  收集器自动追踪运行时依赖;
- portableData.cjs 重写为 .portable 标记检测;
- scripts/migratePortableLocalStorage.cjs 离线脚本把 Local Storage
  的 http://localhost:5173 origin 批量迁移为 app://netcatty;
- scripts/scanWorkspaceDeps.cjs 扫描 workspace 运行时依赖。
This commit is contained in:
2026-10-19 21:11:57 +08:00
committed by trae-solo-agent
parent 336c3afd47
commit 2e71e03d71
34 changed files with 3012 additions and 230 deletions

View File

@@ -130,16 +130,137 @@ export interface SchedulerStatus {
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<string, { method: string; path: string; arrayField?: string }> = {
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" },
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" },
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" },
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" },
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" },
};
// 某些 API 返回数组 —— 在 REMOTE_API_MAP 里用 arrayField 标记
// 找不到映射的 API 默认返回 null,让调用方自己兜底
const ARRAY_METHODS = new Set([
"listReportSnapshots", "listDailyReports", "listRules", "getLatestAll",
"getLatestByDevice", "getRecentResults", "getActiveAlerts", "listAlerts",
"getAllExtensions", "getEnabledDevices", "getCatalog",
]);
async function remoteFetch(name: string, ...args: any[]): Promise<any> {
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}?token=${encodeURIComponent(REMOTE_TOKEN)}`;
if (name === "getLatestByDevice" && args[0]) {
url += `&device_id=${encodeURIComponent(args[0])}`;
}
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 {
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;
// 优先用 arrayField 约定的字段
if (mapping.arrayField && Array.isArray(data[mapping.arrayField])) {
return data[mapping.arrayField];
}
if (data.alerts !== undefined) return data.alerts;
if (data.devices !== undefined) {
if (name === "getAllExtensions") {
// getAllExtensions 返回 Record<string, DeviceExtension>
const map: Record<string, any> = {};
for (const d of data.devices) {
map[d.host_id] = d;
}
return map;
}
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) 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;
return !!api || IS_REMOTE_BROWSER;
}
function invoke(name: string, ...args: any[]): Promise<any> {
if (!api || typeof api[name] !== "function") {
console.warn(`[Inspection] api.${name} 不可用`);
return Promise.resolve(null);
if (api && typeof api[name] === "function") {
return api[name](...args);
}
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);
}
// ---------- 标签 ----------
@@ -167,6 +288,8 @@ export const inspectionDeviceApi = {
get: (hostId: string): Promise<DeviceExtension | null> => invoke("getExtension", hostId),
upsert: (hostId: string, data: Partial<DeviceExtension>): Promise<DeviceExtension> =>
invoke("upsertExtension", hostId, data),
batchSetEnabled: (enabled: boolean): Promise<{ ok: boolean; count: number; enabled: boolean; error?: string }> =>
invoke("batchSetInspectionEnabled", enabled),
remove: (hostId: string): Promise<boolean> => invoke("deleteExtension", hostId),
getEffectiveInterval: (hostId: string): Promise<number> =>
invoke("getEffectiveInterval", hostId),
@@ -229,6 +352,8 @@ export const inspectionSchedulerApi = {
status: (): Promise<SchedulerStatus> => invoke("schedulerStatus"),
scanNow: (hostId: string): Promise<any> => invoke("scanNow", hostId),
scanAllNow: (): Promise<any> => invoke("scanAllNow"),
getAutoEnabled: (): Promise<boolean> => invoke("getAutoEnabled"),
setAutoEnabled: (enabled: boolean): Promise<{ enabled: boolean }> => invoke("setAutoEnabled", enabled),
};
// ---------- 单次巡检执行 ----------