修复: 主面板刷新后流式恢复(restoreGeneratingState 加 fromMainPanel 绕过 localStorage 快照)
This commit is contained in:
@@ -2179,10 +2179,20 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
// F-15 阶段2: 注册上下文管理事件监听器(清空/压缩生命周期事件 → toast/刷新)
|
// F-15 阶段2: 注册上下文管理事件监听器(清空/压缩生命周期事件 → toast/刷新)
|
||||||
await initContextEventListeners()
|
await initContextEventListeners()
|
||||||
// 分离模式:接管主窗口当前对话(含生成中态),保持正在进行的对话
|
// 恢复生成中态(刷新后/分离窗口接管):
|
||||||
|
// - 分离模式(detached=true):resumeInDetached 接管主窗口当前对话
|
||||||
|
// - 主面板模式(detached=false):restoreGeneratingState 恢复主面板自身的生成态
|
||||||
|
// (UX-260617-13: 主面板刷新后无等价恢复链 → streaming=false + 无占位气泡 + 后端
|
||||||
|
// 事件不进流式分支,退化为非流式。现对齐 resumeInDetached 同款恢复链)。
|
||||||
|
// 两者互斥(detached 三元决定),不会双重恢复;restoreGeneratingState 内幂等守卫(state.streaming 已 true 则跳过)。
|
||||||
|
// 在 startListener(L0 ai-client-ready 握手)之后调用,握手仍先于恢复逻辑生效。
|
||||||
if (props.detached) {
|
if (props.detached) {
|
||||||
const params = new URLSearchParams(location.hash.split('?')[1] || '')
|
const params = new URLSearchParams(location.hash.split('?')[1] || '')
|
||||||
await store.resumeInDetached(params.get('conv'))
|
await store.resumeInDetached(params.get('conv'))
|
||||||
|
} else {
|
||||||
|
// fromMainPanel=true: 主面板刷新无 df-ai-gen 快照(detachPanel 未触发),
|
||||||
|
// 用 activeConversationId + ai_is_generating 真值守卫恢复(绕过 localStorage 路径)
|
||||||
|
await store.restoreGeneratingState({ fromMainPanel: true })
|
||||||
}
|
}
|
||||||
console.debug(`[启动] AiChat IPC 完成: ${(performance.now() - t0).toFixed(0)}ms`)
|
console.debug(`[启动] AiChat IPC 完成: ${(performance.now() - t0).toFixed(0)}ms`)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
//! - _unlistenMove / _unlistenResize: 主窗口 move/resize 跟随的 unlistener
|
//! - _unlistenMove / _unlistenResize: 主窗口 move/resize 跟随的 unlistener
|
||||||
//!
|
//!
|
||||||
//! 耦合:
|
//! 耦合:
|
||||||
//! - resumeInDetached 调 aiShared.nextMsgId 占位 ai 气泡 + invoke('ai_is_generating') 核对后端真值
|
//! - resumeInDetached / restoreGeneratingState 调 aiShared.nextMsgId 占位 ai 气泡 +
|
||||||
|
//! invoke('ai_is_generating') 核对后端真值(主面板刷新 + 分离窗口恢复共用同款逻辑)
|
||||||
//! - reattachPanel/detachPanel 调 useAiPanel.persistUiState
|
//! - reattachPanel/detachPanel 调 useAiPanel.persistUiState
|
||||||
|
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
@@ -83,39 +84,75 @@ async function reattachPanel() {
|
|||||||
localStorage.removeItem('df-ai-text')
|
localStorage.removeItem('df-ai-text')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重建生成中态(分离窗口 resumeInDetached 与主面板 onMounted 恢复共用)。
|
||||||
|
*
|
||||||
|
* 读 localStorage df-ai-gen/df-ai-text,核对后端 ai_is_generating 真值后重建:
|
||||||
|
* 1. state.currentText = df-ai-text(已生成文本)
|
||||||
|
* 2. push 占位 assistant 气泡(content='')
|
||||||
|
* 3. state.streaming = true
|
||||||
|
* 4. state.generatingConvId = df-ai-gen
|
||||||
|
*
|
||||||
|
* 幂等:若 state.streaming 已是 true(已恢复过/正在生成),直接返回不重复 push。
|
||||||
|
* 这对主面板刷新场景至关重要:detachPanel 时主窗口 state 已被清 streaming=false
|
||||||
|
* (见 detachPanel 注释),分离窗口用各自 webview JS context 独立 state;主面板刷新后
|
||||||
|
* state 从单例初值 streaming=false 起步,本函数安全重建。
|
||||||
|
*
|
||||||
|
* R4: 先核对 ai_is_generating 真值才重建,避免 localStorage 残留导致假气泡 +
|
||||||
|
* 130s 后 watchdog 假超时。后端 false 时清残留快照返回。
|
||||||
|
*
|
||||||
|
* @returns true=已重建;false=未恢复(无快照/后端未生成/核对失败/已生成中)
|
||||||
|
*/
|
||||||
|
export async function restoreGeneratingState(opts?: { fromMainPanel?: boolean }): Promise<boolean> {
|
||||||
|
// 幂等:已生成中态不再重建(防主面板 + 分离窗口共享 state 时双重恢复;
|
||||||
|
// 主面板与分离窗口 state 各自独立,但稳妥起见仍守卫)
|
||||||
|
if (state.streaming) return false
|
||||||
|
const fromMainPanel = opts?.fromMainPanel ?? false
|
||||||
|
// 主面板刷新场景:detachPanel 未触发 → df-ai-gen 不存在。
|
||||||
|
// 此时用 state.activeConversationId 作为生成中会话(刷新前 loadConversations 已恢复),
|
||||||
|
// 无 df-ai-text 快照 → currentText 从空开始(刷新前已生成部分丢失,刷新固有代价;
|
||||||
|
// 后端后续 AiTextDelta 仍会追加,核心是恢复 streaming 态 + 占位气泡消除假死)。
|
||||||
|
// 分离窗口场景:读 detachPanel 写入的 df-ai-gen 快照。
|
||||||
|
const gen = fromMainPanel ? state.activeConversationId : localStorage.getItem('df-ai-gen')
|
||||||
|
if (!gen) return false
|
||||||
|
// 核对后端 ai_is_generating 真值
|
||||||
|
let backendGenerating = false
|
||||||
|
try {
|
||||||
|
backendGenerating = await invoke<boolean>('ai_is_generating')
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[AI] restoreGeneratingState: 核对 ai_is_generating 失败,跳过重建生成态', e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!backendGenerating) {
|
||||||
|
// 后端已不再生成,清残留快照(仅分离窗口快照路径),不重建
|
||||||
|
if (!fromMainPanel) {
|
||||||
|
localStorage.removeItem('df-ai-gen')
|
||||||
|
localStorage.removeItem('df-ai-text')
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// 恢复已生成文本并补占位,后续 delta 继续追加
|
||||||
|
// 主面板无 df-ai-text 快照,保持 currentText 当前值(空),仅靠占位气泡 + streaming 态接收后续 delta
|
||||||
|
if (!fromMainPanel) {
|
||||||
|
state.currentText = localStorage.getItem('df-ai-text') || ''
|
||||||
|
}
|
||||||
|
state.messages.push({
|
||||||
|
id: `ai-${nextMsgId()}`,
|
||||||
|
role: 'assistant',
|
||||||
|
content: '',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
})
|
||||||
|
state.streaming = true
|
||||||
|
state.generatingConvId = gen
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/** 分离窗口挂载时接管主窗口当前对话(含生成中态) */
|
/** 分离窗口挂载时接管主窗口当前对话(含生成中态) */
|
||||||
async function resumeInDetached(convId: string | null) {
|
async function resumeInDetached(convId: string | null) {
|
||||||
// R5: 不自动 switchConversation —— 分离窗口可看历史列表,但不主动覆盖主窗口 activeConversationId
|
// R5: 不自动 switchConversation —— 分离窗口可看历史列表,但不主动覆盖主窗口 activeConversationId
|
||||||
// (避免主窗口期间发消息落错会话)。若需切换会话,由用户在分离窗口显式操作触发。
|
// (避免主窗口期间发消息落错会话)。若需切换会话,由用户在分离窗口显式操作触发。
|
||||||
void convId
|
void convId
|
||||||
const gen = localStorage.getItem('df-ai-gen')
|
await restoreGeneratingState()
|
||||||
if (gen) {
|
|
||||||
// R4: 先核对后端 ai_is_generating 真值,generating=true 才重建生成态,
|
|
||||||
// 避免 df-ai-gen 残留导致假气泡 + 130s 后 watchdog 假超时
|
|
||||||
let backendGenerating = false
|
|
||||||
try {
|
|
||||||
backendGenerating = await invoke<boolean>('ai_is_generating')
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[AI] resumeInDetached: 核对 ai_is_generating 失败,跳过重建生成态', e)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!backendGenerating) {
|
|
||||||
// 后端已不再生成,清残留快照,不重建
|
|
||||||
localStorage.removeItem('df-ai-gen')
|
|
||||||
localStorage.removeItem('df-ai-text')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 接管正在生成的对话:恢复已生成文本并补占位,后续 delta 继续追加
|
|
||||||
state.currentText = localStorage.getItem('df-ai-text') || ''
|
|
||||||
state.messages.push({
|
|
||||||
id: `ai-${nextMsgId()}`,
|
|
||||||
role: 'assistant',
|
|
||||||
content: '',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
})
|
|
||||||
state.streaming = true
|
|
||||||
state.generatingConvId = gen
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 关闭分离窗口自身(分离窗口侧调用) */
|
/** 关闭分离窗口自身(分离窗口侧调用) */
|
||||||
@@ -182,6 +219,7 @@ export function useAiWindow() {
|
|||||||
detachPanel,
|
detachPanel,
|
||||||
reattachPanel,
|
reattachPanel,
|
||||||
resumeInDetached,
|
resumeInDetached,
|
||||||
|
restoreGeneratingState,
|
||||||
closeDetachedWindow,
|
closeDetachedWindow,
|
||||||
dockDetached,
|
dockDetached,
|
||||||
syncToMain,
|
syncToMain,
|
||||||
|
|||||||
Reference in New Issue
Block a user