diff --git a/vehicle-data-platform/apps/web/src/pages/Quality.tsx b/vehicle-data-platform/apps/web/src/pages/Quality.tsx index 13655521..76eb5900 100644 --- a/vehicle-data-platform/apps/web/src/pages/Quality.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Quality.tsx @@ -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; + 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 (
@@ -742,7 +817,11 @@ export function Quality({ - + 告警事件闭环} + style={{ marginTop: 16 }} + >
{[ { label: '事件触发', value: `${activeRuleCount} 类活跃`, detail: '断链、无来源、VIN 缺失、字段缺失和容量异常进入告警池。' }, diff --git a/vehicle-data-platform/apps/web/src/test/App.test.tsx b/vehicle-data-platform/apps/web/src/test/App.test.tsx index 6c9d92f7..e5b4dd5f 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -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('【告警优先队列汇总】'));