agent-Specialization/core/main_terminal_parts/tools_definition.py
JOJO b7fe5f2b02 feat(memory): improve workspace memory and skill workflows
Implement workspace-scoped conversation storage with legacy migration based on host workspace metadata, active host workspace state protection, and frontend/backend conversation list scoping.

Add proactive recent-conversation prompt support with a configurable personal-space limit, frozen per conversation metadata, empty-conversation filtering, and a dedicated prompt template.

Add conversation_search and conversation_review tools with readable formatted results, workspace-only access, current-conversation exclusion, multi-keyword search, list mode, message/tool counts, read/save review modes, long-review fallback to .agents/review, and frontend detail renderers.

Move internal workspace artifacts under .agents, update upload/skills/compact/review paths, show .agents internal directories in prompt file trees, and adjust versioning ignores.

Add create_skill and a Skills tool category, move read_skill into that category, validate and archive skill folders, support host global skills and Docker/API private skills, and sync private skills into .agents/skills and prompts.

Update memory stamping with conversation/time source metadata and include frontend icon/status/display updates plus tests for workspace conversation storage and skills management.
2026-05-27 03:04:43 +08:00

1116 lines
68 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
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/<name>/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_identifyAI自称最多20字、user_nameAI如何称呼用户最多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)