agent-Specialization/core/main_terminal_parts/tools_execution.py

1447 lines
76 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import asyncio
import json
import time
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Set
try:
from config import (
OUTPUT_FORMATS, DATA_DIR, PROMPTS_DIR, NEED_CONFIRMATION,
MAX_TERMINALS, TERMINAL_BUFFER_SIZE, TERMINAL_DISPLAY_SIZE,
MAX_READ_FILE_CHARS, READ_TOOL_DEFAULT_MAX_CHARS,
READ_TOOL_DEFAULT_CONTEXT_BEFORE, READ_TOOL_DEFAULT_CONTEXT_AFTER,
READ_TOOL_MAX_CONTEXT_BEFORE, READ_TOOL_MAX_CONTEXT_AFTER,
READ_TOOL_DEFAULT_MAX_MATCHES, READ_TOOL_MAX_MATCHES,
READ_TOOL_MAX_FILE_SIZE,
TERMINAL_SANDBOX_MOUNT_PATH,
TERMINAL_SANDBOX_MODE,
TERMINAL_SANDBOX_CPUS,
TERMINAL_SANDBOX_MEMORY,
PROJECT_MAX_STORAGE_MB,
CUSTOM_TOOLS_ENABLED,
READONLY_RUN_COMMAND_ALLOWED,
READONLY_RUN_COMMAND_ALLOWED_GIT_SUBCOMMANDS,
READONLY_RUN_COMMAND_BLOCKED_TOKENS,
)
except ImportError:
import sys
project_root = Path(__file__).resolve().parents[2]
if str(project_root) not in sys.path:
sys.path.insert(0, str(project_root))
from config import (
OUTPUT_FORMATS, DATA_DIR, PROMPTS_DIR, NEED_CONFIRMATION,
MAX_TERMINALS, TERMINAL_BUFFER_SIZE, TERMINAL_DISPLAY_SIZE,
MAX_READ_FILE_CHARS, READ_TOOL_DEFAULT_MAX_CHARS,
READ_TOOL_DEFAULT_CONTEXT_BEFORE, READ_TOOL_DEFAULT_CONTEXT_AFTER,
READ_TOOL_MAX_CONTEXT_BEFORE, READ_TOOL_MAX_CONTEXT_AFTER,
READ_TOOL_DEFAULT_MAX_MATCHES, READ_TOOL_MAX_MATCHES,
READ_TOOL_MAX_FILE_SIZE,
TERMINAL_SANDBOX_MOUNT_PATH,
TERMINAL_SANDBOX_MODE,
TERMINAL_SANDBOX_CPUS,
TERMINAL_SANDBOX_MEMORY,
PROJECT_MAX_STORAGE_MB,
CUSTOM_TOOLS_ENABLED,
READONLY_RUN_COMMAND_ALLOWED,
READONLY_RUN_COMMAND_ALLOWED_GIT_SUBCOMMANDS,
READONLY_RUN_COMMAND_BLOCKED_TOKENS,
)
from modules.file_manager import FileManager
from modules.search_engine import SearchEngine
from modules.terminal_ops import TerminalOperator
from modules.memory_manager import MemoryManager
from modules.terminal_manager import TerminalManager
from modules.todo_manager import TodoManager
from modules.sub_agent_manager import SubAgentManager
from modules.webpage_extractor import extract_webpage_content, tavily_extract
from modules.ocr_client import OCRClient
from modules.easter_egg_manager import EasterEggManager
from modules.personalization_manager import (
load_personalization_config,
save_personalization_config,
build_personalization_prompt,
MAX_SHORT_FIELD_LENGTH,
MAX_CONSIDERATION_LENGTH,
MAX_CONSIDERATION_ITEMS,
ALLOWED_THEMES,
)
from modules.skills_manager import (
get_skills_catalog,
build_skills_list,
merge_enabled_skills,
build_skills_prompt,
)
from modules.custom_tool_registry import CustomToolRegistry, build_default_tool_category
from modules.custom_tool_executor import CustomToolExecutor
try:
from config.limits import THINKING_FAST_INTERVAL
except ImportError:
THINKING_FAST_INTERVAL = 10
from modules.container_monitor import collect_stats, inspect_state
from core.tool_config import TOOL_CATEGORIES
from utils.api_client import DeepSeekClient
from utils.context_manager import ContextManager
from utils.tool_result_formatter import format_tool_result_for_context
from utils.logger import setup_logger
from config.model_profiles import (
get_model_profile,
get_model_prompt_replacements,
get_model_context_window,
)
logger = setup_logger(__name__)
DISABLE_LENGTH_CHECK = True
class MainTerminalToolsExecutionMixin:
_TERMINAL_SERIES_TOOLS = {
"terminal_session",
"terminal_input",
"terminal_snapshot",
}
_SUB_AGENT_SERIES_TOOLS = {
"create_sub_agent",
"wait_sub_agent",
"close_sub_agent",
"terminate_sub_agent",
"get_sub_agent_status",
}
_READONLY_ALLOWED_TOOLS = {
"web_search",
"extract_webpage",
"read_file",
"view_image",
"view_video",
"vlm_analyze",
"ocr_image",
"update_memory",
"todo_create",
"todo_update_task",
"todo_get",
"sleep",
}
_APPROVAL_REQUIRED_TOOLS = {
"run_command",
"run_python",
"terminal_input",
"write_file",
"edit_file",
"create_file",
"create_folder",
"delete_file",
"rename_file",
"save_webpage",
"terminal_session",
}
def _record_sub_agent_message(self, message: Optional[str], task_id: Optional[str] = None, inline: bool = False):
"""以 system 消息记录子智能体状态。"""
if not message:
return
if task_id and task_id in self._announced_sub_agent_tasks:
return
if task_id:
self._announced_sub_agent_tasks.add(task_id)
logger.info(
"[SubAgent] record message | task=%s | inline=%s | content=%s",
task_id,
inline,
message.replace("\n", "\\n")[:200],
)
metadata = {"sub_agent_notice": True, "inline": inline, "is_auto_generated": True, "auto_message_type": "sub_agent_notice"}
if task_id:
metadata["task_id"] = task_id
self.context_manager.add_conversation("system", message, metadata=metadata)
print(f"{OUTPUT_FORMATS['info']} {message}")
# 扩展的只读命令白名单(包含常用管道命令)
_READONLY_ALLOWED_EXECUTABLES = {
"grep", "find", "ls", "pwd", "tree", "cat", "head", "tail",
"less", "rg", "wc", "du", "stat", "file", "sed", "awk", "git",
"sort", "uniq", "cut", "tr", "echo", "printf", "date",
"basename", "dirname", "which", "strings", "ps", "who", "id",
}
# 管道中绝对禁止的目标命令(宁可错杀)
_DANGEROUS_PIPE_TARGETS = {
"sh", "bash", "zsh", "fish", "dash", "ksh", "csh", "tcsh",
"rm", "mv", "cp", "dd", "rmdir", "unlink", "mkdir", "touch",
"chmod", "chown", "chgrp", "chattr", "setfacl",
"tee", "sponge", "xargs", "parallel",
"exec", "source", ".", "eval",
}
def _is_readonly_run_command_allowed(self, command: Any) -> bool:
cmd = str(command or "").strip()
if not cmd:
return False
lowered = cmd.lower()
# 1. 绝对禁止的token子shell、逻辑运算符、重定向
# 管道符 | 单独处理,不在此禁止
absolute_forbidden = ("&&", "||", ";", ">", "<", "$(", "`", "&",)
if any(token in cmd for token in absolute_forbidden):
return False
# 2. 如果包含管道,使用管道专用检查
if "|" in cmd:
return self._is_readonly_pipeline_allowed(cmd)
# 3. 普通单命令检查
return self._is_readonly_single_command_allowed(cmd)
def _is_readonly_pipeline_allowed(self, command: str) -> bool:
"""检查管道链中每个命令是否都是只读的"""
# 分割管道段(保留空字符串检查)
segments = [seg.strip() for seg in command.split("|") if seg.strip()]
if not segments:
return False
for segment in segments:
# 段内禁止子shell双重检查
if "$(" in segment or "`" in segment:
return False
# 提取命令名
parts = segment.split()
if not parts:
return False
executable = parts[0].lower()
# 检查段内是否有输出重定向(管道内也不允许)
if ">" in segment:
return False
# 检查是否是安全的只读命令
if not self._is_safe_readonly_executable(executable, segment):
return False
# 检查是否是危险的管道目标
if self._is_dangerous_pipe_target(segment):
return False
return True
def _is_readonly_single_command_allowed(self, command: str) -> bool:
"""检查单个命令是否是只读的"""
lowered = command.lower()
parts = command.split()
if not parts:
return False
executable = parts[0].lower()
# 使用基础白名单
basic_allowed = set(READONLY_RUN_COMMAND_ALLOWED or ())
if executable in basic_allowed and executable not in {"git", "sed", "awk"}:
return True
# 特殊命令检查
if executable == "git":
if len(parts) < 2:
return False
git_sub = parts[1].lower()
return git_sub in set(READONLY_RUN_COMMAND_ALLOWED_GIT_SUBCOMMANDS or ())
if executable == "sed":
return len(parts) >= 2 and parts[1] == "-n"
if executable == "awk":
return "system(" not in lowered
return False
def _is_safe_readonly_executable(self, executable: str, segment: str) -> bool:
"""检查单个命令是否在扩展白名单中"""
if executable not in self._READONLY_ALLOWED_EXECUTABLES:
return False
# 特殊命令的安全检查
lowered = segment.lower()
if executable == "sed":
return "-n" in segment
if executable == "awk":
return "system(" not in lowered
if executable == "git":
parts = segment.split()
if len(parts) < 2:
return False
git_sub = parts[1].lower()
return git_sub in set(READONLY_RUN_COMMAND_ALLOWED_GIT_SUBCOMMANDS or ())
# 检查其他命令是否有危险的参数组合
# 例如sort -o输出到文件是危险的
if executable == "sort":
return "-o" not in segment and "--output" not in lowered
return True
def _is_dangerous_pipe_target(self, segment: str) -> bool:
"""检查管道目标是否会执行写入/修改/危险操作"""
lowered = segment.lower()
parts = lowered.split()
if not parts:
return False
cmd = parts[0]
# 检查是否在危险命令列表中
if cmd in self._DANGEROUS_PIPE_TARGETS:
return True
# 检查是否有危险的参数(如 -exec, -delete 等)
dangerous_args = ("-exec", "-delete", "-ok", "-execdir", "-okdir")
if any(arg in lowered for arg in dangerous_args):
return True
return False
def evaluate_tool_permission(self, tool_name: str, arguments: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
mode = "unrestricted"
try:
mode = self.get_permission_mode()
except Exception:
mode = str(getattr(self, "current_permission_mode", "unrestricted") or "unrestricted")
args = arguments or {}
if mode == "readonly":
if tool_name == "run_command":
if self._is_readonly_run_command_allowed(args.get("command")):
return {"allowed": True, "mode": mode}
return {
"allowed": False,
"mode": mode,
"code": "readonly_denied",
"message": (
"当前处于只读模式run_command 仅允许只读命令"
"grep/find/ls/cat/rg/git status 等)且禁止多指令拼接。"
)
}
if tool_name not in self._READONLY_ALLOWED_TOOLS:
return {
"allowed": False,
"mode": mode,
"code": "readonly_denied",
"message": "当前处于只读模式,已拒绝会修改工作区或执行高风险操作的工具调用。"
}
return {"allowed": True, "mode": mode}
if mode == "approval":
if tool_name in {"run_command", "terminal_input"}:
command_text = args.get("command") or args.get("input")
if self._is_readonly_run_command_allowed(command_text):
return {"allowed": True, "mode": mode, "requires_approval": False}
if tool_name in self._APPROVAL_REQUIRED_TOOLS:
return {"allowed": True, "mode": mode, "requires_approval": True}
return {"allowed": True, "mode": mode}
return {"allowed": True, "mode": mode}
@staticmethod
def _skill_meta_key(skill_id: str) -> str:
return f"skill_read::{skill_id}"
@staticmethod
def _file_read_meta_key() -> str:
return "read_file::visited_paths"
def _normalize_tool_path(self, path: Any) -> str:
raw = str(path or "").strip().replace("\\", "/")
if not raw:
return ""
if raw.startswith("/workspace/"):
raw = raw.split("/workspace/", 1)[1]
try:
raw_path = Path(raw)
if raw_path.is_absolute():
abs_path = raw_path.expanduser().resolve()
else:
abs_path = (Path(self.context_manager.project_path).resolve() / raw_path).resolve()
project_root = Path(self.context_manager.project_path).resolve()
try:
rel = abs_path.relative_to(project_root)
return str(rel).replace("\\", "/").lstrip("./").lower()
except Exception:
return str(abs_path).replace("\\", "/").lower()
except Exception:
return raw.lstrip("./").lower()
def _mark_skill_read_from_result(self, tool_name: str, arguments: Dict[str, Any], result: Dict[str, Any]) -> None:
if tool_name != "read_file":
return
if not isinstance(result, dict) or not result.get("success"):
return
normalized = self._normalize_tool_path(result.get("path") or arguments.get("path"))
if not normalized:
return
if normalized.endswith("skills/terminal-guide/skill.md"):
self.context_manager._set_meta_flag(self._skill_meta_key("terminal-guide"), True)
if normalized.endswith("skills/sub-agent-guide/skill.md"):
self.context_manager._set_meta_flag(self._skill_meta_key("sub-agent-guide"), True)
if normalized.endswith("skills/run-command-guide/skill.md"):
self.context_manager._set_meta_flag(self._skill_meta_key("run-command-guide"), True)
def _get_visited_read_files(self) -> Set[str]:
raw = self.context_manager._get_meta_flag(self._file_read_meta_key(), [])
if isinstance(raw, set):
items = raw
elif isinstance(raw, list):
items = set(raw)
elif isinstance(raw, tuple):
items = set(raw)
else:
items = set()
visited: Set[str] = set()
for item in items:
normalized = self._normalize_tool_path(item)
if normalized:
visited.add(normalized)
return visited
def _mark_file_read_from_result(self, tool_name: str, arguments: Dict[str, Any], result: Dict[str, Any]) -> None:
if tool_name != "read_file":
return
if not isinstance(result, dict) or not result.get("success"):
return
normalized = self._normalize_tool_path(result.get("path") or arguments.get("path"))
if not normalized:
return
visited = self._get_visited_read_files()
if normalized in visited:
return
visited.add(normalized)
self.context_manager._set_meta_flag(
self._file_read_meta_key(),
sorted(visited),
)
def _has_read_file_in_conversation(self, path: Any) -> bool:
normalized = self._normalize_tool_path(path)
if not normalized:
return False
return normalized in self._get_visited_read_files()
def _mark_file_as_read_visited(self, path: Any) -> None:
normalized = self._normalize_tool_path(path)
if not normalized:
return
visited = self._get_visited_read_files()
if normalized in visited:
return
visited.add(normalized)
self.context_manager._set_meta_flag(
self._file_read_meta_key(),
sorted(visited),
)
def _target_file_exists(self, path: Any) -> bool:
try:
valid, _, full_path = self.file_manager._validate_path(str(path or ""))
if not valid or full_path is None:
return False
return full_path.exists() and full_path.is_file()
except Exception:
return False
def _check_read_before_edit_prerequisite(self, tool_name: str, arguments: Dict[str, Any]) -> Optional[Dict[str, Any]]:
target_path = arguments.get("file_path")
if not target_path:
return None
if tool_name == "write_file":
append_mode = bool(arguments.get("append", False))
if append_mode:
return None
# 覆盖模式仅对“已存在文件”执行先读后写约束;
# 新文件创建仍允许直接 write_file。
if not self._target_file_exists(target_path):
return None
if self._has_read_file_in_conversation(target_path):
return None
return {
"success": False,
"error": f"调用 {tool_name} 前必须先使用 read_file 阅读目标文件",
"message": (
f"文件 {target_path} 尚未在当前对话中通过 read_file 阅读。"
"请先使用 read_fileread/search/extract 任一模式)读取后再编辑。"
),
"required_tool": "read_file",
"required_path": str(target_path),
"enforcement": "read_before_edit_required",
}
def _is_skill_enabled_for_enforcement(self, skill_id: str) -> bool:
enabled_skill_ids = getattr(self, "enabled_skill_ids", None)
if isinstance(enabled_skill_ids, set):
return skill_id in enabled_skill_ids
if isinstance(enabled_skill_ids, (list, tuple)):
return skill_id in set(enabled_skill_ids)
try:
config = load_personalization_config(self.data_dir)
catalog = get_skills_catalog()
enabled = merge_enabled_skills(
config.get("enabled_skills") if isinstance(config, dict) else None,
catalog,
config.get("skills_catalog_snapshot") if isinstance(config, dict) else None,
)
return skill_id in set(enabled or [])
except Exception:
return False
def _required_skill_for_tool(self, tool_name: str, arguments: Optional[Dict[str, Any]] = None) -> Optional[str]:
if tool_name in self._TERMINAL_SERIES_TOOLS and bool(getattr(self, "skill_strict_terminal_enabled", False)):
return "terminal-guide"
if tool_name in self._SUB_AGENT_SERIES_TOOLS and bool(getattr(self, "skill_strict_sub_agent_enabled", False)):
return "sub-agent-guide"
if (
tool_name == "run_command"
):
run_in_background = bool((arguments or {}).get("run_in_background", False))
if (
run_in_background
and bool(getattr(self, "skill_strict_run_command_background_enabled", False))
):
return "run-command-guide"
if (
(not run_in_background)
and bool(getattr(self, "skill_strict_run_command_foreground_enabled", False))
):
return "run-command-guide"
return None
def _check_skill_strict_prerequisite(self, tool_name: str, arguments: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
skill_id = self._required_skill_for_tool(tool_name, arguments)
if not skill_id:
return None
# 仅当该 skill 在个人空间中启用时才执行强约束
if not self._is_skill_enabled_for_enforcement(skill_id):
return None
already_read = bool(
self.context_manager._get_meta_flag(
self._skill_meta_key(skill_id),
False
)
)
if already_read:
return None
required_path = f"skills/{skill_id}/SKILL.md"
return {
"success": False,
"error": f"调用 {tool_name} 前必须先阅读 {required_path}",
"message": f"请先使用 read_file 阅读 {required_path},随后再调用该工具。",
"required_skill": skill_id,
"required_path": required_path,
"enforcement": "skill_read_required"
}
async def handle_tool_call(self, tool_name: str, arguments: Dict) -> str:
"""处理工具调用(添加参数预检查和改进错误处理)"""
logger.info("[handle_tool_call] 工具调用开始: tool_name=%s, arguments=%s", tool_name, arguments)
# 导入字符限制配置
from config import (
MAX_READ_FILE_CHARS,
MAX_RUN_COMMAND_CHARS, MAX_EXTRACT_WEBPAGE_CHARS
)
# 检查是否需要确认
if tool_name in NEED_CONFIRMATION:
if not await self.confirm_action(tool_name, arguments):
return json.dumps({"success": False, "error": "用户取消操作"})
# === 新增:预检查参数大小和格式 ===
try:
# 检查参数总大小
arguments_str = json.dumps(arguments, ensure_ascii=False)
if len(arguments_str) > 200000: # 200KB限制
return json.dumps({
"success": False,
"error": f"参数过大({len(arguments_str)}字符)超过200KB限制",
"suggestion": "请分块处理或减少参数内容"
}, ensure_ascii=False)
# 针对特定工具的内容检查
if tool_name == "write_file":
content = arguments.get("content", "")
length_limit = 200000
if not DISABLE_LENGTH_CHECK and len(content) > length_limit:
return json.dumps({
"success": False,
"error": f"文件内容过长({len(content)}字符),超过{length_limit}字符限制",
"suggestion": "请分块写入,或设置 append=true 多次写入"
}, ensure_ascii=False)
if '\\' in content and content.count('\\') > len(content) / 10:
print(f"{OUTPUT_FORMATS['warning']} 检测到大量转义字符,可能存在格式问题")
except Exception as e:
return json.dumps({
"success": False,
"error": f"参数预检查失败: {str(e)}"
}, ensure_ascii=False)
# 自定义工具预解析(仅管理员)
custom_tool = None
if self.custom_tools_enabled and getattr(self, "user_role", "user") == "admin":
try:
self.custom_tool_registry.reload()
except Exception:
pass
custom_tool = self.custom_tool_registry.get_tool(tool_name)
# Skill 强约束:未阅读对应 SKILL.md 时,阻止 terminal/sub-agent 系列工具执行
strict_error = self._check_skill_strict_prerequisite(tool_name, arguments)
if strict_error:
return json.dumps(strict_error, ensure_ascii=False)
try:
if custom_tool:
result = await self.custom_tool_executor.run(tool_name, arguments)
elif tool_name == "read_file":
result = self._handle_read_tool(arguments)
self._mark_skill_read_from_result(tool_name, arguments, result)
self._mark_file_read_from_result(tool_name, arguments, result)
elif tool_name in {"vlm_analyze", "ocr_image"}:
path = arguments.get("path")
prompt = arguments.get("prompt")
if not path:
return json.dumps({"success": False, "error": "缺少 path 参数", "warnings": []}, ensure_ascii=False)
result = self.ocr_client.vlm_analyze(path=path, prompt=prompt or "")
elif tool_name == "view_image":
path = (arguments.get("path") or "").strip()
if not path:
return json.dumps({"success": False, "error": "path 不能为空"}, ensure_ascii=False)
host_unrestricted = self._is_host_mode()
if path.startswith("/workspace"):
if host_unrestricted:
path = path.split("/workspace", 1)[1].lstrip("/")
else:
return json.dumps({"success": False, "error": "非法路径,超出项目根目录,请使用不带/workspace的相对路径"}, ensure_ascii=False)
if host_unrestricted and (Path(path).is_absolute() or (len(path) > 1 and path[1] == ":")):
abs_path = Path(path).expanduser().resolve()
else:
abs_path = (Path(self.context_manager.project_path) / path).resolve()
if not host_unrestricted:
try:
abs_path.relative_to(Path(self.context_manager.project_path).resolve())
except Exception:
return json.dumps({"success": False, "error": "非法路径,超出项目根目录,请使用不带/workspace的相对路径"}, ensure_ascii=False)
if not abs_path.exists() or not abs_path.is_file():
return json.dumps({"success": False, "error": f"图片不存在: {path}"}, ensure_ascii=False)
if abs_path.stat().st_size > 10 * 1024 * 1024:
return json.dumps({"success": False, "error": "图片过大,需 <= 10MB"}, ensure_ascii=False)
allowed_ext = {".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".svg"}
if abs_path.suffix.lower() not in allowed_ext:
return json.dumps({"success": False, "error": f"不支持的图片格式: {abs_path.suffix}"}, ensure_ascii=False)
# 记录待附加图片,供上层将图片附加到工具结果
self.pending_image_view = {
"path": str(path)
}
result = {"success": True, "message": "图片已附加到工具结果中,将随 tool 返回。", "path": path}
elif tool_name == "view_video":
path = (arguments.get("path") or "").strip()
if not path:
return json.dumps({"success": False, "error": "path 不能为空"}, ensure_ascii=False)
host_unrestricted = self._is_host_mode()
if path.startswith("/workspace"):
if host_unrestricted:
path = path.split("/workspace", 1)[1].lstrip("/")
else:
return json.dumps({"success": False, "error": "非法路径,超出项目根目录,请使用相对路径"}, ensure_ascii=False)
if host_unrestricted and (Path(path).is_absolute() or (len(path) > 1 and path[1] == ":")):
abs_path = Path(path).expanduser().resolve()
else:
abs_path = (Path(self.context_manager.project_path) / path).resolve()
if not host_unrestricted:
try:
abs_path.relative_to(Path(self.context_manager.project_path).resolve())
except Exception:
return json.dumps({"success": False, "error": "非法路径,超出项目根目录,请使用相对路径"}, ensure_ascii=False)
if not abs_path.exists() or not abs_path.is_file():
return json.dumps({"success": False, "error": f"视频不存在: {path}"}, ensure_ascii=False)
allowed_ext = {".mp4", ".mov", ".mkv", ".avi", ".webm"}
if abs_path.suffix.lower() not in allowed_ext:
return json.dumps({"success": False, "error": f"不支持的视频格式: {abs_path.suffix}"}, ensure_ascii=False)
if abs_path.stat().st_size > 50 * 1024 * 1024:
return json.dumps({"success": False, "error": "视频过大,需 <= 50MB"}, ensure_ascii=False)
self.pending_video_view = {"path": str(path)}
result = {
"success": True,
"message": "视频已附加到工具结果中,将随 tool 返回。",
"path": path
}
# 终端会话管理工具
elif tool_name == "terminal_session":
action = arguments["action"]
if action == "open":
result = self.terminal_manager.open_terminal(
session_name=arguments.get("session_name", "default"),
working_dir=arguments.get("working_dir"),
make_active=True
)
if result["success"]:
print(f"{OUTPUT_FORMATS['session']} 终端会话已打开: {arguments.get('session_name', 'default')}")
elif action == "close":
result = self.terminal_manager.close_terminal(
session_name=arguments.get("session_name", "default")
)
if result["success"]:
print(f"{OUTPUT_FORMATS['session']} 终端会话已关闭: {arguments.get('session_name', 'default')}")
elif action == "list":
result = self.terminal_manager.list_terminals()
elif action == "reset":
result = self.terminal_manager.reset_terminal(
session_name=arguments.get("session_name")
)
if result["success"]:
print(f"{OUTPUT_FORMATS['session']} 终端会话已重置: {result['session']}")
else:
result = {"success": False, "error": f"未知操作: {action}"}
result["action"] = action
# 终端输入工具
elif tool_name == "terminal_input":
output_wait = arguments.get("output_wait")
if output_wait is None:
output_wait = arguments.get("timeout")
result = self.terminal_manager.send_to_terminal(
command=arguments["command"],
session_name=arguments.get("session_name"),
output_wait=output_wait
)
if result["success"]:
print(f"{OUTPUT_FORMATS['terminal']} 执行命令: {arguments['command']}")
elif tool_name == "terminal_snapshot":
result = self.terminal_manager.get_terminal_snapshot(
session_name=arguments.get("session_name"),
lines=arguments.get("lines"),
max_chars=arguments.get("max_chars")
)
# sleep工具
elif tool_name == "sleep":
seconds = arguments.get("seconds")
wait_sub_agent_ids = arguments.get("wait_sub_agent_ids")
wait_runcommand_id = arguments.get("wait_runcommand_id")
reason = arguments.get("reason", "等待操作完成")
provided = 0
if seconds is not None:
provided += 1
if wait_sub_agent_ids:
provided += 1
if wait_runcommand_id:
provided += 1
if provided == 0:
result = {
"success": False,
"error": "sleep 至少需要提供一个参数seconds / wait_sub_agent_ids / wait_runcommand_id"
}
elif provided > 1:
result = {
"success": False,
"error": "sleep 的等待参数互斥seconds / wait_sub_agent_ids / wait_runcommand_id 只能提供一个"
}
elif wait_sub_agent_ids:
if not isinstance(wait_sub_agent_ids, list) or not wait_sub_agent_ids:
result = {"success": False, "error": "wait_sub_agent_ids 必须是非空数组"}
else:
normalized_ids = []
invalid = []
for item in wait_sub_agent_ids:
try:
agent_id = int(item)
if agent_id <= 0:
raise ValueError()
normalized_ids.append(agent_id)
except Exception:
invalid.append(item)
if invalid:
result = {"success": False, "error": f"wait_sub_agent_ids 含非法值: {invalid}"}
else:
manager = getattr(self, "sub_agent_manager", None)
if not manager:
result = {"success": False, "error": "子智能体管理器不可用"}
else:
task_ids = []
missing = []
for aid in normalized_ids:
task = manager.lookup_task(agent_id=aid)
if not task:
missing.append(aid)
else:
task_ids.append(task.get("task_id"))
if missing:
result = {"success": False, "error": f"未找到对应子智能体: {missing}"}
else:
wait_results = []
waited_task_ids = []
for tid in task_ids:
wait_result = await asyncio.to_thread(
manager.wait_for_completion,
task_id=tid,
timeout_seconds=None,
)
wait_results.append(wait_result)
waited_task_ids.append(tid)
try:
task = manager.tasks.get(tid)
if isinstance(task, dict):
task["notified"] = True
task["updated_at"] = time.time()
except Exception:
pass
try:
manager._save_state()
except Exception:
pass
result = {
"success": True,
"mode": "wait_sub_agent_ids",
"agent_ids": normalized_ids,
"waited_task_ids": waited_task_ids,
"results": wait_results,
"message": f"已等待 {len(normalized_ids)} 个子智能体结束"
}
try:
if not hasattr(self, "_announced_sub_agent_tasks"):
self._announced_sub_agent_tasks = set()
for tid in waited_task_ids:
if tid:
self._announced_sub_agent_tasks.add(tid)
except Exception:
pass
elif wait_runcommand_id:
bg_manager = getattr(self, "background_command_manager", None)
if not bg_manager:
result = {"success": False, "error": "后台命令管理器不可用"}
else:
rec = bg_manager.get_record(str(wait_runcommand_id))
current_conv = getattr(self.context_manager, "current_conversation_id", None)
if not rec:
result = {"success": False, "error": f"未找到后台命令: {wait_runcommand_id}"}
elif rec.get("conversation_id") and current_conv and rec.get("conversation_id") != current_conv:
result = {"success": False, "error": "该后台命令不属于当前对话"}
else:
wait_result = await asyncio.to_thread(
bg_manager.wait_for_completion,
str(wait_runcommand_id),
None,
True,
)
bg_manager.mark_claimed(str(wait_runcommand_id))
result = {
"success": bool(wait_result.get("success", False)),
"mode": "wait_runcommand_id",
"command_id": str(wait_runcommand_id),
"result": wait_result,
"message": "后台 run_command 等待完成"
}
else:
max_sleep = 3600 # 最多等待3600秒1小时
seconds_parse_ok = True
try:
seconds = float(seconds)
except Exception:
result = {"success": False, "error": "seconds 必须是数字"}
seconds_parse_ok = False
seconds = 0.0
if seconds_parse_ok and seconds > max_sleep:
result = {
"success": False,
"error": f"等待时间过长,最多允许 {max_sleep}",
"suggestion": f"建议分多次等待或减少等待时间"
}
elif seconds_parse_ok:
# 确保秒数为正数
if seconds <= 0:
result = {
"success": False,
"error": "等待时间必须大于0"
}
else:
print(f"{OUTPUT_FORMATS['info']} 等待 {seconds} 秒: {reason}")
await asyncio.sleep(seconds)
result = {
"success": True,
"message": f"已等待 {seconds}",
"reason": reason,
"timestamp": datetime.now().isoformat()
}
print(f"{OUTPUT_FORMATS['success']} 等待完成")
elif tool_name == "create_file":
result = self.file_manager.create_file(
path=arguments["path"],
file_type=arguments["file_type"]
)
if isinstance(result, dict) and result.get("success"):
# create_file 新建文件后,视为当前会话已“接触”该文件,
# 避免后续 write_file/edit_file 被“先 read_file 再编辑”误拦截。
self._mark_file_as_read_visited(result.get("path") or arguments.get("path"))
# 添加备注
if result["success"] and arguments.get("annotation"):
self.context_manager.update_annotation(
result["path"],
arguments["annotation"]
)
if result.get("success"):
result["message"] = (
f"已创建空文件: {result['path']}。请使用 write_file 写入内容,或使用 edit_file 进行替换。"
)
elif tool_name == "delete_file":
result = self.file_manager.delete_file(arguments["path"])
# 如果删除成功,同时删除备注
if result.get("success") and result.get("action") == "deleted":
deleted_path = result.get("path")
# 删除备注
if deleted_path in self.context_manager.file_annotations:
del self.context_manager.file_annotations[deleted_path]
self.context_manager.save_annotations()
print(f"🧹 已删除文件备注: {deleted_path}")
elif tool_name == "rename_file":
result = self.file_manager.rename_file(
arguments["old_path"],
arguments["new_path"]
)
# 如果重命名成功更新备注和聚焦的key
# 如果重命名成功,更新备注
if result.get("success") and result.get("action") == "renamed":
old_path = result.get("old_path")
new_path = result.get("new_path")
# 更新备注
if old_path in self.context_manager.file_annotations:
annotation = self.context_manager.file_annotations[old_path]
del self.context_manager.file_annotations[old_path]
self.context_manager.file_annotations[new_path] = annotation
self.context_manager.save_annotations()
print(f"📝 已更新文件备注: {old_path} -> {new_path}")
elif tool_name == "write_file":
read_guard_error = self._check_read_before_edit_prerequisite(tool_name, arguments)
if read_guard_error:
return json.dumps(read_guard_error, ensure_ascii=False)
path = arguments.get("file_path")
content = arguments.get("content", "")
append_flag = bool(arguments.get("append", False))
if not path:
result = {"success": False, "error": "缺少必要参数: file_path"}
else:
mode = "a" if append_flag else "w"
result = self.file_manager.write_file(path, content, mode=mode)
if isinstance(result, dict) and result.get("success"):
# write_file 成功后,该文件视作当前会话已“接触”,
# 允许后续继续 write/edit 而不再触发先读拦截。
self._mark_file_as_read_visited(result.get("path") or path)
elif tool_name == "edit_file":
read_guard_error = self._check_read_before_edit_prerequisite(tool_name, arguments)
if read_guard_error:
return json.dumps(read_guard_error, ensure_ascii=False)
path = arguments.get("file_path")
old_text = arguments.get("old_string")
new_text = arguments.get("new_string")
if not path:
result = {"success": False, "error": "缺少必要参数: file_path"}
elif old_text is None or new_text is None:
result = {"success": False, "error": "缺少必要参数: old_string/new_string"}
elif old_text == new_text:
result = {"success": False, "error": "old_string 与 new_string 相同,无法执行替换"}
elif not old_text:
result = {"success": False, "error": "old_string 不能为空,请从 read_file 内容中精确复制"}
else:
result = self.file_manager.replace_in_file(path, old_text, new_text)
elif tool_name == "create_folder":
result = self.file_manager.create_folder(arguments["path"])
elif tool_name == "web_search":
allowed, quota_info = self.record_search_call()
if not allowed:
return json.dumps({
"success": False,
"error": f"搜索配额已用尽,将在 {quota_info.get('reset_at')} 重置。请向用户说明情况并提供替代方案。",
"quota": quota_info
}, ensure_ascii=False)
search_response = await self.search_engine.search_with_summary(
query=arguments["query"],
max_results=arguments.get("max_results"),
topic=arguments.get("topic"),
time_range=arguments.get("time_range"),
days=arguments.get("days"),
start_date=arguments.get("start_date"),
end_date=arguments.get("end_date"),
country=arguments.get("country"),
include_domains=arguments.get("include_domains")
)
if search_response["success"]:
result = {
"success": True,
"summary": search_response["summary"],
"filters": search_response.get("filters", {}),
"query": search_response.get("query"),
"results": search_response.get("results", []),
"total_results": search_response.get("total_results", 0)
}
else:
result = {
"success": False,
"error": search_response.get("error", "搜索失败"),
"filters": search_response.get("filters", {}),
"query": search_response.get("query"),
"results": search_response.get("results", []),
"total_results": search_response.get("total_results", 0)
}
elif tool_name == "extract_webpage":
url = arguments["url"]
try:
# 从config获取API密钥
from config import TAVILY_API_KEY
full_content, _ = await extract_webpage_content(
urls=url,
api_key=TAVILY_API_KEY,
extract_depth="basic",
max_urls=1
)
# 字符数检查
char_count = len(full_content)
if char_count > MAX_EXTRACT_WEBPAGE_CHARS:
result = {
"success": False,
"error": f"网页提取返回了过长的{char_count}字符请不要提取这个网页可以使用网页保存功能然后使用read工具查找或查看网页",
"char_count": char_count,
"limit": MAX_EXTRACT_WEBPAGE_CHARS,
"url": url
}
else:
result = {
"success": True,
"url": url,
"content": full_content
}
except Exception as e:
result = {
"success": False,
"error": f"网页提取失败: {str(e)}",
"url": url
}
elif tool_name == "save_webpage":
url = arguments["url"]
target_path = arguments["target_path"]
try:
from config import TAVILY_API_KEY
except ImportError:
TAVILY_API_KEY = None
if not TAVILY_API_KEY or TAVILY_API_KEY == "your-tavily-api-key":
result = {
"success": False,
"error": "Tavily API密钥未配置无法保存网页",
"url": url,
"path": target_path
}
else:
try:
extract_result = await tavily_extract(
urls=url,
api_key=TAVILY_API_KEY,
extract_depth="basic",
max_urls=1
)
if not extract_result or "error" in extract_result:
error_message = extract_result.get("error", "提取失败,未返回任何内容") if isinstance(extract_result, dict) else "提取失败"
result = {
"success": False,
"error": error_message,
"url": url,
"path": target_path
}
else:
results_list = extract_result.get("results", []) if isinstance(extract_result, dict) else []
primary_result = None
for item in results_list:
if item.get("raw_content"):
primary_result = item
break
if primary_result is None and results_list:
primary_result = results_list[0]
if not primary_result:
failed_list = extract_result.get("failed_results", []) if isinstance(extract_result, dict) else []
result = {
"success": False,
"error": "提取成功结果为空,无法保存",
"url": url,
"path": target_path,
"failed": failed_list
}
else:
content_to_save = primary_result.get("raw_content") or primary_result.get("content") or ""
if not content_to_save:
result = {
"success": False,
"error": "网页内容为空,未写入文件",
"url": url,
"path": target_path
}
else:
write_result = self.file_manager.write_file(target_path, content_to_save, mode="w")
if not write_result.get("success"):
result = {
"success": False,
"error": write_result.get("error", "写入文件失败"),
"url": url,
"path": target_path
}
else:
char_count = len(content_to_save)
byte_size = len(content_to_save.encode("utf-8"))
result = {
"success": True,
"url": url,
"path": write_result.get("path", target_path),
"char_count": char_count,
"byte_size": byte_size,
"message": f"网页内容已以纯文本保存到 {write_result.get('path', target_path)},可用 read_file 的 search/extract 查看,必要时再用终端命令。"
}
if isinstance(extract_result, dict) and extract_result.get("failed_results"):
result["warnings"] = extract_result["failed_results"]
except Exception as e:
result = {
"success": False,
"error": f"网页保存失败: {str(e)}",
"url": url,
"path": target_path
}
elif tool_name == "run_python":
result = await self.terminal_ops.run_python_code(
arguments["code"],
timeout=arguments.get("timeout")
)
elif tool_name == "run_command":
run_in_background = bool(arguments.get("run_in_background", False))
timeout_value = arguments.get("timeout")
if run_in_background:
if timeout_value is None or float(timeout_value) <= 0:
result = {
"success": False,
"error": "后台模式下 timeout 参数必填且需大于0"
}
elif float(timeout_value) > 3600:
result = {
"success": False,
"error": "后台模式下 timeout 最大为 3600 秒"
}
else:
bg_manager = getattr(self, "background_command_manager", None)
if not bg_manager:
result = {"success": False, "error": "后台命令管理器不可用"}
else:
result = bg_manager.create_background_command(
terminal_ops=self.terminal_ops,
command=arguments["command"],
timeout=timeout_value,
conversation_id=getattr(self.context_manager, "current_conversation_id", None),
wait_seconds=5.0,
)
else:
if timeout_value is None or float(timeout_value) <= 0:
result = {
"success": False,
"error": "timeout 参数必填且需大于0"
}
elif float(timeout_value) > 30:
result = {
"success": False,
"error": "前台模式下 timeout 最大为 30 秒"
}
else:
result = await self.terminal_ops.run_command(
arguments["command"],
timeout=timeout_value
)
# 字符数检查
if result.get("success") and "output" in result:
char_count = len(result["output"])
if char_count > MAX_RUN_COMMAND_CHARS:
result = {
"success": False,
"error": f"结果内容过大,有{char_count}字符请使用限制字符数的获取内容方式根据程度选择10k以内的数",
"char_count": char_count,
"limit": MAX_RUN_COMMAND_CHARS,
"command": arguments["command"]
}
elif tool_name == "update_memory":
operation = arguments["operation"]
content = arguments.get("content")
index = arguments.get("index")
# 参数校验
if operation == "append" and (not content or not str(content).strip()):
result = {"success": False, "error": "append 操作需要 content"}
elif operation == "replace" and (index is None or index <= 0 or not content or not str(content).strip()):
result = {"success": False, "error": "replace 操作需要有效的 index 和 content"}
elif operation == "delete" and (index is None or index <= 0):
result = {"success": False, "error": "delete 操作需要有效的 index"}
else:
# 统一使用 main 记忆类型
result = self.memory_manager.update_entries(
memory_type="main",
operation=operation,
content=content,
index=index
)
elif tool_name == "todo_create":
result = self.todo_manager.create_todo_list(
overview=arguments.get("overview", ""),
tasks=arguments.get("tasks", [])
)
elif tool_name == "todo_update_task":
task_indices = arguments.get("task_indices")
if task_indices is None:
task_indices = arguments.get("task_index")
result = self.todo_manager.update_task_status(
task_indices=task_indices,
completed=arguments.get("completed", True)
)
elif tool_name == "create_sub_agent":
result = self.sub_agent_manager.create_sub_agent(
agent_id=arguments.get("agent_id"),
summary=arguments.get("summary", ""),
task=arguments.get("task", ""),
deliverables_dir=arguments.get("deliverables_dir", ""),
run_in_background=arguments.get("run_in_background", False),
timeout_seconds=arguments.get("timeout_seconds"),
thinking_mode=arguments.get("thinking_mode"),
conversation_id=self.context_manager.current_conversation_id
)
# 如果不是后台运行,阻塞等待完成
if not arguments.get("run_in_background", False) and result.get("success"):
task_id = result.get("task_id")
wait_result = self.sub_agent_manager.wait_for_completion(
task_id=task_id,
timeout_seconds=arguments.get("timeout_seconds")
)
# 合并结果
result.update(wait_result)
# 阻塞式执行不需要额外插入 system 消息
result.pop("system_message", None)
# 标记已通知,避免后续轮询再插入 system 消息
try:
task = self.sub_agent_manager.tasks.get(task_id)
if isinstance(task, dict):
task["notified"] = True
task["updated_at"] = time.time()
self.sub_agent_manager._save_state()
except Exception:
pass
elif tool_name == "terminate_sub_agent":
result = self.sub_agent_manager.terminate_sub_agent(
agent_id=arguments.get("agent_id")
)
elif tool_name == "get_sub_agent_status":
result = self.sub_agent_manager.get_sub_agent_status(
agent_ids=arguments.get("agent_ids", [])
)
elif tool_name == "trigger_easter_egg":
result = self.easter_egg_manager.trigger_effect(arguments.get("effect"))
elif tool_name == "manage_personalization":
logger.info("[handle_tool_call] 进入manage_personalization分支")
result = await self._execute_manage_personalization(arguments)
logger.info("[handle_tool_call] manage_personalization执行完成: result=%s", result)
else:
result = {"success": False, "error": f"未知工具: {tool_name}"}
except Exception as e:
logger.error(f"工具执行失败: {tool_name} - {e}")
logger.exception("[handle_tool_call] 工具执行异常详情")
result = {"success": False, "error": f"工具执行异常: {str(e)}"}
logger.info("[handle_tool_call] 工具调用结束: tool_name=%s, result=%s", tool_name, result)
return json.dumps(result, ensure_ascii=False)
async def _execute_manage_personalization(self, arguments: Dict) -> Dict:
"""执行个性化管理操作"""
print(f"[DEBUG] _execute_manage_personalization 被调用: {arguments}")
logger.info("[_execute_manage_personalization] 方法被调用: arguments=%s", arguments)
action = arguments.get("action")
logger.info("[_execute_manage_personalization] action=%s", action)
if action == "read":
logger.info("[_execute_manage_personalization] 进入read分支")
# 读取所有配置
try:
config = load_personalization_config(self.data_dir)
# 只返回可修改的字段
readable_fields = ["self_identify", "user_name", "profession", "tone", "considerations", "theme", "enabled"]
result = {k: v for k, v in config.items() if k in readable_fields}
# 构建详细返回信息
field_descriptions = {
"enabled": "个性化功能总开关",
"self_identify": f"AI自称: {result.get('self_identify') or '(未设置)'}",
"user_name": f"AI如何称呼用户: {result.get('user_name') or '(未设置)'}",
"profession": f"用户职业: {result.get('profession') or '(未设置)'}",
"tone": f"交流语气: {result.get('tone') or '(未设置)'}",
"considerations": f"注意事项: {len(result.get('considerations') or [])}",
"theme": f"主题配色: {result.get('theme', 'classic')}"
}
details = "\n".join([f"- {field_descriptions.get(k, k)}: {v}" for k, v in result.items()])
logger.info("[_execute_manage_personalization] read成功")
return {
"success": True,
"data": result,
"message": f"个性化配置读取成功:\n{details}"
}
except Exception as e:
logger.error("[_execute_manage_personalization] read失败: %s", e)
return {"success": False, "error": f"读取配置失败: {str(e)}"}
elif action == "update":
logger.info("[_execute_manage_personalization] 进入update分支")
field = arguments.get("field")
value = arguments.get("value")
logger.info("[_execute_manage_personalization] field=%s, value=%s", field, value)
if not field:
logger.warning("[_execute_manage_personalization] field未指定")
return {"success": False, "error": "更新操作需要指定 field 参数"}
# 验证字段是否允许修改
logger.info("[_execute_manage_personalization] 验证字段: field=%s", field)
allowed_fields = ["self_identify", "user_name", "profession", "tone", "considerations", "theme"]
if field not in allowed_fields:
logger.warning("[_execute_manage_personalization] 字段不允许修改: %s not in %s", field, allowed_fields)
return {"success": False, "error": f"字段 '{field}' 不允许修改,可修改字段: {allowed_fields}"}
# 验证value
validation_errors = []
if field in ["self_identify", "user_name", "profession", "tone"]:
# 字符串字段验证
if not isinstance(value, str):
validation_errors.append(f"{field} 必须是字符串")
elif len(value) > MAX_SHORT_FIELD_LENGTH:
validation_errors.append(f"{field} 不能超过 {MAX_SHORT_FIELD_LENGTH} 个字符")
elif field == "considerations":
# 注意事项数组验证
if not isinstance(value, list):
validation_errors.append("considerations 必须是字符串数组")
else:
if len(value) > MAX_CONSIDERATION_ITEMS:
validation_errors.append(f"considerations 不能超过 {MAX_CONSIDERATION_ITEMS}")
for i, item in enumerate(value):
if not isinstance(item, str):
validation_errors.append(f"considerations[{i}] 必须是字符串")
elif len(item) > MAX_CONSIDERATION_LENGTH:
validation_errors.append(f"considerations[{i}] 不能超过 {MAX_CONSIDERATION_LENGTH} 个字符")
elif field == "theme":
# 主题验证
if not isinstance(value, str):
validation_errors.append("theme 必须是字符串")
elif value not in ALLOWED_THEMES:
validation_errors.append(f"theme 必须是以下之一: {list(ALLOWED_THEMES)}")
if validation_errors:
logger.warning("[_execute_manage_personalization] 验证失败: %s", validation_errors)
return {
"success": False,
"error": "验证失败",
"validation_errors": validation_errors
}
# 加载当前配置并更新
try:
logger.info("[_execute_manage_personalization] 开始加载配置")
config = load_personalization_config(self.data_dir)
old_value = config.get(field) # 记录旧值
config[field] = value
logger.info("[_execute_manage_personalization] 配置已修改: field=%s, old=%s, new=%s", field, old_value, value)
# 保存配置
save_personalization_config(self.data_dir, config)
print(f"[DEBUG] 配置已保存: field={field}, value={value}")
logger.info("[_execute_manage_personalization] 配置已保存到文件")
# 重新加载配置到当前终端
self.apply_personalization_preferences(config)
logger.info("[_execute_manage_personalization] 配置已应用到终端")
# 调试日志:记录主题变更
logger.info("[manage_personalization] 配置已更新并应用: field=%s, value=%s", field, value)
if field == "theme":
logger.info("[manage_personalization] 主题已变更: old=%s, new=%s", old_value, value)
# 构建返回结果
result = {
"success": True,
"message": f"字段 '{field}' 更新成功",
"updated_field": field,
"updated_value": value
}
# 如果是主题更新,添加主题变更标记
if field == "theme":
result["theme_changed"] = True
result["new_theme"] = value
logger.info("[manage_personalization] 返回结果包含主题变更标记: theme_changed=true, new_theme=%s", value)
return result
except Exception as e:
logger.error("[_execute_manage_personalization] 保存失败: %s", e)
logger.exception("[_execute_manage_personalization] 保存异常详情")
return {"success": False, "error": f"保存配置失败: {str(e)}"}
else:
logger.warning("[_execute_manage_personalization] 未知的action: %s", action)
return {"success": False, "error": f"未知的 action: {action},可选: read, update"}
async def confirm_action(self, action: str, arguments: Dict) -> bool:
"""确认危险操作"""
print(f"\n{OUTPUT_FORMATS['confirm']} 需要确认的操作:")
print(f" 操作: {action}")
print(f" 参数: {json.dumps(arguments, ensure_ascii=False, indent=2)}")
response = input("\n是否继续? (y/n): ").strip().lower()
return response == 'y'