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 () => {