feat(admin): 优化后台运维与任务管理

完善平台、基准模型、定价规则和实时负载的紧凑查询与滚动展示,并固定关键操作列。

新增管理员任务记录、批量计费结算和用户组限流、额度及模型权限维护能力,同时补充任务脱敏、查询索引与 OpenAPI 契约。

验证:pnpm openapi、Go 全量测试、pnpm lint、pnpm test、pnpm build、gofmt 与差异格式检查均通过。
This commit is contained in:
2026-07-25 01:32:49 +08:00
parent b29d539d49
commit de10875439
38 changed files with 5015 additions and 425 deletions
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useState, type FormEvent, type ReactNode } from 'react';
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, Switch, Table, TableCell, TableHead, TableRow } from '../../components/ui';
import { Badge, Button, Card, CardContent, ConfirmDialog, EmptyState, FormDialog, Input, Label, ScreenMessage, Select, Switch, Table, TableCell, TableHead, TableRow } from '../../components/ui';
import type { LoadState, PlatformWithModelsInput } from '../../types';
import {
authTypes,
@@ -40,8 +40,14 @@ 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 [filterDialogOpen, setFilterDialogOpen] = useState(false);
const [viewMode, setViewMode] = useState<'platforms' | 'models'>('platforms');
const [platformQuery, setPlatformQuery] = useState('');
const [platformProvider, setPlatformProvider] = useState('');
const [platformStatus, setPlatformStatus] = useState('');
const [modelQuery, setModelQuery] = useState('');
const [modelType, setModelType] = useState('');
const [modelStatus, setModelStatus] = useState('');
const [selectedPlatformId, setSelectedPlatformId] = useState('');
const [validationMessage, setValidationMessage] = useState('');
const [globalProxyNoticeOpen, setGlobalProxyNoticeOpen] = useState(false);
@@ -62,12 +68,36 @@ export function PlatformManagementPanel(props: {
const availableModels = useMemo(() => props.baseModels.filter((item) => item.status !== 'hidden'), [props.baseModels]);
const selectedModels = useMemo(() => selectedModelsForForm(props.baseModels, form), [form, props.baseModels]);
const platformModelCount = useMemo(() => countModelsByPlatform(props.platformModels), [props.platformModels]);
const selectedModelPlatformId = selectedPlatformId || props.platforms[0]?.id || '';
const selectedModelPlatformId = selectedPlatformId;
const platformFilterActive = Boolean(platformQuery || platformProvider || platformStatus);
const modelFilterActive = Boolean(selectedModelPlatformId || modelQuery || modelType || modelStatus);
const currentFilterActive = viewMode === 'platforms' ? platformFilterActive : modelFilterActive;
const currentAdvancedFilterCount = viewMode === 'platforms'
? [platformProvider, platformStatus].filter(Boolean).length
: [selectedModelPlatformId, modelType, modelStatus].filter(Boolean).length;
const platformProviderOptions = useMemo(
() => Array.from(new Set(props.platforms.map((platform) => platform.provider).filter(Boolean))).sort((a, b) => a.localeCompare(b)),
[props.platforms],
);
const modelTypeOptions = useMemo(
() => Array.from(new Set(props.platformModels.flatMap((model) => model.modelType))).sort((a, b) => a.localeCompare(b)),
[props.platformModels],
);
const filteredPlatforms = useMemo(
() => filterAdminPlatforms(props.platforms, props.platformModels, {
query: platformQuery,
provider: platformProvider,
status: platformStatus,
now,
}),
[now, platformProvider, platformQuery, platformStatus, props.platformModels, props.platforms],
);
const filteredPlatformModels = useMemo(() => {
const keyword = modelQuery.trim().toLowerCase();
return props.platformModels.filter((model) => {
const matchesPlatform = !selectedModelPlatformId || model.platformId === selectedModelPlatformId;
const platform = platformMap.get(model.platformId);
const cooldown = cooldownRemainingMs(model.cooldownUntil, now) > 0 || cooldownRemainingMs(platform?.cooldownUntil, now) > 0;
const text = [
model.displayName,
model.modelName,
@@ -79,9 +109,14 @@ export function PlatformManagementPanel(props: {
platform?.internalName,
platform?.platformKey,
].filter(Boolean).join(' ').toLowerCase();
return matchesPlatform && (!keyword || text.includes(keyword));
const matchesStatus = !modelStatus ||
(modelStatus === 'cooldown' ? cooldown : modelStatus === 'enabled' ? model.enabled : !model.enabled);
return matchesPlatform &&
(!keyword || text.includes(keyword)) &&
(!modelType || model.modelType.includes(modelType)) &&
matchesStatus;
});
}, [modelQuery, platformMap, props.platformModels, selectedModelPlatformId]);
}, [modelQuery, modelStatus, modelType, now, platformMap, props.platformModels, selectedModelPlatformId]);
useEffect(() => {
const timer = window.setInterval(() => setNow(Date.now()), 1000);
@@ -179,56 +214,172 @@ export function PlatformManagementPanel(props: {
}
}
function resetCurrentFilters() {
if (viewMode === 'platforms') {
setPlatformQuery('');
setPlatformProvider('');
setPlatformStatus('');
return;
}
setSelectedPlatformId('');
setModelQuery('');
setModelType('');
setModelStatus('');
}
return (
<section className="pageStack">
<ScreenMessage message={validationMessage} variant="error" onClose={() => setValidationMessage('')} />
<Card>
<CardHeader>
<div>
<CardTitle></CardTitle>
<p className="mutedText">Base URL</p>
</div>
<Button type="button" onClick={openCreateDialog}>
<Plus size={15} />
</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>
<Card className="compactAdminTableCard">
<CardContent className="compactAdminTableContent">
<div className="compactAdminToolbar platformCompactToolbar">
<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>
<span className="platformSearchBox compactAdminSearch">
<Search size={15} />
<Input
aria-label={viewMode === 'platforms' ? '搜索平台' : '搜索模型'}
value={viewMode === 'platforms' ? platformQuery : modelQuery}
placeholder={viewMode === 'platforms' ? '平台名、Key、Provider、Base URL 或绑定模型' : '模型名称、别名、类型或平台名称'}
onChange={(event) => {
if (viewMode === 'platforms') setPlatformQuery(event.target.value);
else setModelQuery(event.target.value);
}}
/>
</span>
<div className="compactAdminToolbarActions">
<Button type="button" variant="outline" size="sm" onClick={() => setFilterDialogOpen(true)}>
<SlidersHorizontal size={14} />
{currentAdvancedFilterCount ? `${currentAdvancedFilterCount}` : ''}
</Button>
<Button type="button" variant="outline" size="sm" disabled={!currentFilterActive} onClick={resetCurrentFilters}>
<RotateCcw size={14} />
</Button>
<Button type="button" size="sm" onClick={openCreateDialog}>
<Plus size={15} />
</Button>
</div>
</div>
{viewMode === 'platforms' ? (
<PlatformTable
now={now}
platformModelCount={platformModelCount}
platforms={props.platforms}
providerMap={providerMap}
pricingRuleSets={props.pricingRuleSets}
togglingPlatformId={togglingPlatformId}
onDelete={setPendingDeletePlatform}
onCreate={openCreateDialog}
onEdit={openEditDialog}
onToggleStatus={togglePlatformStatus}
/>
<section className="platformFilteredView">
<PlatformTable
filtered={platformFilterActive}
now={now}
platformModelCount={platformModelCount}
platforms={filteredPlatforms}
providerMap={providerMap}
pricingRuleSets={props.pricingRuleSets}
togglingPlatformId={togglingPlatformId}
onDelete={setPendingDeletePlatform}
onCreate={openCreateDialog}
onEdit={openEditDialog}
onToggleStatus={togglePlatformStatus}
/>
</section>
) : (
<PlatformModelTable
baseModels={props.baseModels}
modelQuery={modelQuery}
filterActive={modelFilterActive}
models={filteredPlatformModels}
platformId={selectedModelPlatformId}
platformMap={platformMap}
platforms={props.platforms}
providerMap={providerMap}
onModelQueryChange={setModelQuery}
now={now}
onPlatformChange={setSelectedPlatformId}
/>
)}
{props.message && <p className="formMessage">{props.message}</p>}
</CardContent>
</Card>
<FormDialog
ariaLabel="平台管理高级筛选"
className="compactFilterDialog"
closeLabel="关闭"
footer={(
<>
<Button
type="button"
variant="outline"
disabled={!currentAdvancedFilterCount}
onClick={() => {
if (viewMode === 'platforms') {
setPlatformProvider('');
setPlatformStatus('');
} else {
setSelectedPlatformId('');
setModelType('');
setModelStatus('');
}
}}
>
<RotateCcw size={15} />
</Button>
<Button type="submit"></Button>
</>
)}
open={filterDialogOpen}
title={viewMode === 'platforms' ? '平台高级筛选' : '模型高级筛选'}
onClose={() => setFilterDialogOpen(false)}
onSubmit={(event) => {
event.preventDefault();
setFilterDialogOpen(false);
}}
>
{viewMode === 'platforms' ? (
<>
<Label>
Provider
<Select value={platformProvider || 'all'} onChange={(event) => setPlatformProvider(event.target.value === 'all' ? '' : event.target.value)}>
<option value="all"> Provider</option>
{platformProviderOptions.map((provider) => <option value={provider} key={provider}>{providerMap.get(provider)?.displayName ?? provider}</option>)}
</Select>
</Label>
<Label>
<Select value={platformStatus || 'all'} onChange={(event) => setPlatformStatus(event.target.value === 'all' ? '' : event.target.value)}>
<option value="all"></option>
<option value="enabled"></option>
<option value="disabled"></option>
<option value="cooldown"></option>
</Select>
</Label>
</>
) : (
<>
<Label>
<Select value={selectedModelPlatformId} onChange={(event) => setSelectedPlatformId(event.target.value)}>
<option value=""></option>
{props.platforms.map((platform) => (
<option value={platform.id} key={platform.id}>{platformDisplayName(platform)}</option>
))}
</Select>
</Label>
<Label>
<Select value={modelType || 'all'} onChange={(event) => setModelType(event.target.value === 'all' ? '' : event.target.value)}>
<option value="all"></option>
{modelTypeOptions.map((type) => <option value={type} key={type}>{type}</option>)}
</Select>
</Label>
<Label>
<Select value={modelStatus || 'all'} onChange={(event) => setModelStatus(event.target.value === 'all' ? '' : event.target.value)}>
<option value="all"></option>
<option value="enabled"></option>
<option value="disabled"></option>
<option value="cooldown"></option>
</Select>
</Label>
</>
)}
</FormDialog>
<FormDialog
bodyClassName="platformDialogBody"
className="platformDialog"
@@ -423,6 +574,7 @@ function ModelBindingPolicy(props: { form: PlatformWizardForm; onChange: (value:
}
function PlatformTable(props: {
filtered: boolean;
now: number;
platformModelCount: Map<string, number>;
platforms: IntegrationPlatform[];
@@ -435,6 +587,9 @@ function PlatformTable(props: {
onToggleStatus: (platform: IntegrationPlatform, status: 'enabled' | 'disabled') => void;
}) {
if (!props.platforms.length) {
if (props.filtered) {
return <EmptyState title="没有匹配的平台" description="调整平台关键词、Provider 或状态筛选后再试。" />;
}
return (
<div className="platformEmptyState">
<div className="platformEmptyIcon"><ServerCog size={24} /></div>
@@ -448,7 +603,7 @@ function PlatformTable(props: {
);
}
return (
<Table className="platformDataTable">
<Table className="shTableViewport platformDataTable platformManagementTableViewport">
<TableRow className="shTableHeader">
<TableHead></TableHead>
<TableHead>Provider</TableHead>
@@ -457,12 +612,14 @@ function PlatformTable(props: {
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
{props.platforms.map((platform) => {
const pricing = platformPricingSummary(platform, props.pricingRuleSets);
const rateLimit = platformRateLimitSummary(platform.rateLimitPolicy);
const runtime = platformRuntimeSummary(platform);
const runtimeDescription = platformRuntimeDescription(platform);
const platformCooldownMs = cooldownRemainingMs(platform.cooldownUntil, props.now);
const isEnabled = platform.status === 'enabled';
const isToggling = props.togglingPlatformId === platform.id;
@@ -512,7 +669,20 @@ function PlatformTable(props: {
<Badge variant={isEnabled ? 'success' : 'destructive'}>{isEnabled ? '已启用' : '已禁用'}</Badge>
)}
</strong>
<small>{platformCooldownMs > 0 ? `剩余 ${formatCooldownRemaining(platformCooldownMs)}` : runtime}</small>
{platformCooldownMs > 0 && <small> {formatCooldownRemaining(platformCooldownMs)}</small>}
</span>
</TableCell>
<TableCell>
<span className="platformTableName">
<strong>{platform.priority}</strong>
{platform.effectivePriority !== undefined && platform.effectivePriority !== platform.priority && (
<small> {platform.effectivePriority}</small>
)}
</span>
</TableCell>
<TableCell>
<span className="platformTableName">
<strong title={runtimeDescription}>{runtimeDescription}</strong>
</span>
</TableCell>
<TableCell>
@@ -534,36 +704,15 @@ function PlatformTable(props: {
function PlatformModelTable(props: {
baseModels: BaseModelCatalogItem[];
modelQuery: string;
filterActive: boolean;
models: PlatformModel[];
platformId: string;
platformMap: Map<string, IntegrationPlatform>;
platforms: IntegrationPlatform[];
providerMap: Map<string, CatalogProvider>;
now: number;
onModelQueryChange: (value: string) => void;
onPlatformChange: (value: string) => void;
}) {
return (
<section className="platformModelView">
<div className="platformModelToolbar">
<Label>
<Select value={props.platformId} onChange={(event) => props.onPlatformChange(event.target.value)}>
{props.platforms.map((platform) => (
<option value={platform.id} key={platform.id}>{platformDisplayName(platform)}</option>
))}
</Select>
</Label>
<Label>
<Input
value={props.modelQuery}
placeholder="模型名称、别名、类型或平台名称"
onChange={(event) => props.onModelQueryChange(event.target.value)}
/>
</Label>
</div>
{!props.platforms.length ? (
<EmptyState title="暂无平台" description="请先添加平台,再查看对应平台模型。" />
) : (
@@ -1213,6 +1362,42 @@ function cooldownRemainingMs(cooldownUntil: string | undefined, now: number) {
return Math.max(until - now, 0);
}
export function filterAdminPlatforms(
platforms: IntegrationPlatform[],
models: PlatformModel[],
options: { query: string; provider: string; status: string; now: number },
) {
const searchByPlatform = new Map<string, string[]>();
models.forEach((model) => {
const values = searchByPlatform.get(model.platformId) ?? [];
values.push(...[
model.displayName,
model.modelName,
model.providerModelName,
model.modelAlias,
...model.modelType,
].filter((value): value is string => Boolean(value)));
searchByPlatform.set(model.platformId, values);
});
const keyword = options.query.trim().toLowerCase();
return platforms.filter((platform) => {
const cooldown = cooldownRemainingMs(platform.cooldownUntil, options.now) > 0;
const text = [
platform.name,
platform.internalName,
platform.platformKey,
platform.provider,
platform.baseUrl,
...(searchByPlatform.get(platform.id) ?? []),
].filter(Boolean).join(' ').toLowerCase();
const matchesStatus = !options.status ||
(options.status === 'cooldown' ? cooldown : platform.status === options.status);
return (!keyword || text.includes(keyword)) &&
(!options.provider || platform.provider === options.provider) &&
matchesStatus;
});
}
function formatCooldownRemaining(milliseconds: number) {
const minutes = milliseconds / 60000;
if (minutes >= 1) return `${trimNumber(Math.ceil(minutes * 10) / 10)} 分钟`;
@@ -1220,11 +1405,11 @@ function formatCooldownRemaining(milliseconds: number) {
return `${Math.max(seconds, 1)}`;
}
function platformRuntimeSummary(platform: IntegrationPlatform) {
function platformRuntimeDescription(platform: IntegrationPlatform) {
const retryPolicy = platform.retryPolicy ?? {};
const retryEnabled = readBoolean(retryPolicy, 'enabled', true);
const maxAttempts = readNumber(retryPolicy, 'maxAttempts') ?? 2;
return `优先级 ${platform.priority} · ${retryEnabled ? `同平台最多尝试 ${maxAttempts}` : '同平台不重试'} · ${proxyModeText(readNetworkProxyConfig(platform.config ?? {}).proxyMode)}`;
return `${retryEnabled ? `同平台最多尝试 ${maxAttempts}` : '同平台不重试'} · ${proxyModeText(readNetworkProxyConfig(platform.config ?? {}).proxyMode)}`;
}
function proxyModeText(mode: PlatformWizardForm['proxyMode']) {