feat(platform): add alert notification drill package

This commit is contained in:
lingniu
2026-07-04 20:34:15 +08:00
parent e6188e2569
commit c0b99e9a3a
2 changed files with 54 additions and 1 deletions

View File

@@ -143,6 +143,51 @@ function alertEscalationChecklist(plan: QualityNotificationPlan | null, release?
].join('\n');
}
function alertNotificationDrillPackage(plan: QualityNotificationPlan | null, health: OpsHealth | null, release?: string) {
const rules = plan?.rules ?? [];
const policies = plan?.policies ?? [];
const issues = plan?.priorityIssues ?? [];
const p0Rules = rules.filter((rule) => rule.level === 'P0');
const p1Rules = rules.filter((rule) => rule.level === 'P1');
const p0Issues = issues.filter((issue) => issue.priority === 'P0');
const firstP0Policy = policies.find((policy) => policy.name.startsWith('P0')) ?? policies[0];
const firstP1Policy = policies.find((policy) => policy.name.startsWith('P1')) ?? policies[1] ?? policies[0];
const unhealthyLinks = (health?.linkHealth ?? []).filter((item) => item.status !== 'ok');
const nextIssue = issues[0];
return [
'【告警通知演练包】',
`运行版本:${release?.trim() || '-'}`,
`链路状态:${unhealthyLinks.length > 0 ? unhealthyLinks.map((item) => `${item.name}=${item.status}`).join('') : '正常'}`,
`规则规模P0 ${p0Rules.length.toLocaleString()}P1 ${p1Rules.length.toLocaleString()} 类,待通知 ${issues.length.toLocaleString()}`,
`当前优先级:${p0Issues.length > 0 ? `P0 ${p0Issues.length.toLocaleString()}` : issues.length > 0 ? 'P1/普通告警待跟进' : '暂无待通知告警'}`,
'',
'通知策略核对:',
`P0策略${firstP0Policy ? `${firstP0Policy.name} / ${firstP0Policy.target} / ${firstP0Policy.channel} / ${formatEscalationMinutes(firstP0Policy.escalationMinutes)}` : '-'}`,
`P0验收${firstP0Policy?.acceptanceCriteria || '-'}`,
`P1策略${firstP1Policy ? `${firstP1Policy.name} / ${firstP1Policy.target} / ${firstP1Policy.channel} / ${formatEscalationMinutes(firstP1Policy.escalationMinutes)}` : '-'}`,
`P1验收${firstP1Policy?.acceptanceCriteria || '-'}`,
'',
'演练步骤:',
'1. 在告警事件页确认 P0/P1 命中数量与本页一致',
'2. 对第一条待通知告警打开车辆服务、实时、轨迹、RAW 四类证据',
'3. 复制单条告警通知给责任团队,记录发送时间',
'4. 超过升级窗口仍未恢复时,按策略升级到业务责任人',
'5. 恢复后按验收口径复核,并确认告警不再命中',
'',
'演练样例:',
nextIssue ? [
`[${nextIssue.priority}] ${nextIssue.vehicleLabel}`,
`问题:${qualityIssueLabel(nextIssue.issueType)} / ${nextIssue.protocol}`,
`SLA${nextIssue.sla}`,
`通知文本:${nextIssue.notificationText}`,
`车辆服务:${window.location.origin}${window.location.pathname}${nextIssue.vehicleHash}`,
`实时证据:${window.location.origin}${window.location.pathname}${nextIssue.realtimeHash}`,
`轨迹证据:${window.location.origin}${window.location.pathname}${nextIssue.historyHash}`,
`RAW证据${window.location.origin}${window.location.pathname}${nextIssue.rawHash}`
].join('\n') : '暂无待通知样例'
].join('\n');
}
function countEvidenceLinks(issue: QualityPriorityIssue) {
return [issue.vehicleHash, issue.realtimeHash, issue.historyHash, issue.rawHash].filter(Boolean).length;
}
@@ -271,7 +316,7 @@ export function NotificationRules() {
<Card
bordered
loading={loading}
title={<Space><span></span><Button size="small" aria-label="复制待通知汇总" disabled={priorityIssues.length === 0} icon={<IconCopy />} onClick={() => copyText(priorityIssueDigest(plan, release), '待通知告警')}></Button><Button size="small" aria-label="复制升级值班清单" icon={<IconCopy />} onClick={() => copyText(alertEscalationChecklist(plan, release), '升级值班清单')}></Button></Space>}
title={<Space><span></span><Button size="small" aria-label="复制待通知汇总" disabled={priorityIssues.length === 0} icon={<IconCopy />} onClick={() => copyText(priorityIssueDigest(plan, release), '待通知告警')}></Button><Button size="small" aria-label="复制升级值班清单" icon={<IconCopy />} onClick={() => copyText(alertEscalationChecklist(plan, release), '升级值班清单')}></Button><Button size="small" aria-label="复制通知演练包" icon={<IconCopy />} onClick={() => copyText(alertNotificationDrillPackage(plan, health, release), '通知演练包')}></Button></Space>}
style={{ marginTop: 16 }}
>
<div className="vp-alert-command-board">

View File

@@ -3481,6 +3481,14 @@ test('renders notification rules as a standalone operations page', async () => {
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆服务http://localhost:3000/#/detail?keyword=VIN-RULES-001'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('RAW证据http://localhost:3000/#/history-query?keyword=VIN-RULES-001&tab=raw'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('超过升级窗口未恢复时'));
fireEvent.click(screen.getByRole('button', { name: '复制通知演练包' }));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【告警通知演练包】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('运行版本platform-rules-test'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('规则规模P0 1 类P1 1 类,待通知 1 条'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('P0策略P0 实时中断 / 接入运维 + 业务责任人 / 邮件 / 企业微信 / 30 分钟升级'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('2. 对第一条待通知告警打开车辆服务、实时、轨迹、RAW 四类证据'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('通知文本【P0 告警通知】通知规则页待通知'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹证据http://localhost:3000/#/history?keyword=VIN-RULES-001'));
fireEvent.click(screen.getByRole('button', { name: '复制单条告警通知' }));
expect(writeText).toHaveBeenCalledWith('【P0 告警通知】通知规则页待通知');
fireEvent.click(screen.getByRole('button', { name: '车辆服务' }));