feat: add aiprovider service foundation
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
VITE_API_URL=/api/v1
|
||||
VITE_WS_URL=ws://localhost:8000/ws
|
||||
VITE_WS_URL=
|
||||
VITE_SA_GATEWAY=http
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "planet-frontend",
|
||||
"version": "0.22.14",
|
||||
"version": "0.23.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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}`])
|
||||
|
||||
46
frontend/src/services/situational-awareness/http-gateway.ts
Normal file
46
frontend/src/services/situational-awareness/http-gateway.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
23
frontend/src/services/situational-awareness/index.ts
Normal file
23
frontend/src/services/situational-awareness/index.ts
Normal 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
|
||||
}
|
||||
35
frontend/src/services/situational-awareness/mock-gateway.ts
Normal file
35
frontend/src/services/situational-awareness/mock-gateway.ts
Normal 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
|
||||
}
|
||||
}
|
||||
5
frontend/src/services/situational-awareness/port.ts
Normal file
5
frontend/src/services/situational-awareness/port.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { BGPOverviewOptions, BGPOverviewSnapshot } from './types'
|
||||
|
||||
export interface SituationalAwarenessGateway {
|
||||
getBGPOverview(options?: BGPOverviewOptions): Promise<BGPOverviewSnapshot>
|
||||
}
|
||||
112
frontend/src/services/situational-awareness/types.ts
Normal file
112
frontend/src/services/situational-awareness/types.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user