From d32258e13b34eea8e360de8798e4a8e585ab4d93 Mon Sep 17 00:00:00 2001 From: WuYongJie <549727955@qq.com> Date: Thu, 12 Nov 2026 15:09:33 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=A9=E5=9B=BE=E7=BB=93=E6=9E=9C=E7=9B=B4?= =?UTF-8?q?=E6=8E=A5=E5=BD=92=E6=A1=A3=E5=88=B0=E4=B8=83=E7=89=9B=E5=AD=98?= =?UTF-8?q?=E5=82=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/ai/extend/service.ts | 27 ++++++++++++---- tests/routes/extend-provider-config.test.ts | 36 +++++++++++++++++++++ 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/lib/ai/extend/service.ts b/lib/ai/extend/service.ts index cf4bd9b09..85d6a1416 100644 --- a/lib/ai/extend/service.ts +++ b/lib/ai/extend/service.ts @@ -12,6 +12,8 @@ import { expandWithVip98kOutpaint, VIP98K_OUTPAINT_MODEL } from "@/lib/ai/extend import { db } from "@/lib/db"; import { generatedImage } from "@/lib/db/schema"; import { uploadFile } from "@/lib/storage/service"; +import { archiveTemporaryImage } from "@/lib/storage/image-archive"; +import { randomUUID } from "node:crypto"; type ExtendConfig = { provider?: ExtendProvider; @@ -434,7 +436,10 @@ export async function executeExtendJob( } const startTime = Date.now(); - let buffer: Buffer; + let buffer!: Buffer; + let uploadResult: { thumbnail: string; original: string } | undefined; + let archivedBytes: number | undefined; + let downloadAsset: Awaited>["downloadAsset"]; let modelId: string; if (config.provider === "fal") { buffer = await expandWithFalOutpaint({ imageUrl: inputImageUrl, apiKey: config.apiKey!, xScale, yScale }); @@ -450,21 +455,28 @@ export async function executeExtendJob( } else { const prepared = config.provider === "vip98k" ? inputImageUrl : await normalizeDashscopeExpansionInput(inputImageUrl); const url = await extendImage(prepared, config, xScale, yScale, requestId); - const imageResponse = await fetch(url); - if (!imageResponse.ok) { - throw new ExtendServiceError("下载拓展图片失败", 500, `${imageResponse.status} ${imageResponse.statusText}`); + if (config.provider === "vip98k" && !includeBase64) { + const archived = await archiveTemporaryImage(url, requestId || randomUUID()); + uploadResult = { original: archived.url, thumbnail: archived.thumbnailUrl }; + archivedBytes = archived.bytes; + downloadAsset = archived.downloadAsset; + } else { + const imageResponse = await fetch(url); + if (!imageResponse.ok) { + throw new ExtendServiceError("下载拓展图片失败", 500, `${imageResponse.status} ${imageResponse.statusText}`); + } + buffer = Buffer.from(await imageResponse.arrayBuffer()); } - buffer = Buffer.from(await imageResponse.arrayBuffer()); modelId = config.provider === "vip98k" ? "image-expansion" : "extend-1.0"; } const imageBase64Result = includeBase64 ? buffer.toString("base64") : undefined; - const uploadResult = await uploadBufferPairToS3(buffer, config.provider === "fal" || config.provider === "vip98k" ? "png" : "jpg"); + uploadResult ??= await uploadBufferPairToS3(buffer, config.provider === "fal" || config.provider === "vip98k" ? "png" : "jpg"); console.log("[Extend Service] Result uploaded:", { requestId, includeBase64, inputImageUrl: inputImageUrl.slice(0, 120), modelId, - outputBytes: buffer.length, + outputBytes: archivedBytes ?? buffer.length, }); let mediaAssetId: string | null = null; let generatedImageId: string | null = null; @@ -513,6 +525,7 @@ export async function executeExtendJob( xScale, yScale, imageSize: "2K", + ...(downloadAsset ? { downloadAsset } : {}), }, }); mediaAssetId = media?.id || null; diff --git a/tests/routes/extend-provider-config.test.ts b/tests/routes/extend-provider-config.test.ts index 5056025ea..b361e35fc 100644 --- a/tests/routes/extend-provider-config.test.ts +++ b/tests/routes/extend-provider-config.test.ts @@ -13,6 +13,7 @@ test("native expansion sends the original image and never switches models after "@/lib/db": {}, "@/lib/db/schema": {}, "@/lib/storage/service": {}, + "@/lib/storage/image-archive": {}, "@/lib/ai/vip98k-runtime": { executeConfiguredVip98kTask: async (params: unknown) => { calls.push(params); throw new Error(message); }, }, @@ -22,6 +23,41 @@ test("native expansion sends the original image and never switches models after } }); +test("native expansion archives the completed image without transferring its bytes through the app", async () => { + const calls: string[] = []; + 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 () => ({ outputUrls: ["https://relay.test/result.png"] }) }, + "@/lib/storage/image-archive": { archiveTemporaryImage: async (url: string, id: string) => { + calls.push(url, id); + return { url: "https://cdn.test/native.png", thumbnailUrl: "https://cdn.test/native.png", bytes: 12375212 }; + } }, + })("lib/ai/extend/service.ts"); + const result = await service.executeExtendJob({ userId: "test", imageUrl: "https://source.test/original.jpg", requestId: "request-1", skipGallerySave: true }); + expect(calls).toEqual(["https://relay.test/result.png", "request-1"]); + expect(result.original_url).toBe("https://cdn.test/native.png"); + expect(result.image_base64).toBeUndefined(); +}); + +test("native expansion still returns image bytes to callers requesting base64", async () => { + const image = await sharp({ create: { width: 12, height: 8, channels: 3, background: "red" } }).png().toBuffer(); + const service = serverLoader({ + "@/lib/db/queries": { getSystemSetting: async () => ({ value: { provider: "vip98k", modelId: "image-expansion" } }) }, + "@/lib/db": {}, + "@/lib/db/schema": {}, + "@/lib/storage/service": { uploadFile: async (name: string) => ({ url: `https://cdn.test/${name}` }) }, + "@/lib/storage/image-archive": { archiveTemporaryImage: () => { throw new Error("Base64 callers need the image bytes"); } }, + "@/lib/ai/vip98k-runtime": { executeConfiguredVip98kTask: async () => ({ outputUrls: ["https://relay.test/result.png"] }) }, + fetch: async () => new Response(new Uint8Array(image)), + })("lib/ai/extend/service.ts"); + const result = await service.executeExtendJob({ userId: "test", imageUrl: "https://source.test/original.jpg", includeBase64: true, skipGallerySave: true }); + expect(result.image_base64).toBe(image.toString("base64")); + expect(result.original_url).toMatch(/\.png$/); +}); + test.describe("image extension provider configuration", () => { test("uses the most recently updated Alibaba image provider key", () => { expect( -- 2.47.2