import { useEffect, useRef, useState } from 'react' import { ReloadOutlined } from '@ant-design/icons' import { Alert, Button, Card, Col, Descriptions, Modal, Row, Select, Space, Spin, Statistic, Table, Tabs, Tag, Typography, message, type TableColumnsType, type TabsProps, } from 'antd' import AppLayout from '../../components/AppLayout/AppLayout' import MarkdownRenderer from '../../components/MarkdownRenderer/MarkdownRenderer' import { formatDateTimeZhCN } from '../../utils/datetime' import { type BGPAnomaly, type BGPBriefRecord, type BGPBriefRecordSummary, type BGPCollectorCoverage, type BGPEvent, type BGPIncident, type CollectorSummary, type EventSummary, type Summary, getSituationalAwarenessGateway, } from '../../services/situational-awareness' const { Title, Text } = Typography const OVERVIEW_OPTIONS = { incidentPageSize: 50, anomalyPageSize: 100, eventPageSize: 20, } const DEFAULT_BGP_TAB = 'collectors' const situationalAwarenessGateway = getSituationalAwarenessGateway() function severityColor(severity: string) { if (severity === 'critical') return 'red' if (severity === 'high') return 'orange' if (severity === 'medium') return 'gold' return 'blue' } function formatBriefOptions(records: BGPBriefRecordSummary[]) { return records.map((item) => ({ value: item.id, label: `${formatDateTimeZhCN(item.generated_at)} · ${item.model}`, })) } function sortBriefRecords(records: T[]) { return [...records].sort((left, right) => right.generated_at.localeCompare(left.generated_at)) } function renderCollectorLocation(_: unknown, record: BGPCollectorCoverage) { return [record.city, record.country].filter(Boolean).join(', ') || '-' } function renderCollectorLatestEvent(_: unknown, record: BGPCollectorCoverage) { const time = formatDateTimeZhCN(record.latest_observed_at) return record.latest_event_type ? `${record.latest_event_type} @ ${time}` : time } function renderCollectorScope(value: BGPCollectorCoverage['baseline_scope']) { const cities = value?.cities?.slice(0, 3).join(' / ') || '' const countries = value?.countries?.slice(0, 3).join(' / ') || '' return cities && countries ? `${cities} | ${countries}` : cities || countries || '-' } function renderIncidentCollectors(value: string[]) { return value && value.length > 0 ? `${value.length}个 (${value.slice(0, 3).join(', ')})` : '-' } function renderIncidentRegions(value: Array<{ country?: string; city?: string }>) { if (!value || value.length === 0) return '-' return value .slice(0, 3) .map((item) => [item.city, item.country].filter(Boolean).join(', ')) .join(' / ') } function renderIncidentCables(value: BGPIncident['related_cables']) { if (!value || value.length === 0) return '-' return value .slice(0, 2) .map((item) => { const landing = item.landing_point || [item.city, item.country].filter(Boolean).join(', ') const cable = item.cable_names && item.cable_names.length > 0 ? item.cable_names[0] : '附近登陆点' const distance = item.distance_km !== undefined ? ` ${item.distance_km}km` : '' return `${landing} (${cable}${distance})` }) .join(' / ') } function renderPercentage(value: number) { return `${Math.round((value || 0) * 100)}%` } function renderAnomalyAsn(_: unknown, record: BGPAnomaly) { if (record.origin_asn && record.new_origin_asn) { return `AS${record.origin_asn} -> AS${record.new_origin_asn}` } if (record.origin_asn) { return `AS${record.origin_asn}` } return '-' } function renderOptionalAsn(value: number | null) { return value ? `AS${value}` : '-' } const collectorColumns: TableColumnsType = [ { title: '观测站', dataIndex: 'collector', width: 120, }, { title: '位置', width: 180, render: renderCollectorLocation, }, { title: '近24h事件数', dataIndex: 'recent_24h_observation_count', width: 120, }, { title: '近7d事件数', dataIndex: 'recent_7d_observation_count', width: 120, }, { title: '前缀数', dataIndex: 'prefix_count', width: 120, }, { title: 'Origin ASN 数', dataIndex: 'origin_asn_count', width: 140, }, { title: '最近事件', width: 220, render: renderCollectorLatestEvent, }, { title: '日常覆盖范围', dataIndex: 'baseline_scope', width: 280, render: renderCollectorScope, }, ] const incidentColumns: TableColumnsType = [ { title: '开始时间', dataIndex: 'started_at', width: 180, render: (value: string | null) => formatDateTimeZhCN(value), }, { title: '类型', dataIndex: 'incident_type', width: 180, }, { title: '严重度', dataIndex: 'severity', width: 120, render: (value: string) => {value}, }, { title: '影响前缀', dataIndex: 'affected_prefixes', width: 200, render: (value: string[]) => (value && value.length > 0 ? value.join(', ') : '-'), }, { title: '观测站', dataIndex: 'affected_collectors', width: 180, render: renderIncidentCollectors, }, { title: '区域', dataIndex: 'affected_regions', width: 220, render: renderIncidentRegions, }, { title: '附近基础设施', dataIndex: 'related_cables', width: 260, render: renderIncidentCables, }, { title: '置信度', dataIndex: 'confidence', width: 120, render: renderPercentage, }, { title: '摘要', dataIndex: 'summary', width: 320, }, ] const anomalyColumns: TableColumnsType = [ { title: '时间', dataIndex: 'created_at', width: 180, render: (value: string | null) => formatDateTimeZhCN(value), }, { title: '类型', dataIndex: 'anomaly_type', width: 180, }, { title: '严重度', dataIndex: 'severity', width: 120, render: (value: string) => {value}, }, { title: '前缀', dataIndex: 'prefix', width: 180, render: (value: string | null) => value || '-', }, { title: 'ASN', key: 'asn', width: 160, render: renderAnomalyAsn, }, { title: '来源', dataIndex: 'source', width: 140, }, { title: '置信度', dataIndex: 'confidence', width: 120, render: renderPercentage, }, { title: '摘要', dataIndex: 'summary', width: 320, }, ] const eventColumns: TableColumnsType = [ { title: '时间', dataIndex: 'observed_at', width: 180, render: (value: string | null) => formatDateTimeZhCN(value), }, { title: '观测站', dataIndex: 'collector', width: 140, render: (value: string | null) => value || '-', }, { title: '类型', dataIndex: 'event_type', width: 120, }, { title: '前缀', dataIndex: 'prefix', width: 200, render: (value: string | null) => value || '-', }, { title: 'Origin ASN', dataIndex: 'origin_asn', width: 140, render: renderOptionalAsn, }, { title: 'Peer ASN', dataIndex: 'peer_asn', width: 140, render: renderOptionalAsn, }, ] async function loadInitialBrief(savedBriefs: BGPBriefRecordSummary[]) { const latestBrief = await situationalAwarenessGateway.getLatestBGPBrief() if (latestBrief) { return latestBrief } if (savedBriefs[0]) { return situationalAwarenessGateway.getBGPBrief(savedBriefs[0].id) } return null } function BGP() { const [messageApi, contextHolder] = message.useMessage() const [summaryLoading, setSummaryLoading] = useState(false) const [activeTab, setActiveTab] = useState(DEFAULT_BGP_TAB) const [incidents, setIncidents] = useState([]) const [anomalies, setAnomalies] = useState([]) const [events, setEvents] = useState([]) const [collectors, setCollectors] = useState([]) const [collectorsLoading, setCollectorsLoading] = useState(false) const [incidentsLoading, setIncidentsLoading] = useState(false) const [anomaliesLoading, setAnomaliesLoading] = useState(false) const [eventsLoading, setEventsLoading] = useState(false) const [incidentSummary, setIncidentSummary] = useState(null) const [eventSummary, setEventSummary] = useState(null) const [collectorSummary, setCollectorSummary] = useState(null) const [compactViewport, setCompactViewport] = useState(false) const [tableHeight, setTableHeight] = useState(360) const [briefLoading, setBriefLoading] = useState(false) const [briefDetailLoading, setBriefDetailLoading] = useState(false) const [brief, setBrief] = useState(null) const [briefOptions, setBriefOptions] = useState([]) const [selectedBriefId, setSelectedBriefId] = useState(null) const [briefListLoaded, setBriefListLoaded] = useState(false) const [briefModalOpen, setBriefModalOpen] = useState(false) const tableRegionRef = useRef(null) useEffect(() => { const load = async () => { setSummaryLoading(true) try { const summary = await situationalAwarenessGateway.getBGPSummary() setIncidentSummary(summary.incidentSummary) setEventSummary(summary.eventSummary) setCollectorSummary(summary.collectorSummary) } catch (error) { console.error('Failed to load BGP overview:', error) } finally { setSummaryLoading(false) } } void load() }, []) useEffect(() => { const loadTabData = async () => { if (activeTab === 'collectors' && collectors.length === 0 && !collectorsLoading) { setCollectorsLoading(true) try { setCollectors(await situationalAwarenessGateway.getBGPCollectors()) } catch (error) { console.error('Failed to load BGP collectors:', error) } finally { setCollectorsLoading(false) } return } if (activeTab === 'incidents' && incidents.length === 0 && !incidentsLoading) { setIncidentsLoading(true) try { setIncidents(await situationalAwarenessGateway.getBGPIncidents(OVERVIEW_OPTIONS.incidentPageSize)) } catch (error) { console.error('Failed to load BGP incidents:', error) } finally { setIncidentsLoading(false) } return } if (activeTab === 'anomalies' && anomalies.length === 0 && !anomaliesLoading) { setAnomaliesLoading(true) try { setAnomalies(await situationalAwarenessGateway.getBGPAnomalies(OVERVIEW_OPTIONS.anomalyPageSize)) } catch (error) { console.error('Failed to load BGP anomalies:', error) } finally { setAnomaliesLoading(false) } return } if (activeTab === 'events' && events.length === 0 && !eventsLoading) { setEventsLoading(true) try { setEvents(await situationalAwarenessGateway.getBGPEvents(OVERVIEW_OPTIONS.eventPageSize)) } catch (error) { console.error('Failed to load BGP events:', error) } finally { setEventsLoading(false) } return } if (activeTab === 'brief' && !briefListLoaded && !briefDetailLoading) { setBriefDetailLoading(true) try { const savedBriefs = sortBriefRecords(await situationalAwarenessGateway.listBGPBriefs()) const initialBrief = await loadInitialBrief(savedBriefs) setBriefOptions(savedBriefs) setBrief(initialBrief) setSelectedBriefId(initialBrief?.id || savedBriefs[0]?.id || null) setBriefListLoaded(true) } catch (error) { console.error('Failed to load BGP briefs:', error) } finally { setBriefDetailLoading(false) } } } void loadTabData() }, [ activeTab, anomalies.length, anomaliesLoading, briefDetailLoading, briefListLoaded, collectors.length, collectorsLoading, events.length, eventsLoading, incidents.length, incidentsLoading, ]) useEffect(() => { const updateViewportMode = () => { if (typeof window === 'undefined') return setCompactViewport(window.innerHeight <= 860 || window.innerWidth <= 1440) } updateViewportMode() window.addEventListener('resize', updateViewportMode) return () => window.removeEventListener('resize', updateViewportMode) }, []) useEffect(() => { const updateTableHeight = () => { const regionHeight = tableRegionRef.current?.offsetHeight || 0 const minimumHeight = compactViewport ? 180 : 240 const verticalOffset = compactViewport ? 40 : 52 setTableHeight(Math.max(minimumHeight, regionHeight - verticalOffset)) } updateTableHeight() if (typeof ResizeObserver === 'undefined') { return undefined } const observer = new ResizeObserver(updateTableHeight) if (tableRegionRef.current) { observer.observe(tableRegionRef.current) } return () => observer.disconnect() }, [compactViewport, collectors.length, incidents.length, anomalies.length, events.length]) const summaryItems = [ { label: '近24h事件', value: collectorSummary?.recent_24h_events || 0 }, { label: '活跃观测站', value: collectorSummary?.active_collectors || 0 }, { label: '观测前缀', value: collectorSummary?.observed_prefixes || eventSummary?.prefix_count || 0 }, { label: '事件总数', value: incidentSummary?.total || 0 }, { label: '活跃事件', value: incidentSummary?.by_status?.active || 0 }, { label: '严重事件', value: incidentSummary?.by_severity?.critical || 0 }, ] const handleGenerateBrief = async () => { setBriefLoading(true) try { const record = await situationalAwarenessGateway.generateBGPBrief() setBrief(record) setSelectedBriefId(record.id) setBriefOptions((current) => sortBriefRecords([record, ...current.filter((item) => item.id !== record.id)])) setBriefListLoaded(true) messageApi.success('BGP AI 简报已生成') } catch (error) { console.error('Failed to generate BGP brief:', error) messageApi.error('BGP AI 简报生成失败,请检查 AI Provider 或稍后再试') } finally { setBriefLoading(false) } } const handleBriefSelectionChange = async (briefId: string) => { setSelectedBriefId(briefId) setBriefDetailLoading(true) try { setBrief(await situationalAwarenessGateway.getBGPBrief(briefId)) } catch (error) { console.error('Failed to load saved BGP brief:', error) messageApi.error('BGP AI 简报加载失败,请稍后再试') } finally { setBriefDetailLoading(false) } } const briefTabContent = (
BGP AI 简报
每次生成都会保存为 Markdown。打开页面时默认展示最近一次简报,也可以从历史列表切换查看。