Compare commits
2 Commits
main
...
codex/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d74da71bda | ||
|
|
8f79020bc6 |
@ -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;
|
||||
|
||||
@ -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>();
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 |  |",
|
||||
"| B | [查看](https://cdn.example.com/table-link.webp?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 = [
|
||||
"生成完成",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user