按契约 ../../reference/token-streaming.md(F073),
把正文展示从「done 时整段到达 → 打字机逐字播放」改为真实增量即时渲染:
后端 answer_delta 一到就上屏,用户等待 ≈ 模型生成速度本身(不再额外加打字机时长)。
后端已实测部署(2026-09-20 两份真实载荷):流目标均为 summary;一轮 delta 235~746 片、
平均 1.7 字/片;answer/summary 完整事件与 result.response 是同一份正文的兼容副本。
① 新增「增量 markdown 规整器」
normalizeChatMarkdown 会插入空行,而它的每处决策只依赖相邻两行(行结束才稳定)。
若不做增量规整,answer_end 的「整块替换」在多段落回答上必然与已流文本分歧 → 触发整段重建 →
<scope> 标记被重解析 → 思考中卡片每轮闪现 + 退场动画重播。
规整器:已结束行立即按完整决策发出;当前未结束行扣住,直到能判定「已是列表项前缀」
(LIST_ITEM_RE 无 $ 锚,命中即稳定)或「不可能成为列表项」。
不变式:每步 concat(已返回增量) === normalizeChatMarkdown(concat(已输入)) →
正常路径上 answer_end 退化为纯尾缀追加(零行重建)。
② contentStreaming 标记在 answerStreamStart 置真、只在 finishTask 清除
(end/abort 都不清)——end 之后 flushContent 追加的卡片/参考资料仍即时渲染;
mock 与旧后端轮次从未置位。用户停止走 stopAiMessage(state=Stop),canAnimateAiText 已排除 Stop。
src/components/answer-stream.ts(纯函数):normalizeChatMarkdown/LIST_ITEM_RE 移入、
事件解析 parseAnswerStreamEvent、状态机 applyAnswerStreamEvent、增量规整器ApiChatEventMap 增 3 个事件(answerStreamStart/End/Abort);4 个新 case
(delta 过规整器后走既有 message 通道吃 rAF 批处理);progress/heartbeat 在流开始后停发 scope;
flushContent 重构(流已完成时跳过正文、totalResponse 仍拼最终全文);
discardActiveStream 挂到 error / failTurn / EOF 三条路径(abort 必须先于 error/close 发出)finishTask 清 contentStreaming;close 的 DMS 补写判据改 hasVisibleMessageContent;
saveHistory 按 500ms 节流(流式期每 rAF 全量序列化整个 chatHistory)BusinessRecord.vue:shouldDriveTypewriter 加 && !record.contentStreaming(唯一改动点)_entry-coordinator.ts 导出 answer-stream;新增 verify-answer-stream.mjs、probe-answer-stream.mjs流式把整篇综合说明当正文先铺满,卡片照旧排在正文之后 → 用户要先滚过全文才看到卡片, 与改造前「短开场 → 卡片 → 综合说明」的观感不一致(这次是我漏掉的版式回归)。
改法:flushContent 把输出拆成 leading(卡片)/ trailing(notices、补问、二维码、参考资料) 两块;
流式轮通过新事件 answerStreamCompose {streamId, leading, trailing} 交给应用层按
基线 + leading + 正文 + trailing 重写一次内容(卡片因此在正文之上);
旧路径完全不变(正文 → 卡片 → 参考资料,有断言守着)。
组装逻辑抽成纯函数 composeStreamedContent(顺带剥掉基线里的 <scope>,避免卡片闪现)。
finish());
reducer(缺口/重复序号/过期流/end 后 delta/abort 复位/非法负载不抛)probe-answer-stream.mjs 打两类问题,统计事件序/片数/首 delta 耗时/
concat(deltas)===answer_end.textnpm run build + validate-harnessbaseText 里的 <scope> 块再拼,消除卡片闪现summary.notices 是否仍输出(本方案保留)