feat(platform): add alert notification handoff
This commit is contained in:
@@ -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 缺失、字段缺失和容量异常进入告警池。' },
|
||||
|
||||
@@ -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('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: '复制优先队列通知汇总' }));
|
||||
|
||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【告警优先队列汇总】'));
|
||||
|
||||
Reference in New Issue
Block a user