diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 0b2bcc4..a93a750 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -245,7 +245,7 @@ export function App() { void ensureRouteData(token); }, [activePage, adminSection, taskListRequestKey, transactionListRequestKey, workspaceSection, token]); useEffect(() => { - if (!token || activePage !== 'admin' || adminSection !== 'platforms') return undefined; + if (!token || activePage !== 'admin' || adminSection !== 'realtimeLoad') return undefined; const timer = window.setInterval(() => { void Promise.all([listModelRateLimitStatuses(token), listPlatforms(token)]) .then(([rateLimitResponse, platformResponse]) => { @@ -1179,7 +1179,9 @@ function dataKeysForRoute( case 'baseModels': return ['baseModels', 'providers', 'pricingRuleSets', 'runtimePolicySets']; case 'platforms': - return ['platforms', 'models', 'modelRateLimits', 'providers', 'baseModels', 'pricingRuleSets', 'networkProxyConfig']; + return ['platforms', 'models', 'providers', 'baseModels', 'pricingRuleSets', 'networkProxyConfig']; + case 'realtimeLoad': + return ['platforms', 'modelRateLimits']; case 'tenants': return ['tenants', 'userGroups']; case 'users': diff --git a/apps/web/src/navigation.ts b/apps/web/src/navigation.ts index f6b365d..1caf236 100644 --- a/apps/web/src/navigation.ts +++ b/apps/web/src/navigation.ts @@ -26,8 +26,8 @@ export const primaryModules = [ { title: '管理工作台', path: '/admin', - description: '租户、用户、用户组、全局模型、平台、限流、重试、队列和回调 outbox。', - items: ['租户管理', '用户管理', '用户组策略', '全局模型', '队列限流'], + description: '租户、用户、用户组、全局模型、平台、实时负载、重试、队列和回调 outbox。', + items: ['租户管理', '用户管理', '用户组策略', '全局模型', '实时负载'], }, { title: 'API 文档', @@ -50,6 +50,7 @@ export const adminPages = [ { title: '用户组策略', path: '/admin/user-groups', description: '用户组成员、充值折扣、调用折扣、TPM/RPM/并发和队列优先级。' }, { title: '全局模型配置', path: '/admin/models/global', description: '基准模型库、能力 schema、基准定价和默认限流模板。' }, { title: '平台管理', path: '/admin/platforms', description: '平台 CRUD、凭证、默认折扣、平台模型、限流和重试策略。' }, + { title: '实时负载', path: '/admin/realtime-load', description: '按平台模型查看实时 RPM、TPM、并发、排队和冷却状态。' }, { title: '运行与队列', path: '/admin/runtime/queues', description: 'TPM/RPM 窗口、并发 lease、cooldown、任务恢复和队列积压。' }, { title: '回调与结算', path: '/admin/callbacks', description: '任务进度 callback outbox、结算 outbox、失败重试和手动 replay。' }, ]; diff --git a/apps/web/src/pages/AdminPage.tsx b/apps/web/src/pages/AdminPage.tsx index 036ee7c..56f5f32 100644 --- a/apps/web/src/pages/AdminPage.tsx +++ b/apps/web/src/pages/AdminPage.tsx @@ -25,6 +25,7 @@ import { TenantsPanel, UserGroupsPanel, UsersPanel } from './admin/IdentityManag import { PlatformManagementPanel } from './admin/PlatformManagementPanel'; import { PricingRulesPanel } from './admin/PricingRulesPanel'; import { ProviderManagementPanel } from './admin/ProviderManagementPanel'; +import { RealtimeLoadPanel } from './admin/RealtimeLoadPanel'; import { RuntimePoliciesPanel } from './admin/RuntimePoliciesPanel'; const tabs = [ @@ -34,6 +35,7 @@ const tabs = [ { value: 'runtime', label: '运行策略', icon: }, { value: 'baseModels', label: '基准模型库', icon: }, { value: 'platforms', label: '平台管理', icon: }, + { value: 'realtimeLoad', label: '实时负载', icon: }, { value: 'tenants', label: '租户', icon: }, { value: 'users', label: '用户', icon: }, { value: 'userGroups', label: '用户组', icon: }, @@ -138,8 +140,6 @@ export function AdminPage(props: { baseModels={props.data.baseModels} message={props.operationMessage} networkProxyConfig={props.data.networkProxyConfig} - modelRateLimits={props.data.modelRateLimits} - modelRateLimitsUpdatedAt={props.data.modelRateLimitsUpdatedAt} platformModels={props.data.models} platforms={props.data.platforms} pricingRuleSets={props.data.pricingRuleSets} @@ -149,6 +149,13 @@ export function AdminPage(props: { onSavePlatform={props.onSavePlatform} /> )} + {props.section === 'realtimeLoad' && ( + + )} {props.section === 'tenants' && } {props.section === 'users' && } {props.section === 'userGroups' && } diff --git a/apps/web/src/pages/admin/PlatformManagementPanel.tsx b/apps/web/src/pages/admin/PlatformManagementPanel.tsx index c01a13e..dd3eee4 100644 --- a/apps/web/src/pages/admin/PlatformManagementPanel.tsx +++ b/apps/web/src/pages/admin/PlatformManagementPanel.tsx @@ -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: { - - setViewMode('platforms')}>平台视图 - setViewMode('models')}>模型视图 - setViewMode('limits')}>实时限流 - - {viewMode === 'platforms' ? ( - + setViewMode('platforms')}>平台视图 + setViewMode('models')}>模型视图 + + {viewMode === 'platforms' ? ( + - ) : viewMode === 'models' ? ( - - ) : ( - )} {props.message && {props.message}} @@ -574,65 +564,6 @@ function PlatformModelTable(props: { ) )} - - ); - } - -function RateLimitStatusTable(props: { statuses: ModelRateLimitStatus[]; platformMap: Map; now: number; updatedAt: number | null }) { - if (!props.statuses.length) { - return ; - } - return ( - - - 按综合满载率从高到低排序 - 每 3 秒刷新一次;TPM 显示已结算 + 预占;最新更新时间 {formatTimeOfDay(props.updatedAt)}。 - - - - - 模型 - 平台 - - 并发 - 正在执行 / 并发 / 排队 - - TPM - RPM - 状态 - 满载率 - - {props.statuses.map((status) => { - const platform = props.platformMap.get(status.platformId); - return ( - - - - {status.displayName || status.modelAlias || status.modelName} - {status.providerModelName || status.modelName} - - - - - {platform ? platformDisplayName(platform) : status.platformName} - {status.provider} - - - {concurrencyMetricCell(status)} - {metricCell(status.tpm, true)} - {metricCell(status.rpm)} - {modelRuntimeStatusCell(status, props.now)} - - 0.8 ? 'true' : undefined}> - {formatPercent(status.loadRatio)} - - - - - ); - })} - - ); } @@ -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 {formatLimit(metric.currentValue)} / 不限{includeReserved ? reservedMetricText(metric) : '未配置上限'}; - return ( - - {formatLimit(metric.currentValue)} / {formatLimit(metric.limitValue)} - {includeReserved ? reservedMetricText(metric) : `窗口 ${formatPercent(metric.ratio)}`} - - ); -} - -function concurrencyMetricCell(status: ModelRateLimitStatus) { - const queuedTasks = status.queuedTasks ?? 0; - const limitText = status.concurrent.limited ? formatLimit(status.concurrent.limitValue) : '不限'; - return ( - - {formatLimit(status.concurrent.currentValue)} / {limitText} / {formatLimit(queuedTasks)} - - ); -} - -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 ( - - 模型冷却中 - 剩余 {formatCooldownRemaining(modelCooldownMs)} - - ); - } - if (platformCooldownMs > 0) { - return ( - - 平台冷却中 - 剩余 {formatCooldownRemaining(platformCooldownMs)} - - ); - } - return ( - - {status.enabled ? '可用' : '已停用'} - {status.enabled ? '参与路由' : '不参与路由'} - - ); + 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); diff --git a/apps/web/src/pages/admin/RealtimeLoadPanel.tsx b/apps/web/src/pages/admin/RealtimeLoadPanel.tsx new file mode 100644 index 0000000..235b6a2 --- /dev/null +++ b/apps/web/src/pages/admin/RealtimeLoadPanel.tsx @@ -0,0 +1,189 @@ +import { useEffect, useMemo, useState } from 'react'; +import { Gauge } from 'lucide-react'; +import type { IntegrationPlatform, ModelRateLimitStatus } from '@easyai-ai-gateway/contracts'; +import { Badge, Card, CardContent, CardHeader, CardTitle, EmptyState, Table, TableCell, TableHead, TableRow } from '../../components/ui'; + +export function RealtimeLoadPanel(props: { + modelRateLimits: ModelRateLimitStatus[]; + modelRateLimitsUpdatedAt: number | null; + platforms: IntegrationPlatform[]; +}) { + const [now, setNow] = useState(() => Date.now()); + const platformMap = useMemo(() => new Map(props.platforms.map((item) => [item.id, item])), [props.platforms]); + + useEffect(() => { + const timer = window.setInterval(() => setNow(Date.now()), 1000); + return () => window.clearInterval(timer); + }, []); + + return ( + + + + + 实时负载 + 按平台模型查看实时 RPM、TPM、并发、排队、冷却和综合满载率。 + + + + + + + + ); +} + +function RateLimitStatusTable(props: { statuses: ModelRateLimitStatus[]; platformMap: Map; now: number; updatedAt: number | null }) { + if (!props.statuses.length) { + return ; + } + return ( + + + 按综合满载率从高到低排序 + 每 3 秒刷新一次;TPM 显示已结算 + 预占;最新更新时间 {formatTimeOfDay(props.updatedAt)}。 + + + + + 模型 + 平台 + + 并发 + 正在执行 / 并发 / 排队 + + TPM + RPM + 状态 + 满载率 + + {props.statuses.map((status) => { + const platform = props.platformMap.get(status.platformId); + return ( + + + + {status.displayName || status.modelAlias || status.modelName} + {status.providerModelName || status.modelName} + + + + + {platform ? platformDisplayName(platform) : status.platformName} + {status.provider} + + + {concurrencyMetricCell(status)} + {metricCell(status.tpm, true)} + {metricCell(status.rpm)} + {modelRuntimeStatusCell(status, props.now)} + + 0.8 ? 'true' : undefined}> + {formatPercent(status.loadRatio)} + + + + + ); + })} + + + + ); +} + +function platformDisplayName(platform: IntegrationPlatform) { + return platform.internalName?.trim() || platform.name; +} + +function metricCell(metric: ModelRateLimitStatus['rpm'], includeReserved = false) { + if (!metric.limited) return {formatLimit(metric.currentValue)} / 不限{includeReserved ? reservedMetricText(metric) : '未配置上限'}; + return ( + + {formatLimit(metric.currentValue)} / {formatLimit(metric.limitValue)} + {includeReserved ? reservedMetricText(metric) : `窗口 ${formatPercent(metric.ratio)}`} + + ); +} + +function concurrencyMetricCell(status: ModelRateLimitStatus) { + const queuedTasks = status.queuedTasks ?? 0; + const limitText = status.concurrent.limited ? formatLimit(status.concurrent.limitValue) : '不限'; + return ( + + {formatLimit(status.concurrent.currentValue)} / {limitText} / {formatLimit(queuedTasks)} + + ); +} + +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 ( + + 模型冷却中 + 剩余 {formatCooldownRemaining(modelCooldownMs)} + + ); + } + if (platformCooldownMs > 0) { + return ( + + 平台冷却中 + 剩余 {formatCooldownRemaining(platformCooldownMs)} + + ); + } + return ( + + {status.enabled ? '可用' : '已停用'} + {status.enabled ? '参与路由' : '不参与路由'} + + ); +} + +function cooldownRemainingMs(cooldownUntil: string | undefined, now: number) { + if (!cooldownUntil) return 0; + const until = Date.parse(cooldownUntil); + if (!Number.isFinite(until)) return 0; + return Math.max(until - now, 0); +} + +function formatCooldownRemaining(milliseconds: number) { + const minutes = milliseconds / 60000; + if (minutes >= 1) return `${trimNumber(Math.ceil(minutes * 10) / 10)} 分钟`; + const seconds = Math.ceil(milliseconds / 1000); + return `${Math.max(seconds, 1)} 秒`; +} + +function formatPercent(value: number) { + if (!Number.isFinite(value) || value <= 0) return '0%'; + return `${trimNumber(value * 100)}%`; +} + +function formatLimit(value: number) { + if (Math.abs(value) >= 10000) return `${trimNumber(value / 10000)}万`; + if (Math.abs(value) >= 1000) return `${trimNumber(value / 1000)}k`; + return trimNumber(value); +} + +function trimNumber(value: number) { + return Number.isInteger(value) ? String(value) : value.toFixed(2).replace(/\.?0+$/, ''); +} diff --git a/apps/web/src/routing.ts b/apps/web/src/routing.ts index 00725da..b26e250 100644 --- a/apps/web/src/routing.ts +++ b/apps/web/src/routing.ts @@ -32,6 +32,7 @@ const adminPaths: Record = { baseModels: '/admin/base-models', pricing: '/admin/pricing', platforms: '/admin/platforms', + realtimeLoad: '/admin/realtime-load', tenants: '/admin/tenants', users: '/admin/users', userGroups: '/admin/user-groups', diff --git a/apps/web/src/types.ts b/apps/web/src/types.ts index e1ebf32..363e5a5 100644 --- a/apps/web/src/types.ts +++ b/apps/web/src/types.ts @@ -11,6 +11,7 @@ export type AdminSection = | 'baseModels' | 'pricing' | 'platforms' + | 'realtimeLoad' | 'tenants' | 'users' | 'userGroups'
{props.message}
按平台模型查看实时 RPM、TPM、并发、排队、冷却和综合满载率。