From 01c17a66fc2e76b92a66607ccecf572da5d72623 Mon Sep 17 00:00:00 2001 From: thzxx Date: Sun, 19 Apr 2026 19:48:46 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=B7=A5=E4=BD=9C=E6=B5=81=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=E6=80=A7=E5=92=8C=E5=8D=8F=E8=B0=83=E6=80=A7=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复 6 个问题: 1. 新一轮对话清理上轮工具卡片和终端输出(避免越积越多) 2. 中止 Agent Loop 时保留已执行的工具记录(不再丢失) 3. 切换历史会话时清理工作空间状态 4. handleRetry 保存 think 内容(之前丢失) 5. 中止流程统一:agent-engine 抛出 AbortError 由消费方处理(消除重复消息风险) 6. 导出 clearToolCardsExternal/clearTerminalExternal 供外部调用 --- src/renderer/components/history-modal.ts | 5 +++++ src/renderer/components/input-area.ts | 25 ++++++++++++++++++---- src/renderer/components/workspace-panel.ts | 19 ++++++++++++++++ src/renderer/services/agent-engine.ts | 14 ++++++------ 4 files changed, 53 insertions(+), 10 deletions(-) diff --git a/src/renderer/components/history-modal.ts b/src/renderer/components/history-modal.ts index 7ce706b..3683075 100644 --- a/src/renderer/components/history-modal.ts +++ b/src/renderer/components/history-modal.ts @@ -5,6 +5,7 @@ import { state, KEYS } from '../state/state.js'; import { debounce, escapeHtml, formatTime } from '../utils/utils.js'; import { exportAsMarkdown, exportAsHtml, exportAsTxt, renderMessages, clearMessages, updateTotalTokens } from './chat-area.js'; +import { clearToolCardsExternal, clearTerminalExternal } from './workspace-panel.js'; import { showConfirm } from './prompt-modal.js'; import { ChatDB } from '../db/chat-db.js'; import { logSession, logWarn } from '../services/log-service.js'; @@ -179,6 +180,10 @@ async function loadHistorySession(sessionId: string): Promise { (document.querySelector('#historyBar') as HTMLElement).style.display = ''; (document.querySelector('#inputArea') as HTMLElement).style.display = 'none'; + // 切换会话时清理工作空间状态 + clearToolCardsExternal(); + clearTerminalExternal(); + clearMessages(); renderMessages(); updateTotalTokens(); diff --git a/src/renderer/components/input-area.ts b/src/renderer/components/input-area.ts index 5ea3ad8..a03696c 100644 --- a/src/renderer/components/input-area.ts +++ b/src/renderer/components/input-area.ts @@ -12,7 +12,7 @@ import { appendToolCallCardToPlaceholder, updateToolCallCardInPlaceholder } from './chat-area.js'; import { showToast } from './toast.js'; -import { addToolCard, updateToolCard } from './workspace-panel.js'; +import { addToolCard, updateToolCard, clearToolCardsExternal, clearTerminalExternal } from './workspace-panel.js'; import { ChatDB } from '../db/chat-db.js'; import { OllamaAPI } from '../api/ollama.js'; import { runAgentLoop } from '../services/agent-engine.js'; @@ -266,6 +266,12 @@ async function handleRetry(): Promise { const toolCallingEnabled = state.get('toolCallingEnabled', false); const model = getSelectedModel() || currentSession.model; + // 新一轮开始:清理上一轮的工作空间状态 + if (toolCallingEnabled) { + clearToolCardsExternal(); + clearTerminalExternal(); + } + appendAssistantPlaceholder(); state.set(KEYS.IS_STREAMING, true); updateSendButton(true); @@ -285,9 +291,10 @@ async function handleRetry(): Promise { }); try { + let retryThinkContent = ''; await runAgentLoop(userMsg.content || '', userMsg.images || [], historyMessages, { - onThinking: (thinking) => updateLastAssistantMessage('', thinking, null), - onContent: (content) => updateLastAssistantMessage(content, null, null), + onThinking: (thinking) => { retryThinkContent = thinking; updateLastAssistantMessage('', thinking, null); }, + onContent: (content) => updateLastAssistantMessage(content, retryThinkContent || null, null), onToolCallStart: (call) => { addToolCard({ name: call.function.name, arguments: call.function.arguments, @@ -310,6 +317,7 @@ async function handleRetry(): Promise { onDone: async (finalContent, toolRecords, loopStats) => { const assistantMsg: ChatMessage = { role: 'assistant', content: finalContent || '', timestamp: Date.now(), + ...(retryThinkContent && { think: retryThinkContent }), ...(toolRecords?.length && { toolCalls: toolRecords }), ...(loopStats?.eval_count && { eval_count: loopStats.eval_count }), ...(loopStats?.total_duration && { total_duration: loopStats.total_duration }), @@ -317,7 +325,7 @@ async function handleRetry(): Promise { state.update(KEYS.CURRENT_SESSION, (s: ChatSession | null) => ({ ...s, messages: [...s.messages, assistantMsg], updatedAt: Date.now() })); - updateLastAssistantMessage(finalContent, null, loopStats || null); + updateLastAssistantMessage(finalContent, retryThinkContent || null, loopStats || null); renderMessages(); await saveCurrentSession(); updateTotalTokens(); @@ -856,6 +864,10 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio filePreviewEl.innerHTML = ''; autoResizeTextarea(); + // 新一轮开始:清理上一轮的工作空间状态 + clearToolCardsExternal(); + clearTerminalExternal(); + appendAssistantPlaceholder(); state.set(KEYS.IS_STREAMING, true); updateSendButton(true); @@ -951,11 +963,16 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio contentDiv.innerHTML = html; } } + // 获取中止时已执行的工具记录 + const abortToolRecords = state.get('_abortToolRecords', null); + if (abortToolRecords) state.set('_abortToolRecords', null); + const partialMsg: ChatMessage = { role: 'assistant', content: assistantContent, timestamp: Date.now(), ...(thinkContent && { think: thinkContent }), + ...(abortToolRecords?.length && { toolCalls: abortToolRecords }), stopped: true }; state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({ diff --git a/src/renderer/components/workspace-panel.ts b/src/renderer/components/workspace-panel.ts index b2efcfe..bb673cd 100644 --- a/src/renderer/components/workspace-panel.ts +++ b/src/renderer/components/workspace-panel.ts @@ -63,6 +63,25 @@ const IDLE_TIPS = [ let tipIndex = Math.floor(Math.random() * IDLE_TIPS.length); let tipTimer: ReturnType | null = null; +/** 清空工具卡片(供外部调用) */ +export function clearToolCardsExternal(): void { + toolCards = []; + if (activeTab === 'tools') renderToolCalls(); + updateToolsHint(); +} + +/** 清空终端(供外部调用) */ +export function clearTerminalExternal(): void { + if (terminal) { + terminal.lines = []; + terminal.running = false; + } + currentAiCommand = null; + if (activeTab === 'terminal') renderTerminal(); + updateStopBtnState(); + updateHint(); +} + /** 工具调用卡片列表 */ let toolCards: ToolCallRecord[] = []; let toolAutoScroll = true; diff --git a/src/renderer/services/agent-engine.ts b/src/renderer/services/agent-engine.ts index fbdfefc..d7d2f54 100644 --- a/src/renderer/services/agent-engine.ts +++ b/src/renderer/services/agent-engine.ts @@ -518,8 +518,10 @@ export async function runAgentLoop( // 检查是否已中止 if (state.get(KEYS.ABORT_CONTROLLER)?.signal.aborted) { logInfo('ReAct Agent Loop 已中止'); - callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats()); - return; + if (allToolRecords.length > 0) { + state.set('_abortToolRecords', allToolRecords); + } + throw new DOMException('Aborted', 'AbortError'); } logAgentLoop(loopCount, maxLoops); @@ -591,11 +593,11 @@ export async function runAgentLoop( } catch (err) { if (abortController.signal.aborted) { logInfo('流式调用已中止'); - if (content || thinking) { - messages.push({ role: 'assistant', content, ...(thinking && { thinking }) }); + // 保存工具记录到 state,供消费方的 catch 处理中止消息 + if (allToolRecords.length > 0) { + state.set('_abortToolRecords', allToolRecords); } - callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats()); - return; + throw err; // 抛出 AbortError,由消费方统一处理 } logError('流式调用异常', (err as Error).message); // 清理预算警告临时消息