Files
metona-ollama-desktop/src/renderer/services/tool-registry.ts
T
thzxx 20cbfbd169 v0.11.11: 彻底移除技能系统,工具 44→42,SQLite 7→6 张表
- 删除 src/renderer/services/skill-manager.ts (480行)
- sqlite.ts: 删除 skills 表/索引/迁移/SkillRow接口/CRUD
- ipc.ts: 删除 6 个 skills IPC handler
- preload.ts/types.d.ts: 删除 skills bridge 方法/类型
- agent-engine.ts: 删除技能匹配上下文注入/自动提取
- tool-registry.ts: 删除 skill_list/skill_view 工具定义/执行
- log-service.ts/sub-agent.ts/workspace-panel.ts: 清理引用
- index.html: 帮助面板/工具卡片更新,44→42
- README/release-notes/DEVELOPMENT/AGENT: 文档全量同步
2026-06-22 18:13:01 +08:00

943 lines
42 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: 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.' }
}
}
}
},
// ══════════════════════════════════════════════
// v4.0 新增工具:记忆和会话管理
// ══════════════════════════════════════════════
{
type: 'function',
function: {
name: 'memory_search',
description: 'Search agent memories by semantic similarity or keywords. Returns relevant memories about the user, preferences, rules, and past conversations.',
parameters: {
type: 'object',
required: ['query'],
properties: {
query: { type: 'string', description: 'Search query for memories.' },
limit: { type: 'integer', description: 'Max results to return. Default: 8.' },
type: { type: 'string', enum: ['fact', 'preference', 'rule'], description: 'Filter by memory type.' }
}
}
}
},
{
type: 'function',
function: {
name: 'memory_add',
description: 'Add a new memory entry for future reference. Use when you learn something important about the user, their preferences, or rules they want you to follow.',
parameters: {
type: 'object',
required: ['type', 'content'],
properties: {
type: { type: 'string', enum: ['fact', 'preference', 'rule'], description: 'Memory type: fact (about user), preference (user preference), rule (must follow).' },
content: { type: 'string', description: 'The memory content. Be concise and specific.' },
importance: { type: 'integer', description: 'Importance 1-10. Higher = more important. Default: 5.' },
tags: { type: 'array', items: { type: 'string' }, description: 'Tags for search matching. 2-5 keywords.' }
}
}
}
},
{
type: 'function',
function: {
name: 'memory_replace',
description: 'Replace an existing memory entry with updated content. Uses substring matching to find the entry to replace.',
parameters: {
type: 'object',
required: ['target', 'old_text', 'new_content'],
properties: {
target: { type: 'string', enum: ['memory', 'user'], description: 'Target store: memory (agent notes) or user (user profile).' },
old_text: { type: 'string', description: 'Unique substring that identifies the entry to replace.' },
new_content: { type: 'string', description: 'The new content to replace the old entry with.' }
}
}
}
},
{
type: 'function',
function: {
name: 'memory_remove',
description: 'Remove a memory entry. Uses substring matching to find the entry to remove.',
parameters: {
type: 'object',
required: ['target', 'old_text'],
properties: {
target: { type: 'string', enum: ['memory', 'user'], description: 'Target store: memory (agent notes) or user (user profile).' },
old_text: { type: 'string', description: 'Unique substring that identifies the entry to remove.' }
}
}
}
},
{
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.' }
}
}
}
},
// ══════════════════════════════════════════════
// 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<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_search', 'memory_add', 'memory_replace', 'memory_remove',
'session_list', 'session_read', 'spawn_task',
'browser_open', 'browser_screenshot', 'browser_evaluate', 'browser_extract',
'browser_click', 'browser_type', 'browser_scroll', '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);
}
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,直接处理)
if (toolName === 'memory_search') {
const { searchMemories } = await import('./memory-manager.js');
const query = args.query as string;
const limit = (args.limit as number) || 8;
const type = args.type as string | undefined;
let results = searchMemories(query, limit);
if (type) results = results.filter(r => r.type === type);
logToolResult('memory_search', true, `${results.length} 条结果`);
return { success: true, results, total: results.length };
}
if (toolName === 'memory_add') {
const { addMemory } = await import('./memory-manager.js');
const type = args.type as 'fact' | 'preference' | 'rule';
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: 'content 不能为空且至少 2 个字符' };
}
const entry = await addMemory({ type, content, importance, tags });
logToolResult('memory_add', true, `${type}: ${content.slice(0, 50)}`);
return { success: true, id: entry.id, type: entry.type, content: entry.content };
}
if (toolName === 'memory_replace') {
const { replaceMemoryByContent } = await import('./memory-manager.js');
const target = String(args.target || 'memory') as 'memory' | 'user';
const oldText = String(args.old_text || '');
const newContent = String(args.new_content || '');
const result = await replaceMemoryByContent(target, oldText, newContent);
logToolResult('memory_replace', result.success, result.message);
return result;
}
if (toolName === 'memory_remove') {
const { removeMemoryByContent } = await import('./memory-manager.js');
const target = String(args.target || 'memory') as 'memory' | 'user';
const oldText = String(args.old_text || '');
const result = await removeMemoryByContent(target, oldText);
logToolResult('memory_remove', result.success, result.message);
return result;
}
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;
}
// 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_search: '🧠', memory_add: '💾', memory_replace: '🔄', memory_remove: '🗑️',
session_list: '📋', session_read: '📖', spawn_task: '🤖',
browser_open: '🌐', browser_screenshot: '📸', browser_evaluate: '⚡', browser_extract: '📰',
browser_click: '👆', browser_type: '⌨️', browser_scroll: '↕️', browser_close: '❌'
};
return icons[name] || '🔧';
}
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_search: '搜索记忆', memory_add: '添加记忆', memory_replace: '替换记忆', memory_remove: '删除记忆',
session_list: '会话列表', session_read: '读取会话', spawn_task: '子代理委派',
browser_open: '打开网页', browser_screenshot: '浏览器截图', browser_evaluate: '执行JS', browser_extract: '提取内容',
browser_click: '点击元素', browser_type: '输入文本', browser_scroll: '滚动页面', browser_close: '关闭浏览器'
};
return names[name] || name;
}