fix(admin): 前台模型目录 - 兼容缺失供应线路与价格档数组
This commit is contained in:
@@ -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 })),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
48
web/test/admin-logical-models.test.ts
Normal file
48
web/test/admin-logical-models.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user