mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-06-13 10:55:59 +00:00
fix(frontend): render user messages as plain text and cap blockquote nesting (#3502)
* fix(frontend): render user messages as plain text and cap blockquote nesting User messages are typed or pasted plain text, not authored Markdown, but they were rendered through the full Streamdown pipeline. Pasted source files got fragmented (indented chunks become code blocks, paragraphs collapse and lose indentation), "$...$" spans were KaTeX-ified, and a message with thousands of nested ">" markers overflowed the call stack in marked's recursive blockquote lexer, permanently crashing the thread on every load. Render human message content verbatim with pre-wrap instead, and cap blockquote nesting at 100 levels at the Streamdown chokepoint so model output cannot trigger the same recursion either. Closes #3500 * fix(frontend): absorb marked lexer crashes with a render fallback boundary Review found two gaps in the nesting cap: marked's list and blockquote tokenizers are mutually recursive, so a list marker in front of the quote chain ("- > > > ...") bypassed the blockquote-only regex and still overflowed the stack; and the line-based rewrite was fence-blind, silently truncating literal ">" runs inside code blocks. Add an error boundary around Streamdown that renders the raw content as plain pre-wrap text when rendering throws (retrying on the next content change), keep the cap as a fast path for the dominant pure-">" case, and make it skip fenced and indented code lines.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
import {
|
||||
capBlockquoteNesting,
|
||||
preprocessStreamdownMarkdown,
|
||||
} from "@/core/streamdown/preprocess";
|
||||
|
||||
test("capBlockquoteNesting returns normal content unchanged", () => {
|
||||
const input = "# Title\n\n> a quote\n>> nested\n\nsome `code`";
|
||||
expect(capBlockquoteNesting(input)).toBe(input);
|
||||
});
|
||||
|
||||
test("capBlockquoteNesting keeps nesting at or below the cap untouched", () => {
|
||||
const input = "> ".repeat(100) + "hi";
|
||||
expect(capBlockquoteNesting(input)).toBe(input);
|
||||
});
|
||||
|
||||
test("capBlockquoteNesting caps pathological nesting and preserves content", () => {
|
||||
const result = capBlockquoteNesting("> ".repeat(5000) + "hi");
|
||||
expect((result.match(/>/g) ?? []).length).toBe(100);
|
||||
expect(result.endsWith("hi")).toBe(true);
|
||||
});
|
||||
|
||||
test("capBlockquoteNesting handles markers without spaces", () => {
|
||||
const result = capBlockquoteNesting(">".repeat(5000) + "hi");
|
||||
expect((result.match(/>/g) ?? []).length).toBe(100);
|
||||
expect(result.endsWith("hi")).toBe(true);
|
||||
});
|
||||
|
||||
test("capBlockquoteNesting leaves fenced code content untouched", () => {
|
||||
const literal = ">".repeat(150);
|
||||
const input = `${"> ".repeat(3000)}hi\n\`\`\`text\n${literal}\n\`\`\``;
|
||||
const result = capBlockquoteNesting(input);
|
||||
expect(result.split("\n")[2]).toBe(literal);
|
||||
});
|
||||
|
||||
test("capBlockquoteNesting leaves indented code blocks untouched", () => {
|
||||
const literal = " " + ">".repeat(150);
|
||||
const input = `${"> ".repeat(3000)}hi\n\n${literal}`;
|
||||
const result = capBlockquoteNesting(input);
|
||||
expect(result.split("\n")[2]).toBe(literal);
|
||||
});
|
||||
|
||||
test("capBlockquoteNesting only rewrites pathological lines", () => {
|
||||
const normal = "> normal quote";
|
||||
const deep = "> ".repeat(3000) + "deep";
|
||||
const result = capBlockquoteNesting(`${normal}\n${deep}\nplain`);
|
||||
const lines = result.split("\n");
|
||||
expect(lines[0]).toBe(normal);
|
||||
expect((lines[1]?.match(/>/g) ?? []).length).toBe(100);
|
||||
expect(lines[2]).toBe("plain");
|
||||
});
|
||||
|
||||
test("preprocessStreamdownMarkdown leaves non-mermaid content unchanged", () => {
|
||||
const input = "just some text";
|
||||
expect(preprocessStreamdownMarkdown(input)).toBe(input);
|
||||
});
|
||||
Reference in New Issue
Block a user