Compare commits
2 Commits
main
...
codex/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d74da71bda | ||
|
|
8f79020bc6 |
@ -281,6 +281,7 @@ import {
|
|||||||
extractCanvasModelName,
|
extractCanvasModelName,
|
||||||
extractImageUrls,
|
extractImageUrls,
|
||||||
extractVideoUrls,
|
extractVideoUrls,
|
||||||
|
filterStandaloneAssistantImageUrls,
|
||||||
formatMessageDateLabel,
|
formatMessageDateLabel,
|
||||||
getAssistantImageNodeIdentityKey,
|
getAssistantImageNodeIdentityKey,
|
||||||
getCanvasAssistantMessagesAfterTargetUser,
|
getCanvasAssistantMessagesAfterTargetUser,
|
||||||
@ -14925,9 +14926,12 @@ export function CanvasChatPanel({
|
|||||||
const shouldDeferAssistantMedia =
|
const shouldDeferAssistantMedia =
|
||||||
(isClawAssistantMessage || shouldTypeAgentAssistantMessage) &&
|
(isClawAssistantMessage || shouldTypeAgentAssistantMessage) &&
|
||||||
(shouldAnimateAnalysis || shouldAnimatePlainText);
|
(shouldAnimateAnalysis || shouldAnimatePlainText);
|
||||||
|
const standaloneImageUrls = isClawAssistantMessage
|
||||||
|
? filterStandaloneAssistantImageUrls(rawText, imageUrls)
|
||||||
|
: imageUrls;
|
||||||
const visibleImageUrls = shouldDeferAssistantMedia
|
const visibleImageUrls = shouldDeferAssistantMedia
|
||||||
? []
|
? []
|
||||||
: imageUrls;
|
: standaloneImageUrls;
|
||||||
const visibleVideoUrls = shouldDeferAssistantMedia
|
const visibleVideoUrls = shouldDeferAssistantMedia
|
||||||
? []
|
? []
|
||||||
: videoUrls;
|
: 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[] {
|
export function extractVideoUrls(text: string): string[] {
|
||||||
if (!text) return [];
|
if (!text) return [];
|
||||||
const urls = new Set<string>();
|
const urls = new Set<string>();
|
||||||
|
|||||||
@ -225,7 +225,7 @@ function appendMissingCanvasSkillTags(
|
|||||||
return [
|
return [
|
||||||
...tags,
|
...tags,
|
||||||
...additions.filter((tag) => !existingIds.has(tag.id)),
|
...additions.filter((tag) => !existingIds.has(tag.id)),
|
||||||
].slice(0, 30);
|
].slice(0, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeCanvasSkillTags(value: unknown): CanvasSkillTag[] {
|
export function normalizeCanvasSkillTags(value: unknown): CanvasSkillTag[] {
|
||||||
@ -261,7 +261,7 @@ export function normalizeCanvasSkillTags(value: unknown): CanvasSkillTag[] {
|
|||||||
return DEFAULT_CANVAS_SKILL_TAGS;
|
return DEFAULT_CANVAS_SKILL_TAGS;
|
||||||
}
|
}
|
||||||
|
|
||||||
return normalized.slice(0, 30);
|
return normalized.slice(0, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeCanvasSkillTagSettings(
|
export function normalizeCanvasSkillTagSettings(
|
||||||
|
|||||||
@ -216,6 +216,23 @@ test.describe.serial("/api/admin/settings", () => {
|
|||||||
expect(normalized.tags).toEqual(customTags);
|
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", () => {
|
test("canvas skill tags still use defaults when no stored setting exists", () => {
|
||||||
const normalized = normalizeCanvasSkillTagSettings(null, {
|
const normalized = normalizeCanvasSkillTagSettings(null, {
|
||||||
requireCurrentVersion: true,
|
requireCurrentVersion: true,
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import {
|
|||||||
collectMessageImageUrls,
|
collectMessageImageUrls,
|
||||||
extractAssistantImageUrls,
|
extractAssistantImageUrls,
|
||||||
extractVideoUrls,
|
extractVideoUrls,
|
||||||
|
filterStandaloneAssistantImageUrls,
|
||||||
|
getMarkdownTableImageUrlKeys,
|
||||||
normalizeMediaUrl,
|
normalizeMediaUrl,
|
||||||
sortCanvasMessagesByCreatedAt,
|
sortCanvasMessagesByCreatedAt,
|
||||||
stripMediaMarkup,
|
stripMediaMarkup,
|
||||||
@ -5175,6 +5177,29 @@ test.describe("canvas chat pure helpers", () => {
|
|||||||
expect(urls).toEqual(["https://cdn.example.com/result.png?token=secret"]);
|
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", () => {
|
test("extracts and strips video markup without removing surrounding text", () => {
|
||||||
const text = [
|
const text = [
|
||||||
"生成完成",
|
"生成完成",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user