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 { 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 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); } 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 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 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]; return (
{activeRuleCount.toLocaleString()}
活跃规则
{p0RuleCount.toLocaleString()}
P0规则
{policies.length.toLocaleString()}
通知策略
{priorityIssues.length.toLocaleString()}
待通知告警
{release || '-'}
运行版本
告警处置队列} 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} ) : ( 暂无待通知告警 )}
规则运行手册} >
{[ { 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}
))}
); }