历史图片回退保持归档入口和正确媒体类型 #37
@ -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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -448,7 +448,7 @@ function extractChatMediaUrls(text: string, kind: ChatMediaKind): string[] {
|
||||
}
|
||||
}
|
||||
|
||||
const markdownLinkPattern = /\[([^\]]*)]\((https?:\/\/[^)\s]+)\)/gi;
|
||||
const markdownLinkPattern = /(?<!!)\[([^\]]*)]\((https?:\/\/[^)\s]+)\)/gi;
|
||||
while ((match = markdownLinkPattern.exec(text)) !== null) {
|
||||
const label = String(match[1] || "");
|
||||
const hintedKind = /音频|语音|声音|audio|mp3/i.test(label)
|
||||
@ -491,7 +491,7 @@ export function stripMediaMarkup(text: string): string {
|
||||
.replace(/<audio[\s\S]*?<\/audio>/gi, "")
|
||||
.replace(/<audio\b[^>]*\/?\s*>/gi, "")
|
||||
.replace(/<a[\s\S]*?<\/a>/gi, "")
|
||||
.replace(/\[([^\]]*)]\((https?:\/\/[^)\s]+)\)/gi, (match, label, url) => {
|
||||
.replace(/(?<!!)\[([^\]]*)]\((https?:\/\/[^)\s]+)\)/gi, (match, label, url) => {
|
||||
const hintedMedia = /音频|语音|声音|audio|mp3|视频|影片|播放|video/i.test(
|
||||
String(label || "")
|
||||
);
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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 = ``;
|
||||
expect(extractVideoUrls(markdown)).toEqual([]);
|
||||
expect(extractAudioUrls(markdown)).toEqual([]);
|
||||
expect(extractAssistantImageUrls(markdown)).toContain(url);
|
||||
expect(stripMediaMarkup(markdown)).toBe(markdown);
|
||||
}
|
||||
});
|
||||
import {
|
||||
collectMessageImageUrls,
|
||||
extractAssistantImageUrls,
|
||||
|
||||
@ -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"); } } },
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user