feat(platform): add alert notification handoff
This commit is contained in:
@@ -500,6 +500,71 @@ function notificationPolicyRunbookText(policies: QualityNotificationPolicy[], ru
|
|||||||
].join('\n');
|
].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) {
|
async function copyText(value: string, label: string) {
|
||||||
const text = value.trim();
|
const text = value.trim();
|
||||||
if (!text) {
|
if (!text) {
|
||||||
@@ -694,6 +759,16 @@ export function Quality({
|
|||||||
const copyPolicyRunbook = () => {
|
const copyPolicyRunbook = () => {
|
||||||
copyText(notificationPolicyRunbookText(policies, rules, health?.runtime?.platformRelease), '通知策略Runbook');
|
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 (
|
return (
|
||||||
<div className="vp-page">
|
<div className="vp-page">
|
||||||
@@ -742,7 +817,11 @@ export function Quality({
|
|||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</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">
|
<div className="vp-alert-flow">
|
||||||
{[
|
{[
|
||||||
{ label: '事件触发', value: `${activeRuleCount} 类活跃`, detail: '断链、无来源、VIN 缺失、字段缺失和容量异常进入告警池。' },
|
{ label: '事件触发', value: `${activeRuleCount} 类活跃`, detail: '断链、无来源、VIN 缺失、字段缺失和容量异常进入告警池。' },
|
||||||
|
|||||||
@@ -3897,6 +3897,23 @@ test('copies priority queue notification digest on quality page', async () => {
|
|||||||
|
|
||||||
expect(await screen.findByText('处置优先队列')).toBeInTheDocument();
|
expect(await screen.findByText('处置优先队列')).toBeInTheDocument();
|
||||||
expect(await screen.findByText('platform-20260704153951')).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('RAW:http://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: '复制优先队列通知汇总' }));
|
fireEvent.click(screen.getByRole('button', { name: '复制优先队列通知汇总' }));
|
||||||
|
|
||||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【告警优先队列汇总】'));
|
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【告警优先队列汇总】'));
|
||||||
|
|||||||
Reference in New Issue
Block a user