fix(admin): repair model discovery and simplify model setup
Staging Backend Quality Gate / validate (push) Successful in 14m16s
Staging Backend Quality Gate / publish (push) Successful in 1h2m51s

This commit is contained in:
Jesse_Chen
2026-08-08 17:29:17 +08:00
parent f1a4db8a90
commit fc47939ba9
9 changed files with 143 additions and 89 deletions
@@ -0,0 +1,8 @@
export async function createDiscoveredModelId(providerId: string, providerModel: string) {
const digest = await globalThis.crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(`${providerId}\0${providerModel}`),
);
const hex = Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
return `m-${hex.slice(0, 62)}`;
}
@@ -24,6 +24,7 @@ import {
import { useCallback, useEffect, useState } from "react";
import { adminRequestJson, type AdminIdentity } from "@/lib/admin/providers";
import { createDiscoveredModelId } from "./model-id";
import { formatAdminDate } from "./resource-table";
const { Text } = Typography;
@@ -72,10 +73,7 @@ type ProviderForm = {
apiKey?: string;
enabled: boolean;
};
type ModelForm = Omit<ModelVersion, "id" | "configId" | "version" | "providerCode" | "status" | "createdAt" | "publishedAt" | "settings"> & {
versionId?: string;
settingsJson: string;
};
type ModelForm = Pick<ModelVersion, "providerId" | "providerModel" | "creditCost" | "enabled" | "isDefault">;
type ModelsPayload = { data: ModelVersion[]; total: number; providers: Provider[] };
type DiscoveredModel = { id: string; label?: string };
@@ -106,7 +104,6 @@ export default function ModelManagement() {
const [modelForm] = Form.useForm<ModelForm>();
const [filterForm] = Form.useForm<ModelFilters>();
const selectedProviderId = Form.useWatch("providerId", modelForm);
const selectedProviderModel = Form.useWatch("providerModel", modelForm);
const [models, setModels] = useState<ModelVersion[]>([]);
const [providers, setProviders] = useState<Provider[]>([]);
const [loading, setLoading] = useState(true);
@@ -167,38 +164,19 @@ export default function ModelManagement() {
function openModel(model?: ModelVersion, providerId?: string) {
setEditingModel(model ?? null);
setDiscoveredModels([]);
setDiscoveredModels(model ? [{ id: model.providerModel, label: model.label }] : []);
modelForm.setFieldsValue(model ? {
modelId: model.modelId,
versionId: model.id,
providerId: model.providerId,
label: model.label,
description: model.description,
providerModel: model.providerModel,
modelTier: model.modelTier,
creditCost: model.creditCost,
contextWindow: model.contextWindow,
inputCostMicrousdPerMillion: model.inputCostMicrousdPerMillion,
outputCostMicrousdPerMillion: model.outputCostMicrousdPerMillion,
enabled: model.enabled,
isDefault: model.isDefault,
fallbackModelId: model.fallbackModelId,
settingsJson: JSON.stringify(model.settings, null, 2),
} : {
modelId: "",
providerId: providerId ?? providers[0]?.id,
label: "",
description: "",
providerModel: "",
modelTier: "standard",
creditCost: 1,
contextWindow: null,
inputCostMicrousdPerMillion: 0,
outputCostMicrousdPerMillion: 0,
enabled: false,
isDefault: false,
fallbackModelId: null,
settingsJson: "{}",
});
setModelOpen(true);
}
@@ -239,7 +217,7 @@ export default function ModelManagement() {
body: JSON.stringify({ providerId }),
});
setDiscoveredModels(payload.data);
if (!payload.data.length) message.info("未发现可用模型,可继续手工输入");
if (!payload.data.length) message.info("未发现可用模型,请检查供应商配置后重试");
} catch (error) {
message.error(error instanceof Error ? error.message : "获取模型列表失败");
} finally {
@@ -247,46 +225,30 @@ export default function ModelManagement() {
}
}
function selectDiscoveredModel(providerModel: string) {
const discovered = discoveredModels.find((item) => item.id === providerModel);
const values = modelForm.getFieldsValue(["modelId", "label"]);
modelForm.setFieldsValue({
providerModel,
...(!values.modelId ? {
modelId: providerModel.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "model",
} : {}),
...(!values.label ? { label: discovered?.label?.trim() || providerModel } : {}),
});
}
async function saveModel(values: ModelForm) {
setSaving(true);
try {
let settings: unknown;
try {
settings = JSON.parse(values.settingsJson);
} catch {
throw new Error("设置 JSON 格式不正确");
}
const discovered = discoveredModels.find((item) => item.id === values.providerModel);
const modelId = editingModel?.modelId ?? await createDiscoveredModelId(values.providerId, values.providerModel);
await adminRequestJson("/api/admin/models", {
method: "POST",
body: JSON.stringify({
action: "saveDraft",
modelId: values.modelId.trim(),
modelId,
versionId: editingModel?.id ?? null,
providerId: values.providerId,
label: values.label.trim(),
description: values.description?.trim() ?? "",
label: editingModel?.label ?? (discovered?.label?.trim() || values.providerModel).slice(0, 60),
providerModel: values.providerModel.trim(),
modelTier: values.modelTier,
creditCost: values.creditCost,
contextWindow: values.contextWindow ?? null,
inputCostMicrousdPerMillion: values.inputCostMicrousdPerMillion,
outputCostMicrousdPerMillion: values.outputCostMicrousdPerMillion,
enabled: values.enabled,
description: editingModel?.description ?? "",
modelTier: editingModel?.modelTier ?? "standard",
contextWindow: editingModel?.contextWindow ?? null,
inputCostMicrousdPerMillion: editingModel?.inputCostMicrousdPerMillion ?? 0,
outputCostMicrousdPerMillion: editingModel?.outputCostMicrousdPerMillion ?? 0,
isDefault: values.isDefault,
fallbackModelId: values.fallbackModelId?.trim() || null,
settings,
fallbackModelId: editingModel?.fallbackModelId ?? null,
settings: editingModel?.settings ?? {},
}),
});
message.success("模型草稿已保存");
@@ -415,39 +377,40 @@ export default function ModelManagement() {
</Form>
</Modal>
<Modal title={editingModel ? `编辑 ${editingModel.modelId}` : "新增模型草稿"} open={modelOpen} width={860} okText="保存草稿" cancelText="取消" confirmLoading={saving} onOk={() => modelForm.submit()} onCancel={() => setModelOpen(false)} destroyOnHidden>
<Modal title={editingModel ? `编辑 ${editingModel.modelId}` : "新增模型草稿"} open={modelOpen} width={720} okText="保存草稿" cancelText="取消" confirmLoading={saving} onOk={() => modelForm.submit()} onCancel={() => setModelOpen(false)} destroyOnHidden>
<Form<ModelForm> form={modelForm} layout="vertical" onFinish={saveModel}>
<Row gutter={16}>
<Col xs={24} md={8}><Form.Item name="modelId" label="模型 ID" rules={[{ required: true }, { pattern: /^[a-z0-9][a-z0-9._-]{0,63}$/ }]}><Input disabled={Boolean(editingModel)} /></Form.Item></Col>
<Col xs={24} md={8}><Form.Item name="label" label="显示名称" rules={[{ required: true }]}><Input /></Form.Item></Col>
<Col xs={24} md={8}><Form.Item label="供应商" required><Space.Compact block><Form.Item name="providerId" noStyle rules={[{ required: true, message: "请选择供应商" }]}><Select onChange={() => setDiscoveredModels([])} options={providers.map((item) => ({ value: item.id, label: `${item.name} (${item.code})` }))} /></Form.Item><Button loading={discovering} disabled={!selectedProviderId} onClick={() => void discoverModels(selectedProviderId)}></Button></Space.Compact></Form.Item></Col>
</Row>
<Form.Item label="发现的模型" extra="选择后会填入供应商模型名;模型 ID 和显示名称仅在为空时自动补全。">
<Form.Item label="供应商" required>
<Space.Compact block>
<Form.Item name="providerId" noStyle rules={[{ required: true, message: "请选择供应商" }]}>
<Select
onChange={() => {
setDiscoveredModels([]);
modelForm.setFieldValue("providerModel", "");
}}
options={providers.map((item) => ({ value: item.id, label: `${item.name} (${item.code})` }))}
/>
</Form.Item>
<Button loading={discovering} disabled={!selectedProviderId} onClick={() => void discoverModels(selectedProviderId)}></Button>
</Space.Compact>
</Form.Item>
<Form.Item name="providerModel" label="模型" rules={[{ required: true, message: "请从供应商模型列表中选择" }]} extra="获取列表后选择供应商提供的模型。">
<Select
showSearch
allowClear
optionFilterProp="label"
placeholder={selectedProviderId ? "获取模型列表,或在下方手工输入" : "请先选择供应商"}
placeholder={selectedProviderId ? "获取并选择模型" : "请先选择供应商"}
disabled={!selectedProviderId || !discoveredModels.length}
value={discoveredModels.some((item) => item.id === selectedProviderModel) ? selectedProviderModel : undefined}
options={discoveredModels.map((item) => ({ value: item.id, label: item.label ? `${item.label} (${item.id})` : item.id }))}
onSelect={selectDiscoveredModel}
/>
</Form.Item>
<Row gutter={16}>
<Col xs={24} md={12}><Form.Item name="providerModel" label="供应商模型名(可手工输入)" rules={[{ required: true }]}><Input /></Form.Item></Col>
<Col xs={24} md={6}><Form.Item name="modelTier" label="模型档位" rules={[{ required: true }]}><Select options={Object.entries(modelTierLabels).map(([value, label]) => ({ value, label }))} /></Form.Item></Col>
<Col xs={24} md={6}><Form.Item name="creditCost" label="单次点数" rules={[{ required: true }]}><InputNumber min={1} precision={0} style={{ width: "100%" }} /></Form.Item></Col>
<Col xs={24} md={12}><Form.Item name="creditCost" label="单次点数" rules={[{ required: true }]}><InputNumber min={1} precision={0} style={{ width: "100%" }} /></Form.Item></Col>
<Col xs={24} md={12}>
<Space size="large">
<Form.Item name="enabled" label="启用" valuePropName="checked"><Switch /></Form.Item>
<Form.Item name="isDefault" label="默认模型" valuePropName="checked" dependencies={["enabled"]} rules={[({ getFieldValue }) => ({ validator(_, value) { return value && !getFieldValue("enabled") ? Promise.reject(new Error("默认模型必须启用")) : Promise.resolve(); } })]}><Switch /></Form.Item>
</Space>
</Col>
</Row>
<Form.Item name="description" label="说明"><Input.TextArea rows={2} /></Form.Item>
<Row gutter={16}>
<Col xs={24} md={8}><Form.Item name="contextWindow" label="上下文窗口"><InputNumber min={1} precision={0} style={{ width: "100%" }} /></Form.Item></Col>
<Col xs={24} md={8}><Form.Item name="inputCostMicrousdPerMillion" label="输入成本(微美元/百万 Token" rules={[{ required: true }]}><InputNumber min={0} precision={0} style={{ width: "100%" }} /></Form.Item></Col>
<Col xs={24} md={8}><Form.Item name="outputCostMicrousdPerMillion" label="输出成本(微美元/百万 Token" rules={[{ required: true }]}><InputNumber min={0} precision={0} style={{ width: "100%" }} /></Form.Item></Col>
</Row>
<Form.Item name="fallbackModelId" label="回退模型 ID"><Input allowClear /></Form.Item>
<Form.Item name="settingsJson" label="设置 JSON" rules={[{ required: true }]}><Input.TextArea rows={5} spellCheck={false} /></Form.Item>
<Space size="large"><Form.Item name="enabled" label="启用" valuePropName="checked"><Switch /></Form.Item><Form.Item name="isDefault" label="默认模型" valuePropName="checked" dependencies={["enabled"]} rules={[({ getFieldValue }) => ({ validator(_, value) { return value && !getFieldValue("enabled") ? Promise.reject(new Error("默认模型必须启用")) : Promise.resolve(); } })]}><Switch /></Form.Item></Space>
</Form>
</Modal>
<Modal