重构: 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
+16 -8
View File
@@ -10,7 +10,7 @@
import { invoke } from '@tauri-apps/api/core'
import { state } from '@/stores/ai'
import { nextMsgId } from './aiShared'
import { nextMsgId, getConvState, convStates } from './aiShared'
import { setStreaming } from './streamingGuard'
import { persistUiState } from './useAiPanel'
@@ -52,18 +52,24 @@ async function detachPanel(convId?: string) {
}
// 快照当前生成态,供分离窗口接管(保持正在进行的对话)
// F-09:用 per-conv key 写入(多会话各自快照互不覆盖)
if (state.streaming && targetConv && state.generatingConvs.has(targetConv)) {
localStorage.setItem(genKey(targetConv), targetConv)
localStorage.setItem(textKey(targetConv), state.currentText)
// 批4 双轨收口:读 getConvState(enum 真相源)派生,替代旧 generatingConvs.has。
// 桥接语义:has(convId)=true 等价于 conv_state∈{generating,stopping,compressed}(非终止三态)。
if (state.streaming && targetConv) {
const cs = getConvState(targetConv)
const gen = cs === 'generating' || cs === 'stopping' || cs === 'compressed'
if (gen) {
localStorage.setItem(genKey(targetConv), targetConv)
localStorage.setItem(textKey(targetConv), state.currentText)
}
}
// 主窗口 state 与分离窗口 state 独立(各自 webview 独立 JS context,
// stores/ai.ts 模块级单例仅在同 webview 内共享),清主窗口 state 不影响分离窗口生成态。
// 分离窗口接管生成后,主窗口不应再持该会话生成态残留(防重开面板时 UI 显生成中幽灵/进度条)。
// F-09:仅从 Set 移除该 conv(其他会话可能仍在生成),不全局清空。
// F-09:仅从 convStates 移除该 conv(其他会话可能仍在生成),不全局清空。
// watchdog 在主窗口 AiChat 卸载(App.vue v-if detached)→ stopListener → clearStreamWatchdog 时已清,
// 此处仅清内存 state 视觉残留;localStorage 快照保留供 resumeInDetached 恢复。
setStreaming(false, { convId: targetConv || null, reason: 'detachPanel-clear-main' })
if (targetConv) state.generatingConvs.delete(targetConv)
if (targetConv) convStates.delete(targetConv)
const sep = targetConv ? `?conv=${encodeURIComponent(targetConv)}` : ''
const url = window.location.origin + window.location.pathname + '#/ai-detached' + sep
const win = new WebviewWindow(label, {
@@ -135,7 +141,7 @@ function cleanupAllDetachedSnapshots(): void {
* 1. state.currentText = df-ai-text-${convId}(已生成文本)
* 2. push 占位 assistant 气泡(content='')
* 3. state.streaming = true
* 4. state.generatingConvs.add(df-ai-gen)
* 4. convStates.set(df-ai-gen, 'generating')(批4 双轨收口:替代旧 generatingConvs.add)
*
* 幂等:若 state.streaming 已是 true(已恢复过/正在生成),直接返回不重复 push。
* 这对主面板刷新场景至关重要:detachPanel 时主窗口 state 已被清 streaming=false
@@ -194,7 +200,9 @@ export async function restoreGeneratingState(opts?: { fromMainPanel?: boolean; c
timestamp: Date.now(),
})
setStreaming(true, { convId: gen, reason: 'restoreGeneratingState' })
state.generatingConvs.add(gen)
// 批4 双轨收口:恢复生成中态写 convStates(enum 真相源),替代旧 generatingConvs.add。
// setStreaming 联动已退役(批4),streaming 单值与 convStates 项并行写无冲突。
convStates.set(gen, 'generating')
return true
}