重构: aichat 双轨状态机收口 + AiCompressed 事件拆分

- generating bool + CONV_STATE_ENABLED 开关双轨退役,ConvState enum 单一真相源
- can_accept_request 接入 chat 域入口(覆盖 Stopping 竞态,严谨于 is_active)
- AiCompressed 拆 AiManualCompressed/AiAutoCompressed(治自动压缩误触桌面toast+刷新)
- convStates/getConvState 下沉 aiShared.ts 破循环依赖
This commit is contained in:
lxy
2026-06-25 03:20:42 +08:00
parent fc705443bd
commit c011f864fd
22 changed files with 526 additions and 428 deletions
+17 -7
View File
@@ -7,14 +7,15 @@
//! - 模块级 listener:消费后端 emit 的 ai-chat-event 生命周期事件(serde 默认 PascalCase,
//! 对齐 useAiEvents.ts 既有 9 事件):
//! AiCompressing → isCompressing=true
//! AiCompressed → isCompressing=false + (成功 toast 由调用方弹,摘要已在事件里)
//! AiManualCompressed → isCompressing=false + (手动 IPC,成功 toast 由调用方弹,摘要已在事件里)
//! AiAutoCompressed → isCompressing=false + (loop 自动压缩,静默仅复位,治 Task#1 不弹 toast)
//! AiContextCleared → (刷新提示由调用方弹)
//! AiError → isCompressing=false + (错误 toast 由调用方弹)
//!
//! 设计:
//! - 与 useAiEvents.startListener 共存:后者监听 AiChatEvent 联合类型(不含
//! AiCompressing/AiCompressed/AiContextCleared,因 types.ts 不在本任务白名单),
//! 本模块注册第二个 listen('ai-chat-event') 监听器,专门按 type 字符串分发上述 4 类事件。
//! AiCompressing/AiManualCompressed|AiAutoCompressed/AiContextCleared,因 types.ts 不在本任务白名单),
//! 本模块注册第二个 listen('ai-chat-event') 监听器,专门按 type 字符串分发上述 5 类事件。
//! 两个监听器都触发同一后端事件,但各取所需类型,Tauri 事件多 listener 是合法的。
//! - toast/刷新副作用经回调注入(composable 无组件上下文,无 toast ref),
//! AiChat.vue 调 initContextListener(onCleared/onCompressed/onError) 注入副作用,
@@ -32,7 +33,7 @@ import { aiApi } from '@/api'
import { state } from '@/stores/ai'
import { resolveAiLang } from './aiShared'
/// 压缩中 loading(防重复点击;AiCompressing→true / AiCompressed|AiError→false)
/// 压缩中 loading(防重复点击;AiCompressing→true / AiManualCompressed|AiAutoCompressed|AiError→false)
const isCompressing = ref(false)
/// 已注册的 ai-chat-event 第二监听器(生命周期事件);幂等
@@ -43,7 +44,8 @@ let _unlistenContext: UnlistenFn | null = null
interface ContextCallbacks {
/** 清空完成(AiContextCleared):toast + 刷新消息列表 */
onCleared?: (conversationId: string) => void
/** 压缩成功(AiCompressed):toast + 渲染摘要 system 消息 */
/** 手动压缩成功(AiManualCompressed):toast + 渲染摘要 system 消息。
* 注:仅 AiManualCompressed 触发;AiAutoCompressed(loop 自动)静默不进此回调(治 Task#1)。 */
onCompressed?: (conversationId: string, summary: string) => void
/** 压缩失败 / 错误(AiError with conversation_id):toast */
onError?: (conversationId: string, message: string) => void
@@ -76,10 +78,18 @@ async function initContextListener(callbacks: ContextCallbacks = {}): Promise<vo
case 'AiCompressing':
isCompressing.value = true
break
case 'AiCompressed':
case 'AiManualCompressed':
// 手动 IPC 压缩(用户点压缩按钮):复位 isCompressing + 弹 toast + 刷新视图回填摘要。
isCompressing.value = false
_callbacks.onCompressed?.(convId || '', payload.summary || '')
break
case 'AiAutoCompressed':
// 治 Task#1:loop 自动压缩对桌面静默——仅复位 isCompressing(冗余兜底,Auto 路径
// 前端 isCompressing 本就为 false,只有手动 compressContext 才置 true),不调
// onCompressed(防每次发送误弹 toast + 误 switchConversation 打断 LLM 流)。
// 摘要 system 消息已由后端 insert_at,后续 stream 自然带出,无需整会话刷新。
isCompressing.value = false
break
case 'AiContextCleared':
_callbacks.onCleared?.(convId || '')
break
@@ -122,7 +132,7 @@ async function compressContext(): Promise<void> {
try {
await aiApi.compressContext(convId, resolveAiLang())
// 后端 IPC 同步等到 LLM 压缩完成才 resolve(非 spawn 异步),成功即复位 isCompressing;
// case 'AiCompressed' 事件也复位(双保险,防事件丢失致按钮永久禁用)
// case 'AiManualCompressed' 事件也复位(双保险,防事件丢失致按钮永久禁用)
isCompressing.value = false
} catch (e) {
isCompressing.value = false