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 from modules.mcp_server_registry import MCPServerRegistry, build_default_mcp_category 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, model_supports_image, model_supports_video, ) logger = setup_logger(__name__) DISABLE_LENGTH_CHECK = True class MainTerminalToolsDefinitionMixin: @staticmethod def _mcp_disabled_message() -> str: return "当前为docker模式,MCP仅支持宿主机模式" def _is_mcp_disabled_in_docker_mode(self) -> bool: session = getattr(self, "container_session", None) return bool(session and getattr(session, "mode", None) == "docker") def _inject_intent(self, properties: Dict[str, Any]) -> Dict[str, Any]: """在工具参数中注入 intent(简短意图说明),仅当开关启用时。 字段含义:要求模型用不超过15个中文字符对即将执行的动作做简要说明,供前端展示。 """ if not self.tool_intent_enabled: return properties if not isinstance(properties, dict): return properties intent_field = { "intent": { "type": "string", "description": "用不超过15个字向用户说明你要做什么,例如:等待下载完成/创建日志文件" } } # 将 intent 放在最前面以提高模型关注度 return {**intent_field, **properties} def _apply_intent_to_tools(self, tools: List[Dict]) -> List[Dict]: """遍历工具列表,为缺少 intent 的工具补充字段(开关启用时生效)。""" if not self.tool_intent_enabled: return tools intent_field = { "intent": { "type": "string", "description": "用不超过15个字向用户说明你要做什么,例如:等待下载完成/创建日志文件/搜索最新新闻" } } for tool in tools: func = tool.get("function") or {} tool_name = str(func.get("name") or "").strip() # MCP 扩展工具参数需与远端 schema 严格一致,不注入 intent if tool_name.startswith("mcp__"): continue params = func.get("parameters") or {} if not isinstance(params, dict): continue if params.get("type") != "object": continue props = params.get("properties") if not isinstance(props, dict): continue # 补充 intent 属性 if "intent" not in props: params["properties"] = {**intent_field, **props} # 将 intent 加入必填 required_list = params.get("required") if isinstance(required_list, list): if "intent" not in required_list: required_list.insert(0, "intent") params["required"] = required_list else: params["required"] = ["intent"] return tools def _build_custom_tools(self) -> List[Dict]: if not (self.custom_tools_enabled and getattr(self, "user_role", "user") == "admin"): return [] try: definitions = self.custom_tool_registry.reload() except Exception: definitions = self.custom_tool_registry.list_tools() if not definitions: # 更新分类为空列表,避免旧缓存 if "custom" in self.tool_categories_map: self.tool_categories_map["custom"].tools = [] return [] tools: List[Dict] = [] tool_ids: List[str] = [] for item in definitions: tool_id = item.get("id") if not tool_id: continue if item.get("invalid_id"): # 跳过不合法的工具 ID,避免供应商严格校验时报错 continue tool_ids.append(tool_id) params = item.get("parameters") or {"type": "object", "properties": {}} if isinstance(params, dict) and params.get("type") != "object": params = {"type": "object", "properties": {}} required = item.get("required") if isinstance(required, list): params = dict(params) params["required"] = required tools.append({ "type": "function", "function": { "name": tool_id, "description": item.get("description") or f"自定义工具: {tool_id}", "parameters": params } }) # 覆盖 custom 分类的工具列表 if "custom" in self.tool_categories_map: self.tool_categories_map["custom"].tools = tool_ids return tools def _build_mcp_tools(self) -> List[Dict]: if not getattr(self, "mcp_tools_enabled", False): if hasattr(self, "tool_categories_map") and "mcp" in self.tool_categories_map: self.tool_categories_map["mcp"].tools = ["list_mcp_servers"] stale_keys = [ key for key in list(getattr(self, "tool_categories_map", {}).keys()) if isinstance(key, str) and key.startswith("mcp_server__") ] for key in stale_keys: self.tool_categories_map.pop(key, None) self.tool_category_states.pop(key, None) self._refresh_disabled_tools() self.mcp_tool_alias_map = {} return [] if self._is_mcp_disabled_in_docker_mode(): self.mcp_tool_alias_map = {} if "mcp" in self.tool_categories_map: self.tool_categories_map["mcp"].tools = ["list_mcp_servers"] elif getattr(self, "mcp_tools_enabled", False): default_mcp_cat = build_default_mcp_category() self.tool_categories_map["mcp"] = type(next(iter(TOOL_CATEGORIES.values())))( label=default_mcp_cat["label"], tools=["list_mcp_servers"], default_enabled=True, silent_when_disabled=False, ) stale_keys = [ key for key in list(getattr(self, "tool_categories_map", {}).keys()) if isinstance(key, str) and key.startswith("mcp_server__") ] for key in stale_keys: self.tool_categories_map.pop(key, None) self.tool_category_states.pop(key, None) self._refresh_disabled_tools() return [] # 类别层面的禁用优先生效(即使策略里尚未填充具体工具列表) mcp_cat = getattr(self, "tool_categories_map", {}).get("mcp") if mcp_cat is not None: mcp_enabled = self.tool_category_states.get("mcp", getattr(mcp_cat, "default_enabled", True)) forced = getattr(self, "admin_forced_category_states", {}).get("mcp") if isinstance(forced, bool): mcp_enabled = forced if not mcp_enabled: self.mcp_tool_alias_map = {} self.tool_categories_map["mcp"].tools = ["list_mcp_servers"] stale_keys = [ key for key in list(getattr(self, "tool_categories_map", {}).keys()) if isinstance(key, str) and key.startswith("mcp_server__") ] for key in stale_keys: self.tool_categories_map.pop(key, None) self.tool_category_states.pop(key, None) self._refresh_disabled_tools() return [] manager = getattr(self, "mcp_client_manager", None) if manager is None: self.mcp_tool_alias_map = {} return [] try: tool_defs, alias_map = manager.build_openai_tools( ensure_discovery=True, discovery_timeout_seconds=10, ) except Exception as exc: logger.warning("构建 MCP 工具列表失败: %s", exc) tool_defs, alias_map = [], {} # 保存映射,供执行层按 alias 分发到具体 server/tool alias_map = alias_map or {} if "mcp" in self.tool_categories_map: self.tool_categories_map["mcp"].tools = ["list_mcp_servers"] elif getattr(self, "mcp_tools_enabled", False): default_mcp_cat = build_default_mcp_category() self.tool_categories_map["mcp"] = type(next(iter(TOOL_CATEGORIES.values())))( label=default_mcp_cat["label"], tools=["list_mcp_servers"], default_enabled=True, silent_when_disabled=False, ) server_aliases: Dict[str, List[str]] = {} for alias, binding in alias_map.items(): sid = str(getattr(binding, "server_id", "") or "").strip() if not sid: continue server_aliases.setdefault(sid, []).append(alias) enabled_servers = [] try: enabled_servers = list(self.mcp_server_registry.list_enabled_servers()) except Exception: enabled_servers = [] active_server_category_ids = set() for server in enabled_servers: sid = str(server.get("id") or "").strip() if not sid: continue cat_id = MCPServerRegistry.make_server_category_id(sid) active_server_category_ids.add(cat_id) aliases = list(server_aliases.get(sid, [])) existing = self.tool_categories_map.get(cat_id) label = MCPServerRegistry.make_server_category_label(server) if existing is not None: existing.label = label existing.tools = aliases else: self.tool_categories_map[cat_id] = type(next(iter(TOOL_CATEGORIES.values())))( label=label, tools=aliases, default_enabled=True, silent_when_disabled=False, ) if cat_id not in self.tool_category_states: self.tool_category_states[cat_id] = True stale_keys = [ key for key in list(self.tool_categories_map.keys()) if isinstance(key, str) and key.startswith("mcp_server__") and key not in active_server_category_ids ] for key in stale_keys: self.tool_categories_map.pop(key, None) self.tool_category_states.pop(key, None) if hasattr(self, "_prune_runtime_tool_overrides"): self._prune_runtime_tool_overrides() if hasattr(self, "_apply_runtime_tool_overrides"): self._apply_runtime_tool_overrides() filtered_tools: List[Dict[str, Any]] = [] filtered_alias_map: Dict[str, Any] = {} for tool_def in tool_defs: alias = str(((tool_def or {}).get("function") or {}).get("name") or "").strip() if not alias: continue binding = alias_map.get(alias) if binding is None: continue sid = str(getattr(binding, "server_id", "") or "").strip() if not sid: continue cat_id = MCPServerRegistry.make_server_category_id(sid) cat = self.tool_categories_map.get(cat_id) cat_enabled = self.tool_category_states.get( cat_id, getattr(cat, "default_enabled", True) if cat is not None else True, ) forced = getattr(self, "admin_forced_category_states", {}).get(cat_id) if isinstance(forced, bool): cat_enabled = forced if not cat_enabled: continue filtered_tools.append(tool_def) filtered_alias_map[alias] = binding self.mcp_tool_alias_map = filtered_alias_map self._refresh_disabled_tools() return filtered_tools def define_tools(self) -> List[Dict]: """定义可用工具(添加确认工具)""" now = datetime.now() current_time = f"{now.year}年{now.month}月{now.day}日 {now.hour}点(24小时制)" tools = [ { "type": "function", "function": { "name": "sleep", "description": "等待工具。三种模式三选一:1) seconds:短暂延迟;2) wait_sub_agent_ids:等待指定子智能体全部结束并直接返回结果;3) wait_runcommand_id:等待指定后台 run_command 结束并直接返回结果。若同时提供多个等待参数会报错。", "parameters": { "type": "object", "properties": self._inject_intent({ "seconds": { "type": "number", "description": "等待的秒数,可以是小数(如0.2秒)。建议范围:0.1-10秒" }, "wait_sub_agent_ids": { "type": "array", "items": {"type": "integer"}, "description": "等待这些子智能体全部结束后返回(可提供一个或多个编号)。" }, "wait_runcommand_id": { "type": "string", "description": "等待指定后台 run_command 的 command_id 结束后返回。" }, "reason": { "type": "string", "description": "等待的原因说明(可选)" } }), "required": [] } } }, { "type": "function", "function": { "name": "write_file", "description": "将内容写入本地文件系统;append 为 False 时覆盖原文件,True 时追加到末尾。", "parameters": { "type": "object", "properties": self._inject_intent({ "file_path": { "type": "string", "description": "要写入的相对路径" }, "content": { "type": "string", "description": "要写入文件的内容" }, "append": { "type": "boolean", "description": "是否追加到文件而不是覆盖它", "default": False } }), "required": ["file_path", "content"] } } }, { "type": "function", "function": { "name": "read_skill", "description": "按 skill 名称读取 skills//SKILL.md 内容;内部等价于 read_file 的 read 模式,并返回解析后的 path。", "parameters": { "type": "object", "properties": self._inject_intent({ "skill_name": { "type": "string", "description": "skill 名称(支持 skill id 或技能名称)" } }), "required": ["skill_name"] } } }, { "type": "function", "function": { "name": "create_skill", "description": "验证并归档一个已创建好的 skill 文件夹。当你觉得本次工作的流程、经验或踩坑值得沉淀为 skill,便于以后面对类似问题时高效工作,或用户主动要求“把今天/本次经历转化为 skill”时,应先阅读 skill-creator,按规范创建对应 skill 文件夹与 SKILL.md,再调用本工具验证并归档。source_dir 可为当前工作区相对路径、工作区内绝对路径,宿主机模式也支持电脑上其它位置的绝对路径。验证通过后移动到正式 skills 库;不覆盖已存在 skill。", "parameters": { "type": "object", "properties": self._inject_intent({ "source_dir": { "type": "string", "description": "要归档的 skill 文件夹路径;skill 名称使用该文件夹名称。" } }), "required": ["source_dir"] } } }, { "type": "function", "function": { "name": "read_file", "description": "读取/搜索/抽取 UTF-8 文本文件内容。通过 type 参数选择 read(阅读)、search(搜索)、extract(具体行段),支持限制返回字符数。若文件非 UTF-8 或过大,请改用 run_python。", "parameters": { "type": "object", "properties": self._inject_intent({ "path": {"type": "string", "description": "文件路径"}, "type": { "type": "string", "enum": ["read", "search", "extract"], "description": "读取模式:read=阅读、search=搜索、extract=按行抽取" }, "max_chars": { "type": "integer", "description": "返回内容的最大字符数,默认与 config 一致" }, "start_line": { "type": "integer", "description": "[read] 可选的起始行号(1开始)" }, "end_line": { "type": "integer", "description": "[read] 可选的结束行号(>=start_line)" }, "query": { "type": "string", "description": "[search] 搜索关键词" }, "max_matches": { "type": "integer", "description": "[search] 最多返回多少条命中(默认5,最大50)" }, "context_before": { "type": "integer", "description": "[search] 命中行向上追加的行数(默认1,最大3)" }, "context_after": { "type": "integer", "description": "[search] 命中行向下追加的行数(默认1,最大5)" }, "case_sensitive": { "type": "boolean", "description": "[search] 是否区分大小写,默认 false" }, "segments": { "type": "array", "description": "[extract] 需要抽取的行区间", "items": { "type": "object", "properties": { "label": { "type": "string", "description": "该片段的标签(可选)" }, "start_line": { "type": "integer", "description": "起始行号(>=1)" }, "end_line": { "type": "integer", "description": "结束行号(>=start_line)" } }, "required": ["start_line", "end_line"] }, "minItems": 1 } }), "required": ["path", "type"] } } }, { "type": "function", "function": { "name": "edit_file", "description": "在文件中执行精确的字符串替换;建议先使用 read_file 获取最新内容以确保精确匹配。", "parameters": { "type": "object", "properties": self._inject_intent({ "file_path": { "type": "string", "description": "要修改文件的相对路径" }, "old_string": { "type": "string", "description": "要替换的文本(需与文件内容精确匹配,保留缩进;建议提供至少3行提升定位稳定性。需要批量替换的场景可以单行或不足一行)" }, "new_string": { "type": "string", "description": "用于替换的新文本(必须不同于 old_string)" }, "replace_all": { "type": "boolean", "enum": [True, False], "description": "是否替换所有匹配内容(必填)。false=仅替换首个匹配,true=替换全部匹配。" } }), "required": ["file_path", "old_string", "new_string", "replace_all"] } } }, { "type": "function", "function": { "name": "vlm_analyze", "description": "使用大参数视觉语言模型(Qwen3.5)理解图片:文字、物体、布局、表格等,仅支持本地路径。", "parameters": { "type": "object", "properties": self._inject_intent({ "path": {"type": "string", "description": "项目内的图片相对路径"}, "prompt": {"type": "string", "description": "传递给 VLM 的中文提示词,如“请总结这张图的内容”“表格的总金额是多少”“图中是什么车?”。"} }), "required": ["path", "prompt"] } } }, { "type": "function", "function": { "name": "terminal_session", "description": "管理持久化终端会话,可打开、关闭、列出或切换终端。请在授权工作区内执行命令,禁止启动需要完整 TTY 的程序(python REPL、vim、top 等)。", "parameters": { "type": "object", "properties": self._inject_intent({ "action": { "type": "string", "enum": ["open", "close", "list", "reset"], "description": "操作类型:open-打开新终端,close-关闭终端,list-列出所有终端,reset-重置终端" }, "session_name": { "type": "string", "description": "终端会话名称(open、close、reset时需要)" }, "working_dir": { "type": "string", "description": "工作目录,相对于项目路径(open时可选)" } }), "required": ["action"] } } }, { "type": "function", "function": { "name": "terminal_input", "description": "向指定终端发送命令或输入。禁止启动会占用终端界面的程序(python/node/nano/vim 等);如遇卡死请结合 terminal_snapshot 并使用 terminal_session 的 reset 恢复。output_wait 必填:本次收集/等待输出的最大时长(秒,1-300),不会封装命令、不会强杀进程;在窗口内若检测到命令已完成会提前返回,否则到时返回已产生的输出并保持命令继续运行。需要强制超时终止请使用 run_command。\n用法建议:\n1) 短命令多次执行可设 5-10 秒;\n2) 只需确认启动的长任务(下载/clone/pip/启动服务)可设约 30 秒,运行期间可做其他事情,后续用 terminal_snapshot 判断状态/是否完成;\n3) 必须等待结果再继续的任务(大表/文件批量处理、前端构建、批量测试、压缩/解压、数据导出/统计)设足够覆盖实际执行时间;\n4) 可能长时间无输出的任务(遍历海量文件/转码/打包)短窗口不一定有输出,可适当加长或加完成标记;\n5) 长时间无输出且怀疑卡死时,使用 terminal_session 的 reset 重置终端;\n6) 需要仅发送回车时,可传入一个空格字符作为 command(等效按下 Enter)。\n若不确定上一条命令是否结束,先用 terminal_snapshot 确认后再继续输入。", "parameters": { "type": "object", "properties": self._inject_intent({ "command": { "type": "string", "description": "要执行的命令或发送的输入" }, "session_name": { "type": "string", "description": "目标终端会话名称(必填)" }, "output_wait": { "type": "number", "description": "等待/收集输出的最大秒数(1-300,必填);非命令超时,到点即返回已产生输出并保持命令运行" } }), "required": ["command", "output_wait", "session_name"] } } }, { "type": "function", "function": { "name": "terminal_snapshot", "description": "获取指定终端最近的输出快照,用于判断当前状态。默认返回末尾的50行,可通过参数调整。", "parameters": { "type": "object", "properties": self._inject_intent({ "session_name": { "type": "string", "description": "目标终端会话名称(可选,默认活动终端)" }, "lines": { "type": "integer", "description": "返回的最大行数(可选)" }, "max_chars": { "type": "integer", "description": "返回的最大字符数(可选)" } }) } } }, { "type": "function", "function": { "name": "web_search", "description": f"当现有资料不足时搜索外部信息(当前时间 {current_time})。调用前说明目的,精准撰写 query,并合理设置时间/主题参数;避免重复或无意义的搜索。", "parameters": { "type": "object", "properties": self._inject_intent({ "query": { "type": "string", "description": "搜索查询内容(不要包含日期或时间范围)" }, "max_results": { "type": "integer", "description": "最大结果数,可选" }, "topic": { "type": "string", "description": "搜索主题,可选值:general(默认)/news/finance" }, "time_range": { "type": "string", "description": "相对时间范围,可选 day/week/month/year,支持缩写 d/w/m/y;与 days 和 start_date/end_date 互斥" }, "days": { "type": "integer", "description": "最近 N 天,仅当 topic=news 时可用;与 time_range、start_date/end_date 互斥" }, "start_date": { "type": "string", "description": "开始日期,YYYY-MM-DD;必须与 end_date 同时提供,与 time_range、days 互斥" }, "end_date": { "type": "string", "description": "结束日期,YYYY-MM-DD;必须与 start_date 同时提供,与 time_range、days 互斥" }, "country": { "type": "string", "description": "国家过滤,仅 topic=general 可用,使用英文小写国名" }, "include_domains": { "type": "array", "description": "仅包含这些域名(可选,最多300个)", "items": { "type": "string" } } }), "required": ["query"] } } }, { "type": "function", "function": { "name": "extract_webpage", "description": "在 web_search 结果不够详细时提取网页正文。调用前说明用途,注意提取内容会消耗大量 token,超过80000字符将被拒绝。", "parameters": { "type": "object", "properties": self._inject_intent({ "url": {"type": "string", "description": "要提取内容的网页URL"} }), "required": ["url"] } } }, { "type": "function", "function": { "name": "save_webpage", "description": "提取网页内容并保存为纯文本文件,适合需要长期留存的长文档。请提供网址与目标路径(含 .txt 后缀),落地后请通过终端命令查看。", "parameters": { "type": "object", "properties": self._inject_intent({ "url": {"type": "string", "description": "要保存的网页URL"}, "target_path": {"type": "string", "description": "保存位置,包含文件名,相对于项目根目录"} }), "required": ["url", "target_path"] } } }, { "type": "function", "function": { "name": "list_mcp_servers", "description": "列出当前已配置的 MCP 服务与工具映射(mcp__... 别名)。可选刷新远程 tools/list 缓存后再返回。", "parameters": { "type": "object", "properties": self._inject_intent({ "refresh": { "type": "boolean", "description": "是否先同步 MCP 服务工具缓存(默认 false)" }, "server_id": { "type": "string", "description": "仅查看指定 MCP 服务(可选)" }, "include_disabled": { "type": "boolean", "description": "是否包含已禁用服务(默认 false)" } }), "required": [] } } }, { "type": "function", "function": { "name": "run_python", "description": "执行一次性 Python 脚本,可用于处理二进制或非 UTF-8 文件(如 Excel、Word、PDF、图片),或进行数据分析与验证。必须提供 timeout(最长60秒);一旦超时,脚本会被打断且无法继续执行(需要重新运行),并返回已捕获输出。", "parameters": { "type": "object", "properties": self._inject_intent({ "code": {"type": "string", "description": "Python代码"}, "timeout": { "type": "number", "description": "超时时长(秒),必填,最大60" } }), "required": ["code", "timeout"] } } }, { "type": "function", "function": { "name": "run_command", "description": "执行一次性终端命令,适合查看文件信息(file/ls/stat/iconv 等)、转换编码或调用 CLI 工具。禁止启动交互式程序。必须提供 timeout。前台模式(run_in_background=false,默认)上限120秒,超时会打断;后台模式(run_in_background=true)上限3600秒,会先等待5秒返回已有输出并继续在后台运行,完成后由系统通知。", "parameters": { "type": "object", "properties": self._inject_intent({ "command": {"type": "string", "description": "终端命令"}, "timeout": { "type": "number", "description": "超时时长(秒),必填。前台最大120;后台最大3600。" }, "run_in_background": { "type": "boolean", "description": "是否后台运行。true 时先等待5秒返回已有输出,并在后台持续执行直至结束。" } }), "required": ["command", "timeout"] } } }, { "type": "function", "function": { "name": "update_memory", "description": "按条目更新记忆列表(自动编号)。append 追加新条目;replace 用序号替换;delete 用序号删除。应积极记录长期偏好、项目事实、已验证经验、用户明确要求记住的内容;不要记录密钥、隐私或未确认猜测。", "parameters": { "type": "object", "properties": self._inject_intent({ "content": {"type": "string", "description": "条目内容。append/replace 时必填"}, "operation": {"type": "string", "enum": ["append", "replace", "delete"], "description": "操作类型"}, "index": {"type": "integer", "description": "要替换/删除的序号(从1开始)"} }), "required": ["operation"] } } }, { "type": "function", "function": { "name": "conversation_search", "description": "在当前工作区的历史对话中搜索或列出对话,返回对话标题与 id。最多提供 3 个关键词,命中任一关键词即可;如果不提供关键词,则按时间列出最近的历史对话。可提供日期范围和最大返回数量。不能跨工作区搜索,也不会搜索当前对话。", "parameters": { "type": "object", "properties": self._inject_intent({ "keywords": { "type": "array", "description": "搜索关键词列表,最多3个;仅搜索标题和首条用户消息。可为空。", "items": {"type": "string"}, "maxItems": 3 }, "query": {"type": "string", "description": "兼容字段:单个搜索关键词,可为空;优先使用 keywords"}, "start_date": {"type": "string", "description": "开始日期 YYYY-MM-DD,可选"}, "end_date": {"type": "string", "description": "结束日期 YYYY-MM-DD,可选"}, "limit": {"type": "integer", "description": "最大返回数量,默认10,最大100"} }), "required": [] } } }, { "type": "function", "function": { "name": "conversation_review", "description": "按 id 回顾当前工作区内的历史对话。mode=read 时直接返回回顾内容;若内容超过 50000 字符,将自动保存到 .agents/review/ 并提示分段或查找阅读。mode=save 时保存 Markdown 文件到 .agents/review/ 并返回路径。若 id 不属于当前工作区,将返回不存在或不属于当前工作区。", "parameters": { "type": "object", "properties": self._inject_intent({ "conversation_id": {"type": "string", "description": "要回顾的对话 id,例如 conv_20250924_210942_114"}, "mode": { "type": "string", "enum": ["read", "save"], "description": "必要参数。read=直接返回回顾内容;save=保存为 .agents/review/ 下的 Markdown 文件并返回路径。" } }), "required": ["conversation_id", "mode"] } } }, { "type": "function", "function": { "name": "todo_create", "description": "创建待办列表,最多 8 条任务;若已有列表将被覆盖。当用户提出稍微复杂的要求、预计需要超过 3 个步骤时,应积极创建待办事项。", "parameters": { "type": "object", "properties": self._inject_intent({ "overview": {"type": "string", "description": "一句话概述待办清单要完成的目标,50 字以内。"}, "tasks": { "type": "array", "description": "任务列表,1~8 条,每条写清“动词+对象+目标”。", "items": { "type": "object", "properties": { "title": {"type": "string", "description": "单个任务描述,写成可执行的步骤"} }, "required": ["title"] }, "minItems": 1, "maxItems": 8 } }), "required": ["overview", "tasks"] } } }, { "type": "function", "function": { "name": "todo_update_task", "description": "批量勾选或取消任务(支持单个或多个任务);全部勾选时提示所有任务已完成。", "parameters": { "type": "object", "properties": self._inject_intent({ "task_index": {"type": "integer", "description": "任务序号(1-8),兼容旧参数"}, "task_indices": { "type": "array", "items": {"type": "integer"}, "minItems": 1, "maxItems": 8, "description": "要更新的任务序号列表(1-8),可一次勾选多个" }, "completed": {"type": "boolean", "description": "true=打勾,false=取消"} }), "required": ["completed"] } } }, { "type": "function", "function": { "name": "close_sub_agent", "description": "强制关闭指定子智能体,适用于长时间无响应、超时或卡死的任务。使用前请确认必要的日志/文件已保留,操作会立即终止该任务。", "parameters": { "type": "object", "properties": self._inject_intent({ "task_id": {"type": "string", "description": "子智能体任务ID"}, "agent_id": {"type": "integer", "description": "子智能体编号(1~5),若缺少 task_id 可用"} }) } } }, { "type": "function", "function": { "name": "create_sub_agent", "description": "创建一个子智能体来处理独立任务。子智能体拥有完整的工具能力(读写文件、执行命令、搜索网页等),与主智能体共享工作区。\n\n适用场景:\n1. 可以独立完成的任务(生成文档、代码分析、测试执行)\n2. 需要大量工具调用的任务(批量文件处理、数据收集)\n3. 可以并行处理的子任务(多模块开发、多方面分析)\n4. 刚开始修改一个项目、需要先找到某个功能模块的修改位置时,可优先创建子智能体快速了解代码定位与相关上下文\n\n何时使用后台运行:\n- 任务耗时较长(预计超过5分钟)\n- 你可以继续处理其他工作,不需要立即使用结果\n- 多个独立任务可以并行执行\n\n何时使用阻塞运行(默认):\n- 任务较快(几分钟内完成)\n- 后续工作依赖子智能体的结果\n- 需要立即查看和使用输出\n\n重要限制:\n- 最多同时运行 5 个子智能体\n- 禁止多个子智能体操作相同的文件或目录(会导致冲突)\n- 禁止子智能体间的工作有重叠(如同时修改同一模块、同时测试同一功能)\n- 每个子智能体应该有明确独立的职责范围", "parameters": { "type": "object", "properties": self._inject_intent({ "agent_id": { "type": "integer", "description": "子智能体编号(1-99),用于标识和管理。同一对话中每个编号只能使用一次。建议按顺序分配:1、2、3..." }, "summary": { "type": "string", "description": "任务简短摘要(10-30字),用于显示和跟踪。例如:'生成API文档'、'分析性能瓶颈'、'编写单元测试'。" }, "task": { "type": "string", "description": "详细的任务描述,必须包括:\n1. 任务目标:要完成什么\n2. 具体要求:如何完成、注意事项\n3. 交付内容:在交付目录生成哪些文件\n4. 工作范围:明确指定操作的文件/目录范围,避免与其他子智能体冲突\n\n示例:'分析 src/api/ 目录下的所有 Python 文件,检查代码质量问题(复杂度、重复代码、潜在bug),生成分析报告 analysis.md 到交付目录。'" }, "deliverables_dir": { "type": "string", "description": "交付文件夹的相对路径(相对于项目根目录)。子智能体会将所有结果文件放在此目录。\n\n要求:必须是不存在的新目录;若目录不存在会自动创建;若目录已存在(无论是否为空)将报错。\n\n留空则使用默认路径:sub_agent_results/agent_{agent_id}\n\n示例:'docs/api'、'reports/performance'、'tests/generated'" }, "run_in_background": { "type": "boolean", "description": "是否后台运行。\n\ntrue(后台):立即返回,子智能体在后台执行,完成后会通知你。适合耗时任务或可以并行处理的任务。\n\nfalse(阻塞,默认):等待子智能体完成后返回结果。适合快速任务或后续工作依赖结果的情况。" }, "timeout_seconds": { "type": "integer", "description": "超时时间(秒),范围 60-3600。超时后子智能体会被强制终止,已生成的部分结果会保留。默认 600 秒(10分钟)。" }, "thinking_mode": { "type": "string", "enum": ["fast", "thinking"], "description": "子智能体思考模式,根据任务复杂度选择:\n\nfast(快速模式)- 适合简单明确的任务:\n- 网络信息搜集和整理\n- 批量文件读取和简单处理\n- 执行已知的命令序列\n- 生成简单的文档或报告\n- 数据格式转换\n\nthinking(思考模式)- 适合复杂任务:\n- 代码架构分析和重构设计\n- 复杂算法实现和优化\n- 多步骤问题诊断和调试\n- 技术方案选型和对比\n- 需要深度推理的代码审查\n\n不填则使用默认模式。" } }), "required": ["agent_id", "summary", "task", "deliverables_dir", "thinking_mode"] } } }, { "type": "function", "function": { "name": "terminate_sub_agent", "description": "强制终止正在运行的子智能体。用于:\n1. 任务不再需要\n2. 子智能体陷入死循环或执行错误\n3. 用户要求停止\n\n终止后无法恢复,但已生成的部分结果会保留在交付目录。", "parameters": { "type": "object", "properties": self._inject_intent({ "agent_id": { "type": "integer", "description": "要终止的子智能体编号。" } }), "required": ["agent_id"] } } }, { "type": "function", "function": { "name": "get_sub_agent_status", "description": "查询一个或多个子智能体的当前状态和工作进度。用于检查后台运行的子智能体是否完成、当前在做什么、使用了哪些工具。", "parameters": { "type": "object", "properties": self._inject_intent({ "agent_ids": { "type": "array", "items": {"type": "integer"}, "description": "要查询的子智能体编号列表。必须指定至少一个编号。例如:[1] 或 [1, 2, 3]。" } }), "required": ["agent_ids"] } } }, { "type": "function", "function": { "name": "trigger_easter_egg", "description": "触发隐藏彩蛋,用于展示非功能性特效。需指定 effect 参数,例如 flood(灌水)或 snake(贪吃蛇)。", "parameters": { "type": "object", "properties": self._inject_intent({ "effect": { "type": "string", "description": "彩蛋标识,目前支持 flood(灌水)与 snake(贪吃蛇)。" } }), "required": ["effect"] } } }, { "type": "function", "function": { "name": "manage_personalization", "description": "管理用户个性化设置。支持读取所有配置,或更新单个字段。可修改字段:self_identify(AI自称,最多20字)、user_name(AI如何称呼用户,最多20字)、profession(用户职业,最多20字)、tone(交流语气,最多20字)、considerations(注意事项列表,字符串数组,最多10项每项最多50字)、theme(主题配色:classic-经典/light-明亮/dark-暗黑)、communication_style(交流风格:default-标准AI风格/human_like-拟人聊天风格)。更新时会自动验证格式,验证通过后立即保存并生效,新主题会立即应用到界面。", "parameters": { "type": "object", "properties": self._inject_intent({ "action": { "type": "string", "enum": ["read", "update"], "description": "操作类型:read读取所有配置,update更新单个字段" }, "field": { "type": "string", "enum": ["self_identify", "user_name", "profession", "tone", "considerations", "theme", "communication_style"], "description": "要更新的字段名(仅action=update时需要)" }, "value": { "description": "新值(仅action=update时需要)。注意事项需提供字符串数组,其他字段提供字符串" } }), "required": ["action"] } } } ] # 多模态模型自带能力,不再暴露 vlm_analyze,改为 view_image / view_video model_key = getattr(self, "model_key", None) if model_key and (model_supports_image(model_key) or model_supports_video(model_key)): tools = [ tool for tool in tools if (tool.get("function") or {}).get("name") != "vlm_analyze" ] if model_supports_image(model_key): tools.append({ "type": "function", "function": { "name": "view_image", "description": "将指定本地图片附加到工具结果中(tool 消息携带 image_url),便于模型主动查看图片内容。", "parameters": { "type": "object", "properties": self._inject_intent({ "path": { "type": "string", "description": "项目内的图片相对路径(不要以 /workspace 开头);宿主机模式可用绝对路径。支持 png/jpg/webp/gif/bmp/svg。" } }), "required": ["path"] } } }) if model_supports_video(model_key): tools.append({ "type": "function", "function": { "name": "view_video", "description": "将指定本地视频附加到工具结果中(tool 消息携带 video_url),便于模型查看视频内容。", "parameters": { "type": "object", "properties": self._inject_intent({ "path": { "type": "string", "description": "项目内的视频相对路径(不要以 /workspace 开头);宿主机模式可用绝对路径。支持 mp4/mov/mkv/avi/webm。" } }), "required": ["path"] } } }) # 附加自定义工具(仅管理员可见) custom_tools = self._build_custom_tools() if custom_tools: tools.extend(custom_tools) # 附加 MCP 工具(由管理员统一配置) mcp_tools = self._build_mcp_tools() if mcp_tools: tools.extend(mcp_tools) if self.disabled_tools: tools = [ tool for tool in tools if tool.get("function", {}).get("name") not in self.disabled_tools ] # 调试日志:记录工具列表 tool_names = [t.get("function", {}).get("name") for t in tools] logger.info("[define_tools] 可用工具列表: %s", tool_names) print(f"[DEBUG] define_tools: 工具数量={len(tools)}") # 检查personalization分类状态 if hasattr(self, 'tool_categories_map') and 'personalization' in self.tool_categories_map: cat = self.tool_categories_map['personalization'] state = self.tool_category_states.get('personalization', cat.default_enabled) print(f"[DEBUG] personalization分类: default_enabled={cat.default_enabled}, current_state={state}") else: print(f"[DEBUG] personalization分类不在tool_categories_map中") if "manage_personalization" in tool_names: logger.info("[define_tools] manage_personalization 工具已启用") print("[DEBUG] manage_personalization 在可用工具列表中") else: logger.warning("[define_tools] manage_personalization 工具未找到!disabled_tools=%s", self.disabled_tools) print(f"[DEBUG] manage_personalization 不在可用工具列表中!disabled_tools={self.disabled_tools}") return self._apply_intent_to_tools(tools)