优化: L2统一状态机完整(ConvState持久化+事件暴露+前端status联合)
1a PerConvState.conv_state持久化(入口/reset/drop写收敛,CONV_STATE_ENABLED门控) 1b AiChatEvent加AiConvStateChanged变体,guard持app_handle在迁移点emit 1c 前端convStates真相源+停止按钮三态(status union)+MaxRoundsCard读conv_state 双轨过渡:enum视图与generating bool并行,off降级可回退
This commit is contained in:
@@ -20,7 +20,7 @@
|
||||
//! - nextMsgId 已下沉到 aiShared(原为本模块导出,useAiStream 亦依赖之构成循环依赖,故抽出)
|
||||
|
||||
import { listen, emit } from '@tauri-apps/api/event'
|
||||
import { ref } from 'vue'
|
||||
import { ref, reactive } from 'vue'
|
||||
import { aiApi } from '@/api'
|
||||
import { useAppSettingsStore } from '@/stores/appSettings'
|
||||
import { state } from '@/stores/ai'
|
||||
@@ -29,7 +29,7 @@ import { nextMsgId, findToolCall, startApprovalTimer, clearApprovalTimer, clearA
|
||||
import { resetStreamWatchdog, clearStreamWatchdog, clearAllStreamWatchdogs } from './useAiStream'
|
||||
import { setStreaming } from './streamingGuard'
|
||||
import { loadConversations } from './useAiConversations'
|
||||
import type { AiChatEvent, AiMessage, AiToolCallInfo } from '@/api/types'
|
||||
import type { AiChatEvent, AiMessage, AiToolCallInfo, ConvState } from '@/api/types'
|
||||
|
||||
let _unlistenAiEvent: (() => void) | null = null
|
||||
let _unlistenConvChanged: (() => void) | null = null
|
||||
@@ -138,6 +138,36 @@ export interface PendingHelp {
|
||||
}
|
||||
export const pendingHelp = ref<PendingHelp | null>(null)
|
||||
|
||||
// L2 统一状态机(批2 1c):per-conv conv_state 真相源(消费后端 AiConvStateChanged 事件)。
|
||||
//
|
||||
// 模块级 reactive Map(convId → ConvState)直接 import 读(同 pendingMaxRounds/pendingHelp 模式,
|
||||
// 不进 store.state,与生成生命周期语义独立于消息/会话列表)。停止按钮/MaxRoundsCard 读之
|
||||
// 派生三态(generating/stopping/error),旧 streaming/generatingConvs bool 双轨过渡保留——
|
||||
// conv_state 未收到时(CONV_STATE_ENABLED=off 或后端老版)消费方回退旧 bool 判断(兜底)。
|
||||
//
|
||||
// 写收敛:仅本模块 handleEvent 的 AiConvStateChanged case 写(setConvState 辅助),其他地方只读。
|
||||
// 兜底同步:idle/error/compressed 在 case 内同步做旧 generatingConvs 收敛(AiCompleted/AiError 同义),
|
||||
// 保持双轨真相一致——后端 emit 可能与 AiCompleted/AiError 不严格按序到达,任一来源触发收敛均可。
|
||||
export const convStates = reactive(new Map<string, ConvState>())
|
||||
|
||||
/** 取某 conv 的 conv_state;未追踪过返回 null(消费方据此回退旧 bool 判断) */
|
||||
export function getConvState(convId: string | null | undefined): ConvState | null {
|
||||
if (!convId) return null
|
||||
return convStates.get(convId) ?? null
|
||||
}
|
||||
|
||||
/** 写入 conv_state(本模块内 AiConvStateChanged case 唯一写入口) */
|
||||
function setConvState(convId: string | null | undefined, s: ConvState): void {
|
||||
if (!convId) return
|
||||
// idle 为终态收敛:写后可保留也可删,这里删以让 getConvState 回退旧 bool(语义对齐:
|
||||
// 后端 generating=false 即 Idle,前端旧 bool 也是 false,删 Map 项后消费方回退 bool 即得正确态)。
|
||||
if (s === 'idle') {
|
||||
convStates.delete(convId)
|
||||
} else {
|
||||
convStates.set(convId, s)
|
||||
}
|
||||
}
|
||||
|
||||
/** 通知会话列表发生变化(供 newConversation/deleteConversation/rename/archive 等触发刷新侧栏) */
|
||||
export function notifyConversationChanged() {
|
||||
emit('ai-conversation-changed', {})
|
||||
@@ -470,6 +500,34 @@ function handleToolEvent(event: AiChatEvent): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/** L2 状态机域:消费 AiConvStateChanged,维护 per-conv conv_state 真相源。
|
||||
*
|
||||
* 非终止态(generating/stopping/compressed):仅写 convStates,不动看门狗/旧 bool
|
||||
* (看门狗由活跃 delta/工具事件重置,这里不干预;旧 bool 由 AiCompleted/AiError 收敛保持双轨一致)。
|
||||
* 终止态(idle/error):
|
||||
* - 写 convStates(idle 删 Map 项让消费方回退旧 bool,与后端 generating=false 语义对齐)
|
||||
* - 同步做旧 generatingConvs 收敛(delete,对齐 AiCompleted/AiError 收尾语义)——
|
||||
* 后端 emit AiConvStateChanged{idle} 与 AiCompleted 可能不严格按序到达,任一先到即收敛,
|
||||
* 双轨真相一致,后到的另一事件幂等(delete 已无项 no-op)。
|
||||
* 返回值:true=已处理(命中 case);false=未命中。 */
|
||||
function handleConvStateEvent(event: AiChatEvent): boolean {
|
||||
if (event.type !== 'AiConvStateChanged') return false
|
||||
const convId = event.conversation_id || state.activeConversationId
|
||||
if (!convId) return true // 无 convId 无法路由,忽略(防误写全局态)
|
||||
const cs = event.conv_state
|
||||
setConvState(convId, cs)
|
||||
// 终止态同步收敛旧 generatingConvs(双轨过渡,与 AiCompleted/AiError 同义)。
|
||||
// compressed 非终止(派生态,压缩完回原态),不收敛。
|
||||
if (cs === 'idle' || cs === 'error') {
|
||||
state.generatingConvs.delete(convId)
|
||||
} else if (cs === 'generating' || cs === 'stopping' || cs === 'compressed') {
|
||||
// 非终止态同步加 generatingConvs(确保旧 bool 真相与 conv_state 一致,
|
||||
// 防 AiConvStateChanged{generating} 先于首个 delta 到达时旧 bool 漏加致停止按钮/卡片错态)。
|
||||
state.generatingConvs.add(convId)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/** lifecycle 域:整轮收尾(AiCompleted)与异常中断(AiError) */
|
||||
function handleLifecycleEvent(event: AiChatEvent): boolean {
|
||||
switch (event.type) {
|
||||
@@ -480,6 +538,10 @@ function handleLifecycleEvent(event: AiChatEvent): boolean {
|
||||
state.currentText = ''
|
||||
setStreaming(false, { convId: event.conversation_id || null, reason: 'AiCompleted' })
|
||||
state.generatingConvs.delete(event.conversation_id || '')
|
||||
// L2 双轨过渡:AiCompleted 收尾同步收敛 convStates→idle(删 Map 项)。
|
||||
// 后端 CONV_STATE_ENABLED=on 时 AiConvStateChanged{idle} 已先到此处幂等 no-op;
|
||||
// off 或老后端无 AiConvStateChanged 时此处保证 convStates 不陈旧(消费方回退旧 bool 一致)。
|
||||
convStates.delete(event.conversation_id || '')
|
||||
state.agentRound = 0 // AE-2025-07: agentic 结束,复位轮次(隐藏进度条)
|
||||
// TD-260621-02 per-conv:仅当 pendingMaxRounds===本 conv 才清 null(避免清其他会话的挂起)。
|
||||
if (pendingMaxRounds.value && pendingMaxRounds.value === (event.conversation_id || '')) {
|
||||
@@ -544,6 +606,10 @@ function handleLifecycleEvent(event: AiChatEvent): boolean {
|
||||
flushCurrentText()
|
||||
setStreaming(false, { convId: event.conversation_id || null, reason: 'AiError' })
|
||||
state.generatingConvs.delete(event.conversation_id || '')
|
||||
// L2 双轨过渡:AiError 收尾将 convStates 显式置 error(保留 error 项,供停止按钮显"重试"态)。
|
||||
// 与 AiConvStateChanged{error} 双写幂等(后端 on 时状态事件已先到;off/老后端时此处保证 Error 态可见)。
|
||||
// error 态在新会话发送(AiTextDelta 等首事件到达→generatingConvs.add)或下次 AiCompleted 时自然收敛。
|
||||
if (event.conversation_id) convStates.set(event.conversation_id, 'error')
|
||||
state.currentText = ''
|
||||
state.agentRound = 0 // AE-2025-07: agentic 异常中断,复位轮次
|
||||
state.queue = [] // B-32:错误收尾清队列,防生成中入队的消息被静默丢失(drainQueue 仅 AiCompleted 触发)
|
||||
@@ -640,6 +706,10 @@ export function handleEvent(event: AiChatEvent) {
|
||||
state.activeConversationId = convId
|
||||
void appSettings.set('df-ai-active-conv', convId)
|
||||
}
|
||||
// L2 状态机:AiConvStateChanged 是 per-conv 状态信号(F-09 多会话并发下须追踪所有会话),
|
||||
// 在 isCurrent 守卫**之前**处理,避免切走会话时 conv_state 不更新致停止按钮/MaxRoundsCard 错态。
|
||||
// 该事件非流式活跃信号,不重置看门狗、不进 generatingConvs 通用 add 逻辑(由 conv_state 值决定 add/delete)。
|
||||
if (handleConvStateEvent(event)) return
|
||||
// 事件不属于当前展示对话(生成中切走了)→ 不污染当前视图,仅完成/错误/求助时刷新侧边栏
|
||||
// L1 求助协议(§2.3):AiHelpRequired 同 AiError 后端已 guard.reset 终止 loop,需移出生成集。
|
||||
const isCurrent = !convId || convId === state.activeConversationId
|
||||
@@ -654,6 +724,10 @@ export function handleEvent(event: AiChatEvent) {
|
||||
// 标记正在生成的对话(完成/错误/求助事件除外——三者后端均已终止 loop)
|
||||
if (convId && event.type !== 'AiCompleted' && event.type !== 'AiError' && event.type !== 'AiHelpRequired') {
|
||||
state.generatingConvs.add(convId)
|
||||
// L2 双轨过渡:会话进入生成(活跃事件到达)时,清 convStates 中可能残留的 error 项,
|
||||
// 避免上一轮错误态 conv_state 未收敛致停止按钮错显"重试"(此时实际已在生成)。
|
||||
// CONV_STATE_ENABLED=on 时后端 AiConvStateChanged{generating} 已更新;此处兜底 off/老后端。
|
||||
convStates.delete(convId)
|
||||
}
|
||||
// 流式看门狗:活跃事件(delta/工具/新轮/审批结果)重置;审批等待/完成/错误在 case 内 clear
|
||||
// TD-260621-01 per-conv:传 convId 走 per-conv Map(各会话独立计时,互不顶替/连累)。
|
||||
|
||||
Reference in New Issue
Block a user