fix(admin): 前台模型目录 - 兼容缺失供应线路与价格档数组

This commit is contained in:
ddCat
2026-08-22 17:42:04 +08:00
parent 1bbd880c84
commit 1e6f782ada
3 changed files with 73 additions and 6 deletions

View File

@@ -238,8 +238,8 @@ export default function LogicalModelsPage() {
width: 110,
render: (_, item) => (
<div className="text-xs">
<div>{item.routes.filter((route) => route.enabled && route.available).length} 条可用</div>
<div className="text-foreground/45">共 {item.routes.length} 条</div>
<div>{(item.routes || []).filter((route) => route.enabled && route.available).length} 条可用</div>
<div className="text-foreground/45">共 {(item.routes || []).length} 条</div>
</div>
),
},
@@ -610,8 +610,9 @@ function PricingFields() {
}
function logicalPriceLabel(item: AdminLogicalModel) {
if (!item.priceTiers.length) return <span className="text-xs text-foreground/45">待配置系统规格价格</span>;
return <span className="text-xs">{item.priceTiers.length} 个系统规格档</span>;
const priceTiers = item.priceTiers || [];
if (!priceTiers.length) return <span className="text-xs text-foreground/45">待配置系统规格价格</span>;
return <span className="text-xs">{priceTiers.length} 个系统规格档</span>;
}
function logicalModelToForm(item: AdminLogicalModel): LogicalModelFormValues {
@@ -631,7 +632,7 @@ function logicalModelToForm(item: AdminLogicalModel): LogicalModelFormValues {
cachedPriceMicrocredits: 0,
capabilitySpec: item.capabilitySpec,
defaultOptions: item.defaultOptions,
routes: item.routes.map((route) => ({ channelModelId: route.channelModelId, enabled: route.enabled, priority: route.priority, weight: route.weight })),
routes: (item.routes || []).map((route) => ({ channelModelId: route.channelModelId, enabled: route.enabled, priority: route.priority, weight: route.weight })),
};
}

View File

@@ -72,6 +72,22 @@ export type AdminLogicalModel = PublicLogicalModel & {
routes: AdminLogicalRoute[];
};
/**
* Keep admin model responses usable when a server is upgraded before its
* existing process or historical response shape has caught up with the
* current contract. Collection fields must always be arrays for the page.
*/
export function normalizeAdminLogicalModel(model: AdminLogicalModel): AdminLogicalModel {
return {
...model,
priceTiers: Array.isArray(model.priceTiers) ? model.priceTiers : [],
legacyModelIds: Array.isArray(model.legacyModelIds) ? model.legacyModelIds : [],
capabilityProfiles: Array.isArray(model.capabilityProfiles) ? model.capabilityProfiles : [],
defaultOptions: model.defaultOptions && typeof model.defaultOptions === "object" ? model.defaultOptions : {},
routes: Array.isArray(model.routes) ? model.routes : [],
};
}
export type LogicalModelMutation = {
code: string;
name: string;
@@ -118,7 +134,9 @@ export function quoteLogicalModel(id: string, intent: ModelRequestIntent, signal
}
export function listAdminLogicalModels() {
return request<{ models: AdminLogicalModel[] }>(apiClient.get("/admin/logical-models"));
return request<{ models?: AdminLogicalModel[] }>(apiClient.get("/admin/logical-models")).then((result) => ({
models: Array.isArray(result?.models) ? result.models.filter(Boolean).map(normalizeAdminLogicalModel) : [],
}));
}
export function createAdminLogicalModel(input: LogicalModelMutation) {

View File

@@ -0,0 +1,48 @@
import { describe, expect, test } from "bun:test";
import { normalizeAdminLogicalModel, type AdminLogicalModel } from "../src/services/api/logical-models";
function legacyModel() {
return {
id: "model-1",
code: "seedance",
name: "Seedance",
description: "",
capability: "video",
sortOrder: 0,
pricePolicy: "channel",
billingMode: "fixed_request",
unitPriceMicrocredits: 0,
inputPriceMicrocredits: 0,
outputPriceMicrocredits: 0,
cachedPriceMicrocredits: 0,
capabilitySpec: { version: 1, capability: "video" },
enabled: true,
activeRevisionId: "revision-1",
revisionVersion: 1,
available: false,
} as AdminLogicalModel;
}
describe("后台前台模型响应归一化", () => {
test("兼容旧响应中缺失的集合字段", () => {
const model = normalizeAdminLogicalModel(legacyModel());
expect(model.routes).toEqual([]);
expect(model.priceTiers).toEqual([]);
expect(model.legacyModelIds).toEqual([]);
expect(model.capabilityProfiles).toEqual([]);
expect(model.defaultOptions).toEqual({});
});
test("保留当前响应中的线路和价格档", () => {
const input = legacyModel();
input.routes = [{ id: "route-1" } as AdminLogicalModel["routes"][number]];
input.priceTiers = [{ selector: { resolution: "720p" } } as AdminLogicalModel["priceTiers"][number]];
const model = normalizeAdminLogicalModel(input);
expect(model.routes).toHaveLength(1);
expect(model.priceTiers).toHaveLength(1);
});
});