From dbe96db34d188243ea296bab63685357bb035fb1 Mon Sep 17 00:00:00 2001 From: WuYongJie <549727955@qq.com> Date: Thu, 5 Nov 2026 11:03:32 +0800 Subject: [PATCH] Serve model result previews through shared image bytes and archived copies --- app/api/image-proxy/route.ts | 97 +++++++++++++++-------- lib/image-delivery.ts | 16 +++- tests/routes/image-delivery-qiniu.test.ts | 53 +++++++++++++ tests/routes/legacy-oss-recovery.test.ts | 2 +- 4 files changed, 133 insertions(+), 35 deletions(-) diff --git a/app/api/image-proxy/route.ts b/app/api/image-proxy/route.ts index f630c0617..656a29344 100644 --- a/app/api/image-proxy/route.ts +++ b/app/api/image-proxy/route.ts @@ -3,6 +3,7 @@ import { isRetiredStorageUrl, normalizeLegacyAliyunStorageUrl, RETIRED_STORAGE_M import { NextRequest, NextResponse } from "next/server"; import { formatDownloadFailure, getDownloadFailure } from "@/lib/canvas/download-error"; import { createLogger } from "@/lib/observability/logger"; +import { buildImageProxyUrl, isAlibabaModelImageUrl } from "@/lib/image-delivery"; export const runtime = "nodejs"; const logger = createLogger("image.download"); @@ -242,6 +243,7 @@ function resolveDirectImageTarget( requestedQuality: number | null, requestedFormat: string | null ): URL | null { + if (isAlibabaModelImageUrl(target.toString())) return null; if (!isBrowserImageRequest(request)) { return null; } @@ -336,6 +338,25 @@ export async function GET(request: NextRequest) { ); } + if (!original && isAlibabaModelImageUrl(target.toString())) { + try { + const { getArchivedImageUrlMappings } = await import("@/lib/storage/image-archive-db"); + const mappings = await getArchivedImageUrlMappings([target.toString()]); + const archived = mappings[target.toString()]; + if (archived) { + const preview = buildImageProxyUrl(archived, { + width: width || undefined, + quality, + format: ["webp", "avif", "jpeg", "png"].includes(requestedFormat || "") ? requestedFormat as OutputFormat : undefined, + }); + if (/^https?:\/\//i.test(preview)) return redirectToDirectImage(new URL(preview)); + target = new URL(archived); + } + } catch (error) { + logger.warn("preview_archive_lookup_failed", error, { requestId }); + } + } + if (original && searchParams.get("download") === "resolve") { try { const { buildImageProxyUrl } = await import("@/lib/image-delivery"); @@ -388,7 +409,7 @@ export async function GET(request: NextRequest) { const fetchSignal = AbortSignal.any([ request.signal, - AbortSignal.timeout(original ? 60_000 : width && width <= 256 ? SMALL_PREVIEW_FETCH_TIMEOUT_MS : FETCH_TIMEOUT_MS), + AbortSignal.timeout(original ? 60_000 : isAlibabaModelImageUrl(target.toString()) || width && width <= 256 ? SMALL_PREVIEW_FETCH_TIMEOUT_MS : FETCH_TIMEOUT_MS), ]); const upstreamFailure = (error: unknown) => { if (request.signal.aborted) return new NextResponse(null, { status: 499 }); @@ -399,37 +420,51 @@ export async function GET(request: NextRequest) { { status: timedOut ? 504 : 502 }, ); }; - let upstreamResponse: Response; - try { - upstreamResponse = await fetch(target.toString(), { - signal: fetchSignal, - headers: { - Accept: original ? "*/*" : "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", - }, - }); - } catch (error) { - return upstreamFailure(error); - } - - if (!upstreamResponse.ok) { - if (original) return downloadFailure(`HTTP ${upstreamResponse.status}`); - return NextResponse.json( - { error: `Upstream responded with ${upstreamResponse.status}` }, - { status: 502 } - ); - } - - const contentType = upstreamResponse.headers.get("content-type") || "application/octet-stream"; - if (!contentType.toLowerCase().startsWith("image/")) { - if (original) return downloadFailure("Source is not an image"); - return NextResponse.json({ error: "Upstream is not an image" }, { status: 400 }); - } - + let contentType: string; let originalBuffer: Buffer; - try { - originalBuffer = Buffer.from(await upstreamResponse.arrayBuffer()); - } catch (error) { - return upstreamFailure(error); + if (!original && isAlibabaModelImageUrl(target.toString())) { + try { + const { fetchImageBytes } = await import("@/lib/storage/image-byte-cache"); + const data = await fetchImageBytes(target.toString(), { timeoutMs: SMALL_PREVIEW_FETCH_TIMEOUT_MS }); + originalBuffer = data.buffer; + contentType = data.contentType; + if (request.signal.aborted) return new NextResponse(null, { status: 499 }); + if (!contentType.toLowerCase().startsWith("image/")) { + return NextResponse.json({ error: "Upstream is not an image" }, { status: 400 }); + } + } catch (error) { + return upstreamFailure(error); + } + } else { + let upstreamResponse: Response; + try { + upstreamResponse = await fetch(target.toString(), { + signal: fetchSignal, + headers: { + Accept: original ? "*/*" : "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", + }, + }); + } catch (error) { + return upstreamFailure(error); + } + + if (!upstreamResponse.ok) { + if (original) return downloadFailure(`HTTP ${upstreamResponse.status}`); + return NextResponse.json( + { error: `Upstream responded with ${upstreamResponse.status}` }, + { status: 502 } + ); + } + contentType = upstreamResponse.headers.get("content-type") || "application/octet-stream"; + if (!contentType.toLowerCase().startsWith("image/")) { + if (original) return downloadFailure("Source is not an image"); + return NextResponse.json({ error: "Upstream is not an image" }, { status: 400 }); + } + try { + originalBuffer = Buffer.from(await upstreamResponse.arrayBuffer()); + } catch (error) { + return upstreamFailure(error); + } } if (original) { return buildImageResponse(originalBuffer, { diff --git a/lib/image-delivery.ts b/lib/image-delivery.ts index fffdfe9a2..1846b3799 100644 --- a/lib/image-delivery.ts +++ b/lib/image-delivery.ts @@ -6,8 +6,9 @@ const IMAGE_PROXY_PATH = "/api/image-proxy"; export function buildImageFallbackUrl(value: string | null | undefined): string { const source = value || ""; const legacy = normalizeLegacyAliyunStorageUrl(source); - if (!legacy) return source; - const proxy = buildImageProxyUrl(legacy); + const modelImage = isAlibabaModelImageUrl(source); + if (!legacy && !modelImage) return source; + const proxy = buildImageProxyUrl(legacy || source, modelImage ? { width: 960, quality: 86, format: "webp" } : {}); return typeof window === "undefined" ? proxy : new URL(proxy, window.location.origin).toString(); } @@ -90,6 +91,15 @@ function isAliyunOssImageHost(hostname: string): boolean { return normalized.endsWith(".aliyuncs.com") && normalized.includes(".oss-"); } +/** Model result buckets allow downloading objects but deny OSS image processing. */ +export function isAlibabaModelImageUrl(value: string): boolean { + try { + return /^dashscope(?:-[a-z0-9-]+)?\.oss-[a-z0-9-]+\.aliyuncs\.com$/i.test(new URL(value).hostname); + } catch { + return false; + } +} + function isTrustedDirectStorageImageHost(hostname: string): boolean { const normalized = hostname.trim().toLowerCase(); let configuredCdnHost = ""; @@ -231,7 +241,7 @@ export function buildImageProxyUrl(url: string, options: ImageProxyOptions = {}) return trustedDirectStorageUrl; } - if (options.delivery !== "proxy" && !normalizeLegacyAliyunStorageUrl(source)) { + if (options.delivery !== "proxy" && !normalizeLegacyAliyunStorageUrl(source) && !isAlibabaModelImageUrl(source)) { const directDisplayUrl = resolveAliyunOssDisplayUrl(source, options); if (directDisplayUrl) { return directDisplayUrl; diff --git a/tests/routes/image-delivery-qiniu.test.ts b/tests/routes/image-delivery-qiniu.test.ts index 5e9268db6..ed02cef4e 100644 --- a/tests/routes/image-delivery-qiniu.test.ts +++ b/tests/routes/image-delivery-qiniu.test.ts @@ -3,8 +3,61 @@ import { NextRequest } from "next/server"; import sharp from "sharp"; import { GET } from "@/app/api/image-proxy/route"; import { buildImageProxyUrl, getFullscreenPreviewSize, getImageDownloadExtension } from "@/lib/image-delivery"; +import { serverLoader } from "../helpers/canvas-download-server-loader"; +import { NextResponse } from "next/server"; test.describe("Qiniu image delivery", () => { + test("model result buckets use our thumbnail endpoint without forbidden OSS transforms", () => { + const source = "https://dashscope-a717.oss-accelerate.aliyuncs.com/output.png"; + const preview = new URL(buildImageProxyUrl(source, { width: 960, format: "webp" }), "https://app.example"); + expect(preview.pathname).toBe("/api/image-proxy"); + expect(preview.searchParams.get("url")).toBe(source); + expect(preview.searchParams.has("x-oss-process")).toBe(false); + expect(buildImageProxyUrl("https://other-bucket.oss-cn-shanghai.aliyuncs.com/image.png", { width: 960 })).toContain("x-oss-process="); + }); + + test("a completed archive serves old model-result references from Qiniu immediately", async () => { + const source = "https://dashscope-a717.oss-accelerate.aliyuncs.com/archived.png"; + const route = serverLoader({ + "next/server": { NextResponse }, + "@/lib/storage/image-archive-db": { getArchivedImageUrlMappings: async () => ({ [source]: "https://yybb.top/archived.png" }) }, + fetch: () => { throw new Error("Must use the completed archive"); }, + })("app/api/image-proxy/route.ts"); + const request = new URL("https://app.example/api/image-proxy"); + request.searchParams.set("url", source); request.searchParams.set("w", "960"); request.searchParams.set("f", "webp"); + const response = await route.GET(new NextRequest(request, { headers: { "sec-fetch-dest": "image" } })); + expect(response.status).toBe(307); + expect(response.headers.get("location")).toBe("https://yybb.top/archived.png?imageView2/2/w/960/format/webp/q/82"); + }); + + test("unarchived model previews share one original fetch with archiving and return resized WebP", async () => { + const source = `https://dashscope-a717.oss-accelerate.aliyuncs.com/${crypto.randomUUID()}.png`; + const original = await sharp({ create: { width: 1024, height: 1024, channels: 3, background: "#345678" } }).png().toBuffer(); + let requests = 0; + const load = serverLoader({ + "next/server": { NextResponse }, + "@/lib/storage/image-archive-db": { getArchivedImageUrlMappings: async () => ({}) }, + fetch: async (url: string) => { + expect(url).toBe(source); requests++; + await new Promise(resolve => setTimeout(resolve, 30)); + return new Response(original, { headers: { "content-type": "image/png" } }); + }, + }); + const route = load("app/api/image-proxy/route.ts"); + const archiveRead = load("lib/storage/image-byte-cache.ts").fetchImageBytes(source); + const responses = await Promise.all([100, 200].map(width => { + const request = new URL("https://app.example/api/image-proxy"); + request.searchParams.set("url", source); request.searchParams.set("w", String(width)); request.searchParams.set("f", "webp"); + return route.GET(new NextRequest(request, { headers: { "sec-fetch-dest": "image" } })); + })); + expect((await archiveRead).buffer.equals(original)).toBe(true); + expect(requests).toBe(1); + for (let i = 0; i < responses.length; i++) { + expect(responses[i].status).toBe(200); + expect(responses[i].headers.get("content-type")).toBe("image/webp"); + expect((await sharp(Buffer.from(await responses[i].arrayBuffer())).metadata()).width).toBe([100, 200][i]); + } + }); test("sizes a fullscreen square to its visible height and reuses the chat preview on standard density screens", () => { const size = getFullscreenPreviewSize({ imageWidth: 2048, imageHeight: 2048, viewportWidth: 1657, viewportHeight: 884 }); expect(size.displayWidth).toBeCloseTo(839.8); diff --git a/tests/routes/legacy-oss-recovery.test.ts b/tests/routes/legacy-oss-recovery.test.ts index caf6398b1..976041c8b 100644 --- a/tests/routes/legacy-oss-recovery.test.ts +++ b/tests/routes/legacy-oss-recovery.test.ts @@ -38,7 +38,7 @@ test("legacy display URLs go through archive resolution while other Alibaba buck const url = buildImageProxyUrl(legacy, { width: 400 }); expect(url).toMatch(/^\/api\/image-proxy\?/); expect(new URL(url, "https://app.example").searchParams.get("url")).toBe(legacy); - expect(buildImageProxyUrl("https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/result.png", { width: 400 })).toContain("x-oss-process="); + expect(buildImageProxyUrl("https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/result.png", { width: 400 })).toMatch(/^\/api\/image-proxy\?/); }); test("browser image fallbacks remain on one stable archive URL instead of retrying the old source", () => { -- 2.47.2