This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new f963be9c4 fix(ai): stop markdown repair breaking headings (#5730)
f963be9c4 is described below

commit f963be9c4513b1b32ef5427f2cd64d220cd8342a
Author: 0 <[email protected]>
AuthorDate: Mon Oct 19 19:54:09 2026 +0800

    fix(ai): stop markdown repair breaking headings (#5730)
    
    Verified against `rocketmq-studio` at fbdc5f22b before merging: the defect 
is still present on the
    current trunk and the change merges cleanly.
    
    Maintainer follow-ups on top of the contributor commit:
    - fix(ai): guard the heading and bullet repairs against well-formed markers
    
    Each follow-up commit message records the review finding it addresses.
    
    Locally verified: targeted suites green on the integration branch; a full 
backend and frontend
    build plus test pass runs on the trunk once this batch lands.
---
 web/src/pages/ai/render/__tests__/markdown.test.ts | 54 ++++++++++++++++++++++
 web/src/pages/ai/render/markdown.ts                | 10 +++-
 2 files changed, 63 insertions(+), 1 deletion(-)

diff --git a/web/src/pages/ai/render/__tests__/markdown.test.ts 
b/web/src/pages/ai/render/__tests__/markdown.test.ts
new file mode 100644
index 000000000..be8035ee1
--- /dev/null
+++ b/web/src/pages/ai/render/__tests__/markdown.test.ts
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { describe, expect, it } from 'vitest';
+import { normalizeAiMarkdown } from '../markdown';
+
+describe('normalizeAiMarkdown', () => {
+  it('inserts the space CommonMark requires after a heading marker or bullet', 
() => {
+    expect(normalizeAiMarkdown('##结论')).toBe('## 结论');
+    expect(normalizeAiMarkdown('#标题')).toBe('# 标题');
+    expect(normalizeAiMarkdown('-第一项')).toBe('- 第一项');
+    expect(normalizeAiMarkdown('+加号列表')).toBe('+ 加号列表');
+    expect(normalizeAiMarkdown('```bashls -la\n```')).toBe('```bash\nls 
-la\n```');
+  });
+
+  it('leaves well-formed markdown unchanged (idempotence)', () => {
+    const wellFormed =
+      '# 标题\n\n## 结论\n\n### 三级\n\n- 第一项\n- 第二项\n\n```bash\nls -la\n```\n';
+    expect(normalizeAiMarkdown(wellFormed)).toBe(wellFormed);
+  });
+
+  it('does not rewrite a well-formed ATX heading into a deeper stray marker', 
() => {
+    // A plain (?=\S) lookahead lets `#{1,6}` backtrack: `## heading` matched 
as one `#`
+    // followed by `#` and was rewritten to `# # heading` — every level-2+ 
heading in an
+    // assistant answer was corrupted before this guard existed.
+    expect(normalizeAiMarkdown('## heading')).toBe('## heading');
+    expect(normalizeAiMarkdown('### heading')).toBe('### heading');
+    expect(normalizeAiMarkdown('###### heading')).toBe('###### heading');
+  });
+
+  it('leaves thematic breaks and setext underlines intact', () => {
+    // --- / *** are valid CommonMark thematic breaks, and --- under a title 
line is a
+    // setext heading. Repairing them would rewrite a horizontal rule into a 
bullet
+    // with stray dashes.
+    expect(normalizeAiMarkdown('---')).toBe('---');
+    expect(normalizeAiMarkdown('***')).toBe('***');
+    expect(normalizeAiMarkdown('标题\n---\n')).toBe('标题\n---\n');
+    expect(normalizeAiMarkdown('前文\n\n---\n\n后文\n')).toBe('前文\n\n---\n\n后文\n');
+  });
+});
diff --git a/web/src/pages/ai/render/markdown.ts 
b/web/src/pages/ai/render/markdown.ts
index d509bfb39..cea42e281 100644
--- a/web/src/pages/ai/render/markdown.ts
+++ b/web/src/pages/ai/render/markdown.ts
@@ -52,7 +52,15 @@ export function normalizeAiMarkdown(content: string): string 
{
         fence = { marker: marker[1][0], length: marker[1].length };
         return line.replace(/^```(bash|sh|shell|json|ya?ml|sql|text)(?=\S)/i, 
'```$1\n');
       }
-      return line.replace(/^(#{1,6})(?=\S)/, '$1 ').replace(/^([-+*])(?=\S)/, 
'$1 ');
+      // The marker run must be maximal: with a plain (?=\S) lookahead the 
engine backtracks
+      // `#{1,6}` and a WELL-FORMED `## heading` matches as a single `#` 
followed by `#`,
+      // rewriting it to `# # heading`. Requiring the next character to be 
neither whitespace
+      // nor `#` keeps the repair to genuinely malformed markers like `##结论`.
+      //
+      // A bullet repair must not fire on a run of the same marker: 
`---`/`+++`/`***` are thematic
+      // breaks (and `---` under a title line is a setext heading), and 
inserting the space would
+      // rewrite them into a bullet with stray markers.
+      return line.replace(/^(#{1,6})(?=[^\s#])/, '$1 
').replace(/^([-+*])(?!\1\1)(?=\S)/, '$1 ');
     })
     .join('\n');
 }

Reply via email to