From a9bd03ef572a4898ea1ba977e906408dc2f28d7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=80=E7=89=87=E4=B8=80=E7=89=87?= <3037678834@qq.com> Date: Sat, 31 Oct 2026 23:00:39 +0800 Subject: [PATCH] fix(citations): keep copied markdown references valid (#7743) * fix(citations): keep copied markdown references valid formatCitationMarkdownReference pasted the source title and URL into "[title](url)" verbatim. A title with a stray "[" or "]" ends the link text early, and a URL with an unbalanced parenthesis ends the destination, so a copied reference no longer rendered as a link. Escape brackets in the title and wrap a parenthesized URL in the angle-bracket form. * fix(citations): only angle-bracket a parenthesized URL normalizeUrl already percent-encodes "<", ">" and whitespace, so including them in the trigger could wrap such a URL in an angle-bracket destination that CommonMark forbids, which is worse than the bare form. Parentheses are the only reachable delimiter that breaks the bare destination. --- README.md | 5 +++- frontend/src/core/citations/sources.ts | 8 +++++- .../tests/unit/core/citations/sources.test.ts | 26 +++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ce39624736..1f097e2c36 100644 --- a/README.md +++ b/README.md @@ -1708,7 +1708,10 @@ provider, document title, and retrieved excerpt in chat, including after reload and when a subagent performed the retrieval. Missing page numbers and public URLs remain absent; sources without a URL are readable as local evidence. Inline citations remain readable after Markdown sanitization, and supplied page -numbers use the selected interface language. New providers use the shared +numbers use the selected interface language. +Copying a source reference produces a Markdown link that escapes brackets in the +visible title and wraps a URL containing parentheses in angle brackets, so the +copied reference stays a valid link. New providers use the shared envelope; existing RAGFlow citation histories remain supported. Expand a completed subtask card to inspect its report's forwarded knowledge sources, including when reopening a saved conversation. diff --git a/frontend/src/core/citations/sources.ts b/frontend/src/core/citations/sources.ts index 63813e6936..21d6840464 100644 --- a/frontend/src/core/citations/sources.ts +++ b/frontend/src/core/citations/sources.ts @@ -206,7 +206,13 @@ export function extractCitationSources( export function formatCitationMarkdownReference( source: CitationSource, ): string { - return `[${source.title}](${source.url})`; + // Link text may contain balanced brackets, but a stray "[" or "]" would end + // it early; escape both so a copied reference keeps the visible title. + const title = source.title.replace(/([\\[\]])/g, "\\$1"); + // A bare link destination stops at an unbalanced parenthesis. The + // angle-bracket form keeps URLs with parentheses valid. + const url = /[()]/.test(source.url) ? `<${source.url}>` : source.url; + return `[${title}](${url})`; } function normalizeTitle(title: string, domain: string): string { diff --git a/frontend/tests/unit/core/citations/sources.test.ts b/frontend/tests/unit/core/citations/sources.test.ts index bba339ed49..aaeba36c5c 100644 --- a/frontend/tests/unit/core/citations/sources.test.ts +++ b/frontend/tests/unit/core/citations/sources.test.ts @@ -1042,4 +1042,30 @@ describe("formatCitationMarkdownReference", () => { "[Paper A](https://example.com/a)", ); }); + + it("escapes brackets in the title so the copied link keeps its label", () => { + const [source] = extractCitationSources( + "Evidence [citation:Paper A](https://example.com/a).", + ); + + expect( + formatCitationMarkdownReference({ + ...source!, + title: "Why ] matters [draft", + }), + ).toBe("[Why \\] matters \\[draft](https://example.com/a)"); + }); + + it("wraps a URL with parentheses so the destination stays intact", () => { + const [source] = extractCitationSources( + "Evidence [citation:Paper A](https://example.com/a).", + ); + + expect( + formatCitationMarkdownReference({ + ...source!, + url: "https://example.com/a)b(c", + }), + ).toBe("[Paper A]()"); + }); });