fix: harden image download fallbacks
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-05-08 15:01:03 +08:00
parent 2f52589469
commit 2babdd6a47
4 changed files with 122 additions and 31 deletions

View File

@ -481,6 +481,25 @@ function buildDownloadFilename(url: string): string {
return fallback;
}
function triggerBrowserDownload(
url: string,
filename?: string,
options?: { openInNewTab?: boolean }
) {
const link = document.createElement("a");
link.href = url;
if (filename) {
link.download = filename;
}
if (options?.openInNewTab) {
link.target = "_blank";
}
link.rel = "noopener noreferrer";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function getAssistantImageNodeIdentityKey(node: any): string {
if (!node || node.type !== "image") return "";
const data = ((node.data as any) || {}) as Record<string, any>;
@ -2482,22 +2501,20 @@ export function CanvasChatPanel({
}
const blob = await response.blob();
const blobUrl = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = blobUrl;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
triggerBrowserDownload(blobUrl, filename);
window.setTimeout(() => window.URL.revokeObjectURL(blobUrl), 1000);
return;
} catch {
const link = document.createElement("a");
link.href = originalUrl;
link.target = "_blank";
link.rel = "noopener noreferrer";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch (error) {
console.error("下载原图失败:", error);
try {
triggerBrowserDownload(originalUrl, filename);
toast.message("代理下载失败,已切换为原图直链下载");
return;
} catch (fallbackError) {
console.error("原图直链下载失败:", fallbackError);
}
triggerBrowserDownload(originalUrl, undefined, { openInNewTab: true });
toast.message("已打开原图,可右键另存为");
}
}, []);

View File

@ -48,6 +48,7 @@ import {
normalizeCanvasToolbarConfig,
type CanvasToolbarConfig,
} from "@/lib/canvas-toolbar-config";
import { inferOriginalImageUrl } from "@/lib/image-url";
const VIDEO_CANVAS_SYMBOL_ICON_MAP: Record<string, LucideIcon> = {
idea: Lightbulb,
@ -1170,7 +1171,7 @@ export function CanvasWorkspace({
const nodeData = node.data as any;
const rawImageUrl = String(nodeData?.src || "").trim();
if (!rawImageUrl) return null;
let imageUrl = rawImageUrl;
let imageUrl = inferOriginalImageUrl(rawImageUrl) || rawImageUrl;
if (browserOrigin && rawImageUrl.startsWith("/")) {
imageUrl = `${browserOrigin}${rawImageUrl}`;
} else if (browserOrigin && rawImageUrl.startsWith("//")) {

View File

@ -1158,6 +1158,25 @@ const sanitizeFileName = (name: string) => {
return safe || "image";
};
const triggerBrowserDownload = (
url: string,
filename?: string,
options?: { openInNewTab?: boolean }
) => {
const link = document.createElement("a");
link.href = url;
if (filename) {
link.download = filename;
}
if (options?.openInNewTab) {
link.target = "_blank";
}
link.rel = "noopener noreferrer";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const GENERIC_CANVAS_PROMPTS = new Set([
"对话生成",
"图片生成",
@ -6083,7 +6102,15 @@ export function ImageNode({
// 下载图片 - 通过 fetch 获取图片数据实现真正的下载(避免跨域问题导致跳转)
const handleDownload = async () => {
if (!data.src) return;
const originalUrl = inferOriginalImageUrl(data.src) || data.src;
const originalUrl = String(inferOriginalImageUrl(data.src) || data.src || "").trim();
if (!originalUrl) {
toast.error("下载失败,请重试");
return;
}
const urlParts = originalUrl.split(".");
const ext = urlParts[urlParts.length - 1]?.split("?")[0] || "png";
const filename = `${sanitizeFileName(displayTitle)}.${ext}`;
try {
toast.info("正在准备下载...");
@ -6102,11 +6129,7 @@ export function ImageNode({
// 创建下载链接
const link = document.createElement("a");
link.href = blobUrl;
// 从 URL 中提取文件扩展名,默认使用 png
const urlParts = originalUrl.split(".");
const ext = urlParts[urlParts.length - 1]?.split("?")[0] || "png";
link.download = `${sanitizeFileName(displayTitle)}.${ext}`;
link.download = filename;
// 触发下载
document.body.appendChild(link);
@ -6119,10 +6142,21 @@ export function ImageNode({
toast.success("下载成功");
} catch (error) {
console.error("下载失败:", error);
toast.error("下载失败,请重试");
try {
triggerBrowserDownload(originalUrl, filename);
toast.message("代理下载失败,已切换为原图直链下载");
return;
} catch (fallbackError) {
console.error("原图直链下载失败:", fallbackError);
}
// 降级方案:直接打开图片
window.open(originalUrl, "_blank", "noopener,noreferrer");
try {
triggerBrowserDownload(originalUrl, undefined, { openInNewTab: true });
toast.message("已打开原图,可直接另存为");
} catch (openError) {
console.error("打开原图失败:", openError);
toast.error("下载失败,请重试");
}
}
};

View File

@ -7,6 +7,25 @@ import { ImageAnnotator } from "./image-annotator";
import { AngleEditDialog } from "./angle-edit-dialog";
import { buildImageProxyUrl } from "@/lib/image-delivery";
function triggerBrowserDownload(
url: string,
filename?: string,
options?: { openInNewTab?: boolean }
) {
const link = document.createElement("a");
link.href = url;
if (filename) {
link.download = filename;
}
if (options?.openInNewTab) {
link.target = "_blank";
}
link.rel = "noopener noreferrer";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
export function ImagePreview({
src,
alt,
@ -184,19 +203,39 @@ export function ImagePreview({
// 下载图片函数(下载原图)
const handleDownload = async () => {
const safeOriginalUrl = String(original || "").trim();
if (!safeOriginalUrl) {
toast.error("下载失败,请重试");
return;
}
const filename = `generated-image-${Date.now()}.jpg`;
try {
const response = await fetch(original);
const response = await fetch(
buildImageProxyUrl(safeOriginalUrl, { delivery: "proxy" })
);
if (!response.ok) {
throw new Error(`下载失败(${response.status})`);
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `gemini-image-${Date.now()}.jpg`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
triggerBrowserDownload(url, filename);
window.URL.revokeObjectURL(url);
toast.success("下载成功");
} catch (error) {
console.error("下载失败:", error);
try {
triggerBrowserDownload(safeOriginalUrl, filename);
toast.message("代理下载失败,已切换为原图直链下载");
} catch (fallbackError) {
console.error("原图直链下载失败:", fallbackError);
try {
triggerBrowserDownload(safeOriginalUrl, undefined, { openInNewTab: true });
toast.message("已打开原图,可直接另存为");
} catch (openError) {
console.error("打开原图失败:", openError);
toast.error("下载失败,请重试");
}
}
}
};