Files
metona-ollama-desktop/src/renderer/services/tool-registry.ts
T
紫影233 7c78a65a9c fix: 全面修复内置工具问题 + 架构缺陷修复 (v0.14.8)
P0: replace_in_files glob重写, search_files正则修复, list_directory递归分页修复, git stash/tag参数修复, buildSearchResponse query字段修复; P1: compress命令注入修复, run_command输出限制, download_file UA+重试; P2: web_fetch extract_mode/mobile_ua生效, read_multiple_files默认值对齐, CONFIRM_TOOLS扩展, 工具图标/名称映射补全; P3: random死代码清理, IPC类型补全; 架构: 系统提示词重复渲染修复, 版本号动态注入, 上下文余量字段修复, 工具记录丢失修复
2026-07-07 11:44:48 +08:00

1059 lines
48 KiB
TypeScript
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.
/**
* Tool Registry - 工具注册与调度中心
* 管理所有可用工具的定义,负责执行调度
*/
import type { ToolDefinition, ToolResult } from '../types.js';
import { state, KEYS } from '../state/state.js';
import { logToolStart, logToolResult, logError, logInfo, logWarn } from './log-service.js';
import { getMCPToolDefinitions } from './mcp-client.js';
export const TOOL_DEFINITIONS: ToolDefinition[] = [
{
type: 'function',
function: {
name: 'read_file',
description: 'Read a file from the local filesystem. Supports text mode (utf-8/latin1, line-based pagination) and binary mode (base64, byte-based pagination). Files up to 5MB (text) or 50MB (binary). When truncated, returns remaining_lines/remaining_bytes and a hint to continue reading. Use start_line/end_line for text files, offset_bytes/limit_bytes for binary files or raw byte access.',
parameters: {
type: 'object',
required: ['path'],
properties: {
path: { type: 'string', description: 'The file path to read. Absolute or relative to workspace.' },
encoding: { type: 'string', enum: ['utf-8', 'latin1', 'base64'], description: 'File encoding. Default: utf-8. Use base64 for binary content.' },
start_line: { type: 'integer', description: 'Start line (1-indexed) for text files. Use with end_line for pagination.' },
end_line: { type: 'integer', description: 'End line (inclusive). Default: start_line + 2000.' },
mode: { type: 'string', enum: ['text', 'binary'], description: 'Read mode. text = decode as string, binary = return base64. Default: text.' },
offset_bytes: { type: 'integer', description: 'Byte offset to start reading from (0-indexed). Use for binary files or large text.' },
limit_bytes: { type: 'integer', description: 'Max bytes to read. Default: 50000 (text) or 102400 (binary).' }
}
}
}
},
{
type: 'function',
function: {
name: 'write_file',
description: 'Write content to a local file. Creates parent directories automatically. The "path" parameter is REQUIRED — always specify a file path (relative to workspace or absolute). For example: path="output.txt" writes to workspace. Supports text (utf-8/latin1) and binary (base64). Use mode="append" to add to existing file. Max 10MB overwrite / 5MB append.',
parameters: {
type: 'object',
required: ['path', 'content'],
properties: {
path: { type: 'string', description: 'REQUIRED — File path. Relative paths use workspace as base. E.g. "output.txt", "docs/report.md", or absolute "/home/user/file.txt".' },
content: { type: 'string', description: 'The content to write. For binary files, pass base64-encoded string with encoding="base64".' },
encoding: { type: 'string', enum: ['utf-8', 'latin1', 'base64'], description: 'File encoding. Use "base64" to write binary content (images, PDFs, etc). Default: utf-8.' },
mode: { type: 'string', enum: ['overwrite', 'append'], description: 'Write mode. overwrite = replace file, append = add to end. Default: overwrite.' }
}
}
}
},
{
type: 'function',
function: {
name: 'list_directory',
description: 'List directory contents up to 2000 entries. Returns names, types, sizes, modification times. Supports offset pagination, recursion, and extension filtering.',
parameters: {
type: 'object',
required: ['path'],
properties: {
path: { type: 'string', description: 'Directory path.' },
recursive: { type: 'boolean', description: 'List recursively. Default: false.' },
max_depth: { type: 'integer', description: 'Max recursion depth. Default: 3.' },
include_hidden: { type: 'boolean', description: 'Include hidden files. Default: false.' },
offset: { type: 'integer', description: 'Skip first N entries for pagination. Default: 0.' }
}
}
}
},
{
type: 'function',
function: {
name: 'search_files',
description: 'Search files by name pattern or text content within files. Supports regular expressions with use_regex=true.',
parameters: {
type: 'object',
required: ['path', 'query'],
properties: {
path: { type: 'string', description: 'Root directory to search.' },
query: { type: 'string', description: 'Search query (glob, text, or regex).' },
search_type: { type: 'string', enum: ['filename', 'content', 'both'], description: 'Search target. Default: both.' },
case_sensitive: { type: 'boolean', description: 'Case sensitive. Default: false.' },
max_results: { type: 'integer', description: 'Max results. Default: 50.' },
file_extensions: { type: 'array', items: { type: 'string' }, description: 'Filter extensions, e.g. [".ts", ".js"]' },
use_regex: { type: 'boolean', description: 'Treat query as regular expression. Default: false.' }
}
}
}
},
{
type: 'function',
function: {
name: 'create_directory',
description: 'Create a new directory. Creates parents automatically.',
parameters: {
type: 'object',
required: ['path'],
properties: {
path: { type: 'string', description: 'Directory path to create.' }
}
}
}
},
{
type: 'function',
function: {
name: 'delete_file',
description: 'Delete a file or directory. Returns deleted item count and total size. Use recursive=true for non-empty directories.',
parameters: {
type: 'object',
required: ['path'],
properties: {
path: { type: 'string', description: 'Path to delete.' },
recursive: { type: 'boolean', description: 'Recursive delete for directories. Returns filesDeleted count and total deletedSize.' }
}
}
}
},
{
type: 'function',
function: {
name: 'run_command',
description: 'Execute a shell command via workspace. No timeout limit — runs until the process exits. Requires user confirmation. Output is streamed in real-time through the workspace process.',
parameters: {
type: 'object',
required: ['command'],
properties: {
command: { type: 'string', description: 'Shell command to execute.' },
cwd: { type: 'string', description: 'Working directory. Defaults to workspace directory.' }
}
}
}
},
{
type: 'function',
function: {
name: 'move_file',
description: 'Move or rename a file/directory. Requires user confirmation.',
parameters: {
type: 'object',
required: ['source', 'destination'],
properties: {
source: { type: 'string', description: 'Source path.' },
destination: { type: 'string', description: 'Destination path.' }
}
}
}
},
{
type: 'function',
function: {
name: 'copy_file',
description: 'Copy a file or directory to a new location. Requires user confirmation.',
parameters: {
type: 'object',
required: ['source', 'destination'],
properties: {
source: { type: 'string', description: 'Source path.' },
destination: { type: 'string', description: 'Destination path.' },
recursive: { type: 'boolean', description: 'Copy directories recursively. Default: true.' }
}
}
}
},
{
type: 'function',
function: {
name: 'web_fetch',
description: 'Fetch and extract full text content from a URL. Typically used after web_search: pick the most relevant result URLs from search and call web_fetch on each to get detailed content before answering. Automatically retries on failure (up to 2 times with exponential backoff). Auto-upgrades to browser rendering for JavaScript-heavy pages.',
parameters: {
type: 'object',
required: ['url'],
properties: {
url: { type: 'string', description: 'URL to fetch (http/https).' },
max_chars: { type: 'integer', description: 'Max characters to return. Default: 0 (no limit, return full content).' },
extract_mode: { type: 'string', enum: ['text', 'markdown'], description: 'Extraction mode. Default: text.' },
mobile_ua: { type: 'boolean', description: 'Use mobile User-Agent. Some sites return simpler content for mobile. Default: false.' },
retry: { type: 'boolean', description: 'Enable auto-retry on failure (5xx/network errors, up to 2 times). Default: true.' }
}
}
}
},
{
type: 'function',
function: {
name: 'edit_file',
description: 'Find and replace text in a file. Supports literal string matching and regex (use_regex=true). Use all=true to replace all occurrences. More efficient than write_file for small edits.',
parameters: {
type: 'object',
required: ['path', 'old_text', 'new_text'],
properties: {
path: { type: 'string', description: 'File path to edit.' },
old_text: { type: 'string', description: 'Text to find. Can be a regex pattern when use_regex=true.' },
new_text: { type: 'string', description: 'Replacement text.' },
all: { type: 'boolean', description: 'Replace all occurrences. Default: false.' },
use_regex: { type: 'boolean', description: 'Treat old_text as a regular expression. Default: false.' }
}
}
}
},
{
type: 'function',
function: {
name: 'get_file_info',
description: 'Get detailed file or directory information: size, dates, permissions, type.',
parameters: {
type: 'object',
required: ['path'],
properties: {
path: { type: 'string', description: 'File or directory path.' }
}
}
}
},
{
type: 'function',
function: {
name: 'tree',
description: 'Display directory structure as a tree. Shows nested files and folders. Default depth: 5 levels.',
parameters: {
type: 'object',
required: ['path'],
properties: {
path: { type: 'string', description: 'Root directory path.' },
max_depth: { type: 'integer', description: 'Max depth. Default: 3.' },
include_hidden: { type: 'boolean', description: 'Include hidden files. Default: false.' }
}
}
}
},
{
type: 'function',
function: {
name: 'download_file',
description: 'Download a file from a URL to a local path. Requires user confirmation.',
parameters: {
type: 'object',
required: ['url', 'destination'],
properties: {
url: { type: 'string', description: 'URL to download (http/https).' },
destination: { type: 'string', description: 'Local path to save the file.' }
}
}
}
},
{
type: 'function',
function: {
name: 'diff_files',
description: 'Compare two files and show differences. Returns a unified diff.',
parameters: {
type: 'object',
required: ['file1', 'file2'],
properties: {
file1: { type: 'string', description: 'First file path.' },
file2: { type: 'string', description: 'Second file path.' },
context_lines: { type: 'integer', description: 'Context lines around changes. Default: 3.' }
}
}
}
},
{
type: 'function',
function: {
name: 'replace_in_files',
description: 'Find and replace text across multiple files matching a glob pattern. Requires user confirmation.',
parameters: {
type: 'object',
required: ['path', 'glob', 'old_text', 'new_text'],
properties: {
path: { type: 'string', description: 'Root directory.' },
glob: { type: 'string', description: 'File pattern, e.g. "*.ts", "**/*.js".' },
old_text: { type: 'string', description: 'Text to find.' },
new_text: { type: 'string', description: 'Replacement text.' }
}
}
}
},
{
type: 'function',
function: {
name: 'read_multiple_files',
description: 'Read up to 50 files at once in parallel. Returns content of all requested files (10KB each by default).',
parameters: {
type: 'object',
required: ['paths'],
properties: {
paths: { type: 'array', items: { type: 'string' }, description: 'Array of file paths to read.' },
max_chars_per_file: { type: 'integer', description: 'Max chars per file. Default: 10000.' }
}
}
}
},
{
type: 'function',
function: {
name: 'git',
description: 'Git operations: status, log, diff, add, commit, push, pull, branch, checkout, merge, stash, remote, clone, init. All operations are automatic (no confirmation).',
parameters: {
type: 'object',
required: ['action'],
properties: {
action: { type: 'string', enum: ['status', 'log', 'diff', 'add', 'commit', 'push', 'pull', 'branch', 'checkout', 'merge', 'stash', 'remote', 'clone', 'init', 'reset', 'tag'], description: 'Git action to perform.' },
path: { type: 'string', description: 'Repository path. Defaults to workspace.' },
files: { type: 'array', items: { type: 'string' }, description: 'Files for add/diff/checkout.' },
message: { type: 'string', description: 'Commit message for commit action.' },
branch: { type: 'string', description: 'Branch name for branch/checkout/merge.' },
tag_name: { type: 'string', description: 'Tag name for tag action (creates an annotated tag if message is provided).' },
stash_sub: { type: 'string', enum: ['push', 'pop', 'apply', 'list', 'drop'], description: 'Stash subcommand. Default: push.' },
remote: { type: 'string', description: 'Remote name for push/pull/remote.' },
remote_url: { type: 'string', description: 'Remote URL for remote add.' },
count: { type: 'integer', description: 'Number of log entries. Default: 20.' },
all: { type: 'boolean', description: 'Show all (branches, stashes, etc). Default: false.' },
staged: { type: 'boolean', description: 'Show staged changes in diff. Default: false.' },
new_branch: { type: 'boolean', description: 'Create new branch in branch action. Default: false.' },
delete_branch: { type: 'boolean', description: 'Delete branch in branch action. Default: false.' },
force: { type: 'boolean', description: 'Force push/checkout. Default: false.' },
url: { type: 'string', description: 'Repository URL for clone.' }
}
}
}
},
{
type: 'function',
function: {
name: 'compress',
description: 'Create or extract archives (zip/tar.gz). Requires user confirmation for create.',
parameters: {
type: 'object',
required: ['action', 'path'],
properties: {
action: { type: 'string', enum: ['create', 'extract'], description: 'Create or extract archive.' },
path: { type: 'string', description: 'Source path (create) or archive path (extract).' },
destination: { type: 'string', description: 'Output archive path (create) or extract dir (extract).' },
format: { type: 'string', enum: ['zip', 'tar.gz'], description: 'Archive format. Default: tar.gz.' }
}
}
}
},
{
type: 'function',
function: {
name: 'web_search',
description: 'Search the web with auto-fetch. Always use this when the user asks for information, news, facts, or current data. Never rely on training data or past conversation results — call this tool every time the user asks to search. Set fetch_top to auto-fetch full page content from the top N results.',
parameters: {
type: 'object',
required: ['query', 'fetch_top'],
properties: {
query: { type: 'string', description: 'The search query. Be specific and concise for best results.' },
max_results: { type: 'integer', description: 'Maximum number of results to return. Default: 30, max: 30.' },
time_range: { type: 'string', enum: ['day', 'week', 'month', 'year'], description: 'Filter results by time. Supported by Bing and Google. Use for recent news, latest version, etc.' },
enhance_snippets: { type: 'boolean', description: 'Auto-fetch detailed snippet for results with too-short descriptions. Default: true.' },
fetch_top: { type: 'integer', description: 'Number of top results to auto-fetch full content for. Min: 3. Results shorter than this are useless — snippets lack detail.' }
}
}
}
},
// ══════════════════════════════════════════════
// 记忆工具(统一入口,读写工作空间 MEMORY.md)
// ══════════════════════════════════════════════
{
type: 'function',
function: {
name: 'memory',
description: `Manage agent memories stored in MEMORY.md. Usage by action:
- search: {"action":"search","query":"keywords","limit":8}
- add: {"action":"add","type":"fact","content":"the memory content","importance":8,"tags":["tag1","tag2"]} ← type and content are REQUIRED for add!
- replace: {"action":"replace","old_text":"unique substring","new_content":"replacement"}
- remove: {"action":"remove","old_text":"unique substring"}
- read_all: {"action":"read_all"}
CRITICAL: For add action, you MUST include both "type" (fact/preference/rule) and "content" fields. If you omit them, the call will fail.`,
parameters: {
type: 'object',
required: ['action'],
properties: {
action: { type: 'string', enum: ['search', 'add', 'replace', 'remove', 'read_all'], description: 'Which operation to perform.' },
query: { type: 'string', description: '[search] Keywords to search for.' },
limit: { type: 'integer', description: '[search] Max results. Default: 8.' },
type: { type: 'string', enum: ['fact', 'preference', 'rule'], description: '[add REQUIRED] Memory type. fact=user info, preference=user preference, rule=behavior rule.' },
content: { type: 'string', description: '[add REQUIRED, replace] Memory text. Keep it concise (8-100 chars).' },
importance: { type: 'integer', description: '[add] Priority 1-10. Default: 5. Use 8+ for critical info.' },
tags: { type: 'array', items: { type: 'string' }, description: '[add] 2-5 keywords for search matching.' },
old_text: { type: 'string', description: '[replace, remove REQUIRED] Unique substring to identify the entry.' },
new_content: { type: 'string', description: '[replace REQUIRED] New text to replace with.' }
}
}
}
},
// ══════════════════════════════════════════════
// 会话管理工具
// ══════════════════════════════════════════════
{
type: 'function',
function: {
name: 'session_list',
description: 'List previous chat sessions with their titles and timestamps. Useful for referencing past conversations.',
parameters: {
type: 'object',
properties: {
limit: { type: 'integer', description: 'Max sessions to return. Default: 20.' },
search: { type: 'string', description: 'Filter sessions by title keyword.' }
}
}
}
},
{
type: 'function',
function: {
name: 'session_read',
description: 'Read the messages from a previous chat session. Use when the user references something from a past conversation.',
parameters: {
type: 'object',
required: ['session_id'],
properties: {
session_id: { type: 'string', description: 'The session ID to read.' },
max_messages: { type: 'integer', description: 'Max messages to return. Default: 50.' }
}
}
}
},
// ══════════════════════════════════════════════
// v4.3 新增工具:子代理委派
// ══════════════════════════════════════════════
{
type: 'function',
function: {
name: 'spawn_task',
description: 'Spawn a sub-agent to independently execute a task using read-only tools (file reading, web search, browser viewing, memory/session queries). Use this to parallelize independent research or analysis sub-tasks. The model is configured in Settings and cannot be overridden per call.',
parameters: {
type: 'object',
required: ['task'],
properties: {
task: { type: 'string', description: 'The task description for the sub-agent to execute.' },
context: { type: 'string', description: 'Optional additional context or reference data for the sub-agent.' }
}
}
}
},
// ══════════════════════════════════════════════
// 新增工具:Plan Mode 执行追踪
// ══════════════════════════════════════════════
{
type: 'function',
function: {
name: 'plan_track',
description: 'Manage Plan Mode execution progress. Use this to mark plan steps as completed so you always know what remains. REQUIRED in Plan Mode — call plan_track after completing each step.',
parameters: {
type: 'object',
required: ['action'],
properties: {
action: { type: 'string', enum: ['status', 'mark_done', 'mark_all_done'], description: 'status=查看当前进度, mark_done=标记步骤完成, mark_all_done=全部完成' },
step_index: { type: 'integer', description: 'Step number (1-indexed) to mark as done. Required for mark_done.' },
step_label: { type: 'string', description: 'Optional: description of what was completed, for logging.' }
}
}
}
},
// ══════════════════════════════════════════════
// v5.0 新增工具:浏览器控制
// ══════════════════════════════════════════════
// v5.1 Browser 控制工具(增强版)
// ══════════════════════════════════════════════
{
type: 'function',
function: {
name: 'browser_open',
description: 'Open a URL in the agent browser. Waits for page load. Use wait_selector to wait for a specific element (useful for SPA pages). Returns page title and URL.',
parameters: {
type: 'object',
required: ['url'],
properties: {
url: { type: 'string', description: 'The URL to open (http/https).' },
wait_selector: { type: 'string', description: 'CSS selector to wait for after page load. Useful for SPA/dynamic pages. Default: none.' }
}
}
}
},
{
type: 'function',
function: {
name: 'browser_screenshot',
description: 'Take a screenshot of the current page. Supports viewport (default), full_page (entire scrollable page), or selector (specific element only). Returns base64 PNG.',
parameters: {
type: 'object',
properties: {
full_page: { type: 'boolean', description: 'Capture the entire scrollable page. Default: false.' },
selector: { type: 'string', description: 'CSS selector of element to capture. Overrides full_page.' }
}
}
}
},
{
type: 'function',
function: {
name: 'browser_evaluate',
description: 'Execute JavaScript in the browser page. Returns the result as JSON string. Use to read DOM, extract data, or interact programmatically.',
parameters: {
type: 'object',
required: ['js'],
properties: {
js: { type: 'string', description: 'JavaScript code to execute in the page context.' }
}
}
}
},
{
type: 'function',
function: {
name: 'browser_extract',
description: 'Extract text and links from the current page. Use selector to extract only from a specific element (e.g. "main", "#content", "article"). Without selector, extracts full page. Returns title, text, and up to 50 links.',
parameters: {
type: 'object',
properties: {
selector: { type: 'string', description: 'CSS selector to extract from. Default: entire body.' },
max_chars: { type: 'integer', description: 'Max characters to return. Default: 15000.' }
}
}
}
},
{
type: 'function',
function: {
name: 'browser_click',
description: 'Click an element by CSS selector. Use wait=true to wait up to 10s for the element to appear first (useful after page navigation). Scrolls element into view before clicking.',
parameters: {
type: 'object',
required: ['selector'],
properties: {
selector: { type: 'string', description: 'CSS selector for the element to click.' },
wait: { type: 'boolean', description: 'Wait for element to appear before clicking (max 10s). Default: false.' }
}
}
}
},
{
type: 'function',
function: {
name: 'browser_type',
description: 'Type text into an input field. Use clear=false to append instead of replacing. Use submit=true to press Enter or submit the parent form after typing.',
parameters: {
type: 'object',
required: ['selector', 'text'],
properties: {
selector: { type: 'string', description: 'CSS selector for the input element.' },
text: { type: 'string', description: 'Text to type into the input.' },
clear: { type: 'boolean', description: 'Clear existing text first. Default: true.' },
submit: { type: 'boolean', description: 'Submit the form or press Enter after typing. Default: false.' }
}
}
}
},
{
type: 'function',
function: {
name: 'browser_scroll',
description: 'Scroll the page. Use direction for up/down/top/bottom, or selector to scroll a specific element into view.',
parameters: {
type: 'object',
properties: {
direction: { type: 'string', enum: ['down', 'up', 'top', 'bottom'], description: 'Scroll direction. Default: down.' },
selector: { type: 'string', description: 'CSS selector to scroll into view (overrides direction).' }
}
}
}
},
{
type: 'function',
function: {
name: 'browser_wait',
description: 'Wait for a condition. Use selector to wait for an element to appear, or time_ms for a fixed delay. Default: 1 second.',
parameters: {
type: 'object',
properties: {
selector: { type: 'string', description: 'CSS selector to wait for. Returns found: true/false.' },
time_ms: { type: 'integer', description: 'Fixed wait time in milliseconds. Default: 1000.' }
}
}
}
},
{
type: 'function',
function: {
name: 'browser_close',
description: 'Close the agent browser and free resources.',
parameters: { type: 'object', properties: {} }
}
},
{
type: 'function',
function: {
name: 'datetime',
description: 'Get the precise current system time. Returns ISO timestamp, Unix time (seconds and milliseconds), locale-formatted date/time, timezone, and individual components (year/month/day/hour/minute/second/millisecond/day_of_week). Use format parameter to request specific output: "iso" / "unix" / "date" / "time" / "full" (default: full).',
parameters: {
type: 'object',
properties: {
format: { type: 'string', enum: ['full', 'iso', 'unix', 'date', 'time'], description: 'Output format. Default: full (all fields).' },
timezone: { type: 'string', description: 'IANA timezone name (e.g., "Asia/Shanghai", "America/New_York"). Default: system timezone.' }
}
}
}
},
{
type: 'function',
function: {
name: 'calculator',
description: 'Safely evaluate a mathematical expression. Supports + - * / ** % () and floating-point numbers. Uses a pure JS recursive descent parser — no eval(), CSP-safe. Returns the numeric result.',
parameters: {
type: 'object',
required: ['expression'],
properties: {
expression: { type: 'string', description: 'The mathematical expression to evaluate (e.g., "(3 + 5) * 2 ** 3"). Max 500 characters.' }
}
}
}
},
{
type: 'function',
function: {
name: 'random',
description: 'Generate random numbers or pick random items. Supports: int (integer range, default 0-100), float (decimal, default 0-1), pick (select from array of items), string (random alphanumeric, default length 8). Use count for multiple values.',
parameters: {
type: 'object',
properties: {
type: { type: 'string', enum: ['int', 'float', 'pick', 'string'], description: 'Random type. Default: int.' },
min: { type: 'number', description: 'Minimum value (int/float). Default: 0.' },
max: { type: 'number', description: 'Maximum value (int/float). Default: 100 (int) or 1 (float).' },
count: { type: 'integer', description: 'Number of results. Max 100. Default: 1.' },
items: { type: 'array', items: { type: 'string' }, description: 'Item pool for pick type.' },
length: { type: 'integer', description: 'String length for string type. Max 256. Default: 8.' }
}
}
}
},
{
type: 'function',
function: {
name: 'uuid',
description: 'Generate cryptographically random UUID v4 (e.g., "550e8400-e29b-41d4-a716-446655440000"). Uses Node.js crypto.randomUUID(). Supports batch generation up to 20.',
parameters: {
type: 'object',
properties: {
count: { type: 'integer', description: 'Number of UUIDs. Max 20. Default: 1.' }
}
}
}
},
{
type: 'function',
function: {
name: 'json_format',
description: 'Format and validate a JSON string. Returns pretty-printed JSON with configurable indentation. Optionally sort object keys alphabetically. Also works as a JSON syntax validator — returns error details on invalid input.',
parameters: {
type: 'object',
required: ['json'],
properties: {
json: { type: 'string', description: 'The JSON string to format/validate.' },
indent: { type: 'integer', description: 'Indentation spaces. Default: 2.' },
sort_keys: { type: 'boolean', description: 'Sort object keys alphabetically. Default: false.' }
}
}
}
},
{
type: 'function',
function: {
name: 'hash',
description: 'Compute cryptographic hash of text. Supports MD5, SHA-1, SHA-256, SHA-384, SHA-512. Default: SHA-256. Returns hex-encoded digest.',
parameters: {
type: 'object',
required: ['text'],
properties: {
text: { type: 'string', description: 'The text to hash.' },
algorithm: { type: 'string', enum: ['md5', 'sha1', 'sha256', 'sha384', 'sha512'], description: 'Hash algorithm. Default: sha256.' }
}
}
}
}
];
// 需要用户确认的工具:写操作、删除、命令执行、压缩、浏览器操作
const CONFIRM_TOOLS = [
'run_command',
'write_file', 'create_directory', 'delete_file',
'edit_file', 'replace_in_files', 'move_file', 'copy_file',
'download_file', 'compress',
'browser_open', 'browser_click', 'browser_type', 'browser_evaluate',
];
export function needsConfirmation(toolName: string): boolean {
if (toolName === 'run_command') {
// run_command 确认模式由 runCommandMode 状态控制
const runCommandMode = (window as any).__runCommandMode || 'confirm';
return runCommandMode === 'confirm';
}
return CONFIRM_TOOLS.includes(toolName);
}
/** 设置 run_command 执行模式 */
export function setRunCommandMode(mode: 'auto' | 'confirm' | 'disabled'): void {
(window as any).__runCommandMode = mode;
if (mode === 'disabled') {
setToolEnabled('run_command', false);
} else {
setToolEnabled('run_command', true);
}
}
let enabledTools: Set<string> = new Set([
'read_file', 'list_directory', 'search_files',
'write_file', 'create_directory', 'delete_file',
'run_command',
'move_file', 'copy_file', 'web_fetch', 'web_search', 'edit_file',
'get_file_info', 'tree', 'download_file', 'diff_files', 'replace_in_files',
'read_multiple_files', 'git', 'compress',
'memory',
'session_list', 'session_read', 'spawn_task',
'browser_open', 'browser_screenshot', 'browser_evaluate', 'browser_extract',
'browser_click', 'browser_type', 'browser_scroll', 'browser_wait', 'browser_close',
'datetime', 'calculator',
'random', 'uuid', 'json_format', 'hash'
]);
export function setToolEnabled(toolName: string, enabled: boolean): void {
if (enabled) enabledTools.add(toolName);
else enabledTools.delete(toolName);
}
/** Plan Mode 激活时注册 plan_track,关闭时移除 */
export function setPlanModeActive(active: boolean): void {
if (active) {
enabledTools.add('plan_track');
} else {
enabledTools.delete('plan_track');
// 清除追踪器状态
state.set('_planTracker', null);
}
}
export function getEnabledToolDefinitions(): ToolDefinition[] {
const builtIn = TOOL_DEFINITIONS.filter(def => enabledTools.has(def.function.name));
return [...builtIn, ..._mcpToolCache];
}
/** MCP 工具缓存 */
let _mcpToolCache: ToolDefinition[] = [];
/** 刷新 MCP 工具缓存(在 MCP 服务器配置变更时调用) */
export async function refreshMCPTools(): Promise<void> {
try {
const rawTools = await getMCPToolDefinitions();
// v5.1.2 Shadowing 防护:MCP 工具不能覆盖内置工具
const builtInNames = new Set(TOOL_DEFINITIONS.map(d => d.function.name));
const filtered = rawTools.filter(t => {
if (builtInNames.has(t.function.name)) {
logWarn(`MCP 工具 "${t.function.name}" 与内置工具重名,已跳过`);
return false;
}
return true;
});
const skipped = rawTools.length - filtered.length;
_mcpToolCache = filtered;
if (filtered.length > 0) {
logInfo(`MCP 工具已注册: ${filtered.length}${skipped > 0 ? `${skipped} 个因重名被跳过)` : ''}`, filtered.map(t => t.function.name).join(', '));
}
} catch (err) {
logError('MCP 工具刷新失败', (err as Error).message);
_mcpToolCache = [];
}
}
export async function executeTool(toolName: string, args: Record<string, unknown>): Promise<ToolResult> {
// MCP 工具路由
if (toolName.startsWith('mcp_')) {
const { executeMCPTool } = await import('./mcp-client.js');
return executeMCPTool(toolName, args);
}
if (!enabledTools.has(toolName)) {
logError(`工具未启用: ${toolName}`);
return { success: false, error: `工具 ${toolName} 未启用` };
}
const bridge = window.metonaDesktop;
if (!bridge?.isDesktop) {
logError(`工具调用仅支持桌面版: ${toolName}`);
return { success: false, error: '工具调用仅支持桌面版' };
}
try {
// 内存工具(不走 IPC,直接处理 MEMORY.md
if (toolName === 'memory') {
const { search, addEntry, replaceEntry, removeEntry, loadAllEntries, DuplicateEntryError } = await import('./memory-service.js');
const action = args.action as string;
switch (action) {
case 'search': {
const query = args.query as string;
const limit = (args.limit as number) || 8;
if (!query) return { success: false, error: '缺少 query 参数' };
const results = await search(query, limit);
logToolResult('memory', true, `${results.length} 条结果`);
return { success: true, action, results, total: results.length };
}
case 'add': {
const type = (args.type as 'fact' | 'preference' | 'rule') || 'fact'; // 兜底默认 fact
const content = args.content as string;
const importance = (args.importance as number) || 5;
const tags = (args.tags as string[]) || [];
if (!content || content.length < 2) return { success: false, error: 'add 操作缺少 content 参数或内容太短。示例: {"action":"add","type":"fact","content":"具体的记忆内容"}' };
try {
const entry = await addEntry(type, content, importance, tags);
logToolResult('memory', true, `${type}: ${content.slice(0, 50)}`);
return { success: true, action, id: entry.id, type: entry.type, content: entry.content };
} catch (err) {
if (err instanceof DuplicateEntryError) {
const dupErr = err as InstanceType<typeof DuplicateEntryError>;
logToolResult('memory', true, `重复跳过: ${dupErr.existingPreview}`);
return { success: true, action, duplicate: true, existing_id: dupErr.existingId, message: `已跳过: 相同内容的记忆已存在 (${dupErr.existingPreview})。不要再重复添加,任务已完成。` };
}
throw err;
}
}
case 'replace': {
const oldText = String(args.old_text || '');
const newContent = String(args.new_content || '');
const result = await replaceEntry(oldText, newContent);
logToolResult('memory', result.success, result.message);
return { success: result.success, action, ...(result.success ? {} : { error: result.message }), message: result.message };
}
case 'remove': {
const oldText = String(args.old_text || '');
const result = await removeEntry(oldText);
logToolResult('memory', result.success, result.message);
return { success: result.success, action, ...(result.success ? {} : { error: result.message }), message: result.message };
}
case 'read_all': {
const entries = await loadAllEntries();
logToolResult('memory', true, `${entries.length} 条记忆`);
return { success: true, action, entries, total: entries.length };
}
default:
return { success: false, error: `未知操作: ${action}。支持: search / add / replace / remove / read_all` };
}
}
if (toolName === 'session_list') {
const bridge = window.metonaDesktop;
if (!bridge?.db) return { success: false, error: '桌面 API 不可用' };
const limit = (args.limit as number) || 20;
const search = (args.search as string) || '';
const sessions = await bridge.db.getAllSessions();
let filtered = sessions.map((s: any) => ({
id: s.id,
title: s.title,
model: s.model,
messageCount: 0, // 从 SQLite 获取的原始行不含 messages
createdAt: s.created_at,
updatedAt: s.updated_at
}));
if (search) {
filtered = filtered.filter((s: any) => s.title.toLowerCase().includes(search.toLowerCase()));
}
filtered.sort((a: any, b: any) => b.updatedAt - a.updatedAt);
filtered = filtered.slice(0, limit);
logToolResult('session_list', true, `${filtered.length} 个会话`);
return { success: true, sessions: filtered, total: filtered.length };
}
if (toolName === 'session_read') {
const bridge = window.metonaDesktop;
if (!bridge?.db) return { success: false, error: '桌面 API 不可用' };
const sessionId = args.session_id as string;
const maxMessages = (args.max_messages as number) || 50;
if (!sessionId) return { success: false, error: '缺少 session_id 参数' };
const session = await bridge.db.getSession(sessionId);
if (!session) return { success: false, error: `会话 ${sessionId} 不存在` };
const messages = await bridge.db.getMessages(sessionId);
const msgs = messages.slice(0, maxMessages).map((m: any) => ({
role: m.role,
content: m.content || '',
timestamp: m.created_at
}));
logToolResult('session_read', true, `${msgs.length} 条消息`);
return { success: true, session: { id: session.id, title: session.title, model: session.model }, messages: msgs, total: messages.length };
}
// v4.3 spawn_task 走子代理(模型只能从设置面板指定,不允许 AI 自选)
if (toolName === 'spawn_task') {
const { executeSubAgent } = await import('./sub-agent.js');
const task = args.task as string;
const context = args.context as string | undefined;
// 只使用设置面板配置的子代理模型,忽略 AI 传入的 model 参数
const configuredModel = state.get<string>('subAgentModel', '');
let model: string | undefined;
if (configuredModel) {
// 设置面板存的模型名直接使用(面板加载时已验证过列表)
model = configuredModel;
}
if (!task) return { success: false, error: '缺少 task 参数' };
logInfo(`子代理委派: ${task.slice(0, 80)}${model ? ` (模型: ${model})` : ' (跟随当前模型)'}`);
const result = await executeSubAgent(task, context, model ? { model } : {});
logToolResult('spawn_task', result.success, result.success ? `完成, ${(result as any).loops} 轮` : result.error);
return result;
}
// plan_track — Plan Mode 执行追踪
if (toolName === 'plan_track') {
const action = args.action as string;
const stepIndex = args.step_index as number | undefined;
const stepLabel = (args.step_label as string) || '';
const tracker = getPlanTracker();
if (action === 'status') {
return { success: true, action: 'status', steps: tracker.steps, total: tracker.total, done: tracker.done };
}
if (action === 'mark_done' && typeof stepIndex === 'number' && stepIndex > 0) {
const idx = stepIndex - 1;
if (idx >= 0 && idx < tracker.steps.length) {
tracker.steps[idx].done = true;
tracker.done = tracker.steps.filter(s => s.done).length;
if (stepLabel) tracker.steps[idx].label = stepLabel;
savePlanTracker(tracker);
const remaining = tracker.total - tracker.done;
logToolResult('plan_track', true, `步骤 ${stepIndex} 已完成 (${tracker.done}/${tracker.total}${remaining > 0 ? `, 剩余 ${remaining}` : ', 全部完成!'})`);
return { success: true, action: 'mark_done', step: stepIndex, done: tracker.done, total: tracker.total, remaining };
}
return { success: false, error: `步骤 ${stepIndex} 不存在(共 ${tracker.total} 步)` };
}
if (action === 'mark_all_done') {
for (const s of tracker.steps) s.done = true;
tracker.done = tracker.total;
savePlanTracker(tracker);
logToolResult('plan_track', true, `全部 ${tracker.total} 步已完成`);
return { success: true, action: 'mark_all_done', done: tracker.total, total: tracker.total, remaining: 0 };
}
return { success: false, error: `未知操作: ${action}` };
}
// run_command 走 workspace IPC
if (toolName === 'run_command') {
const command = args.command as string;
const cwd = args.cwd as string | undefined;
if (!command) {
return { success: false, error: '缺少 command 参数' };
}
logInfo(`工作空间命令执行: ${command.slice(0, 100)}`);
const result = await bridge.workspace.execTool(command, cwd);
logToolResult('run_command', result.success, result.success ? undefined : result.stderr?.slice(0, 200));
return {
success: result.success,
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode,
duration: result.duration
};
}
// 其他工具:IPC 调用(无超时,由 renderer 内部处理)
const result = await bridge.tool.execute(toolName, args);
logToolResult(toolName, result.success, result.success ? undefined : result.error);
return result;
} catch (err) {
logError(`工具异常: ${toolName}`, (err as Error).message);
return { success: false, error: (err as Error).message };
}
}
export function getToolIcon(name: string): string {
const icons: Record<string, string> = {
read_file: '📄', write_file: '✏️', list_directory: '📁',
search_files: '🔍', create_directory: '📂', delete_file: '🗑️', run_command: '💻',
move_file: '📦', copy_file: '📋', web_fetch: '🌐',
edit_file: '✂️', get_file_info: '️', tree: '🌳', download_file: '⬇️',
diff_files: '🔀', replace_in_files: '🔄', read_multiple_files: '📚',
git: '🔖', compress: '🗜️', web_search: '🔍',
memory: '🧠',
session_list: '📋', session_read: '📖', spawn_task: '🤖',
plan_track: '📋',
browser_open: '🌐', browser_screenshot: '📸', browser_evaluate: '⚡', browser_extract: '📰',
browser_click: '👆', browser_type: '⌨️', browser_scroll: '↕️', browser_close: '❌',
browser_wait: '⏳',
datetime: '🕐', calculator: '🔢', random: '🎲', uuid: '🔑', json_format: '📝', hash: '#️⃣'
};
return icons[name] || '🔧';
}
/** ── Plan Mode 执行追踪器 ── */
export interface PlanStep {
index: number;
label: string;
done: boolean;
}
export interface PlanTracker {
steps: PlanStep[];
total: number;
done: number;
active: boolean;
}
let _planTracker: PlanTracker = { steps: [], total: 0, done: 0, active: false };
export function getPlanTracker(): PlanTracker {
// 每次读取时同步 state 中的最新状态
const saved = state.get<PlanTracker | null>('_planTracker', null);
if (saved) _planTracker = saved;
return _planTracker;
}
export function savePlanTracker(tracker: PlanTracker): void {
_planTracker = tracker;
state.set('_planTracker', tracker);
}
export function initPlanTracker(steps: string[]): PlanTracker {
const tracker: PlanTracker = {
steps: steps.map((label, i) => ({ index: i + 1, label, done: false })),
total: steps.length,
done: 0,
active: true,
};
savePlanTracker(tracker);
logInfo('Plan Tracker 已初始化', `${tracker.total} 个步骤`);
return tracker;
}
export function clearPlanTracker(): void {
_planTracker = { steps: [], total: 0, done: 0, active: false };
state.set('_planTracker', null);
}
export function formatPlanStatus(): string {
const t = getPlanTracker();
if (!t.active || t.steps.length === 0) return '';
const lines = t.steps.map(s => {
const icon = s.done ? '✅' : '⬜';
return `${icon} 步骤${s.index}: ${s.label}`;
});
const pct = t.total > 0 ? Math.round(t.done / t.total * 100) : 0;
const hint = t.done >= t.total ? '✅ 全部步骤已完成,请给出最终回答。' : '';
return `[Plan Mode 执行进度: ${t.done}/${t.total} (${pct}%)]\n${lines.join('\n')}${hint ? '\n\n' + hint : ''}`;
}
export function formatToolName(name: string): string {
const names: Record<string, string> = {
read_file: '读取文件', write_file: '写入文件', list_directory: '列出目录',
search_files: '搜索文件', create_directory: '创建目录', delete_file: '删除文件',
run_command: '执行命令', move_file: '移动文件', copy_file: '复制文件',
web_fetch: '网页抓取', edit_file: '编辑文件',
get_file_info: '文件信息', tree: '目录树', download_file: '下载文件',
diff_files: '文件对比', replace_in_files: '批量替换', read_multiple_files: '批量读取',
git: 'Git 操作', compress: '压缩/解压', web_search: '联网搜索',
memory: '记忆管理',
session_list: '会话列表', session_read: '读取会话', spawn_task: '子代理委派',
plan_track: '计划追踪',
browser_open: '打开网页', browser_screenshot: '浏览器截图', browser_evaluate: '执行JS', browser_extract: '提取内容',
browser_click: '点击元素', browser_type: '输入文本', browser_scroll: '滚动页面', browser_close: '关闭浏览器',
browser_wait: '等待',
datetime: '日期时间', calculator: '计算器', random: '随机数', uuid: '生成UUID', json_format: 'JSON格式化', hash: '哈希计算'
};
return names[name] || name;
}