feat(platform): add alert notification handoff

This commit is contained in:
lingniu
2026-07-04 20:54:22 +08:00
parent bfb16c548e
commit df6404e789
2 changed files with 97 additions and 1 deletions

View File

@@ -500,6 +500,71 @@ function notificationPolicyRunbookText(policies: QualityNotificationPolicy[], ru
].join('\n');
}
function notificationHandoffText({
rows,
summary,
rules,
policies,
filters,
platformRelease
}: {
rows: PriorityIssueRow[];
summary: QualitySummary;
rules: AlertRuleRow[];
policies: QualityNotificationPolicy[];
filters: Record<string, string>;
platformRelease?: string;
}) {
const release = platformRelease?.trim();
const activeRules = rules.filter((row) => row.count > 0);
const p0Rows = rows.filter((row) => row.priority === 'P0');
const p1Rows = rows.filter((row) => row.priority === 'P1');
const primaryPolicy = policies.find((item) => item.name.startsWith('P0')) ?? policies[0];
const filterLines = [
filters.keyword ? `关键词=${filters.keyword}` : '',
filters.protocol ? `数据来源=${qualityProtocolLabel(filters.protocol)}` : '',
filters.issueType ? `问题类型=${qualityIssueLabel(filters.issueType)}` : ''
].filter(Boolean);
const issueLines = rows.length > 0
? rows.map((row, index) => {
const lookup = qualityIssueVehicleLookup(row);
const dateFrom = issueEvidenceDate(row.lastSeen);
const dateTo = nextDate(dateFrom);
const evidenceFilters = {
...(dateFrom ? { dateFrom } : {}),
...(dateTo ? { dateTo } : {})
};
return [
`${index + 1}. [${row.priority}] ${row.vehicleLabel}`,
` 来源:${qualityProtocolLabel(row.protocol)};问题:${qualityIssueLabel(row.issueType)}SLA${row.sla}`,
` 建议动作:${row.actionLabel};最后时间:${row.lastSeen || '-'}`,
` 详情:${row.detail || '-'}`,
` 实时:${appURL(buildAppHash({ page: 'realtime', keyword: lookup.key, protocol: row.protocol }))}`,
` 轨迹:${appURL(buildAppHash({ page: 'history', keyword: lookup.key, protocol: row.protocol, filters: evidenceFilters }))}`,
` RAW${appURL(buildAppHash({ page: 'history-query', keyword: lookup.key, protocol: row.protocol, filters: { tab: 'raw', ...evidenceFilters, includeFields: 'true' } }))}`,
` 车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: lookup.key, protocol: row.protocol }))}`
].join('\n');
})
: ['当前筛选下暂无待通知车辆'];
return [
'【告警通知交接包】',
...(release ? [`运行版本:${release}`] : []),
`当前筛选:${filterLines.length > 0 ? filterLines.join('') : '全部告警'}`,
`问题车辆:${Number(summary.issueVehicleCount ?? 0).toLocaleString()};问题记录:${Number(summary.issueRecordCount ?? 0).toLocaleString()}`,
`优先级P0 ${p0Rows.length.toLocaleString()} 条 / P1 ${p1Rows.length.toLocaleString()}`,
`活跃规则:${activeRules.length.toLocaleString()}${activeRules.length > 0 ? `${activeRules.map((row) => `${row.level} ${row.title || qualityIssueLabel(row.issueType)} ${row.count}`).join('')}` : ''}`,
primaryPolicy ? `主通知策略:${[primaryPolicy.name, primaryPolicy.target, primaryPolicy.channel, formatEscalationMinutes(primaryPolicy.escalationMinutes)].filter(Boolean).join(' / ')}` : '主通知策略:-',
primaryPolicy?.acceptanceCriteria ? `统一验收:${primaryPolicy.acceptanceCriteria}` : '统一验收车辆服务状态恢复实时、轨迹、RAW 证据可查询',
'',
'待通知车辆:',
...issueLines,
'',
`告警事件:${qualityShareURL()}`,
`通知规则:${appURL(buildAppHash({ page: 'notification-rules' }))}`,
`运维质量:${appURL(buildAppHash({ page: 'ops-quality' }))}`
].join('\n');
}
async function copyText(value: string, label: string) {
const text = value.trim();
if (!text) {
@@ -694,6 +759,16 @@ export function Quality({
const copyPolicyRunbook = () => {
copyText(notificationPolicyRunbookText(policies, rules, health?.runtime?.platformRelease), '通知策略Runbook');
};
const copyNotificationHandoff = () => {
copyText(notificationHandoffText({
rows: priorityRows,
summary: notificationPlan?.summary ?? summary,
rules,
policies,
filters,
platformRelease: health?.runtime?.platformRelease
}), '告警通知交接包');
};
return (
<div className="vp-page">
@@ -742,7 +817,11 @@ export function Quality({
</Card>
</Col>
</Row>
<Card bordered title="告警事件闭环" style={{ marginTop: 16 }}>
<Card
bordered
title={<Space><span></span><Button size="small" onClick={copyNotificationHandoff}></Button></Space>}
style={{ marginTop: 16 }}
>
<div className="vp-alert-flow">
{[
{ label: '事件触发', value: `${activeRuleCount} 类活跃`, detail: '断链、无来源、VIN 缺失、字段缺失和容量异常进入告警池。' },

View File

@@ -3897,6 +3897,23 @@ test('copies priority queue notification digest on quality page', async () => {
expect(await screen.findByText('处置优先队列')).toBeInTheDocument();
expect(await screen.findByText('platform-20260704153951')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '复制通知交接包' }));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【告警通知交接包】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('运行版本platform-20260704153951'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前筛选:全部告警'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('问题车辆2问题记录2'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('优先级P0 1 条 / P1 1 条'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('活跃规则2 类'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('主通知策略P0 实时中断 / 接入运维 + 业务责任人 / 站内告警 / 邮件 / 企业微信 / 30 分钟升级'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('统一验收:来源恢复并持续 10 分钟,车辆服务可查到实时与历史证据'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. [P0] 粤A汇总1 / VIN-DIGEST-001'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时http://localhost:3000/#/realtime?keyword=VIN-DIGEST-001&protocol=VEHICLE_SERVICE'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('RAWhttp://localhost:3000/#/history-query?keyword=VIN-DIGEST-001&protocol=VEHICLE_SERVICE&tab=raw&dateFrom=2026-07-03&dateTo=2026-07-04&includeFields=true'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('2. [P1] 粤A汇总2 / 13307795426'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('通知规则http://localhost:3000/#/notification-rules'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('运维质量http://localhost:3000/#/ops-quality'));
fireEvent.click(screen.getByRole('button', { name: '复制优先队列通知汇总' }));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【告警优先队列汇总】'));