540 lines
23 KiB
TypeScript
540 lines
23 KiB
TypeScript
import { useMemo, useState, type FormEvent } from 'react';
|
||
import { Pencil, Plus, RotateCcw, Trash2 } from 'lucide-react';
|
||
import type {
|
||
BaseModelCatalogItem,
|
||
BaseModelUpsertRequest,
|
||
CatalogProvider,
|
||
PricingRuleSet,
|
||
RuntimePolicySet,
|
||
} from '@easyai-ai-gateway/contracts';
|
||
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;
|
||
modelAlias: string;
|
||
status: string;
|
||
pricingRuleSetId: string;
|
||
runtimePolicySetId: string;
|
||
runtimePolicyOverrideJson: string;
|
||
pricingVersion: string;
|
||
capabilities: CapabilityEditorState;
|
||
billingJson: string;
|
||
rateLimitJson: string;
|
||
metadataJson: string;
|
||
};
|
||
|
||
const statuses = ['active', 'deprecated', 'hidden'];
|
||
const fallbackTypes = platformModelTypeDefinitions.map((item) => item.key);
|
||
|
||
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>(() => 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.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 = `${baseModelAlias(item)} ${item.providerModelName} ${item.canonicalModelKey}`.toLowerCase();
|
||
return matchesProvider && matchesType && (!keyword || text.includes(keyword));
|
||
});
|
||
}, [props.baseModels, providerFilter, query, typeFilter]);
|
||
|
||
async function submit(event: FormEvent<HTMLFormElement>) {
|
||
event.preventDefault();
|
||
setLocalError('');
|
||
try {
|
||
await props.onSaveBaseModel(formToPayload(form), editingId || undefined);
|
||
closeDialog();
|
||
} catch (err) {
|
||
setLocalError(err instanceof Error ? err.message : '模型保存失败');
|
||
}
|
||
}
|
||
|
||
function openCreateDialog() {
|
||
const providerKey = providerOptions[0] ?? '';
|
||
setEditingId('');
|
||
setForm({
|
||
...createEmptyForm(),
|
||
providerKey,
|
||
canonicalModelKey: providerKey ? `${providerKey}:` : '',
|
||
pricingRuleSetId: defaultPricingRuleSetId,
|
||
runtimePolicySetId: defaultRuntimePolicySetId,
|
||
});
|
||
setDialogOpen(true);
|
||
}
|
||
|
||
function editModel(model: BaseModelCatalogItem) {
|
||
setEditingId(model.id);
|
||
setForm({
|
||
...modelToForm(model),
|
||
pricingRuleSetId: model.pricingRuleSetId || defaultPricingRuleSetId,
|
||
runtimePolicySetId: model.runtimePolicySetId || defaultRuntimePolicySetId,
|
||
});
|
||
setDialogOpen(true);
|
||
}
|
||
|
||
function closeDialog() {
|
||
setEditingId('');
|
||
setForm(createEmptyForm());
|
||
setLocalError('');
|
||
setDialogOpen(false);
|
||
}
|
||
|
||
async function deleteModel(model: BaseModelCatalogItem) {
|
||
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>
|
||
<CardHeader>
|
||
<CardTitle>基准模型库</CardTitle>
|
||
<Badge variant="secondary">{props.baseModels.length}</Badge>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="providerToolbar">
|
||
<p>默认模型来自原 server-main integration-platform,保留模型类型、能力、图标、计费和限流元数据。</p>
|
||
<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" />
|
||
<Select value={providerFilter} onChange={(event) => setProviderFilter(event.target.value)}>
|
||
<option value="all">全部厂商</option>
|
||
{providerOptions.map((item) => <option value={item} key={item}>{providerNames.get(item) ?? item}</option>)}
|
||
</Select>
|
||
<Select value={typeFilter} onChange={(event) => setTypeFilter(event.target.value)}>
|
||
<option value="all">全部能力</option>
|
||
{typeOptions.map((item) => <option value={item} key={item}>{item}</option>)}
|
||
</Select>
|
||
</div>
|
||
{(props.message || localError) && <p className="formMessage">{localError || props.message}</p>}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<section className="baseModelGrid">
|
||
{filteredModels.map((model) => (
|
||
<ModelCard
|
||
key={model.id}
|
||
model={model}
|
||
pricingRuleSetName={pricingRuleSetName(model.pricingRuleSetId, props.pricingRuleSets)}
|
||
providerName={providerNames.get(model.providerKey)}
|
||
runtimePolicySetName={runtimePolicySetName(model.runtimePolicySetId, props.runtimePolicySets)}
|
||
onDelete={() => setPendingDeleteModel(model)}
|
||
onEdit={() => editModel(model)}
|
||
onReset={() => setPendingResetModel(model)}
|
||
/>
|
||
))}
|
||
{!filteredModels.length && (
|
||
<Card>
|
||
<CardContent className="emptyState">
|
||
<strong>没有匹配的基准模型</strong>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
</section>
|
||
|
||
<FormDialog
|
||
ariaLabel={editingId ? '编辑基准模型' : '新增基准模型'}
|
||
className="baseModelDialog"
|
||
eyebrow={editingId ? 'Edit Base Model' : 'New Base Model'}
|
||
footer={(
|
||
<>
|
||
<Button type="submit" disabled={props.state === 'loading'}>
|
||
{editingId ? <Pencil size={15} /> : <Plus size={15} />}
|
||
{editingId ? '保存修改' : '新增模型'}
|
||
</Button>
|
||
<Button type="button" variant="outline" onClick={closeDialog}>
|
||
<RotateCcw size={15} />
|
||
取消
|
||
</Button>
|
||
</>
|
||
)}
|
||
formClassName="baseModelForm"
|
||
open={dialogOpen}
|
||
title={editingId ? '编辑基准模型' : '新增基准模型'}
|
||
onClose={closeDialog}
|
||
onSubmit={submit}
|
||
>
|
||
<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>
|
||
</FormItem>
|
||
<FormItem label="模型名">
|
||
<Input value={form.providerModelName} onChange={(event) => setForm({ ...form, providerModelName: event.target.value })} />
|
||
</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 })} />
|
||
</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>
|
||
</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 })} />
|
||
</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.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 (
|
||
<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)}
|
||
/>
|
||
);
|
||
}
|
||
|
||
function JsonField(props: { label: string; value: string; onChange: (value: string) => void }) {
|
||
return (
|
||
<Label className="spanTwo">
|
||
{props.label}
|
||
<Textarea value={props.value} onChange={(event) => props.onChange(event.target.value)} rows={5} spellCheck={false} />
|
||
</Label>
|
||
);
|
||
}
|
||
|
||
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,
|
||
modelAlias: baseModelAlias(model),
|
||
status: model.status,
|
||
pricingRuleSetId: model.pricingRuleSetId ?? '',
|
||
runtimePolicySetId: model.runtimePolicySetId ?? '',
|
||
pricingVersion: String(model.pricingVersion || 1),
|
||
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: modelTypes.length ? modelTypes : [primaryType],
|
||
modelAlias,
|
||
capabilities: capabilitiesFromForm(syncPrimaryCapability(capabilities, primaryType)),
|
||
baseBillingConfig: parseJsonObject(form.billingJson, '基准计费 JSON'),
|
||
defaultRateLimitPolicy: parseJsonObject(form.rateLimitJson, '限流 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 [];
|
||
}
|
||
|
||
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) {
|
||
try {
|
||
const parsed = value.trim() ? JSON.parse(value) : {};
|
||
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') {
|
||
throw new Error(`${label} 必须是 JSON object`);
|
||
}
|
||
return parsed as Record<string, unknown>;
|
||
} catch (err) {
|
||
if (err instanceof Error && err.message.includes('必须是')) throw err;
|
||
throw new Error(`${label} 格式不正确`);
|
||
}
|
||
}
|
||
|
||
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 : '';
|
||
}
|
||
|
||
function readRecord(value: unknown) {
|
||
return value && !Array.isArray(value) && typeof value === 'object' ? (value as Record<string, unknown>) : undefined;
|
||
}
|