Browse Source

存储校验信息

feature/0722ui
Luckyu_js 5 days ago
parent
commit
b936ab0c11
  1. 61
      src/app/api/__tests__/popiserverModels.test.ts
  2. 116
      src/app/api/_popiserverModels.ts
  3. 62
      src/app/api/models/[modelId]/__tests__/route.test.ts
  4. 4
      src/app/api/models/[modelId]/route.ts
  5. 41
      src/store/__tests__/modelStore.test.ts
  6. 6
      src/store/modelStore.ts

61
src/app/api/__tests__/popiserverModels.test.ts

@ -10,6 +10,7 @@ import {
fetchPopiModelDetail,
fetchPopiModels,
getPopiModelExtensionFields,
getPopiModelSupportParameterDimensions,
popiModelToProviderModel,
} from "../_popiserverModels";
@ -188,6 +189,66 @@ describe("popiserver model helpers", () => {
]);
});
it("parses support parameter dimensions from JSON strings", () => {
expect(getPopiModelSupportParameterDimensions({
id: 59,
uploadImageLimit: [1, 10],
uploadVideoLimit: [],
uploadAudioLimit: [1],
supportParameterDimensions: JSON.stringify({
image: {
formats: ["png", "jpg"],
aspectRatio: { min: null, max: null },
width: { min: 300, max: 1500 },
height: { min: "300", max: "1500" },
totalPixels: { min: null, max: null },
},
video: {
formats: [""],
width: { min: null, max: null },
},
audio: {
formats: ["", "MP3"],
},
}),
})).toEqual({
image: {
formats: ["png", "jpg"],
count: { min: 1, max: 10 },
width: { min: 300, max: 1500 },
height: { min: 300, max: 1500 },
},
audio: {
formats: ["mp3"],
count: { min: 1 },
},
});
});
it("creates count-only media rules from upload limits", () => {
expect(getPopiModelSupportParameterDimensions({
id: 60,
uploadVideoLimit: [2, 4],
uploadAudioLimit: [],
supportParameterDimensions: JSON.stringify({
image: {
formats: [""],
},
}),
})).toEqual({
video: {
count: { min: 2, max: 4 },
},
});
});
it("ignores invalid support parameter dimensions JSON", () => {
expect(getPopiModelSupportParameterDimensions({
id: 59,
supportParameterDimensions: "{invalid",
})).toBeUndefined();
});
it("uses displayDimensions label/value options for model parameters", () => {
const parameters = buildPopiModelParameters({
id: 37,

116
src/app/api/_popiserverModels.ts

@ -59,6 +59,10 @@ export type PopiAIModel = {
categories?: PopiCategory[];
metadata?: unknown;
otherShowData?: unknown;
supportParameterDimensions?: unknown;
uploadImageLimit?: unknown;
uploadVideoLimit?: unknown;
uploadAudioLimit?: unknown;
};
type PopiDisplayDimensionOption = {
@ -89,6 +93,26 @@ type ProviderResult = {
export type PopiModelExtensionFields = Record<string, unknown> | unknown[];
export type NumericRange = {
min?: number | null;
max?: number | null;
};
export type MediaSupportParameterRule = {
formats?: string[];
count?: NumericRange;
aspectRatio?: NumericRange;
width?: NumericRange;
height?: NumericRange;
totalPixels?: NumericRange;
};
export type ModelSupportParameterDimensions = {
image?: MediaSupportParameterRule;
video?: MediaSupportParameterRule;
audio?: MediaSupportParameterRule;
};
export type ModelsSuccessResponse = {
success: true;
models: ProviderModel[];
@ -211,6 +235,98 @@ export function getPopiModelExtensionFields(model: PopiAIModel): PopiModelExtens
return parsePopiExtensionFields(model.otherShowData);
}
function parseSupportParameterDimensionsPayload(value: unknown): Record<string, unknown> | undefined {
if (!value) return undefined;
if (typeof value === "object" && !Array.isArray(value)) return value as Record<string, unknown>;
if (typeof value !== "string" || value.trim().length === 0) return undefined;
try {
const parsed = JSON.parse(value) as unknown;
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
? parsed as Record<string, unknown>
: undefined;
} catch {
return undefined;
}
}
function finiteNumberOrNull(value: unknown): number | null | undefined {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string" && value.trim().length > 0) {
const parsed = Number(value);
if (Number.isFinite(parsed)) return parsed;
}
return undefined;
}
function normalizeNumericRange(value: unknown): NumericRange | undefined {
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
const record = value as Record<string, unknown>;
const min = finiteNumberOrNull(record.min);
const max = finiteNumberOrNull(record.max);
const range: NumericRange = {};
if (min !== undefined) range.min = min;
if (max !== undefined) range.max = max;
return Object.keys(range).length > 0 ? range : undefined;
}
function normalizeFormats(value: unknown): string[] | undefined {
if (!Array.isArray(value)) return undefined;
const formats = value
.map((item) => typeof item === "string" ? item.trim().toLowerCase() : "")
.filter((item) => item.length > 0);
return formats.length > 0 ? formats : undefined;
}
function normalizeUploadLimit(value: unknown): NumericRange | undefined {
if (!Array.isArray(value)) return undefined;
const min = finiteNumberOrNull(value[0]);
const max = finiteNumberOrNull(value[1]);
const range: NumericRange = {};
if (min !== undefined) range.min = min;
if (max !== undefined) range.max = max;
return Object.keys(range).length > 0 ? range : undefined;
}
function normalizeMediaRule(value: unknown, options: { dimensions: boolean }): MediaSupportParameterRule | undefined {
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
const record = value as Record<string, unknown>;
const rule: MediaSupportParameterRule = {};
const formats = normalizeFormats(record.formats);
if (formats) rule.formats = formats;
if (options.dimensions) {
const aspectRatio = normalizeNumericRange(record.aspectRatio);
const width = normalizeNumericRange(record.width);
const height = normalizeNumericRange(record.height);
const totalPixels = normalizeNumericRange(record.totalPixels);
if (aspectRatio) rule.aspectRatio = aspectRatio;
if (width) rule.width = width;
if (height) rule.height = height;
if (totalPixels) rule.totalPixels = totalPixels;
}
return Object.keys(rule).length > 0 ? rule : undefined;
}
export function getPopiModelSupportParameterDimensions(
model: PopiAIModel
): ModelSupportParameterDimensions | undefined {
const payload = parseSupportParameterDimensionsPayload(model.supportParameterDimensions);
if (!payload) return undefined;
const result: ModelSupportParameterDimensions = {};
const image = normalizeMediaRule(payload.image, { dimensions: true });
const video = normalizeMediaRule(payload.video, { dimensions: true });
const audio = normalizeMediaRule(payload.audio, { dimensions: false });
if (image) result.image = image;
if (video) result.video = video;
if (audio) result.audio = audio;
const imageCount = normalizeUploadLimit(model.uploadImageLimit);
const videoCount = normalizeUploadLimit(model.uploadVideoLimit);
const audioCount = normalizeUploadLimit(model.uploadAudioLimit);
if (imageCount) result.image = { ...(result.image ?? {}), count: imageCount };
if (videoCount) result.video = { ...(result.video ?? {}), count: videoCount };
if (audioCount) result.audio = { ...(result.audio ?? {}), count: audioCount };
return Object.keys(result).length > 0 ? result : undefined;
}
export function popiModelToProviderModel(model: PopiAIModel): ProviderModel {
const subTypes = [...getTaskSubTypes(model)];
const primarySubType = subTypes[0];

62
src/app/api/models/[modelId]/__tests__/route.test.ts

@ -0,0 +1,62 @@
import { NextRequest } from "next/server";
import { afterEach, describe, expect, it, vi } from "vitest";
import { GET } from "../route";
import { fetchPopiModelDetail } from "@/app/api/_popiserverModels";
vi.mock("@/app/api/_auth", () => ({
requireLogin: vi.fn(() => ({ token: "login-token" })),
}));
vi.mock("@/app/api/_popiserverModels", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/app/api/_popiserverModels")>();
return {
...actual,
fetchPopiModelDetail: vi.fn(),
};
});
describe("/api/models/[modelId] route", () => {
afterEach(() => {
vi.clearAllMocks();
});
it("returns parsed support parameter dimensions from model detail", async () => {
vi.mocked(fetchPopiModelDetail).mockResolvedValue({
id: 59,
code: "doubao-seedream-4-5-251128",
name: "Seedream 4.5",
ratio: ["1:1"],
categories: [{ taskSubType: 103 }],
isSupportImages: true,
uploadImageLimit: [1, 10],
supportParameterDimensions: JSON.stringify({
image: {
formats: ["png", "jpg"],
width: { min: 300, max: 1500 },
height: { min: 300, max: 1500 },
},
video: {
formats: [""],
},
}),
});
const response = await GET(
new NextRequest("http://localhost/api/models/59"),
{ params: Promise.resolve({ modelId: "59" }) }
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
success: true,
supportParameterDimensions: {
image: {
formats: ["png", "jpg"],
count: { min: 1, max: 10 },
width: { min: 300, max: 1500 },
height: { min: 300, max: 1500 },
},
},
});
});
});

4
src/app/api/models/[modelId]/route.ts

@ -8,8 +8,10 @@ import {
buildPopiModelParameters,
fetchPopiModelDetail,
getPopiModelExtensionFields,
getPopiModelSupportParameterDimensions,
popiModelToProviderModel,
type PopiModelExtensionFields,
type ModelSupportParameterDimensions,
} from "@/app/api/_popiserverModels";
import { POPI_PROVIDER_ID } from "@/lib/providerMode";
@ -19,6 +21,7 @@ interface SchemaSuccessResponse {
inputs: ModelInput[];
model?: ProviderModel;
extensionFields?: PopiModelExtensionFields;
supportParameterDimensions?: ModelSupportParameterDimensions;
cached: boolean;
}
@ -56,6 +59,7 @@ export async function GET(
inputs: buildPopiModelInputs(model),
model: popiModelToProviderModel(model),
extensionFields: getPopiModelExtensionFields(model),
supportParameterDimensions: getPopiModelSupportParameterDimensions(model),
cached: false,
});
} catch (error) {

41
src/store/__tests__/modelStore.test.ts

@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useModelStore } from "@/store/modelStore";
import type { ProviderModel } from "@/lib/providers/types";
import {
@ -9,6 +9,8 @@ import {
POPI_VIDEO_SUBTYPE_SCRIPT_ANALYSIS,
} from "@/constants/generationTask";
const originalFetch = global.fetch;
function model(id: string, subType: number): ProviderModel {
return {
id,
@ -53,6 +55,11 @@ describe("modelStore", () => {
});
});
afterEach(() => {
global.fetch = originalFetch;
vi.restoreAllMocks();
});
it("groups subtype 113 models into the multi-angle category only", () => {
const regularImage = model("regular-image", 103);
const multiAngle = model("multi-angle", 113);
@ -156,4 +163,36 @@ describe("modelStore", () => {
expect(byKind.video).toEqual([]);
expect(byKind.image).toEqual([regularImage]);
});
it("stores support parameter dimensions from fetched model details", async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({
success: true,
model: model("59", 103),
parameters: [],
inputs: [],
supportParameterDimensions: {
image: {
formats: ["png", "jpg"],
count: { min: 1, max: 10 },
width: { min: 300, max: 1500 },
},
},
}),
}) as unknown as typeof fetch;
const detail = await useModelStore.getState().fetchModelDetail("59");
expect(detail?.supportParameterDimensions).toEqual({
image: {
formats: ["png", "jpg"],
count: { min: 1, max: 10 },
width: { min: 300, max: 1500 },
},
});
expect(useModelStore.getState().getModelDetail("59")?.supportParameterDimensions).toEqual(
detail?.supportParameterDimensions
);
});
});

