diff --git a/vehicle-data-platform/apps/web/src/pages/Quality.tsx b/vehicle-data-platform/apps/web/src/pages/Quality.tsx index d1e4194c..9bb95928 100644 --- a/vehicle-data-platform/apps/web/src/pages/Quality.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Quality.tsx @@ -449,6 +449,26 @@ function normalizePriorityIssues(rows?: QualityPriorityIssue[]): PriorityIssueRo return rows; } +function notificationPolicyRunbookText(policies: QualityNotificationPolicy[], rules: AlertRuleRow[]) { + const activeRules = rules.filter((row) => row.count > 0); + const lines = policies.map((policy, index) => [ + `${index + 1}. ${policy.name} / ${policy.target}`, + ` 触发:${policy.condition}`, + ` 渠道:${policy.channel}`, + ` 升级:${formatEscalationMinutes(policy.escalationMinutes) || '-'}`, + ` 验收:${policy.acceptanceCriteria || '-'}` + ].join('\n')); + return [ + '【告警通知策略Runbook】', + `活跃规则:${activeRules.length.toLocaleString()} 类`, + `P0规则:${activeRules.filter((row) => row.level === 'P0').length.toLocaleString()} 类`, + '', + ...lines, + '', + `告警筛选:${qualityShareURL()}` + ].join('\n'); +} + async function copyText(value: string, label: string) { const text = value.trim(); if (!text) { @@ -629,6 +649,9 @@ export function Quality({ } copyText(priorityIssueDigestText(priorityRows, notificationPlan?.summary ?? summary, health?.runtime?.platformRelease, policies), '优先队列通知汇总'); }; + const copyPolicyRunbook = () => { + copyText(notificationPolicyRunbookText(policies, rules), '通知策略Runbook'); + }; return (
@@ -783,7 +806,10 @@ export function Quality({ ]} /> - + 通知策略} + >
{policies.map((item) => (
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 1b088eba..475d6bcd 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -2881,6 +2881,11 @@ test('renders quality issues as vehicle-service governance labels', async () => test('shows alert rule and notification policy workspace on quality page', async () => { window.history.replaceState(null, '', '/#/quality'); + const writeText = vi.fn(() => Promise.resolve()); + Object.defineProperty(navigator, 'clipboard', { + configurable: true, + value: { writeText } + }); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { @@ -2961,6 +2966,12 @@ test('shows alert rule and notification policy workspace on quality page', async expect(screen.getByText('站内告警 / 邮件 / 企业微信')).toBeInTheDocument(); expect(screen.getByText('30 分钟升级')).toBeInTheDocument(); expect(screen.getByText('验收:来源恢复并持续 10 分钟,车辆服务可查到实时与历史证据')).toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button', { name: '复制通知策略Runbook' })); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【告警通知策略Runbook】')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('P0 实时中断 / 接入运维 + 业务责任人')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('升级:30 分钟升级')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('验收:来源恢复并持续 10 分钟,车辆服务可查到实时与历史证据')); }); test('shows actionable priority queue on quality page', async () => {