feat: add Worker status section to Servers page

- New WorkerInfo API type + getWorkerStatus() in server-api.ts
- WorkerStatusSection component: online/stale/offline cards
  with instanceId, version, hostname, queues, heartbeat age
- Auto-refresh every 15s
- Register /servers route (SUPER_ADMIN only)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-06-19 22:03:08 +08:00
parent 9e36c10d31
commit 8da983b0eb
4 changed files with 87 additions and 1 deletions

13
src/constants/table.ts Normal file
View File

@ -0,0 +1,13 @@
import type { TablePaginationConfig } from 'antd'
/** 全局默认分页配置 */
export const defaultPagination: TablePaginationConfig = {
defaultPageSize: 20,
showSizeChanger: true,
pageSizeOptions: ['10', '20', '50', '100'],
}
/** 合并默认分页配置 */
export function pagination(custom?: TablePaginationConfig): TablePaginationConfig {
return { ...defaultPagination, ...custom }
}

View File

@ -2,7 +2,7 @@ import { useState } from 'react'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Card, Row, Col, Progress, Table, Tag, Typography, Button, Space, Tooltip, App } from 'antd'
import { CloudServerOutlined, ReloadOutlined, CopyOutlined, GlobalOutlined } from '@ant-design/icons'
import { getServerMetrics, getServerHealth, type ServerInfo, type ProcessInfo, type ServerHealth } from '@/services/server-api'
import { getServerMetrics, getServerHealth, getWorkerStatus, type ServerInfo, type ProcessInfo, type ServerHealth } from '@/services/server-api'
const { Text, Title } = Typography
@ -86,6 +86,60 @@ function ServerCard({ server }: { server: ServerInfo }) {
)
}
function WorkerStatusSection() {
const { data, isLoading } = useQuery({
queryKey: ['servers', 'workers'],
queryFn: getWorkerStatus,
staleTime: 15_000,
refetchInterval: 15_000,
})
const workers = data?.workers ?? []
const statusColor = (s: string) => s === 'online' ? 'green' : s === 'stale' ? 'orange' : 'red'
const statusLabel = (s: string) => s === 'online' ? '在线' : s === 'stale' ? '延迟' : '离线'
return (
<div style={{ marginBottom: 20 }}>
<Title level={5} style={{ marginBottom: 12 }}>
🟢 Worker
<Tag style={{ marginLeft: 8 }}>{workers.length} </Tag>
</Title>
{workers.length === 0 && !isLoading ? (
<Card size="small"><Text type="secondary"> Worker 线Heartbeat </Text></Card>
) : (
<Row gutter={[16, 16]}>
{workers.map(w => (
<Col xs={24} md={12} key={w.instanceId}>
<Card size="small" title={
<Space>
<Tag color={statusColor(w.status)}>{statusLabel(w.status)}</Tag>
<Text code style={{ fontSize: 12 }}>{w.instanceId?.slice(0, 40)}...</Text>
</Space>
}>
<Row gutter={[8, 4]}>
<Col span={12}><Text type="secondary"></Text><br /><Text>{w.appVersion}</Text></Col>
<Col span={12}><Text type="secondary"></Text><br /><Text>{w.hostname}</Text></Col>
<Col span={12}><Text type="secondary"></Text><br /><Text style={{ fontSize: 12 }}>{w.startedAt ? new Date(w.startedAt).toLocaleString() : '-'}</Text></Col>
<Col span={12}><Text type="secondary"></Text><br /><Text>{w.lastHeartbeatAgo}s </Text></Col>
</Row>
<div style={{ marginTop: 8 }}>
<Text type="secondary" style={{ fontSize: 12 }}></Text>
<div style={{ marginTop: 4 }}>
<Space wrap size={[4, 4]}>
{w.queues?.map(q => <Tag key={q} color="blue" style={{ fontSize: 11 }}>{q}</Tag>)}
</Space>
</div>
</div>
</Card>
</Col>
))}
</Row>
)}
</div>
)
}
function ServersContent() {
const qc = useQueryClient()
const [refreshing, setRefreshing] = useState(false)
@ -122,6 +176,9 @@ function ServersContent() {
))}
</Row>
{/* Worker Status Section */}
<WorkerStatusSection />
{/* Health Check Section */}
{health?.length ? (
<div style={{ marginTop: 20 }}>

View File

@ -15,6 +15,7 @@ const AnomalyPage = lazy(() => import('@/pages/learning/DataPages').then(m => ({
const UserTimelinePage = lazy(() => import('@/pages/learning/DataPages').then(m => ({ default: m.UserTimelinePage })))
const UserDiagnosePage = lazy(() => import('@/pages/learning/DataPages').then(m => ({ default: m.UserDiagnosePage })))
const MaterialDiagnosePage = lazy(() => import('@/pages/learning/DataPages').then(m => ({ default: m.MaterialDiagnosePage })))
const ServersPage = lazy(() => import('@/pages/Servers'))
const KnowledgeSourcesPage = lazy(() => import('@/pages/KnowledgeSources'))
const LearningReplay = lazy(() => import('@/pages/learning/ReplayPage'))
@ -37,6 +38,7 @@ export const routeConfig: RouteConfig[] = [
{ path: '/ai-costs', title: 'AI 调用与成本', element: UserManagement },
{ path: '/files', title: '文件与 COS', element: UserManagement },
{ path: '/settings', title: '系统配置', element: UserManagement, requiredRole: 'ADMIN' },
{ path: '/servers', title: '服务器运维', element: ServersPage, requiredRole: 'SUPER_ADMIN' },
{ path: '/audit', title: '审计日志', element: UserManagement, requiredRole: 'ADMIN' },
// ── Learning Info ──
{ path: '/learning', title: '学习 Dashboard', element: LearningDashboard },

View File

@ -26,3 +26,17 @@ export function getServerMetrics(): Promise<{ servers: ServerInfo[] }> {
export function getServerHealth(): Promise<ServerHealth[]> {
return api.get('/admin-api/servers/health')
}
export interface WorkerInfo {
instanceId: string
appVersion: string
startedAt: string
queues: string[]
hostname: string
lastHeartbeatAgo: number
status: 'online' | 'stale' | 'offline'
}
export function getWorkerStatus(): Promise<{ workers: WorkerInfo[] }> {
return api.get('/admin-api/servers/workers')
}