fix: download canvas images from original source

This commit is contained in:
134 2026-06-15 20:57:12 +08:00
parent 71dcd5026b
commit c631f906b1
2 changed files with 121 additions and 31 deletions

View File

@ -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<string>();
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);
}
}
}
}

View File

@ -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-");
}