/** * 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: 2000.' } } } } }, { 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.' }, 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']; 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 = 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 { 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): Promise { // 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; 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('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 = { 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: '❌' }; 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); 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 = { 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: '关闭浏览器' }; return names[name] || name; }