fix: 修复代码块复制、事件去重、任务结束、停止按钮bug

Phase 1 关键Bug修复:

1. 代码块复制功能
   - 删除 index.html 中的全局事件监听(47-53行)
   - 避免与 Vue 方法的双重监听冲突

2. 事件去重机制
   - 在 handleTaskEvent() 添加基于 idx 的去重检查
   - 使用 Set 存储已处理的事件索引(最多1000个)
   - 在 restoreTaskState() 时清理已处理事件

3. 任务结束逻辑
   - 修复 handleTaskComplete() 异步状态清理问题
   - 新增 clearTaskState() 统一清理方法
   - 在 stopPolling() 中清除 currentTaskId

4. 停止按钮状态同步
   - 改进 stopTask() 错误处理
   - 添加后端确认等待(300ms)
   - 确保失败时也清理前端状态
   - 添加 finally 块清除 dropToolEvents

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
JOJO 2026-04-04 01:03:06 +08:00
parent ce02ea21fb
commit de82a37958
4 changed files with 77 additions and 24 deletions

View File

@ -44,13 +44,6 @@
button.setAttribute('aria-label', button.dataset.originalLabel || '复制代码');
});
}
document.addEventListener('click', function(e) {
const target = e.target;
if (target && target.classList && target.classList.contains('copy-code-btn')) {
const blockId = target.getAttribute('data-code');
if (blockId) copyCodeBlock(blockId);
}
});
</script>
<script src="/static/easter-eggs/registry.js"></script>
<script src="/static/easter-eggs/flood.js"></script>

View File

@ -261,24 +261,40 @@ export const messageMethods = {
this.stopRequested = true;
this.dropToolEvents = shouldDropToolEvents;
// 使用 REST API 取消任务
try {
const { useTaskStore } = await import('../../stores/task');
const taskStore = useTaskStore();
if (taskStore.currentTaskId) {
await taskStore.cancelTask();
debugLog('[Message] 任务已取消');
}
} catch (error) {
console.error('[Message] 取消任务失败:', error);
}
// 立即清理前端状态,避免出现"不可输入也不可停止"的卡死状态
// 等待后端确认
await new Promise(resolve => setTimeout(resolve, 300));
// 清理前端状态
this.clearPendingTools('user_stop');
this.streamingMessage = false;
this.taskInProgress = false;
this.forceUnlockMonitor('user_stop');
} catch (error) {
console.error('[Message] 取消任务失败:', error);
// 即使失败也清理状态
this.clearPendingTools('user_stop');
this.streamingMessage = false;
this.taskInProgress = false;
this.forceUnlockMonitor('user_stop');
this.uiPushToast({
title: '停止失败',
message: '任务可能仍在运行,请刷新页面',
type: 'warning'
});
} finally {
// 确保清除 dropToolEvents 标志
this.dropToolEvents = false;
}
},
async clearChat() {

View File

@ -18,6 +18,26 @@ export const taskPollingMethods = {
const eventData = event.data || {};
const eventIdx = event.idx;
// 事件去重检查
if (typeof eventIdx === 'number') {
if (!this._processedEventIndices) {
this._processedEventIndices = new Set();
}
if (this._processedEventIndices.has(eventIdx)) {
debugLog(`[TaskPolling] 跳过重复事件 #${eventIdx}`);
return;
}
this._processedEventIndices.add(eventIdx);
// 限制 Set 大小(保留最近 1000 个)
if (this._processedEventIndices.size > 1000) {
const firstIdx = Math.min(...this._processedEventIndices);
this._processedEventIndices.delete(firstIdx);
}
}
// 检查事件的 conversation_id 是否匹配当前对话
// 如果不匹配,忽略该事件(避免切换对话后旧任务的事件显示到新对话中)
if (eventData.conversation_id && this.currentConversationId) {
@ -529,31 +549,42 @@ export const taskPollingMethods = {
} else {
debugLog('[TaskPolling] 任务完成');
}
// 同步处理状态更新
this.streamingMessage = false;
this.stopRequested = false;
if (hasRunningSubAgents) {
this.taskInProgress = true;
this.waitingForSubAgent = true;
} else {
this.taskInProgress = false;
this.waitingForSubAgent = false;
this.clearTaskState(); // 清理任务状态
}
this.$forceUpdate();
// 停止轮询
// 只更新统计,不重新加载历史
if (this.currentConversationId) {
setTimeout(() => {
this.fetchConversationTokenStatistics();
this.updateCurrentContextTokens();
}, 500);
}
},
// 新增统一清理方法
clearTaskState() {
(async () => {
const { useTaskStore } = await import('../../stores/task');
const taskStore = useTaskStore();
taskStore.stopPolling();
taskStore.clearTask(); // 使用 clearTask 而不是 stopPolling
})();
// 只更新统计,不重新加载历史(避免重复显示)
setTimeout(() => {
if (this.currentConversationId) {
this.fetchConversationTokenStatistics();
this.updateCurrentContextTokens();
if (typeof this.clearProcessedEvents === 'function') {
this.clearProcessedEvents();
}
}, 500);
},
handleUserMessage(data: any) {
@ -631,10 +662,22 @@ export const taskPollingMethods = {
}
},
/**
*
*/
clearProcessedEvents() {
if (this._processedEventIndices) {
this._processedEventIndices.clear();
}
},
/**
*
*/
async restoreTaskState() {
// 清理已处理的事件索引
this.clearProcessedEvents();
try {
const { useTaskStore } = await import('../../stores/task');
const taskStore = useTaskStore();

View File

@ -180,6 +180,7 @@ export const useTaskStore = defineStore('task', {
this.pollingInterval = null;
}
this.isPolling = false;
this.currentTaskId = null; // 清除任务 ID
},
async cancelTask() {