Compare commits

...

2 Commits

Author SHA1 Message Date
134
d74da71bda fix(canvas): raise Claw skill tag limit 2026-08-05 21:35:42 +08:00
134
8f79020bc6 fix(canvas): dedupe Claw table images 2026-08-05 19:17:49 +08:00
5 changed files with 123 additions and 3 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

@ -225,7 +225,7 @@ function appendMissingCanvasSkillTags(
return [
...tags,
...additions.filter((tag) => !existingIds.has(tag.id)),
].slice(0, 30);
].slice(0, 50);
}
export function normalizeCanvasSkillTags(value: unknown): CanvasSkillTag[] {
@ -261,7 +261,7 @@ export function normalizeCanvasSkillTags(value: unknown): CanvasSkillTag[] {
return DEFAULT_CANVAS_SKILL_TAGS;
}
return normalized.slice(0, 30);
return normalized.slice(0, 50);
}
export function normalizeCanvasSkillTagSettings(

View File

@ -216,6 +216,23 @@ test.describe.serial("/api/admin/settings", () => {
expect(normalized.tags).toEqual(customTags);
});
test("canvas skill tag settings retain more than thirty configured skills", () => {
const configuredTags: CanvasSkillTag[] = Array.from({ length: 31 }, (_, index) => ({
id: `configured-skill-${index + 1}`,
name: `Configured skill ${index + 1}`,
prompt: `Run configured skill ${index + 1}`,
}));
const normalized = normalizeCanvasSkillTagSettings({
enabled: true,
title: "Claw 快捷技能",
tags: configuredTags,
version: CANVAS_SKILL_TAGS_CONFIG_VERSION,
});
expect(normalized.tags).toEqual(configuredTags);
});
test("canvas skill tags still use defaults when no stored setting exists", () => {
const normalized = normalizeCanvasSkillTagSettings(null, {
requireCurrentVersion: true,

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 = [
"生成完成",