fix: 工作流完整性和协调性修复

修复 6 个问题:
1. 新一轮对话清理上轮工具卡片和终端输出(避免越积越多)
2. 中止 Agent Loop 时保留已执行的工具记录(不再丢失)
3. 切换历史会话时清理工作空间状态
4. handleRetry 保存 think 内容(之前丢失)
5. 中止流程统一:agent-engine 抛出 AbortError 由消费方处理(消除重复消息风险)
6. 导出 clearToolCardsExternal/clearTerminalExternal 供外部调用
This commit is contained in:
thzxx
2026-04-19 19:48:46 +08:00
parent f5fd851224
commit 01c17a66fc
4 changed files with 53 additions and 10 deletions
+5
View File
@@ -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<void> {
(document.querySelector('#historyBar') as HTMLElement).style.display = '';
(document.querySelector('#inputArea') as HTMLElement).style.display = 'none';
// 切换会话时清理工作空间状态
clearToolCardsExternal();
clearTerminalExternal();
clearMessages();
renderMessages();
updateTotalTokens();
+21 -4
View File
@@ -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<void> {
const toolCallingEnabled = state.get<boolean>('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<void> {
});
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<void> {
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<void> {
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<ToolCallRecord[] | null>('_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) => ({
@@ -63,6 +63,25 @@ const IDLE_TIPS = [
let tipIndex = Math.floor(Math.random() * IDLE_TIPS.length);
let tipTimer: ReturnType<typeof setInterval> | 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;
+8 -6
View File
@@ -518,8 +518,10 @@ export async function runAgentLoop(
// 检查是否已中止
if (state.get<AbortController | null>(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);
// 清理预算警告临时消息