chore: commit pending gateway changes

This commit is contained in:
2026-05-10 22:34:15 +08:00
parent 53f8edfb67
commit d59756a27c
71 changed files with 15106 additions and 656 deletions
+288 -123
View File
@@ -4,91 +4,109 @@ import type {
BaseModelCatalogItem,
BaseModelUpsertRequest,
CatalogProvider,
PricingRuleSet,
RuntimePolicySet,
} from '@easyai-ai-gateway/contracts';
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, FormDialog, Input, Label, Select, Textarea } from '../../components/ui';
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, ConfirmDialog, FormDialog, FormItem, Label, Input, Select, Textarea } from '../../components/ui';
import type { LoadState } from '../../types';
import { BaseModelCapabilityEditor } from './BaseModelCapabilityEditor';
import {
addCapabilityType,
capabilitiesFromForm,
capabilitiesToForm,
createCapabilityEditorState,
platformModelTypeDefinitions,
removeCapabilityType,
syncPrimaryCapability,
type CapabilityEditorState,
} from './base-model-capabilities';
import { ModelCatalogCard } from './ModelCatalogCard';
type ModelForm = {
providerKey: string;
canonicalModelKey: string;
providerModelName: string;
modelType: string;
displayName: string;
modelAlias: string;
status: string;
pricingRuleSetId: string;
runtimePolicySetId: string;
runtimePolicyOverrideJson: string;
pricingVersion: string;
capabilitiesJson: string;
capabilities: CapabilityEditorState;
billingJson: string;
rateLimitJson: string;
metadataJson: string;
};
const statuses = ['active', 'deprecated', 'hidden'];
const fallbackTypes = [
'text_generate',
'image_generate',
'image_edit',
'image_analysis',
'video_generate',
'image_to_video',
'omni_video',
'text_embedding',
'text_to_speech',
'audio_generate',
'digital_human_generate',
'text_to_model',
'image_to_model',
'multiview_to_model',
'mesh_edit',
];
const fallbackTypes = platformModelTypeDefinitions.map((item) => item.key);
const emptyForm: ModelForm = {
providerKey: '',
canonicalModelKey: '',
providerModelName: '',
modelType: 'text_generate',
displayName: '',
status: 'active',
pricingVersion: '1',
capabilitiesJson: '{}',
billingJson: '{}',
rateLimitJson: '{}',
metadataJson: '{}',
};
function createEmptyForm(modelType = 'text_generate'): ModelForm {
return {
providerKey: '',
canonicalModelKey: '',
providerModelName: '',
modelAlias: '',
status: 'active',
pricingRuleSetId: '',
runtimePolicySetId: '',
runtimePolicyOverrideJson: '{}',
pricingVersion: '1',
capabilities: createCapabilityEditorState(modelType),
billingJson: '{}',
rateLimitJson: '{}',
metadataJson: '{}',
};
}
export function BaseModelCatalogPanel(props: {
baseModels: BaseModelCatalogItem[];
message: string;
pricingRuleSets: PricingRuleSet[];
providers: CatalogProvider[];
runtimePolicySets: RuntimePolicySet[];
state: LoadState;
onDeleteBaseModel: (baseModelId: string) => Promise<void>;
onResetAllBaseModels: () => Promise<void>;
onResetBaseModel: (baseModelId: string) => Promise<void>;
onSaveBaseModel: (input: BaseModelUpsertRequest, baseModelId?: string) => Promise<void>;
}) {
const [dialogOpen, setDialogOpen] = useState(false);
const [editingId, setEditingId] = useState('');
const [form, setForm] = useState<ModelForm>(emptyForm);
const [form, setForm] = useState<ModelForm>(() => createEmptyForm());
const [localError, setLocalError] = useState('');
const [query, setQuery] = useState('');
const [providerFilter, setProviderFilter] = useState('all');
const [typeFilter, setTypeFilter] = useState('all');
const [pendingDeleteModel, setPendingDeleteModel] = useState<BaseModelCatalogItem | null>(null);
const [pendingResetModel, setPendingResetModel] = useState<BaseModelCatalogItem | null>(null);
const [pendingResetAll, setPendingResetAll] = useState(false);
const providerNames = useMemo(
() => new Map(props.providers.map((item) => [item.providerKey, item.displayName])),
[props.providers],
);
const typeOptions = useMemo(
() => Array.from(new Set([...fallbackTypes, ...props.baseModels.map((item) => item.modelType).filter(Boolean)])),
() => Array.from(new Set([...fallbackTypes, ...props.baseModels.flatMap(readModelTypes)])),
[props.baseModels],
);
const providerOptions = useMemo(
() => Array.from(new Set([...props.providers.map((item) => item.providerKey), ...props.baseModels.map((item) => item.providerKey)])),
[props.baseModels, props.providers],
);
const defaultPricingRuleSetId = useMemo(() => defaultRuleSetId(props.pricingRuleSets), [props.pricingRuleSets]);
const defaultRuntimePolicySetId = useMemo(() => defaultRuntimePolicyId(props.runtimePolicySets), [props.runtimePolicySets]);
const systemModelCount = useMemo(() => props.baseModels.filter(isSystemBaseModel).length, [props.baseModels]);
const customizedSystemModelCount = useMemo(
() => props.baseModels.filter((item) => isSystemBaseModel(item) && item.customizedAt).length,
[props.baseModels],
);
const filteredModels = useMemo(() => {
const keyword = query.trim().toLowerCase();
return props.baseModels.filter((item) => {
const matchesProvider = providerFilter === 'all' || item.providerKey === providerFilter;
const matchesType = typeFilter === 'all' || readModelTypes(item).includes(typeFilter);
const text = `${item.displayName} ${item.providerModelName} ${item.canonicalModelKey}`.toLowerCase();
const text = `${baseModelAlias(item)} ${item.providerModelName} ${item.canonicalModelKey}`.toLowerCase();
return matchesProvider && matchesType && (!keyword || text.includes(keyword));
});
}, [props.baseModels, providerFilter, query, typeFilter]);
@@ -107,34 +125,63 @@ export function BaseModelCatalogPanel(props: {
function openCreateDialog() {
const providerKey = providerOptions[0] ?? '';
setEditingId('');
setForm({ ...emptyForm, providerKey, canonicalModelKey: providerKey ? `${providerKey}:` : '' });
setForm({
...createEmptyForm(),
providerKey,
canonicalModelKey: providerKey ? `${providerKey}:` : '',
pricingRuleSetId: defaultPricingRuleSetId,
runtimePolicySetId: defaultRuntimePolicySetId,
});
setDialogOpen(true);
}
function editModel(model: BaseModelCatalogItem) {
setEditingId(model.id);
setForm(modelToForm(model));
setForm({
...modelToForm(model),
pricingRuleSetId: model.pricingRuleSetId || defaultPricingRuleSetId,
runtimePolicySetId: model.runtimePolicySetId || defaultRuntimePolicySetId,
});
setDialogOpen(true);
}
function closeDialog() {
setEditingId('');
setForm(emptyForm);
setForm(createEmptyForm());
setLocalError('');
setDialogOpen(false);
}
async function deleteModel(model: BaseModelCatalogItem) {
const confirmed = window.confirm(`确认删除基准模型 ${model.displayName}`);
if (!confirmed) return;
try {
await props.onDeleteBaseModel(model.id);
setPendingDeleteModel(null);
if (editingId === model.id) closeDialog();
} catch (err) {
setLocalError(err instanceof Error ? err.message : '模型删除失败');
}
}
async function resetModel(model: BaseModelCatalogItem) {
try {
await props.onResetBaseModel(model.id);
setPendingResetModel(null);
if (editingId === model.id) closeDialog();
} catch (err) {
setLocalError(err instanceof Error ? err.message : '模型重置失败');
}
}
async function resetAllModels() {
try {
await props.onResetAllBaseModels();
setPendingResetAll(false);
if (editingId) closeDialog();
} catch (err) {
setLocalError(err instanceof Error ? err.message : '模型批量重置失败');
}
}
return (
<div className="pageStack">
<Card>
@@ -145,10 +192,16 @@ export function BaseModelCatalogPanel(props: {
<CardContent>
<div className="providerToolbar">
<p> server-main integration-platform</p>
<Button type="button" onClick={openCreateDialog}>
<Plus size={15} />
</Button>
<div className="inlineActions">
<Button type="button" variant="outline" disabled={!systemModelCount || props.state === 'loading'} onClick={() => setPendingResetAll(true)}>
<RotateCcw size={15} />
</Button>
<Button type="button" onClick={openCreateDialog}>
<Plus size={15} />
</Button>
</div>
</div>
<div className="modelCatalogFilters">
<Input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="搜索模型名称 / canonical key" />
@@ -170,9 +223,12 @@ export function BaseModelCatalogPanel(props: {
<ModelCard
key={model.id}
model={model}
pricingRuleSetName={pricingRuleSetName(model.pricingRuleSetId, props.pricingRuleSets)}
providerName={providerNames.get(model.providerKey)}
onDelete={() => void deleteModel(model)}
runtimePolicySetName={runtimePolicySetName(model.runtimePolicySetId, props.runtimePolicySets)}
onDelete={() => setPendingDeleteModel(model)}
onEdit={() => editModel(model)}
onReset={() => setPendingResetModel(model)}
/>
))}
{!filteredModels.length && (
@@ -206,86 +262,130 @@ export function BaseModelCatalogPanel(props: {
onClose={closeDialog}
onSubmit={submit}
>
<Label>
Provider
<FormItem label="Provider">
<Select value={form.providerKey} onChange={(event) => setForm({ ...form, providerKey: event.target.value })}>
<option value=""></option>
{providerOptions.map((item) => <option value={item} key={item}>{providerNames.get(item) ?? item}</option>)}
</Select>
</Label>
<Label>
<Select value={form.modelType} onChange={(event) => setForm({ ...form, modelType: event.target.value })}>
{typeOptions.map((item) => <option value={item} key={item}>{item}</option>)}
</Select>
</Label>
<Label>
</FormItem>
<FormItem label="模型名">
<Input value={form.providerModelName} onChange={(event) => setForm({ ...form, providerModelName: event.target.value })} />
</Label>
<Label>
<Input value={form.displayName} onChange={(event) => setForm({ ...form, displayName: event.target.value })} />
</Label>
<Label className="spanTwo">
Canonical Key
</FormItem>
<FormItem
description="作为调用和展示的模型名;多个不同平台模型设置同一别名后,可按该别名自动负载。"
label="模型别名"
>
<Input value={form.modelAlias} onChange={(event) => setForm({ ...form, modelAlias: event.target.value })} />
</FormItem>
<FormItem className="spanTwo" label="Canonical Key">
<Input value={form.canonicalModelKey} onChange={(event) => setForm({ ...form, canonicalModelKey: event.target.value })} />
</Label>
<Label>
</FormItem>
<FormItem label="状态">
<Select value={form.status} onChange={(event) => setForm({ ...form, status: event.target.value })}>
{statuses.map((item) => <option value={item} key={item}>{item}</option>)}
</Select>
</Label>
<Label>
</FormItem>
<FormItem label="计价规则">
<Select value={form.pricingRuleSetId || defaultPricingRuleSetId} onChange={(event) => setForm({ ...form, pricingRuleSetId: event.target.value })}>
{!props.pricingRuleSets.length && <option value=""></option>}
{props.pricingRuleSets.map((item) => <option value={item.id} key={item.id}>{item.name}</option>)}
</Select>
</FormItem>
<FormItem label="运行策略">
<Select value={form.runtimePolicySetId || defaultRuntimePolicySetId} onChange={(event) => setForm({ ...form, runtimePolicySetId: event.target.value })}>
{!props.runtimePolicySets.length && <option value=""></option>}
{props.runtimePolicySets.map((item) => <option value={item.id} key={item.id}>{item.name}</option>)}
</Select>
</FormItem>
<FormItem label="定价版本">
<Input value={form.pricingVersion} onChange={(event) => setForm({ ...form, pricingVersion: event.target.value })} />
</Label>
<JsonField label="能力 JSON" value={form.capabilitiesJson} onChange={(value) => setForm({ ...form, capabilitiesJson: value })} />
</FormItem>
<BaseModelCapabilityEditor
modelType={primaryCapabilityType(form.capabilities)}
typeOptions={typeOptions}
value={form.capabilities}
onChange={(capabilities) => setForm({ ...form, capabilities })}
/>
<JsonField label="基准计费 JSON" value={form.billingJson} onChange={(value) => setForm({ ...form, billingJson: value })} />
<JsonField label="限流 JSON" value={form.rateLimitJson} onChange={(value) => setForm({ ...form, rateLimitJson: value })} />
<JsonField label="旧版限流 JSON(兼容字段)" value={form.rateLimitJson} onChange={(value) => setForm({ ...form, rateLimitJson: value })} />
<JsonField label="运行策略覆盖 JSON" value={form.runtimePolicyOverrideJson} onChange={(value) => setForm({ ...form, runtimePolicyOverrideJson: value })} />
<JsonField label="元数据 JSON" value={form.metadataJson} onChange={(value) => setForm({ ...form, metadataJson: value })} />
</FormDialog>
<ConfirmDialog
confirmLabel="删除模型"
description="删除后该基准模型不可恢复,已关联的平台模型可能无法继续跟随基准配置。"
loading={props.state === 'loading'}
open={Boolean(pendingDeleteModel)}
title={`确认删除基准模型 ${baseModelAlias(pendingDeleteModel)}`}
onCancel={() => setPendingDeleteModel(null)}
onConfirm={() => pendingDeleteModel ? deleteModel(pendingDeleteModel) : undefined}
/>
<ConfirmDialog
confirmLabel="重置为默认"
description="系统内置模型会恢复到默认模型库快照,当前手动修改的能力、计价、运行策略和元数据会被默认值覆盖。"
loading={props.state === 'loading'}
open={Boolean(pendingResetModel)}
title={`确认重置 ${baseModelAlias(pendingResetModel)}`}
onCancel={() => setPendingResetModel(null)}
onConfirm={() => pendingResetModel ? resetModel(pendingResetModel) : undefined}
/>
<ConfirmDialog
confirmLabel="重置所有"
description={`将恢复 ${systemModelCount} 个系统内置基准模型的默认快照,其中 ${customizedSystemModelCount} 个显示为已修改。用户添加的模型不会被影响。`}
loading={props.state === 'loading'}
open={pendingResetAll}
title="确认重置所有系统内置模型?"
onCancel={() => setPendingResetAll(false)}
onConfirm={resetAllModels}
/>
</div>
);
}
function ModelCard(props: {
model: BaseModelCatalogItem;
pricingRuleSetName: string;
providerName?: string;
runtimePolicySetName: string;
onDelete: () => void;
onEdit: () => void;
onReset: () => void;
}) {
const types = readModelTypes(props.model);
const chips = [...types.slice(0, 5), ...(types.length > 5 ? [`+${types.length - 5}`] : [])];
const isSystem = isSystemBaseModel(props.model);
return (
<article className="baseModelCard">
<ModelIcon model={props.model} />
<div className="baseModelCardBody">
<div>
<strong>{props.model.displayName}</strong>
<span>{props.model.providerModelName}</span>
</div>
<div className="providerCatalogMeta">
<Badge variant={props.model.status === 'active' ? 'success' : 'secondary'}>{props.model.status}</Badge>
<span>{props.providerName ?? props.model.providerKey}</span>
<span>{props.model.canonicalModelKey}</span>
</div>
<div className="modelAbilityChips">
{types.slice(0, 5).map((item) => <span key={item}>{item}</span>)}
{types.length > 5 && <span>+{types.length - 5}</span>}
</div>
</div>
<div className="providerCatalogActions">
<Button type="button" variant="outline" size="sm" onClick={props.onEdit}>
<Pencil size={14} />
</Button>
<Button type="button" variant="destructive" size="sm" onClick={props.onDelete}>
<Trash2 size={14} />
</Button>
</div>
</article>
<ModelCatalogCard
actions={(
<>
{isSystem && (
<Button type="button" variant="outline" size="sm" onClick={props.onReset}>
<RotateCcw size={14} />
</Button>
)}
<Button type="button" variant="outline" size="sm" onClick={props.onEdit}>
<Pencil size={14} />
</Button>
<Button type="button" variant="destructive" size="sm" onClick={props.onDelete}>
<Trash2 size={14} />
</Button>
</>
)}
badges={[
<Badge key="status" variant={props.model.status === 'active' ? 'success' : 'secondary'}>{props.model.status}</Badge>,
<Badge key="catalog-type" variant={isSystem ? 'secondary' : 'outline'}>{baseModelCatalogTypeLabel(props.model)}</Badge>,
...(isSystem && props.model.customizedAt ? [<Badge key="customized" variant="warning"></Badge>] : []),
]}
chips={chips}
iconPath={readModelIconPath(props.model)}
iconText={props.model.providerKey}
meta={[props.providerName ?? props.model.providerKey, props.pricingRuleSetName, props.runtimePolicySetName, props.model.canonicalModelKey]}
subtitle={props.model.providerModelName}
title={baseModelAlias(props.model)}
/>
);
}
@@ -298,54 +398,115 @@ function JsonField(props: { label: string; value: string; onChange: (value: stri
);
}
function ModelIcon(props: { model: BaseModelCatalogItem }) {
const iconPath = readString(props.model.metadata?.iconPath) || readString(readRecord(props.model.metadata?.rawModel)?.icon_path);
if (iconPath) {
return (
<div className="providerCatalogLogo">
<img src={iconPath} alt="" />
</div>
);
}
return <div className="providerCatalogLogo">{props.model.providerKey.slice(0, 2).toUpperCase()}</div>;
}
function modelToForm(model: BaseModelCatalogItem): ModelForm {
const capabilities = readObject(model.capabilities);
const metadataTypes = Array.isArray(model.metadata?.originalTypes) ? { originalTypes: model.metadata.originalTypes } : {};
const modelTypes = readModelTypes(model);
const capabilityState = capabilitiesToForm({ ...capabilities, ...metadataTypes }, modelTypes[0] ?? 'text_generate');
return {
providerKey: model.providerKey,
canonicalModelKey: model.canonicalModelKey,
providerModelName: model.providerModelName,
modelType: model.modelType,
displayName: model.displayName,
modelAlias: baseModelAlias(model),
status: model.status,
pricingRuleSetId: model.pricingRuleSetId ?? '',
runtimePolicySetId: model.runtimePolicySetId ?? '',
pricingVersion: String(model.pricingVersion || 1),
capabilitiesJson: stringifyJson(model.capabilities),
capabilities: capabilityState,
billingJson: stringifyJson(model.baseBillingConfig),
rateLimitJson: stringifyJson(model.defaultRateLimitPolicy),
runtimePolicyOverrideJson: stringifyJson(model.runtimePolicyOverride),
metadataJson: stringifyJson(model.metadata),
};
}
function readModelIconPath(model: BaseModelCatalogItem) {
return readString(model.metadata?.iconPath) || readString(readRecord(model.metadata?.rawModel)?.icon_path);
}
function baseModelAlias(model?: BaseModelCatalogItem | null) {
return model?.modelAlias || model?.displayName || model?.providerModelName || '';
}
function primaryCapabilityType(capabilities: CapabilityEditorState) {
return capabilities.types[0] ?? 'text_generate';
}
function isSystemBaseModel(model: BaseModelCatalogItem) {
if (model.catalogType) return model.catalogType === 'system';
return model.metadata?.source === 'server-main.integration-platform' || Boolean(model.metadata?.rawModel || model.metadata?.sourceProviderCode);
}
function baseModelCatalogTypeLabel(model: BaseModelCatalogItem) {
return isSystemBaseModel(model) ? '系统内置' : '用户添加';
}
function formToPayload(form: ModelForm): BaseModelUpsertRequest {
const modelTypes = uniqueStrings(form.capabilities.types).filter(Boolean);
const primaryType = modelTypes[0] ?? 'text_generate';
const capabilities = syncCapabilityTypes(form.capabilities, modelTypes);
const modelAlias = form.modelAlias.trim() || form.providerModelName.trim();
const metadata = parseJsonObject(form.metadataJson, '元数据 JSON');
return {
providerKey: form.providerKey.trim(),
canonicalModelKey: form.canonicalModelKey.trim() || undefined,
providerModelName: form.providerModelName.trim(),
modelType: form.modelType.trim(),
displayName: form.displayName.trim() || form.providerModelName.trim(),
capabilities: parseJsonObject(form.capabilitiesJson, '能力 JSON'),
modelType: modelTypes.length ? modelTypes : [primaryType],
modelAlias,
capabilities: capabilitiesFromForm(syncPrimaryCapability(capabilities, primaryType)),
baseBillingConfig: parseJsonObject(form.billingJson, '基准计费 JSON'),
defaultRateLimitPolicy: parseJsonObject(form.rateLimitJson, '限流 JSON'),
metadata: parseJsonObject(form.metadataJson, '元数据 JSON'),
pricingRuleSetId: form.pricingRuleSetId.trim() || undefined,
runtimePolicySetId: form.runtimePolicySetId.trim() || undefined,
runtimePolicyOverride: parseJsonObject(form.runtimePolicyOverrideJson, '运行策略覆盖 JSON'),
metadata: { ...metadata, alias: modelAlias, originalTypes: modelTypes },
pricingVersion: Number.parseInt(form.pricingVersion, 10) || 1,
status: form.status,
};
}
function uniqueStrings(values: string[]) {
return Array.from(new Set(values.map((item) => item.trim()).filter(Boolean)));
}
function syncCapabilityTypes(state: CapabilityEditorState, modelTypes: string[]): CapabilityEditorState {
const nextTypes = uniqueStrings(modelTypes).filter(Boolean);
if (!nextTypes.length) return state;
let next = state;
nextTypes.forEach((type) => {
next = addCapabilityType(next, type);
});
next.types
.filter((type) => !nextTypes.includes(type))
.forEach((type) => {
next = removeCapabilityType(next, type);
});
return { ...next, types: nextTypes };
}
function readModelTypes(model: BaseModelCatalogItem) {
if (Array.isArray(model.modelType)) return model.modelType.map(String);
const values = model.metadata?.originalTypes ?? model.capabilities?.originalTypes;
if (Array.isArray(values)) return values.map(String);
return [model.modelType].filter(Boolean);
return [];
}
function defaultRuleSetId(ruleSets: PricingRuleSet[]) {
return ruleSets.find((item) => item.ruleSetKey === 'default-multimodal-v1')?.id ?? ruleSets[0]?.id ?? '';
}
function defaultRuntimePolicyId(policySets: RuntimePolicySet[]) {
return policySets.find((item) => item.policyKey === 'default-runtime-v1')?.id ?? policySets[0]?.id ?? '';
}
function pricingRuleSetName(ruleSetId: string | undefined, ruleSets: PricingRuleSet[]) {
if (!ruleSetId) return defaultRuleSetId(ruleSets) ? '默认计价规则' : '未绑定计价规则';
return ruleSets.find((item) => item.id === ruleSetId)?.name ?? '未知计价规则';
}
function runtimePolicySetName(policySetId: string | undefined, policySets: RuntimePolicySet[]) {
if (!policySetId) return defaultRuntimePolicyId(policySets) ? '默认运行策略' : '未绑定运行策略';
return policySets.find((item) => item.id === policySetId)?.name ?? '未知运行策略';
}
function parseJsonObject(value: string, label: string) {
@@ -361,10 +522,14 @@ function parseJsonObject(value: string, label: string) {
}
}
function stringifyJson(value?: Record<string, unknown>) {
function stringifyJson(value?: object) {
return JSON.stringify(value ?? {}, null, 2);
}
function readObject(value: unknown) {
return value && !Array.isArray(value) && typeof value === 'object' ? value as Record<string, unknown> : {};
}
function readString(value: unknown) {
return typeof value === 'string' ? value : '';
}