From 78b111130baa94703b48738cc0658b0e620dea04 Mon Sep 17 00:00:00 2001 From: WuYongJie <549727955@qq.com> Date: Thu, 12 Nov 2026 14:23:09 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BD=BF=E7=94=A898K=E5=8E=9F=E7=94=9F?= =?UTF-8?q?=E6=99=BA=E8=83=BD=E6=89=A9=E5=9B=BE=E5=B9=B6=E4=BF=9D=E7=95=99?= =?UTF-8?q?=E5=AE=8C=E6=95=B4=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/ai/extend/service.ts | 128 ++---------------- lib/ai/providers/vip98k.ts | 1 + tests/routes/extend-provider-config.test.ts | 18 +++ tests/routes/vip98k-reference-routing.test.ts | 14 ++ 4 files changed, 46 insertions(+), 115 deletions(-) diff --git a/lib/ai/extend/service.ts b/lib/ai/extend/service.ts index 7adb6d77c..cf4bd9b09 100644 --- a/lib/ai/extend/service.ts +++ b/lib/ai/extend/service.ts @@ -22,8 +22,7 @@ type ExtendConfig = { routeOptionId?: string; }; -const VIP98K_EXPANSION_FALLBACK_MODEL = "google/nano-banana-pro/edit"; -const VIP98K_SOURCE_DOWNLOAD_TIMEOUT_MS = 120_000; +const SOURCE_DOWNLOAD_TIMEOUT_MS = 120_000; const DASHSCOPE_MIN_IMAGE_EDGE = 512; const DASHSCOPE_MAX_IMAGE_EDGE = 4096; const DASHSCOPE_MAX_IMAGE_BYTES = 10 * 1024 * 1024; @@ -146,7 +145,7 @@ async function normalizeDashscopeExpansionInput(imageUrl: string): Promise { if (config.provider === "vip98k") { - try { - const result = await executeConfiguredVip98kTask({ - modelId: "image-expansion", - kind: "image-tool", - prompt: `Expand the image canvas naturally to ${xScale}x width and ${yScale}x height. Preserve the original subject, perspective, lighting and style.`, - imageUrls: [imageUrl], - imageParams: { xScale, yScale }, - }); - const outputUrl = result.outputUrls[0]; - if (!outputUrl) { - throw new ExtendServiceError("VIP98K 图像拓展未返回图片", 502); - } - return outputUrl; - } catch (error) { - if (!isTemporaryVip98kExpansionError(error)) throw error; - console.warn( - "[Extend Service] VIP98K expansion tool is temporarily unavailable; using VIP98K generative outpainting fallback", - error instanceof Error ? error.message : String(error), - ); - return extendImageWithVip98kEditFallback(imageUrl, xScale, yScale); + const result = await executeConfiguredVip98kTask({ + modelId: "image-expansion", + kind: "image-tool", + imageUrls: [imageUrl], + imageParams: { xScale, yScale }, + }); + const outputUrl = result.outputUrls[0]; + if (!outputUrl) { + throw new ExtendServiceError("VIP98K 图像拓展未返回图片", 502); } + return outputUrl; } if (!config.apiKey) { @@ -560,7 +458,7 @@ export async function executeExtendJob( modelId = config.provider === "vip98k" ? "image-expansion" : "extend-1.0"; } const imageBase64Result = includeBase64 ? buffer.toString("base64") : undefined; - const uploadResult = await uploadBufferPairToS3(buffer, config.provider === "fal" || modelId === VIP98K_OUTPAINT_MODEL ? "png" : "jpg"); + const uploadResult = await uploadBufferPairToS3(buffer, config.provider === "fal" || config.provider === "vip98k" ? "png" : "jpg"); console.log("[Extend Service] Result uploaded:", { requestId, includeBase64, diff --git a/lib/ai/providers/vip98k.ts b/lib/ai/providers/vip98k.ts index 00e543d3d..cb6ea31ad 100644 --- a/lib/ai/providers/vip98k.ts +++ b/lib/ai/providers/vip98k.ts @@ -1666,6 +1666,7 @@ export function buildTaskBody(params: Vip98kTaskParams, invocation: Vip98kInvoca } if (params.modelId.toLowerCase() === "image-expansion") { bindVip98kExpansionParams(body, imageParams); + body.output_format = "png"; } if (params.kind === "image-tool" && params.modelId.toLowerCase() === "ocr") { // Old synced examples include generic image fields and both language aliases. diff --git a/tests/routes/extend-provider-config.test.ts b/tests/routes/extend-provider-config.test.ts index 11824e35f..5056025ea 100644 --- a/tests/routes/extend-provider-config.test.ts +++ b/tests/routes/extend-provider-config.test.ts @@ -3,6 +3,24 @@ import sharp from "sharp"; import { resolveAlibabaExtendApiKey, resolveFalExtendApiKey } from "@/lib/ai/extend/config"; import { composeOutpaint, getOutpaintGeometry } from "@/lib/ai/extend/fal-outpaint"; import { expandWithVip98kOutpaint, getVip98kOutpaintGeometry } from "@/lib/ai/extend/vip98k-outpaint"; +import { serverLoader } from "../helpers/canvas-download-server-loader"; + +test("native expansion sends the original image and never switches models after an upstream failure", async () => { + for (const message of ["HTTP 503 unavailable", "request timed out"]) { + const calls: any[] = []; + const service = serverLoader({ + "@/lib/db/queries": { getSystemSetting: async () => ({ value: { provider: "vip98k", modelId: "image-expansion" } }) }, + "@/lib/db": {}, + "@/lib/db/schema": {}, + "@/lib/storage/service": {}, + "@/lib/ai/vip98k-runtime": { + executeConfiguredVip98kTask: async (params: unknown) => { calls.push(params); throw new Error(message); }, + }, + })("lib/ai/extend/service.ts"); + await expect(service.executeExtendJob({ userId: "test", imageUrl: "https://source.test/original.jpg", xScale: 1.5, yScale: 2 })).rejects.toThrow(message); + expect(calls).toEqual([{ modelId: "image-expansion", kind: "image-tool", imageUrls: ["https://source.test/original.jpg"], imageParams: { xScale: 1.5, yScale: 2 } }]); + } +}); test.describe("image extension provider configuration", () => { test("uses the most recently updated Alibaba image provider key", () => { diff --git a/tests/routes/vip98k-reference-routing.test.ts b/tests/routes/vip98k-reference-routing.test.ts index 47ac0d5ea..7640898d6 100644 --- a/tests/routes/vip98k-reference-routing.test.ts +++ b/tests/routes/vip98k-reference-routing.test.ts @@ -54,6 +54,20 @@ const textToImageInvocation: Vip98kTaskInvocation = { "/api/external/tools/result?tool_id=google%2Fnano-banana-2%2Ftext-to-image&taskId={task_id}", }; +test("native expansion maps final canvas scales to per-side ratios and requests PNG", () => { + const invocation: Vip98kTaskInvocation = { + kind: "task", method: "POST", path: "/extensions/v1/tasks", contentType: "application/json", + resultMethod: "GET", resultPath: "/extensions/v1/tasks/{task_id}/result", + requestExample: { model: "image-expansion", metadata: { top: 0.1, bottom: 0.1, left: 0.1, right: 0.1 }, output_format: "" }, + inputSchema: { properties: { model: { type: "string" }, file_urls: { type: "array" }, metadata: { type: "object" }, output_format: { type: "string" } } }, + }; + const { body } = buildTaskBody({ apiKey: "test", modelId: "image-expansion", kind: "image-tool", prompt: "", imageUrls: ["https://source.test/original.jpg"], videoUrls: [], modelConfig: {}, imageParams: { xScale: 1.5, yScale: 2 } }, invocation); + expect(body).toMatchObject({ model: "image-expansion", file_urls: ["https://source.test/original.jpg"], metadata: { left: 0.25, right: 0.25, top: 0.5, bottom: 0.5 }, output_format: "png" }); + expect(body).not.toHaveProperty("mask"); + expect(body).not.toHaveProperty("prompt"); + expect(body).not.toHaveProperty("x_scale"); +}); + test("text-to-image uses the advertised task contract with native size and ratio, without example reference images", () => { const configured: Vip98kTaskInvocation = { ...textToImageInvocation, path: "/v1/images/generations", requestExample: { model: "nano-banana-pro", prompt: "sample" } }; const task: Vip98kTaskInvocation = { ...configured, kind: "task", path: "/extensions/v1/tasks",