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 { state, KEYS } from '../state/state.js';
|
||||||
import { debounce, escapeHtml, formatTime } from '../utils/utils.js';
|
import { debounce, escapeHtml, formatTime } from '../utils/utils.js';
|
||||||
import { exportAsMarkdown, exportAsHtml, exportAsTxt, renderMessages, clearMessages, updateTotalTokens } from './chat-area.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 { showConfirm } from './prompt-modal.js';
|
||||||
import { ChatDB } from '../db/chat-db.js';
|
import { ChatDB } from '../db/chat-db.js';
|
||||||
import { logSession, logWarn } from '../services/log-service.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('#historyBar') as HTMLElement).style.display = '';
|
||||||
(document.querySelector('#inputArea') as HTMLElement).style.display = 'none';
|
(document.querySelector('#inputArea') as HTMLElement).style.display = 'none';
|
||||||
|
|
||||||
|
// 切换会话时清理工作空间状态
|
||||||
|
clearToolCardsExternal();
|
||||||
|
clearTerminalExternal();
|
||||||
|
|
||||||
clearMessages();
|
clearMessages();
|
||||||
renderMessages();
|
renderMessages();
|
||||||
updateTotalTokens();
|
updateTotalTokens();
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
appendToolCallCardToPlaceholder, updateToolCallCardInPlaceholder
|
appendToolCallCardToPlaceholder, updateToolCallCardInPlaceholder
|
||||||
} from './chat-area.js';
|
} from './chat-area.js';
|
||||||
import { showToast } from './toast.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 { ChatDB } from '../db/chat-db.js';
|
||||||
import { OllamaAPI } from '../api/ollama.js';
|
import { OllamaAPI } from '../api/ollama.js';
|
||||||
import { runAgentLoop } from '../services/agent-engine.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 toolCallingEnabled = state.get<boolean>('toolCallingEnabled', false);
|
||||||
const model = getSelectedModel() || currentSession.model;
|
const model = getSelectedModel() || currentSession.model;
|
||||||
|
|
||||||
|
// 新一轮开始:清理上一轮的工作空间状态
|
||||||
|
if (toolCallingEnabled) {
|
||||||
|
clearToolCardsExternal();
|
||||||
|
clearTerminalExternal();
|
||||||
|
}
|
||||||
|
|
||||||
appendAssistantPlaceholder();
|
appendAssistantPlaceholder();
|
||||||
state.set(KEYS.IS_STREAMING, true);
|
state.set(KEYS.IS_STREAMING, true);
|
||||||
updateSendButton(true);
|
updateSendButton(true);
|
||||||
@@ -285,9 +291,10 @@ async function handleRetry(): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
let retryThinkContent = '';
|
||||||
await runAgentLoop(userMsg.content || '', userMsg.images || [], historyMessages, {
|
await runAgentLoop(userMsg.content || '', userMsg.images || [], historyMessages, {
|
||||||
onThinking: (thinking) => updateLastAssistantMessage('', thinking, null),
|
onThinking: (thinking) => { retryThinkContent = thinking; updateLastAssistantMessage('', thinking, null); },
|
||||||
onContent: (content) => updateLastAssistantMessage(content, null, null),
|
onContent: (content) => updateLastAssistantMessage(content, retryThinkContent || null, null),
|
||||||
onToolCallStart: (call) => {
|
onToolCallStart: (call) => {
|
||||||
addToolCard({
|
addToolCard({
|
||||||
name: call.function.name, arguments: call.function.arguments,
|
name: call.function.name, arguments: call.function.arguments,
|
||||||
@@ -310,6 +317,7 @@ async function handleRetry(): Promise<void> {
|
|||||||
onDone: async (finalContent, toolRecords, loopStats) => {
|
onDone: async (finalContent, toolRecords, loopStats) => {
|
||||||
const assistantMsg: ChatMessage = {
|
const assistantMsg: ChatMessage = {
|
||||||
role: 'assistant', content: finalContent || '', timestamp: Date.now(),
|
role: 'assistant', content: finalContent || '', timestamp: Date.now(),
|
||||||
|
...(retryThinkContent && { think: retryThinkContent }),
|
||||||
...(toolRecords?.length && { toolCalls: toolRecords }),
|
...(toolRecords?.length && { toolCalls: toolRecords }),
|
||||||
...(loopStats?.eval_count && { eval_count: loopStats.eval_count }),
|
...(loopStats?.eval_count && { eval_count: loopStats.eval_count }),
|
||||||
...(loopStats?.total_duration && { total_duration: loopStats.total_duration }),
|
...(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) => ({
|
state.update(KEYS.CURRENT_SESSION, (s: ChatSession | null) => ({
|
||||||
...s, messages: [...s.messages, assistantMsg], updatedAt: Date.now()
|
...s, messages: [...s.messages, assistantMsg], updatedAt: Date.now()
|
||||||
}));
|
}));
|
||||||
updateLastAssistantMessage(finalContent, null, loopStats || null);
|
updateLastAssistantMessage(finalContent, retryThinkContent || null, loopStats || null);
|
||||||
renderMessages();
|
renderMessages();
|
||||||
await saveCurrentSession();
|
await saveCurrentSession();
|
||||||
updateTotalTokens();
|
updateTotalTokens();
|
||||||
@@ -856,6 +864,10 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
|
|||||||
filePreviewEl.innerHTML = '';
|
filePreviewEl.innerHTML = '';
|
||||||
autoResizeTextarea();
|
autoResizeTextarea();
|
||||||
|
|
||||||
|
// 新一轮开始:清理上一轮的工作空间状态
|
||||||
|
clearToolCardsExternal();
|
||||||
|
clearTerminalExternal();
|
||||||
|
|
||||||
appendAssistantPlaceholder();
|
appendAssistantPlaceholder();
|
||||||
state.set(KEYS.IS_STREAMING, true);
|
state.set(KEYS.IS_STREAMING, true);
|
||||||
updateSendButton(true);
|
updateSendButton(true);
|
||||||
@@ -951,11 +963,16 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
|
|||||||
contentDiv.innerHTML = html;
|
contentDiv.innerHTML = html;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 获取中止时已执行的工具记录
|
||||||
|
const abortToolRecords = state.get<ToolCallRecord[] | null>('_abortToolRecords', null);
|
||||||
|
if (abortToolRecords) state.set('_abortToolRecords', null);
|
||||||
|
|
||||||
const partialMsg: ChatMessage = {
|
const partialMsg: ChatMessage = {
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: assistantContent,
|
content: assistantContent,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
...(thinkContent && { think: thinkContent }),
|
...(thinkContent && { think: thinkContent }),
|
||||||
|
...(abortToolRecords?.length && { toolCalls: abortToolRecords }),
|
||||||
stopped: true
|
stopped: true
|
||||||
};
|
};
|
||||||
state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({
|
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 tipIndex = Math.floor(Math.random() * IDLE_TIPS.length);
|
||||||
let tipTimer: ReturnType<typeof setInterval> | null = null;
|
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 toolCards: ToolCallRecord[] = [];
|
||||||
let toolAutoScroll = true;
|
let toolAutoScroll = true;
|
||||||
|
|||||||
@@ -518,8 +518,10 @@ export async function runAgentLoop(
|
|||||||
// 检查是否已中止
|
// 检查是否已中止
|
||||||
if (state.get<AbortController | null>(KEYS.ABORT_CONTROLLER)?.signal.aborted) {
|
if (state.get<AbortController | null>(KEYS.ABORT_CONTROLLER)?.signal.aborted) {
|
||||||
logInfo('ReAct Agent Loop 已中止');
|
logInfo('ReAct Agent Loop 已中止');
|
||||||
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
|
if (allToolRecords.length > 0) {
|
||||||
return;
|
state.set('_abortToolRecords', allToolRecords);
|
||||||
|
}
|
||||||
|
throw new DOMException('Aborted', 'AbortError');
|
||||||
}
|
}
|
||||||
|
|
||||||
logAgentLoop(loopCount, maxLoops);
|
logAgentLoop(loopCount, maxLoops);
|
||||||
@@ -591,11 +593,11 @@ export async function runAgentLoop(
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (abortController.signal.aborted) {
|
if (abortController.signal.aborted) {
|
||||||
logInfo('流式调用已中止');
|
logInfo('流式调用已中止');
|
||||||
if (content || thinking) {
|
// 保存工具记录到 state,供消费方的 catch 处理中止消息
|
||||||
messages.push({ role: 'assistant', content, ...(thinking && { thinking }) });
|
if (allToolRecords.length > 0) {
|
||||||
|
state.set('_abortToolRecords', allToolRecords);
|
||||||
}
|
}
|
||||||
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
|
throw err; // 抛出 AbortError,由消费方统一处理
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
logError('流式调用异常', (err as Error).message);
|
logError('流式调用异常', (err as Error).message);
|
||||||
// 清理预算警告临时消息
|
// 清理预算警告临时消息
|
||||||
|
|||||||
Reference in New Issue
Block a user