Skip to content

fix(web): 链接正则的嵌套量词让某个会话打开即卡死(D95) - #57

Merged
Yang-Code01 merged 1 commit into
mainfrom
fix/md-link-regex
Nov 3, 2026
Merged

Yang-Code01 merged 1 commit into
mainfrom
fix/md-link-regex

Conversation

@Yang-Code01

Copy link
Copy Markdown
Owner

Summary

INLINE 里只有链接那一组的 label 是嵌套量词,其余六组都已经是 A|B 重复。把内层的贪心 + 去掉即可(Kleene 闭包同语言,只去掉歧义):

 const INLINE =
   /(`[^`\n]+`)|(!\[[^\]\n]*\]\([^\s)]+(?:\s+"[^"]*")?\))|
-   ((?<!!)\[(?:[^\[\]\n]+|\[[^\]\n]*\])+\]\([^\s)]+(?:\s+"[^"]*")?\))|
+   ((?<!!)\[(?:[^\[\]\n]|\[[^\]\n]*\])+\]\([^\s)]+(?:\s+"[^"]*")?\))|
    (\*\*[^*\n]+\*\*|__[^_\n]+__)|(~~[^~\n]+~~)|(…)/g;

inlineInto 里取 label 的那条正则(/^\[((?:…)+)\]\(([^\s)]+)/)同步改,一处 2 行。

为什么只有它会卡死:renderMarkdown 是一行一行送进 inlineInto 的。(x+|\[…\])+ 对同一段标签文本有 2^n 种切分,而 link 分支在整行里逐个位置尝试匹配;只要这一行里有一段够长的 […] 后面不是 ](,引擎就得把所有切分试完才认输——主线程不再回到事件循环,Chromium 就报「此页面没有响应」。

踩到的是会话 c24330ab…(名字 bug)第 23 条 tool 结果里的一行 TypeScript:

      const active = (this.termActive && this.termTabs.get(this.termActive)) || [...this.t…

111 字符、[ 之后最长内层 80 字符,没有 ](。第 1–22 条都能画,画到这一条页面就永久白屏。

user 消息不走这条路(web/src/chat.ts 里 body.textContent = item.text 纯文本渲染),所以带图消息里的 [Image: …] 注释不触发;触发的一律是 tool 输出里的代码 / JSON / ANSI 构建日志。

Evidence

OK  3142ms nodes/msgs=4366/159   sid=c24330ab     ← 修复前为 HUNG
OK  3095ms nodes/msgs=17261/703  sid=90da6b98
OK  3087ms nodes/msgs=13999/568  sid=b76727ed
OK  3131ms nodes/msgs=6559/233   sid=1efeff34
  • After (回归): 13 组语法案例上 old/new 输出逐字一致([a](b.md)、[label with [inner] text](u)、[[a]b](u)、![badge](a)](b) 这种嵌套、[x] (y)、![img](a.png)、103/300 字符 label、中文前后缀、[a][b](u));该会话全部 213 段 text/result/command 用新正则一趟跑完,0 停滞。npm --prefix web run build(tsc --noEmit + vite build)通过。

Merge Danger

Door: two-way

Blast Radius: markdown

改的是纯正则 + 一条注释,回滚就是一个 revert;没有数据、协议或磁盘格式变化。唯一风险是「本来不该被认成链接的方括号文本现在被认成链接」——Kleene 闭包同语言,逐字匹配掉的只是切分歧义,13 组等价性案例里没有差异。

顺带记录:D95 在 main 的决策日志里是空号(D94 之后直接没有),这篇占这个位置,docs/DECISIONS.md 里那一节写了「为什么」和实测阈值。

`INLINE` 第 3 组(markdown 链接)的 label 是 `(?:[^\[\]\n]+|\[[^\]\n]*\])+`,
内层贪心 `+` 与末尾 `+` 叠成歧义:同一段标签有指数级切分方式。md 逐行送
`inlineInto`,所以只要某一行里有一段够长的 `[…]` 后面不是 `](`,引擎要
穷举完所有切分才认输,主线程回不到事件循环 → Chromium 报「页面无响应」。

会话 c24330ab(名字 bug)第 23 条 tool 结果里那行
`… || [...this.termTabs.get(this.termActive))` 就够:111 字符、最长内层
80 字符,旧正则单次 1434ms、新 0.04ms。前 61 条里共有 6 行这种文本,
整页渲染串起来就是十几秒起步的白屏。

改为逐字匹配 `(?:[^\[\]\n]|\[[^\]\n]*\])+`(Pike 式 `A|B` 重复,无歧义),
`inlineInto` 里取 label 的那条同步改。接受的语法集合不变。
@Yang-Code01
Yang-Code01 merged commit eacb443 into main Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant