1281 lines
55 KiB
TypeScript
1281 lines
55 KiB
TypeScript
import { useEffect, useMemo, useState, type FormEvent, type ReactNode } from 'react';
|
||
import { Boxes, CheckCircle2, Gauge, Globe2, KeyRound, Pencil, Plus, RotateCcw, Search, ServerCog, ShieldCheck, SlidersHorizontal, Trash2, X } from 'lucide-react';
|
||
import type { BaseModelCatalogItem, CatalogProvider, IntegrationPlatform, ModelRateLimitStatus, PlatformModel, PricingRuleSet } from '@easyai-ai-gateway/contracts';
|
||
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, ConfirmDialog, EmptyState, FormDialog, Input, Label, ScreenMessage, Select, Table, TableCell, TableHead, TableRow } from '../../components/ui';
|
||
import type { LoadState, PlatformWithModelsInput } from '../../types';
|
||
import {
|
||
authTypes,
|
||
applyProviderDefaults,
|
||
baseModelTypes,
|
||
baseModelTypeText,
|
||
createEmptyPlatformForm,
|
||
modelsForProvider,
|
||
platformModelPayloads,
|
||
platformPayload,
|
||
providerLabel,
|
||
selectedModelsForForm,
|
||
stableModelAlias,
|
||
validatePlatformForm,
|
||
type PlatformWizardForm,
|
||
} from './platform-form';
|
||
import { ModelCatalogCard } from './ModelCatalogCard';
|
||
|
||
export function PlatformManagementPanel(props: {
|
||
baseModels: BaseModelCatalogItem[];
|
||
message: string;
|
||
modelRateLimits: ModelRateLimitStatus[];
|
||
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>;
|
||
}) {
|
||
const defaultProvider = props.providers[0]?.providerKey ?? props.baseModels[0]?.providerKey ?? '';
|
||
const [now, setNow] = useState(() => Date.now());
|
||
const [dialogOpen, setDialogOpen] = useState(false);
|
||
const [viewMode, setViewMode] = useState<'platforms' | 'models' | 'limits'>('platforms');
|
||
const [modelQuery, setModelQuery] = 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 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)])).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 || props.platforms[0]?.id || '';
|
||
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 text = [
|
||
model.displayName,
|
||
model.modelName,
|
||
model.providerModelName,
|
||
model.modelAlias,
|
||
...model.modelType,
|
||
model.provider,
|
||
platform?.name,
|
||
platform?.internalName,
|
||
platform?.platformKey,
|
||
].filter(Boolean).join(' ').toLowerCase();
|
||
return matchesPlatform && (!keyword || text.includes(keyword));
|
||
});
|
||
}, [modelQuery, 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 : '删除平台失败');
|
||
}
|
||
}
|
||
|
||
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>
|
||
<button type="button" data-active={viewMode === 'limits'} onClick={() => setViewMode('limits')}>实时限流</button>
|
||
</div>
|
||
{viewMode === 'platforms' ? (
|
||
<PlatformTable
|
||
now={now}
|
||
platformModelCount={platformModelCount}
|
||
platforms={props.platforms}
|
||
providerMap={providerMap}
|
||
pricingRuleSets={props.pricingRuleSets}
|
||
onDelete={setPendingDeletePlatform}
|
||
onCreate={openCreateDialog}
|
||
onEdit={openEditDialog}
|
||
/>
|
||
) : viewMode === 'models' ? (
|
||
<PlatformModelTable
|
||
baseModels={props.baseModels}
|
||
modelQuery={modelQuery}
|
||
models={filteredPlatformModels}
|
||
platformId={selectedModelPlatformId}
|
||
platformMap={platformMap}
|
||
platforms={props.platforms}
|
||
providerMap={providerMap}
|
||
onModelQueryChange={setModelQuery}
|
||
now={now}
|
||
onPlatformChange={setSelectedPlatformId}
|
||
/>
|
||
) : (
|
||
<RateLimitStatusTable statuses={props.modelRateLimits} platformMap={platformMap} now={now} />
|
||
)}
|
||
{props.message && <p className="formMessage">{props.message}</p>}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<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 })} />
|
||
{form.modelOverrideRateLimit && (
|
||
<>
|
||
<Label>模型 RPM<Input value={form.modelRpmLimit} placeholder="不填则不限制" inputMode="numeric" onChange={(event) => onChange({ ...form, modelRpmLimit: event.target.value })} /></Label>
|
||
<Label>模型 RPS<Input value={form.modelRpsLimit} placeholder="不填则不限制" inputMode="numeric" onChange={(event) => onChange({ ...form, modelRpsLimit: event.target.value })} /></Label>
|
||
<Label>模型 TPM<Input value={form.modelTpmLimit} placeholder="不填则不限制" inputMode="numeric" onChange={(event) => onChange({ ...form, modelTpmLimit: event.target.value })} /></Label>
|
||
<Label>模型并发<Input value={form.modelConcurrencyLimit} placeholder="不填则不限制" inputMode="numeric" onChange={(event) => onChange({ ...form, modelConcurrencyLimit: event.target.value })} /></Label>
|
||
</>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function PlatformTable(props: {
|
||
now: number;
|
||
platformModelCount: Map<string, number>;
|
||
platforms: IntegrationPlatform[];
|
||
providerMap: Map<string, CatalogProvider>;
|
||
pricingRuleSets: PricingRuleSet[];
|
||
onCreate: () => void;
|
||
onDelete: (platform: IntegrationPlatform) => void;
|
||
onEdit: (platform: IntegrationPlatform) => void;
|
||
}) {
|
||
if (!props.platforms.length) {
|
||
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="platformDataTable">
|
||
<TableRow className="shTableHeader">
|
||
<TableHead>平台</TableHead>
|
||
<TableHead>Provider</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 runtime = platformRuntimeSummary(platform);
|
||
const platformCooldownMs = cooldownRemainingMs(platform.cooldownUntil, props.now);
|
||
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="platformTableName">
|
||
<strong>
|
||
{platformCooldownMs > 0 ? (
|
||
<Badge variant="warning">冷却中</Badge>
|
||
) : (
|
||
<Badge variant={platform.status === 'enabled' ? 'success' : 'secondary'}>{platform.status}</Badge>
|
||
)}
|
||
</strong>
|
||
<small>{platformCooldownMs > 0 ? `剩余 ${formatCooldownRemaining(platformCooldownMs)}` : runtime}</small>
|
||
</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[];
|
||
modelQuery: string;
|
||
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="请先添加平台,再查看对应平台模型。" />
|
||
) : (
|
||
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 RateLimitStatusTable(props: { statuses: ModelRateLimitStatus[]; platformMap: Map<string, IntegrationPlatform>; now: number }) {
|
||
if (!props.statuses.length) {
|
||
return <EmptyState title="暂无限流状态" description="模型产生请求后会在这里显示实时 RPM、TPM 和并发窗口。" />;
|
||
}
|
||
return (
|
||
<section className="platformLimitView">
|
||
<div className="platformLimitHeader">
|
||
<span><Gauge size={15} />按综合满载率从高到低排序</span>
|
||
<small>每 3 秒刷新一次;TPM 显示已结算 + 预占。</small>
|
||
</div>
|
||
<div className="platformLimitTableViewport">
|
||
<Table className="platformDataTable platformLimitTable">
|
||
<TableRow className="shTableHeader">
|
||
<TableHead>模型</TableHead>
|
||
<TableHead>平台</TableHead>
|
||
<TableHead>并发</TableHead>
|
||
<TableHead>TPM</TableHead>
|
||
<TableHead>RPM</TableHead>
|
||
<TableHead>状态</TableHead>
|
||
<TableHead>满载率</TableHead>
|
||
</TableRow>
|
||
{props.statuses.map((status) => {
|
||
const platform = props.platformMap.get(status.platformId);
|
||
return (
|
||
<TableRow key={status.platformModelId}>
|
||
<TableCell>
|
||
<span className="platformTableName">
|
||
<strong>{status.displayName || status.modelAlias || status.modelName}</strong>
|
||
<small>{status.providerModelName || status.modelName}</small>
|
||
</span>
|
||
</TableCell>
|
||
<TableCell>
|
||
<span className="platformTableName">
|
||
<strong>{platform ? platformDisplayName(platform) : status.platformName}</strong>
|
||
<small>{status.provider}</small>
|
||
</span>
|
||
</TableCell>
|
||
<TableCell>{metricCell(status.concurrent)}</TableCell>
|
||
<TableCell>{metricCell(status.tpm, true)}</TableCell>
|
||
<TableCell>{metricCell(status.rpm)}</TableCell>
|
||
<TableCell>{modelRuntimeStatusCell(status, props.now)}</TableCell>
|
||
<TableCell>
|
||
<span className="rateLoadCell">
|
||
<strong>{formatPercent(status.loadRatio)}</strong>
|
||
<span className="rateLoadTrack"><i style={{ width: `${Math.min(status.loadRatio * 100, 100)}%` }} /></span>
|
||
</span>
|
||
</TableCell>
|
||
</TableRow>
|
||
);
|
||
})}
|
||
</Table>
|
||
</div>
|
||
</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 providerModelName = props.form.modelNameMappings[model.id] ?? model.providerModelName;
|
||
return (
|
||
<div className="platformModelChoice" key={model.id}>
|
||
<div className="platformModelChoiceMain">
|
||
<span>
|
||
<strong>{modelLabel}</strong>
|
||
<small>{props.providerMap.get(model.providerKey)?.displayName ?? model.providerKey} · {model.providerModelName} · {baseModelTypeText(model)}</small>
|
||
</span>
|
||
</div>
|
||
<div className="platformModelChoiceFields">
|
||
<Label>
|
||
调用模型名
|
||
<Input
|
||
aria-label={`${modelLabel} 调用模型名`}
|
||
placeholder={model.providerModelName}
|
||
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))).filter(Boolean), [props.models]);
|
||
const draftIdSet = new Set(draftIds);
|
||
const keyword = query.trim().toLowerCase();
|
||
const filteredModels = props.models.filter((model) => {
|
||
if (providerFilter !== 'all' && model.providerKey !== providerFilter) return false;
|
||
if (!keyword) return true;
|
||
return [
|
||
stableModelAlias(model),
|
||
model.providerModelName,
|
||
model.canonicalModelKey,
|
||
baseModelTypeText(model),
|
||
model.providerKey,
|
||
props.providerMap.get(model.providerKey)?.displayName,
|
||
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) => (
|
||
<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} · {model.providerModelName} · {baseModelTypeText(model)}</small>
|
||
</span>
|
||
<Badge variant="outline">{baseModelTypeText(model)}</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);
|
||
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),
|
||
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))
|
||
.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 metricCell(metric: ModelRateLimitStatus['rpm'], includeReserved = false) {
|
||
if (!metric.limited) return <span className="rateMetricCell"><strong>{formatLimit(metric.currentValue)} / 不限</strong><small>未配置上限</small></span>;
|
||
return (
|
||
<span className="rateMetricCell">
|
||
<strong>{formatLimit(metric.currentValue)} / {formatLimit(metric.limitValue)}</strong>
|
||
<small>{includeReserved && metric.reservedValue > 0 ? `已用 ${formatLimit(metric.usedValue)} · 预占 ${formatLimit(metric.reservedValue)}` : `窗口 ${formatPercent(metric.ratio)}`}</small>
|
||
</span>
|
||
);
|
||
}
|
||
|
||
function modelRuntimeStatusCell(status: ModelRateLimitStatus, now: number) {
|
||
const modelCooldownMs = cooldownRemainingMs(status.modelCooldownUntil, now);
|
||
const platformCooldownMs = cooldownRemainingMs(status.platformCooldownUntil, now);
|
||
if (modelCooldownMs > 0) {
|
||
return (
|
||
<span className="platformTableName">
|
||
<strong><Badge variant="warning">模型冷却中</Badge></strong>
|
||
<small>剩余 {formatCooldownRemaining(modelCooldownMs)}</small>
|
||
</span>
|
||
);
|
||
}
|
||
if (platformCooldownMs > 0) {
|
||
return (
|
||
<span className="platformTableName">
|
||
<strong><Badge variant="warning">平台冷却中</Badge></strong>
|
||
<small>剩余 {formatCooldownRemaining(platformCooldownMs)}</small>
|
||
</span>
|
||
);
|
||
}
|
||
return (
|
||
<span className="platformTableName">
|
||
<strong><Badge variant={status.enabled ? 'success' : 'secondary'}>{status.enabled ? '可用' : '已停用'}</Badge></strong>
|
||
<small>{status.enabled ? '参与路由' : '不参与路由'}</small>
|
||
</span>
|
||
);
|
||
}
|
||
|
||
function cooldownRemainingMs(cooldownUntil: string | undefined, now: number) {
|
||
if (!cooldownUntil) return 0;
|
||
const until = Date.parse(cooldownUntil);
|
||
if (!Number.isFinite(until)) return 0;
|
||
return Math.max(until - now, 0);
|
||
}
|
||
|
||
function formatCooldownRemaining(milliseconds: number) {
|
||
const minutes = milliseconds / 60000;
|
||
if (minutes >= 1) return `${trimNumber(Math.ceil(minutes * 10) / 10)} 分钟`;
|
||
const seconds = Math.ceil(milliseconds / 1000);
|
||
return `${Math.max(seconds, 1)} 秒`;
|
||
}
|
||
|
||
function formatPercent(value: number) {
|
||
if (!Number.isFinite(value) || value <= 0) return '0%';
|
||
return `${trimNumber(value * 100)}%`;
|
||
}
|
||
|
||
function platformRuntimeSummary(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)}`;
|
||
}
|
||
|
||
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+$/, '');
|
||
}
|