mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-08 16:38:01 +08:00
Fix: handle unpaired <> in messages (#17983)
This commit is contained in:
@@ -21,11 +21,13 @@ import { useFetchDocumentThumbnailsByIds } from '@/hooks/use-document-request';
|
||||
import { useLoadingPause } from '@/hooks/use-loading-pause';
|
||||
import {
|
||||
currentReg,
|
||||
escapeUnmatchedAngleBrackets,
|
||||
parseCitationIndex,
|
||||
preprocessLaTeX,
|
||||
replaceRetrievingToSection,
|
||||
replaceTextByOldReg,
|
||||
replaceThinkToSection,
|
||||
unescapeAngleBrackets,
|
||||
} from '@/utils/chat';
|
||||
import classNames from 'classnames';
|
||||
import { omit } from 'lodash';
|
||||
@@ -65,7 +67,11 @@ const MarkdownContent = ({
|
||||
const { setDocumentIds, data: fileThumbnails } =
|
||||
useFetchDocumentThumbnailsByIds();
|
||||
const contentWithCursor = useMemo(() => {
|
||||
let text = DOMPurify.sanitize(content, {
|
||||
// Escape standalone < and > outside matched <...> tags
|
||||
// so DOMPurify doesn't strip them as HTML.
|
||||
const safeContent = escapeUnmatchedAngleBrackets(content);
|
||||
|
||||
let text = DOMPurify.sanitize(safeContent, {
|
||||
ADD_TAGS: ['think', 'section', 'details', 'summary', 'retrieving'],
|
||||
ADD_ATTR: ['class'],
|
||||
});
|
||||
@@ -78,11 +84,13 @@ const MarkdownContent = ({
|
||||
const thinkSummary = loading
|
||||
? `${t('chat.thinking')}...`
|
||||
: t('chat.thought');
|
||||
return pipe(
|
||||
(value: string) => replaceThinkToSection(value, thinkSummary),
|
||||
replaceRetrievingToSection,
|
||||
preprocessLaTeX,
|
||||
)(nextText);
|
||||
return unescapeAngleBrackets(
|
||||
pipe(
|
||||
(value: string) => replaceThinkToSection(value, thinkSummary),
|
||||
replaceRetrievingToSection,
|
||||
preprocessLaTeX,
|
||||
)(nextText),
|
||||
);
|
||||
}, [content, loading, t]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -19,11 +19,13 @@ import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for
|
||||
|
||||
import {
|
||||
currentReg,
|
||||
escapeUnmatchedAngleBrackets,
|
||||
parseCitationIndex,
|
||||
preprocessLaTeX,
|
||||
replaceRetrievingToSection,
|
||||
replaceTextByOldReg,
|
||||
replaceThinkToSection,
|
||||
unescapeAngleBrackets,
|
||||
} from '@/utils/chat';
|
||||
import { citationMarkerReg } from '@/utils/citation-utils';
|
||||
import { getDirAttribute } from '@/utils/text-direction';
|
||||
@@ -172,7 +174,11 @@ function MarkdownContent({
|
||||
const { setDocumentIds, data: fileThumbnails } =
|
||||
useFetchDocumentThumbnailsByIds();
|
||||
const contentWithCursor = useMemo(() => {
|
||||
let text = DOMPurify.sanitize(content, {
|
||||
// Escape standalone < and > outside matched <...> tags
|
||||
// so DOMPurify doesn't strip them as HTML.
|
||||
const safeContent = escapeUnmatchedAngleBrackets(content);
|
||||
|
||||
let text = DOMPurify.sanitize(safeContent, {
|
||||
ADD_TAGS: ['think', 'section', 'details', 'summary', 'retrieving'],
|
||||
ADD_ATTR: ['class'],
|
||||
});
|
||||
@@ -184,11 +190,13 @@ function MarkdownContent({
|
||||
const thinkSummary = loading
|
||||
? `${t('chat.thinking')}...`
|
||||
: t('chat.thought');
|
||||
return pipe(
|
||||
(value: string) => replaceThinkToSection(value, thinkSummary),
|
||||
replaceRetrievingToSection,
|
||||
preprocessLaTeX,
|
||||
)(nextText);
|
||||
return unescapeAngleBrackets(
|
||||
pipe(
|
||||
(value: string) => replaceThinkToSection(value, thinkSummary),
|
||||
replaceRetrievingToSection,
|
||||
preprocessLaTeX,
|
||||
)(nextText),
|
||||
);
|
||||
}, [content, loading, t]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -17,11 +17,13 @@ import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for
|
||||
|
||||
import {
|
||||
currentReg,
|
||||
escapeUnmatchedAngleBrackets,
|
||||
parseCitationIndex,
|
||||
preprocessLaTeX,
|
||||
replaceRetrievingToSection,
|
||||
replaceTextByOldReg,
|
||||
replaceThinkToSection,
|
||||
unescapeAngleBrackets,
|
||||
} from '@/utils/chat';
|
||||
import { citationMarkerReg } from '@/utils/citation-utils';
|
||||
import { getDirAttribute } from '@/utils/text-direction';
|
||||
@@ -66,7 +68,11 @@ const MarkdownContent = ({
|
||||
const { setDocumentIds, data: fileThumbnails } =
|
||||
useFetchDocumentThumbnailsByIds();
|
||||
const contentWithCursor = useMemo(() => {
|
||||
let text = DOMPurify.sanitize(content, {
|
||||
// Escape standalone < and > outside matched <...> tags
|
||||
// so DOMPurify doesn't strip them as HTML.
|
||||
const safeContent = escapeUnmatchedAngleBrackets(content);
|
||||
|
||||
let text = DOMPurify.sanitize(safeContent, {
|
||||
ADD_TAGS: ['think', 'section', 'details', 'summary', 'retrieving'],
|
||||
ADD_ATTR: ['class'],
|
||||
});
|
||||
@@ -75,11 +81,13 @@ const MarkdownContent = ({
|
||||
text = t('chat.searching');
|
||||
}
|
||||
const nextText = replaceTextByOldReg(text);
|
||||
return pipe(
|
||||
replaceThinkToSection,
|
||||
replaceRetrievingToSection,
|
||||
preprocessLaTeX,
|
||||
)(nextText);
|
||||
return unescapeAngleBrackets(
|
||||
pipe(
|
||||
replaceThinkToSection,
|
||||
replaceRetrievingToSection,
|
||||
preprocessLaTeX,
|
||||
)(nextText),
|
||||
);
|
||||
}, [content, t]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -103,6 +103,56 @@ export function replaceRetrievingToSection(text: string = '') {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Placeholder markers used internally to protect standalone < and > from
|
||||
// DOMPurify stripping. These Unicode symbols (U+27E8/U+27E9) are extremely
|
||||
// unlikely to appear in normal user input.
|
||||
const LT_MARKER = '\u27E8LT\u27E9';
|
||||
const GT_MARKER = '\u27E8GT\u27E9';
|
||||
|
||||
/**
|
||||
* Escape standalone < and > that are NOT part of a matched <...> pair,
|
||||
* so that DOMPurify won't strip them as HTML tags.
|
||||
* Only brackets inside text segments (outside complete tags) are escaped;
|
||||
* matched <...> tags are left intact for DOMPurify to handle.
|
||||
*/
|
||||
export function escapeUnmatchedAngleBrackets(content: string): string {
|
||||
if (!content) return content;
|
||||
|
||||
const segments: string[] = [];
|
||||
const tags: string[] = [];
|
||||
let lastIndex = 0;
|
||||
|
||||
const regex = /<[^>]*>/g;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = regex.exec(content)) !== null) {
|
||||
segments.push(content.slice(lastIndex, match.index));
|
||||
tags.push(match[0]);
|
||||
lastIndex = regex.lastIndex;
|
||||
}
|
||||
segments.push(content.slice(lastIndex));
|
||||
|
||||
const escapedSegments = segments.map((seg) =>
|
||||
seg.replace(/</g, LT_MARKER).replace(/>/g, GT_MARKER),
|
||||
);
|
||||
|
||||
return escapedSegments
|
||||
.map((seg, i) => (i < tags.length ? seg + tags[i] : seg))
|
||||
.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore escaped angle bracket markers back to HTML entities (</>).
|
||||
* Must be called *after* preprocessLaTeX (which would otherwise convert
|
||||
* </> back to raw <, >).
|
||||
*/
|
||||
export function unescapeAngleBrackets(content: string): string {
|
||||
if (!content) return content;
|
||||
return content
|
||||
.replace(new RegExp(LT_MARKER, 'g'), '<')
|
||||
.replace(new RegExp(GT_MARKER, 'g'), '>');
|
||||
}
|
||||
|
||||
export function setInitialChatVariableEnabledFieldValue(
|
||||
field: ChatVariableEnabledField,
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user