fix: 工作流完整性和协调性修复
修复 6 个问题: 1. 新一轮对话清理上轮工具卡片和终端输出(避免越积越多) 2. 中止 Agent Loop 时保留已执行的工具记录(不再丢失) 3. 切换历史会话时清理工作空间状态 4. handleRetry 保存 think 内容(之前丢失) 5. 中止流程统一:agent-engine 抛出 AbortError 由消费方处理(消除重复消息风险) 6. 导出 clearToolCardsExternal/clearTerminalExternal 供外部调用
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
// 清理预算警告临时消息
|
||||
|
||||
Reference in New Issue
Block a user