feat: 监控告警体系统一重构 + 自动化任务调度引擎 + 监控大屏拆分

告警中心
- 新增统一告警(alert_unified)、监控告警规则(monitor_alert_rule)、指标目录(alert_metric_catalog)
- 通知策略与消息分发(alert_notify_policy/dispatch/message)、静默规则
- 告警引擎按阈值/SNMP 拆分(alert_engine_monitor/snmp)

监控采集
- SNMP 常量/模板与历史指标(snmp templates/constants、snmp_metrics_history、snmp_collect)
- 中间件监控 Redis 采集与种子(middleware_monitor_redis/seed)
- 站点探测 pkg(siteprobe) + 站点视图(site_view)

自动化任务
- cron 调度器(cronscheduler)、脚本运行器(scriptrunner)、系统任务(systask)及运行记录(cron_job_run、system_task)

审计与凭证
- 审计设置(audit_setting) + 历史清理(auditcleaner)

前端
- 监控页面(host/middleware/site/snmp)拆分为子模块并大屏化
- 告警中心多 Tab(规则/指标目录/通知策略/静默)、任务管理组件重构、Agent CLI Markdown 渲染
- 移除旧脚本卡片/详情/日志组件,统一为 ScriptTable/ScriptCodeEditor/TerminalOutput

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
LoganandClaude committed 2026-07-01 23:44:45 +08:00
1 parent 092d0e4b9e
commit 471353e3d7
190 files changed
+16640 -3484

No files matched your search

