feat(admin): 优化后台运维与任务管理
完善平台、基准模型、定价规则和实时负载的紧凑查询与滚动展示,并固定关键操作列。 新增管理员任务记录、批量计费结算和用户组限流、额度及模型权限维护能力,同时补充任务脱敏、查询索引与 OpenAPI 契约。 验证:pnpm openapi、Go 全量测试、pnpm lint、pnpm test、pnpm build、gofmt 与差异格式检查均通过。
This commit is contained in:
@@ -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']) {
|
||||
|
||||
Reference in New Issue
Block a user