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