fix(weixin): normalize markdown image syntax#5297
Conversation
| .replace(/^#{1,6}\s+/gm, '') | ||
| .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1 ($2)') | ||
| .replace(/!\[([^\]]*)\]\([^)]+\)/g, '[$1]') | ||
| .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1 ($2)') |
There was a problem hiding this comment.
[Suggestion] Add a comment explaining why the image regex must run before the link regex.
The image pattern  contains the link pattern [alt](url) as a substring, so if the link regex runs first it matches the inner portion and produces !alt (url) instead of [alt] — exactly the bug this PR fixes. The ordering dependency is the load-bearing logic here, but it's completely undocumented. The old test even had a comment describing the wrong behavior ("link regex fires before image regex"); this PR removed that comment without replacing it with one explaining the constraint.
Without a comment, a future refactor (alphabetizing, consolidating into a config-driven loop, etc.) could silently reintroduce the regression.
| .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1 ($2)') | |
| // Image regex MUST run before the link regex:  contains | |
| // [alt](url) as a substring, so the link regex would match it first. | |
| .replace(/!\[([^\]]*)\]\([^)]+\)/g, '[$1]') | |
| .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1 ($2)') |
— glm-5.2 via Qwen Code /review
| it('converts image syntax (link regex fires before image regex)', () => { | ||
| // In the current implementation, the link regex fires before the image regex, | ||
| // so `` becomes `!alt (url)` rather than `[alt]` | ||
| it('converts image syntax to alt text', () => { |
There was a problem hiding this comment.
[Suggestion] Consider adding a test that covers mixed images and links in the same input string.
The entire motivation for this reorder is correct coexistence of image and link syntax — yet the tests only exercise each pattern in isolation. A future developer who swaps the two .replace() lines back would see all current tests pass. Adding a mixed-content test would guard against that regression:
| it('converts image syntax to alt text', () => { | |
| it('converts image syntax to alt text', () => { | |
| const result = markdownToPlainText(''); | |
| expect(result).toBe('[alt]'); | |
| }); | |
| it('handles mixed images and links without cross-contamination', () => { | |
| expect( | |
| markdownToPlainText( | |
| ' and [docs](https://docs.com)', | |
| ), | |
| ).toBe('[logo] and docs (https://docs.com)'); | |
| }); |
— glm-5.2 via Qwen Code /review
|
@qwen-code /triage |
✅ Local verification report — PR #5297
|
| Check | Result |
|---|---|
Merges cleanly into main (git merge-tree) |
✅ no conflicts; weixin package byte-identical between merge-base and current main |
Full weixin suite on PR (vitest run) |
✅ 44/44 (media.test.ts 11 + send.test.ts 33) |
| BASE suite as-is | ✅ 33/33 (asserting the old buggy !alt (url) output) |
| Genuine regression test — PR's new test against BASE source | ✅ FAILS as expected: expected '!alt (https://img.png)' to be '[alt]' → proves the test catches the real bug |
npm run build --workspace=@qwen-code/channel-weixin (tsc --build) |
✅ exit 0 |
eslint send.ts + send.test.ts |
✅ exit 0 |
prettier --check send.ts + send.test.ts |
✅ "All matched files use Prettier code style!" |
git diff --check |
✅ exit 0 |
Differential behavior A/B (exact regex chains from both versions)
| Input | BEFORE (main) |
AFTER (PR) | |
|---|---|---|---|
 |
!alt (https://img.png) ❌ |
[alt] ✅ |
the fix |
 |
!a cat (https://img.png) ❌ |
[a cat] ✅ |
fixed |
[click here](https://example.com) |
click here (https://example.com) |
click here (https://example.com) |
unchanged (no regression) |
hello! [link](https://x.com) |
hello! link (https://x.com) |
hello! link (https://x.com) |
unchanged (lone ! ≠ image) |
[site](a) and  |
site (a) and !pic (b) ❌ |
site (a) and [pic] ✅ |
link part unchanged, image part fixed |
 (empty alt) |
[] |
[] |
identical on both |
![alt][1] (reference-style) |
![alt][1] |
![alt][1] |
unchanged (no inline URL) |
Across 15 cases: normal links are byte-identical before/after; every difference is an image token that is now rendered correctly.
Minor observation (not a blocker, out of scope)
The nested clickable image [](link) is rendered imperfectly by both versions — [alt (https://img.png)] before vs [[alt]](https://link.com) after. This is a pre-existing limitation of the naive regex approach, not introduced by this PR, and is outside the stated scope. Could be a future follow-up if clickable images matter for WeChat output.
Reproduce
git worktree add --detach /tmp/wt-after $(gh pr view 5297 --json headRefOid -q .headRefOid)
git worktree add --detach /tmp/wt-before origin/main
ln -s "$PWD/node_modules" /tmp/wt-after/node_modules
ln -s "$PWD/node_modules" /tmp/wt-before/node_modules
# PR (green):
(cd /tmp/wt-after/packages/channels/weixin && ../../../../node_modules/.bin/vitest run --config vitest.config.ts)
# Prove the test catches the bug: copy PR's test onto BASE source -> image test FAILS
cp /tmp/wt-after/packages/channels/weixin/src/send.test.ts /tmp/wt-before/packages/channels/weixin/src/send.test.ts
(cd /tmp/wt-before/packages/channels/weixin && ../../../../node_modules/.bin/vitest run --config vitest.config.ts)🇨🇳 中文版(点击展开)
✅ 本地验证报告 — PR #5297 fix(weixin): normalize markdown image syntax
结论:可以合并。 修复准确、范围聚焦,修正了一个真实存在的 bug,且不会影响普通链接的渲染。测试、构建、lint、格式化全部通过。
PR 改动内容
在 markdownToPlainText(packages/channels/weixin/src/send.ts)中交换了两个 replace() 调用的顺序,让图片正则先于链接正则执行,并同步更新了测试断言。
main 上的根因: 对于 ,链接正则 \[([^\]]+)\]\(([^)]+)\) 先命中,匹配了 [alt](url) 这一段,把前面的 ! 留在匹配之外 → 输出 !alt (url),图片正则之后就无内容可匹配。调整顺序后,图片正则 !\[...\]\(...\) 先吃掉整个 token → [alt];由于结果不再带有 (url),链接正则不会误伤它。普通链接 [text](url)(无前导 !)依旧由链接正则按原样处理。
验证方式
在 tmux 中、两个隔离的 detached worktree(pr5297-head = be81974,origin/main = 49ff31b)里运行真实测试,并软链接复用仓库 hoist 的 node_modules。环境:Node v22.22.2、npm 10.9.7、vitest 3.2.4。
| 检查项 | 结果 |
|---|---|
干净合入 main(git merge-tree) |
✅ 无冲突;weixin 包在 merge-base 与当前 main 之间字节一致 |
PR 上 weixin 全量测试(vitest run) |
✅ 44/44(media.test.ts 11 + send.test.ts 33) |
| BASE 原样测试 | ✅ 33/33(断言旧的错误输出 !alt (url)) |
| 回归测试有效性 — 用 PR 的新测试跑 BASE 源码 | ✅ 如期失败:expected '!alt (https://img.png)' to be '[alt]' → 证明该测试确实能捕获此 bug |
npm run build(tsc --build) |
✅ exit 0 |
eslint send.ts + send.test.ts |
✅ exit 0 |
prettier --check |
✅ 全部符合 Prettier 风格 |
git diff --check |
✅ exit 0 |
行为差异 A/B(直接取两个版本的真实正则链对比)
| 输入 | 改动前(main) |
改动后(PR) | 说明 |
|---|---|---|---|
 |
!alt (https://img.png) ❌ |
[alt] ✅ |
本次修复 |
[click here](https://example.com) |
click here (https://example.com) |
click here (https://example.com) |
不变(无回归) |
[site](a) and  |
site (a) and !pic (b) ❌ |
site (a) and [pic] ✅ |
链接部分不变,图片部分修复 |
hello! [link](https://x.com) |
hello! link (https://x.com) |
hello! link (https://x.com) |
不变(单独的 ! 不构成图片) |
(空 alt) |
[] |
[] |
两版本一致 |
15 个用例中:普通链接在改动前后字节级一致;所有差异都是图片 token 现在被正确渲染。
一点小观察(非阻塞、超出本次范围)
可点击图片 [](link) 在两个版本里都渲染得不理想——改动前 [alt (https://img.png)],改动后 [[alt]](https://link.com)。这是朴素正则方案的既有局限,并非本 PR 引入,且不在本次修复范围内。若 WeChat 输出需要支持可点击图片,可作为后续跟进。
Verified locally with real vitest runs under tmux on isolated git worktrees of the PR head and origin/main. Differential harness used the verbatim regex chains from both versions.
|
@qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Hey @tt-a1i, thanks for the fix! The change itself looks correct and well-scoped — swapping the regex order so image syntax is handled before link syntax is the right approach.
However, the PR body doesn't follow the PR template. A few required sections are missing:
- What this PR does / Why it's needed — the "Summary" section covers the what but not the why (e.g. what user-facing problem does this fix?)
- Reviewer Test Plan — including Evidence (Before/After) and Tested-on table
- Risk & Scope — even a one-liner like "no risk, image-only change, normal links unaffected" helps reviewers triage quickly
- Linked Issues — if there's a related issue
- 中文说明 — bilingual description block
Could you update the PR body to match the template? The fix itself is solid — just the description needs the standard format so reviewers (and future git log readers) can quickly understand the context.
中文说明
你好 @tt-a1i,感谢修复!代码改动本身正确且范围合理——调换正则顺序让图片语法先于链接语法处理,是正确的做法。
不过 PR 描述没有按照 PR 模板 填写,缺少以下几个必填部分:
- What this PR does / Why it's needed — "Summary" 只说了 改了什么,没有说 为什么改(比如修了什么用户可见的问题?)
- Reviewer Test Plan — 包括 Before/After 对比和 Tested-on 表格
- Risk & Scope — 哪怕只写一句"无风险,仅影响图片,普通链接不受影响"也好
- Linked Issues — 如果有相关 issue 的话
- 中文说明 — 双语描述
请按照模板更新 PR 描述。代码本身没问题,只是需要标准格式方便审查和后续查阅。
— Qwen Code · qwen3.7-max
|
@qwen-code /triage |
|
Thanks for the PR! Template looks good ✓ — the earlier template concern from the CI bot has been addressed. On direction: this is a real user-facing bug. WeChat doesn't render markdown, so On approach: the fix is exactly two lines swapped — reorder the image regex to run before the link regex. This is the minimal correct solution. The image token Moving on to code review. 🔍 中文说明感谢贡献! 模板已完善 ✓ — 之前 CI 机器人提出的模板问题已修复。 方向:这是一个真实的用户可见 bug。微信不渲染 markdown, 方案:修复就是交换两行正则顺序——让图片正则先于链接正则执行。这是最小正确解。图片 token 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
Code ReviewIndependent proposal (before reading diff): The bug is that Diff comparison: the PR does exactly this — a clean two-line swap. My proposal and the PR are identical in approach. No correctness bugs, no security concerns, no regressions. The existing link test ( Edge cases considered:
TestingThis is a pure string transformation (markdown → plain text) with no TUI surface. Verification is via unit tests, which are the correct test vehicle here. Before (main, buggy behavior): After (this PR, fixed behavior): All tests pass in both configurations. The link conversion test continues to pass unchanged. 中文说明代码审查独立方案(读 diff 之前):bug 是 Diff 对比:PR 正是这样做的——干净的两行交换。我的方案与 PR 完全一致。 无正确性 bug、无安全问题、无回归。现有链接测试( 考虑的边界情况:
测试这是一个纯字符串转换(markdown → 纯文本),无 TUI 界面。验证通过单元测试完成,这是正确的测试方式。 改动前(main,bug 行为): 34 个测试全部通过,图片测试断言 改动后(本 PR,修复行为): 33 个测试全部通过,图片测试断言 两种配置下所有测试均通过。链接转换测试不受影响。 — Qwen Code · qwen3.7-max |
|
Clean fix, ships exactly what it promises. Going back to my independent proposal from Stage 2: the PR and I arrived at the same solution — reorder the two regex lines. There's no simpler path, and the PR doesn't add anything unnecessary. This solves a real problem: WeChat users receiving malformed image output ( The unit tests are the right verification for a pure string-transformation function — they confirm both the fix and that normal links remain unaffected. The PR author also correctly identified and disclosed the pre-existing nested-image limitation as out of scope. If I had to maintain this in six months, I'd thank the author — the fix is self-explanatory and the test documents the expected behavior clearly. Approving. ✅ 中文说明干净的修复,完全实现了承诺的效果。 回顾我在第二阶段的独立方案:PR 和我得出了相同的解决方案——调换两行正则顺序。没有更简单的路径,PR 也没有添加任何不必要的东西。 这解决了一个真实的问题:微信用户收到格式错误的图片输出( 单元测试是纯字符串转换函数的正确验证方式——同时确认了修复有效且普通链接不受影响。PR 作者也正确识别并说明了嵌套图片的既有局限不在范围内。 如果六个月后需要维护这段代码,我会感谢作者——修复不言自明,测试清晰记录了预期行为。 批准 ✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
What this PR does
In the WeChat (Weixin) channel, outbound markdown is flattened to plain text by
markdownToPlainTextbecause WeChat does not render markdown. This PR reorders the regex pass so that the image syntaxis normalized before the ordinary link syntax[text](url).Previously the link regex ran first and matched the
[alt](url)portion inside, leaving the leading!outside the match, so an image rendered as!alt (url). With the image rule running first, the whole image token is consumed and rendered as[alt]; since the result no longer contains a trailing(url), the link rule correctly leaves it alone. Ordinary links[text](url)(with no leading!) are still handled by the link rule exactly as before. The regression test that asserted the old buggy!alt (url)output is updated to assert the corrected[alt]output.Why it's needed
The previous regex ordering produced malformed plain text for any markdown image sent through the WeChat channel: an image such as
came out as!alt (https://img.png)instead of the intended[alt]alt-text placeholder. This is a user-facing correctness bug in WeChat message output. The fix makes image flattening behave consistently with the existing alt-text intent while leaving normal-link behavior untouched.Reviewer Test Plan
How to verify
Reproduce the original bug and confirm the fix from
packages/channels/weixin:markdownToPlainText('')main(before):!alt (https://img.png)(buggy)[alt]markdownToPlainText('[click here](https://example.com)')staysclick here (https://example.com)on both versions.Verification commands (run from
packages/channels/weixinunless noted):npx vitest run --config vitest.config.ts src/send.test.tsnpx vitest run --config vitest.config.tsnpm run build --workspace=@qwen-code/channel-weixinnpx eslint packages/channels/weixin/src/send.ts packages/channels/weixin/src/send.test.tsnpx prettier --check packages/channels/weixin/src/send.ts packages/channels/weixin/src/send.test.tsgit diff --checkEvidence (Before & After)
N/A — non-visible logic fix (plain-text conversion); covered by the unit tests above.
Tested on
✅ tested ·⚠️ not tested · N/A
Environment (optional)
Unit tests only (npm workspaces); scoped to the
@qwen-code/channel-weixinpackage.Risk & Scope
markdownToPlainTextin the WeChat channel. The behavior change is limited to image tokens(now[alt]); normal links are byte-identical before and after.[](link)remain a pre-existing limitation of the naive regex approach and are not addressed here.Linked Issues
None.
中文说明
这个 PR 做了什么
在微信(Weixin)通道里,由于微信不渲染 markdown,发出的 markdown 会被
markdownToPlainText压平为纯文本。本 PR 调整了正则处理的顺序,让图片语法在普通链接语法[text](url)之前被规整。此前链接正则先执行,匹配到
内部的[alt](url)片段,把前导的!留在匹配之外,于是图片被渲染成!alt (url)。让图片规则先执行后,整个图片 token 被一次性消费并渲染为[alt];由于结果中不再含有结尾的(url),链接规则便不会误伤它。普通链接[text](url)(无前导!)仍由链接规则按原样处理。原本断言旧的错误输出!alt (url)的回归测试,已更新为断言修复后的[alt]输出。为什么需要
之前的正则顺序会让任何经微信通道发出的 markdown 图片产生格式错误的纯文本:例如
会输出成!alt (https://img.png),而非预期的[alt]替代文本占位。这是微信消息输出中一个面向用户的正确性 bug。本修复使图片压平行为与既有的替代文本意图保持一致,同时不影响普通链接的行为。评审者测试计划
如何验证
在
packages/channels/weixin下复现原 bug 并确认修复:markdownToPlainText('')main上(改动前)期望:!alt (https://img.png)(错误)[alt]markdownToPlainText('[click here](https://example.com)')在两个版本都保持click here (https://example.com)。验证命令(除特别说明外,均在
packages/channels/weixin下运行):npx vitest run --config vitest.config.ts src/send.test.tsnpx vitest run --config vitest.config.tsnpm run build --workspace=@qwen-code/channel-weixinnpx eslint packages/channels/weixin/src/send.ts packages/channels/weixin/src/send.test.tsnpx prettier --check packages/channels/weixin/src/send.ts packages/channels/weixin/src/send.test.tsgit diff --check证据(前后对比)
N/A —— 非可视化逻辑修复(纯文本转换);已由上述单元测试覆盖。
测试平台
✅ 已测试 ·⚠️ 未测试 · N/A
环境(可选)
仅单元测试(npm workspaces);范围限于
@qwen-code/channel-weixin包。风险与范围
markdownToPlainText。行为变化仅限于图片 token(现为[alt]);普通链接在改动前后字节级一致。[](link)仍是朴素正则方案的既有局限,本 PR 不予处理。关联 Issue
无。
AI Assistance Disclosure
I used Codex to review the changes, sanity-check the implementation against existing patterns, and help spot potential edge cases.