修复: 切会话残片覆盖回复与收尾工具卡丢失
- flushCurrentText 加非流式守卫,非流式只清不写 - 后台会话终止即清流式态,残片不跨轮引爆 - 异常收尾尾部工具气泡折进轨迹卡不再蒸发 - 停止与看门狗超时先保文再复位
This commit is contained in:
@@ -889,7 +889,10 @@ const renderItems = computed<RenderItem[]>(() => {
|
||||
if (c && c.trim() && !isToolResultJson(c)) { fi = i; break }
|
||||
}
|
||||
const finalMsg = fi >= 0 ? msgs[fi] : msgs[msgs.length - 1]
|
||||
const traceMsgs = msgs.slice(0, msgs.indexOf(finalMsg))
|
||||
// 过程消息 = finalMsg 之外全部(含其后尾气泡):异常收尾(错误/停止/流中断)时末轮气泡
|
||||
// 常只有 toolCalls 无正文,按「finalMsg 之前」切片会把尾气泡丢出渲染与轨迹卡,
|
||||
// 执行中可见的工具卡在收尾瞬间整块消失(用户实测「空白覆盖一大堆内容」)。
|
||||
const traceMsgs = msgs.filter(m => m !== finalMsg)
|
||||
// 完成:轨迹折叠为一张卡,轮次内容在卡内 rail 面板展开(不再摊平成独立消息项)
|
||||
if (traceMsgs.length > 0) {
|
||||
const key = 'trace-' + traceMsgs[0].id
|
||||
|
||||
@@ -382,6 +382,7 @@ export const switchingConvs = new Set<string>()
|
||||
// 本模块函数运行时惰性取最新 state。
|
||||
export interface AiStateShape {
|
||||
currentText: string
|
||||
streaming: boolean
|
||||
messages: AiMessage[]
|
||||
activeConversationId: string | null
|
||||
}
|
||||
@@ -434,6 +435,15 @@ export function flushCurrentText(): void {
|
||||
resetDelta()
|
||||
return
|
||||
}
|
||||
// 非流式守卫:currentText 仅在流式进行中有意义。终态收尾后的迟到 delta、后台会话
|
||||
// 完成后切回残留的缓冲,都会让 streaming=false 时 currentText 仍非空;此刻回填会把
|
||||
// 上一轮已完成回复整条覆盖为残片(用户实测「空白内容覆盖了之前一大堆内容」),
|
||||
// 故非流式一律只清不写。合法回填点(收尾/新轮/停止/超时)均在 streaming 翻 false 前调用。
|
||||
if (!s.streaming) {
|
||||
s.currentText = ''
|
||||
resetDelta()
|
||||
return
|
||||
}
|
||||
// 从末尾向前找最后一个非 isError assistant 气泡写入(跳过重试错误气泡,保留部分回复)
|
||||
for (let i = s.messages.length - 1; i >= 0; i--) {
|
||||
const m = s.messages[i]
|
||||
|
||||
@@ -97,6 +97,10 @@ export function handleEvent(event: AiChatEvent) {
|
||||
if (event.type === 'AiCompleted' || event.type === 'AiError' || event.type === 'AiHelpRequired') {
|
||||
// 非当前会话终止:收敛会话状态(删 Map 项回不在生成)
|
||||
convStates.delete(convId || '')
|
||||
// 同步清该会话 per-conv 流式态。残留的 streaming/currentText 是死数据:切回时会被
|
||||
// switchConversation 恢复进 currentText,下轮发送的防御 flush 又把它写进上一轮气泡,
|
||||
// 把已完成回复覆盖为残片;回复正文以切换时 DB 加载为准,流式态无需保留。
|
||||
clearConvStreamState(convId || '')
|
||||
// FE1:后台会话终止只刷新列表(防抖调度,避免与 AiCompleted 收尾的 notify 双拉)。
|
||||
scheduleConversationsRefresh()
|
||||
// 后台会话终止也触发队列续发/清队(完成续发;错误清该会话队列)
|
||||
|
||||
@@ -505,6 +505,9 @@ async function sendQueuedNow(index: number) {
|
||||
* 同时推一条 stopLoopFailed 错误消息让用户知晓停止未生效(可再点或等待收尾)。
|
||||
*/
|
||||
async function stopChat() {
|
||||
// 本地复位前先回填已流出文本:streaming 翻 false 后流式分块即清,迟到的 AiCompleted
|
||||
// 又会因 flushCurrentText 的非流式守卫跳过回填,不在此处 flush 则部分回复随停止丢失。
|
||||
flushCurrentText()
|
||||
setStreaming(false, { convId: state.activeConversationId, reason: 'stopChat' }) // 本地先复位,不依赖后端 AiCompleted(审批态看门狗已 clear,竞态丢失则卡死)
|
||||
clearStreamWatchdog() // 清残留看门狗
|
||||
// UX-260616-05 决策 a(逐条续发):不再清队列,保留供 AiCompleted 链式触发 drainQueue 续发。
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
import { state } from '@/stores/ai'
|
||||
import { t } from '@/i18n/i18n-helpers'
|
||||
import { nextMsgId, convStates } from './aiShared'
|
||||
import { nextMsgId, convStates, flushCurrentText } from './aiShared'
|
||||
import type { MessageId } from '@/api/types'
|
||||
import { forceResetStreaming } from './streamingGuard'
|
||||
import { emit } from '@tauri-apps/api/event'
|
||||
@@ -67,6 +67,10 @@ export function onStreamTimeout(convId?: string) {
|
||||
// 不管 stillGenerating 检测(即使已经 false 也再清理一次,完备包底)。
|
||||
console.warn(`[AI-Watchdog] 超时触发,强制收敛状态${convId ? `(convId=${convId})` : '(全局)'}`)
|
||||
|
||||
// 先把已流出的部分回复回填气泡再清态:forceResetStreaming 会清 currentText,streaming
|
||||
// 翻 false 又清流式分块,不 flush 则屏幕上已显示的部分正文一并蒸发(内容空白消失)。
|
||||
// flushCurrentText 只认当前活跃会话的流式态,仅本会话是当前视图时才 flush。
|
||||
if (!convId || convId === state.activeConversationId) flushCurrentText()
|
||||
// 彻底清理三处状态源,保证状态机收敛到 idle
|
||||
forceResetStreaming('onStreamTimeout(无数据包底)', convId)
|
||||
if (convId) {
|
||||
|
||||
Reference in New Issue
Block a user