fix: 修复工具调用确认后卡死问题

- 工具确认按钮添加 e.stopPropagation() 防止事件冒泡到 overlay
- 防止重复绑定 overlay click handler (initialized flag)
- 新确认打开时自动取消旧确认,防止竞态条件
- 工具执行添加 30 秒超时保护,防止 IPC 挂起
- Agent Loop 添加 5 分钟全局超时
- 每轮循环检查 abort 信号
- 工具执行失败时输出 console.error 便于调试
This commit is contained in:
Metona
2026-04-06 18:15:18 +08:00
parent 35a0c58bde
commit 43c827e9ff
2 changed files with 55 additions and 7 deletions
+20 -6
View File
@@ -7,10 +7,12 @@ import { getToolIcon, formatToolName } from '../services/tool-registry.js';
let modalEl: HTMLElement | null = null;
let resolveConfirm: ((confirmed: boolean) => void) | null = null;
let initialized = false;
export function initToolConfirmModal(): void {
modalEl = document.querySelector('#toolConfirmModal')!;
if (!modalEl) return;
if (!modalEl || initialized) return;
initialized = true;
modalEl.addEventListener('click', (e) => {
if (e.target === modalEl) {
@@ -24,9 +26,19 @@ export function showToolConfirm(call: ToolCall): Promise<boolean> {
if (!modalEl) {
modalEl = document.querySelector('#toolConfirmModal')!;
if (!modalEl) { resolve(false); return; }
modalEl.addEventListener('click', (e) => {
if (e.target === modalEl) cancelConfirm();
});
if (!initialized) {
initialized = true;
modalEl.addEventListener('click', (e) => {
if (e.target === modalEl) cancelConfirm();
});
}
}
// 如果有未完成的确认,先取消
if (resolveConfirm) {
const oldResolve = resolveConfirm;
resolveConfirm = null;
oldResolve(false);
}
resolveConfirm = resolve;
@@ -68,11 +80,13 @@ export function showToolConfirm(call: ToolCall): Promise<boolean> {
modalEl.style.display = '';
const onConfirm = () => {
const onConfirm = (e: Event) => {
e.stopPropagation();
cleanup();
resolveConfirm?.(true);
};
const onCancel = () => {
const onCancel = (e: Event) => {
e.stopPropagation();
cleanup();
resolveConfirm?.(false);
};
+35 -1
View File
@@ -86,10 +86,25 @@ export async function runAgentLoop(
let loopCount = 0;
const maxLoops = 10;
const allToolRecords: ToolCallRecord[] = [];
const loopStartTime = Date.now();
const MAX_LOOP_TIME = 300000; // 5分钟总超时
while (loopCount < maxLoops) {
loopCount++;
// 全局超时检查
if (Date.now() - loopStartTime > MAX_LOOP_TIME) {
console.warn('[AgentEngine] 达到最大运行时间(5分钟),自动停止');
callbacks.onDone(content || '(达到最大运行时间限制)', allToolRecords);
return;
}
// 检查是否已中止
if (state.get<AbortController | null>(KEYS.ABORT_CONTROLLER)?.signal.aborted) {
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined);
return;
}
let thinking = '';
let content = '';
const toolCalls: ToolCall[] = [];
@@ -171,10 +186,21 @@ export async function runAgentLoop(
}
for (const call of toolCalls) {
// 检查是否已中止
if (abortController.signal.aborted) {
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined);
return;
}
callbacks.onToolCallStart(call);
if (needsConfirmation(call.function.name)) {
const confirmed = await callbacks.onConfirmTool(call);
// 确认后再次检查是否已中止
if (abortController.signal.aborted) {
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined);
return;
}
if (!confirmed) {
const record: ToolCallRecord = {
name: call.function.name,
@@ -196,7 +222,14 @@ export async function runAgentLoop(
}
try {
const result = await executeTool(call.function.name, call.function.arguments);
// 工具执行超时保护(30秒)
const TOOL_TIMEOUT = 30000;
const result = await Promise.race([
executeTool(call.function.name, call.function.arguments),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('工具执行超时(30秒)')), TOOL_TIMEOUT)
)
]);
const record: ToolCallRecord = {
name: call.function.name,
arguments: call.function.arguments,
@@ -214,6 +247,7 @@ export async function runAgentLoop(
callbacks.onToolCallResult(call.function.name, result, call);
} catch (err) {
const errorMsg = (err as Error).message;
console.error(`[AgentEngine] 工具执行失败 (${call.function.name}):`, errorMsg);
const record: ToolCallRecord = {
name: call.function.name,
arguments: call.function.arguments,