feat: add realtime load admin page

This commit is contained in:
2026-05-12 10:18:34 +08:00
parent 7e220b7477
commit ddfd4f9035
7 changed files with 219 additions and 150 deletions
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState, type FormEvent, type ReactNode } from 'react';
import { Boxes, CheckCircle2, Gauge, Globe2, KeyRound, Pencil, Plus, RotateCcw, Search, ServerCog, ShieldCheck, SlidersHorizontal, Trash2, X } from 'lucide-react';
import type { BaseModelCatalogItem, CatalogProvider, IntegrationPlatform, ModelRateLimitStatus, PlatformModel, PricingRuleSet } from '@easyai-ai-gateway/contracts';
import { Boxes, CheckCircle2, Globe2, KeyRound, Pencil, Plus, RotateCcw, Search, ServerCog, ShieldCheck, SlidersHorizontal, Trash2, X } from 'lucide-react';
import type { BaseModelCatalogItem, CatalogProvider, IntegrationPlatform, PlatformModel, PricingRuleSet } from '@easyai-ai-gateway/contracts';
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, ConfirmDialog, EmptyState, FormDialog, Input, Label, ScreenMessage, Select, Table, TableCell, TableHead, TableRow } from '../../components/ui';
import type { LoadState, PlatformWithModelsInput } from '../../types';
import {
@@ -23,8 +23,6 @@ import { ModelCatalogCard } from './ModelCatalogCard';
export function PlatformManagementPanel(props: {
baseModels: BaseModelCatalogItem[];
message: string;
modelRateLimits: ModelRateLimitStatus[];
modelRateLimitsUpdatedAt: number | null;
networkProxyConfig: { globalHttpProxy?: string; globalHttpProxySet: boolean; globalHttpProxySource?: string } | null;
platforms: IntegrationPlatform[];
platformModels: PlatformModel[];
@@ -37,7 +35,7 @@ export function PlatformManagementPanel(props: {
const defaultProvider = props.providers[0]?.providerKey ?? props.baseModels[0]?.providerKey ?? '';
const [now, setNow] = useState(() => Date.now());
const [dialogOpen, setDialogOpen] = useState(false);
const [viewMode, setViewMode] = useState<'platforms' | 'models' | 'limits'>('platforms');
const [viewMode, setViewMode] = useState<'platforms' | 'models'>('platforms');
const [modelQuery, setModelQuery] = useState('');
const [selectedPlatformId, setSelectedPlatformId] = useState('');
const [validationMessage, setValidationMessage] = useState('');
@@ -174,13 +172,12 @@ export function PlatformManagementPanel(props: {
</Button>
</CardHeader>
<CardContent>
<div className="platformViewTabs">
<button type="button" data-active={viewMode === 'platforms'} onClick={() => setViewMode('platforms')}></button>
<button type="button" data-active={viewMode === 'models'} onClick={() => setViewMode('models')}></button>
<button type="button" data-active={viewMode === 'limits'} onClick={() => setViewMode('limits')}></button>
</div>
{viewMode === 'platforms' ? (
<PlatformTable
<div className="platformViewTabs">
<button type="button" data-active={viewMode === 'platforms'} onClick={() => setViewMode('platforms')}></button>
<button type="button" data-active={viewMode === 'models'} onClick={() => setViewMode('models')}></button>
</div>
{viewMode === 'platforms' ? (
<PlatformTable
now={now}
platformModelCount={platformModelCount}
platforms={props.platforms}
@@ -190,8 +187,8 @@ export function PlatformManagementPanel(props: {
onCreate={openCreateDialog}
onEdit={openEditDialog}
/>
) : viewMode === 'models' ? (
<PlatformModelTable
) : (
<PlatformModelTable
baseModels={props.baseModels}
modelQuery={modelQuery}
models={filteredPlatformModels}
@@ -203,13 +200,6 @@ export function PlatformManagementPanel(props: {
now={now}
onPlatformChange={setSelectedPlatformId}
/>
) : (
<RateLimitStatusTable
now={now}
platformMap={platformMap}
statuses={props.modelRateLimits}
updatedAt={props.modelRateLimitsUpdatedAt}
/>
)}
{props.message && <p className="formMessage">{props.message}</p>}
</CardContent>
@@ -574,65 +564,6 @@ function PlatformModelTable(props: {
<EmptyState title="没有匹配的模型" description="换个平台或搜索关键词试试。" />
)
)}
</section>
);
}
function RateLimitStatusTable(props: { statuses: ModelRateLimitStatus[]; platformMap: Map<string, IntegrationPlatform>; now: number; updatedAt: number | null }) {
if (!props.statuses.length) {
return <EmptyState title="暂无限流状态" description="模型产生请求后会在这里显示实时 RPM、TPM 和并发窗口。" />;
}
return (
<section className="platformLimitView">
<div className="platformLimitHeader">
<span><Gauge size={15} /></span>
<small> 3 TPM + {formatTimeOfDay(props.updatedAt)}</small>
</div>
<div className="platformLimitTableViewport">
<Table className="platformDataTable platformLimitTable">
<TableRow className="shTableHeader">
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="platformLimitMetricHead platformLimitNumberHead" title="正在执行 / 并发上限 / 排队任务">
<span></span>
<small> / / </small>
</TableHead>
<TableHead className="platformLimitNumberHead">TPM</TableHead>
<TableHead className="platformLimitNumberHead">RPM</TableHead>
<TableHead className="platformLimitStatusHead"></TableHead>
<TableHead className="platformLimitNumberHead"></TableHead>
</TableRow>
{props.statuses.map((status) => {
const platform = props.platformMap.get(status.platformId);
return (
<TableRow key={status.platformModelId}>
<TableCell>
<span className="platformTableName">
<strong>{status.displayName || status.modelAlias || status.modelName}</strong>
<small>{status.providerModelName || status.modelName}</small>
</span>
</TableCell>
<TableCell>
<span className="platformTableName">
<strong>{platform ? platformDisplayName(platform) : status.platformName}</strong>
<small>{status.provider}</small>
</span>
</TableCell>
<TableCell className="platformLimitNumberCell">{concurrencyMetricCell(status)}</TableCell>
<TableCell className="platformLimitNumberCell">{metricCell(status.tpm, true)}</TableCell>
<TableCell className="platformLimitNumberCell">{metricCell(status.rpm)}</TableCell>
<TableCell className="platformLimitStatusCell">{modelRuntimeStatusCell(status, props.now)}</TableCell>
<TableCell className="platformLimitNumberCell">
<span className="rateLoadCell" data-overloaded={status.loadRatio > 0.8 ? 'true' : undefined}>
<strong>{formatPercent(status.loadRatio)}</strong>
<span className="rateLoadTrack"><i style={{ width: `${Math.min(status.loadRatio * 100, 100)}%` }} /></span>
</span>
</TableCell>
</TableRow>
);
})}
</Table>
</div>
</section>
);
}
@@ -1200,65 +1131,7 @@ function rateLimitMetricText(metric: string) {
concurrent: '并发',
queue_size: '队列',
};
return labels[metric] ?? metric;
}
function metricCell(metric: ModelRateLimitStatus['rpm'], includeReserved = false) {
if (!metric.limited) return <span className="rateMetricCell"><strong>{formatLimit(metric.currentValue)} / </strong><small>{includeReserved ? reservedMetricText(metric) : '未配置上限'}</small></span>;
return (
<span className="rateMetricCell">
<strong>{formatLimit(metric.currentValue)} / {formatLimit(metric.limitValue)}</strong>
<small>{includeReserved ? reservedMetricText(metric) : `窗口 ${formatPercent(metric.ratio)}`}</small>
</span>
);
}
function concurrencyMetricCell(status: ModelRateLimitStatus) {
const queuedTasks = status.queuedTasks ?? 0;
const limitText = status.concurrent.limited ? formatLimit(status.concurrent.limitValue) : '不限';
return (
<span className="rateMetricCell" title="正在执行 / 并发上限 / 排队任务">
<strong>{formatLimit(status.concurrent.currentValue)} / {limitText} / {formatLimit(queuedTasks)}</strong>
</span>
);
}
function reservedMetricText(metric: ModelRateLimitStatus['rpm']) {
return `已结算 ${formatLimit(metric.usedValue)} + 预占 ${formatLimit(metric.reservedValue)}`;
}
function formatTimeOfDay(timestamp: number | null) {
if (!timestamp) return '暂无';
const date = new Date(timestamp);
const pad = (value: number) => String(value).padStart(2, '0');
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}
function modelRuntimeStatusCell(status: ModelRateLimitStatus, now: number) {
const modelCooldownMs = cooldownRemainingMs(status.modelCooldownUntil, now);
const platformCooldownMs = cooldownRemainingMs(status.platformCooldownUntil, now);
if (modelCooldownMs > 0) {
return (
<span className="platformTableName">
<strong><Badge variant="warning"></Badge></strong>
<small> {formatCooldownRemaining(modelCooldownMs)}</small>
</span>
);
}
if (platformCooldownMs > 0) {
return (
<span className="platformTableName">
<strong><Badge variant="warning"></Badge></strong>
<small> {formatCooldownRemaining(platformCooldownMs)}</small>
</span>
);
}
return (
<span className="platformTableName">
<strong><Badge variant={status.enabled ? 'success' : 'secondary'}>{status.enabled ? '可用' : '已停用'}</Badge></strong>
<small>{status.enabled ? '参与路由' : '不参与路由'}</small>
</span>
);
return labels[metric] ?? metric;
}
function cooldownRemainingMs(cooldownUntil: string | undefined, now: number) {
@@ -1275,11 +1148,6 @@ function formatCooldownRemaining(milliseconds: number) {
return `${Math.max(seconds, 1)}`;
}
function formatPercent(value: number) {
if (!Number.isFinite(value) || value <= 0) return '0%';
return `${trimNumber(value * 100)}%`;
}
function platformRuntimeSummary(platform: IntegrationPlatform) {
const retryPolicy = platform.retryPolicy ?? {};
const retryEnabled = readBoolean(retryPolicy, 'enabled', true);