优化: 会话状态 per-conv 化与交互守卫
- 待审批跨会话 per-conv 合并,审批类事件全局放行 - 看门狗/重试计数/流式残段按会话键控迁移 - 输入草稿与子代理完成卡数据按会话隔离 - 切换兜底 activeId,收敛守卫防幽灵气泡 - 发送错误按限流/额度/网络分类文案
This commit is contained in:
@@ -15,8 +15,8 @@ import { listen, emit } from '@tauri-apps/api/event'
|
||||
import { aiApi } from '@/api'
|
||||
import { useAppSettingsStore } from '@/stores/appSettings'
|
||||
import { state } from '@/stores/ai'
|
||||
import { nextMsgId, setConvState, convStates, switchingConvs, getConvStreamState, setConvCurrentText, setConvStreaming, clearConvStreamState, clearAllApprovalTimers, clearAllToolSlowTimers, flushCurrentText, friendlyError, notifyConversationChanged } from './aiShared'
|
||||
import { resetStreamWatchdog, clearAllStreamWatchdogs } from './useAiStream'
|
||||
import { nextMsgId, setConvState, convStates, switchingConvs, getConvStreamState, setConvCurrentText, setConvStreaming, clearConvStreamState, clearAllApprovalTimers, clearAllToolSlowTimers, flushCurrentText, friendlyError, notifyConversationChanged, clearLastDelta, discardConvPendingApprovals, addConvAgentFinished, clearConvAgentFinished, type AgentFinishedStatus } from './aiShared'
|
||||
import { resetStreamWatchdog, clearAllStreamWatchdogs, clearResumeAttempts } from './useAiStream'
|
||||
import { setStreaming } from './streamingGuard'
|
||||
import { scheduleConversationsRefresh } from './useAiConversations'
|
||||
import { handleStreamingEvent } from './useAiStreamingEvents'
|
||||
@@ -44,6 +44,13 @@ const appSettings = useAppSettingsStore()
|
||||
// 模块级 Set 复用,避免每事件(delta/token 高频)新建数组字面量做 includes。
|
||||
const NO_RESET_WATCHDOG = new Set<AiChatEvent['type']>(['AiApprovalRequired', 'AiCompleted', 'AiError', 'AiMaxRoundsReached', 'AiDirAuthRequired', 'AiHelpRequired'])
|
||||
|
||||
/** 全局审批类事件:跨会话放行,不因非当前会话被过滤。审批挂起是等用户决策的跨会话资产,
|
||||
* 丢弃后用户在切回该会话前没有任何感知与处理入口;归口仍按 conversation_id 写 per-conv
|
||||
* 待审批集合(aiShared.addPendingApproval),不污染当前视图。 */
|
||||
function isGlobalApprovalEvent(event: AiChatEvent): boolean {
|
||||
return event.type === 'AiApprovalRequired' || event.type === 'AiDirAuthRequired'
|
||||
}
|
||||
|
||||
/** L2 状态机域:消费 AiConvStateChanged,写 convStates(per-conv conv_state 真相源)。
|
||||
* 返回 true=已处理;无 convId 无法路由时忽略(防误写全局态)。 */
|
||||
function handleConvStateEvent(event: AiChatEvent): boolean {
|
||||
@@ -54,6 +61,34 @@ function handleConvStateEvent(event: AiChatEvent): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/** S1 子代理完成域:消费 AiAgentFinished,写 per-conv 完成卡集合(aiShared.convAgentFinished)。
|
||||
* AiChatEvent 联合类型(types.ts 归口)暂未收编该事件,经局部 shape 窄化取字段,不改 types。
|
||||
* 归集挂在 isCurrent 守卫之前(同 AiConvStateChanged 范式):后台会话的子代理结束也要落卡,
|
||||
* 切回仍可见;「不串当前视图」由消费侧只读当前会话数组天然保证,无需在此按活跃会话过滤。 */
|
||||
function handleAgentFinishedEvent(event: AiChatEvent): boolean {
|
||||
if ((event as { type?: string }).type !== 'AiAgentFinished') return false
|
||||
const ev = event as unknown as {
|
||||
agent_id: string
|
||||
summary: string
|
||||
status: AgentFinishedStatus
|
||||
rounds: number
|
||||
tokens: number
|
||||
}
|
||||
const convId = event.conversation_id
|
||||
if (!convId || !ev.agent_id) return true
|
||||
addConvAgentFinished(convId, {
|
||||
agentId: ev.agent_id,
|
||||
summary: ev.summary,
|
||||
status: ev.status,
|
||||
rounds: ev.rounds,
|
||||
tokens: ev.tokens,
|
||||
})
|
||||
// 子代理执行是该会话的真实活跃信号(父 loop 在 spawn_agent 内等待):此前该事件穿透到
|
||||
// 活跃事件分支会重置看门狗,现提前归集后显式补上,保持既有不误判停滞的语义。
|
||||
resetStreamWatchdog(convId)
|
||||
return true
|
||||
}
|
||||
|
||||
/** 跨端用户消息域:miniapp→桌面 user 气泡同步(仅处理 AiUserMessage)。
|
||||
* 去重:末条已是同 content 的 user 则跳过(防本地乐观 push + 事件回灌双气泡)。 */
|
||||
function handleUserMessageEvent(event: AiChatEvent): boolean {
|
||||
@@ -91,9 +126,12 @@ export function handleEvent(event: AiChatEvent) {
|
||||
if (handleConvStateEvent(event)) return
|
||||
// 跨端用户消息同步:在 isCurrent 守卫之前处理(补 user 气泡信号,非流式活跃事件)
|
||||
if (handleUserMessageEvent(event)) return
|
||||
// 事件不属于当前展示对话(生成中切走了)→ 不污染当前视图,仅完成/错误/求助时刷新侧边栏
|
||||
// S1 子代理完成卡归集:在 isCurrent 守卫之前处理(per-conv 落卡,后台会话不丢卡)
|
||||
if (handleAgentFinishedEvent(event)) return
|
||||
// 事件不属于当前展示对话(生成中切走了)→ 不污染当前视图,仅完成/错误/求助时刷新侧边栏;
|
||||
// 全局审批类事件例外(isGlobalApprovalEvent),放行到下方按域分派
|
||||
const isCurrent = !convId || convId === state.activeConversationId
|
||||
if (!isCurrent) {
|
||||
if (!isCurrent && !isGlobalApprovalEvent(event)) {
|
||||
if (event.type === 'AiCompleted' || event.type === 'AiError' || event.type === 'AiHelpRequired') {
|
||||
// 非当前会话终止:收敛会话状态(删 Map 项回不在生成)
|
||||
convStates.delete(convId || '')
|
||||
@@ -101,6 +139,12 @@ export function handleEvent(event: AiChatEvent) {
|
||||
// switchConversation 恢复进 currentText,下轮发送的防御 flush 又把它写进上一轮气泡,
|
||||
// 把已完成回复覆盖为残片;回复正文以切换时 DB 加载为准,流式态无需保留。
|
||||
clearConvStreamState(convId || '')
|
||||
// 对齐前台 cleanupTerminatedConversation 的 per-conv 收敛:清 delta 去重记录(残留记录
|
||||
// 会在切回后把下一轮首个与上轮末个相同内容的 delta 误判重复丢弃)、待审批集合(终止即作废,
|
||||
// 切回时由 pendingToolCalls 从后端审计表按需重建)、自动续跑计数(终止即清,不带入新轮)
|
||||
clearLastDelta(convId || '')
|
||||
discardConvPendingApprovals(convId || '')
|
||||
clearResumeAttempts(convId || undefined)
|
||||
// FE1:后台会话终止只刷新列表(防抖调度,避免与 AiCompleted 收尾的 notify 双拉)。
|
||||
scheduleConversationsRefresh()
|
||||
// 后台会话终止也触发队列续发/清队(完成续发;错误清该会话队列)
|
||||
@@ -159,6 +203,10 @@ export async function startListener() {
|
||||
setStreaming(false, { convId: deleted, reason: 'conv-deleted' })
|
||||
convStates.delete(deleted)
|
||||
}
|
||||
// S1:会话删除连带清其完成卡(Map 不持已删会话条目;存在即清,不限当前视图)
|
||||
if (deleted) {
|
||||
clearConvAgentFinished(deleted)
|
||||
}
|
||||
})
|
||||
_unlistenApprovalClear = await listen('ai-approval-clear-timers', () => { clearAllApprovalTimers() })
|
||||
emit('ai-client-ready', { lastConvId: state.activeConversationId })
|
||||
|
||||
Reference in New Issue
Block a user