修复: aichat 切换/新建对话缺陷(队列串会话/回切丢消息/审批误终态化/删除复活/切换链路)

This commit is contained in:
lxy
2026-08-08 14:20:10 +08:00
parent 795e05f42d
commit 9f75db5b15
8 changed files with 511 additions and 325 deletions
+29 -20
View File
@@ -66,18 +66,18 @@
path 挂起归一进 ToolCard 内联审批(单真相源:状态层合);关(false,兜底回退)时挂载
DirAuthDialog 独立弹窗老链路。两路共存,appSettings 随时回退无需改代码。 -->
<DirAuthDialog v-if="!unifiedApproval" @toast="(p) => showToast(p.msg, p.type)" />
<!-- 待发送队列(生成中排队的消息,完成后自动续发) -->
<div v-if="store.state.queue.length > 0" class="ai-queue" :class="{ 'ai-queue--timeout': queueTimedOut }">
<!-- 待发送队列(生成中排队的消息,完成后自动续发;仅显示当前活跃会话的条目 -->
<div v-if="activeQueue.length > 0" class="ai-queue" :class="{ 'ai-queue--timeout': queueTimedOut }">
<div class="ai-queue-head">
<span class="ai-queue-title">{{ $t('aiChat.queueTitle', { n: store.state.queue.length }) }}<span v-if="queueWaitSeconds >= 0" class="ai-queue-timer">({{ queueWaitSeconds }}s)</span></span>
<span class="ai-queue-title">{{ $t('aiChat.queueTitle', { n: activeQueue.length }) }}<span v-if="queueWaitSeconds >= 0" class="ai-queue-timer">({{ queueWaitSeconds }}s)</span></span>
<div class="ai-queue-actions">
<button v-if="queueTimedOut" class="ai-queue-force" @click="handleForceSend">{{ $t('ai.forceSendBtn') }}</button>
<button class="ai-queue-clear" @click="store.clearQueue">{{ $t('aiChat.clearQueue') }}</button>
<button class="ai-queue-clear" @click="store.clearQueue(store.state.activeConversationId)">{{ $t('aiChat.clearQueue') }}</button>
</div>
</div>
<div class="ai-queue-list">
<div v-for="(item, idx) in store.state.queue" :key="idx" class="ai-queue-item">
<!-- UX-260616-06 决策只编 text:编辑态切 inline input,回车/失焦写回,ESC 取消 -->
<div v-for="(item, idx) in activeQueue" :key="idx" class="ai-queue-item">
<!-- 编辑态只编 text:inline input,回车/失焦写回,ESC 取消 -->
<template v-if="editingQueueIdx === idx">
<span v-if="item.skill" class="ai-queue-skill">/{{ item.skill }}</span>
<input
@@ -94,9 +94,9 @@
<template v-else>
<span v-if="item.skill" class="ai-queue-skill">/{{ item.skill }}</span>
<span class="ai-queue-text">{{ item.text }}</span>
<!-- UX-260616-06:编辑按钮 -->
<!-- 编辑按钮 -->
<button class="ai-queue-btn" @click="startEditQueued(idx)" :title="$t('aiChat.queueEdit')">{{ $t('aiChat.queueEdit') }}</button>
<!-- UX-260616-07 决策 a:立即发送(打断当前+发本条) -->
<!-- 立即发送(打断当前+发本条) -->
<button class="ai-queue-btn" @click="handleSendQueuedNow(idx)" :title="$t('aiChat.queueSendNow')">{{ $t('aiChat.queueSendNow') }}</button>
<button class="ai-queue-x" @click="store.cancelQueued(idx)" :title="$t('common.cancel')">×</button>
</template>
@@ -385,33 +385,40 @@ function onBeforeUnloadGuard(e: BeforeUnloadEvent) {
// 第二批抽离: handleKeydown/autoResize/handleSend 已迁移至 ChatInput.vue
// (ChatInput 经 emit('sent')/emit('cancel-edit')/emit('error') 与父交互)
// ── B-260616-02 L2 发送韧性:排队超时检测 + 强制发送 ──
// ── L2 发送韧性:排队超时检测 + 强制发送 ──
/** 队首排队是否已超 30s */
const queueTimedOut = computed(() => store.isQueueTimedOut())
/** 当前活跃会话的待发送队列(按 activeConversationId 过滤)。
* 队列是全局数组,多会话并发可各自排队;旧实现渲染全部会话的 queue,切/新建后会显示
* 旧会话排队消息。此处仅显当前活跃会话条目,相关操作(编辑/删除/立即发送/清空/超时)均按此收敛。 */
const activeQueue = computed(() =>
store.state.queue.filter(q => q.conversationId === store.state.activeConversationId),
)
/** 队首排队是否已超 30s(仅看当前活跃会话队列) */
const queueTimedOut = computed(() => store.isQueueTimedOut(store.state.activeConversationId))
/** 队首已排队秒数(用于 UI 展示倒计时;无队列时 -1) */
const queueWaitSeconds = computed(() => {
const first = store.state.queue[0]
const first = activeQueue.value[0]
if (!first) return -1
return Math.floor((Date.now() - first.enqueuedAt) / 1000)
})
/** 强制发送:委托 tryForceSend 唯一负责弹 confirm + shift 队列 + force_send */
/** 强制发送:委托 tryForceSend 唯一负责弹 confirm + 移除队列 + force_send(作用于当前活跃会话) */
async function handleForceSend() {
await store.tryForceSend(confirmDialog)
await store.tryForceSend(confirmDialog, store.state.activeConversationId)
}
// ── UX-260616-06 队列消息编辑(决策只编 text)+ UX-260616-07 立即发送(决策 a 插队)──
// ── 队列消息编辑(只编 text)+ 立即发送(插队)──
/** 当前正在编辑的队列项 index;null=非编辑态 */
const editingQueueIdx = ref<number | null>(null)
/** 编辑态输入框文本(回车写回 / ESC 取消还原) */
const editingQueueText = ref('')
/** 进入编辑态:记录 index + 快照当前 text */
/** 进入编辑态:记录 index + 快照当前 text(activeQueue 视图内下标) */
function startEditQueued(idx: number) {
const item = store.state.queue[idx]
const item = activeQueue.value[idx]
if (!item) return
editingQueueIdx.value = idx
editingQueueText.value = item.text
@@ -431,7 +438,7 @@ function saveEditQueued() {
editingQueueText.value = ''
}
/** UX-260616-07 立即发送:委托 store.sendQueuedNow(splice+stop+sendMessage) */
/** 立即发送:委托 store.sendQueuedNow(移除+stop+sendMessage) */
async function handleSendQueuedNow(idx: number) {
await store.sendQueuedNow(idx)
}
@@ -620,7 +627,8 @@ async function initContextEventListeners(): Promise<void> {
// 后端 emit ai_context_cleared 后会重置 session.messages,前端刷对话拉最新状态。
// 守卫:事件到达前用户可能切走,activeConversationId 变 null,跳过刷新避免空指针。
if (store.state.activeConversationId) {
void store.switchConversation(store.state.activeConversationId)
// force=true:同会话刷新(同会话点击短路对程序化刷新放行,清空后需重拉最新上下文)
void store.switchConversation(store.state.activeConversationId, true)
}
},
// 治 Task#1:仅手动压缩触发(useAiContext 的 AiManualCompressed case 调此回调);
@@ -629,7 +637,8 @@ async function initContextEventListeners(): Promise<void> {
showToast(t('aiChat.compressSuccess'), 'info')
// 刷新对话(历史消息落库标 compressed + system 摘要消息回填到视图)
if (store.state.activeConversationId) {
void store.switchConversation(store.state.activeConversationId)
// force=true:同会话刷新(压缩后需重拉最新消息视图)
void store.switchConversation(store.state.activeConversationId, true)
}
},
onError: (_convId, message) => {