From db63f38b9c365e443483e3fcffab35e4a8ba820e Mon Sep 17 00:00:00 2001 From: WuYongJie <549727955@qq.com> Date: Thu, 5 Nov 2026 09:57:38 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8E=86=E5=8F=B2=E5=9B=BE=E7=89=87=E5=9B=9E?= =?UTF-8?q?=E9=80=80=E4=BF=9D=E6=8C=81=E5=BD=92=E6=A1=A3=E5=85=A5=E5=8F=A3?= =?UTF-8?q?=E5=B9=B6=E6=AD=A3=E7=A1=AE=E8=AF=86=E5=88=AB=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E5=85=83=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/canvas/CanvasChatPanel.tsx | 50 +++++++++---------- .../canvas/chat-panel/canvas-chat-media.ts | 4 +- components/canvas/nodes/ImageNode.tsx | 14 +++--- components/elements/response.tsx | 10 ++-- lib/image-delivery.ts | 9 ++++ tests/routes/canvas-chat-helpers.test.ts | 11 ++++ tests/routes/legacy-oss-recovery.test.ts | 8 +++ 7 files changed, 67 insertions(+), 39 deletions(-) diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 58efc189b..735839f54 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -81,7 +81,7 @@ import { canvasService } from "@/services/canvas"; import { inferOriginalImageUrl, } from "@/lib/image-url"; -import { buildImageProxyUrl } from "@/lib/image-delivery"; +import { buildImageFallbackUrl, buildImageProxyUrl } from "@/lib/image-delivery"; import { isImageGenerationModelDescriptor, isVideoModelDescriptor, @@ -15812,8 +15812,8 @@ export function CanvasChatPanel({ loading="lazy" decoding="async" onError={(event) => { - if (event.currentTarget.src !== activeClawdSkillTag.iconUrl) { - event.currentTarget.src = activeClawdSkillTag.iconUrl || ""; + if (event.currentTarget.src !== buildImageFallbackUrl(activeClawdSkillTag.iconUrl)) { + event.currentTarget.src = buildImageFallbackUrl(activeClawdSkillTag.iconUrl); } }} /> @@ -16131,8 +16131,8 @@ export function CanvasChatPanel({ loading="lazy" decoding="async" onError={(event) => { - if (event.currentTarget.src !== attachment.url) { - event.currentTarget.src = attachment.url; + if (event.currentTarget.src !== buildImageFallbackUrl(attachment.url)) { + event.currentTarget.src = buildImageFallbackUrl(attachment.url); } }} /> @@ -16208,8 +16208,8 @@ export function CanvasChatPanel({ loading="lazy" decoding="async" onError={(event) => { - if (event.currentTarget.src !== attachment.url) { - event.currentTarget.src = attachment.url; + if (event.currentTarget.src !== buildImageFallbackUrl(attachment.url)) { + event.currentTarget.src = buildImageFallbackUrl(attachment.url); } }} /> @@ -16394,8 +16394,8 @@ export function CanvasChatPanel({ loading="lazy" decoding="async" onError={(event) => { - if (event.currentTarget.src !== tag.iconUrl) { - event.currentTarget.src = tag.iconUrl || ""; + if (event.currentTarget.src !== buildImageFallbackUrl(tag.iconUrl)) { + event.currentTarget.src = buildImageFallbackUrl(tag.iconUrl); } }} /> @@ -16695,8 +16695,8 @@ export function CanvasChatPanel({ loading="lazy" decoding="async" onError={(event) => { - if (event.currentTarget.src !== attachment.url) { - event.currentTarget.src = attachment.url; + if (event.currentTarget.src !== buildImageFallbackUrl(attachment.url)) { + event.currentTarget.src = buildImageFallbackUrl(attachment.url); } }} /> @@ -17032,8 +17032,8 @@ export function CanvasChatPanel({ loading="lazy" decoding="async" onError={(event) => { - if (event.currentTarget.src !== url) { - event.currentTarget.src = url; + if (event.currentTarget.src !== buildImageFallbackUrl(url)) { + event.currentTarget.src = buildImageFallbackUrl(url); } }} onClick={() => setFullscreenPreview({ url })} @@ -17885,8 +17885,8 @@ export function CanvasChatPanel({ loading="lazy" decoding="async" onError={(event) => { - if (event.currentTarget.src !== slotAttachment.url) { - event.currentTarget.src = slotAttachment.url; + if (event.currentTarget.src !== buildImageFallbackUrl(slotAttachment.url)) { + event.currentTarget.src = buildImageFallbackUrl(slotAttachment.url); } }} /> @@ -18210,8 +18210,8 @@ export function CanvasChatPanel({ loading="lazy" decoding="async" onError={(event) => { - if (event.currentTarget.src !== activeClawdSkillTag.iconUrl) { - event.currentTarget.src = activeClawdSkillTag.iconUrl || ""; + if (event.currentTarget.src !== buildImageFallbackUrl(activeClawdSkillTag.iconUrl)) { + event.currentTarget.src = buildImageFallbackUrl(activeClawdSkillTag.iconUrl); } }} /> @@ -18470,8 +18470,8 @@ export function CanvasChatPanel({ loading="lazy" decoding="async" onError={(event) => { - if (event.currentTarget.src !== item.url) { - event.currentTarget.src = item.url; + if (event.currentTarget.src !== buildImageFallbackUrl(item.url)) { + event.currentTarget.src = buildImageFallbackUrl(item.url); } }} /> @@ -18655,8 +18655,8 @@ export function CanvasChatPanel({ loading="lazy" decoding="async" onError={(event) => { - if (event.currentTarget.src !== slotAttachment.url) { - event.currentTarget.src = slotAttachment.url; + if (event.currentTarget.src !== buildImageFallbackUrl(slotAttachment.url)) { + event.currentTarget.src = buildImageFallbackUrl(slotAttachment.url); } }} /> @@ -18723,8 +18723,8 @@ export function CanvasChatPanel({ loading="lazy" decoding="async" onError={(event) => { - if (event.currentTarget.src !== slotAttachment.url) { - event.currentTarget.src = slotAttachment.url; + if (event.currentTarget.src !== buildImageFallbackUrl(slotAttachment.url)) { + event.currentTarget.src = buildImageFallbackUrl(slotAttachment.url); } }} /> @@ -19423,8 +19423,8 @@ export function CanvasChatPanel({ loading="lazy" decoding="async" onError={(event) => { - if (event.currentTarget.src !== tagPreview.url) { - event.currentTarget.src = tagPreview.url; + if (event.currentTarget.src !== buildImageFallbackUrl(tagPreview.url)) { + event.currentTarget.src = buildImageFallbackUrl(tagPreview.url); } }} /> diff --git a/components/canvas/chat-panel/canvas-chat-media.ts b/components/canvas/chat-panel/canvas-chat-media.ts index 2bb4d8a06..4206efbbd 100644 --- a/components/canvas/chat-panel/canvas-chat-media.ts +++ b/components/canvas/chat-panel/canvas-chat-media.ts @@ -448,7 +448,7 @@ function extractChatMediaUrls(text: string, kind: ChatMediaKind): string[] { } } - const markdownLinkPattern = /\[([^\]]*)]\((https?:\/\/[^)\s]+)\)/gi; + const markdownLinkPattern = /(?/gi, "") .replace(/]*\/?\s*>/gi, "") .replace(//gi, "") - .replace(/\[([^\]]*)]\((https?:\/\/[^)\s]+)\)/gi, (match, label, url) => { + .replace(/(? { const hintedMedia = /音频|语音|声音|audio|mp3|视频|影片|播放|video/i.test( String(label || "") ); diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index e673c09e2..c12d2a354 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -186,7 +186,7 @@ import { normalizeDetailUiSubjectAnalysis, } from "@/lib/detail-ui-subject-analysis"; import type { DetailUiScreenOverlaySnapshot } from "@/lib/detail-ui-surface-adapter"; -import { buildImageProxyUrl } from "@/lib/image-delivery"; +import { buildImageFallbackUrl, buildImageProxyUrl } from "@/lib/image-delivery"; import { DEFAULT_PUBLIC_THREED_MODEL_ALIAS_CONFIG, getDefaultThreeDModelAliasId, @@ -8466,8 +8466,8 @@ export function ImageNode({ draggable={false} loading="lazy" onError={(event) => { - if (event.currentTarget.src !== data.src) { - event.currentTarget.src = data.src; + if (event.currentTarget.src !== buildImageFallbackUrl(data.src)) { + event.currentTarget.src = buildImageFallbackUrl(data.src); } }} onLoad={handleDisplayImageLoad} @@ -8495,8 +8495,8 @@ export function ImageNode({ draggable={false} loading="lazy" onError={(event) => { - if (event.currentTarget.src !== compareSource) { - event.currentTarget.src = compareSource; + if (event.currentTarget.src !== buildImageFallbackUrl(compareSource)) { + event.currentTarget.src = buildImageFallbackUrl(compareSource); } }} src={displayCompareSrc} @@ -8563,8 +8563,8 @@ export function ImageNode({ } }} onError={(event) => { - if (event.currentTarget.src !== data.src) { - event.currentTarget.src = data.src; + if (event.currentTarget.src !== buildImageFallbackUrl(data.src)) { + event.currentTarget.src = buildImageFallbackUrl(data.src); } }} onLoad={handleDisplayImageLoad} diff --git a/components/elements/response.tsx b/components/elements/response.tsx index 83126a680..60b5f263e 100644 --- a/components/elements/response.tsx +++ b/components/elements/response.tsx @@ -15,7 +15,7 @@ import { splitOpenClawChartBlocks, } from "@/components/elements/openclaw-chart-card"; import { ResponsePdfDownloadButton } from "@/components/elements/response-pdf-download"; -import { buildImageProxyUrl } from "@/lib/image-delivery"; +import { buildImageFallbackUrl, buildImageProxyUrl } from "@/lib/image-delivery"; import { splitMarkdownImages } from "@/lib/canvas/markdown-image-segments"; import { stableStreamingMarkdown } from "@/lib/canvas/markdown-image-streaming"; import { getGatewayErrorPageMessage } from "@/lib/request-error"; @@ -442,8 +442,8 @@ function MarkdownTableImagePreview({ loading="lazy" decoding="async" onError={(event) => { - if (event.currentTarget.src !== src) { - event.currentTarget.src = src; + if (event.currentTarget.src !== buildImageFallbackUrl(src)) { + event.currentTarget.src = buildImageFallbackUrl(src); } }} /> @@ -473,8 +473,8 @@ function MarkdownTableImagePreview({ decoding="async" onClick={(event) => event.stopPropagation()} onError={(event) => { - if (event.currentTarget.src !== originalSrc) { - event.currentTarget.src = originalSrc; + if (event.currentTarget.src !== buildImageFallbackUrl(originalSrc)) { + event.currentTarget.src = buildImageFallbackUrl(originalSrc); } }} /> diff --git a/lib/image-delivery.ts b/lib/image-delivery.ts index 656106ab7..f34e7af1b 100644 --- a/lib/image-delivery.ts +++ b/lib/image-delivery.ts @@ -2,6 +2,15 @@ import { normalizeLegacyAliyunStorageUrl } from "./storage/retired-storage"; const IMAGE_PROXY_PATH = "/api/image-proxy"; +/** Error fallbacks must keep unavailable legacy sources behind archive resolution. */ +export function buildImageFallbackUrl(value: string | null | undefined): string { + const source = value || ""; + const legacy = normalizeLegacyAliyunStorageUrl(source); + if (!legacy) return source; + const proxy = buildImageProxyUrl(legacy); + return typeof window === "undefined" ? proxy : new URL(proxy, window.location.origin).toString(); +} + type ImageProxyFormat = "webp" | "avif" | "jpeg" | "png"; export type ImageProxyOptions = { diff --git a/tests/routes/canvas-chat-helpers.test.ts b/tests/routes/canvas-chat-helpers.test.ts index 4cb96185f..b874a2f38 100644 --- a/tests/routes/canvas-chat-helpers.test.ts +++ b/tests/routes/canvas-chat-helpers.test.ts @@ -1,4 +1,15 @@ import { expect, test } from "@playwright/test"; + +test("image metadata containing video or audio paths remains an image", () => { + for (const folder of ["video-canvas-demo", "audio-preview"]) { + const url = `https://demo-sparkai.oss-cn-shanghai.aliyuncs.com/${folder}/cover.png`; + const markdown = `![旧链接图片|||${url}|||2K](${url})`; + expect(extractVideoUrls(markdown)).toEqual([]); + expect(extractAudioUrls(markdown)).toEqual([]); + expect(extractAssistantImageUrls(markdown)).toContain(url); + expect(stripMediaMarkup(markdown)).toBe(markdown); + } +}); import { collectMessageImageUrls, extractAssistantImageUrls, diff --git a/tests/routes/legacy-oss-recovery.test.ts b/tests/routes/legacy-oss-recovery.test.ts index 880268c97..caf6398b1 100644 --- a/tests/routes/legacy-oss-recovery.test.ts +++ b/tests/routes/legacy-oss-recovery.test.ts @@ -41,6 +41,14 @@ test("legacy display URLs go through archive resolution while other Alibaba buck expect(buildImageProxyUrl("https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/result.png", { width: 400 })).toContain("x-oss-process="); }); +test("browser image fallbacks remain on one stable archive URL instead of retrying the old source", () => { + const delivery = serverLoader({ window: { location: { origin: "https://app.example" } } })("lib/image-delivery.ts"); + const fallback = delivery.buildImageFallbackUrl(legacy); + expect(fallback).toMatch(/^https:\/\/app\.example\/api\/image-proxy\?/); + expect(delivery.buildImageFallbackUrl(fallback)).toBe(fallback); + expect(delivery.buildImageFallbackUrl(permanent)).toBe(permanent); +}); + function archiveLoader(rows: Array<{ sourceUrl: string; targetUrl: string | null }>) { return serverLoader({ "@/lib/db": { db: { select: () => ({ from: () => ({ where: async () => rows }) }), insert: () => { throw new Error("Legacy sources must not be queued"); } } },