From 26eb32ea221d299cd19962ea3c4dbb6921db1958 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Thu, 24 Sep 2026 16:56:49 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8C=89=20review=20=E6=94=B6=E5=8F=A3?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=9D=A1=E4=B8=8E=E5=AF=B9=E8=AF=9D=20Markdo?= =?UTF-8?q?wn=20=E7=9A=84=E5=AE=B9=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - apps/ai-game-creator-shell/src/components/ChatMarkdownMessage/index.tsx 围栏归一化补「同一行出现第二段围栏串就跳过」判据:行内代码 `文本 ```x``` ` 的末尾那截曾被当成收场围栏拆开,凭空造出一个开场围栏、把后面的正文全变成代码 - apps/ai-game-creator-shell/src/components/ChatMarkdownMessage/index.tsx 注释补归一化顺序与行内代码判据 - apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectTurnStatus.ts 修正 displayBusy 的说明:原注释仍写着卡片只读 nativeRunning,与现状相反 - apps/ai-game-creator-shell/tests/ChatMarkdownMessage.test.tsx 新增行内代码用例(去掉新判据即红) - docs/project-memory/shared-memory/decision-log.md 新增 2026-09-24 条:记录卡片口径翻转(更正 2026-09-22「卡片口径取保守」那条)并处置其「running 在渲染层就没有消费者」的预言,同时记录状态条几何约束与对话 Markdown 容错口径 - docs/project-memory/shared-memory/pitfalls.md 围栏归一化条补行内代码判据 --- .../components/ChatMarkdownMessage/index.tsx | 22 ++++++++++++++----- .../controller/useDirectProjectTurnStatus.ts | 8 ++++--- .../tests/ChatMarkdownMessage.test.tsx | 9 ++++++++ .../shared-memory/decision-log.md | 10 +++++++++ docs/project-memory/shared-memory/pitfalls.md | 2 +- 5 files changed, 41 insertions(+), 10 deletions(-) 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 1a52578f0..bc7c55761 100644 --- a/apps/ai-game-creator-shell/src/components/ChatMarkdownMessage/index.tsx +++ b/apps/ai-game-creator-shell/src/components/ChatMarkdownMessage/index.tsx @@ -49,21 +49,30 @@ function normalizeMarkdownBlankLines(text: string) { * 别的字符(`const s = "```";`)也不会被拆开。 * * 引用块与列表项开头的围栏(`> ```js`、`- ```js`)是**合法结构**,不是粘住的:整行跳过, - * 拆开只会把它们从引用块 / 列表项里挪出来。 + * 拆开只会把它们从引用块 / 列表项里挪出来。同一行出现第二段围栏串(行内代码 + * ``文本 ```x``` ``)时同样跳过——末尾那截不是收场围栏。 */ const GLUED_FENCE_LINE = /([^\s`~])[ \t]*(`{3,}|~{3,})([A-Za-z0-9+#._-]*)[ \t]*$/; const BLOCK_MARKER_LINE = /^\s*(?:[-*+]|\d+[.)]|>)\s/; +const FENCE_RUN_TOKEN = /(`{3,}|~{3,})/g; function normalizeMarkdownFences(text: string) { return text .replace(/\r\n?/g, '\n') .split('\n') - .map((line) => - BLOCK_MARKER_LINE.test(line) - ? line - : line.replace(GLUED_FENCE_LINE, '$1\n$2$3'), - ) + .map((line) => { + if (BLOCK_MARKER_LINE.test(line)) { + return line; + } + // 行里还有第二条围栏串时不动:那是行内代码(`文本 ```x``` `),末尾那截不是收场围栏。 + // 拆开它会凭空多出一个开场围栏,把后面的正文全变成代码。 + const runs = line.match(FENCE_RUN_TOKEN); + if (runs && runs.length > 1) { + return line; + } + return line.replace(GLUED_FENCE_LINE, '$1\n$2$3'); + }) .join('\n'); } @@ -368,6 +377,7 @@ function ChatMarkdownMessageImpl({ streaming ? streamingMarkdownComponents : markdownComponents } > + {/* 顺序有讲究:先拆粘住的围栏,空行压缩的 ` ```…``` ` 配对才认得出真正的代码块。 */} {preserveBlankLines ? normalizeMarkdownFences(text) : normalizeMarkdownBlankLines(normalizeMarkdownFences(text))} diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectTurnStatus.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectTurnStatus.ts index 05c9f456e..3a2b15d60 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectTurnStatus.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectTurnStatus.ts @@ -13,11 +13,13 @@ import type { * 一次讲清楚,组件只读这一个对象: * * - `nativeRunning`:**原生真相**。只由订阅 reducer 的 `turnRunning` 给出(`turn.started` - * 已到、`turn.completed` 未到)。它决定"陶泥儿正在处理"这类原生过程提示。 + * 已到、`turn.completed` 未到)。 * - `commandInFlight`:**本地真相**。本次会话的发送命令是否在飞(写权限门 → invoke → * 收尾);它从按下发送那一刻就为真,与原生是否已经开始无关。 - * - `displayBusy`:header / composer 该读的忙态,就是两者的并集:只要有一条成立就不能再 - * 接受新的发送。 + * - `displayBusy`:header / composer / 「陶泥儿正在处理」卡片该读的忙态,就是两者的并集: + * 只要有一条成立就不能再接受新的发送。卡片读它而不是 `nativeRunning`:`turn.started` + * 要等宿主应答返回才发出,只认原生真相会让模型首 token 之前那十来秒没有任何「正在处理」 + * 的交代(2026-09-24 口令)。 * - `latestTurnState`:最新一轮在界面上的三态(投影结果);没有回合时为 null。 * * 约定:新增"忙/在跑"类判据一律先落进这里,不要在组件里再拼布尔。 diff --git a/apps/ai-game-creator-shell/tests/ChatMarkdownMessage.test.tsx b/apps/ai-game-creator-shell/tests/ChatMarkdownMessage.test.tsx index 88dbeef88..d2f5d5b4a 100644 --- a/apps/ai-game-creator-shell/tests/ChatMarkdownMessage.test.tsx +++ b/apps/ai-game-creator-shell/tests/ChatMarkdownMessage.test.tsx @@ -230,6 +230,15 @@ describe('ChatMarkdownMessage', () => { expect(container.textContent).toContain('行内 code 保持原样'); }); + it('行内代码里的三段反引号保持原样,不凭空造出围栏', () => { + const { container } = render( + , + ); + + expect(container.querySelector('p code')?.textContent).toBe('x'); + expect(container.querySelectorAll('pre')).toHaveLength(0); + }); + it('引用块与列表项开头的围栏是合法结构,不做拆分', () => { const { container } = render( ```js`、`- ```js`)都不动——后两者拆开只会把围栏从引用块 / 列表项里挪出来。归一化对 `preserveBlankLines`(文件预览)同样生效。 +- **处理(现行口径)**:`normalizeMarkdownFences` 在解析前把「围栏前是非空白字符、围栏到行尾只剩语言标识(可空)」的行拆成两行。判据刻意收窄:整行 / 缩进围栏、行内代码(单个反引号)、代码里出现的 ``` (`const s = "```";`)、引用块 / 列表项开头的合法围栏(`> ```js`、`- ```js`)以及同一行里出现第二段围栏串的行内代码(``文本 ```x``` ``)都不动——后三者拆开只会把围栏从引用块 / 列表项里挪出来,或者凭空造出一个开场围栏。归一化对 `preserveBlankLines`(文件预览)同样生效。 - **同时**:块级 `pre` 与块内 `code` 都要给 `whitespace-pre-wrap` + `break-words`(两处各写过 `white-space`,只改一处不换行),否则窄面板里长行会把消息拉宽、顶出横向滚动条。 - **验证**:`npx vitest run apps/ai-game-creator-shell/tests/ChatMarkdownMessage.test.tsx`(去掉归一化或换行类名即红);真实 Chromium 夹具里长代码行 `horizontalOverflow: false`、粘住的开场 / 收场围栏都渲染成正确结构。