fix(claw): restore spreadsheet attachment badges
This commit is contained in:
parent
92e0f49fbc
commit
b4e3561510
@ -467,14 +467,16 @@ function isClawdChatMessage(message: unknown) {
|
|||||||
|
|
||||||
function isClawdSpreadsheetAttachment(
|
function isClawdSpreadsheetAttachment(
|
||||||
url: string,
|
url: string,
|
||||||
contentType?: string | null
|
contentType?: string | null,
|
||||||
|
name?: string | null
|
||||||
) {
|
) {
|
||||||
const normalizedType = String(contentType || "").toLowerCase();
|
const normalizedType = String(contentType || "").toLowerCase();
|
||||||
return (
|
return (
|
||||||
normalizedType.includes("spreadsheet") ||
|
normalizedType.includes("spreadsheet") ||
|
||||||
normalizedType === "text/csv" ||
|
normalizedType === "text/csv" ||
|
||||||
normalizedType === "text/tab-separated-values" ||
|
normalizedType === "text/tab-separated-values" ||
|
||||||
/\.(xlsx|csv|tsv)(?:$|[?#])/i.test(String(url || ""))
|
/\.(xlsx|csv|tsv)(?:$|[?#])/i.test(String(url || "")) ||
|
||||||
|
/\.(xlsx|csv|tsv)$/i.test(String(name || "").trim())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -976,38 +978,43 @@ function parseClawdSpreadsheetAttachmentsFromText(
|
|||||||
.filter((part: any) => part?.type === "text" && part?.text)
|
.filter((part: any) => part?.type === "text" && part?.text)
|
||||||
.map((part: any) => String(part.text))
|
.map((part: any) => String(part.text))
|
||||||
.join("\n");
|
.join("\n");
|
||||||
const summaryMatch = text.match(
|
|
||||||
/(?:^|\n)已上传表格:\n((?:- [^\n]+(?:\n|$))+)/m
|
|
||||||
);
|
|
||||||
if (!summaryMatch?.[1]) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const attachments: ClawdSpreadsheetAttachment[] = [];
|
const attachments: ClawdSpreadsheetAttachment[] = [];
|
||||||
for (const line of summaryMatch[1].trim().split("\n")) {
|
const sectionPattern =
|
||||||
const lineMatch = line.match(/^- (.+?)(?:((.+)))?$/);
|
/(?:^|\n)(已上传表格|已上传文件):\n((?:- [^\n]+(?:\n|$))+)/gm;
|
||||||
const name = String(lineMatch?.[1] || "").trim();
|
for (const sectionMatch of text.matchAll(sectionPattern)) {
|
||||||
const details = String(lineMatch?.[2] || "");
|
const sectionType = String(sectionMatch[1] || "");
|
||||||
if (!name) continue;
|
const sectionLines = String(sectionMatch[2] || "");
|
||||||
|
for (const line of sectionLines.trim().split("\n")) {
|
||||||
|
const lineMatch = line.match(/^- (.+?)(?:((.+)))?$/);
|
||||||
|
const name = String(lineMatch?.[1] || "").trim();
|
||||||
|
const details = String(lineMatch?.[2] || "");
|
||||||
|
if (
|
||||||
|
!name ||
|
||||||
|
(sectionType === "已上传文件" &&
|
||||||
|
!isClawdSpreadsheetAttachment("", "", name))
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const sizeMatch = details.match(/([\d.]+)\s*(KB|MB)/i);
|
const sizeMatch = details.match(/([\d.]+)\s*(KB|MB)/i);
|
||||||
const sheetCountMatch = details.match(/(\d+)\s*个工作表/);
|
const sheetCountMatch = details.match(/(\d+)\s*个工作表/);
|
||||||
const rowCountMatch = details.match(/(\d+)\s*行/);
|
const rowCountMatch = details.match(/(\d+)\s*行/);
|
||||||
const sizeValue = Number(sizeMatch?.[1]);
|
const sizeValue = Number(sizeMatch?.[1]);
|
||||||
const sizeUnit = String(sizeMatch?.[2] || "").toUpperCase();
|
const sizeUnit = String(sizeMatch?.[2] || "").toUpperCase();
|
||||||
|
|
||||||
attachments.push({
|
attachments.push({
|
||||||
name,
|
name,
|
||||||
size:
|
size:
|
||||||
Number.isFinite(sizeValue) && sizeValue > 0
|
Number.isFinite(sizeValue) && sizeValue > 0
|
||||||
? Math.round(
|
? Math.round(
|
||||||
sizeValue * (sizeUnit === "MB" ? 1024 * 1024 : 1024)
|
sizeValue * (sizeUnit === "MB" ? 1024 * 1024 : 1024)
|
||||||
)
|
)
|
||||||
: undefined,
|
: undefined,
|
||||||
sheetCount: sheetCountMatch ? Number(sheetCountMatch[1]) : undefined,
|
sheetCount: sheetCountMatch ? Number(sheetCountMatch[1]) : undefined,
|
||||||
rowCount: rowCountMatch ? Number(rowCountMatch[1]) : undefined,
|
rowCount: rowCountMatch ? Number(rowCountMatch[1]) : undefined,
|
||||||
truncated: details.includes("已截断"),
|
truncated: details.includes("已截断"),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return attachments;
|
return attachments;
|
||||||
}
|
}
|
||||||
@ -1030,11 +1037,7 @@ function getClawdSpreadsheetAttachments(
|
|||||||
: Array.isArray(videoMetadata.spreadsheetAttachments)
|
: Array.isArray(videoMetadata.spreadsheetAttachments)
|
||||||
? videoMetadata.spreadsheetAttachments
|
? videoMetadata.spreadsheetAttachments
|
||||||
: [];
|
: [];
|
||||||
if (rawAttachments.length === 0) {
|
const normalizedAttachments = rawAttachments
|
||||||
return parseClawdSpreadsheetAttachmentsFromText(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rawAttachments
|
|
||||||
.map((attachment: any) => {
|
.map((attachment: any) => {
|
||||||
const name = String(
|
const name = String(
|
||||||
attachment?.name || attachment?.fileName || ""
|
attachment?.name || attachment?.fileName || ""
|
||||||
@ -1068,14 +1071,72 @@ function getClawdSpreadsheetAttachments(
|
|||||||
attachment: ClawdSpreadsheetAttachment | null
|
attachment: ClawdSpreadsheetAttachment | null
|
||||||
): attachment is ClawdSpreadsheetAttachment => Boolean(attachment)
|
): attachment is ClawdSpreadsheetAttachment => Boolean(attachment)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const knownNames = new Set(
|
||||||
|
normalizedAttachments.map((attachment: ClawdSpreadsheetAttachment) =>
|
||||||
|
attachment.name.toLowerCase()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const inferredAttachments = [
|
||||||
|
...(((message as any).attachments || []) as any[]),
|
||||||
|
...((message.parts || []).filter((part: any) => part?.type === "file") as any[]),
|
||||||
|
];
|
||||||
|
for (const attachment of inferredAttachments) {
|
||||||
|
const name = String(attachment?.name || attachment?.fileName || "").trim();
|
||||||
|
const url = String(attachment?.url || attachment?.data || "").trim();
|
||||||
|
const contentType = String(
|
||||||
|
attachment?.contentType ||
|
||||||
|
attachment?.content_type ||
|
||||||
|
attachment?.mediaType ||
|
||||||
|
""
|
||||||
|
).trim();
|
||||||
|
if (
|
||||||
|
!name ||
|
||||||
|
knownNames.has(name.toLowerCase()) ||
|
||||||
|
!isClawdSpreadsheetAttachment(url, contentType, name)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
normalizedAttachments.push({ name, contentType: contentType || undefined });
|
||||||
|
knownNames.add(name.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const attachment of parseClawdSpreadsheetAttachmentsFromText(message)) {
|
||||||
|
if (knownNames.has(attachment.name.toLowerCase())) continue;
|
||||||
|
normalizedAttachments.push(attachment);
|
||||||
|
knownNames.add(attachment.name.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedAttachments;
|
||||||
}
|
}
|
||||||
|
|
||||||
function stripClawdSpreadsheetSummary(text: string) {
|
function stripClawdSpreadsheetSummary(
|
||||||
|
text: string,
|
||||||
|
attachments: ClawdSpreadsheetAttachment[]
|
||||||
|
) {
|
||||||
|
const spreadsheetNames = new Set(
|
||||||
|
attachments.map((attachment) => attachment.name.toLowerCase())
|
||||||
|
);
|
||||||
return text
|
return text
|
||||||
.replace(
|
.replace(
|
||||||
/(?:^|\n{2})已上传表格:\n(?:- [^\n]*(?:\n|$))+/g,
|
/(?:^|\n{2})已上传表格:\n(?:- [^\n]*(?:\n|$))+/g,
|
||||||
"\n"
|
"\n"
|
||||||
)
|
)
|
||||||
|
.replace(
|
||||||
|
/(?:^|\n{2})已上传文件:\n((?:- [^\n]*(?:\n|$))+)/g,
|
||||||
|
(_section, lines: string) => {
|
||||||
|
const remainingLines = String(lines)
|
||||||
|
.trim()
|
||||||
|
.split("\n")
|
||||||
|
.filter((line) => {
|
||||||
|
const name = line.replace(/^- /, "").trim().toLowerCase();
|
||||||
|
return !spreadsheetNames.has(name);
|
||||||
|
});
|
||||||
|
return remainingLines.length > 0
|
||||||
|
? `\n\n已上传文件:\n${remainingLines.join("\n")}`
|
||||||
|
: "\n";
|
||||||
|
}
|
||||||
|
)
|
||||||
.replace(/\n{3,}/g, "\n\n")
|
.replace(/\n{3,}/g, "\n\n")
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
@ -12426,6 +12487,7 @@ export function CanvasChatPanel({
|
|||||||
id: userMessageId,
|
id: userMessageId,
|
||||||
role: "user",
|
role: "user",
|
||||||
parts: [{ type: "text", text: optimisticMessageText }],
|
parts: [{ type: "text", text: optimisticMessageText }],
|
||||||
|
attachments: sanitizedAttachmentsSnapshot,
|
||||||
createdAt: optimisticCreatedAt,
|
createdAt: optimisticCreatedAt,
|
||||||
metadata: {
|
metadata: {
|
||||||
createdAt: optimisticCreatedAt.toISOString(),
|
createdAt: optimisticCreatedAt.toISOString(),
|
||||||
@ -16254,8 +16316,10 @@ export function CanvasChatPanel({
|
|||||||
const isSpreadsheetAttachment =
|
const isSpreadsheetAttachment =
|
||||||
isClawdSpreadsheetAttachment(
|
isClawdSpreadsheetAttachment(
|
||||||
fileUrl,
|
fileUrl,
|
||||||
attachmentContentType
|
attachmentContentType,
|
||||||
|
part.name
|
||||||
);
|
);
|
||||||
|
if (isSpreadsheetAttachment) return;
|
||||||
const attachmentLabel =
|
const attachmentLabel =
|
||||||
part.name ||
|
part.name ||
|
||||||
(isVideoAttachment
|
(isVideoAttachment
|
||||||
@ -16332,7 +16396,10 @@ export function CanvasChatPanel({
|
|||||||
.trim();
|
.trim();
|
||||||
const text =
|
const text =
|
||||||
spreadsheetAttachments.length > 0
|
spreadsheetAttachments.length > 0
|
||||||
? stripClawdSpreadsheetSummary(normalizedText)
|
? stripClawdSpreadsheetSummary(
|
||||||
|
normalizedText,
|
||||||
|
spreadsheetAttachments
|
||||||
|
)
|
||||||
: normalizedText;
|
: normalizedText;
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
renderedUserParts.push(
|
renderedUserParts.push(
|
||||||
@ -16355,8 +16422,10 @@ export function CanvasChatPanel({
|
|||||||
const isSpreadsheetAttachment =
|
const isSpreadsheetAttachment =
|
||||||
isClawdSpreadsheetAttachment(
|
isClawdSpreadsheetAttachment(
|
||||||
att.url,
|
att.url,
|
||||||
att.contentType
|
att.contentType,
|
||||||
|
att.name
|
||||||
);
|
);
|
||||||
|
if (isSpreadsheetAttachment) return;
|
||||||
renderedUserParts.push(
|
renderedUserParts.push(
|
||||||
isFileAttachment ? (
|
isFileAttachment ? (
|
||||||
<a
|
<a
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import type { Attachment, ChatMessage } from "@/lib/types";
|
import type { Attachment, ChatMessage } from "@/lib/types";
|
||||||
|
import { isOpenClawChatFileAttachment } from "@/lib/openclaw-chat/media-attachments";
|
||||||
|
|
||||||
export const SESSION_TITLE_MAX_LEN = 18;
|
export const SESSION_TITLE_MAX_LEN = 18;
|
||||||
export const MAX_CHAT_TEXT_PART_LENGTH = 2000;
|
export const MAX_CHAT_TEXT_PART_LENGTH = 2000;
|
||||||
@ -85,6 +86,9 @@ export function normalizeChatMediaType(value?: string | null): string {
|
|||||||
if (ALLOWED_CHAT_MEDIA_TYPES.has(normalized)) {
|
if (ALLOWED_CHAT_MEDIA_TYPES.has(normalized)) {
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
if (isOpenClawChatFileAttachment("", normalized)) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
return "image/jpeg";
|
return "image/jpeg";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -235,7 +235,11 @@ import {
|
|||||||
normalizeUploadedAssetId,
|
normalizeUploadedAssetId,
|
||||||
validateCanvasImageUploadFile,
|
validateCanvasImageUploadFile,
|
||||||
} from "@/components/canvas/chat-panel/canvas-upload-helpers";
|
} from "@/components/canvas/chat-panel/canvas-upload-helpers";
|
||||||
import type { PickedObjectAttachment } from "@/components/canvas/chat-panel/canvas-chat-session";
|
import {
|
||||||
|
normalizeChatMediaType,
|
||||||
|
sanitizeChatAttachment,
|
||||||
|
type PickedObjectAttachment,
|
||||||
|
} from "@/components/canvas/chat-panel/canvas-chat-session";
|
||||||
import type { CanvasAgentContext } from "@/types/canvas/agent-protocol";
|
import type { CanvasAgentContext } from "@/types/canvas/agent-protocol";
|
||||||
import type { ChatMessage } from "@/lib/types";
|
import type { ChatMessage } from "@/lib/types";
|
||||||
import type { DBMessage } from "@/lib/db/schema";
|
import type { DBMessage } from "@/lib/db/schema";
|
||||||
@ -253,6 +257,22 @@ function dbTextMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
test.describe("canvas chat pure helpers", () => {
|
test.describe("canvas chat pure helpers", () => {
|
||||||
|
test("preserves spreadsheet attachment content types", () => {
|
||||||
|
const spreadsheetContentType =
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
||||||
|
|
||||||
|
expect(normalizeChatMediaType(spreadsheetContentType)).toBe(
|
||||||
|
spreadsheetContentType
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
sanitizeChatAttachment({
|
||||||
|
name: "测试用.xlsx",
|
||||||
|
url: "https://cdn.example.com/uploads/test.xlsx",
|
||||||
|
contentType: spreadsheetContentType,
|
||||||
|
})?.contentType
|
||||||
|
).toBe(spreadsheetContentType);
|
||||||
|
});
|
||||||
|
|
||||||
test("keeps optimistic messages without timestamps after persisted history", () => {
|
test("keeps optimistic messages without timestamps after persisted history", () => {
|
||||||
const messages = [
|
const messages = [
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user