截断问题
TRUNCATION LESSONS
状态:🟢 现行 · 最后核对 2026-06-19
2026-06-15 ~ 2026-06-18 | 最终修复:
f2eaea3
走过的弯路(按时间顺序)
| 我们以为的原因 | 修了什么 | 结果 |
|---|---|---|
| thinking filter 吞文字 | </thinking> 之后文字被丢弃 |
修了一处 |
| parseAndRenderMessage 拆 DOM | 关掉了 | 修了一处 |
| trust_parse 策略3 误匹配 | 限制最后3行 | 修了一处 |
| innerHTML 在流式中每 chunk 解析 | 改成流式后一次性 innerHTML | 没修好 |
| Chromium 渲染 bug | 改成 textContent | 没修好 |
| TCP 帧切割 UTF-8 字节 | split('\n') → split('\n\n') SSE 事件层解析 |
修了传输层——但还是截断 |
innerHTML 中文+<i> 浏览器解析 bug |
纯 DOM API——createTextNode + createElement('i') |
✅ 修好了 |
真正的根因(两个)
根因 1:split('\n') 按行拆 SSE 事件 —— 语义丢失
TCP 会把一个中文 UTF-8 字符拆成两个 Uint8Array 帧。decoder.decode(value, {stream:true}) 虽然内部缓冲了不完整字节,但 split('\n') 之后取出来的一行,字符序列已经被重组了——在 TCP 切割点上的字符间隔不是真实的 SSE 语义边界。
修法:split('\n\n')。SSE 协议的最小语义单元是事件,不是行。等 \n\n 到了再处理,配合 decoder({stream:true}) 的不完整字节缓冲。
根因 2:innerHTML + 中文 + <i> 标签 —— 浏览器解析不可靠
即使文本是完整的、HTML 标签是闭合的,Chromium 的 HTML 解析器在中文和 <i> 标签混合的某些字符序列上,会自作主张地"修正"DOM——丢掉中间的内容。这是浏览器底层行为,无法通过代码修复。
修法:不经过 innerHTML。用 DOM API 直接操作——textContent 设全文(安全),正则找到 (...) 位置,createTextNode 放普通文字,createElement('i') 放括号内容,appendChild 组装。浏览器从头到尾不触发 HTML 解析。
最终架构
后端: chat → SSE 流式
前端: reader.read() → decoder.decode({stream:true}) → split('\n\n') SSE事件层
↓
流式中: textSpan.textContent = fullResponse(纯文本)
↓
[DONE]后: createTextNode + createElement('i') + appendChild(DOM操作,不经过innerHTML)
核心教训
- SSE 用
\n\n拆事件,不用\n拆行。 前者是协议语义单元,后者是 TCP 碎片的偶然边界。 - innerHTML + 中文 + 行内标签是不可靠的。 不论你怎么保证标签闭合——浏览器解析器就这个行为。
- DOM API 比 innerHTML 安全一百倍。 慢一点,但不会丢内容。
- 先关新功能再排查。 截断问题反复三周,很大原因是我们每次修了一个又引入另一个。正确的排查顺序:关新功能 → 二分排查 → 确认根因 → 一处修复。