修复: 消息重叠与审批卡死
This commit is contained in:
@@ -46,9 +46,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, nextTick } from 'vue'
|
||||
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ToolCard from './ToolCard.vue'
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
|
||||
import type { AiToolCallInfo } from '@/api/types'
|
||||
|
||||
// useI18n needed for $t() in template (collapseAll / expandAll) + groupDisplayName(i18n key)
|
||||
@@ -299,6 +300,34 @@ function scrollToFirstPending(): void {
|
||||
card.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
}
|
||||
|
||||
// BUG-260624-02(授权弹窗卡死根治·诊断 workflow high 置信):监听 pending 审批卡到达事件,
|
||||
// 自动展开折叠分组 + scroll 到 pending 卡。根因:统一审批开关 df-ai-unified-approval 默认开 →
|
||||
// path 审批走 ToolCard 内联,但同名工具≥2 分组默认折叠(initDefaultCollapse)+ group-hidden
|
||||
// (display:none)→ 审批按钮对用户不可见 → 5min 超时(aiShared APPROVAL_TIMEOUT_MS)静默
|
||||
// authorizeDir/approve(deny)→ 误显"用户拒绝" → 用户全程未见审批入口即"卡死/被拒"。
|
||||
// scrollToFirstPending(:285)已能自动展开折叠组(offsetParent===null → collapsedGroups 清空),
|
||||
// 原仅 AiChat 头部徽标手动触发(:230);此处 pending 到达即自动调用,让审批卡必可见可操作。
|
||||
let _unlistenPending: UnlistenFn | null = null
|
||||
// 论证 workflow 回归中优:防卸载竞态——await listen 解析前组件已卸载(HMR/快速切会话)时,
|
||||
// onBeforeUnmount 跑到 _unlistenPending 仍 null 跳过 unlisten → 孤儿监听器泄漏。disposed 标志兜底。
|
||||
let _disposed = false
|
||||
onMounted(async () => {
|
||||
const fn = await listen('ai-pending-arrived', () => {
|
||||
// nextTick 等 pending_approval 类挂到 DOM 后再查(scrollToFirstPending 查 .ai-tool-card--pending_approval)。
|
||||
// 多个 ToolCardList 实例(每条 AI 消息一个)各自 listen,querySelector 只命中自身 root 内的
|
||||
// pending 卡,非含 pending 卡的实例查 null no-op,天然过滤不串扰。
|
||||
nextTick(() => scrollToFirstPending())
|
||||
})
|
||||
// 解析时若已卸载(HMR/极速 mount/unmount),立即 unlisten 刚拿到的句柄,防孤儿监听器。
|
||||
if (_disposed) { fn(); return }
|
||||
_unlistenPending = fn
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
_disposed = true
|
||||
_unlistenPending?.()
|
||||
_unlistenPending = null
|
||||
})
|
||||
|
||||
defineExpose({ collapseInactive, scrollToFirstPending })
|
||||
|
||||
/* -- 分组图标(复用 ToolCard 的 toolCategory 逻辑) -- */
|
||||
|
||||
@@ -425,7 +425,19 @@ watch(() => store.state.currentText, (text) => {
|
||||
// 不再用 onContentChange 的 nextTick(微任务先于 rAF,滚到旧 scrollHeight 致错位抖动)。
|
||||
// 仅流式渲染触发;非流式 currentText 变化(罕见)走 onContentChange 兜底。
|
||||
if (store.state.streaming && text) scheduleStreamParse(text)
|
||||
else onContentChange()
|
||||
else {
|
||||
// BUG-260624-01(消息重叠根治·诊断 workflow 确认):currentText 归零(新轮 AiAgentRound
|
||||
// 清空 / 生成结束)时,同步清上一轮流式分块缓存 streamingBlocks + lastStreamText。
|
||||
// 原:streaming watch(:435)仅在 streaming 翻 false 时清;但 agent 多轮连续生成时
|
||||
// streaming 保持 true,新轮 AiAgentRound push 新空气泡 + currentText='',streamingBlocks
|
||||
// 残留上一轮 → 新气泡 isLastAi 命中(:1017)渲染残留分块 → 与上一条回复重叠堆叠。
|
||||
// 在 currentText 归零处清,精确覆盖"新轮切换"与"生成结束"两个时机。
|
||||
if (!text) {
|
||||
streamingBlocks.value = []
|
||||
lastStreamText = ''
|
||||
}
|
||||
onContentChange()
|
||||
}
|
||||
})
|
||||
watch(mdReady, (ready) => {
|
||||
if (ready && store.state.streaming && store.state.currentText) {
|
||||
@@ -844,6 +856,17 @@ watch(() => store.state.activeConversationId, () => {
|
||||
wasNearBottom = true
|
||||
// B-260618-24: 切会话重置跟随意图(新会话默认跟随底部,防 A 上滑残留 isFollowingBottom=false)
|
||||
isFollowingBottom = true
|
||||
// BUG-260624-01(消息重叠根治·论证 workflow 完整性高优):MessageList 是持久单实例(AiChat.vue
|
||||
// 无 :key/v-if 卸载),实例级 streamingBlocks(:69)跨会话/跨轮残留是消息重叠主根因之一。
|
||||
// 钉在「会话切换」这一确切时机清,根治两条残留路径:① switchConversation(useAiConversations)
|
||||
// 清 currentText 但实例级 streamingBlocks 未同步;② restoreGeneratingState(useAiWindow)
|
||||
// 切到正在生成的 conv / 分离窗口挂载。比靠 currentText 副作用间接触发更精确,不依赖
|
||||
// switchConversation 内部语句顺序(对齐 no-patch-groundwork:状态变更点收敛,非渲染侧补丁)。
|
||||
if (streamingBlocks.value.length) {
|
||||
streamingBlocks.value = []
|
||||
lastStreamText = ''
|
||||
}
|
||||
if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null }
|
||||
})
|
||||
|
||||
// 流式 rAF 清理:组件卸载时若仍有 pending rAF(streaming 中途切走/关窗),取消防泄漏。
|
||||
@@ -1008,7 +1031,7 @@ defineExpose({
|
||||
<!-- 文本内容(流式或固定,Markdown 渲染) -->
|
||||
<!-- UX-2025-01:流式分块v-for,已完成块DOM稳定不重建→选文字保持 -->
|
||||
<div
|
||||
v-if="item.msg.content || (isLastAi(item.msg) && store.state.currentText)"
|
||||
v-if="item.msg.content || (isLastAi(item.msg) && store.state.streaming && store.state.currentText)"
|
||||
class="ai-msg-bubble ai-msg-bubble--ai ai-md"
|
||||
:class="{ 'ai-msg-bubble--error': item.msg.isError }"
|
||||
:key="'md-' + item.msg.id + '-' + (isLastAi(item.msg) ? _mdRenderKey : 0)"
|
||||
|
||||
@@ -195,6 +195,13 @@ export function flushCurrentText() {
|
||||
break
|
||||
}
|
||||
}
|
||||
// BUG-260624-01(消息重叠根治·诊断 workflow 确认):回填后立即自清 currentText。
|
||||
// 原:清空依赖调用方(AiAgentRound/AiCompleted/AiError/AiHelpRequired 各跟一行 currentText='')。
|
||||
// 竞态:任一新增 flush 调用点漏清,或事件乱序致渲染先于调用方清空,全局单例 currentText
|
||||
// 残留 → MessageList 渲染侧 isLastAi(msg)&¤tText 把残留文本渲到新气泡 → 重叠堆叠。
|
||||
// 自清把 flush 语义收敛为"回填并归零",消除对调用方清空顺序的依赖。各调用方后续的
|
||||
// currentText='' 对已清空值幂等,无副作用。
|
||||
state.currentText = ''
|
||||
}
|
||||
|
||||
/** token 用量展示开关(读 appSettings,与 Settings.vue 共享 key `df-show-token-usage`) */
|
||||
@@ -378,6 +385,11 @@ function handleStreamingEvent(event: AiChatEvent): boolean {
|
||||
// 传 kind='path' → 到点回调调 authorizeDir(id,'deny')(非 ai_approve,避免后端 kind==Risk 校验拒卡死)。
|
||||
startApprovalTimer(event.id, event.tool, 'path')
|
||||
}
|
||||
// BUG-260624-02(授权弹窗卡死根治·诊断 workflow high 置信):path 类审批卡归一进 ToolCard
|
||||
// 内联后,可能落入同名工具≥2 的折叠分组(ToolCardList group-hidden display:none)对用户
|
||||
// 不可见 → 5min 超时(aiShared APPROVAL_TIMEOUT_MS)静默 authorizeDir('deny')→ 误显
|
||||
// "用户拒绝" → 用户全程未见审批入口即卡死。通知 ToolCardList 自动展开折叠组 + scroll。
|
||||
void emit('ai-pending-arrived', { toolCallId: event.id })
|
||||
return true
|
||||
}
|
||||
// 开关关(兜底回退):push 到 pendingDirAuths 驱动 DirAuthDialog 弹窗。
|
||||
@@ -470,6 +482,8 @@ function handleToolEvent(event: AiChatEvent): boolean {
|
||||
// AE-2025-06: 审批等待开始→启动审批超时计时器(5min 不处理自动拒绝)
|
||||
// 传 kind='risk' → 到点回调调 aiApi.approve(id,false)(后端 ai_approve kind==Risk 链路)。
|
||||
startApprovalTimer(event.id, event.name, 'risk')
|
||||
// BUG-260624-02:同 AiDirAuthRequired,risk 类 pending 卡落折叠分组不可见时自动展开 + scroll。
|
||||
void emit('ai-pending-arrived', { toolCallId: event.id })
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user