diff --git a/web/src/pages/admin/logical-models/logical-models-page.tsx b/web/src/pages/admin/logical-models/logical-models-page.tsx
index 70a54701c..5079ebafe 100644
--- a/web/src/pages/admin/logical-models/logical-models-page.tsx
+++ b/web/src/pages/admin/logical-models/logical-models-page.tsx
@@ -238,8 +238,8 @@ export default function LogicalModelsPage() {
width: 110,
render: (_, item) => (
-
{item.routes.filter((route) => route.enabled && route.available).length} 条可用
-
共 {item.routes.length} 条
+
{(item.routes || []).filter((route) => route.enabled && route.available).length} 条可用
+
共 {(item.routes || []).length} 条
),
},
@@ -610,8 +610,9 @@ function PricingFields() {
}
function logicalPriceLabel(item: AdminLogicalModel) {
- if (!item.priceTiers.length) return 待配置系统规格价格;
- return {item.priceTiers.length} 个系统规格档;
+ const priceTiers = item.priceTiers || [];
+ if (!priceTiers.length) return 待配置系统规格价格;
+ return {priceTiers.length} 个系统规格档;
}
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 })),
};
}
diff --git a/web/src/services/api/logical-models.ts b/web/src/services/api/logical-models.ts
index bccb24c75..2f167c4ca 100644
--- a/web/src/services/api/logical-models.ts
+++ b/web/src/services/api/logical-models.ts
@@ -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) {
diff --git a/web/test/admin-logical-models.test.ts b/web/test/admin-logical-models.test.ts
new file mode 100644
index 000000000..8d076df9c
--- /dev/null
+++ b/web/test/admin-logical-models.test.ts
@@ -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);
+ });
+});