修正 Markdown 代码块 HTML 转义
Project CI / Repository checks (pull_request) Failing after 2m46s
Project CI / Frontend tests (pull_request) Failing after 2m53s
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Failing after 5m30s

移除整段 Markdown 的 HTML 预转义并继续使用 skipHtml

补充行内与围栏代码 HTML 字面量回归测试

同步项目开发工作台安全 Markdown 渲染约定
This commit is contained in:
2026-09-04 16:27:57 +08:00
parent 7b3b050b47
commit dee5753543
3 changed files with 26 additions and 8 deletions
@@ -200,12 +200,6 @@ const streamingMarkdownComponents: Components = {
),
};
function escapeHtmlTagsForDisplay(text: string) {
return text.replace(/<\/?[A-Za-z][^>]*>/gu, (tag) =>
tag.replaceAll('<', '&lt;').replaceAll('>', '&gt;'),
);
}
export function ChatMarkdownMessage({
text,
role,
@@ -224,7 +218,7 @@ export function ChatMarkdownMessage({
streaming ? streamingMarkdownComponents : markdownComponents
}
>
{escapeHtmlTagsForDisplay(text)}
{text}
</ReactMarkdown>
</MarkdownErrorBoundary>
);
@@ -105,6 +105,30 @@ describe('ChatMarkdownMessage', () => {
expect(code?.className).not.toContain('rounded');
});
it('保留行内代码中的 HTML 字面量', () => {
const { container } = render(
<ChatMarkdownMessage role="assistant" text={'`<span>`'} />,
);
expect(container.querySelector('code')?.textContent).toBe('<span>');
});
it('保留围栏代码中的 HTML 字面量而不双重转义', () => {
const { container } = render(
<ChatMarkdownMessage
role="assistant"
text={'```html\n<div class="card"></div>\n```'}
/>,
);
expect(container.querySelector('pre code')?.textContent).toContain(
'<div class="card"></div>',
);
expect(container.querySelector('pre code')?.textContent).not.toContain(
'&lt;div',
);
});
it('累计文本变化后可从 Markdown 错误回退中恢复', () => {
const consoleError = vi
.spyOn(console, 'error')
@@ -185,7 +185,7 @@ completed -> starting(nextSlice)
idle -> focused(document|art|audio|version) -> idle
```
- 文档:合法 Agent 文本回执直接使用对话投影内容;项目文件只允许读取当前 manifest 已登记资产或已完成任务产物中的 Markdown、文本、JSON、YAML、TOML,必须经过 `file.read` auto 权限、相对路径、项目边界、普通文件、符号链接 / 硬链接、读取漂移、2 MiB、UTF-8 与扩展名白名单校验。正文使用不执行 HTML、不加载远程图片、不产生可点击外链的安全 Markdown 渲染,并在中央画布内独立滚动;读取失败显示错误空态。
- 文档:合法 Agent 文本回执直接使用对话投影内容;项目文件只允许读取当前 manifest 已登记资产或已完成任务产物中的 Markdown、文本、JSON、YAML、TOML,必须经过 `file.read` auto 权限、相对路径、项目边界、普通文件、符号链接 / 硬链接、读取漂移、2 MiB、UTF-8 与扩展名白名单校验。正文使用不执行 HTML、不加载远程图片、不产生可点击外链的安全 Markdown 渲染,并在中央画布内独立滚动;读取失败显示错误空态。Markdown 渲染使用 `react-markdown``skipHtml`,依赖库对代码 span / fenced code 的文本转义;不得在整段 Markdown 上预转义 HTML,否则会把代码中的 `<tag>` 双重转义为字面量 `&lt;tag&gt;`
- 美术:PNG、JPEG、WEBP、GIF、SVG、AVIF、BMP、MP4、WebM、MOV 只在资源卡本体中按既有受控读取、文件签名与解码门禁展示;中央详情不重复加载或放大图片 / 视频本体。SVG 继续拒绝脚本、事件处理器、外部资源引用和实体声明。
- 音频:只读取 manifest 已登记音频或已成功导入且登记到 manifest 的附件,按文件签名接受 MP3、WAV、OGG / Opus、M4A、AAC、FLAC;聚焦态展示实际格式、浏览器解码后的时长以及带播放进度和暂停能力的内置播放器。音频任务声明中的未登记路径继续不得读取或播放。
- 版本:只展示 manifest 中正式、不可变的迭代版本记录;版本卡展示项目修订、创建原因与父版本,聚焦态同时展示直接子版本和资源绑定。点击版本卡后高亮仍存在于当前资源投影中的引用资源;缺失历史资源只保留绑定身份,不生成幽灵资源卡。资源替换仍留给后续切片。