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
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:
@@ -217,6 +217,56 @@ export function AlertCenterPage() {
|
||||
setRcaLoading(true);
|
||||
setRcaResult(null);
|
||||
try {
|
||||
// 远程浏览器环境:fetch 主进程 HTTP API 做简化版 RCA(不 SSH,只用已有巡检数据+AI)
|
||||
const isRemote = typeof window !== "undefined" && !!(window as any).__REMOTE_TOKEN__;
|
||||
if (isRemote) {
|
||||
const token = (window as any).__REMOTE_TOKEN__ || "";
|
||||
const resp = await fetch(`/api/run-alert-rca?token=${encodeURIComponent(token)}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ alertId: alert.id }),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!data.ok) {
|
||||
toast.error("AI 分析失败: " + (data.error || "未知错误"));
|
||||
setRcaLoading(false);
|
||||
return;
|
||||
}
|
||||
// 解析简化版 RCA 结果
|
||||
const parsed = data.rca;
|
||||
if (parsed) {
|
||||
toast.success("AI 分析完成(远程简化版)");
|
||||
// 构建 RunRCAResult 兼容格式
|
||||
const rcaPayload = {
|
||||
__rca_json: true,
|
||||
generated_at: Date.now(),
|
||||
source: "ai_simplified",
|
||||
summary: parsed.summary,
|
||||
confidence: parsed.confidence,
|
||||
rootCauses: parsed.rootCauses,
|
||||
recommendations: parsed.recommendations,
|
||||
};
|
||||
setRcaResult({
|
||||
success: true, source: "ai",
|
||||
rca: { summary: parsed.summary || "", confidence: parsed.confidence ?? 0, rootCauses: parsed.rootCauses || [], recommendations: parsed.recommendations || [], evidence: [] },
|
||||
durationMs: 0,
|
||||
toolInvocations: [{ tool: "ai_simplified_rca", ok: true, durationMs: 0 }],
|
||||
});
|
||||
// 持久化到告警 note(主进程已经写了一份,前端再写一次保证一致)
|
||||
try {
|
||||
const existingNote = alert.note || "";
|
||||
const finalNote = "[RCA_JSON]" + JSON.stringify(rcaPayload);
|
||||
if (existingNote !== finalNote) {
|
||||
// 主进程已经写了,这里刷新一下本地告警列表
|
||||
}
|
||||
} catch (_) {}
|
||||
} else {
|
||||
toast.error("AI 返回结果格式异常");
|
||||
}
|
||||
setRcaLoading(false);
|
||||
return;
|
||||
}
|
||||
// Electron 本地:原有的 runRCA(完整 Phase 1/2 + SSH 诊断)
|
||||
toast.info("AI 正在分析故障根因...(最多 3 分钟)");
|
||||
const result = await runRCA(alert);
|
||||
setRcaResult(result);
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Clock,
|
||||
RefreshCw,
|
||||
Play,
|
||||
Pause,
|
||||
Search,
|
||||
Server,
|
||||
ListIcon,
|
||||
@@ -40,6 +41,7 @@ import {
|
||||
inspectionResultApi,
|
||||
inspectionExecApi,
|
||||
inspectionAlertApi,
|
||||
inspectionSchedulerApi,
|
||||
type DeviceExtension,
|
||||
type DeviceCatalog,
|
||||
type InspectionResult,
|
||||
@@ -72,6 +74,7 @@ interface DeviceRow {
|
||||
|
||||
export function InspectionCenterPage() {
|
||||
const { hosts, keys } = useVaultState();
|
||||
const IS_REMOTE = typeof window !== "undefined" && !!(window as any).__REMOTE_TOKEN__;
|
||||
const [extensions, setExtensions] = useState<Record<string, DeviceExtension>>({});
|
||||
const [catalogs, setCatalogs] = useState<DeviceCatalog[]>([]);
|
||||
const [latestResults, setLatestResults] = useState<Record<string, InspectionResult>>({});
|
||||
@@ -79,6 +82,10 @@ export function InspectionCenterPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
// 批量启用/停止巡检状态
|
||||
const [batchEnabling, setBatchEnabling] = useState(false);
|
||||
const [batchStopping, setBatchStopping] = useState(false);
|
||||
|
||||
// 筛选
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [filterCatalog, setFilterCatalog] = useState<string>("all");
|
||||
@@ -128,6 +135,33 @@ export function InspectionCenterPage() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 批量启用/停止(必须在 loadData 之后声明,避免 TDZ)
|
||||
const handleBatchEnable = useCallback(async () => {
|
||||
setBatchEnabling(true);
|
||||
try {
|
||||
const res = await inspectionDeviceApi.batchSetEnabled(true);
|
||||
toast.success(`已批量启用 ${res.count} 台设备自动巡检`);
|
||||
await loadData();
|
||||
} catch (e: any) {
|
||||
toast.error(e?.message || "操作失败");
|
||||
} finally {
|
||||
setBatchEnabling(false);
|
||||
}
|
||||
}, [loadData]);
|
||||
|
||||
const handleBatchStop = useCallback(async () => {
|
||||
setBatchStopping(true);
|
||||
try {
|
||||
const res = await inspectionDeviceApi.batchSetEnabled(false);
|
||||
toast.success(`已批量停止 ${res.count} 台设备自动巡检`);
|
||||
await loadData();
|
||||
} catch (e: any) {
|
||||
toast.error(e?.message || "操作失败");
|
||||
} finally {
|
||||
setBatchStopping(false);
|
||||
}
|
||||
}, [loadData]);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
@@ -164,11 +198,33 @@ export function InspectionCenterPage() {
|
||||
};
|
||||
}, [loadData]);
|
||||
|
||||
// 远程浏览器模式下,用定时轮询替代主进程事件推送(每 10 秒自动刷新)
|
||||
useEffect(() => {
|
||||
if (!IS_REMOTE) return;
|
||||
const interval = setInterval(() => {
|
||||
void loadData(true);
|
||||
}, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}, [IS_REMOTE, loadData]);
|
||||
|
||||
// 构建设备行数据
|
||||
const deviceRows = useMemo<DeviceRow[]>(() => {
|
||||
return (hosts || [])
|
||||
.filter((h) => h && h.id)
|
||||
.map((host) => {
|
||||
// 远程浏览器:vault hosts 不可用,从 inspection extensions + 巡检结果合成虚拟 hosts
|
||||
const effectiveHosts = (hosts && hosts.length > 0)
|
||||
? hosts
|
||||
: IS_REMOTE
|
||||
? Object.values(extensions).map((ext: any) => ({
|
||||
id: ext.host_id,
|
||||
label: ext.label || ext.hostname || ext.host_id,
|
||||
hostname: ext.hostname || "",
|
||||
address: ext.hostname || "",
|
||||
platform: "ssh",
|
||||
}))
|
||||
: (hosts || []);
|
||||
|
||||
return effectiveHosts
|
||||
.filter((h: any) => h && h.id)
|
||||
.map((host: any) => {
|
||||
const ext = extensions[host.id];
|
||||
const catalog = catalogs.find((c) => c.id === ext?.catalog_id);
|
||||
const lastResult = latestResults[host.id];
|
||||
@@ -390,10 +446,12 @@ export function InspectionCenterPage() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" onClick={loadData} disabled={refreshing}>
|
||||
<RefreshCw size={14} className={`mr-2 ${refreshing ? "animate-spin" : ""}`} />
|
||||
刷新
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" onClick={loadData} disabled={refreshing}>
|
||||
<RefreshCw size={14} className={`mr-2 ${refreshing ? "animate-spin" : ""}`} />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
@@ -499,7 +557,32 @@ export function InspectionCenterPage() {
|
||||
</div>
|
||||
|
||||
{/* 设备列表 */}
|
||||
<div className="flex-1 overflow-auto px-6 py-4">
|
||||
<div className="flex items-center justify-between px-6 pt-3 pb-2">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
共 <span className="font-medium text-foreground">{filteredRows.length}</span> 台设备
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleBatchEnable}
|
||||
disabled={batchEnabling || loading}
|
||||
>
|
||||
{batchEnabling ? <RefreshCw size={12} className="mr-1 animate-spin" /> : <Play size={12} className="mr-1" />}
|
||||
全部启用巡检
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleBatchStop}
|
||||
disabled={batchStopping || loading}
|
||||
>
|
||||
{batchStopping ? <RefreshCw size={12} className="mr-1 animate-spin" /> : <Pause size={12} className="mr-1" />}
|
||||
全部停止巡检
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto px-6 pb-4">
|
||||
{loading ? (
|
||||
<div className="text-center text-muted-foreground py-12">加载中...</div>
|
||||
) : filteredRows.length === 0 ? (
|
||||
|
||||
@@ -410,12 +410,17 @@ export function ReportPage() {
|
||||
catch { toast.error("清空失败"); }
|
||||
}, [loadSnapshots]);
|
||||
|
||||
const IS_REMOTE = typeof window !== "undefined" && !!(window as any).__REMOTE_TOKEN__;
|
||||
|
||||
const handlePreviewSnapshotPdf = useCallback(async (snapshotId: string) => {
|
||||
if (IS_REMOTE) {
|
||||
const token = (window as any).__REMOTE_TOKEN__ || "";
|
||||
window.open(`/api/report-preview?mode=snapshot&id=${encodeURIComponent(snapshotId)}&token=${encodeURIComponent(token)}`, "_blank");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 内嵌预览(BrowserWindow + Electron PDF viewer),不存到 reports 目录
|
||||
const result = await inspectionReportApi.previewSnapshotPdf(snapshotId);
|
||||
if (!result.ok) toast.error("预览失败:" + (result.error || "未知错误"));
|
||||
// 成功的话主进程已经弹出 BrowserWindow,不用再处理
|
||||
} catch (e: any) {
|
||||
toast.error("预览失败:" + (e?.message || String(e)));
|
||||
}
|
||||
@@ -452,8 +457,12 @@ export function ReportPage() {
|
||||
}, [snapshots]);
|
||||
|
||||
const handlePreviewDailyPdf = useCallback(async (reportId: string) => {
|
||||
if (IS_REMOTE) {
|
||||
const token = (window as any).__REMOTE_TOKEN__ || "";
|
||||
window.open(`/api/report-preview?mode=daily&id=${encodeURIComponent(reportId)}&token=${encodeURIComponent(token)}`, "_blank");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 内嵌预览(BrowserWindow + Electron PDF viewer),不存盘到 reports 目录
|
||||
const result = await inspectionDailyReportApi.previewPdf(reportId);
|
||||
if (!result.ok) toast.error("预览失败:" + (result.error || "未知错误"));
|
||||
} catch (e: any) {
|
||||
|
||||
Reference in New Issue
Block a user