diff --git a/components/agents/AgentDetailReferenceStitchDialog.tsx b/components/agents/AgentDetailReferenceStitchDialog.tsx index 72ef8f5f1..6c925a2f1 100644 --- a/components/agents/AgentDetailReferenceStitchDialog.tsx +++ b/components/agents/AgentDetailReferenceStitchDialog.tsx @@ -72,11 +72,13 @@ async function loadImage(url: string) { width: MERGED_IMAGE_MAX_WIDTH, quality: 92, format: "png", + delivery: "proxy", }), buildImageProxyUrl(normalizedSource, { width: Math.min(MERGED_IMAGE_MAX_WIDTH, 960), quality: 86, format: "webp", + delivery: "proxy", }), normalizedSource, ].filter((item): item is string => Boolean(item)) diff --git a/components/agents/AgentPsdTemplateEditorDialog.tsx b/components/agents/AgentPsdTemplateEditorDialog.tsx index 6b5c66ca9..58a894eb2 100644 --- a/components/agents/AgentPsdTemplateEditorDialog.tsx +++ b/components/agents/AgentPsdTemplateEditorDialog.tsx @@ -2742,7 +2742,10 @@ export function AgentPsdTemplateEditorDialog({ const fetchCandidates = Array.from( new Set( - [normalizedUrl, buildImageProxyUrl(normalizedUrl)].filter(Boolean) + [ + normalizedUrl, + buildImageProxyUrl(normalizedUrl, { delivery: "proxy" }), + ].filter(Boolean) ) ); diff --git a/components/agents/agent-psd-template-export-preview.tsx b/components/agents/agent-psd-template-export-preview.tsx index 5f32c11db..4e422a887 100644 --- a/components/agents/agent-psd-template-export-preview.tsx +++ b/components/agents/agent-psd-template-export-preview.tsx @@ -1055,7 +1055,9 @@ export function AgentPsdTemplateExportPreview({ try { setIsDownloading(true); if (!shouldCompose) { - const response = await fetch(buildImageProxyUrl(imageUrl)); + const response = await fetch( + buildImageProxyUrl(imageUrl, { delivery: "proxy" }) + ); if (!response.ok) { throw new Error(`下载失败(${response.status})`); } @@ -1066,7 +1068,7 @@ export function AgentPsdTemplateExportPreview({ } const { image, dispose } = await loadImageFromCandidates([ - buildImageProxyUrl(imageUrl), + buildImageProxyUrl(imageUrl, { delivery: "proxy" }), imageUrl, ]); diff --git a/components/canvas/ObjectPickerOverlay.tsx b/components/canvas/ObjectPickerOverlay.tsx index 62351a007..654f792c2 100644 --- a/components/canvas/ObjectPickerOverlay.tsx +++ b/components/canvas/ObjectPickerOverlay.tsx @@ -29,7 +29,7 @@ async function drawMarkerOnImage( return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = 'anonymous'; - const loadUrl = buildImageProxyUrl(imageUrl); + const loadUrl = buildImageProxyUrl(imageUrl, { delivery: "proxy" }); img.onload = () => { // 创建 canvas diff --git a/components/canvas/RealtimeCanvasDialog.tsx b/components/canvas/RealtimeCanvasDialog.tsx index e363b0f1c..e8ddc734e 100644 --- a/components/canvas/RealtimeCanvasDialog.tsx +++ b/components/canvas/RealtimeCanvasDialog.tsx @@ -1113,7 +1113,7 @@ export function RealtimeCanvasDialog({ return new Promise((resolve, reject) => { const image = new Image(); const resolvedSrc = /^https?:\/\//i.test(src) - ? buildImageProxyUrl(src) + ? buildImageProxyUrl(src, { delivery: "proxy" }) : src; image.crossOrigin = "anonymous"; image.onload = () => resolve(image); diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index fccefc651..24a498c5d 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -334,7 +334,7 @@ async function drawMarkerOnImage( return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = "anonymous"; - const loadUrl = buildImageProxyUrl(imageUrl); + const loadUrl = buildImageProxyUrl(imageUrl, { delivery: "proxy" }); img.onload = () => { const canvas = document.createElement("canvas"); diff --git a/components/image-annotator.tsx b/components/image-annotator.tsx index 94322c21c..a5c740fcd 100644 --- a/components/image-annotator.tsx +++ b/components/image-annotator.tsx @@ -108,7 +108,7 @@ export function ImageAnnotator({ (submitMode === "publish-new-canvas" ? "发布到新的画布" : "发送到新对话"); const forceDarkAppearance = appearance === "dark"; const toCanvasLoadUrl = useCallback((url: string) => { - return buildImageProxyUrl(url); + return buildImageProxyUrl(url, { delivery: "proxy" }); }, []); const revokeObjectUrl = useCallback(() => { diff --git a/lib/image-delivery.ts b/lib/image-delivery.ts index 65dc5916f..e9238d220 100644 --- a/lib/image-delivery.ts +++ b/lib/image-delivery.ts @@ -6,6 +6,7 @@ export type ImageProxyOptions = { width?: number; quality?: number; format?: ImageProxyFormat; + delivery?: "display" | "proxy"; }; function clamp(value: number, min: number, max: number): number { @@ -36,12 +37,70 @@ function isBypassUrl(url: string): boolean { ); } +function isAliyunOssImageHost(hostname: string): boolean { + const normalized = hostname.trim().toLowerCase(); + return normalized.endsWith(".aliyuncs.com") && normalized.includes(".oss-"); +} + +function resolveAliyunOssDisplayUrl( + url: string, + options: Pick +): string | null { + let target: URL; + try { + target = new URL(url); + } catch { + return null; + } + + if (!isAliyunOssImageHost(target.hostname)) { + return null; + } + + if (target.searchParams.has("x-oss-process")) { + return target.toString(); + } + + const width = normalizeWidth(options.width); + const quality = normalizeQuality(options.quality); + const format = options.format; + + if (!width && !quality && !format) { + return target.toString(); + } + + const processSteps = ["image"]; + if (width) { + processSteps.push(`resize,w_${width}`); + } + if (quality) { + processSteps.push(`quality,q_${quality}`); + } + if (format) { + const ossFormat = format === "jpeg" ? "jpg" : format; + if (ossFormat !== "webp" && ossFormat !== "jpg" && ossFormat !== "png") { + return null; + } + processSteps.push(`format,${ossFormat}`); + } + + target.searchParams.set("x-oss-process", processSteps.join("/")); + return target.toString(); +} + export function buildImageProxyUrl(url: string, options: ImageProxyOptions = {}): string { const source = String(url || "").trim(); if (isBypassUrl(source)) { return source; } + if (options.delivery !== "proxy") { + const directDisplayUrl = resolveAliyunOssDisplayUrl(source, options); + if (directDisplayUrl) { + return directDisplayUrl; + } + } + const width = normalizeWidth(options.width); const quality = normalizeQuality(options.quality); const format = options.format;