fix(canvas): dedupe Claw table images

This commit is contained in:
134 2026-08-05 19:17:49 +08:00 committed by root
parent c6c7c703b5
commit ee5b5d146d
3 changed files with 104 additions and 1 deletions

View File

@ -281,6 +281,7 @@ import {
extractCanvasModelName,
extractImageUrls,
extractVideoUrls,
filterStandaloneAssistantImageUrls,
formatMessageDateLabel,
getAssistantImageNodeIdentityKey,
getCanvasAssistantMessagesAfterTargetUser,
@ -14925,9 +14926,12 @@ export function CanvasChatPanel({
const shouldDeferAssistantMedia =
(isClawAssistantMessage || shouldTypeAgentAssistantMessage) &&
(shouldAnimateAnalysis || shouldAnimatePlainText);
const standaloneImageUrls = isClawAssistantMessage
? filterStandaloneAssistantImageUrls(rawText, imageUrls)
: imageUrls;
const visibleImageUrls = shouldDeferAssistantMedia
? []
: imageUrls;
: standaloneImageUrls;
const visibleVideoUrls = shouldDeferAssistantMedia
? []
: videoUrls;

View File

@ -245,6 +245,80 @@ export function extractAssistantImageUrls(
});
}
function isMarkdownTableRow(line: string): boolean {
const trimmed = String(line || "").trim();
if (!trimmed || !trimmed.includes("|")) return false;
const cells = trimmed
.replace(/^\|/, "")
.replace(/\|$/, "")
.split("|");
return cells.length >= 2;
}
function isMarkdownTableSeparator(line: string): boolean {
if (!isMarkdownTableRow(line)) return false;
const cells = String(line || "")
.trim()
.replace(/^\|/, "")
.replace(/\|$/, "")
.split("|")
.map((cell) => cell.trim().replace(/\s+/g, ""));
return cells.length >= 2 && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
}
export function getMarkdownTableImageUrlKeys(text: string): Set<string> {
const urlKeys = new Set<string>();
const lines = String(text || "").replace(/\r\n/g, "\n").split("\n");
let inCodeFence = false;
const addUrl = (url: string) => {
const key = normalizeImageIdentityUrl(url);
if (key) urlKeys.add(key);
};
for (let index = 0; index < lines.length - 1; index += 1) {
const line = lines[index] || "";
if (/^\s*```/.test(line)) {
inCodeFence = !inCodeFence;
continue;
}
if (
inCodeFence ||
!isMarkdownTableRow(line) ||
!isMarkdownTableSeparator(lines[index + 1] || "")
) {
continue;
}
let endIndex = index + 2;
while (endIndex < lines.length && isMarkdownTableRow(lines[endIndex] || "")) {
endIndex += 1;
}
const tableText = lines.slice(index, endIndex).join("\n");
extractAssistantImageUrls(tableText).forEach(addUrl);
extractPlainImageUrls(tableText).forEach(addUrl);
index = endIndex - 1;
}
return urlKeys;
}
export function filterStandaloneAssistantImageUrls(
text: string,
imageUrls: string[]
): string[] {
const tableImageUrlKeys = getMarkdownTableImageUrlKeys(text);
if (tableImageUrlKeys.size === 0) return imageUrls;
return imageUrls.filter((url) => {
const key = normalizeImageIdentityUrl(url);
return !key || !tableImageUrlKeys.has(key);
});
}
export function extractVideoUrls(text: string): string[] {
if (!text) return [];
const urls = new Set<string>();

View File

@ -3,6 +3,8 @@ import {
collectMessageImageUrls,
extractAssistantImageUrls,
extractVideoUrls,
filterStandaloneAssistantImageUrls,
getMarkdownTableImageUrlKeys,
normalizeMediaUrl,
sortCanvasMessagesByCreatedAt,
stripMediaMarkup,
@ -5175,6 +5177,29 @@ test.describe("canvas chat pure helpers", () => {
expect(urls).toEqual(["https://cdn.example.com/result.png?token=secret"]);
});
test("does not append Claw images that already render inside markdown tables", () => {
const markdown = [
"| 商品 | 主图 |",
"| --- | --- |",
"| A | ![主图](https://cdn.example.com/table-image.png?token=secret) |",
"| B | [查看](https://cdn.example.com/table-link.webp?token=secret) |",
"",
"补充图片:",
"![补充](https://cdn.example.com/standalone-image.jpg?token=secret)",
].join("\n");
const imageUrls = extractAssistantImageUrls(markdown);
expect(getMarkdownTableImageUrlKeys(markdown)).toEqual(
new Set([
"https://cdn.example.com/table-image.png",
"https://cdn.example.com/table-link.webp",
])
);
expect(filterStandaloneAssistantImageUrls(markdown, imageUrls)).toEqual([
"https://cdn.example.com/standalone-image.jpg?token=secret",
]);
});
test("extracts and strips video markup without removing surrounding text", () => {
const text = [
"生成完成",