Skip to content

Commit a38b2c1

Browse files
authored
fix: 🧩 修复 md 渲染时会把 `` 格式渲染成代码块问题 (#281)
* fix: 🧩 修复 md 渲染时会把 `` 格式渲染成代码块问题 * feat: 增加 URL 安全验证以防止 XSS 攻击,并优化 Markdown 组件配置
1 parent 99ad0b4 commit a38b2c1

File tree

5 files changed

+418
-381
lines changed

5 files changed

+418
-381
lines changed
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import React from 'react';
2+
import type { Components } from 'react-markdown';
3+
4+
import SyntaxHighlight from '@/extensions/AI/components/SyntaxHighlight';
5+
6+
/**
7+
* URL 安全验证:防止 XSS 攻击
8+
* 只允许 http、https、mailto 协议的链接
9+
*/
10+
const isValidUrl = (url: string | undefined): boolean => {
11+
if (!url) return false;
12+
13+
try {
14+
const parsed = new URL(url, window.location.href);
15+
16+
return ['http:', 'https:', 'mailto:'].includes(parsed.protocol);
17+
} catch {
18+
return false;
19+
}
20+
};
21+
22+
/**
23+
* 统一的 ReactMarkdown 组件配置
24+
* 修复代码块渲染问题:正确区分行内代码和代码块
25+
*/
26+
export const markdownComponents: Components = {
27+
// 代码渲染:区分行内代码和代码块
28+
code: ({ className, children, ...props }) => {
29+
// 行内代码:单反引号,没有 className
30+
if (!className) {
31+
return (
32+
<code
33+
className="px-1.5 py-0.5 bg-gray-200 text-gray-800 rounded text-sm font-mono"
34+
{...props}
35+
>
36+
{children}
37+
</code>
38+
);
39+
}
40+
41+
// 代码块:三反引号,使用语法高亮
42+
return (
43+
<SyntaxHighlight className={className} {...props}>
44+
{children}
45+
</SyntaxHighlight>
46+
);
47+
},
48+
49+
// pre 标签配置
50+
pre: ({ children, className, ...props }) => (
51+
<pre className={`rounded ${className || ''}`} {...props}>
52+
{children}
53+
</pre>
54+
),
55+
56+
// 段落
57+
p: ({ children }) => <p className="mb-2 last:mb-0">{children}</p>,
58+
59+
// 标题
60+
h1: ({ children }) => <h1 className="text-2xl font-bold mb-2 text-gray-900">{children}</h1>,
61+
h2: ({ children }) => <h2 className="text-xl font-semibold mb-2 text-gray-800">{children}</h2>,
62+
h3: ({ children }) => <h3 className="text-lg font-medium mb-1.5 text-gray-700">{children}</h3>,
63+
h4: ({ children }) => <h4 className="text-base font-medium mb-1 text-gray-700">{children}</h4>,
64+
h5: ({ children }) => <h5 className="text-sm font-medium mb-1 text-gray-600">{children}</h5>,
65+
h6: ({ children }) => <h6 className="text-sm font-normal mb-1 text-gray-600">{children}</h6>,
66+
67+
// 列表
68+
ul: ({ children }) => <ul className="list-disc pl-4 mb-2 space-y-1">{children}</ul>,
69+
ol: ({ children }) => <ol className="list-decimal pl-4 mb-2 space-y-1">{children}</ol>,
70+
li: ({ children }) => <li className="text-sm">{children}</li>,
71+
72+
// 强调
73+
strong: ({ children }) => <strong className="font-semibold text-gray-800">{children}</strong>,
74+
em: ({ children }) => <em className="italic text-gray-700">{children}</em>,
75+
76+
// 引用
77+
blockquote: ({ children }) => (
78+
<blockquote className="border-l-4 border-gray-300 pl-4 italic text-gray-600 my-2">
79+
{children}
80+
</blockquote>
81+
),
82+
83+
// 链接 - 带 XSS 防护
84+
a: ({ children, href }) => {
85+
const isSafe = href && isValidUrl(href);
86+
87+
return (
88+
<a
89+
href={isSafe ? href : '#'}
90+
className="text-blue-600 hover:text-blue-700 underline text-[12px] font-medium"
91+
target="_blank"
92+
rel="noopener noreferrer"
93+
>
94+
{children}
95+
</a>
96+
);
97+
},
98+
99+
// 表格
100+
table: ({ children }) => (
101+
<table className="border-collapse border border-gray-300 my-2 w-full">{children}</table>
102+
),
103+
thead: ({ children }) => <thead className="bg-gray-100">{children}</thead>,
104+
tbody: ({ children }) => <tbody>{children}</tbody>,
105+
tr: ({ children }) => <tr className="border-b border-gray-300">{children}</tr>,
106+
th: ({ children }) => (
107+
<th className="border border-gray-300 px-3 py-2 text-left font-semibold">{children}</th>
108+
),
109+
td: ({ children }) => <td className="border border-gray-300 px-3 py-2">{children}</td>,
110+
};
111+
112+
/**
113+
* 紧凑版的 ReactMarkdown 组件配置(用于头脑风暴等空间受限的场景)
114+
*/
115+
export const compactMarkdownComponents: Components = {
116+
code: markdownComponents.code,
117+
pre: markdownComponents.pre,
118+
119+
p: ({ children }) => <p className="mb-1 last:mb-0 text-[12px] leading-relaxed">{children}</p>,
120+
121+
h1: ({ children }) => (
122+
<h1 className="text-[12px] font-bold mb-0.5 text-gray-900 leading-tight">{children}</h1>
123+
),
124+
h2: ({ children }) => (
125+
<h2 className="text-[12px] font-semibold mb-0.5 text-gray-800 leading-tight">{children}</h2>
126+
),
127+
h3: ({ children }) => (
128+
<h3 className="text-[12px] font-medium mb-0.5 text-gray-700 leading-tight">{children}</h3>
129+
),
130+
h4: ({ children }) => (
131+
<h4 className="text-[12px] font-normal mb-0.5 text-gray-700 leading-tight">{children}</h4>
132+
),
133+
h5: ({ children }) => (
134+
<h5 className="text-[11px] font-normal mb-0.5 text-gray-600 leading-tight">{children}</h5>
135+
),
136+
h6: ({ children }) => (
137+
<h6 className="text-[11px] font-normal mb-0.5 text-gray-600 leading-tight">{children}</h6>
138+
),
139+
140+
ul: ({ children }) => <ul className="list-disc pl-3 mb-1 space-y-0.5 text-[12px]">{children}</ul>,
141+
ol: ({ children }) => (
142+
<ol className="list-decimal pl-3 mb-1 space-y-0.5 text-[12px]">{children}</ol>
143+
),
144+
li: ({ children }) => <li className="text-[12px] leading-relaxed">{children}</li>,
145+
146+
strong: ({ children }) => <strong className="font-semibold text-gray-800">{children}</strong>,
147+
em: ({ children }) => <em className="italic text-gray-700">{children}</em>,
148+
149+
blockquote: ({ children }) => (
150+
<blockquote className="border-l-2 border-gray-300 pl-2 italic text-gray-600 text-[12px] my-1 bg-gray-50/50 py-0.5 rounded-r">
151+
{children}
152+
</blockquote>
153+
),
154+
155+
// 链接 - 带 XSS 防护
156+
a: ({ children, href }) => {
157+
const isSafe = href && isValidUrl(href);
158+
159+
return (
160+
<a
161+
href={isSafe ? href : '#'}
162+
className="text-blue-600 hover:text-blue-700 underline text-[12px] font-medium"
163+
target="_blank"
164+
rel="noopener noreferrer"
165+
>
166+
{children}
167+
</a>
168+
);
169+
},
170+
171+
table: ({ children }) => (
172+
<table className="border-collapse border border-gray-300 text-[12px] my-1.5 rounded overflow-hidden shadow-sm">
173+
{children}
174+
</table>
175+
),
176+
thead: ({ children }) => <thead className="bg-gray-100">{children}</thead>,
177+
tbody: ({ children }) => <tbody>{children}</tbody>,
178+
tr: ({ children }) => <tr className="border-b border-gray-300">{children}</tr>,
179+
th: ({ children }) => (
180+
<th className="border border-gray-300 px-2 py-1 text-left font-semibold">{children}</th>
181+
),
182+
td: ({ children }) => <td className="border border-gray-300 px-2 py-1">{children}</td>,
183+
};

src/extensions/AIBrainstorm/AIBrainstormComponent.tsx

Lines changed: 3 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import ReactMarkdown from 'react-markdown';
88
import remarkGfm from 'remark-gfm';
99

1010
import ConcurrencySelector from '../AI/components/ConcurrencySelector';
11-
import SyntaxHighlight from '../AI/components/SyntaxHighlight';
1211

12+
import { compactMarkdownComponents } from '@/components/business/ai/markdown-components';
1313
import ModelSelector from '@/components/business/module-select';
1414
import { ChatAiApi, type StreamChunk } from '@/services/chat-ai';
1515

@@ -345,77 +345,7 @@ export const AIBrainstormComponent: React.FC<AIBrainstormComponentProps> = ({
345345
<div className="markdown-content text-[12px] leading-relaxed text-gray-700">
346346
<ReactMarkdown
347347
remarkPlugins={[remarkGfm]}
348-
components={{
349-
code: SyntaxHighlight,
350-
pre: ({ children, className, ...props }: any) => (
351-
<pre className={`rounded text-[12px] ${className || ''}`} {...props}>
352-
{children}
353-
</pre>
354-
),
355-
p: ({ children }) => (
356-
<p className="mb-1 last:mb-0 text-[12px] leading-relaxed">{children}</p>
357-
),
358-
h1: ({ children }) => (
359-
<h1 className="text-[12px] font-bold mb-0.5 text-gray-900 leading-tight">
360-
{children}
361-
</h1>
362-
),
363-
h2: ({ children }) => (
364-
<h2 className="text-[12px] font-semibold mb-0.5 text-gray-800 leading-tight">
365-
{children}
366-
</h2>
367-
),
368-
h3: ({ children }) => (
369-
<h3 className="text-[12px] font-medium mb-0.5 text-gray-700 leading-tight">
370-
{children}
371-
</h3>
372-
),
373-
h4: ({ children }) => (
374-
<h4 className="text-[12px] font-normal mb-0.5 text-gray-700 leading-tight">
375-
{children}
376-
</h4>
377-
),
378-
h5: ({ children }) => (
379-
<h5 className="text-[11px] font-normal mb-0.5 text-gray-600 leading-tight">
380-
{children}
381-
</h5>
382-
),
383-
h6: ({ children }) => (
384-
<h6 className="text-[11px] font-normal mb-0.5 text-gray-600 leading-tight">
385-
{children}
386-
</h6>
387-
),
388-
ul: ({ children }) => (
389-
<ul className="list-disc pl-3 mb-1 space-y-0.5 text-[12px]">
390-
{children}
391-
</ul>
392-
),
393-
ol: ({ children }) => (
394-
<ol className="list-decimal pl-3 mb-1 space-y-0.5 text-[12px]">
395-
{children}
396-
</ol>
397-
),
398-
li: ({ children }) => (
399-
<li className="text-[12px] leading-relaxed">{children}</li>
400-
),
401-
strong: ({ children }) => (
402-
<strong className="font-semibold text-gray-800">{children}</strong>
403-
),
404-
em: ({ children }) => <em className="italic text-gray-700">{children}</em>,
405-
blockquote: ({ children }) => (
406-
<blockquote className="border-l-2 border-gray-300 pl-2 italic text-gray-600 text-[12px] my-1 bg-gray-50/50 py-0.5 rounded-r">
407-
{children}
408-
</blockquote>
409-
),
410-
a: ({ children, href }) => (
411-
<a
412-
href={href}
413-
className="text-blue-600 hover:text-blue-700 underline text-[12px] font-medium"
414-
>
415-
{children}
416-
</a>
417-
),
418-
}}
348+
components={compactMarkdownComponents}
419349
>
420350
{response.content || '✨ 生成中...'}
421351
</ReactMarkdown>
@@ -514,95 +444,7 @@ export const AIBrainstormComponent: React.FC<AIBrainstormComponentProps> = ({
514444
<div className="markdown-content text-[12px] leading-relaxed text-gray-700">
515445
<ReactMarkdown
516446
remarkPlugins={[remarkGfm]}
517-
components={{
518-
code: SyntaxHighlight,
519-
pre: ({ children, className, ...props }: any) => (
520-
<pre className={`rounded text-[12px] ${className || ''}`} {...props}>
521-
{children}
522-
</pre>
523-
),
524-
p: ({ children }) => (
525-
<p className="mb-1 last:mb-0 text-[12px] leading-relaxed">{children}</p>
526-
),
527-
h1: ({ children }) => (
528-
<h1 className="text-[12px] font-bold mb-0.5 text-gray-900 leading-tight">
529-
{children}
530-
</h1>
531-
),
532-
h2: ({ children }) => (
533-
<h2 className="text-[12px] font-semibold mb-0.5 text-gray-800 leading-tight">
534-
{children}
535-
</h2>
536-
),
537-
h3: ({ children }) => (
538-
<h3 className="text-[12px] font-medium mb-0.5 text-gray-700 leading-tight">
539-
{children}
540-
</h3>
541-
),
542-
h4: ({ children }) => (
543-
<h4 className="text-[12px] font-normal mb-0.5 text-gray-700 leading-tight">
544-
{children}
545-
</h4>
546-
),
547-
h5: ({ children }) => (
548-
<h5 className="text-[11px] font-normal mb-0.5 text-gray-600 leading-tight">
549-
{children}
550-
</h5>
551-
),
552-
h6: ({ children }) => (
553-
<h6 className="text-[11px] font-normal mb-0.5 text-gray-600 leading-tight">
554-
{children}
555-
</h6>
556-
),
557-
ul: ({ children }) => (
558-
<ul className="list-disc pl-3 mb-1 space-y-0.5 text-[12px]">
559-
{children}
560-
</ul>
561-
),
562-
ol: ({ children }) => (
563-
<ol className="list-decimal pl-3 mb-1 space-y-0.5 text-[12px]">
564-
{children}
565-
</ol>
566-
),
567-
li: ({ children }) => (
568-
<li className="text-[12px] leading-relaxed">{children}</li>
569-
),
570-
strong: ({ children }) => (
571-
<strong className="font-semibold text-gray-800">{children}</strong>
572-
),
573-
em: ({ children }) => <em className="italic text-gray-700">{children}</em>,
574-
blockquote: ({ children }) => (
575-
<blockquote className="border-l-2 border-gray-300 pl-2 italic text-gray-600 text-[12px] my-1 bg-gray-50/50 py-0.5 rounded-r">
576-
{children}
577-
</blockquote>
578-
),
579-
a: ({ children, href }) => (
580-
<a
581-
href={href}
582-
className="text-blue-600 hover:text-blue-700 underline text-[12px] font-medium"
583-
>
584-
{children}
585-
</a>
586-
),
587-
table: ({ children }) => (
588-
<table className="border-collapse border border-gray-300 text-[12px] my-1.5 rounded overflow-hidden shadow-sm">
589-
{children}
590-
</table>
591-
),
592-
thead: ({ children }) => <thead className="bg-gray-100">{children}</thead>,
593-
tbody: ({ children }) => <tbody>{children}</tbody>,
594-
tr: ({ children }) => (
595-
<tr className="border-b border-gray-300">{children}</tr>
596-
),
597-
th: ({ children }) => (
598-
<th className="border border-gray-300 px-2 py-1 text-left font-semibold">
599-
{children}
600-
</th>
601-
),
602-
td: ({ children }) => (
603-
<td className="border border-gray-300 px-2 py-1">{children}</td>
604-
),
605-
}}
447+
components={compactMarkdownComponents}
606448
>
607449
{response.content || '无内容'}
608450
</ReactMarkdown>

0 commit comments

Comments
 (0)