import { Button, Card, Space, Table, Tag, Toast, Typography } from '@douyinfe/semi-ui'; import { IconCopy } from '@douyinfe/semi-icons'; import { useEffect, useState } from 'react'; import { api } from '../api/client'; import type { OpsHealth, QualityAlertRule, QualityNotificationPlan, QualityNotificationPolicy, QualityPriorityIssue } from '../api/types'; import { PageHeader } from '../components/PageHeader'; import { buildCsv, downloadCsv, type CsvColumn } from '../domain/csvExport'; import { qualityIssueLabel } from '../domain/qualityIssue'; function formatEscalationMinutes(minutes?: number) { if (!Number.isFinite(minutes) || Number(minutes) <= 0) return '-'; const value = Number(minutes); if (value < 60) return `${value} 分钟升级`; const hours = Math.floor(value / 60); const rest = value % 60; return rest > 0 ? `${hours} 小时 ${rest} 分钟升级` : `${hours} 小时升级`; } function formatDurationFromMinutes(minutes?: number | null) { if (minutes == null || !Number.isFinite(minutes)) return '-'; const value = Math.max(0, Math.round(minutes)); if (value < 60) return `${value.toLocaleString()} 分钟`; const hours = Math.floor(value / 60); const rest = value % 60; return rest > 0 ? `${hours.toLocaleString()} 小时 ${rest} 分钟` : `${hours.toLocaleString()} 小时`; } function ruleColor(rule: QualityAlertRule): 'green' | 'orange' | 'red' | 'grey' { if ((rule.count ?? 0) <= 0) return 'green'; if (rule.level === 'P0') return 'red'; if (rule.level === 'P1') return 'orange'; return 'grey'; } function ruleTitle(rule: QualityAlertRule) { return rule.title || qualityIssueLabel(rule.issueType); } type NotificationExportRow = { category: string; name: string; level: string; ownerOrTarget: string; triggerOrCondition: string; notifyOrChannel: string; slaOrEscalation: string; acceptance: string; count: number | string; release: string; }; type SlaEscalationRow = { issue: QualityPriorityIssue; policy?: QualityNotificationPolicy; elapsedMinutes?: number; remainingMinutes?: number; status: 'overdue' | 'due_soon' | 'tracking' | 'unknown'; statusLabel: string; statusColor: 'red' | 'orange' | 'green' | 'grey'; }; type NotificationExecutionRow = { key: string; issue: QualityPriorityIssue; rule?: QualityAlertRule; policy?: QualityNotificationPolicy; slaRow: SlaEscalationRow; evidenceText: string; evidenceColor: 'green' | 'orange'; }; type NotificationCoverageOwnerRow = { owner: string; activeRules: number; p0Rules: number; hitCount: number; primaryPolicy?: QualityNotificationPolicy; }; const notificationExportColumns: CsvColumn[] = [ { title: '类型', value: (row) => row.category }, { title: '名称', value: (row) => row.name }, { title: '级别', value: (row) => row.level }, { title: '责任方', value: (row) => row.ownerOrTarget }, { title: '触发条件', value: (row) => row.triggerOrCondition }, { title: '通知方式', value: (row) => row.notifyOrChannel }, { title: 'SLA/升级', value: (row) => row.slaOrEscalation }, { title: '验收口径', value: (row) => row.acceptance }, { title: '当前命中', value: (row) => row.count }, { title: '运行版本', value: (row) => row.release } ]; function parseIssueTime(value?: string) { const normalized = String(value ?? '').trim().replace(' ', 'T'); if (!normalized) return undefined; const ms = Date.parse(normalized); return Number.isFinite(ms) ? ms : undefined; } function policyForPriority(issue: QualityPriorityIssue, policies: QualityNotificationPolicy[]) { const prefix = issue.priority === 'P0' ? 'P0' : 'P1'; return policies.find((policy) => policy.name.startsWith(prefix)) ?? policies[0]; } function slaEscalationRows(issues: QualityPriorityIssue[], policies: QualityNotificationPolicy[], nowMs = Date.now()): SlaEscalationRow[] { return issues.map((issue) => { const policy = policyForPriority(issue, policies); const lastSeenMs = parseIssueTime(issue.lastSeen); if (lastSeenMs == null || !policy?.escalationMinutes) { return { issue, policy, status: 'unknown', statusLabel: '待确认时间', statusColor: 'grey' }; } const elapsedMinutes = Math.max(0, (nowMs - lastSeenMs) / 60000); const remainingMinutes = policy.escalationMinutes - elapsedMinutes; if (remainingMinutes <= 0) { return { issue, policy, elapsedMinutes, remainingMinutes, status: 'overdue', statusLabel: `已超时 ${formatDurationFromMinutes(Math.abs(remainingMinutes))}`, statusColor: 'red' }; } if (remainingMinutes <= 15) { return { issue, policy, elapsedMinutes, remainingMinutes, status: 'due_soon', statusLabel: `${formatDurationFromMinutes(remainingMinutes)} 后升级`, statusColor: 'orange' }; } return { issue, policy, elapsedMinutes, remainingMinutes, status: 'tracking', statusLabel: `${formatDurationFromMinutes(remainingMinutes)} 后升级`, statusColor: 'green' }; }).sort((left, right) => { const statusWeight: Record = { overdue: 3, due_soon: 2, tracking: 1, unknown: 0 }; return statusWeight[right.status] - statusWeight[left.status] || (left.remainingMinutes ?? Number.POSITIVE_INFINITY) - (right.remainingMinutes ?? Number.POSITIVE_INFINITY) || left.issue.vehicleLabel.localeCompare(right.issue.vehicleLabel); }); } function slaEscalationReport(rows: SlaEscalationRow[], release?: string) { const overdue = rows.filter((row) => row.status === 'overdue').length; const dueSoon = rows.filter((row) => row.status === 'due_soon').length; const tracking = rows.filter((row) => row.status === 'tracking').length; const next = rows[0]; return [ '【告警SLA升级报告】', `运行版本:${release?.trim() || '-'}`, `升级态势:已超时 ${overdue.toLocaleString()} / 即将升级 ${dueSoon.toLocaleString()} / 正常跟进 ${tracking.toLocaleString()}`, `下一优先:${next ? `[${next.issue.priority}] ${next.issue.vehicleLabel} / ${next.statusLabel}` : '暂无待通知告警'}`, '', '升级队列:', ...(rows.length > 0 ? rows.slice(0, 10).map((row, index) => [ `${index + 1}. [${row.issue.priority}] ${row.issue.vehicleLabel}`, ` 状态:${row.statusLabel}`, ` 策略:${row.policy ? `${row.policy.name} / ${row.policy.target} / ${formatEscalationMinutes(row.policy.escalationMinutes)}` : '-'}`, ` 已等待:${formatDurationFromMinutes(row.elapsedMinutes)}`, ` 问题:${qualityIssueLabel(row.issue.issueType)} / ${row.issue.protocol}`, ` 建议动作:${row.issue.actionLabel}`, ` 车辆服务:${window.location.origin}${window.location.pathname}${row.issue.vehicleHash}`, ` RAW证据:${window.location.origin}${window.location.pathname}${row.issue.rawHash}` ].join('\n')) : ['暂无待升级告警']) ].join('\n'); } function notificationExecutionRows( issues: QualityPriorityIssue[], rules: QualityAlertRule[], policies: QualityNotificationPolicy[], nowMs = Date.now() ): NotificationExecutionRow[] { const slaByIssue = new Map(slaEscalationRows(issues, policies, nowMs).map((row) => [ `${row.issue.priority}-${row.issue.protocol}-${row.issue.issueType}-${row.issue.vehicleLabel}-${row.issue.lastSeen}`, row ])); return issues.map((issue) => { const key = `${issue.priority}-${issue.protocol}-${issue.issueType}-${issue.vehicleLabel}-${issue.lastSeen}`; const rule = rules.find((item) => item.issueType === issue.issueType); const policy = policyForPriority(issue, policies); const slaRow = slaByIssue.get(key) ?? { issue, policy, status: 'unknown', statusLabel: '待确认时间', statusColor: 'grey' } as SlaEscalationRow; return { key, issue, rule, policy, slaRow, evidenceText: evidenceSummary(issue), evidenceColor: evidenceColor(issue) }; }).sort((left, right) => { const priorityWeight = (value: string) => value === 'P0' ? 2 : value === 'P1' ? 1 : 0; const statusWeight: Record = { overdue: 3, due_soon: 2, tracking: 1, unknown: 0 }; return priorityWeight(right.issue.priority) - priorityWeight(left.issue.priority) || statusWeight[right.slaRow.status] - statusWeight[left.slaRow.status] || left.issue.vehicleLabel.localeCompare(right.issue.vehicleLabel); }); } function notificationExecutionMatrixReport(rows: NotificationExecutionRow[], release?: string) { const p0Count = rows.filter((row) => row.issue.priority === 'P0').length; const incompleteEvidence = rows.filter((row) => countEvidenceLinks(row.issue) < 4).length; const overdueCount = rows.filter((row) => row.slaRow.status === 'overdue').length; return [ '【通知执行矩阵】', `运行版本:${release?.trim() || '-'}`, `执行态势:P0 ${p0Count.toLocaleString()} / 已超时 ${overdueCount.toLocaleString()} / 证据不完整 ${incompleteEvidence.toLocaleString()}`, '', ...(rows.length > 0 ? rows.slice(0, 12).map((row, index) => [ `${index + 1}. [${row.issue.priority}] ${row.issue.vehicleLabel}`, ` 问题:${qualityIssueLabel(row.issue.issueType)} / ${row.issue.protocol}`, ` 规则:${row.rule ? `${ruleTitle(row.rule)} / ${row.rule.owner}` : '-'}`, ` 通知:${row.policy ? `${row.policy.target} / ${row.policy.channel}` : '-'}`, ` 升级:${row.policy ? formatEscalationMinutes(row.policy.escalationMinutes) : '-'}`, ` 状态:${row.slaRow.statusLabel}`, ` 证据:${row.evidenceText}`, ` 动作:${row.issue.actionLabel} - ${row.issue.actionDetail || '-'}`, ` 车辆服务:${window.location.origin}${window.location.pathname}${row.issue.vehicleHash}`, ` RAW证据:${window.location.origin}${window.location.pathname}${row.issue.rawHash}` ].join('\n')) : ['暂无待执行通知']) ].join('\n'); } function policyLevel(policy: QualityNotificationPolicy) { if (policy.name.startsWith('P0')) return 'P0'; if (policy.name.startsWith('P1')) return 'P1'; return '-'; } function coverageOwnerRows(rules: QualityAlertRule[], policies: QualityNotificationPolicy[]): NotificationCoverageOwnerRow[] { const rows = new Map(); for (const rule of rules) { if (Number(rule.count ?? 0) <= 0) continue; const owner = rule.owner?.trim() || '未分配责任方'; rows.set(owner, [...(rows.get(owner) ?? []), rule]); } return Array.from(rows.entries()).map(([owner, ownerRules]) => { const p0Rules = ownerRules.filter((rule) => rule.level === 'P0').length; const primaryPolicy = policies.find((policy) => policyLevel(policy) === (p0Rules > 0 ? 'P0' : 'P1')) ?? policies[0]; return { owner, activeRules: ownerRules.length, p0Rules, hitCount: ownerRules.reduce((total, rule) => total + Number(rule.count ?? 0), 0), primaryPolicy }; }).sort((left, right) => right.hitCount - left.hitCount || right.p0Rules - left.p0Rules || left.owner.localeCompare(right.owner)); } function notificationCoverageReport({ plan, release, ownerRows, overdueCount, dueSoonCount, health }: { plan: QualityNotificationPlan | null; release?: string; ownerRows: NotificationCoverageOwnerRow[]; overdueCount: number; dueSoonCount: number; health: OpsHealth | null; }) { const rules = plan?.rules ?? []; const policies = plan?.policies ?? []; const issues = plan?.priorityIssues ?? []; const activeRules = rules.filter((rule) => Number(rule.count ?? 0) > 0); const p0Issues = issues.filter((issue) => issue.priority === 'P0').length; const missingEvidence = issues.filter((issue) => countEvidenceLinks(issue) < 4).length; const unhealthyLinks = (health?.linkHealth ?? []).filter((item) => item.status !== 'ok'); const policyChannels = Array.from(new Set(policies.map((policy) => policy.channel).filter(Boolean))); return [ '【通知覆盖与升级风险】', `运行版本:${release?.trim() || '-'}`, `责任覆盖:${ownerRows.length.toLocaleString()} 个责任方,${activeRules.length.toLocaleString()} 类活跃规则`, `告警压力:待通知 ${issues.length.toLocaleString()} / P0 ${p0Issues.toLocaleString()} / 证据不完整 ${missingEvidence.toLocaleString()}`, `升级风险:已超时 ${overdueCount.toLocaleString()} / 即将升级 ${dueSoonCount.toLocaleString()}`, `通知渠道:${policyChannels.length > 0 ? policyChannels.join(';') : '-'}`, `链路前置:${unhealthyLinks.length > 0 ? unhealthyLinks.map((item) => `${item.name}=${item.status}`).join(';') : '正常'}`, '', '责任方覆盖:', ...(ownerRows.length > 0 ? ownerRows.slice(0, 8).map((row, index) => [ `${index + 1}. ${row.owner}`, ` 活跃规则:${row.activeRules.toLocaleString()},P0规则:${row.p0Rules.toLocaleString()},命中:${row.hitCount.toLocaleString()}`, ` 主策略:${row.primaryPolicy ? `${row.primaryPolicy.name} / ${row.primaryPolicy.target} / ${row.primaryPolicy.channel}` : '-'}`, ` 升级窗口:${row.primaryPolicy ? formatEscalationMinutes(row.primaryPolicy.escalationMinutes) : '-'}`, ` 验收:${row.primaryPolicy?.acceptanceCriteria || '-'}` ].join('\n')) : ['暂无活跃责任方']), '', '管理动作:', '1. P0 命中先确认责任方、渠道和升级窗口是否明确', '2. 证据不完整的车辆先补齐车辆服务、实时、轨迹、RAW 链接', '3. 超时或即将升级的告警必须同步业务责任人', '4. 链路异常时优先恢复采集、MySQL、TDEngine、Redis、Kafka 等前置能力' ].join('\n'); } function notificationRulesRunbook(plan: QualityNotificationPlan | null, release?: string) { const rules = plan?.rules ?? []; const policies = plan?.policies ?? []; const lines = [ '【通知规则Runbook】', `运行版本:${release?.trim() || '-'}`, `活跃规则:${(plan?.activeRuleCount ?? rules.filter((rule) => rule.count > 0).length).toLocaleString()} 类`, `P0规则:${(plan?.p0RuleCount ?? rules.filter((rule) => rule.count > 0 && rule.level === 'P0').length).toLocaleString()} 类`, '', '触发规则:', ...rules.map((rule, index) => [ `${index + 1}. ${ruleTitle(rule)} / ${rule.level} / ${rule.owner}`, ` 触发:${rule.trigger}`, ` 通知:${rule.notify}`, ` SLA:${rule.sla}`, ` 当前命中:${Number(rule.count ?? 0).toLocaleString()}` ].join('\n')), '', '通知策略:', ...policies.map((policy, index) => [ `${index + 1}. ${policy.name} / ${policy.target}`, ` 条件:${policy.condition}`, ` 渠道:${policy.channel}`, ` 升级:${formatEscalationMinutes(policy.escalationMinutes)}`, ` 验收:${policy.acceptanceCriteria || '-'}` ].join('\n')) ]; return lines.join('\n'); } function priorityIssueDigest(plan: QualityNotificationPlan | null, release?: string) { const issues = plan?.priorityIssues ?? []; return [ '【当前待通知告警】', `运行版本:${release?.trim() || '-'}`, `待通知:${issues.length.toLocaleString()} 条`, ...issues.map((issue, index) => [ `${index + 1}. [${issue.priority}] ${issue.vehicleLabel}`, ` 问题:${qualityIssueLabel(issue.issueType)} / ${issue.protocol}`, ` SLA:${issue.sla}`, ` 建议动作:${issue.actionLabel}`, ` 详情:${issue.detail || '-'}`, ` 车辆服务:${window.location.origin}${window.location.pathname}${issue.vehicleHash}`, ` 实时定位:${window.location.origin}${window.location.pathname}${issue.realtimeHash}`, ` 轨迹证据:${window.location.origin}${window.location.pathname}${issue.historyHash}`, ` RAW证据:${window.location.origin}${window.location.pathname}${issue.rawHash}` ].join('\n')) ].join('\n'); } function alertEscalationChecklist(plan: QualityNotificationPlan | null, release?: string) { const rules = plan?.rules ?? []; const policies = plan?.policies ?? []; const issues = plan?.priorityIssues ?? []; const p0Issues = issues.filter((issue) => issue.priority === 'P0'); const nextIssue = issues[0]; const primaryPolicy = policies.find((policy) => policy.name.startsWith('P0')) ?? policies[0]; const activeRules = rules.filter((rule) => Number(rule.count ?? 0) > 0); return [ '【告警升级值班清单】', `运行版本:${release?.trim() || '-'}`, `值班结论:${p0Issues.length > 0 ? `P0 ${p0Issues.length.toLocaleString()} 条,需要立即确认` : issues.length > 0 ? `待通知 ${issues.length.toLocaleString()} 条,按策略跟进` : '暂无待通知告警'}`, `主通知策略:${primaryPolicy ? `${primaryPolicy.name} / ${primaryPolicy.target} / ${primaryPolicy.channel}` : '-'}`, `升级窗口:${primaryPolicy ? formatEscalationMinutes(primaryPolicy.escalationMinutes) : '-'}`, `验收口径:${primaryPolicy?.acceptanceCriteria || '-'}`, '', '活跃规则:', ...(activeRules.length > 0 ? activeRules.map((rule, index) => `${index + 1}. [${rule.level}] ${ruleTitle(rule)} / ${rule.owner} / 命中 ${Number(rule.count ?? 0).toLocaleString()} / SLA ${rule.sla}`) : ['暂无活跃规则']), '', '下一条处置:', nextIssue ? [ `[${nextIssue.priority}] ${nextIssue.vehicleLabel}`, `问题:${qualityIssueLabel(nextIssue.issueType)} / ${nextIssue.protocol}`, `建议动作:${nextIssue.actionLabel} - ${nextIssue.actionDetail || '-'}`, `SLA:${nextIssue.sla}`, `证据:${evidenceSummary(nextIssue)}`, `车辆服务:${window.location.origin}${window.location.pathname}${nextIssue.vehicleHash}`, `实时证据:${window.location.origin}${window.location.pathname}${nextIssue.realtimeHash}`, `轨迹证据:${window.location.origin}${window.location.pathname}${nextIssue.historyHash}`, `RAW证据:${window.location.origin}${window.location.pathname}${nextIssue.rawHash}` ].join('\n') : '暂无待处置告警', '', '值班动作:', '1. 先处理 P0,无来源/VIN 缺失/存储不可写优先于普通字段缺失', '2. 复制单条通知给责任团队,并同步车辆服务、实时、轨迹、RAW 证据', '3. 超过升级窗口未恢复时,按主通知策略升级到业务责任人', '4. 恢复后检查验收口径,并在告警事件页确认不再命中' ].join('\n'); } function alertNotificationDrillPackage(plan: QualityNotificationPlan | null, health: OpsHealth | null, release?: string) { const rules = plan?.rules ?? []; const policies = plan?.policies ?? []; const issues = plan?.priorityIssues ?? []; const p0Rules = rules.filter((rule) => rule.level === 'P0'); const p1Rules = rules.filter((rule) => rule.level === 'P1'); const p0Issues = issues.filter((issue) => issue.priority === 'P0'); const firstP0Policy = policies.find((policy) => policy.name.startsWith('P0')) ?? policies[0]; const firstP1Policy = policies.find((policy) => policy.name.startsWith('P1')) ?? policies[1] ?? policies[0]; const unhealthyLinks = (health?.linkHealth ?? []).filter((item) => item.status !== 'ok'); const nextIssue = issues[0]; return [ '【告警通知演练包】', `运行版本:${release?.trim() || '-'}`, `链路状态:${unhealthyLinks.length > 0 ? unhealthyLinks.map((item) => `${item.name}=${item.status}`).join(';') : '正常'}`, `规则规模:P0 ${p0Rules.length.toLocaleString()} 类,P1 ${p1Rules.length.toLocaleString()} 类,待通知 ${issues.length.toLocaleString()} 条`, `当前优先级:${p0Issues.length > 0 ? `P0 ${p0Issues.length.toLocaleString()} 条` : issues.length > 0 ? 'P1/普通告警待跟进' : '暂无待通知告警'}`, '', '通知策略核对:', `P0策略:${firstP0Policy ? `${firstP0Policy.name} / ${firstP0Policy.target} / ${firstP0Policy.channel} / ${formatEscalationMinutes(firstP0Policy.escalationMinutes)}` : '-'}`, `P0验收:${firstP0Policy?.acceptanceCriteria || '-'}`, `P1策略:${firstP1Policy ? `${firstP1Policy.name} / ${firstP1Policy.target} / ${firstP1Policy.channel} / ${formatEscalationMinutes(firstP1Policy.escalationMinutes)}` : '-'}`, `P1验收:${firstP1Policy?.acceptanceCriteria || '-'}`, '', '演练步骤:', '1. 在告警事件页确认 P0/P1 命中数量与本页一致', '2. 对第一条待通知告警打开车辆服务、实时、轨迹、RAW 四类证据', '3. 复制单条告警通知给责任团队,记录发送时间', '4. 超过升级窗口仍未恢复时,按策略升级到业务责任人', '5. 恢复后按验收口径复核,并确认告警不再命中', '', '演练样例:', nextIssue ? [ `[${nextIssue.priority}] ${nextIssue.vehicleLabel}`, `问题:${qualityIssueLabel(nextIssue.issueType)} / ${nextIssue.protocol}`, `SLA:${nextIssue.sla}`, `通知文本:${nextIssue.notificationText}`, `车辆服务:${window.location.origin}${window.location.pathname}${nextIssue.vehicleHash}`, `实时证据:${window.location.origin}${window.location.pathname}${nextIssue.realtimeHash}`, `轨迹证据:${window.location.origin}${window.location.pathname}${nextIssue.historyHash}`, `RAW证据:${window.location.origin}${window.location.pathname}${nextIssue.rawHash}` ].join('\n') : '暂无待通知样例' ].join('\n'); } function countEvidenceLinks(issue: QualityPriorityIssue) { return [issue.vehicleHash, issue.realtimeHash, issue.historyHash, issue.rawHash].filter(Boolean).length; } function evidenceSummary(issue: QualityPriorityIssue) { const count = countEvidenceLinks(issue); return count >= 4 ? '证据完整' : `缺 ${4 - count} 类证据`; } function evidenceColor(issue: QualityPriorityIssue): 'green' | 'orange' { return countEvidenceLinks(issue) >= 4 ? 'green' : 'orange'; } function notificationExportRows(plan: QualityNotificationPlan | null, release = ''): NotificationExportRow[] { const version = release.trim() || '-'; const ruleRows = (plan?.rules ?? []).map((rule) => ({ category: '触发规则', name: ruleTitle(rule), level: rule.level, ownerOrTarget: rule.owner, triggerOrCondition: rule.trigger, notifyOrChannel: rule.notify, slaOrEscalation: rule.sla, acceptance: '-', count: Number(rule.count ?? 0), release: version })); const policyRows = (plan?.policies ?? []).map((policy) => ({ category: '通知策略', name: policy.name, level: policy.name.startsWith('P0') ? 'P0' : policy.name.startsWith('P1') ? 'P1' : '-', ownerOrTarget: policy.target, triggerOrCondition: policy.condition, notifyOrChannel: policy.channel, slaOrEscalation: formatEscalationMinutes(policy.escalationMinutes), acceptance: policy.acceptanceCriteria || '-', count: '-', release: version })); return [...ruleRows, ...policyRows]; } function navigateHash(hash: string) { if (!hash) return; window.location.hash = hash; } async function copyText(value: string, label: string) { try { await navigator.clipboard.writeText(value); Toast.success(`已复制${label}`); } catch { Toast.error(`复制${label}失败`); } } export function NotificationRules() { const [plan, setPlan] = useState(null); const [health, setHealth] = useState(null); const [loading, setLoading] = useState(true); const load = () => { setLoading(true); Promise.all([ api.alertEventNotificationPlan(new URLSearchParams({ limit: '50' })), api.opsHealth().catch(() => null) ]) .then(([nextPlan, nextHealth]) => { setPlan(nextPlan); setHealth(nextHealth); }) .catch((error: Error) => Toast.error(error.message)) .finally(() => setLoading(false)); }; useEffect(() => { load(); }, []); const rules = plan?.rules ?? []; const policies = plan?.policies ?? []; const priorityIssues = plan?.priorityIssues ?? []; const activeRuleCount = plan?.activeRuleCount ?? rules.filter((rule) => rule.count > 0).length; const p0RuleCount = plan?.p0RuleCount ?? rules.filter((rule) => rule.count > 0 && rule.level === 'P0').length; const release = health?.runtime?.platformRelease ?? ''; const p0IssueCount = priorityIssues.filter((issue) => issue.priority === 'P0').length; const evidenceCompleteCount = priorityIssues.filter((issue) => countEvidenceLinks(issue) >= 4).length; const primaryOwner = rules.find((rule) => rule.count > 0)?.owner || policies[0]?.target || '-'; const nextPriorityIssue = priorityIssues[0]; const slaRows = slaEscalationRows(priorityIssues, policies); const overdueCount = slaRows.filter((row) => row.status === 'overdue').length; const dueSoonCount = slaRows.filter((row) => row.status === 'due_soon').length; const nextSlaRow = slaRows[0]; const executionRows = notificationExecutionRows(priorityIssues, rules, policies); const ownerRows = coverageOwnerRows(rules, policies); const totalActiveHits = ownerRows.reduce((total, row) => total + row.hitCount, 0); const unhealthyLinkCount = (health?.linkHealth ?? []).filter((item) => item.status !== 'ok').length; const exportNotificationRules = () => { const rows = notificationExportRows(plan, release); if (rows.length === 0) { Toast.warning('当前没有可导出的通知规则'); return; } downloadCsv(`notification-rules-${release || 'current'}.csv`, buildCsv(notificationExportColumns, rows)); Toast.success(`已导出 ${rows.length.toLocaleString()} 条通知规则`); }; return (
{activeRuleCount.toLocaleString()}
活跃规则
{p0RuleCount.toLocaleString()}
P0规则
{policies.length.toLocaleString()}
通知策略
{priorityIssues.length.toLocaleString()}
待通知告警
{release || '-'}
运行版本
通知覆盖与升级风险} style={{ marginTop: 16 }} >
0 || overdueCount > 0 ? 'red' : dueSoonCount > 0 ? 'orange' : 'green'}> {p0IssueCount > 0 || overdueCount > 0 ? '高优先级跟进' : dueSoonCount > 0 ? '临近升级' : '覆盖稳定'} 0 ? 'orange' : 'green'}>{unhealthyLinkCount > 0 ? '链路需关注' : '链路正常'} {ownerRows.length.toLocaleString()} 个责任方 将告警规则、通知策略、证据完整度和 SLA 升级风险按责任团队归并,便于值班人确认是否能真正通知到人。
{[ { label: '责任覆盖', value: ownerRows.length.toLocaleString(), detail: `${activeRuleCount.toLocaleString()} 类活跃规则已映射责任方。`, color: ownerRows.length > 0 ? 'green' as const : 'orange' as const }, { label: '活跃命中', value: totalActiveHits.toLocaleString(), detail: '来自当前告警规则命中数量,用于判断通知压力。', color: totalActiveHits > 0 ? 'orange' as const : 'green' as const }, { label: 'P0压力', value: p0IssueCount.toLocaleString(), detail: `${p0RuleCount.toLocaleString()} 类 P0 规则,优先进入升级队列。`, color: p0IssueCount > 0 ? 'red' as const : 'green' as const }, { label: '升级风险', value: `${overdueCount}/${dueSoonCount}`, detail: '已超时 / 即将升级,直接影响通知闭环时效。', color: overdueCount > 0 ? 'red' as const : dueSoonCount > 0 ? 'orange' as const : 'green' as const }, { label: '链路前置', value: unhealthyLinkCount.toLocaleString(), detail: '采集、存储、缓存等前置链路异常会影响告警判定。', color: unhealthyLinkCount > 0 ? 'orange' as const : 'green' as const } ].map((item) => (
{item.label} {item.value} {item.detail}
))}
{ownerRows.length > 0 ? ownerRows.slice(0, 6).map((row) => (
0 ? 'red' : 'orange'}>{row.owner} 命中 {row.hitCount.toLocaleString()} {row.activeRules.toLocaleString()} 类活跃规则,{row.p0Rules.toLocaleString()} 类 P0。
{row.primaryPolicy?.name || '未匹配策略'} {row.primaryPolicy?.channel || '-'} {row.primaryPolicy ? formatEscalationMinutes(row.primaryPolicy.escalationMinutes) : '-'}
)) : ( 暂无活跃告警责任方 )}
告警处置队列} style={{ marginTop: 16 }} >
{[ { label: '待通知告警', value: priorityIssues.length.toLocaleString(), color: priorityIssues.length > 0 ? 'orange' as const : 'green' as const }, { label: 'P0优先', value: p0IssueCount.toLocaleString(), color: p0IssueCount > 0 ? 'red' as const : 'green' as const }, { label: '证据完整', value: `${evidenceCompleteCount}/${priorityIssues.length || 0}`, color: evidenceCompleteCount === priorityIssues.length ? 'green' as const : 'orange' as const }, { label: '主责任方', value: primaryOwner, color: 'blue' as const } ].map((item) => (
{item.label}
{item.value}
))}
下一步处置
{nextPriorityIssue ? ( <> {nextPriorityIssue.vehicleLabel} {nextPriorityIssue.actionDetail || nextPriorityIssue.actionLabel} {nextPriorityIssue.priority} {evidenceSummary(nextPriorityIssue)} {nextPriorityIssue.sla} ) : ( 暂无待通知告警 )}
通知执行矩阵} style={{ marginTop: 16 }} > pagination={false} dataSource={executionRows.slice(0, 10)} rowKey={(row?: NotificationExecutionRow) => row?.key ?? ''} columns={[ { title: '优先级', width: 90, render: (_: unknown, row: NotificationExecutionRow) => {row.issue.priority} }, { title: '车辆', width: 220, render: (_: unknown, row: NotificationExecutionRow) => row.issue.vehicleLabel }, { title: '问题', width: 130, render: (_: unknown, row: NotificationExecutionRow) => qualityIssueLabel(row.issue.issueType) }, { title: '触发规则', width: 180, render: (_: unknown, row: NotificationExecutionRow) => row.rule ? `${ruleTitle(row.rule)} / ${row.rule.owner}` : '-' }, { title: '通知对象', width: 210, render: (_: unknown, row: NotificationExecutionRow) => row.policy?.target || '-' }, { title: '渠道', width: 150, render: (_: unknown, row: NotificationExecutionRow) => row.policy?.channel || '-' }, { title: '升级窗口', width: 120, render: (_: unknown, row: NotificationExecutionRow) => row.policy ? formatEscalationMinutes(row.policy.escalationMinutes) : '-' }, { title: 'SLA状态', width: 150, render: (_: unknown, row: NotificationExecutionRow) => {row.slaRow.statusLabel} }, { title: '证据', width: 110, render: (_: unknown, row: NotificationExecutionRow) => {row.evidenceText} }, { title: '操作', render: (_: unknown, row: NotificationExecutionRow) => ( ) } ]} /> SLA升级态势} style={{ marginTop: 16 }} >
{[ { label: '已超时', value: overdueCount.toLocaleString(), color: overdueCount > 0 ? 'red' as const : 'green' as const }, { label: '即将升级', value: dueSoonCount.toLocaleString(), color: dueSoonCount > 0 ? 'orange' as const : 'green' as const }, { label: '待通知', value: slaRows.length.toLocaleString(), color: slaRows.length > 0 ? 'blue' as const : 'green' as const }, { label: '下一优先', value: nextSlaRow?.statusLabel || '-', color: nextSlaRow?.statusColor ?? 'grey' as const } ].map((item) => (
{item.label} {item.value}
))}
pagination={false} dataSource={slaRows.slice(0, 8)} rowKey={(row?: SlaEscalationRow) => `${row?.issue.priority ?? ''}-${row?.issue.vehicleLabel ?? ''}-${row?.issue.lastSeen ?? ''}`} columns={[ { title: '状态', width: 150, render: (_: unknown, row: SlaEscalationRow) => {row.statusLabel} }, { title: '车辆', width: 220, render: (_: unknown, row: SlaEscalationRow) => row.issue.vehicleLabel }, { title: '优先级', width: 90, render: (_: unknown, row: SlaEscalationRow) => {row.issue.priority} }, { title: '策略', width: 210, render: (_: unknown, row: SlaEscalationRow) => row.policy?.name || '-' }, { title: '已等待', width: 120, render: (_: unknown, row: SlaEscalationRow) => formatDurationFromMinutes(row.elapsedMinutes) }, { title: '责任方', width: 190, render: (_: unknown, row: SlaEscalationRow) => row.policy?.target || '-' }, { title: '处置', render: (_: unknown, row: SlaEscalationRow) => ( ) } ]} />
规则运行手册} >
{[ { label: '触发', value: `${activeRuleCount} 类规则`, detail: '规则从车辆服务质量、来源断链、字段缺失和容量风险中生成。' }, { label: '通知', value: `${policies.length} 套策略`, detail: '策略定义目标人群、渠道、升级窗口和验收口径。' }, { label: '闭环', value: `${p0RuleCount} 类 P0`, detail: 'P0 问题必须有证据链接、恢复时间和验收结果。' } ].map((item) => (
{item.label}
{item.value}
{item.detail}
))}
pagination={false} dataSource={rules} rowKey="issueType" columns={[ { title: '规则', width: 180, render: (_: unknown, row: QualityAlertRule) => ruleTitle(row) }, { title: '级别', width: 90, render: (_: unknown, row: QualityAlertRule) => {row.level} }, { title: '责任团队', width: 130, dataIndex: 'owner' }, { title: '当前命中', width: 110, render: (_: unknown, row: QualityAlertRule) => Number(row.count ?? 0).toLocaleString() }, { title: '触发条件', dataIndex: 'trigger' }, { title: '通知动作', dataIndex: 'notify' }, { title: 'SLA', width: 120, dataIndex: 'sla' } ]} /> pagination={false} dataSource={priorityIssues} rowKey={(row?: QualityPriorityIssue) => `${row?.priority ?? ''}-${row?.protocol ?? ''}-${row?.issueType ?? ''}-${row?.vehicleLabel ?? ''}-${row?.lastSeen ?? ''}`} columns={[ { title: '优先级', width: 90, render: (_: unknown, row: QualityPriorityIssue) => {row.priority} }, { title: '车辆', width: 210, dataIndex: 'vehicleLabel' }, { title: '问题', width: 130, render: (_: unknown, row: QualityPriorityIssue) => qualityIssueLabel(row.issueType) }, { title: 'SLA', width: 120, dataIndex: 'sla' }, { title: '证据完整度', width: 120, render: (_: unknown, row: QualityPriorityIssue) => {evidenceSummary(row)} }, { title: '建议动作', width: 160, dataIndex: 'actionLabel' }, { title: '最后时间', width: 170, dataIndex: 'lastSeen' }, { title: '说明', render: (_: unknown, row: QualityPriorityIssue) => ( {row.detail || row.actionDetail || '-'} ) }, { title: '证据', width: 360, render: (_: unknown, row: QualityPriorityIssue) => ( ) } ]} />
{policies.map((policy: QualityNotificationPolicy) => (
{policy.name} {policy.target}
{policy.condition}
验收:{policy.acceptanceCriteria || '-'}
{formatEscalationMinutes(policy.escalationMinutes)} {policy.channel}
))}
); }