diff --git a/apps/ai-game-creator-shell/src/components/ChatMarkdownMessage/index.tsx b/apps/ai-game-creator-shell/src/components/ChatMarkdownMessage/index.tsx index 057860d7c..00b46b658 100644 --- a/apps/ai-game-creator-shell/src/components/ChatMarkdownMessage/index.tsx +++ b/apps/ai-game-creator-shell/src/components/ChatMarkdownMessage/index.tsx @@ -200,12 +200,6 @@ const streamingMarkdownComponents: Components = { ), }; -function escapeHtmlTagsForDisplay(text: string) { - return text.replace(/<\/?[A-Za-z][^>]*>/gu, (tag) => - tag.replaceAll('<', '<').replaceAll('>', '>'), - ); -} - export function ChatMarkdownMessage({ text, role, @@ -224,7 +218,7 @@ export function ChatMarkdownMessage({ streaming ? streamingMarkdownComponents : markdownComponents } > - {escapeHtmlTagsForDisplay(text)} + {text} ); diff --git a/apps/ai-game-creator-shell/tests/ChatMarkdownMessage.test.tsx b/apps/ai-game-creator-shell/tests/ChatMarkdownMessage.test.tsx index 149b4d25c..ee328f5bc 100644 --- a/apps/ai-game-creator-shell/tests/ChatMarkdownMessage.test.tsx +++ b/apps/ai-game-creator-shell/tests/ChatMarkdownMessage.test.tsx @@ -105,6 +105,30 @@ describe('ChatMarkdownMessage', () => { expect(code?.className).not.toContain('rounded'); }); + it('保留行内代码中的 HTML 字面量', () => { + const { container } = render( + `'} />, + ); + + expect(container.querySelector('code')?.textContent).toBe(''); + }); + + it('保留围栏代码中的 HTML 字面量而不双重转义', () => { + const { container } = render( + \n```'} + />, + ); + + expect(container.querySelector('pre code')?.textContent).toContain( + '
', + ); + expect(container.querySelector('pre code')?.textContent).not.toContain( + '<div', + ); + }); + it('累计文本变化后可从 Markdown 错误回退中恢复', () => { const consoleError = vi .spyOn(console, 'error') diff --git a/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md b/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md index 13bb6403e..66622a6a9 100644 --- a/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md +++ b/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md @@ -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>`。 - 美术:PNG、JPEG、WEBP、GIF、SVG、AVIF、BMP、MP4、WebM、MOV 只在资源卡本体中按既有受控读取、文件签名与解码门禁展示;中央详情不重复加载或放大图片 / 视频本体。SVG 继续拒绝脚本、事件处理器、外部资源引用和实体声明。 - 音频:只读取 manifest 已登记音频或已成功导入且登记到 manifest 的附件,按文件签名接受 MP3、WAV、OGG / Opus、M4A、AAC、FLAC;聚焦态展示实际格式、浏览器解码后的时长以及带播放进度和暂停能力的内置播放器。音频任务声明中的未登记路径继续不得读取或播放。 - 版本:只展示 manifest 中正式、不可变的迭代版本记录;版本卡展示项目修订、创建原因与父版本,聚焦态同时展示直接子版本和资源绑定。点击版本卡后高亮仍存在于当前资源投影中的引用资源;缺失历史资源只保留绑定身份,不生成幽灵资源卡。资源替换仍留给后续切片。