From c631f906b1fa25de666eb7a7c6f231da4384000f Mon Sep 17 00:00:00 2001 From: 134 Date: Mon, 15 Jun 2026 20:57:12 +0800 Subject: [PATCH] fix: download canvas images from original source --- lib/canvas/photoshop-safe-download.ts | 109 ++++++++++++++++++-------- lib/image-url.ts | 43 ++++++++++ 2 files changed, 121 insertions(+), 31 deletions(-) diff --git a/lib/canvas/photoshop-safe-download.ts b/lib/canvas/photoshop-safe-download.ts index 7fad30dcb..b8c8cd45d 100644 --- a/lib/canvas/photoshop-safe-download.ts +++ b/lib/canvas/photoshop-safe-download.ts @@ -1,3 +1,5 @@ +import { extractImageProxySourceUrl, inferOriginalImageUrl } from "@/lib/image-url"; + type DownloadFileItem = { url: string; name?: string; @@ -85,6 +87,45 @@ function buildRemoteFetchHeaders(fileUrl: string, requestOrigin: string, cookie: return headers; } +function resolveHttpUrl(value: string, requestOrigin: string): string { + const raw = String(value || "").trim(); + if (!raw) return ""; + + try { + const parsed = new URL(raw, requestOrigin); + if (!/^https?:$/i.test(parsed.protocol)) return ""; + return parsed.toString(); + } catch { + return ""; + } +} + +function buildCanvasDownloadSourceCandidates(fileUrl: string, requestOrigin: string) { + const candidates: string[] = []; + const seen = new Set(); + + const add = (value: string) => { + const resolved = resolveHttpUrl(value, requestOrigin); + if (!resolved || seen.has(resolved)) return; + seen.add(resolved); + candidates.push(resolved); + }; + + const resolvedFileUrl = resolveHttpUrl(fileUrl, requestOrigin); + const proxySourceUrl = extractImageProxySourceUrl(resolvedFileUrl || fileUrl); + if (proxySourceUrl) { + add(inferOriginalImageUrl(proxySourceUrl) || proxySourceUrl); + add(proxySourceUrl); + } + + if (resolvedFileUrl) { + add(inferOriginalImageUrl(resolvedFileUrl) || resolvedFileUrl); + add(resolvedFileUrl); + } + + return candidates; +} + function getContentTypeMime(contentType: string): string { return String(contentType || "") .split(";")[0] @@ -257,41 +298,47 @@ export async function fetchCanvasDownloadAsset(params: { let lastError = ""; const timeoutMs = params.timeoutMs || DEFAULT_FETCH_TIMEOUT_MS; const retryCount = params.retryCount || DEFAULT_FETCH_RETRY_COUNT; + const candidateUrls = buildCanvasDownloadSourceCandidates( + params.file.url, + params.requestOrigin + ); - for (let attempt = 1; attempt <= retryCount; attempt += 1) { - try { - const response = await fetchWithTimeout( - params.file.url, - { - method: "GET", - cache: "no-store", - redirect: "follow", - headers: buildRemoteFetchHeaders(params.file.url, params.requestOrigin, params.cookie), - }, - timeoutMs - ); + for (const candidateUrl of candidateUrls) { + for (let attempt = 1; attempt <= retryCount; attempt += 1) { + try { + const response = await fetchWithTimeout( + candidateUrl, + { + method: "GET", + cache: "no-store", + redirect: "follow", + headers: buildRemoteFetchHeaders(candidateUrl, params.requestOrigin, params.cookie), + }, + timeoutMs + ); - if (!response.ok) { - lastError = `HTTP ${response.status}`; - if (attempt < retryCount && RETRYABLE_STATUS_CODES.has(response.status)) { - await sleep(350 * attempt); - continue; + if (!response.ok) { + lastError = `HTTP ${response.status}`; + if (attempt < retryCount && RETRYABLE_STATUS_CODES.has(response.status)) { + await sleep(350 * attempt); + continue; + } + break; } - break; - } - const bytes = Buffer.from(await response.arrayBuffer()); - return await normalizeCanvasDownloadAsset({ - item: params.file, - index: params.index, - bytes, - contentType: response.headers.get("content-type") || "", - includeSequencePrefix: params.includeSequencePrefix !== false, - }); - } catch (error) { - lastError = normalizeDownloadError(error); - if (attempt < retryCount) { - await sleep(350 * attempt); + const bytes = Buffer.from(await response.arrayBuffer()); + return await normalizeCanvasDownloadAsset({ + item: params.file, + index: params.index, + bytes, + contentType: response.headers.get("content-type") || "", + includeSequencePrefix: params.includeSequencePrefix !== false, + }); + } catch (error) { + lastError = normalizeDownloadError(error); + if (attempt < retryCount) { + await sleep(350 * attempt); + } } } } diff --git a/lib/image-url.ts b/lib/image-url.ts index 8de2cd926..3dd21dedd 100644 --- a/lib/image-url.ts +++ b/lib/image-url.ts @@ -6,6 +6,41 @@ function isHttpUrl(value: string): boolean { return /^https?:\/\//i.test(value); } +export function extractImageProxySourceUrl(url: string): string { + const raw = String(url || "").trim(); + if (!raw) return ""; + + try { + const parsed = new URL(raw, "https://image-proxy.local"); + if (parsed.pathname !== "/api/image-proxy") return ""; + const source = parsed.searchParams.get("url") || ""; + return isHttpUrl(source) ? source : ""; + } catch { + return ""; + } +} + +function stripAliyunImageProcess(url: string): string { + if (!isHttpUrl(url)) return url; + + try { + const parsed = new URL(url); + const hostname = parsed.hostname.toLowerCase(); + if ( + hostname.endsWith(".aliyuncs.com") && + hostname.includes(".oss-") && + parsed.searchParams.has("x-oss-process") + ) { + parsed.searchParams.delete("x-oss-process"); + return parsed.toString(); + } + } catch { + // keep the original URL + } + + return url; +} + export function isLikelyThumbnailUrl(url: string): boolean { const lowerUrl = url.toLowerCase(); if ( @@ -28,6 +63,14 @@ export function isLikelyThumbnailUrl(url: string): boolean { export function inferOriginalImageUrl(url: string): string { if (!isHttpUrl(url)) return url; + const proxySourceUrl = extractImageProxySourceUrl(url); + if (proxySourceUrl) { + return inferOriginalImageUrl(proxySourceUrl); + } + + const unprocessedUrl = stripAliyunImageProcess(url); + if (unprocessedUrl !== url) return unprocessedUrl; + if (url.includes("gemini-thumb-")) { return url.replace("gemini-thumb-", "gemini-orig-"); }