From 1e6f782ada46cb19ef9860cfa5d9bc2d247568fc Mon Sep 17 00:00:00 2001 From: ddCat <2695092045@qq.com> Date: Sat, 22 Aug 2026 17:42:04 +0800 Subject: [PATCH] =?UTF-8?q?fix(admin):=20=E5=89=8D=E5=8F=B0=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E7=9B=AE=E5=BD=95=20-=20=E5=85=BC=E5=AE=B9=E7=BC=BA?= =?UTF-8?q?=E5=A4=B1=E4=BE=9B=E5=BA=94=E7=BA=BF=E8=B7=AF=E4=B8=8E=E4=BB=B7?= =?UTF-8?q?=E6=A0=BC=E6=A1=A3=E6=95=B0=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../logical-models/logical-models-page.tsx | 11 +++-- web/src/services/api/logical-models.ts | 20 +++++++- web/test/admin-logical-models.test.ts | 48 +++++++++++++++++++ 3 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 web/test/admin-logical-models.test.ts 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); + }); +});