mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-08-01 10:56:02 +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 = useMemo(() => {
|
||||||
const files = message.additional_kwargs?.files;
|
const files = message.additional_kwargs?.files;
|
||||||
if (!Array.isArray(files) || files.length === 0) {
|
if (!Array.isArray(files) || files.length === 0) {
|
||||||
if (rawContent.includes("<uploaded_files>")) {
|
if (
|
||||||
// If the content contains the <uploaded_files> tag, we return the parsed files from the content for backward compatibility.
|
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 parseUploadedFiles(rawContent);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -596,7 +596,10 @@ export interface FileInMessage {
|
|||||||
*/
|
*/
|
||||||
export function stripUploadedFilesTag(content: string): string {
|
export function stripUploadedFilesTag(content: string): string {
|
||||||
return content
|
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();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -606,7 +609,8 @@ export function stripUploadedFilesTag(content: string): string {
|
|||||||
*
|
*
|
||||||
* These markers are *not* user copy — they come from:
|
* 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>``
|
* - ``SkillActivationMiddleware`` → ``<slash_skill_activation>``
|
||||||
* - ``DynamicContextMiddleware`` → ``<system-reminder>`` (carrying
|
* - ``DynamicContextMiddleware`` → ``<system-reminder>`` (carrying
|
||||||
* ``<memory>`` / ``<current_date>`` inside)
|
* ``<memory>`` / ``<current_date>`` inside)
|
||||||
@ -620,6 +624,7 @@ export function stripUploadedFilesTag(content: string): string {
|
|||||||
* its ``hide_from_ui`` flag set.
|
* its ``hide_from_ui`` flag set.
|
||||||
*/
|
*/
|
||||||
export const INTERNAL_MARKER_TAGS = [
|
export const INTERNAL_MARKER_TAGS = [
|
||||||
|
"current_uploads",
|
||||||
"uploaded_files",
|
"uploaded_files",
|
||||||
"slash_skill_activation",
|
"slash_skill_activation",
|
||||||
"system-reminder",
|
"system-reminder",
|
||||||
@ -646,9 +651,32 @@ export function stripInternalMarkers(content: string): string {
|
|||||||
return content.replace(INTERNAL_MARKER_RE, "").trim();
|
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[] {
|
export function parseUploadedFiles(content: string): FileInMessage[] {
|
||||||
// Match <uploaded_files>...</uploaded_files> tag
|
// Match the upload context block; the tag name depends on backend version
|
||||||
const uploadedFilesRegex = /<uploaded_files>([\s\S]*?)<\/uploaded_files>/;
|
// (<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
|
// eslint-disable-next-line @typescript-eslint/prefer-regexp-exec
|
||||||
const match = content.match(uploadedFilesRegex);
|
const match = content.match(uploadedFilesRegex);
|
||||||
|
|
||||||
@ -656,7 +684,7 @@ export function parseUploadedFiles(content: string): FileInMessage[] {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadedFilesContent = match[1];
|
const uploadedFilesContent = match[2];
|
||||||
|
|
||||||
// Check if it's "No files have been uploaded yet."
|
// Check if it's "No files have been uploaded yet."
|
||||||
if (uploadedFilesContent?.includes("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) {
|
while ((fileMatch = fileRegex.exec(uploadedFilesContent ?? "")) !== null) {
|
||||||
files.push({
|
files.push({
|
||||||
filename: fileMatch[1].trim(),
|
filename: fileMatch[1].trim(),
|
||||||
size: parseInt(fileMatch[2].trim(), 10) ?? 0,
|
size: parseHumanReadableSize(fileMatch[2]),
|
||||||
path: fileMatch[3].trim(),
|
path: fileMatch[3].trim(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,8 @@ import {
|
|||||||
hasContent,
|
hasContent,
|
||||||
hasReasoning,
|
hasReasoning,
|
||||||
isAssistantMessageGroupStreaming,
|
isAssistantMessageGroupStreaming,
|
||||||
|
parseUploadedFiles,
|
||||||
|
stripInternalMarkers,
|
||||||
stripUploadedFilesTag,
|
stripUploadedFilesTag,
|
||||||
} from "@/core/messages/utils";
|
} from "@/core/messages/utils";
|
||||||
|
|
||||||
@ -307,6 +309,47 @@ describe("human message internal context stripping", () => {
|
|||||||
expect(getMessageCopyData(message)).toBe("Summarize this paper");
|
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", () => {
|
test("strips slash skill activation context from display content", () => {
|
||||||
const content =
|
const content =
|
||||||
"<slash_skill_activation>\n<skill_content># Secret SKILL.md</skill_content>\n</slash_skill_activation>\nreal user task";
|
"<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