mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-25 23:48:00 +00:00
fix(frontend): strip and parse the <current_uploads> upload context tag (#4402)
* fix(frontend): strip and parse the <current_uploads> upload context tag #4174 renamed the upload context block that UploadsMiddleware prepends to the user message from <uploaded_files> to <current_uploads>, but the frontend tag vocabulary was not updated, so the raw block (file list plus usage guidance) rendered inside the user's chat bubble and the file-chip content fallback stopped matching. Add the new tag to stripUploadedFilesTag, INTERNAL_MARKER_TAGS (covers export and streamdown preprocessing), parseUploadedFiles, and the chip fallback detection. <uploaded_files> stays supported for history and IM-channel messages. * fix(frontend): parse upload-context sizes to bytes for the file chip parseUploadedFiles stored the raw leading number of the human-readable size (e.g. parseInt("177.6 KB") -> 177) into FileInMessage.size, which is documented as bytes. On the content-fallback chip path (history/IM messages without additional_kwargs.files) formatBytes then rendered a 177.6 KB file as "0.2 KB". Convert the backend's "<n> KB" / "<n> MB" form back to bytes so the chip re-renders at the original magnitude; update the parse tests to assert byte values.
This commit is contained in:
parent
90f3a622e9
commit
6aad680596
@ -319,8 +319,11 @@ function MessageContent_({
|
||||
const files = useMemo(() => {
|
||||
const files = message.additional_kwargs?.files;
|
||||
if (!Array.isArray(files) || files.length === 0) {
|
||||
if (rawContent.includes("<uploaded_files>")) {
|
||||
// If the content contains the <uploaded_files> tag, we return the parsed files from the content for backward compatibility.
|
||||
if (
|
||||
rawContent.includes("<current_uploads>") ||
|
||||
rawContent.includes("<uploaded_files>")
|
||||
) {
|
||||
// If the content contains an upload context tag, we return the parsed files from the content for backward compatibility.
|
||||
return parseUploadedFiles(rawContent);
|
||||
}
|
||||
return null;
|
||||
|
||||
@ -596,7 +596,10 @@ export interface FileInMessage {
|
||||
*/
|
||||
export function stripUploadedFilesTag(content: string): string {
|
||||
return content
|
||||
.replace(/<(uploaded_files|slash_skill_activation)>[\s\S]*?<\/\1>/g, "")
|
||||
.replace(
|
||||
/<(current_uploads|uploaded_files|slash_skill_activation)>[\s\S]*?<\/\1>/g,
|
||||
"",
|
||||
)
|
||||
.trim();
|
||||
}
|
||||
|
||||
@ -606,7 +609,8 @@ export function stripUploadedFilesTag(content: string): string {
|
||||
*
|
||||
* These markers are *not* user copy — they come from:
|
||||
*
|
||||
* - ``UploadsMiddleware`` → ``<uploaded_files>``
|
||||
* - ``UploadsMiddleware`` → ``<current_uploads>`` (``<uploaded_files>``
|
||||
* before #4174; still emitted by IM channels and present in history)
|
||||
* - ``SkillActivationMiddleware`` → ``<slash_skill_activation>``
|
||||
* - ``DynamicContextMiddleware`` → ``<system-reminder>`` (carrying
|
||||
* ``<memory>`` / ``<current_date>`` inside)
|
||||
@ -620,6 +624,7 @@ export function stripUploadedFilesTag(content: string): string {
|
||||
* its ``hide_from_ui`` flag set.
|
||||
*/
|
||||
export const INTERNAL_MARKER_TAGS = [
|
||||
"current_uploads",
|
||||
"uploaded_files",
|
||||
"slash_skill_activation",
|
||||
"system-reminder",
|
||||
@ -646,9 +651,32 @@ export function stripInternalMarkers(content: string): string {
|
||||
return content.replace(INTERNAL_MARKER_RE, "").trim();
|
||||
}
|
||||
|
||||
// The upload context block renders sizes as human-readable strings
|
||||
// (uploads_middleware.py::_format_file_entry emits "<n> KB" / "<n> MB",
|
||||
// mirroring formatBytes). Convert them back to bytes so the parsed
|
||||
// FileInMessage.size honours its bytes contract and chips re-render at the
|
||||
// original magnitude instead of e.g. treating "177.6 KB" as 177 bytes.
|
||||
function parseHumanReadableSize(raw: string): number {
|
||||
const match = /([\d.]+)\s*(B|KB|MB|GB|TB)?/i.exec(raw.trim());
|
||||
if (!match) return 0;
|
||||
const value = parseFloat(match[1] ?? "");
|
||||
if (!Number.isFinite(value)) return 0;
|
||||
const multipliers: Record<string, number> = {
|
||||
B: 1,
|
||||
KB: 1024,
|
||||
MB: 1024 ** 2,
|
||||
GB: 1024 ** 3,
|
||||
TB: 1024 ** 4,
|
||||
};
|
||||
const unit = (match[2] ?? "B").toUpperCase();
|
||||
return Math.round(value * (multipliers[unit] ?? 1));
|
||||
}
|
||||
|
||||
export function parseUploadedFiles(content: string): FileInMessage[] {
|
||||
// Match <uploaded_files>...</uploaded_files> tag
|
||||
const uploadedFilesRegex = /<uploaded_files>([\s\S]*?)<\/uploaded_files>/;
|
||||
// Match the upload context block; the tag name depends on backend version
|
||||
// (<current_uploads> since #4174, <uploaded_files> before / on IM paths).
|
||||
const uploadedFilesRegex =
|
||||
/<(current_uploads|uploaded_files)>([\s\S]*?)<\/\1>/;
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-regexp-exec
|
||||
const match = content.match(uploadedFilesRegex);
|
||||
|
||||
@ -656,7 +684,7 @@ export function parseUploadedFiles(content: string): FileInMessage[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
const uploadedFilesContent = match[1];
|
||||
const uploadedFilesContent = match[2];
|
||||
|
||||
// Check if it's "No files have been uploaded yet."
|
||||
if (uploadedFilesContent?.includes("No files have been uploaded yet.")) {
|
||||
@ -677,7 +705,7 @@ export function parseUploadedFiles(content: string): FileInMessage[] {
|
||||
while ((fileMatch = fileRegex.exec(uploadedFilesContent ?? "")) !== null) {
|
||||
files.push({
|
||||
filename: fileMatch[1].trim(),
|
||||
size: parseInt(fileMatch[2].trim(), 10) ?? 0,
|
||||
size: parseHumanReadableSize(fileMatch[2]),
|
||||
path: fileMatch[3].trim(),
|
||||
});
|
||||
}
|
||||
|
||||
@ -14,6 +14,8 @@ import {
|
||||
hasContent,
|
||||
hasReasoning,
|
||||
isAssistantMessageGroupStreaming,
|
||||
parseUploadedFiles,
|
||||
stripInternalMarkers,
|
||||
stripUploadedFilesTag,
|
||||
} from "@/core/messages/utils";
|
||||
|
||||
@ -307,6 +309,47 @@ describe("human message internal context stripping", () => {
|
||||
expect(getMessageCopyData(message)).toBe("Summarize this paper");
|
||||
});
|
||||
|
||||
test("strips current_uploads context from copy data", () => {
|
||||
// Mirrors the block UploadsMiddleware emits since #4174, including the
|
||||
// trailing usage-guidance lines.
|
||||
const message = {
|
||||
id: "human-with-current-uploads",
|
||||
type: "human",
|
||||
content:
|
||||
"<current_uploads>\nThe following files were uploaded in this message:\n\n- paper.docx (177.6 KB)\n Path: /mnt/user-data/uploads/paper.docx\n\nTo work with these files:\n- Use `grep` to search for keywords\n (e.g. `grep(pattern='revenue', path='/mnt/user-data/uploads/')`).\n</current_uploads>\n\nMake a slide deck from this",
|
||||
} as Message;
|
||||
|
||||
expect(getMessageCopyData(message)).toBe("Make a slide deck from this");
|
||||
});
|
||||
|
||||
test("parses uploaded files from a current_uploads block", () => {
|
||||
const content =
|
||||
"<current_uploads>\nThe following files were uploaded in this message:\n\n- paper.docx (177.6 KB)\n Path: /mnt/user-data/uploads/paper.docx\n Document outline (use `read_file` with line ranges to read sections):\n L1: Introduction\n- data.xlsx (12.0 KB)\n Path: /mnt/user-data/uploads/data.xlsx\n</current_uploads>\n\nSummarize";
|
||||
|
||||
// size is bytes (FileInMessage contract): the block's "177.6 KB" /
|
||||
// "12.0 KB" are converted back from the human-readable form the backend
|
||||
// emits, so formatBytes re-renders them at the original magnitude.
|
||||
expect(parseUploadedFiles(content)).toEqual([
|
||||
{
|
||||
filename: "paper.docx",
|
||||
size: Math.round(177.6 * 1024), // 181862
|
||||
path: "/mnt/user-data/uploads/paper.docx",
|
||||
},
|
||||
{
|
||||
filename: "data.xlsx",
|
||||
size: 12 * 1024, // 12288
|
||||
path: "/mnt/user-data/uploads/data.xlsx",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("stripInternalMarkers removes current_uploads blocks on export", () => {
|
||||
const content =
|
||||
"<current_uploads>\n- paper.docx (177.6 KB)\n Path: /mnt/user-data/uploads/paper.docx\n</current_uploads>\n\nExport me";
|
||||
|
||||
expect(stripInternalMarkers(content)).toBe("Export me");
|
||||
});
|
||||
|
||||
test("strips slash skill activation context from display content", () => {
|
||||
const content =
|
||||
"<slash_skill_activation>\n<skill_content># Secret SKILL.md</skill_content>\n</slash_skill_activation>\nreal user task";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user