6
src/store/modelStore.ts

@ -1,5 +1,6 @@
import { create } from "zustand";
import type { ModelInput, ModelParameter, ProviderModel, ModelCapability } from "@/lib/providers/types";
import type { ModelSupportParameterDimensions } from "@/app/api/_popiserverModels";
import {
POPI_IMAGE_SUBTYPE_INPAINTING,
POPI_IMAGE_SUBTYPE_REPAINT,
@ -58,6 +59,7 @@ export interface PopiModelDetail {
parameters: ModelParameter[];
inputs: ModelInput[];
extensionFields?: PopiModelExtensionFields;
supportParameterDimensions?: ModelSupportParameterDimensions;
loadedAt: number;
}
@ -67,6 +69,7 @@ interface ModelDetailResponse {
parameters?: ModelParameter[];
inputs?: ModelInput[];
extensionFields?: PopiModelExtensionFields;
supportParameterDimensions?: ModelSupportParameterDimensions;
error?: string;
}
@ -217,6 +220,9 @@ export const useModelStore = create<ModelStore>((set, get) => ({
parameters: Array.isArray(data.parameters) ? data.parameters : [],
inputs: Array.isArray(data.inputs) ? data.inputs : [],
...(data.extensionFields && typeof data.extensionFields === "object" ? { extensionFields: data.extensionFields } : {}),
...(data.supportParameterDimensions && typeof data.supportParameterDimensions === "object"
? { supportParameterDimensions: data.supportParameterDimensions }
: {}),
loadedAt: Date.now(),
};
set((state) => ({

Loading…
Cancel
Save