优化: 会话状态 per-conv 化与交互守卫

- 待审批跨会话 per-conv 合并,审批类事件全局放行
- 看门狗/重试计数/流式残段按会话键控迁移
- 输入草稿与子代理完成卡数据按会话隔离
- 切换兜底 activeId,收敛守卫防幽灵气泡
- 发送错误按限流/额度/网络分类文案
This commit is contained in:
lxy
2026-10-03 15:35:15 +08:00
parent d406a31722
commit b4fafa0e9a
13 changed files with 460 additions and 96 deletions
+16 -11
View File
@@ -21,7 +21,7 @@ import { aiApi } from '@/api'
import { state } from '@/stores/ai'
import type { ContentPart, AiMessage, MentionSpan, MessageId } from '@/api/types'
import { t } from '@/i18n/i18n-helpers'
import { resetStreamWatchdog, clearStreamWatchdog } from './useAiStream'
import { resetStreamWatchdog, clearStreamWatchdog, clearResumeAttempts } from './useAiStream'
import { setStreaming } from './streamingGuard'
import { startListener } from './useAiEvents'
import { nextMsgId, startApprovalTimer, clearApprovalTimer, clearAllApprovalTimers, resolveAiLang, convStates, setConvCurrentText, setConvStreaming, flushCurrentText } from './aiShared'
@@ -166,7 +166,8 @@ async function regenerate() {
// 只清当前会话的排队消息,不误伤其他会话
const regenConvId = state.activeConversationId
state.queue = state.queue.filter(q => q.conversationId !== regenConvId)
resetStreamWatchdog()
// 会话态走 per-conv 键(与 doSend 一致),不再落 legacy 单 timer fallback
resetStreamWatchdog(regenConvId || undefined)
await startListener()
const lang = resolveAiLang()
@@ -174,7 +175,7 @@ async function regenerate() {
if (!convId) {
// 无活跃对话:回滚占位,报错
setStreaming(false, { reason: 'regenerate-no-conv' })
clearStreamWatchdog()
clearStreamWatchdog(regenConvId || undefined)
state.messages = state.messages.filter(m => m.id !== aiMsgId)
throw new Error(t('aiChat.regenerateUnavailable'))
}
@@ -183,7 +184,7 @@ async function regenerate() {
} catch (e) {
// IPC 失败:回滚 streaming + 移除占位气泡(旧回复后端已弹但前端先弹了,保持弹后态)
setStreaming(false, { convId, reason: 'regenerate-ipc-fail' })
clearStreamWatchdog()
clearStreamWatchdog(convId || undefined)
state.messages = state.messages.filter(m => m.id !== aiMsgId)
throw e
}
@@ -238,14 +239,15 @@ async function editMessage(newMessage: string) {
// 只清当前会话的排队消息,不误伤其他会话
const editConvId = state.activeConversationId
state.queue = state.queue.filter(q => q.conversationId !== editConvId)
resetStreamWatchdog()
// 会话态走 per-conv 键(与 doSend 一致),不再落 legacy 单 timer fallback
resetStreamWatchdog(editConvId || undefined)
await startListener()
const lang = resolveAiLang()
const convId = state.activeConversationId
if (!convId) {
setStreaming(false, { reason: 'editMessage-no-conv' })
clearStreamWatchdog()
clearStreamWatchdog(editConvId || undefined)
state.messages = state.messages.filter(m => m.id !== aiMsgId)
throw new Error(t('aiChat.editNotLastUser'))
}
@@ -254,7 +256,7 @@ async function editMessage(newMessage: string) {
} catch (e) {
// IPC 失败:回滚 streaming + 移除占位气泡(被截断的旧回复后端已标 truncated,前端已移除)
setStreaming(false, { convId, reason: 'editMessage-ipc-fail' })
clearStreamWatchdog()
clearStreamWatchdog(convId || undefined)
state.messages = state.messages.filter(m => m.id !== aiMsgId)
throw e
}
@@ -395,7 +397,7 @@ export async function tryForceSend(confirmFn: (msg: string) => Promise<boolean>,
state.queue.splice(idx, 1)
// 清看门狗:doSend 内 force_send 路径会重设 streaming=true 并重启看门狗,
// 此处无需前置 false(前置 false 会触发消息列表 watch 清流式块再重建,产生瞬态抖动)
clearStreamWatchdog()
clearStreamWatchdog(targetConv || undefined)
try {
await sendMessage(first.text, first.skill, true, first.parts, first.spans, first.conversationId)
return true
@@ -411,7 +413,7 @@ export async function tryForceSend(confirmFn: (msg: string) => Promise<boolean>,
conversationId: first.conversationId,
})
setStreaming(false, { convId: targetConv ?? state.activeConversationId, reason: 'tryForceSend-fail' })
clearStreamWatchdog()
clearStreamWatchdog(targetConv || undefined)
console.error('[AI] tryForceSend force_send 失败,消息已回填队首:', e)
return false
}
@@ -509,7 +511,10 @@ async function stopChat() {
// 又会因 flushCurrentText 的非流式守卫跳过回填,不在此处 flush 则部分回复随停止丢失。
flushCurrentText()
setStreaming(false, { convId: state.activeConversationId, reason: 'stopChat' }) // 本地先复位,不依赖后端 AiCompleted(审批态看门狗已 clear,竞态丢失则卡死)
clearStreamWatchdog() // 清残留看门狗
// 停止属正常收尾:按目标会话清 per-conv 看门狗与自动续跑计数(计数只放收尾路径清,
// 不在发消息时清,否则会掩盖单轮内的真实重试次数)
clearStreamWatchdog(state.activeConversationId || undefined)
clearResumeAttempts(state.activeConversationId || undefined)
// 决策 a(逐条续发):不再清队列,保留供 AiCompleted 链式触发 drainQueue 续发。
// 后端 agentic/mod.rs emit AiCompleted 注释明确「保证前端收事件时后端已可接下一条,队列续发不被拒」;
// 当前未完成回复已入库(agentic/mod.rs save_conversation)保留为截断回复。
@@ -519,7 +524,7 @@ async function stopChat() {
} catch (e) {
// stop 信号未送达后端:重启看门狗(后端继续生成时活跃事件会重置它,真 hang 则 130s 后兜底收尾),
// 避免后端仍在跑却无任何恢复路径(原 clearStreamWatchdog 已把兜底关掉)。
resetStreamWatchdog()
resetStreamWatchdog(state.activeConversationId || undefined)
const errMsg = e instanceof Error ? e.message : String(e)
state.messages.push({
id: `stop-err-${nextMsgId()}`,