+98 -167
View File
@@ -1,4 +1,4 @@
// ScriptManager — 脚本库与执行记录主组件 // Logan.Li
// ScriptManager — 脚本库主组件 // Logan.Li
import React, { useState, useEffect } from 'react';
import { Plus, Search, RotateCcw, Trash2, FileCode } from 'lucide-react';
import { Button } from '@/components/ui/button';
@@ -9,16 +9,16 @@ import {
import { Checkbox } from '@/components/ui/checkbox';
import { toast } from 'sonner';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { scriptApi, taskExecutionApi } from '@/api';
import { scriptApi } from '@/api';
import type { ScriptRuntimeInfo } from '@/api/task/script';
import { usePermission } from '@/hooks/usePermission';
import {
type Script, type ExecutionRecord, mapScript, scriptTypes, genCode,
type Script, mapScript, scriptTypes, genCode, availableScriptTypes,
} from './script-helpers';
import { ScriptRuntimeBanner } from './ScriptRuntimeBanner';
import type { ScriptFormData } from './ScriptFormDialog';
import ScriptFormDialog from './ScriptFormDialog';
import ScriptDetailDialog from './ScriptDetailDialog';
import ScriptLogsDialog from './ScriptLogsDialog';
import ScriptCard from './ScriptCard';
import ScriptExecutionsTable from './ScriptExecutionsTable';
import { ScriptTable } from './ScriptTable';
import { PageShell } from '@/components/layout/PageShell';
import { ContentPanel } from '@/components/layout/ContentPanel';
import { PageEmpty } from '@/components/layout/PageEmpty';
@@ -26,24 +26,20 @@ import type { JsonData } from '@/types/common';
export function ScriptManager() {
const _confirm = useConfirm();
const { hasAnyPermission, isAdmin } = usePermission();
const canWrite = isAdmin || hasAnyPermission(['auto:script', 'auto:cron', 'auto:ansible']);
const [scripts, setScripts] = useState<Script[]>([]);
const [executions, setExecutions] = useState<ExecutionRecord[]>([]);
const [runtime, setRuntime] = useState<ScriptRuntimeInfo | null>(null);
const [runtimeLoading, setRuntimeLoading] = useState(true);
const [loading, setLoading] = useState(false);
const [executing, setExecuting] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [typeFilter, setTypeFilter] = useState<string>('all');
const [activeTab, setActiveTab] = useState('scripts');
const [selectedIds, setSelectedIds] = useState<number[]>([]);
// 弹窗状态
const [editorOpen, setEditorOpen] = useState(false);
const [detailOpen, setDetailOpen] = useState(false);
const [logsOpen, setLogsOpen] = useState(false);
const [editingId, setEditingId] = useState<number | null>(null);
const [selectedScript, setSelectedScript] = useState<Script | null>(null);
const [selectedExec, setSelectedExec] = useState<ExecutionRecord | null>(null);
// 表单状态
const [formData, setFormData] = useState<ScriptFormData>({
name: '', description: '', content: '', type: 'shell', timeout: 300,
});
@@ -61,26 +57,30 @@ export function ScriptManager() {
}
};
const fetchExecutions = async () => {
const fetchRuntime = async () => {
setRuntimeLoading(true);
try {
const res = await taskExecutionApi.getExecutions({ task_type: 'script', page_size: 50 });
const items = (res as JsonData)?.list || [];
setExecutions(items);
const rt = await scriptApi.getRuntime();
setRuntime(rt);
} catch {
// 静默失败
} finally {
setRuntimeLoading(false);
}
};
useEffect(() => {
fetchScripts();
fetchExecutions();
fetchRuntime();
}, []);
const openCreate = () => {
const types = availableScriptTypes(runtime);
const first = types.find((t) => !t.disabled) ?? types[0] ?? scriptTypes[0];
setEditingId(null);
setFormData({
name: '', description: '', content: scriptTypes[0].sample,
type: 'shell', timeout: 300,
name: '', description: '', content: first.sample,
type: first.value, timeout: 300,
});
setEditorOpen(true);
};
@@ -96,7 +96,6 @@ export function ScriptManager() {
timeout: sc.timeout,
});
setEditorOpen(true);
setDetailOpen(false);
};
const handleTypeChange = (t: string) => {
@@ -134,8 +133,8 @@ export function ScriptManager() {
}
setEditorOpen(false);
fetchScripts();
} catch {
toast.error(editingId !== null ? '更新失败' : '创建失败');
} catch (e) {
toast.error(e instanceof Error ? e.message : (editingId !== null ? '更新失败' : '创建失败'));
}
};
@@ -151,8 +150,8 @@ export function ScriptManager() {
});
toast.success('复制成功');
fetchScripts();
} catch {
toast.error('复制失败');
} catch (e) {
toast.error(e instanceof Error ? e.message : '复制失败');
}
};
@@ -168,43 +167,15 @@ export function ScriptManager() {
await scriptApi.deleteScript(sc.id);
toast.success('删除成功');
fetchScripts();
} catch {
toast.error('删除失败');
}
};
const handleExecute = async (sc: Script) => {
const ok = await _confirm({
title: '执行脚本',
description: `将在 AIOps 主机上本地执行「${sc.name}」(${sc.type})。`,
confirmText: '开始执行',
});
if (!ok) return;
setExecuting(true);
try {
const res = await scriptApi.executeScript(sc.id);
const exec = (res as JsonData)?.data ?? res;
if (exec?.status === 'success') {
toast.success(`执行成功,耗时 ${exec.duration ?? 0}ms`);
} else {
toast.error(`执行失败,耗时 ${exec?.duration ?? 0}ms`, {
description: ((exec?.output || '') as string).slice(0, 120),
});
}
fetchScripts();
fetchExecutions();
} catch {
toast.error('执行请求失败');
} finally {
setExecuting(false);
} catch (e) {
toast.error(e instanceof Error ? e.message : '删除失败');
}
};
const filteredScripts = scripts.filter(sc => {
const q = searchQuery.toLowerCase();
const matchSearch = sc.name.toLowerCase().includes(q) ||
sc.description.toLowerCase().includes(q) ||
sc.code.toLowerCase().includes(q);
sc.description.toLowerCase().includes(q);
const matchType = typeFilter === 'all' || sc.type === typeFilter;
return matchSearch && matchType;
});
@@ -233,24 +204,18 @@ export function ScriptManager() {
toast.success(`已删除 ${selectedIds.length} 个脚本`);
setSelectedIds([]);
fetchScripts();
} catch {
toast.error('批量删除失败');
} catch (e) {
toast.error(e instanceof Error ? e.message : '批量删除失败');
}
};
// 页面级 Tab 配置 // Logan.Li
const pageTabs = [
{ id: 'scripts', label: '脚本库' },
{ id: 'jobs', label: '执行记录' },
];
const scriptsToolbar = (
const toolbar = (
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-2 flex-1 min-w-[300px]">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 dg-text-muted" />
<Input
placeholder="搜索名称、编码、描述..."
placeholder="搜索名称、描述..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 dg-input"
@@ -276,103 +241,81 @@ export function ScriptManager() {
<PageShell
variant="list"
title="脚本库"
subtitle="脚本管理与执行记录"
subtitle="脚本编辑与管理"
icon={<FileCode className="h-5 w-5" />}
tabs={pageTabs}
activeTab={activeTab}
onTabChange={(id) => {
setActiveTab(id);
if (id === 'jobs') fetchExecutions();
}}
toolbar={activeTab === 'scripts' ? scriptsToolbar : undefined}
loading={activeTab === 'scripts' && loading}
actions={activeTab === 'scripts' ? (
toolbar={toolbar}
loading={loading}
actions={(
<>
<Button variant="outline" className="dg-btn-secondary" onClick={fetchScripts}>
<RotateCcw className="w-4 h-4 mr-2" />
刷新
</Button>
<Button className="dg-btn-primary" onClick={openCreate}>
<Plus className="w-4 h-4 mr-2" />
新建脚本
</Button>
{canWrite && (
<Button className="dg-btn-primary" onClick={openCreate}>
<Plus className="w-4 h-4 mr-2" />
新建脚本
</Button>
)}
</>
) : (
<Button variant="outline" className="dg-btn-secondary" onClick={fetchExecutions}>
<RotateCcw className="w-4 h-4 mr-2" />
刷新
</Button>
)}
>
{activeTab === 'scripts' ? (
<>
{selectedIds.length > 0 && (
<div className="flex items-center justify-between gap-4 px-4 py-2.5 rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] mb-4">
<div className="flex items-center gap-3">
<Checkbox
checked={allSelected}
onCheckedChange={toggleSelectAll}
className="border-[var(--dg-border)]"
/>
<span className="text-sm dg-text-secondary">
已选择 {selectedIds.length} 项 / 共 {filteredScripts.length} 项
</span>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={() => setSelectedIds([])}>
取消选择
</Button>
<Button
variant="outline"
size="sm"
className="dg-btn-danger dg-btn-sm"
onClick={handleBatchDelete}
>
<Trash2 className="w-4 h-4 mr-2" />
批量删除
</Button>
</div>
</div>
)}
{filteredScripts.length === 0 ? (
<PageEmpty
title={scripts.length === 0 ? '暂无脚本' : '未匹配到符合条件的脚本'}
description={scripts.length === 0 ? '点击「新建脚本」创建第一个' : undefined}
action={scripts.length === 0 ? (
<Button className="dg-btn-primary" onClick={openCreate}>
<Plus className="w-4 h-4 mr-2" />
新建脚本
</Button>
) : undefined}
<div className="mb-4">
<ScriptRuntimeBanner runtime={runtime} loading={runtimeLoading} />
</div>
{selectedIds.length > 0 && (
<div className="flex items-center justify-between gap-4 px-4 py-2.5 rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] mb-4">
<div className="flex items-center gap-3">
<Checkbox
checked={allSelected}
onCheckedChange={toggleSelectAll}
className="border-[var(--dg-border)]"
/>
) : (
<ContentPanel>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredScripts.map((sc) => (
<ScriptCard
key={sc.id}
script={sc}
selected={selectedIds.includes(sc.id)}
executing={executing}
onToggleSelect={() => toggleSelect(sc.id)}
onView={() => { setSelectedScript(sc); setDetailOpen(true); }}
onExecute={() => handleExecute(sc)}
onEdit={() => openEdit(sc)}
onClone={() => handleClone(sc)}
onDelete={() => handleDelete(sc)}
/>
))}
</div>
</ContentPanel>
)}
</>
) : (
<ScriptExecutionsTable
executions={executions}
onRefresh={fetchExecutions}
onViewLogs={(job) => { setSelectedExec(job); setLogsOpen(true); }}
<span className="text-sm dg-text-secondary">
已选择 {selectedIds.length} 项 / 共 {filteredScripts.length} 项
</span>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={() => setSelectedIds([])}>
取消选择
</Button>
<Button
variant="outline"
size="sm"
className="dg-btn-danger dg-btn-sm"
onClick={handleBatchDelete}
>
<Trash2 className="w-4 h-4 mr-2" />
批量删除
</Button>
</div>
</div>
)}
{filteredScripts.length === 0 ? (
<PageEmpty
title={scripts.length === 0 ? '暂无脚本' : '未匹配到符合条件的脚本'}
description={scripts.length === 0 ? '点击「新建脚本」创建第一个' : undefined}
action={scripts.length === 0 && canWrite ? (
<Button className="dg-btn-primary" onClick={openCreate}>
<Plus className="w-4 h-4 mr-2" />
新建脚本
</Button>
) : undefined}
/>
) : (
<ContentPanel>
<ScriptTable
scripts={filteredScripts}
selectedIds={selectedIds}
onToggleSelect={toggleSelect}
onToggleSelectAll={toggleSelectAll}
allSelected={allSelected}
onEdit={openEdit}
onClone={handleClone}
onDelete={handleDelete}
/>
</ContentPanel>
)}
</PageShell>
@@ -385,19 +328,7 @@ export function ScriptManager() {
setFormData={setFormData}
onTypeChange={handleTypeChange}
onSave={handleSave}
/>
<ScriptDetailDialog
open={detailOpen}
onOpenChange={setDetailOpen}
script={selectedScript}
onEdit={openEdit}
/>
<ScriptLogsDialog
open={logsOpen}
onOpenChange={setLogsOpen}
exec={selectedExec}
runtime={runtime}
/>
</>
);