优化: 聊天体验批与Web规范合规

This commit is contained in:
lxy
2026-10-05 23:41:32 +08:00
parent 9e81182b4f
commit 28e6acbf3f
41 changed files with 1518 additions and 570 deletions
+52 -1
View File
@@ -12,6 +12,7 @@
import { invoke } from '@tauri-apps/api/core'
import { state } from '@/stores/ai'
import { useAppSettingsStore } from '@/stores/appSettings'
import { nextMsgId, getConvState, convStates } from './aiShared'
import type { MessageId } from '@/api/types'
import { setStreaming } from './streamingGuard'
@@ -58,6 +59,45 @@ export async function emitDetachedOpenConversation(convId: string): Promise<void
}
}
// ── V11b(2026-10-05)跨窗收回分离窗:reattachPanel 的 UI 入口 ──
// 病灶:reattachPanel(关闭全部分离窗+回主面板)全仓零 UI 调用——分离窗内「关闭」按钮走
// closeDetachedWindow(仅关窗不恢复主面板),用户想收回只能手动重开主面板。主/分离窗是
// 独立 JS context,分离窗按钮无法直调主窗 store;走 Tauri 全局事件广播(对齐 OPEN_CONV_EVENT
// 模式):分离窗 emit → 主窗(label 判定)监听 → 执行 reattachPanel。
const REATTACH_EVENT = 'ai-reattach-panel'
/** 发起方(分离窗)侧:广播收回事件,主窗口消费 */
export async function emitReattachToMain(): Promise<void> {
try {
const { emit } = await import('@tauri-apps/api/event')
await emit(REATTACH_EVENT, {})
} catch (e) {
console.error('[AI] 跨窗收回事件发送失败:', e)
}
}
/** 承接方(主窗口)侧:注册收回监听。handler=reattachPanel(由 AiChat 挂载时注入,归窗口域) */
let _reattachListenerRegistered = false
export function registerMainReattachListener(handler: () => Promise<void>): void {
if (_reattachListenerRegistered) return
_reattachListenerRegistered = true
void (async () => {
try {
const [{ listen }, { getCurrentWebviewWindow }] = await Promise.all([
import('@tauri-apps/api/event'),
import('@tauri-apps/api/webviewWindow'),
])
if (getCurrentWebviewWindow().label !== 'main') return
await listen<Record<string, never>>(REATTACH_EVENT, async () => {
await handler()
})
} catch (e) {
console.error('[AI] 主窗口收回监听注册失败:', e)
_reattachListenerRegistered = false
}
})()
}
/** 承接方(分离窗)侧:注册跨窗切会话监听,切换完成后聚焦本窗口(点击发生在主窗口,
* 焦点须跟到承接会话的窗口,否则用户感知「点了没反应」)。
* handler 由 useAiConversations 注入:切会话逻辑归会话域,避免本模块反向依赖造成模块环。 */
@@ -114,6 +154,12 @@ async function detachPanel(convId?: string) {
localStorage.setItem(textKey(targetConv), state.currentText)
}
}
// V11c: 种子分离窗活跃会话 KV——分离窗 AiChat 挂载同样跑 loadConversations(),
// 读按窗口域分流的 'df-ai-active-conv-detached'(独立 JS context 不与主窗共 pinia state),
// 不种子则分离窗启动回落列表首条而非主窗正看的会话。目标 conv 为空(无活跃会话)跳过。
if (targetConv) {
void useAppSettingsStore().set('df-ai-active-conv-detached', targetConv)
}
// 主窗口 state 与分离窗口 state 独立(各自 webview 独立 JS context,
// stores/ai.ts 模块级单例仅在同 webview 内共享),清主窗口 state 不影响分离窗口生成态。
// 分离窗口接管生成后,主窗口不应再持该会话生成态残留(防重开面板时 UI 显生成中幽灵/进度条)。
@@ -133,7 +179,10 @@ async function detachPanel(convId?: string) {
minHeight: 400,
center: true,
decorations: true,
alwaysOnTop: true, // 分离窗口默认置顶(AI 助手常态:用户在主窗口工作时希望 AI 辅助窗口可见)
// V11a(2026-10-05):默认不置顶——原 true 会让分离窗盖住其他应用(VSCode/浏览器)妨碍工作;
// 用户开过置顶的偏好走 AiChat.vue 的 df-ai-always-on-top KV 恢复链(分离窗挂载时
// setAlwaysOnTop(saved)),默认改 false 不影响已开置顶用户,只改变首次/未开者的初始态。
alwaysOnTop: false,
})
win.once('tauri://created', () => {
state.panelOpen = false
@@ -183,6 +232,8 @@ async function reattachPanel() {
state.docked = false
state.panelOpen = true
persistUiState()
// V11c: 清分离窗活跃会话 KV(分离窗已全关,残留无意义;下次 detachPanel 会重新种子)
void useAppSettingsStore().remove('df-ai-active-conv-detached')
// 时序加固:reattach 关闭分离窗口后,显式停分离窗口残留 listener(分离窗口 close 异步,
// onBeforeUnmount stopListener 可能延迟),防主面板重新挂载 startListener 时与旧 listener 重叠竞态。
stopListener()