feat: add aiprovider service foundation

This commit is contained in:
linkong
2026-04-07 17:30:27 +08:00
parent 3f5505f03e
commit 9a50e72bd1
42 changed files with 1766 additions and 166 deletions

View File

@@ -1,100 +1,20 @@
import { useEffect, useState } from 'react'
import { Alert, Card, Col, Row, Space, Statistic, Table, Tag, Typography } from 'antd'
import axios from 'axios'
import AppLayout from '../../components/AppLayout/AppLayout'
import { formatDateTimeZhCN } from '../../utils/datetime'
import {
getSituationalAwarenessGateway,
type BGPAnomaly,
type BGPCollectorCoverage,
type BGPEvent,
type BGPIncident,
type CollectorSummary,
type EventSummary,
type Summary,
} from '../../services/situational-awareness'
const { Title, Text } = Typography
interface BGPAnomaly {
id: number
source: string
anomaly_type: string
severity: string
status: string
prefix: string | null
origin_asn: number | null
new_origin_asn: number | null
confidence: number
summary: string
created_at: string | null
}
interface BGPEvent {
id: number
collector: string | null
event_type: string
prefix: string | null
origin_asn: number | null
peer_asn: number | null
observed_at: string | null
}
interface BGPCollectorCoverage {
collector: string
city?: string | null
country?: string | null
observation_count: number
recent_24h_observation_count: number
recent_7d_observation_count: number
prefix_count: number
recent_24h_prefix_count: number
recent_7d_prefix_count: number
origin_asn_count: number
peer_asn_count: number
latest_observed_at: string | null
latest_event_type: string | null
baseline_scope: {
countries: string[]
cities: string[]
}
}
interface BGPIncident {
id: number
incident_type: string
title: string
summary: string
severity: string
status: string
confidence: number
affected_prefixes: string[]
affected_asns: number[]
affected_collectors: string[]
affected_regions: Array<{ country?: string; city?: string }>
related_cables: Array<{
landing_point?: string
city?: string
country?: string
distance_km?: number
cable_names?: string[]
}>
created_at: string | null
started_at: string | null
}
interface Summary {
total: number
by_type: Record<string, number>
by_severity: Record<string, number>
by_status: Record<string, number>
}
interface EventSummary {
total: number
collector_count: number
prefix_count: number
by_type: Record<string, number>
}
interface CollectorSummary {
total: number
active_collectors: number
observed_prefixes: number
observed_origins: number
recent_24h_events: number
recent_7d_events: number
}
const situationalAwarenessGateway = getSituationalAwarenessGateway()
function severityColor(severity: string) {
if (severity === 'critical') return 'red'
@@ -117,22 +37,20 @@ function BGP() {
const load = async () => {
setLoading(true)
try {
const [incidentsRes, incidentSummaryRes, anomaliesRes, eventsRes, eventSummaryRes, collectorsRes, collectorSummaryRes] = await Promise.all([
axios.get('/api/v1/bgp/incidents', { params: { page_size: 50 } }),
axios.get('/api/v1/bgp/incidents/summary'),
axios.get('/api/v1/bgp/anomalies', { params: { page_size: 100 } }),
axios.get('/api/v1/bgp/events', { params: { page_size: 20 } }),
axios.get('/api/v1/bgp/events/summary'),
axios.get('/api/v1/bgp/collectors'),
axios.get('/api/v1/bgp/collectors/summary'),
])
setIncidents(incidentsRes.data.data || [])
setIncidentSummary(incidentSummaryRes.data)
setAnomalies(anomaliesRes.data.data || [])
setEvents(eventsRes.data.data || [])
setEventSummary(eventSummaryRes.data)
setCollectors(collectorsRes.data.data || [])
setCollectorSummary(collectorSummaryRes.data)
const snapshot = await situationalAwarenessGateway.getBGPOverview({
incidentPageSize: 50,
anomalyPageSize: 100,
eventPageSize: 20,
})
setIncidents(snapshot.incidents)
setIncidentSummary(snapshot.incidentSummary)
setAnomalies(snapshot.anomalies)
setEvents(snapshot.events)
setEventSummary(snapshot.eventSummary)
setCollectors(snapshot.collectors)
setCollectorSummary(snapshot.collectorSummary)
} catch (error) {
console.error('Failed to load BGP overview:', error)
} finally {
setLoading(false)
}

View File

@@ -47,16 +47,22 @@ interface RestartTaskLogs {
lines: string[]
}
type RestartAction = 'restart-backend' | 'restart-database' | 'restart-system'
type RestartAction = 'restart-backend' | 'restart-ai-provider' | 'restart-database' | 'restart-system'
type RestartStage = 'confirming' | 'waiting_for_shutdown' | 'waiting_for_recovery' | 'recovered' | 'failed' | 'timeout'
const RESTART_ACTION_OPTIONS: Array<{ value: RestartAction; label: string; description: string; command: string }> = [
{
value: 'restart-backend',
label: '重启服务器',
label: '重启后端',
description: '只重启后端服务,页面通常会短暂失联后自动恢复。',
command: './planet.sh restart -b',
},
{
value: 'restart-ai-provider',
label: '重启 AI Provider',
description: '只重启 AI Provider 适配服务,前端页面通常保持在线。',
command: './planet.sh restart -a',
},
{
value: 'restart-database',
label: '重启数据库',
@@ -77,6 +83,11 @@ const RESTART_GUIDE_LINES: Record<RestartAction, string[]> = {
'[ctl] handing restart to detached runner',
'[ctl] waiting for backend health recovery',
],
'restart-ai-provider': [
'[ctl] preparing ai provider restart task',
'[ctl] handing restart to detached runner',
'[ctl] waiting for ai provider health recovery',
],
'restart-database': [
'[ctl] preparing database restart task',
'[ctl] restarting PostgreSQL and Redis containers',
@@ -94,6 +105,9 @@ const RESTART_GUIDE_LINES: Record<RestartAction, string[]> = {
let cachedDashboardStats: Stats | null = null
function getRestartConfirmMessage(action: RestartAction): string {
if (action === 'restart-ai-provider') {
return '将重启 AI Provider 适配服务,页面通常保持在线,但 AI 分析请求会短暂不可用。'
}
if (action === 'restart-database') {
return '将重启 PostgreSQL 和 Redis页面通常保持在线但相关请求可能短暂波动。'
}
@@ -200,6 +214,8 @@ function Dashboard() {
setRestartMessage(
restartAction === 'restart-system'
? '已发送完全重启指令,页面可能暂时失联,恢复后会自动刷新。'
: restartAction === 'restart-ai-provider'
? '已发送 AI Provider 重启指令,正在等待 AI 服务恢复。'
: '已发送重启指令,正在等待服务进入重启流程。'
)
setRestartLogs((current) => [...current, `任务已创建: ${res.data.task_id}`])

View File

@@ -0,0 +1,46 @@
import axios from 'axios'
import type { SituationalAwarenessGateway } from './port'
import type {
BGPAnomaly,
BGPCollectorCoverage,
BGPEvent,
BGPIncident,
BGPOverviewOptions,
BGPOverviewSnapshot,
CollectorSummary,
EventSummary,
ListResponse,
Summary,
} from './types'
const API_BASE_URL = (import.meta as any).env?.VITE_API_URL || '/api/v1'
export class HttpSituationalAwarenessGateway implements SituationalAwarenessGateway {
async getBGPOverview(options: BGPOverviewOptions = {}): Promise<BGPOverviewSnapshot> {
const {
incidentPageSize = 50,
anomalyPageSize = 100,
eventPageSize = 20,
} = options
const [incidentsRes, incidentSummaryRes, anomaliesRes, eventsRes, eventSummaryRes, collectorsRes, collectorSummaryRes] = await Promise.all([
axios.get<ListResponse<BGPIncident>>(`${API_BASE_URL}/bgp/incidents`, { params: { page_size: incidentPageSize } }),
axios.get<Summary>(`${API_BASE_URL}/bgp/incidents/summary`),
axios.get<ListResponse<BGPAnomaly>>(`${API_BASE_URL}/bgp/anomalies`, { params: { page_size: anomalyPageSize } }),
axios.get<ListResponse<BGPEvent>>(`${API_BASE_URL}/bgp/events`, { params: { page_size: eventPageSize } }),
axios.get<EventSummary>(`${API_BASE_URL}/bgp/events/summary`),
axios.get<ListResponse<BGPCollectorCoverage>>(`${API_BASE_URL}/bgp/collectors`),
axios.get<CollectorSummary>(`${API_BASE_URL}/bgp/collectors/summary`),
])
return {
incidents: incidentsRes.data.data || [],
incidentSummary: incidentSummaryRes.data,
anomalies: anomaliesRes.data.data || [],
events: eventsRes.data.data || [],
eventSummary: eventSummaryRes.data,
collectors: collectorsRes.data.data || [],
collectorSummary: collectorSummaryRes.data,
}
}
}

View File

@@ -0,0 +1,23 @@
import type { SituationalAwarenessGateway } from './port'
import { HttpSituationalAwarenessGateway } from './http-gateway'
import { MockSituationalAwarenessGateway } from './mock-gateway'
export * from './types'
export type { SituationalAwarenessGateway } from './port'
let singleton: SituationalAwarenessGateway | null = null
export function createSituationalAwarenessGateway(): SituationalAwarenessGateway {
const provider = (import.meta as any).env?.VITE_SA_GATEWAY || 'http'
if (provider === 'mock') {
return new MockSituationalAwarenessGateway()
}
return new HttpSituationalAwarenessGateway()
}
export function getSituationalAwarenessGateway(): SituationalAwarenessGateway {
if (!singleton) {
singleton = createSituationalAwarenessGateway()
}
return singleton
}

View File

@@ -0,0 +1,35 @@
import type { SituationalAwarenessGateway } from './port'
import type { BGPOverviewOptions, BGPOverviewSnapshot } from './types'
const EMPTY_SNAPSHOT: BGPOverviewSnapshot = {
incidents: [],
incidentSummary: {
total: 0,
by_type: {},
by_severity: {},
by_status: {},
},
anomalies: [],
events: [],
eventSummary: {
total: 0,
collector_count: 0,
prefix_count: 0,
by_type: {},
},
collectors: [],
collectorSummary: {
total: 0,
active_collectors: 0,
observed_prefixes: 0,
observed_origins: 0,
recent_24h_events: 0,
recent_7d_events: 0,
},
}
export class MockSituationalAwarenessGateway implements SituationalAwarenessGateway {
async getBGPOverview(_options: BGPOverviewOptions = {}): Promise<BGPOverviewSnapshot> {
return EMPTY_SNAPSHOT
}
}

View File

@@ -0,0 +1,5 @@
import type { BGPOverviewOptions, BGPOverviewSnapshot } from './types'
export interface SituationalAwarenessGateway {
getBGPOverview(options?: BGPOverviewOptions): Promise<BGPOverviewSnapshot>
}

View File

@@ -0,0 +1,112 @@
export interface BGPAnomaly {
id: number
source: string
anomaly_type: string
severity: string
status: string
prefix: string | null
origin_asn: number | null
new_origin_asn: number | null
confidence: number
summary: string
created_at: string | null
}
export interface BGPEvent {
id: number
collector: string | null
event_type: string
prefix: string | null
origin_asn: number | null
peer_asn: number | null
observed_at: string | null
}
export interface BGPCollectorCoverage {
collector: string
city?: string | null
country?: string | null
observation_count: number
recent_24h_observation_count: number
recent_7d_observation_count: number
prefix_count: number
recent_24h_prefix_count: number
recent_7d_prefix_count: number
origin_asn_count: number
peer_asn_count: number
latest_observed_at: string | null
latest_event_type: string | null
baseline_scope: {
countries: string[]
cities: string[]
}
}
export interface BGPIncident {
id: number
incident_type: string
title: string
summary: string
severity: string
status: string
confidence: number
affected_prefixes: string[]
affected_asns: number[]
affected_collectors: string[]
affected_regions: Array<{ country?: string; city?: string }>
related_cables: Array<{
landing_point?: string
city?: string
country?: string
distance_km?: number
cable_names?: string[]
}>
created_at: string | null
started_at: string | null
}
export interface Summary {
total: number
by_type: Record<string, number>
by_severity: Record<string, number>
by_status: Record<string, number>
}
export interface EventSummary {
total: number
collector_count: number
prefix_count: number
by_type: Record<string, number>
}
export interface CollectorSummary {
total: number
active_collectors: number
observed_prefixes: number
observed_origins: number
recent_24h_events: number
recent_7d_events: number
}
export interface ListResponse<T> {
total: number
page?: number
page_size?: number
data: T[]
}
export interface BGPOverviewSnapshot {
incidents: BGPIncident[]
incidentSummary: Summary | null
anomalies: BGPAnomaly[]
events: BGPEvent[]
eventSummary: EventSummary | null
collectors: BGPCollectorCoverage[]
collectorSummary: CollectorSummary | null
}
export interface BGPOverviewOptions {
incidentPageSize?: number
anomalyPageSize?: number
eventPageSize?: number
}