from __future__ import annotations import asyncio import json import time from typing import Optional from .utils_common import debug_log, brief_log from .state import MONITOR_FILE_TOOLS, MONITOR_MEMORY_TOOLS, MONITOR_SNAPSHOT_CHAR_LIMIT, MONITOR_MEMORY_ENTRY_LIMIT from .monitor import cache_monitor_snapshot from .security import compact_web_search_result from .chat_flow_helpers import detect_tool_failure from .chat_flow_runner_helpers import resolve_monitor_path, resolve_monitor_memory, capture_monitor_snapshot from utils.tool_result_formatter import format_tool_result_for_context from config import TOOL_CALL_COOLDOWN async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, client_sid: str, username: str, iteration: int, conversation_id: Optional[str], last_tool_call_time: float, process_sub_agent_updates, maybe_mark_failure_from_message, mark_force_thinking, get_stop_flag, clear_stop_flag): previous_tool_loop_active = getattr(web_terminal, "_tool_loop_active", False) web_terminal._tool_loop_active = True # 执行每个工具 for tool_call in tool_calls: # 检查停止标志 client_stop_info = get_stop_flag(client_sid, username) if client_stop_info: stop_requested = client_stop_info.get('stop', False) if isinstance(client_stop_info, dict) else client_stop_info if stop_requested: debug_log("在工具调用过程中检测到停止状态") tool_call_id = tool_call.get("id") function_name = tool_call.get("function", {}).get("name") # 通知前端该工具已被取消,避免界面卡住 sender('update_action', { 'preparing_id': tool_call_id, 'status': 'cancelled', 'result': { "success": False, "status": "cancelled", "message": "命令执行被用户取消", "tool": function_name } }) # 在消息列表中记录取消结果,防止重新加载时仍显示运行中 if tool_call_id: messages.append({ "role": "tool", "tool_call_id": tool_call_id, "name": function_name, "content": "命令执行被用户取消", "metadata": {"status": "cancelled"} }) sender('task_stopped', { 'message': '命令执行被用户取消', 'reason': 'user_stop' }) clear_stop_flag(client_sid, username) web_terminal._tool_loop_active = previous_tool_loop_active return {"stopped": True, "last_tool_call_time": last_tool_call_time} # 工具调用间隔控制 current_time = time.time() if last_tool_call_time > 0: elapsed = current_time - last_tool_call_time if elapsed < TOOL_CALL_COOLDOWN: await asyncio.sleep(TOOL_CALL_COOLDOWN - elapsed) last_tool_call_time = time.time() function_name = tool_call["function"]["name"] arguments_str = tool_call["function"]["arguments"] tool_call_id = tool_call["id"] debug_log(f"准备解析JSON,工具: {function_name}, 参数长度: {len(arguments_str)}") debug_log(f"JSON参数前200字符: {arguments_str[:200]}") debug_log(f"JSON参数后200字符: {arguments_str[-200:]}") # 使用改进的参数解析方法 if hasattr(web_terminal, 'api_client') and hasattr(web_terminal.api_client, '_safe_tool_arguments_parse'): success, arguments, error_msg = web_terminal.api_client._safe_tool_arguments_parse(arguments_str, function_name) if not success: debug_log(f"安全解析失败: {error_msg}") error_text = f'工具参数解析失败: {error_msg}' error_payload = { "success": False, "error": error_text, "error_type": "parameter_format_error", "tool_name": function_name, "tool_call_id": tool_call_id, "message": error_text } sender('error', {'message': error_text}) sender('update_action', { 'preparing_id': tool_call_id, 'status': 'completed', 'result': error_payload, 'message': error_text }) error_content = json.dumps(error_payload, ensure_ascii=False) web_terminal.context_manager.add_conversation( "tool", error_content, tool_call_id=tool_call_id, name=function_name ) messages.append({ "role": "tool", "tool_call_id": tool_call_id, "name": function_name, "content": error_content }) continue debug_log(f"使用安全解析成功,参数键: {list(arguments.keys())}") else: # 回退到带有基本修复逻辑的解析 try: arguments = json.loads(arguments_str) if arguments_str.strip() else {} debug_log(f"直接JSON解析成功,参数键: {list(arguments.keys())}") except json.JSONDecodeError as e: debug_log(f"原始JSON解析失败: {e}") # 尝试基本的JSON修复 repaired_str = arguments_str.strip() repair_attempts = [] # 修复1: 未闭合字符串 if repaired_str.count('"') % 2 == 1: repaired_str += '"' repair_attempts.append("添加闭合引号") # 修复2: 未闭合JSON对象 if repaired_str.startswith('{') and not repaired_str.rstrip().endswith('}'): repaired_str = repaired_str.rstrip() + '}' repair_attempts.append("添加闭合括号") # 修复3: 截断的JSON(移除不完整的最后一个键值对) if not repair_attempts: # 如果前面的修复都没用上 last_comma = repaired_str.rfind(',') if last_comma > 0: repaired_str = repaired_str[:last_comma] + '}' repair_attempts.append("移除不完整的键值对") # 尝试解析修复后的JSON try: arguments = json.loads(repaired_str) debug_log(f"JSON修复成功: {', '.join(repair_attempts)}") debug_log(f"修复后参数键: {list(arguments.keys())}") except json.JSONDecodeError as repair_error: debug_log(f"JSON修复也失败: {repair_error}") debug_log(f"修复尝试: {repair_attempts}") debug_log(f"修复后内容前100字符: {repaired_str[:100]}") error_text = f'工具参数解析失败: {e}' error_payload = { "success": False, "error": error_text, "error_type": "parameter_format_error", "tool_name": function_name, "tool_call_id": tool_call_id, "message": error_text } sender('error', {'message': error_text}) sender('update_action', { 'preparing_id': tool_call_id, 'status': 'completed', 'result': error_payload, 'message': error_text }) error_content = json.dumps(error_payload, ensure_ascii=False) web_terminal.context_manager.add_conversation( "tool", error_content, tool_call_id=tool_call_id, name=function_name ) messages.append({ "role": "tool", "tool_call_id": tool_call_id, "name": function_name, "content": error_content }) continue debug_log(f"执行工具: {function_name} (ID: {tool_call_id})") # 发送工具开始事件 tool_display_id = f"tool_{iteration}_{function_name}_{time.time()}" monitor_snapshot = None snapshot_path = None memory_snapshot_type = None if function_name in MONITOR_FILE_TOOLS: snapshot_path = resolve_monitor_path(arguments) monitor_snapshot = capture_monitor_snapshot(web_terminal.file_manager, snapshot_path, MONITOR_SNAPSHOT_CHAR_LIMIT, debug_log) if monitor_snapshot: cache_monitor_snapshot(tool_display_id, 'before', monitor_snapshot) elif function_name in MONITOR_MEMORY_TOOLS: memory_snapshot_type = (arguments.get('memory_type') or 'main').lower() before_entries = None try: before_entries = resolve_monitor_memory(web_terminal.memory_manager._read_entries(memory_snapshot_type), MONITOR_MEMORY_ENTRY_LIMIT) except Exception as exc: debug_log(f"[MonitorSnapshot] 读取记忆失败: {memory_snapshot_type} ({exc})") if before_entries is not None: monitor_snapshot = { 'memory_type': memory_snapshot_type, 'entries': before_entries } cache_monitor_snapshot(tool_display_id, 'before', monitor_snapshot) sender('tool_start', { 'id': tool_display_id, 'name': function_name, 'arguments': arguments, 'preparing_id': tool_call_id, 'monitor_snapshot': monitor_snapshot, 'conversation_id': conversation_id }) brief_log(f"调用了工具: {function_name}") await asyncio.sleep(0.3) start_time = time.time() # 执行工具,同时监听停止标志 debug_log(f"[停止检测] 开始执行工具: {function_name}") tool_task = asyncio.create_task(web_terminal.handle_tool_call(function_name, arguments)) tool_result = None tool_cancelled = False # 在工具执行期间持续检查停止标志 check_count = 0 while not tool_task.done(): await asyncio.sleep(0.1) # 每100ms检查一次 check_count += 1 client_stop_info = get_stop_flag(client_sid, username) if client_stop_info: stop_requested = client_stop_info.get('stop', False) if isinstance(client_stop_info, dict) else client_stop_info if stop_requested: debug_log(f"[停止检测] 工具执行过程中检测到停止请求(检查次数:{check_count}),立即取消工具") tool_task.cancel() tool_cancelled = True break debug_log(f"[停止检测] 工具执行完成,cancelled={tool_cancelled}, 检查次数={check_count}") # 获取工具结果或处理取消 if tool_cancelled: try: await tool_task except asyncio.CancelledError: debug_log("[停止检测] 工具任务已被取消(CancelledError)") except Exception as e: debug_log(f"[停止检测] 工具任务取消时发生异常: {e}") # 返回取消消息 tool_result = json.dumps({ "success": False, "status": "cancelled", "message": "命令执行被用户取消" }, ensure_ascii=False) debug_log("[停止检测] 发送取消通知到前端") # 通知前端工具被取消 sender('update_action', { 'preparing_id': tool_call_id, 'status': 'cancelled', 'result': { "success": False, "status": "cancelled", "message": "命令执行被用户取消", "tool": function_name } }) # 记录取消结果到消息历史 messages.append({ "role": "tool", "tool_call_id": tool_call_id, "name": function_name, "content": "命令执行被用户取消", "metadata": {"status": "cancelled"} }) # 保存取消结果 web_terminal.context_manager.add_conversation( "tool", "命令执行被用户取消", tool_call_id=tool_call_id, name=function_name, metadata={"status": "cancelled"} ) debug_log("[停止检测] 取消结果已保存到对话历史") # 发送停止事件并清除标志 sender('task_stopped', { 'message': '命令执行被用户取消', 'reason': 'user_stop' }) clear_stop_flag(client_sid, username) debug_log("[停止检测] 返回stopped=True") web_terminal._tool_loop_active = previous_tool_loop_active return {"stopped": True, "last_tool_call_time": last_tool_call_time} else: tool_result = await tool_task debug_log(f"工具结果: {tool_result[:200]}...") execution_time = time.time() - start_time if execution_time < 1.5: await asyncio.sleep(1.5 - execution_time) # 更新工具状态 result_data = {} try: result_data = json.loads(tool_result) except: result_data = {'output': tool_result} tool_failed = detect_tool_failure(result_data) action_status = 'completed' action_message = None awaiting_flag = False if function_name in {"write_file", "edit_file"}: diff_path = result_data.get("path") or arguments.get("file_path") summary = result_data.get("summary") or result_data.get("message") if summary: action_message = summary debug_log(f"{function_name} 执行完成: {summary or '无摘要'}") if function_name == "wait_sub_agent": system_msg = result_data.get("system_message") if system_msg: messages.append({ "role": "system", "content": system_msg }) sender('system_message', { 'content': system_msg, 'inline': False }) maybe_mark_failure_from_message(web_terminal, system_msg) monitor_snapshot_after = None if function_name in MONITOR_FILE_TOOLS: result_path = None if isinstance(result_data, dict): result_path = resolve_monitor_path(result_data) if not result_path: candidate_path = result_data.get('path') if isinstance(candidate_path, str) and candidate_path.strip(): result_path = candidate_path.strip() if not result_path: result_path = resolve_monitor_path(arguments, snapshot_path) or snapshot_path monitor_snapshot_after = capture_monitor_snapshot(web_terminal.file_manager, result_path, MONITOR_SNAPSHOT_CHAR_LIMIT, debug_log) elif function_name in MONITOR_MEMORY_TOOLS: memory_after_type = str( arguments.get('memory_type') or (isinstance(result_data, dict) and result_data.get('memory_type')) or memory_snapshot_type or 'main' ).lower() after_entries = None try: after_entries = resolve_monitor_memory(web_terminal.memory_manager._read_entries(memory_after_type), MONITOR_MEMORY_ENTRY_LIMIT) except Exception as exc: debug_log(f"[MonitorSnapshot] 读取记忆失败(after): {memory_after_type} ({exc})") if after_entries is not None: monitor_snapshot_after = { 'memory_type': memory_after_type, 'entries': after_entries } update_payload = { 'id': tool_display_id, 'status': action_status, 'result': result_data, 'preparing_id': tool_call_id, 'conversation_id': conversation_id } if action_message: update_payload['message'] = action_message if awaiting_flag: update_payload['awaiting_content'] = True if monitor_snapshot_after: update_payload['monitor_snapshot_after'] = monitor_snapshot_after cache_monitor_snapshot(tool_display_id, 'after', monitor_snapshot_after) sender('update_action', update_payload) if function_name in ['create_file', 'delete_file', 'rename_file', 'create_folder']: if not web_terminal.context_manager._is_host_mode_without_safety(): structure = web_terminal.context_manager.get_project_structure() sender('file_tree_update', structure) # ===== 增量保存:立即保存工具结果 ===== metadata_payload = None tool_images = None tool_videos = None if isinstance(result_data, dict): # 特殊处理 web_search:保留可供前端渲染的精简结构,以便历史记录复现搜索结果 if function_name == "web_search": try: tool_result_content = json.dumps(compact_web_search_result(result_data), ensure_ascii=False) except Exception: tool_result_content = tool_result else: tool_result_content = format_tool_result_for_context(function_name, result_data, tool_result) metadata_payload = {"tool_payload": result_data} else: tool_result_content = tool_result tool_message_content = tool_result_content # view_image: 将图片直接附加到 tool 结果中(不再插入 user 消息) if function_name == "view_image" and getattr(web_terminal, "pending_image_view", None): inj = web_terminal.pending_image_view web_terminal.pending_image_view = None if ( not tool_failed and isinstance(result_data, dict) and result_data.get("success") is not False ): img_path = inj.get("path") if isinstance(inj, dict) else None if img_path: text_part = tool_result_content if isinstance(tool_result_content, str) else "" tool_message_content = web_terminal.context_manager._build_content_with_images( text_part, [img_path] ) tool_images = [img_path] if metadata_payload is None: metadata_payload = {} metadata_payload["tool_image_path"] = img_path sender('system_message', { 'content': f'系统已按模型请求将图片附加到工具结果: {img_path}' }) # view_video: 将视频直接附加到 tool 结果中(不再插入 user 消息) if function_name == "view_video" and getattr(web_terminal, "pending_video_view", None): inj = web_terminal.pending_video_view web_terminal.pending_video_view = None if ( not tool_failed and isinstance(result_data, dict) and result_data.get("success") is not False ): video_path = inj.get("path") if isinstance(inj, dict) else None if video_path: text_part = tool_result_content if isinstance(tool_result_content, str) else "" video_payload = [video_path] tool_message_content = web_terminal.context_manager._build_content_with_images( text_part, [], video_payload ) tool_videos = [video_path] if metadata_payload is None: metadata_payload = {} metadata_payload["tool_video_path"] = video_path sender('system_message', { 'content': f'系统已按模型请求将视频附加到工具结果: {video_path}' }) # 立即保存工具结果 web_terminal.context_manager.add_conversation( "tool", tool_result_content, tool_call_id=tool_call_id, name=function_name, metadata=metadata_payload, images=tool_images, videos=tool_videos ) debug_log(f"💾 增量保存:工具结果 {function_name}") if function_name != "wait_sub_agent": system_message = result_data.get("system_message") if isinstance(result_data, dict) else None if system_message: web_terminal._record_sub_agent_message(system_message, result_data.get("task_id"), inline=False) maybe_mark_failure_from_message(web_terminal, system_message) # 添加到消息历史(用于API继续对话) messages.append({ "role": "tool", "tool_call_id": tool_call_id, "name": function_name, "content": tool_message_content }) debug_log(f"[SubAgent] after tool={function_name} call_id={tool_call_id} -> poll updates") await process_sub_agent_updates(messages=messages, inline=True, after_tool_call_id=tool_call_id, web_terminal=web_terminal, sender=sender, debug_log=debug_log, maybe_mark_failure_from_message=maybe_mark_failure_from_message) await asyncio.sleep(0.2) if tool_failed: mark_force_thinking(web_terminal, reason=f"{function_name}_failed") web_terminal._tool_loop_active = previous_tool_loop_active return {"stopped": False, "last_tool_call_time": last_tool_call_time}