修复: 流式正文被轮次折叠吞掉(真根因)

- 根因: renderItems 完成轮分桶把整轮含正文折进轨迹卡且默认收起,thinkPlainText 又 reasoning_content||content 把正文当思考塞单行 pill → 流式期间可见、轮末随卡消失(现象=气泡清空回到等待)
- 修复: 过程轮有正文的各成常显气泡(剥 toolCalls,工具卡仍归轨迹卡不重复);思考 pill 只认 reasoning_content
- 历次修补(40c4a21/4652112/524120b/BUG-260624-01/flush guard)都在状态清空层,未碰渲染折叠层 → 故无效;看板 911 留痕教训
- 验收: vue-tsc --noEmit EXIT 0
This commit is contained in:
lxy
2026-10-10 00:51:50 +08:00
parent e61fcdea79
commit bcb0b82f33
2 changed files with 16 additions and 1 deletions
+3
View File
@@ -735,3 +735,6 @@
902. [ ] 超时失败后的一键继续入口: 流式首字节超时(120s,服务收了不响应,不可自动重试)报「响应超时,请重试」后流程中断——轮次重入预算耗尽(3/3)即彻底停,用户只见裸文案。修:超时失败落停靠语义(复用 emit_task_parked),停靠卡带「继续」(ai_continue_loop parked 分流已修好)——错误后一键续跑而非中断。体验缺口实证 2026-10-07 晚 u-switch 故障窗口。 902. [ ] 超时失败后的一键继续入口: 流式首字节超时(120s,服务收了不响应,不可自动重试)报「响应超时,请重试」后流程中断——轮次重入预算耗尽(3/3)即彻底停,用户只见裸文案。修:超时失败落停靠语义(复用 emit_task_parked),停靠卡带「继续」(ai_continue_loop parked 分流已修好)——错误后一键续跑而非中断。体验缺口实证 2026-10-07 晚 u-switch 故障窗口。
903. [ ] u-switch 20:38-21:02 故障窗口根因深查: 11 连败+120s 首字节超时风暴(多会话多 provider 同时),21:0x 自愈。查 moyu-ops 机 u-switch 日志定因(上游抖动/服务重启/连接池耗尽),根治或加监控告警。**【21:5x AI-Ops 定因已实锤,journalctl 实测】上游连环降级雪崩非服务故障:api.deepseek.com 402 余额不足(冷却10m)+enterprise.moyu.info 401 认证失效(冷却10m)+sensenova kimi-k3 429/SSE空响应体(30s冷却循环)+bigmodel glm 429/SSE截断缺 message_stop——多通道同时打光致无首字节,冷却走完自愈。剩余工作=根治项:①switch-gw 的 deepseek 系 4 通道确认摘除(用户指认已摘但配置仍在役)②429 风暴时客户端退避协同(30s 冷却对重试风暴无效)③故障窗口告警。**(10-08 已处置:moyu 令牌过期续期+主链切 ds-flash,见 900 区段外 905) 903. [ ] u-switch 20:38-21:02 故障窗口根因深查: 11 连败+120s 首字节超时风暴(多会话多 provider 同时),21:0x 自愈。查 moyu-ops 机 u-switch 日志定因(上游抖动/服务重启/连接池耗尽),根治或加监控告警。**【21:5x AI-Ops 定因已实锤,journalctl 实测】上游连环降级雪崩非服务故障:api.deepseek.com 402 余额不足(冷却10m)+enterprise.moyu.info 401 认证失效(冷却10m)+sensenova kimi-k3 429/SSE空响应体(30s冷却循环)+bigmodel glm 429/SSE截断缺 message_stop——多通道同时打光致无首字节,冷却走完自愈。剩余工作=根治项:①switch-gw 的 deepseek 系 4 通道确认摘除(用户指认已摘但配置仍在役)②429 风暴时客户端退避协同(30s 冷却对重试风暴无效)③故障窗口告警。**(10-08 已处置:moyu 令牌过期续期+主链切 ds-flash,见 900 区段外 905)
911. [x] **「流式后气泡被清空」真根因(2026-10-10 定位并修复,历次修补无效)**:用户现象=正文流式进气泡 → 轮末气泡清空回到「等待」,每轮工具后都发生、且文本「彻底丢」。**根因(实测定位)**:`MessageList.vue` `renderItems` 的**完成轮分桶**把整轮(含模型正文)折进「执行轨迹卡」且**默认收起**(`flushBucket` 只把最后一个有正文的消息当 finalMsg 出气泡,其余全进 traceMsgs);叠加 `thinkPlainText` 取 `reasoning_content || content` 把正文也当「思考」塞进单行 pill。故流式期间正文可见(currentText 直渲),轮末消息转轨迹卡即「消失」。**修复**:①过程轮里**有正文**的各成一个常显气泡(剥 `toolCalls`,工具卡仍归轨迹卡不重复);②`thinkPlainText` 只认 `reasoning_content`(思考 pill 不再吞正文)。**教训(必须留痕)**:此前 4-5 次修复(`40c4a21` 闪烁/`4652112` 迟到收尾仲裁/`524120b` 切会话残片/`BUG-260624-01` 重叠/`flushCurrentText` guard)全在**状态清空/骨架层**,从未碰**渲染折叠/分桶层** → 故反复不好;凡「流式后内容消失」类投诉,第一嫌疑应是**渲染折叠/分桶**(`renderItems`/trace 桶),而非 streaming/currentText 状态。
+13 -1
View File
@@ -946,7 +946,9 @@ const { ids: expandedThinkIds, toggle: toggleThink } = useToggleSet()
/** 轮次思考纯文本:reasoning_content 优先(DeepSeek/Anthropic thinking 同通道),content 兜底; /** 轮次思考纯文本:reasoning_content 优先(DeepSeek/Anthropic thinking 同通道),content 兜底;
* 压平 markdown 换行;两者皆空回退「过程记录」占位(纯工具轮也有锚点) */ * 压平 markdown 换行;两者皆空回退「过程记录」占位(纯工具轮也有锚点) */
function thinkPlainText(m: AiMessage): string { function thinkPlainText(m: AiMessage): string {
const src = m.reasoning_content || m.content || '' // 只承载「推理内容」(正文不折叠,2026-10-10):原 `reasoning_content || content` 会把模型正文
// 也当思考吞进单行 pill——正文改由常显气泡渲染(见 renderItems),此处只认 reasoning_content。
const src = m.reasoning_content || ''
return src.replace(/\s+/g, ' ').trim() return src.replace(/\s+/g, ' ').trim()
} }
/** pill 单行文本:纯文本截断 + 空轮回退文案(title 承载全文,此处只管单行) */ /** pill 单行文本:纯文本截断 + 空轮回退文案(title 承载全文,此处只管单行) */
@@ -1126,6 +1128,16 @@ const renderItems = computed<RenderItem[]>(() => {
} else { } else {
pushTimeSepIfNeeded(items, finalMsg.timestamp) pushTimeSepIfNeeded(items, finalMsg.timestamp)
} }
// 正文不折叠(2026-10-10 根因修复):此前整轮(含模型正文)折进轨迹卡默认收起 → 流式期间
// 能看到正文、轮末正文随卡收起,用户表象为「气泡被清空/进入等待」,历次在 flush/清空/骨架层
// 修补均无效。现把过程消息里**有正文**的各成一个常显气泡(剥掉 toolCalls——工具卡仍归轨迹卡,
// 不重复渲染),轨迹卡只折叠工具步骤与思考。
for (const m of traceMsgs) {
const c = m.content
if (m.role === 'assistant' && c && c.trim() && !isToolResultJson(c)) {
items.push({ kind: 'msg', key: 'm-' + m.id, msg: { ...m, toolCalls: undefined } })
}
}
items.push({ kind: 'msg', key: 'm-' + finalMsg.id, msg: finalMsg, final: true }) items.push({ kind: 'msg', key: 'm-' + finalMsg.id, msg: finalMsg, final: true })
lastMsgTs = finalMsg.timestamp lastMsgTs = finalMsg.timestamp
} }