优化: aichat 打磨收尾(切换信任保留 + save并发串行 + 后台授权守卫 + 分离窗口失效 + 气泡截图复制)

This commit is contained in:
lxy
2026-08-08 21:08:25 +08:00
parent 65c0f6b2b6
commit 6ba6daf188
14 changed files with 196 additions and 27 deletions
+2 -2
View File
@@ -18,8 +18,8 @@ import { t } from '@/i18n/i18n-helpers'
import type { AiMessage, AiToolCallInfo, ConvState, MessageId } from '@/api/types'
/** 通知会话列表刷新(newConversation/deleteConversation/rename 等触发侧栏更新) */
export function notifyConversationChanged(): void {
emit('ai-conversation-changed', {})
export function notifyConversationChanged(payload?: { deletedConvId?: string }): void {
emit('ai-conversation-changed', payload ?? {})
}
/** 后端原始错误转用户友好提示 */
+8 -8
View File
@@ -21,7 +21,7 @@
//! streaming 是 boolean 无真正非法跃迁,guard 价值在集中入口与日志而非拦截。
import { state } from '@/stores/ai'
import { convStates } from './aiShared'
import { convStates, getConvStreamState, setConvStreaming } from './aiShared'
import { useAppSettingsStore } from '@/stores/appSettings'
/// feature flag key(appSettings KV)。关=回退散布语义(灰度/回退用)。
@@ -56,14 +56,14 @@ export function setStreaming(
// flag 关:回退散布语义,直接赋值不动其他。
if (!isGuardEnabled()) {
state.streaming = value
if (convId) setConvStreaming(convId, value)
else state.streaming = value
return
}
const prev = state.streaming
const prev = convId ? (getConvStreamState(convId)?.streaming ?? false) : state.streaming
// 幂等观测:同值重复写不拒绝(resetStreamWatchdog 等副作用场景需合法重入),
// 仅记 debug 日志供排查「为何重复置位」的卡死场景。
// 幂等观测:同值重复写不拒绝,仅记 debug 日志供排查卡死场景。
if (prev === value) {
console.debug(
`[AI-Guard] setStreaming 幂等(prev=${prev}, convId=${convId ?? 'none'}, reason=${reason ?? 'unknown'})`,
@@ -74,9 +74,9 @@ export function setStreaming(
)
}
// 批4 双轨收口:generating 态真相归 convStates(enum),本 guard 不再联动 generatingConvs
// (bool 轨已退役)。streaming 单值是另一条过渡态(F-09 per-conv streaming 收口前保留)。
state.streaming = value
// 显式 convId 写目标会话 per-conv 流式态;缺省写当前活跃会话(单会话语义不变)。
if (convId) setConvStreaming(convId, value)
else state.streaming = value
}
/**
+1 -1
View File
@@ -474,7 +474,7 @@ async function deleteConversation(id: string) {
await loadConversations()
// 删的是活跃会话:回落相邻会话作为新活跃视图
if (neighborId) await switchConversation(neighborId)
notifyConversationChanged()
notifyConversationChanged({ deletedConvId: id })
}
/** 会话操作失败气泡的 i18n key(M30 族 rename/archive/pin + G3.4 new/delete + G3.3 switch 瞬态失败) */
+14 -2
View File
@@ -15,8 +15,9 @@ import { listen, emit } from '@tauri-apps/api/event'
import { aiApi } from '@/api'
import { useAppSettingsStore } from '@/stores/appSettings'
import { state } from '@/stores/ai'
import { nextMsgId, setConvState, convStates, switchingConvs, getConvStreamState, setConvCurrentText, setConvStreaming, clearAllApprovalTimers, clearAllToolSlowTimers, flushCurrentText, friendlyError, notifyConversationChanged } from './aiShared'
import { nextMsgId, setConvState, convStates, switchingConvs, getConvStreamState, setConvCurrentText, setConvStreaming, clearConvStreamState, clearAllApprovalTimers, clearAllToolSlowTimers, flushCurrentText, friendlyError, notifyConversationChanged } from './aiShared'
import { resetStreamWatchdog, clearAllStreamWatchdogs } from './useAiStream'
import { setStreaming } from './streamingGuard'
import { loadConversations } from './useAiConversations'
import { handleStreamingEvent } from './useAiStreamingEvents'
import { handleToolEvent } from './useAiToolEvents'
@@ -140,7 +141,18 @@ export async function startListener() {
_startPromise = (async () => {
try {
_unlistenAiEvent = await aiApi.onEvent(handleEvent)
_unlistenConvChanged = await listen('ai-conversation-changed', () => { void loadConversations() })
_unlistenConvChanged = await listen('ai-conversation-changed', (e) => {
void loadConversations()
// 主窗口删除了本窗口当前展示的会话:清空视图防陈旧幽灵(分离窗口独立 JS context,经事件同步)。
const deleted = (e.payload as { deletedConvId?: string } | undefined)?.deletedConvId
if (deleted && state.activeConversationId === deleted) {
state.activeConversationId = null
state.messages = []
clearConvStreamState(deleted)
setStreaming(false, { convId: deleted, reason: 'conv-deleted' })
convStates.delete(deleted)
}
})
_unlistenApprovalClear = await listen('ai-approval-clear-timers', () => { clearAllApprovalTimers() })
emit('ai-client-ready', { lastConvId: state.activeConversationId })
} catch (err) {
+15 -6
View File
@@ -204,12 +204,21 @@ export async function restoreGeneratingState(opts?: { fromMainPanel?: boolean; c
if (!fromMainPanel) {
state.currentText = (opts?.convId && localStorage.getItem(textKey(opts.convId))) || localStorage.getItem('df-ai-text') || ''
}
state.messages.push({
id: `ai-${nextMsgId()}` as MessageId,
role: 'assistant',
content: '',
timestamp: Date.now(),
})
// 双占位去重:末条已是空 assistant 占位(本会话流式占位)则复用,不重复 push。
const lastMsg = state.messages[state.messages.length - 1]
const lastIsStreamingPlaceholder = !!lastMsg
&& lastMsg.role === 'assistant'
&& !lastMsg.content?.trim()
&& !lastMsg.isError
&& !(lastMsg.toolCalls && lastMsg.toolCalls.length)
if (!lastIsStreamingPlaceholder) {
state.messages.push({
id: `ai-${nextMsgId()}` as MessageId,
role: 'assistant',
content: '',
timestamp: Date.now(),
})
}
setStreaming(true, { convId: gen, reason: 'restoreGeneratingState' })
// 批4 双轨收口:恢复生成中态写 convStates(enum 真相源),替代旧 generatingConvs.add。
// setStreaming 联动已退役(批4),streaming 单值与 convStates 项并行写无冲突。