228 lines
7.9 KiB
TypeScript
228 lines
7.9 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
import type {
|
|
BaseModelCatalogItem,
|
|
CatalogProvider,
|
|
IntegrationPlatform,
|
|
PlatformModel,
|
|
PricingRule,
|
|
RateLimitWindow,
|
|
} from '@easyai-ai-gateway/contracts';
|
|
import {
|
|
getHealth,
|
|
listBaseModels,
|
|
listCatalogProviders,
|
|
listModels,
|
|
listPlatforms,
|
|
listPricingRules,
|
|
listRateLimitWindows,
|
|
type HealthResponse,
|
|
} from './api';
|
|
|
|
type LoadState = 'idle' | 'loading' | 'ready' | 'error';
|
|
|
|
export function App() {
|
|
const [token, setToken] = useState('');
|
|
const [health, setHealth] = useState<HealthResponse | null>(null);
|
|
const [platforms, setPlatforms] = useState<IntegrationPlatform[]>([]);
|
|
const [models, setModels] = useState<PlatformModel[]>([]);
|
|
const [providers, setProviders] = useState<CatalogProvider[]>([]);
|
|
const [baseModels, setBaseModels] = useState<BaseModelCatalogItem[]>([]);
|
|
const [pricingRules, setPricingRules] = useState<PricingRule[]>([]);
|
|
const [rateLimitWindows, setRateLimitWindows] = useState<RateLimitWindow[]>([]);
|
|
const [state, setState] = useState<LoadState>('idle');
|
|
const [error, setError] = useState('');
|
|
|
|
useEffect(() => {
|
|
getHealth()
|
|
.then(setHealth)
|
|
.catch((err: Error) => setError(err.message));
|
|
}, []);
|
|
|
|
const stats = useMemo(() => {
|
|
const enabledPlatforms = platforms.filter((item) => item.status === 'enabled').length;
|
|
const enabledModels = models.filter((item) => item.enabled).length;
|
|
const activeProviders = providers.filter((item) => item.status === 'active').length;
|
|
const activeRateWindows = rateLimitWindows.filter((item) => item.resetAt >= new Date().toISOString()).length;
|
|
return [
|
|
{ label: '平台', value: platforms.length, tone: 'blue' },
|
|
{ label: '启用平台', value: enabledPlatforms, tone: 'green' },
|
|
{ label: '基准模型', value: baseModels.length, tone: 'violet' },
|
|
{ label: 'Provider', value: activeProviders || providers.length || enabledModels, tone: 'amber' },
|
|
{ label: '定价规则', value: pricingRules.length, tone: 'cyan' },
|
|
{ label: '限流窗口', value: activeRateWindows, tone: 'rose' },
|
|
];
|
|
}, [baseModels.length, models, platforms, pricingRules.length, providers, rateLimitWindows]);
|
|
|
|
async function refresh() {
|
|
setState('loading');
|
|
setError('');
|
|
try {
|
|
const [
|
|
platformResponse,
|
|
modelResponse,
|
|
providerResponse,
|
|
baseModelResponse,
|
|
pricingRuleResponse,
|
|
rateLimitWindowResponse,
|
|
] = await Promise.all([
|
|
listPlatforms(token),
|
|
listModels(token),
|
|
listCatalogProviders(token),
|
|
listBaseModels(token),
|
|
listPricingRules(token),
|
|
listRateLimitWindows(token),
|
|
]);
|
|
setPlatforms(platformResponse.items);
|
|
setModels(modelResponse.items);
|
|
setProviders(providerResponse.items);
|
|
setBaseModels(baseModelResponse.items);
|
|
setPricingRules(pricingRuleResponse.items);
|
|
setRateLimitWindows(rateLimitWindowResponse.items);
|
|
setState('ready');
|
|
} catch (err) {
|
|
setState('error');
|
|
setError(err instanceof Error ? err.message : '加载失败');
|
|
}
|
|
}
|
|
|
|
return (
|
|
<main className="page">
|
|
<header className="topbar">
|
|
<div>
|
|
<p className="eyebrow">EasyAI</p>
|
|
<h1>AI Gateway Console</h1>
|
|
</div>
|
|
<div className="health" data-ok={health?.ok === true}>
|
|
<span />
|
|
{health?.service ?? 'API 未连接'}
|
|
</div>
|
|
</header>
|
|
|
|
<section className="toolbar" aria-label="授权与刷新">
|
|
<label className="tokenField">
|
|
<span>Server Main JWT</span>
|
|
<input
|
|
value={token}
|
|
onChange={(event) => setToken(event.target.value)}
|
|
placeholder="粘贴 server-main access_token"
|
|
/>
|
|
</label>
|
|
<button type="button" onClick={refresh} disabled={!token || state === 'loading'}>
|
|
{state === 'loading' ? '加载中' : '刷新'}
|
|
</button>
|
|
</section>
|
|
|
|
{error && <div className="notice">{error}</div>}
|
|
|
|
<section className="metrics" aria-label="概览">
|
|
{stats.map((item) => (
|
|
<div className="metric" data-tone={item.tone} key={item.label}>
|
|
<span>{item.label}</span>
|
|
<strong>{item.value}</strong>
|
|
</div>
|
|
))}
|
|
</section>
|
|
|
|
<section className="split">
|
|
<div className="panel">
|
|
<div className="panelHeader">
|
|
<h2>平台</h2>
|
|
<span>{platforms.length}</span>
|
|
</div>
|
|
<div className="table" role="table">
|
|
<div className="row head" role="row">
|
|
<span>Provider</span>
|
|
<span>名称</span>
|
|
<span>状态</span>
|
|
<span>优先级</span>
|
|
</div>
|
|
{platforms.map((item) => (
|
|
<div className="row" role="row" key={item.id}>
|
|
<span>{item.provider}</span>
|
|
<span>{item.name}</span>
|
|
<span>{item.status}</span>
|
|
<span>{item.priority}</span>
|
|
</div>
|
|
))}
|
|
{!platforms.length && <p className="empty">暂无平台数据</p>}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="panel">
|
|
<div className="panelHeader">
|
|
<h2>模型</h2>
|
|
<span>{models.length}</span>
|
|
</div>
|
|
<div className="table" role="table">
|
|
<div className="row head" role="row">
|
|
<span>模型</span>
|
|
<span>类型</span>
|
|
<span>平台</span>
|
|
<span>启用</span>
|
|
</div>
|
|
{models.map((item) => (
|
|
<div className="row" role="row" key={item.id}>
|
|
<span>{item.modelName}</span>
|
|
<span>{item.modelType}</span>
|
|
<span>{item.provider ?? item.platformName}</span>
|
|
<span>{item.enabled ? '是' : '否'}</span>
|
|
</div>
|
|
))}
|
|
{!models.length && <p className="empty">暂无模型数据</p>}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="split secondary">
|
|
<div className="panel">
|
|
<div className="panelHeader">
|
|
<h2>基准模型库</h2>
|
|
<span>{baseModels.length}</span>
|
|
</div>
|
|
<div className="table catalogTable" role="table">
|
|
<div className="row head" role="row">
|
|
<span>Provider</span>
|
|
<span>模型</span>
|
|
<span>类型</span>
|
|
<span>版本</span>
|
|
</div>
|
|
{baseModels.map((item) => (
|
|
<div className="row" role="row" key={item.id}>
|
|
<span>{item.providerKey}</span>
|
|
<span>{item.canonicalModelKey}</span>
|
|
<span>{item.modelType}</span>
|
|
<span>{item.pricingVersion}</span>
|
|
</div>
|
|
))}
|
|
{!baseModels.length && <p className="empty">暂无基准模型</p>}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="panel">
|
|
<div className="panelHeader">
|
|
<h2>TPM/RPM 窗口</h2>
|
|
<span>{rateLimitWindows.length}</span>
|
|
</div>
|
|
<div className="table rateTable" role="table">
|
|
<div className="row head" role="row">
|
|
<span>Scope</span>
|
|
<span>指标</span>
|
|
<span>使用</span>
|
|
<span>预占</span>
|
|
</div>
|
|
{rateLimitWindows.map((item) => (
|
|
<div className="row" role="row" key={`${item.scopeType}:${item.scopeKey}:${item.metric}:${item.windowStart}`}>
|
|
<span>{item.scopeKey}</span>
|
|
<span>{item.metric}</span>
|
|
<span>{item.usedValue}/{item.limitValue}</span>
|
|
<span>{item.reservedValue}</span>
|
|
</div>
|
|
))}
|
|
{!rateLimitWindows.length && <p className="empty">暂无限流窗口</p>}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
);
|
|
}
|