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');
}