主要修复: 1. 移除前端"取消跳转到正在运行的对话"的错误逻辑 - 删除 switchConversation 中的任务检查和确认提示 - 删除 createNewConversation 中的跳转回运行对话逻辑 - 删除 loadConversation 中对未定义变量 hasActiveTask 的引用 2. 修复后端工具执行返回值问题 - 修复 execute_tool_calls 在用户停止时返回 None 的 bug - 确保所有返回路径都返回包含 stopped 和 last_tool_call_time 的字典 3. 其他改进 - 添加代码复制功能 (handleCopyCodeClick) - 移除 FocusPanel 相关代码 - 更新个性化配置 (enhanced_tool_display) - 样式和主题优化 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
675 lines
37 KiB
Python
675 lines
37 KiB
Python
import asyncio
|
||
import json
|
||
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,
|
||
)
|
||
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,
|
||
)
|
||
|
||
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,
|
||
build_personalization_prompt,
|
||
)
|
||
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:
|
||
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}
|
||
if task_id:
|
||
metadata["task_id"] = task_id
|
||
self.context_manager.add_conversation("system", message, metadata=metadata)
|
||
print(f"{OUTPUT_FORMATS['info']} {message}")
|
||
|
||
async def handle_tool_call(self, tool_name: str, arguments: Dict) -> str:
|
||
"""处理工具调用(添加参数预检查和改进错误处理)"""
|
||
# 导入字符限制配置
|
||
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)
|
||
|
||
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)
|
||
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":
|
||
result = self.terminal_manager.send_to_terminal(
|
||
command=arguments["command"],
|
||
session_name=arguments.get("session_name"),
|
||
timeout=arguments.get("timeout")
|
||
)
|
||
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", 1)
|
||
reason = arguments.get("reason", "等待操作完成")
|
||
|
||
# 限制最大等待时间
|
||
max_sleep = 600 # 最多等待60秒
|
||
if seconds > max_sleep:
|
||
result = {
|
||
"success": False,
|
||
"error": f"等待时间过长,最多允许 {max_sleep} 秒",
|
||
"suggestion": f"建议分多次等待或减少等待时间"
|
||
}
|
||
else:
|
||
# 确保秒数为正数
|
||
if seconds <= 0:
|
||
result = {
|
||
"success": False,
|
||
"error": "等待时间必须大于0"
|
||
}
|
||
else:
|
||
print(f"{OUTPUT_FORMATS['info']} 等待 {seconds} 秒: {reason}")
|
||
|
||
# 执行等待
|
||
import asyncio
|
||
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 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":
|
||
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)
|
||
|
||
elif tool_name == "edit_file":
|
||
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":
|
||
result = await self.terminal_ops.run_command(
|
||
arguments["command"],
|
||
timeout=arguments.get("timeout")
|
||
)
|
||
|
||
# 字符数检查
|
||
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", ""),
|
||
target_dir=arguments.get("target_dir", ""),
|
||
reference_files=arguments.get("reference_files", []),
|
||
timeout_seconds=arguments.get("timeout_seconds"),
|
||
conversation_id=self.context_manager.current_conversation_id
|
||
)
|
||
|
||
elif tool_name == "wait_sub_agent":
|
||
wait_timeout = arguments.get("timeout_seconds")
|
||
if not wait_timeout:
|
||
task_ref = self.sub_agent_manager.lookup_task(
|
||
task_id=arguments.get("task_id"),
|
||
agent_id=arguments.get("agent_id")
|
||
)
|
||
if task_ref:
|
||
wait_timeout = task_ref.get("timeout_seconds")
|
||
result = self.sub_agent_manager.wait_for_completion(
|
||
task_id=arguments.get("task_id"),
|
||
agent_id=arguments.get("agent_id"),
|
||
timeout_seconds=wait_timeout
|
||
)
|
||
|
||
elif tool_name == "close_sub_agent":
|
||
result = self.sub_agent_manager.terminate_sub_agent(
|
||
task_id=arguments.get("task_id"),
|
||
agent_id=arguments.get("agent_id")
|
||
)
|
||
|
||
elif tool_name == "trigger_easter_egg":
|
||
result = self.easter_egg_manager.trigger_effect(arguments.get("effect"))
|
||
|
||
else:
|
||
result = {"success": False, "error": f"未知工具: {tool_name}"}
|
||
|
||
except Exception as e:
|
||
logger.error(f"工具执行失败: {tool_name} - {e}")
|
||
result = {"success": False, "error": f"工具执行异常: {str(e)}"}
|
||
|
||
return json.dumps(result, ensure_ascii=False)
|
||
|
||
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'
|