Files
easyai-ai-gateway/apps/web/src/pages/admin/PlatformManagementPanel.tsx
T
wangbo de10875439 feat(admin): 优化后台运维与任务管理
完善平台、基准模型、定价规则和实时负载的紧凑查询与滚动展示,并固定关键操作列。

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

验证:pnpm openapi、Go 全量测试、pnpm lint、pnpm test、pnpm build、gofmt 与差异格式检查均通过。
2026-07-25 01:32:49 +08:00

1436 lines
63 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, ConfirmDialog, EmptyState, FormDialog, Input, Label, ScreenMessage, Select, Switch, Table, TableCell, TableHead, TableRow } from '../../components/ui';
import type { LoadState, PlatformWithModelsInput } from '../../types';
import {
authTypes,
applyProviderDefaults,
baseModelTypes,
baseModelTypeTextForProvider,
createEmptyPlatformForm,
modelMatchesProvider,
modelsForProvider,
platformModelPayloads,
platformPayload,
providerModelNameForProvider,
providerModelPresetFor,
providerModelPresets,
providerLabel,
selectedModelsForForm,
stableModelAlias,
validatePlatformForm,
type PlatformWizardForm,
} from './platform-form';
import { ModelCatalogCard } from './ModelCatalogCard';
export function PlatformManagementPanel(props: {
baseModels: BaseModelCatalogItem[];
message: string;
networkProxyConfig: { globalHttpProxy?: string; globalHttpProxySet: boolean; globalHttpProxySource?: string } | null;
platforms: IntegrationPlatform[];
platformModels: PlatformModel[];
pricingRuleSets: PricingRuleSet[];
providers: CatalogProvider[];
state: LoadState;
onDeletePlatform: (platformId: string) => Promise<void>;
onSavePlatform: (input: PlatformWithModelsInput) => Promise<void>;
onTogglePlatformStatus: (platform: IntegrationPlatform, status: 'enabled' | 'disabled') => Promise<void>;
}) {
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);
const [editingPlatform, setEditingPlatform] = useState<IntegrationPlatform | null>(null);
const [pendingDeletePlatform, setPendingDeletePlatform] = useState<IntegrationPlatform | null>(null);
const [togglingPlatformId, setTogglingPlatformId] = useState('');
const providerMap = useMemo(() => new Map(props.providers.map((item) => [item.providerKey, item])), [props.providers]);
const platformMap = useMemo(() => new Map(props.platforms.map((item) => [item.id, item])), [props.platforms]);
const [form, setForm] = useState<PlatformWizardForm>(() => createEmptyPlatformForm(defaultProvider, providerDefaults(providerMap.get(defaultProvider))));
const providerOptions = useMemo(
() => Array.from(new Set([
...props.providers.map((item) => item.providerKey),
...props.baseModels.map((item) => item.providerKey),
...props.baseModels.flatMap((item) => providerModelPresets(item).map((preset) => preset.providerKey)),
])).filter(Boolean),
[props.baseModels, props.providers],
);
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;
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,
model.providerModelName,
model.modelAlias,
...model.modelType,
model.provider,
platform?.name,
platform?.internalName,
platform?.platformKey,
].filter(Boolean).join(' ').toLowerCase();
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, modelStatus, modelType, now, platformMap, props.platformModels, selectedModelPlatformId]);
useEffect(() => {
const timer = window.setInterval(() => setNow(Date.now()), 1000);
return () => window.clearInterval(timer);
}, []);
function openCreateDialog() {
const provider = providerOptions[0] ?? '';
const providerName = providerDisplayName(provider, providerMap);
setValidationMessage('');
setEditingPlatform(null);
setForm({
...createEmptyPlatformForm(provider, providerDefaults(providerMap.get(provider))),
name: providerName,
internalName: providerName,
selectedModelIds: defaultSelectedModelIds(props.baseModels, provider),
});
setDialogOpen(true);
}
function openEditDialog(platform: IntegrationPlatform) {
setValidationMessage('');
setEditingPlatform(platform);
setForm(platformToForm(platform, props.baseModels, props.platformModels, providerDefaults(providerMap.get(platform.provider))));
setDialogOpen(true);
}
function closeDialog() {
setDialogOpen(false);
setEditingPlatform(null);
}
function updateProvider(provider: string) {
const previousProviderName = providerDisplayName(form.provider, providerMap);
const providerName = providerDisplayName(provider, providerMap);
const nextForm = applyProviderDefaults(form, provider, providerDefaults(providerMap.get(provider)));
setForm({
...nextForm,
name: shouldUseProviderDefaultName(form.name, previousProviderName) ? providerName : form.name,
internalName: shouldUseProviderDefaultName(form.internalName, previousProviderName) ? providerName : form.internalName,
selectionMode: 'partial',
selectedModelIds: defaultSelectedModelIds(props.baseModels, provider),
});
}
function updateProxyMode(proxyMode: PlatformWizardForm['proxyMode']) {
if (proxyMode === 'global') {
setGlobalProxyNoticeOpen(true);
}
setForm({
...form,
proxyMode,
httpProxy: proxyMode === 'custom' ? form.httpProxy : '',
});
}
async function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const validationMessage = validatePlatformForm(form, selectedModels.length, { allowEmptyCredentials: Boolean(editingPlatform) });
if (validationMessage) {
setValidationMessage(validationMessage);
return;
}
try {
await props.onSavePlatform({
platformId: editingPlatform?.id,
platform: platformPayload(form, { preserveEmptyCredentials: Boolean(editingPlatform) }),
models: platformModelPayloads(props.baseModels, form),
selectionMode: form.selectionMode,
});
closeDialog();
} catch (err) {
setValidationMessage(err instanceof Error ? err.message : editingPlatform ? '更新平台失败' : '创建平台失败');
}
}
async function deletePlatform(platform: IntegrationPlatform) {
try {
await props.onDeletePlatform(platform.id);
setPendingDeletePlatform(null);
} catch (err) {
setValidationMessage(err instanceof Error ? err.message : '删除平台失败');
}
}
async function togglePlatformStatus(platform: IntegrationPlatform, status: 'enabled' | 'disabled') {
setTogglingPlatformId(platform.id);
setValidationMessage('');
try {
await props.onTogglePlatformStatus(platform, status);
} catch (err) {
setValidationMessage(err instanceof Error ? err.message : '切换平台状态失败');
} finally {
setTogglingPlatformId('');
}
}
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 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' ? (
<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}
filterActive={modelFilterActive}
models={filteredPlatformModels}
platformMap={platformMap}
platforms={props.platforms}
providerMap={providerMap}
now={now}
/>
)}
{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"
closeLabel="关闭"
eyebrow="Integration Platform"
footer={(
<>
<Button type="button" variant="outline" onClick={closeDialog}>
<RotateCcw size={15} />
取消
</Button>
<Button type="submit" disabled={props.state === 'loading'}>
<CheckCircle2 size={15} />
{editingPlatform ? '保存修改' : '创建平台'}
</Button>
</>
)}
open={dialogOpen}
title={editingPlatform ? '编辑平台' : '新增平台'}
onClose={closeDialog}
onSubmit={submit}
>
<FormSection icon={<ServerCog size={16} />} title="基础信息">
<Label>
模型厂商
<Select value={form.provider} onChange={(event) => updateProvider(event.target.value)}>
<option value="">选择厂商</option>
{providerOptions.map((provider) => (
<option value={provider} key={provider}>{providerMap.get(provider)?.displayName ?? provider}</option>
))}
</Select>
</Label>
<Label>
平台名称
<Input value={form.name} placeholder="对外展示名称,例如 OpenAI 官方账号" onChange={(event) => setForm({ ...form, name: event.target.value })} />
</Label>
<Label>
平台备注名
<Input value={form.internalName} placeholder="仅管理后台内部展示,可空" onChange={(event) => setForm({ ...form, internalName: event.target.value })} />
</Label>
<Label>
平台 Key(可空)
<Input value={form.platformKey} placeholder="留空自动生成" onChange={(event) => setForm({ ...form, platformKey: event.target.value })} />
</Label>
<Label>
优先级
<Input value={form.priority} inputMode="numeric" onChange={(event) => setForm({ ...form, priority: event.target.value })} />
</Label>
<Label className="spanTwo">
Base URL
<Input value={form.baseUrl} placeholder="https://api.example.com/v1" onChange={(event) => setForm({ ...form, baseUrl: event.target.value })} />
</Label>
</FormSection>
<FormSection icon={<KeyRound size={16} />} title="授权信息">
<Label>
授权方式
<Select value={form.authType} onChange={(event) => setForm({ ...form, authType: event.target.value })}>
{authTypes.map((item) => <option value={item.value} key={item.value}>{item.label}</option>)}
</Select>
</Label>
<AuthFields form={form} onChange={setForm} />
<ToggleField
checked={form.testMode}
description="开启后任务按模拟流程运行;不会覆盖已填写的平台凭证。"
label="测试模式"
onChange={(checked) => setForm({ ...form, testMode: checked })}
/>
</FormSection>
<FormSection icon={<Globe2 size={16} />} title="网络代理">
<Label>
代理模式
<Select
value={form.proxyMode}
onChange={(event) => updateProxyMode(event.target.value as PlatformWizardForm['proxyMode'])}
>
<option value="none">不使用代理</option>
<option value="global">使用全局代理</option>
<option value="custom">自定义代理</option>
</Select>
</Label>
{form.proxyMode === 'custom' && (
<Label>
HTTP 代理
<Input
value={form.httpProxy}
placeholder="http://127.0.0.1:7890"
onChange={(event) => setForm({ ...form, httpProxy: event.target.value })}
/>
</Label>
)}
</FormSection>
<FormSection icon={<SlidersHorizontal size={16} />} title="路由与计费">
<Label>
定价规则
<Select value={form.pricingRuleSetId} onChange={(event) => setForm({ ...form, pricingRuleSetId: event.target.value })}>
<option value="">跟随默认规则</option>
{props.pricingRuleSets.map((item) => <option value={item.id} key={item.id}>{item.name}</option>)}
</Select>
</Label>
<Label>
平台折扣率
<Input value={form.defaultDiscountFactor} inputMode="decimal" onChange={(event) => setForm({ ...form, defaultDiscountFactor: event.target.value })} />
</Label>
<ToggleField checked={form.retryEnabled} label="失败后同平台重试" onChange={(checked) => setForm({ ...form, retryEnabled: checked })} />
<Label>
最大尝试次数
<Input value={form.retryMaxAttempts} inputMode="numeric" disabled={!form.retryEnabled} onChange={(event) => setForm({ ...form, retryMaxAttempts: event.target.value })} />
</Label>
</FormSection>
<FormSection icon={<ShieldCheck size={16} />} title="限流策略">
<Label>RPM / 分钟请求数<Input value={form.rpmLimit} placeholder="不填或负数为不限" inputMode="numeric" onChange={(event) => setForm({ ...form, rpmLimit: event.target.value })} /></Label>
<Label>RPS / 每秒请求数<Input value={form.rpsLimit} placeholder="不填或负数为不限" inputMode="numeric" onChange={(event) => setForm({ ...form, rpsLimit: event.target.value })} /></Label>
<Label>TPM / 分钟 Token<Input value={form.tpmLimit} placeholder="不填或负数为不限" inputMode="numeric" onChange={(event) => setForm({ ...form, tpmLimit: event.target.value })} /></Label>
<Label>并发请求<Input value={form.concurrencyLimit} placeholder="不填或负数为不限" inputMode="numeric" onChange={(event) => setForm({ ...form, concurrencyLimit: event.target.value })} /></Label>
<div className="platformTogglePair">
<ToggleField checked={form.supportBase64Input} label="支持 Base64 输入" onChange={(checked) => setForm({ ...form, supportBase64Input: checked })} />
<ToggleField checked={form.supportUrlInput} label="支持 URL 输入" onChange={(checked) => setForm({ ...form, supportUrlInput: checked })} />
</div>
</FormSection>
<FormSection icon={<Boxes size={16} />} title={`模型绑定 · ${selectedModels.length}/${availableModels.length}`}>
<ModelBindingPolicy form={form} onChange={setForm} />
<ModelSelection
currentProvider={form.provider}
form={form}
models={availableModels}
providerMap={providerMap}
providerName={providerLabel(providerMap.get(form.provider)?.displayName ?? form.provider)}
onChange={setForm}
/>
</FormSection>
</FormDialog>
<ConfirmDialog
cancelLabel="关闭"
confirmLabel="知道了"
confirmVariant="default"
description="使用全局代理前,请确认网关服务已设置 AI_GATEWAY_GLOBAL_HTTP_PROXY 或 GLOBAL_HTTP_PROXY 环境变量;也兼容 HTTP_PROXY、HTTPS_PROXY、ALL_PROXY。修改环境变量后需要重启服务才会生效。"
loading={props.state === 'loading'}
open={globalProxyNoticeOpen}
title="使用全局代理"
onCancel={() => setGlobalProxyNoticeOpen(false)}
onConfirm={() => setGlobalProxyNoticeOpen(false)}
>
<p className="platformProxyStatus">当前设置代理服务器:{globalProxyStatusText(props.networkProxyConfig)}</p>
</ConfirmDialog>
<ConfirmDialog
confirmLabel="删除平台"
description="已绑定的模型会一并删除,删除后不可恢复。"
loading={props.state === 'loading'}
open={Boolean(pendingDeletePlatform)}
title={`确定删除平台“${pendingDeletePlatform ? platformDisplayName(pendingDeletePlatform) : ''}”?`}
onCancel={() => setPendingDeletePlatform(null)}
onConfirm={() => pendingDeletePlatform ? deletePlatform(pendingDeletePlatform) : undefined}
/>
</section>
);
}
function ModelBindingPolicy(props: { form: PlatformWizardForm; onChange: (value: PlatformWizardForm) => void }) {
const { form, onChange } = props;
return (
<div className="platformModelPolicy spanTwo">
<Label>
默认模型折扣率
<Input value={form.modelDiscountFactor} placeholder="留空继承平台折扣,可在下方逐个覆盖" inputMode="decimal" onChange={(event) => onChange({ ...form, modelDiscountFactor: event.target.value })} />
</Label>
<ToggleField checked={form.modelOverrideRetry} label="覆盖模型重试策略" onChange={(checked) => onChange({ ...form, modelOverrideRetry: checked })} />
{form.modelOverrideRetry && (
<>
<ToggleField checked={form.modelRetryEnabled} label="模型同平台重试" onChange={(checked) => onChange({ ...form, modelRetryEnabled: checked })} />
<Label>
模型最大尝试次数
<Input value={form.modelRetryMaxAttempts} inputMode="numeric" disabled={!form.modelRetryEnabled} onChange={(event) => onChange({ ...form, modelRetryMaxAttempts: event.target.value })} />
</Label>
</>
)}
<ToggleField checked={form.modelOverrideRateLimit} label="覆盖模型限流策略" onChange={(checked) => onChange({ ...form, modelOverrideRateLimit: checked, modelRateLimitOverrideTouched: true })} />
{form.modelOverrideRateLimit && (
<>
<Label>模型 RPM<Input value={form.modelRpmLimit} placeholder="不填则不限制" inputMode="numeric" onChange={(event) => onChange({ ...form, modelRpmLimit: event.target.value, modelRateLimitOverrideTouched: true })} /></Label>
<Label>模型 RPS<Input value={form.modelRpsLimit} placeholder="不填则不限制" inputMode="numeric" onChange={(event) => onChange({ ...form, modelRpsLimit: event.target.value, modelRateLimitOverrideTouched: true })} /></Label>
<Label>模型 TPM<Input value={form.modelTpmLimit} placeholder="不填则不限制" inputMode="numeric" onChange={(event) => onChange({ ...form, modelTpmLimit: event.target.value, modelRateLimitOverrideTouched: true })} /></Label>
<Label>模型并发<Input value={form.modelConcurrencyLimit} placeholder="不填则不限制" inputMode="numeric" onChange={(event) => onChange({ ...form, modelConcurrencyLimit: event.target.value, modelRateLimitOverrideTouched: true })} /></Label>
</>
)}
</div>
);
}
function PlatformTable(props: {
filtered: boolean;
now: number;
platformModelCount: Map<string, number>;
platforms: IntegrationPlatform[];
providerMap: Map<string, CatalogProvider>;
pricingRuleSets: PricingRuleSet[];
togglingPlatformId: string;
onCreate: () => void;
onDelete: (platform: IntegrationPlatform) => void;
onEdit: (platform: IntegrationPlatform) => void;
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>
<strong>还没有接入平台</strong>
<span>点击“添加平台”配置平台信息、运行策略和模型绑定后,平台模型即可参与网关路由。</span>
<Button type="button" onClick={props.onCreate}>
<Plus size={15} />
添加第一个平台
</Button>
</div>
);
}
return (
<Table className="shTableViewport platformDataTable platformManagementTableViewport">
<TableRow className="shTableHeader">
<TableHead>平台</TableHead>
<TableHead>Provider</TableHead>
<TableHead>计价规则</TableHead>
<TableHead>折扣</TableHead>
<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 runtimeDescription = platformRuntimeDescription(platform);
const platformCooldownMs = cooldownRemainingMs(platform.cooldownUntil, props.now);
const isEnabled = platform.status === 'enabled';
const isToggling = props.togglingPlatformId === platform.id;
return (
<TableRow key={platform.id}>
<TableCell>
<span className="platformTableName">
<strong>{platformDisplayName(platform)}</strong>
<small>{platform.internalName ? `对外:${platform.name} · ${platform.platformKey}` : platform.platformKey}</small>
</span>
</TableCell>
<TableCell>{props.providerMap.get(platform.provider)?.displayName ?? platform.provider}</TableCell>
<TableCell>
<span className="platformTableName">
<strong title={pricing.title}>{pricing.title}</strong>
<small>{pricing.subtitle}</small>
</span>
</TableCell>
<TableCell>
<span className="platformTableName">
<strong>{formatDiscountFactor(platform.defaultDiscountFactor)}</strong>
<small>平台默认折扣</small>
</span>
</TableCell>
<TableCell>
<span className="platformTableName">
<strong title={rateLimit.title}>{rateLimit.title}</strong>
<small>{rateLimit.subtitle}</small>
</span>
</TableCell>
<TableCell>{props.platformModelCount.get(platform.id) ?? 0}</TableCell>
<TableCell>
<span className="platformStatusCell">
<span className="platformStatusToggle">
<Switch
aria-label={`${platformDisplayName(platform)} ${isEnabled ? '禁用' : '启用'}平台`}
checked={isEnabled}
disabled={isToggling}
onCheckedChange={(checked) => props.onToggleStatus(platform, checked ? 'enabled' : 'disabled')}
/>
<span>{isEnabled ? '启用' : '禁用'}</span>
</span>
<strong>
{platformCooldownMs > 0 ? (
<Badge variant="warning">冷却中</Badge>
) : (
<Badge variant={isEnabled ? 'success' : 'destructive'}>{isEnabled ? '已启用' : '已禁用'}</Badge>
)}
</strong>
{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>
<div className="tableActions">
<Button type="button" variant="ghost" size="icon" aria-label="编辑平台" onClick={() => props.onEdit(platform)}>
<Pencil size={14} />
</Button>
<Button type="button" variant="ghost" size="icon" aria-label="删除平台" onClick={() => props.onDelete(platform)}>
<Trash2 size={14} />
</Button>
</div>
</TableCell>
</TableRow>
);
})}
</Table>
);
}
function PlatformModelTable(props: {
baseModels: BaseModelCatalogItem[];
filterActive: boolean;
models: PlatformModel[];
platformMap: Map<string, IntegrationPlatform>;
platforms: IntegrationPlatform[];
providerMap: Map<string, CatalogProvider>;
now: number;
}) {
return (
<section className="platformModelView">
{!props.platforms.length ? (
<EmptyState title="暂无平台" description="请先添加平台,再查看对应平台模型。" />
) : (
props.models.length ? (
<section className="baseModelGrid platformModelCardGrid">
{props.models.map((model) => {
const platform = props.platformMap.get(model.platformId);
const provider = platform ? props.providerMap.get(platform.provider) : undefined;
const baseModel = findBaseModelForPlatformModel(platform, props.baseModels, model);
const modelIconPath = readPlatformModelIconPath(model, baseModel);
const modelCooldownMs = cooldownRemainingMs(model.cooldownUntil, props.now);
return (
<ModelCatalogCard
key={model.id}
badges={[
<Badge key="type" variant="outline">{model.modelType.join(', ')}</Badge>,
...(modelCooldownMs > 0 ? [<Badge key="cooldown" variant="warning">冷却中 · {formatCooldownRemaining(modelCooldownMs)}</Badge>] : []),
<Badge key="enabled" variant={model.enabled ? 'success' : 'secondary'}>{model.enabled ? 'enabled' : 'disabled'}</Badge>,
]}
chips={platformModelChips(model)}
iconPath={modelIconPath || provider?.iconPath}
iconText={provider?.displayName ?? model.provider ?? 'model'}
meta={[
platform ? platformDisplayName(platform) : model.platformName ?? '-',
provider?.displayName ?? platform?.provider ?? model.provider ?? '-',
model.modelAlias || model.modelName || '-',
]}
subtitle={model.providerModelName ? `调用模型名:${model.providerModelName}` : model.modelName}
title={model.displayName || model.modelName}
/>
);
})}
</section>
) : (
<EmptyState title="没有匹配的模型" description="换个平台或搜索关键词试试。" />
)
)}
</section>
);
}
function platformDisplayName(platform: IntegrationPlatform) {
return platform.internalName?.trim() || platform.name;
}
function FormSection(props: { children: ReactNode; icon: ReactNode; title: string }) {
return (
<section className="platformFormSection spanTwo">
<header>{props.icon}<strong>{props.title}</strong></header>
<div className="platformSectionGrid">{props.children}</div>
</section>
);
}
function AuthFields(props: { form: PlatformWizardForm; onChange: (value: PlatformWizardForm) => void }) {
const { form, onChange } = props;
const apiKeyPreview = credentialPreviewValue(form.credentialsPreview, 'apiKey', 'api_key', 'key');
const tokenPreview = credentialPreviewValue(form.credentialsPreview, 'token');
const accessKeyPreview = credentialPreviewValue(form.credentialsPreview, 'accessKey', 'access_key');
const secretKeyPreview = credentialPreviewValue(form.credentialsPreview, 'secretKey', 'secret_key');
if (form.authType === 'Token') {
return (
<CredentialField label="Token">
<Input
autoComplete="off"
name="gateway-platform-token"
value={form.token}
placeholder={credentialInputPlaceholder(tokenPreview)}
onChange={(event) => onChange({ ...form, token: event.target.value })}
/>
</CredentialField>
);
}
if (form.authType === 'AccessKey-SecretKey') {
return (
<>
<CredentialField label="AccessKey">
<Input
autoComplete="off"
name="gateway-platform-access-key"
value={form.accessKey}
placeholder={credentialInputPlaceholder(accessKeyPreview)}
onChange={(event) => onChange({ ...form, accessKey: event.target.value })}
/>
</CredentialField>
<CredentialField label="SecretKey">
<Input
autoComplete="off"
name="gateway-platform-secret-key"
value={form.secretKey}
placeholder={credentialInputPlaceholder(secretKeyPreview)}
onChange={(event) => onChange({ ...form, secretKey: event.target.value })}
/>
</CredentialField>
</>
);
}
if (form.authType === 'none') {
return <div className="platformReadonlyField">不写入平台凭证</div>;
}
return (
<CredentialField label="API Key">
<Input
autoComplete="off"
name="gateway-platform-api-key"
value={form.apiKey}
placeholder={credentialInputPlaceholder(apiKeyPreview)}
onChange={(event) => onChange({ ...form, apiKey: event.target.value })}
/>
</CredentialField>
);
}
function CredentialField(props: { children: ReactNode; label: string }) {
return (
<Label className="platformCredentialField">
{props.label}
{props.children}
<small>保持脱敏值不变表示不修改;填写新值表示覆盖;清空后保存表示清除。</small>
</Label>
);
}
function ToggleField(props: { checked: boolean; description?: string; label: string; onChange: (checked: boolean) => void }) {
return (
<label className="platformToggle">
<input type="checkbox" checked={props.checked} onChange={(event) => props.onChange(event.target.checked)} />
<span>
<strong>{props.label}</strong>
{props.description && <small>{props.description}</small>}
</span>
</label>
);
}
function ModelSelection(props: {
currentProvider: string;
form: PlatformWizardForm;
models: BaseModelCatalogItem[];
providerMap: Map<string, CatalogProvider>;
providerName: string;
onChange: (value: PlatformWizardForm) => void;
}) {
const [pickerOpen, setPickerOpen] = useState(false);
const selectedIds = new Set(props.form.selectedModelIds);
const selectedModels = selectedModelsForForm(props.models, props.form);
function removeModel(modelId: string) {
const next = new Set(selectedIds);
next.delete(modelId);
const modelDiscountFactors = { ...props.form.modelDiscountFactors };
const modelNameMappings = { ...props.form.modelNameMappings };
delete modelDiscountFactors[modelId];
delete modelNameMappings[modelId];
props.onChange({
...props.form,
modelDiscountFactors,
modelNameMappings,
selectionMode: 'partial',
selectedModelIds: Array.from(next),
});
}
function updateSelectedModelIds(modelIds: string[]) {
const visibleIds = new Set(props.models.map((model) => model.id));
const nextIds = Array.from(new Set(modelIds)).filter((modelId) => visibleIds.has(modelId));
props.onChange({ ...props.form, selectionMode: 'partial', selectedModelIds: nextIds });
}
function updateModelDiscount(modelId: string, value: string) {
props.onChange({
...props.form,
modelDiscountFactors: {
...props.form.modelDiscountFactors,
[modelId]: value,
},
});
}
function updateModelNameMapping(modelId: string, value: string) {
props.onChange({
...props.form,
modelNameMappings: {
...props.form.modelNameMappings,
[modelId]: value,
},
});
}
return (
<div className="platformModelSelector spanTwo">
<div className="platformModelSelectorHeader">
<div>
<strong>已选模型</strong>
<span>默认添加 {props.providerName} 的模型,也可以从整个模型库添加其它厂商模型。</span>
</div>
<Button type="button" variant="outline" onClick={() => setPickerOpen(true)}>
<Plus size={14} />
添加模型
</Button>
</div>
{!selectedModels.length ? (
<div className="platformModelEmpty">当前没有已选模型,点击“添加模型”从模型库选择。</div>
) : (
<div className="platformModelChoices">
{selectedModels.map((model) => {
const modelLabel = stableModelAlias(model) || model.providerModelName;
const defaultProviderModelName = providerModelNameForProvider(model, props.currentProvider);
const providerModelName = props.form.modelNameMappings[model.id] ?? defaultProviderModelName;
const providerPreset = providerModelPresetFor(model, props.currentProvider);
return (
<div className="platformModelChoice" key={model.id}>
<div className="platformModelChoiceMain">
<span>
<strong>{modelLabel}</strong>
<small>
{props.providerMap.get(model.providerKey)?.displayName ?? model.providerKey}
{providerPreset ? ` · ${props.providerName} 真实名 ${defaultProviderModelName}` : ` · ${model.providerModelName}`}
{' · '}
{baseModelTypeTextForProvider(model, props.currentProvider)}
</small>
</span>
</div>
<div className="platformModelChoiceFields">
<Label>
调用模型名
<Input
aria-label={`${modelLabel} 调用模型名`}
placeholder={defaultProviderModelName}
value={providerModelName}
onChange={(event) => updateModelNameMapping(model.id, event.target.value)}
/>
</Label>
<Label>
折扣率
<Input
aria-label={`${modelLabel} 折扣率`}
inputMode="decimal"
placeholder="继承"
value={props.form.modelDiscountFactors[model.id] ?? ''}
onChange={(event) => updateModelDiscount(model.id, event.target.value)}
/>
</Label>
</div>
<Button type="button" variant="ghost" size="icon" aria-label="移除模型" onClick={() => removeModel(model.id)}>
<Trash2 size={14} />
</Button>
</div>
);
})}
</div>
)}
<ModelPickerDialog
currentProvider={props.currentProvider}
models={props.models}
open={pickerOpen}
providerMap={props.providerMap}
selectedModelIds={props.form.selectedModelIds}
onClose={() => setPickerOpen(false)}
onConfirm={updateSelectedModelIds}
/>
</div>
);
}
function ModelPickerDialog(props: {
currentProvider: string;
models: BaseModelCatalogItem[];
open: boolean;
providerMap: Map<string, CatalogProvider>;
selectedModelIds: string[];
onClose: () => void;
onConfirm: (modelIds: string[]) => void;
}) {
const [query, setQuery] = useState('');
const [providerFilter, setProviderFilter] = useState(props.currentProvider || 'all');
const [draftIds, setDraftIds] = useState<string[]>(props.selectedModelIds);
const providerOptions = useMemo(() => Array.from(new Set([
...props.models.map((model) => model.providerKey),
...props.models.flatMap((model) => providerModelPresets(model).map((preset) => preset.providerKey)),
])).filter(Boolean), [props.models]);
const draftIdSet = new Set(draftIds);
const keyword = query.trim().toLowerCase();
const filteredModels = props.models.filter((model) => {
if (providerFilter !== 'all' && !modelMatchesProvider(model, providerFilter)) return false;
const providerForText = providerFilter === 'all' ? props.currentProvider : providerFilter;
if (!keyword) return true;
return [
stableModelAlias(model),
model.providerModelName,
providerModelNameForProvider(model, providerForText),
model.canonicalModelKey,
baseModelTypeTextForProvider(model, providerForText),
model.providerKey,
props.providerMap.get(model.providerKey)?.displayName,
...providerModelPresets(model).flatMap((preset) => [
preset.providerKey,
preset.providerModelName,
preset.displayName,
preset.sourceProviderName,
]),
JSON.stringify(model.capabilities ?? {}),
].filter(Boolean).join(' ').toLowerCase().includes(keyword);
});
useEffect(() => {
if (!props.open) return;
setQuery('');
setProviderFilter(props.currentProvider || 'all');
setDraftIds(props.selectedModelIds);
}, [props.currentProvider, props.open, props.selectedModelIds]);
useEffect(() => {
if (!props.open) return undefined;
function onKeyDown(event: KeyboardEvent) {
if (event.key !== 'Escape') return;
event.preventDefault();
event.stopImmediatePropagation();
props.onClose();
}
window.addEventListener('keydown', onKeyDown, true);
return () => window.removeEventListener('keydown', onKeyDown, true);
}, [props.open, props.onClose]);
function toggleModel(modelId: string) {
setDraftIds((current) => {
const next = new Set(current);
if (next.has(modelId)) next.delete(modelId);
else next.add(modelId);
return Array.from(next);
});
}
function addFilteredModels() {
setDraftIds((current) => Array.from(new Set([...current, ...filteredModels.map((model) => model.id)])));
}
if (!props.open) return null;
return (
<div className="modelPickerBackdrop" role="presentation">
<section className="modelPickerDialog" role="dialog" aria-modal="true" aria-label="选择模型">
<header className="modelPickerHeader">
<div>
<strong>添加模型</strong>
<span>已选择 {draftIds.length} 个模型</span>
</div>
<Button type="button" variant="ghost" size="icon" onClick={props.onClose} aria-label="关闭">
<X size={15} />
</Button>
</header>
<div className="modelPickerToolbar">
<Label>
来源平台
<Select value={providerFilter} onChange={(event) => setProviderFilter(event.target.value)}>
<option value="all">全部模型库</option>
{providerOptions.map((provider) => (
<option value={provider} key={provider}>{props.providerMap.get(provider)?.displayName ?? provider}</option>
))}
</Select>
</Label>
<Label className="modelPickerSearch">
搜索模型
<div>
<Search size={14} />
<Input
value={query}
placeholder="模型名称、能力或 Key"
onChange={(event) => setQuery(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter') event.preventDefault();
}}
/>
</div>
</Label>
<Button type="button" variant="outline" onClick={addFilteredModels}>
<Plus size={14} />
添加当前结果
</Button>
</div>
<div className="modelPickerList">
{filteredModels.length ? filteredModels.map((model) => {
const providerForText = providerFilter === 'all' ? props.currentProvider : providerFilter;
const preset = providerModelPresetFor(model, providerForText);
const providerModelName = providerModelNameForProvider(model, providerForText);
return (
<label className="modelPickerItem" key={model.id}>
<input type="checkbox" checked={draftIdSet.has(model.id)} onChange={() => toggleModel(model.id)} />
<span>
<strong>{stableModelAlias(model) || model.providerModelName}</strong>
<small>
{props.providerMap.get(model.providerKey)?.displayName ?? model.providerKey}
{preset ? ` · ${providerForText} 真实名 ${providerModelName}` : ` · ${model.providerModelName}`}
{' · '}
{baseModelTypeTextForProvider(model, providerForText)}
</small>
</span>
<Badge variant="outline">{baseModelTypeTextForProvider(model, providerForText)}</Badge>
</label>
);
}) : <div className="platformModelEmpty">没有匹配的模型。</div>}
</div>
<footer className="modelPickerActions">
<Button type="button" variant="outline" onClick={props.onClose}>取消</Button>
<Button type="button" onClick={() => {
props.onConfirm(draftIds);
props.onClose();
}}>
确认添加
</Button>
</footer>
</section>
</div>
);
}
function countModelsByPlatform(models: PlatformModel[]) {
const counts = new Map<string, number>();
models.forEach((model) => counts.set(model.platformId, (counts.get(model.platformId) ?? 0) + 1));
return counts;
}
function providerDefaults(provider?: CatalogProvider) {
return {
defaultAuthType: provider?.defaultAuthType || 'APIKey',
defaultBaseUrl: provider?.defaultBaseUrl || '',
};
}
function platformToForm(
platform: IntegrationPlatform,
baseModels: BaseModelCatalogItem[],
platformModels: PlatformModel[],
defaults?: ReturnType<typeof providerDefaults>,
): PlatformWizardForm {
const config = platform.config ?? {};
const retryPolicy = platform.retryPolicy ?? {};
const rateLimitPolicy = platform.rateLimitPolicy ?? {};
const networkProxy = readNetworkProxyConfig(config);
const currentModels = platformModels.filter((model) => model.platformId === platform.id);
const modelRateLimitPolicy = currentModels.find((model) => model.rateLimitPolicyMode === 'override')?.rateLimitPolicy ?? {};
return {
...createEmptyPlatformForm(platform.provider, defaults),
provider: platform.provider,
platformKey: platform.platformKey,
name: platform.name,
internalName: platform.internalName ?? '',
baseUrl: platform.baseUrl ?? defaults?.defaultBaseUrl ?? '',
authType: platform.authType || defaults?.defaultAuthType || 'APIKey',
credentialsPreview: platform.credentialsPreview ?? {},
apiKey: credentialPreviewValue(platform.credentialsPreview, 'apiKey', 'api_key', 'key'),
token: credentialPreviewValue(platform.credentialsPreview, 'token'),
accessKey: credentialPreviewValue(platform.credentialsPreview, 'accessKey', 'access_key'),
secretKey: credentialPreviewValue(platform.credentialsPreview, 'secretKey', 'secret_key'),
pricingRuleSetId: platform.pricingRuleSetId ?? '',
defaultDiscountFactor: numberText(platform.defaultDiscountFactor, '1'),
priority: numberText(platform.priority, '100'),
retryEnabled: readBoolean(retryPolicy, 'enabled', true),
retryMaxAttempts: numberText(readNumber(retryPolicy, 'maxAttempts'), '2'),
rpmLimit: readLimit(rateLimitPolicy, 'rpm'),
rpsLimit: readLimit(rateLimitPolicy, 'rps'),
tpmLimit: readLimit(rateLimitPolicy, 'tpm_total'),
concurrencyLimit: readLimit(rateLimitPolicy, 'concurrent'),
testMode: readBoolean(config, 'testMode', false),
proxyMode: networkProxy.proxyMode,
httpProxy: networkProxy.httpProxy,
supportBase64Input: readBoolean(config, 'supportBase64Input', true),
supportUrlInput: readBoolean(config, 'supportUrlInput', true),
selectedModelIds: platformModelBaseIds(platform, baseModels, currentModels),
modelDiscountFactors: platformModelDiscountFactors(platform, baseModels, currentModels),
modelNameMappings: platformModelNameMappings(platform, baseModels, currentModels),
modelOverrideRateLimit: currentModels.length > 0 && currentModels.every((model) => model.rateLimitPolicyMode === 'override'),
modelRateLimitOverrideTouched: false,
modelRpmLimit: readLimit(modelRateLimitPolicy, 'rpm'),
modelRpsLimit: readLimit(modelRateLimitPolicy, 'rps'),
modelTpmLimit: readLimit(modelRateLimitPolicy, 'tpm_total'),
modelConcurrencyLimit: readLimit(modelRateLimitPolicy, 'concurrent'),
selectionMode: 'partial',
};
}
function defaultSelectedModelIds(models: BaseModelCatalogItem[], provider: string) {
return modelsForProvider(models, provider).map((model) => model.id);
}
function platformModelBaseIds(platform: IntegrationPlatform, baseModels: BaseModelCatalogItem[], platformModels: PlatformModel[]) {
const ids = platformModels.map((model) => findBaseModelForPlatformModel(platform, baseModels, model)?.id).filter((id): id is string => Boolean(id));
return Array.from(new Set(ids));
}
function platformModelDiscountFactors(platform: IntegrationPlatform, baseModels: BaseModelCatalogItem[], platformModels: PlatformModel[]) {
return platformModels.reduce<Record<string, string>>((acc, model) => {
const baseModel = findBaseModelForPlatformModel(platform, baseModels, model);
if (baseModel?.id && model.discountFactor) acc[baseModel.id] = String(model.discountFactor);
return acc;
}, {});
}
function platformModelNameMappings(platform: IntegrationPlatform, baseModels: BaseModelCatalogItem[], platformModels: PlatformModel[]) {
return platformModels.reduce<Record<string, string>>((acc, model) => {
const baseModel = findBaseModelForPlatformModel(platform, baseModels, model);
if (baseModel?.id) acc[baseModel.id] = model.providerModelName || model.modelName;
return acc;
}, {});
}
function findBaseModelForPlatformModel(platform: IntegrationPlatform | undefined, baseModels: BaseModelCatalogItem[], model: PlatformModel) {
return baseModels.find((item) => item.id === model.baseModelId) ??
baseModels.find((item) => item.canonicalModelKey === model.modelAlias) ??
baseModels.find((item) => stableModelAlias(item) === model.modelAlias) ??
baseModels.find((item) => item.providerModelName === model.modelName && model.modelType.some((type) => baseModelTypes(item).includes(type))) ??
baseModels.find((item) => item.providerKey === platform?.provider && item.providerModelName === model.modelName && model.modelType.some((type) => baseModelTypes(item).includes(type)));
}
function readPlatformModelIconPath(model: PlatformModel, baseModel?: BaseModelCatalogItem) {
return readString(baseModel?.metadata?.iconPath) ||
readString(readRecord(baseModel?.metadata?.rawModel)?.icon_path) ||
readString(readRecord(model.capabilities)?.iconPath) ||
readString(readRecord(model.capabilities)?.icon_path) ||
readString(readRecord(model.billingConfig)?.iconPath) ||
readString(readRecord(model.billingConfig)?.icon_path);
}
function readString(value: unknown) {
return typeof value === 'string' ? value : '';
}
function credentialPreviewValue(preview: Record<string, unknown> | undefined, ...keys: string[]) {
if (!preview) return '';
for (const key of keys) {
const value = preview[key];
if (typeof value === 'string' && value.trim()) return value;
}
return '';
}
function credentialInputPlaceholder(preview: string) {
return preview ? '填写新凭证以覆盖当前值' : '';
}
function readRecord(value: unknown) {
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
function readBoolean(source: Record<string, unknown>, key: string, fallback: boolean) {
const value = source[key];
return typeof value === 'boolean' ? value : fallback;
}
function readNetworkProxyConfig(config: Record<string, unknown>): Pick<PlatformWizardForm, 'proxyMode' | 'httpProxy'> {
const networkProxy = readRecord(config.networkProxy);
const mode = readString(networkProxy.mode || config.proxyMode);
const httpProxy = readString(networkProxy.httpProxy || config.httpProxy);
if (mode === 'global' || mode === 'custom') {
return { proxyMode: mode, httpProxy };
}
return { proxyMode: 'none', httpProxy: '' };
}
function readNumber(source: Record<string, unknown>, key: string) {
const value = source[key];
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
}
function readLimit(policy: Record<string, unknown>, metric: string) {
const rules = Array.isArray(policy.rules) ? policy.rules : [];
const rule = rules.find((item): item is { metric?: unknown; limit?: unknown } => typeof item === 'object' && item !== null && 'metric' in item && item.metric === metric);
return typeof rule?.limit === 'number' ? String(rule.limit) : '';
}
function numberText(value: unknown, fallback: string) {
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? String(value) : fallback;
}
function providerDisplayName(providerKey: string, providerMap: Map<string, CatalogProvider>) {
return providerMap.get(providerKey)?.displayName || providerKey;
}
function shouldUseProviderDefaultName(value: string, previousProviderName: string) {
const trimmed = value.trim();
return !trimmed || trimmed === previousProviderName;
}
function platformModelChips(model: PlatformModel) {
const chips: string[] = [model.pricingMode];
if (model.discountFactor) chips.push(`折扣 ${model.discountFactor}`);
if (model.pricingRuleSetId) chips.push('计价规则');
if (model.runtimePolicySetId) chips.push('运行策略');
return chips.filter(Boolean);
}
function platformPricingSummary(platform: IntegrationPlatform, ruleSets: PricingRuleSet[]) {
const matchedRuleSet = platform.pricingRuleSetId ? ruleSets.find((item) => item.id === platform.pricingRuleSetId) : undefined;
const defaultRuleSet = ruleSets.find((item) => item.category === 'default' && item.status === 'active') ?? ruleSets.find((item) => item.category === 'default');
const title = matchedRuleSet?.name || defaultRuleSet?.name || '默认计价规则';
return {
title,
subtitle: pricingModeText(platform.defaultPricingMode),
};
}
function pricingModeText(mode: IntegrationPlatform['defaultPricingMode']) {
if (mode === 'custom') return '自定义计价';
if (mode === 'inherit') return '直接继承';
return '继承并应用折扣';
}
function formatDiscountFactor(value: number | undefined) {
const factor = typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 1;
return `${trimNumber(factor * 100)}%`;
}
function platformRateLimitSummary(policy: IntegrationPlatform['rateLimitPolicy']) {
const rules = Array.isArray(policy?.rules) ? policy.rules : [];
if (!rules.length) {
return { title: '不限', subtitle: '未配置限流上限' };
}
const labels = rules
.filter((rule) => typeof rule.limit === 'number' && Number.isFinite(rule.limit) && rule.limit > 0)
.map((rule) => `${rateLimitMetricText(rule.metric)} ${formatLimit(rule.limit)}`);
if (!labels.length) {
return { title: '不限', subtitle: '未配置限流上限' };
}
return {
title: labels.slice(0, 2).join(' · '),
subtitle: labels.length > 2 ? labels.slice(2).join(' · ') : '平台级限流',
};
}
function rateLimitMetricText(metric: string) {
const labels: Record<string, string> = {
tpm_total: 'TPM',
tpm_input: '输入TPM',
tpm_output: '输出TPM',
rpm: 'RPM',
rps: 'RPS',
concurrent: '并发',
queue_size: '队列',
};
return labels[metric] ?? metric;
}
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);
}
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)} 分钟`;
const seconds = Math.ceil(milliseconds / 1000);
return `${Math.max(seconds, 1)} 秒`;
}
function platformRuntimeDescription(platform: IntegrationPlatform) {
const retryPolicy = platform.retryPolicy ?? {};
const retryEnabled = readBoolean(retryPolicy, 'enabled', true);
const maxAttempts = readNumber(retryPolicy, 'maxAttempts') ?? 2;
return `${retryEnabled ? `同平台最多尝试 ${maxAttempts} 次` : '同平台不重试'} · ${proxyModeText(readNetworkProxyConfig(platform.config ?? {}).proxyMode)}`;
}
function proxyModeText(mode: PlatformWizardForm['proxyMode']) {
if (mode === 'global') return '全局代理';
if (mode === 'custom') return '自定义代理';
return '直连';
}
function globalProxyStatusText(config: { globalHttpProxy?: string; globalHttpProxySet: boolean } | null) {
if (!config) return '读取中';
const proxy = config.globalHttpProxy?.trim() ?? '';
return config.globalHttpProxySet && proxy ? proxy : '未设置';
}
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+$/, '');
}