v0.16.12: 删除 8 个非必要工具 — get_file_info/diff_files/replace_in_files/random/uuid/json_format/hash/datetime

This commit is contained in:
紫影233
2026-07-16 17:47:02 +08:00
parent 45b50e4dc7
commit 94e0a36981
15 changed files with 301 additions and 693 deletions
+11 -27
View File
@@ -14,7 +14,7 @@
</p> </p>
<p align="center"> <p align="center">
<img src="https://img.shields.io/badge/version-v0.16.11-E8734A?style=flat-square" alt="version"> <img src="https://img.shields.io/badge/version-v0.16.12-E8734A?style=flat-square" alt="version">
<img src="https://img.shields.io/badge/electron-33+-47848F?style=flat-square&logo=electron" alt="electron"> <img src="https://img.shields.io/badge/electron-33+-47848F?style=flat-square&logo=electron" alt="electron">
<img src="https://img.shields.io/badge/typescript-5.7+-3178C6?style=flat-square&logo=typescript" alt="typescript"> <img src="https://img.shields.io/badge/typescript-5.7+-3178C6?style=flat-square&logo=typescript" alt="typescript">
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license"> <img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license">
@@ -37,7 +37,7 @@
| 🤖 | **ReAct Agent Loop** | 始终开启的唯一对话模式。8 状态机(INIT→THINKING→PARSING→EXECUTING→OBSERVING→REFLECTING→COMPRESSING→TERMINATED),最大 85 轮(可配置),智能重试(永久错误立即返回、瞬态错误指数退避),工具去重,智能路径依赖串行化,看门狗超时保护 | | 🤖 | **ReAct Agent Loop** | 始终开启的唯一对话模式。8 状态机(INIT→THINKING→PARSING→EXECUTING→OBSERVING→REFLECTING→COMPRESSING→TERMINATED),最大 85 轮(可配置),智能重试(永久错误立即返回、瞬态错误指数退避),工具去重,智能路径依赖串行化,看门狗超时保护 |
| 🛡️ | **5 层抗幻觉系统** | 提示词加固 → 任务感知 → 中途检测(中英双语规则覆盖全部工具类别)→ 进度锚点 → 完成闸门(6 项检查,幻觉/注入 → 阻断,质量/效率 → 咨询) | | 🛡️ | **5 层抗幻觉系统** | 提示词加固 → 任务感知 → 中途检测(中英双语规则覆盖全部工具类别)→ 进度锚点 → 完成闸门(6 项检查,幻觉/注入 → 阻断,质量/效率 → 咨询) |
| 📋 | **Plan Mode** | 开关切换,AI 首先生成执行计划(Markdown 渲染确认弹窗),批准后按步骤追踪执行,plan_track 工具标记完成状态,**支持断点续传**(中止后可恢复未完成计划) | | 📋 | **Plan Mode** | 开关切换,AI 首先生成执行计划(Markdown 渲染确认弹窗),批准后按步骤追踪执行,plan_track 工具标记完成状态,**支持断点续传**(中止后可恢复未完成计划) |
| 🔧 | **40 个内置工具** | 文件系统(16个) · 命令执行 · 联网搜索 · 浏览器控制(9个) · Git · 记忆 · 会话 · 子代理 · 系统工具 · Plan Mode 追踪 | | 🔧 | **32 个内置工具** | 文件系统(13个) · 命令执行 · 联网搜索 · 浏览器控制(9个) · Git · 记忆 · 会话 · 子代理 · 系统工具 · Plan Mode 追踪 |
| 🧠 | **智能记忆系统** | 三类记忆(fact / preference / rule),存储于工作空间 MEMORY.md 文件,受路径保护仅 memory 工具可访问,写入前安全扫描,容量 500 条,对话结束自动提取,严格格式校验 | | 🧠 | **智能记忆系统** | 三类记忆(fact / preference / rule),存储于工作空间 MEMORY.md 文件,受路径保护仅 memory 工具可访问,写入前安全扫描,容量 500 条,对话结束自动提取,严格格式校验 |
| 📋 | **自定义文件** | SOUL.md(人格,不可压缩)+ AGENT.md(行为准则,内置 fallback)+ USER.md(用户画像,仅工作空间读取,不存在则不注入) | | 📋 | **自定义文件** | SOUL.md(人格,不可压缩)+ AGENT.md(行为准则,内置 fallback)+ USER.md(用户画像,仅工作空间读取,不存在则不注入) |
| 🌐 | **MCP 协议扩展** | JSON-RPC 2.0 over stdio,动态工具发现,Shadowing 防护 | | 🌐 | **MCP 协议扩展** | JSON-RPC 2.0 over stdio,动态工具发现,Shadowing 防护 |
@@ -59,7 +59,7 @@
## 🔧 工具清单 ## 🔧 工具清单
<details> <details>
<summary><strong>📁 文件系统(16 个)</strong></summary> <summary><strong>📁 文件系统(13 个)</strong></summary>
| 工具 | 功能 | | 工具 | 功能 |
|------|------| |------|------|
@@ -72,11 +72,8 @@
| `move_file` | 移动/重命名文件 | | `move_file` | 移动/重命名文件 |
| `copy_file` | 复制文件 | | `copy_file` | 复制文件 |
| `edit_file` | 编辑文件(支持正则替换) | | `edit_file` | 编辑文件(支持正则替换) |
| `get_file_info` | 获取文件元信息 |
| `tree` | 目录树结构(默认5层深度) | | `tree` | 目录树结构(默认5层深度) |
| `download_file` | 下载文件 | | `download_file` | 下载文件 |
| `diff_files` | 文件差异对比 |
| `replace_in_files` | 批量替换 |
| `read_multiple_files` | 批量读取(50文件/10KB | | `read_multiple_files` | 批量读取(50文件/10KB |
| `compress` | 压缩文件/目录 | | `compress` | 压缩文件/目录 |
@@ -150,16 +147,11 @@
</details> </details>
<details> <details>
<summary><strong>🕐 系统工具(6 个)</strong></summary> <summary><strong>🕐 系统工具(1 个)</strong></summary>
| 工具 | 功能 | | 工具 | 功能 |
|------|------| |------|------|
| `datetime` | 获取系统精确时间(ISO/Unix/日期/时间/完整) |
| `calculator` | 安全数学计算(+ - * / ** % (),递归下降解析器) | | `calculator` | 安全数学计算(+ - * / ** % (),递归下降解析器) |
| `random` | 随机生成(整数/浮点/抽取/随机字符串) |
| `uuid` | 生成 UUID v4 唯一标识符 |
| `json_format` | JSON 格式化 + 语法验证 + 键排序 |
| `hash` | 哈希计算(MD5/SHA1/SHA256/SHA384/SHA512 |
</details> </details>
@@ -185,7 +177,7 @@
Ollama API (流式响应,num_ctx 用户可配 128K/256K/512K/1M) Ollama API (流式响应,num_ctx 用户可配 128K/256K/512K/1M)
Tool Registry (40 内置 + MCP 动态 + Plan Mode plan_track) Tool Registry (32 内置 + MCP 动态 + Plan Mode plan_track)
Hook 系统 (pre/post tool/iteration/completion) → 观察结果 → 反思 → 循环 / 最终回答 Hook 系统 (pre/post tool/iteration/completion) → 观察结果 → 反思 → 循环 / 最终回答
``` ```
@@ -253,7 +245,7 @@ npm start
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist
``` ```
产出:`release/Metona Ollama Setup v0.16.11.exe` 产出:`release/Metona Ollama Setup v0.16.12.exe`
## 🛠️ 常用命令 ## 🛠️ 常用命令
@@ -286,7 +278,7 @@ npm run dist # 构建 Windows 安装包
| 🤖 | **ReAct Agent Loop** | Always-on, only chat mode. 8-state machine, up to 85 iterations (configurable), smart retry (permanent errors return immediately, transient errors use exponential backoff), tool dedup, path-aware dependency serialization, watchdog + tiered timeout protection | | 🤖 | **ReAct Agent Loop** | Always-on, only chat mode. 8-state machine, up to 85 iterations (configurable), smart retry (permanent errors return immediately, transient errors use exponential backoff), tool dedup, path-aware dependency serialization, watchdog + tiered timeout protection |
| 🛡️ | **5-Layer Anti-Hallucination** | Prompt hardening → task awareness → mid-task detection (bilingual CN/EN rules covering all tool categories) → progress anchors → completion gate (6 checks: hallucination/injection → block, quality/efficiency → advisory) | | 🛡️ | **5-Layer Anti-Hallucination** | Prompt hardening → task awareness → mid-task detection (bilingual CN/EN rules covering all tool categories) → progress anchors → completion gate (6 checks: hallucination/injection → block, quality/efficiency → advisory) |
| 📋 | **Plan Mode** | Toggle switch. AI first generates an execution plan (Markdown-rendered confirmation dialog), then tracks step-by-step execution, **supports resume** (interrupted plans restore on next session) | | 📋 | **Plan Mode** | Toggle switch. AI first generates an execution plan (Markdown-rendered confirmation dialog), then tracks step-by-step execution, **supports resume** (interrupted plans restore on next session) |
| 🔧 | **40 Built-in Tools** | File system · Command · Web search · Browser · Git · Memory · Sessions · Sub-agent · System · Plan Mode tracking | | 🔧 | **32 Built-in Tools** | File system · Command · Web search · Browser · Git · Memory · Sessions · Sub-agent · System · Plan Mode tracking |
| 🧠 | **Smart Memory System** | Three types (fact / preference / rule), stored in workspace MEMORY.md file, path-protected — only the memory tool can access it, pre-write security scan, 500 capacity, auto-extract on conversation end, strict format validation | | 🧠 | **Smart Memory System** | Three types (fact / preference / rule), stored in workspace MEMORY.md file, path-protected — only the memory tool can access it, pre-write security scan, 500 capacity, auto-extract on conversation end, strict format validation |
| 📋 | **Custom Files** | SOUL.md (persona, never compressed) + AGENT.md (behavior rules, built-in fallback) + USER.md (user profile, workspace only, skipped if absent) | | 📋 | **Custom Files** | SOUL.md (persona, never compressed) + AGENT.md (behavior rules, built-in fallback) + USER.md (user profile, workspace only, skipped if absent) |
| 🌐 | **MCP Protocol Extension** | JSON-RPC 2.0 over stdio, dynamic tool discovery, Shadowing protection | | 🌐 | **MCP Protocol Extension** | JSON-RPC 2.0 over stdio, dynamic tool discovery, Shadowing protection |
@@ -308,7 +300,7 @@ npm run dist # 构建 Windows 安装包
## 🔧 Tool List ## 🔧 Tool List
<details> <details>
<summary><strong>📁 File System (16)</strong></summary> <summary><strong>📁 File System (13)</strong></summary>
| Tool | Function | | Tool | Function |
|------|------| |------|------|
@@ -321,11 +313,8 @@ npm run dist # 构建 Windows 安装包
| `move_file` | Move/rename file | | `move_file` | Move/rename file |
| `copy_file` | Copy file | | `copy_file` | Copy file |
| `edit_file` | Edit file (regex support) | | `edit_file` | Edit file (regex support) |
| `get_file_info` | Get file metadata |
| `tree` | Directory tree (default 5 levels) | | `tree` | Directory tree (default 5 levels) |
| `download_file` | Download file | | `download_file` | Download file |
| `diff_files` | File diff comparison |
| `replace_in_files` | Batch replace |
| `read_multiple_files` | Batch read (50 files/10KB) | | `read_multiple_files` | Batch read (50 files/10KB) |
| `compress` | Compress file/directory | | `compress` | Compress file/directory |
@@ -398,16 +387,11 @@ npm run dist # 构建 Windows 安装包
</details> </details>
<details> <details>
<summary><strong>🕐 System Tools (6)</strong></summary> <summary><strong>🕐 System Tools (1)</strong></summary>
| Tool | Function | | Tool | Function |
|------|------| |------|------|
| `datetime` | Get precise system time (ISO/Unix/date/time/full) |
| `calculator` | Safe math evaluation (+ - * / ** % (), recursive descent parser) | | `calculator` | Safe math evaluation (+ - * / ** % (), recursive descent parser) |
| `random` | Random generation (int/float/pick/string) |
| `uuid` | Generate UUID v4 unique identifier |
| `json_format` | JSON format + validate + sort keys |
| `hash` | Cryptographic hash (MD5/SHA1/SHA256/SHA384/SHA512) |
</details> </details>
@@ -433,7 +417,7 @@ User message → workspace SOUL.md (never compressed) → AGENT.md → USER.md (
Ollama API (Streaming Response, num_ctx user-configurable 128K/256K/512K/1M) Ollama API (Streaming Response, num_ctx user-configurable 128K/256K/512K/1M)
Tool Registry (40 Built-in + MCP Dynamic + Plan Mode plan_track) Tool Registry (32 Built-in + MCP Dynamic + Plan Mode plan_track)
Hook System (pre/post tool/iteration/completion) → Observation → Reflection → Loop / Final Answer Hook System (pre/post tool/iteration/completion) → Observation → Reflection → Loop / Final Answer
``` ```
@@ -501,7 +485,7 @@ npm start
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist
``` ```
Output: `release/Metona Ollama Setup v0.16.11.exe` Output: `release/Metona Ollama Setup v0.16.12.exe`
## 🛠️ Common Commands ## 🛠️ Common Commands
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "metona-ollama-desktop", "name": "metona-ollama-desktop",
"version": "0.16.11", "version": "0.16.12",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "metona-ollama-desktop", "name": "metona-ollama-desktop",
"version": "0.16.11", "version": "0.16.12",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ffmpeg-static": "^5.2.0", "ffmpeg-static": "^5.2.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "metona-ollama-desktop", "name": "metona-ollama-desktop",
"version": "0.16.11", "version": "0.16.12",
"description": "Metona Ollama - TypeScript + Electron 桌面 AI 聊天客户端", "description": "Metona Ollama - TypeScript + Electron 桌面 AI 聊天客户端",
"main": "dist/main/main.js", "main": "dist/main/main.js",
"author": "thzxx", "author": "thzxx",
+1 -25
View File
@@ -36,20 +36,12 @@ import {
handleWebFetch, handleWebFetch,
handleWebSearch, handleWebSearch,
handleEditFile, handleEditFile,
handleGetFileInfo,
handleTree, handleTree,
handleDownloadFile, handleDownloadFile,
handleDiffFiles,
handleReplaceInFiles,
handleReadMultipleFiles, handleReadMultipleFiles,
handleGit, handleGit,
handleCompress, handleCompress,
handleDateTime, handleCalculator
handleCalculator,
handleRandom,
handleUUID,
handleJsonFormat,
handleHash
} from './tool-handlers.js'; } from './tool-handlers.js';
import { browserOpen, browserScreenshot, browserEvaluate, browserExtract, browserClick, browserType, browserScroll, browserClose, browserWait } from './browser.js'; import { browserOpen, browserScreenshot, browserEvaluate, browserExtract, browserClick, browserType, browserScroll, browserClose, browserWait } from './browser.js';
import { startServer, stopServer, stopAllServers, callTool, getAllTools, getServerStatuses, refreshTools, setMCPTimeout } from './mcp-manager.js'; import { startServer, stopServer, stopAllServers, callTool, getAllTools, getServerStatuses, refreshTools, setMCPTimeout } from './mcp-manager.js';
@@ -80,20 +72,12 @@ function summarizeResult(toolName: string, result: Record<string, unknown>): str
case 'web_fetch': return `${result.status} | ${result.length} chars`; case 'web_fetch': return `${result.status} | ${result.length} chars`;
case 'web_search': return `[${String(result._mode) === 'searxng' ? 'SearXNG' : '内置'}] "${result.query}" → ${result.total} 条结果`; case 'web_search': return `[${String(result._mode) === 'searxng' ? 'SearXNG' : '内置'}] "${result.query}" → ${result.total} 条结果`;
case 'edit_file': return `${result.path} (${result.replaceCount} 处替换)`; case 'edit_file': return `${result.path} (${result.replaceCount} 处替换)`;
case 'get_file_info': return `${result.name} (${result.type}, ${result.size}B)`;
case 'tree': return `${result.path} (${result.fileCount} 文件 / ${result.dirCount} 目录)`; case 'tree': return `${result.path} (${result.fileCount} 文件 / ${result.dirCount} 目录)`;
case 'download_file': return `${result.url}${result.destination} (${result.size}B)`; case 'download_file': return `${result.url}${result.destination} (${result.size}B)`;
case 'diff_files': return result.hasChanges ? `${result.file1}${result.file2} (有差异)` : '无差异';
case 'replace_in_files': return `${result.filesChanged} 文件 / ${result.totalReplacements} 处替换`;
case 'read_multiple_files': return `${result.total} 个文件`; case 'read_multiple_files': return `${result.total} 个文件`;
case 'git': return `${result.action}`; case 'git': return `${result.action}`;
case 'compress': return `${result.action}${result.archive || result.destination}`; case 'compress': return `${result.action}${result.archive || result.destination}`;
case 'datetime': return `${result.date || result.iso}`;
case 'calculator': return `${result.expression} = ${result.result}`; case 'calculator': return `${result.expression} = ${result.result}`;
case 'random': return `${String(result.type) === 'pick' ? '🎲 ' : ''}${result.result}`;
case 'uuid': return `${result.result}`;
case 'json_format': return `${Number(result.keys) || 0} keys, ${result.formatted_size}B`;
case 'hash': return `${result.algorithm}: ${String(result.hash ?? '').slice(0, 16)}...`;
default: return '完成'; default: return '完成';
} }
} }
@@ -212,20 +196,12 @@ export async function setupIPC(): Promise<void> {
case 'web_fetch': result = await handleWebFetch(args as { url: string; max_chars?: number; extract_mode?: string; mobile_ua?: boolean; retry?: boolean }); break; case 'web_fetch': result = await handleWebFetch(args as { url: string; max_chars?: number; extract_mode?: string; mobile_ua?: boolean; retry?: boolean }); break;
case 'web_search': result = await handleWebSearch(args as { query: string; max_results?: number; time_range?: string; enhance_snippets?: boolean; fetch_top?: number }); break; case 'web_search': result = await handleWebSearch(args as { query: string; max_results?: number; time_range?: string; enhance_snippets?: boolean; fetch_top?: number }); break;
case 'edit_file': result = await handleEditFile(args as { path: string; old_text: string; new_text: string; all?: boolean; use_regex?: boolean }); break; case 'edit_file': result = await handleEditFile(args as { path: string; old_text: string; new_text: string; all?: boolean; use_regex?: boolean }); break;
case 'get_file_info': result = await handleGetFileInfo(args as { path: string }); break;
case 'tree': result = await handleTree(args as { path: string; max_depth?: number; include_hidden?: boolean }); break; case 'tree': result = await handleTree(args as { path: string; max_depth?: number; include_hidden?: boolean }); break;
case 'download_file': result = await handleDownloadFile(args as { url: string; destination: string }); break; case 'download_file': result = await handleDownloadFile(args as { url: string; destination: string }); break;
case 'diff_files': result = await handleDiffFiles(args as { file1: string; file2: string; context_lines?: number }); break;
case 'replace_in_files': result = await handleReplaceInFiles(args as { path: string; glob: string; old_text: string; new_text: string }); break;
case 'read_multiple_files':result = await handleReadMultipleFiles(args as { paths: string[]; max_chars_per_file?: number }); break; case 'read_multiple_files':result = await handleReadMultipleFiles(args as { paths: string[]; max_chars_per_file?: number }); break;
case 'git': result = await handleGit(args as { action: string; path?: string; files?: string[]; message?: string; branch?: string; tag_name?: string; stash_sub?: string; remote?: string; remote_url?: string; count?: number; all?: boolean; staged?: boolean; new_branch?: boolean; delete_branch?: boolean; force?: boolean; url?: string }); break; case 'git': result = await handleGit(args as { action: string; path?: string; files?: string[]; message?: string; branch?: string; tag_name?: string; stash_sub?: string; remote?: string; remote_url?: string; count?: number; all?: boolean; staged?: boolean; new_branch?: boolean; delete_branch?: boolean; force?: boolean; url?: string }); break;
case 'compress': result = await handleCompress(args as { action: string; path: string; destination?: string; format?: string }); break; case 'compress': result = await handleCompress(args as { action: string; path: string; destination?: string; format?: string }); break;
case 'datetime': result = handleDateTime(args as { format?: string; timezone?: string }); break;
case 'calculator': result = handleCalculator(args as { expression: string }); break; case 'calculator': result = handleCalculator(args as { expression: string }); break;
case 'random': result = handleRandom(args as { type?: string; min?: number; max?: number; count?: number; items?: string[]; length?: number }); break;
case 'uuid': result = handleUUID(args as { count?: number }); break;
case 'json_format': result = handleJsonFormat(args as { json: string; indent?: number; sort_keys?: boolean }); break;
case 'hash': result = handleHash(args as { text: string; algorithm?: string }); break;
// v5.1 Browser 控制(增强版) // v5.1 Browser 控制(增强版)
case 'browser_open': result = await browserOpen(args.url as string, args.wait_selector as string | undefined); break; case 'browser_open': result = await browserOpen(args.url as string, args.wait_selector as string | undefined); break;
case 'browser_screenshot': result = await browserScreenshot({ full_page: args.full_page as boolean, selector: args.selector as string }); break; case 'browser_screenshot': result = await browserScreenshot({ full_page: args.full_page as boolean, selector: args.selector as string }); break;
+1 -1
View File
@@ -101,7 +101,7 @@ export function createMenu(): void {
dialog.showMessageBox(mainWindow!, { dialog.showMessageBox(mainWindow!, {
type: 'info', type: 'info',
title: '关于 Metona Ollama', title: '关于 Metona Ollama',
message: 'Metona Ollama Desktop v0.16.11', message: 'Metona Ollama Desktop v0.16.12',
detail: 'TypeScript + Electron Ollama AI 聊天客户端\n\nhttps://gitee.com/thzxx/metona-ollama', detail: 'TypeScript + Electron Ollama AI 聊天客户端\n\nhttps://gitee.com/thzxx/metona-ollama',
icon: getIconPath() icon: getIconPath()
}); });
-203
View File
@@ -5,7 +5,6 @@
import * as fs from 'fs/promises'; import * as fs from 'fs/promises';
import * as path from 'path'; import * as path from 'path';
import * as os from 'os'; import * as os from 'os';
import { spawn } from 'child_process';
import { checkPathAllowed } from './tool-security.js'; import { checkPathAllowed } from './tool-security.js';
import { sendLog, resolvePath, type ToolResult } from './tool-handlers-shared.js'; import { sendLog, resolvePath, type ToolResult } from './tool-handlers-shared.js';
import { getWorkspaceDir } from './workspace.js'; import { getWorkspaceDir } from './workspace.js';
@@ -580,29 +579,6 @@ export async function handleEditFile(params: { path: string; old_text: string; n
} }
} }
export async function handleGetFileInfo(params: { path: string }): Promise<ToolResult> {
try {
const filePath = resolvePath(params.path);
const allowed = checkPathAllowed(filePath, 'read');
if (!allowed.ok) return { success: false, error: allowed.reason };
const stat = await fs.stat(filePath);
return {
success: true,
path: filePath,
name: path.basename(filePath),
type: stat.isDirectory() ? 'directory' : stat.isFile() ? 'file' : 'other',
size: stat.size,
created: stat.birthtime.toISOString(),
modified: stat.mtime.toISOString(),
accessed: stat.atime.toISOString(),
permissions: (stat.mode & 0o777).toString(8)
};
} catch (err) {
return { success: false, error: (err as Error).message };
}
}
export async function handleTree(params: { path: string; max_depth?: number; include_hidden?: boolean }): Promise<ToolResult> { export async function handleTree(params: { path: string; max_depth?: number; include_hidden?: boolean }): Promise<ToolResult> {
try { try {
const rootPath = resolvePath(params.path); const rootPath = resolvePath(params.path);
@@ -659,185 +635,6 @@ export async function handleTree(params: { path: string; max_depth?: number; inc
} }
} }
/** 运行 shell 命令并返回 stdout/stderr */
function runShell(cmd: string, args: string[]): Promise<{ stdout: string; stderr: string; code: number }> {
return new Promise((resolve) => {
const proc = spawn(cmd, args, { stdio: ['pipe', 'pipe', 'pipe'] });
let stdout = '', stderr = '';
proc.stdout.on('data', (d: Buffer) => { stdout += d.toString(); });
proc.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });
proc.on('close', (code) => resolve({ stdout, stderr, code: code ?? 1 }));
proc.on('error', (err) => resolve({ stdout, stderr: err.message, code: 1 }));
});
}
export async function handleDiffFiles(params: { file1: string; file2: string; context_lines?: number }): Promise<ToolResult> {
try {
const path1 = resolvePath(params.file1);
const path2 = resolvePath(params.file2);
const check1 = checkPathAllowed(path1, 'read');
if (!check1.ok) return { success: false, error: check1.reason };
const check2 = checkPathAllowed(path2, 'read');
if (!check2.ok) return { success: false, error: check2.reason };
const contextSize = params.context_lines || 3;
let diffOutput = '';
let usedCommand = '';
// 1) 优先使用系统 diff 命令
const diffResult = await runShell('diff', ['-u', `-U${contextSize}`, path1, path2]);
if (diffResult.code <= 1) { // diff exits 0=identical, 1=different
diffOutput = diffResult.stdout;
usedCommand = 'diff -u';
}
// 2) diff 不可用,尝试 git diff
if (!usedCommand) {
const gitResult = await runShell('git', ['diff', `--unified=${contextSize}`, '--no-index', path1, path2]);
if (gitResult.code <= 1) {
diffOutput = gitResult.stdout;
usedCommand = 'git diff';
}
}
// 3) 回退到内置简易 diff
if (!usedCommand) {
diffOutput = builtinDiff(path1, path2, contextSize);
usedCommand = 'builtin';
}
const hasChanges = diffOutput.trim().length > 0;
return {
success: true,
file1: path1,
file2: path2,
diff: diffOutput,
hasChanges,
method: usedCommand
};
} catch (err) {
return { success: false, error: (err as Error).message };
}
}
/** 内置简易 diff(回退方案) */
function builtinDiff(path1: string, path2: string, contextSize: number): string {
const fsSync = require('fs');
const content1 = fsSync.readFileSync(path1, 'utf-8');
const content2 = fsSync.readFileSync(path2, 'utf-8');
const lines1 = content1.split('\n');
const lines2 = content2.split('\n');
const diffLines: string[] = [];
let i = 0, j = 0;
while (i < lines1.length || j < lines2.length) {
if (i < lines1.length && j < lines2.length && lines1[i] === lines2[j]) {
if (diffLines.length === 0 || diffLines[diffLines.length - 1].startsWith('-') || diffLines[diffLines.length - 1].startsWith('+')) {
const ctxStart = Math.max(0, i - contextSize);
for (let k = ctxStart; k < i; k++) diffLines.push(` ${lines1[k]}`);
}
diffLines.push(` ${lines1[i]}`);
i++; j++;
} else {
let found = false;
for (let look = 1; look <= 10 && i + look < lines1.length; look++) {
if (lines1[i + look] === lines2[j]) {
for (let k = 0; k < look; k++) diffLines.push(`-${lines1[i + k]}`);
i += look; found = true; break;
}
}
if (!found) {
for (let look = 1; look <= 10 && j + look < lines2.length; look++) {
if (lines1[i] === lines2[j + look]) {
for (let k = 0; k < look; k++) diffLines.push(`+${lines2[j + k]}`);
j += look; found = true; break;
}
}
}
if (!found) {
if (i < lines1.length) { diffLines.push(`-${lines1[i]}`); i++; }
if (j < lines2.length) { diffLines.push(`+${lines2[j]}`); j++; }
}
}
}
return diffLines.join('\n');
}
/** 将 glob 模式转换为正则表达式(支持 **, *, ?) */
function globToRegex(glob: string): RegExp {
// 转义正则特殊字符
let pattern = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&');
// ** → 匹配任意路径(含 /)
pattern = pattern.replace(/\*\*/g, '\u0000'); // 临时占位
// * → 匹配除 / 外的任意字符
pattern = pattern.replace(/\*/g, '[^/]*');
// ? → 匹配除 / 外的单个字符
pattern = pattern.replace(/\?/g, '[^/]');
// 恢复 ** → .*
pattern = pattern.replace(/\u0000/g, '.*');
return new RegExp(`^${pattern}$`);
}
export async function handleReplaceInFiles(params: { path: string; glob: string; old_text: string; new_text: string }): Promise<ToolResult> {
try {
const rootPath = resolvePath(params.path);
const allowed = checkPathAllowed(rootPath, 'write');
if (!allowed.ok) return { success: false, error: allowed.reason };
// 正确的 glob → regex 转换:支持 ** (跨目录通配), * (单层通配), ? (单字符)
const globRegex = globToRegex(params.glob);
const results: Array<{ file: string; replacements: number }> = [];
let totalReplacements = 0;
const maxFiles = 200;
let fileCount = 0;
async function scanDir(dir: string): Promise<void> {
if (fileCount >= maxFiles) return;
let items;
try { items = await fs.readdir(dir, { withFileTypes: true }); } catch { return; }
for (const item of items) {
if (fileCount >= maxFiles) return;
if (item.name.startsWith('.')) continue;
const fullPath = path.join(dir, item.name);
const relPath = path.relative(rootPath, fullPath);
if (item.isDirectory()) {
await scanDir(fullPath);
} else if (globRegex.test(relPath) || globRegex.test(item.name)) {
fileCount++;
try {
const content = await fs.readFile(fullPath, 'utf-8');
if (content.includes(params.old_text)) {
const parts = content.split(params.old_text);
const count = parts.length - 1;
const newContent = parts.join(params.new_text);
await fs.writeFile(fullPath, newContent, 'utf-8');
results.push({ file: path.relative(rootPath, fullPath), replacements: count });
totalReplacements += count;
}
} catch { /* skip unreadable */ }
}
}
}
await scanDir(rootPath);
return {
success: true,
pattern: params.glob,
filesChanged: results.length,
totalReplacements,
results,
truncated: fileCount >= maxFiles
};
} catch (err) {
return { success: false, error: (err as Error).message };
}
}
export async function handleReadMultipleFiles(params: { paths: string[]; max_chars_per_file?: number }): Promise<ToolResult> { export async function handleReadMultipleFiles(params: { paths: string[]; max_chars_per_file?: number }): Promise<ToolResult> {
try { try {
const maxChars = params.max_chars_per_file || 0; // 0 = 不截断 const maxChars = params.max_chars_per_file || 0; // 0 = 不截断
-203
View File
@@ -5,7 +5,6 @@
import * as fs from 'fs/promises'; import * as fs from 'fs/promises';
import * as path from 'path'; import * as path from 'path';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import * as crypto from 'crypto';
import { checkPathAllowed, checkCommandAllowed } from './tool-security.js'; import { checkPathAllowed, checkCommandAllowed } from './tool-security.js';
import { mainWindow } from './main.js'; import { mainWindow } from './main.js';
import { sendLog, resolvePath, type ToolResult } from './tool-handlers-shared.js'; import { sendLog, resolvePath, type ToolResult } from './tool-handlers-shared.js';
@@ -1553,85 +1552,6 @@ export async function handleCompress(params: { action: string; path: string; des
} }
} }
// ── datetime: 获取系统精确时间 ──
export function handleDateTime(params: { format?: string; timezone?: string }): ToolResult {
try {
const now = new Date();
const tz = params.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
const fmt = params.format || 'full';
const iso = now.toISOString();
const unixSec = Math.floor(now.getTime() / 1000);
const unixMs = now.getTime();
// 中文日期(无前导零月份)
const y = now.getFullYear();
const m = now.getMonth() + 1;
const d = now.getDate();
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
const weekday = `星期${weekdays[now.getDay()]}`;
const dateStr = `${y}${m}${d}${weekday}`;
// 24小时制时间
const h = now.getHours();
const min = now.getMinutes();
const sec = now.getSeconds();
const time24 = `${String(h).padStart(2, '0')}:${String(min).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
// 时段
const period = h < 6 ? '凌晨' : h < 12 ? '上午' : h < 14 ? '中午' : h < 18 ? '下午' : '晚上';
// 人性化时间(12小时制 + 时段)
const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
const timeFriendly = `${period} ${h12}:${String(min).padStart(2, '0')}`;
// 完整人性化字符串
const friendly = `${y}${m}${d}${weekday} ${timeFriendly}`;
let result: Record<string, unknown>;
switch (fmt) {
case 'iso':
result = { iso, timezone: tz };
break;
case 'unix':
result = { unix_seconds: unixSec, unix_milliseconds: unixMs, timezone: tz };
break;
case 'date':
result = { date: dateStr, weekday, timezone: tz };
break;
case 'time':
result = { time_24h: time24, period, time_friendly: timeFriendly, timezone: tz };
break;
default: // full
result = {
iso,
unix_seconds: unixSec,
unix_milliseconds: unixMs,
date: dateStr,
weekday,
time_24h: time24,
period,
time_friendly: timeFriendly,
friendly,
timezone: tz,
year: y,
month: m,
day: d,
hour: h,
minute: min,
second: sec,
millisecond: now.getMilliseconds(),
day_of_week: now.getDay(),
};
}
return { success: true, ...result };
} catch (err) {
return { success: false, error: (err as Error).message };
}
}
// ── calculator: 安全数学计算器(纯 JS 递归下降解析,无 eval) ── // ── calculator: 安全数学计算器(纯 JS 递归下降解析,无 eval) ──
export function handleCalculator(params: { expression: string }): ToolResult { export function handleCalculator(params: { expression: string }): ToolResult {
try { try {
@@ -1710,126 +1630,3 @@ function safeCalc(expr: string): number {
if (!isFinite(result)) throw new Error('计算结果为无穷大'); if (!isFinite(result)) throw new Error('计算结果为无穷大');
return result; return result;
} }
// ── random: 随机数生成 ──
export function handleRandom(params: { type?: string; min?: number; max?: number; count?: number; items?: string[]; length?: number }): ToolResult {
try {
const type = params.type || 'int';
switch (type) {
case 'int': {
const min = params.min ?? 0;
const max = params.max ?? 100;
if (min > max) return { success: false, error: 'min 不能大于 max' };
const count = Math.min(params.count ?? 1, 100);
if (count === 1) {
return { success: true, type: 'int', result: Math.floor(Math.random() * (max - min + 1)) + min, range: [min, max] };
}
const results: number[] = [];
for (let i = 0; i < count; i++) results.push(Math.floor(Math.random() * (max - min + 1)) + min);
return { success: true, type: 'int', results, count, range: [min, max] };
}
case 'float': {
const min = params.min ?? 0;
const max = params.max ?? 1;
if (min > max) return { success: false, error: 'min 不能大于 max' };
const count = Math.min(params.count ?? 1, 100);
if (count === 1) {
return { success: true, type: 'float', result: Number((Math.random() * (max - min) + min).toFixed(6)), range: [min, max] };
}
const results: number[] = [];
for (let i = 0; i < count; i++) results.push(Number((Math.random() * (max - min) + min).toFixed(6)));
return { success: true, type: 'float', results, count, range: [min, max] };
}
case 'pick': {
const items = params.items;
if (!items || items.length === 0) return { success: false, error: 'pick 类型需要提供 items 数组' };
const count = Math.min(params.count ?? 1, items.length);
// Fisher-Yates 部分洗牌
const pool = [...items];
const picked: string[] = [];
for (let i = 0; i < count; i++) {
const idx = Math.floor(Math.random() * pool.length);
picked.push(pool[idx]);
pool.splice(idx, 1);
}
return { success: true, type: 'pick', result: count === 1 ? picked[0] : picked, from: items.length, count };
}
case 'string': {
const length = Math.min(params.length ?? 8, 256);
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
// 使用 crypto 增强随机性
const buf = crypto.randomBytes(length);
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(buf[i] % chars.length);
}
return { success: true, type: 'string', result, length, charset: 'alphanumeric' };
}
default:
return { success: false, error: `未知类型: ${type}。支持: int / float / pick / string` };
}
} catch (err) {
return { success: false, error: (err as Error).message };
}
}
// ── uuid: 生成 UUID v4 ──
export function handleUUID(params: { count?: number }): ToolResult {
try {
const crypto = require('crypto');
const count = Math.min(params.count ?? 1, 20);
if (count === 1) {
return { success: true, result: crypto.randomUUID() };
}
const results: string[] = [];
for (let i = 0; i < count; i++) results.push(crypto.randomUUID());
return { success: true, results, count };
} catch (err) {
return { success: false, error: (err as Error).message };
}
}
// ── json_format: JSON 格式化 + 验证 ──
export function handleJsonFormat(params: { json: string; indent?: number; sort_keys?: boolean }): ToolResult {
try {
const parsed = JSON.parse(params.json);
const indent = params.indent ?? 2;
let formatted: string;
if (params.sort_keys) {
formatted = JSON.stringify(sortObjectKeys(parsed), null, indent);
} else {
formatted = JSON.stringify(parsed, null, indent);
}
const size = Buffer.byteLength(formatted, 'utf-8');
return { success: true, formatted, original_size: params.json.length, formatted_size: size, keys: Object.keys(parsed).length };
} catch (err) {
return { success: false, error: `JSON 解析失败: ${(err as Error).message}`, input_preview: params.json.slice(0, 200) };
}
function sortObjectKeys(obj: any): any {
if (Array.isArray(obj)) return obj.map(sortObjectKeys);
if (obj !== null && typeof obj === 'object') {
const sorted: Record<string, any> = {};
for (const k of Object.keys(obj).sort()) sorted[k] = sortObjectKeys(obj[k]);
return sorted;
}
return obj;
}
}
// ── hash: 哈希计算 ──
export function handleHash(params: { text: string; algorithm?: string }): ToolResult {
try {
const crypto = require('crypto');
const algo = (params.algorithm || 'sha256').toLowerCase().replace('-', '');
const validAlgos = ['md5', 'sha1', 'sha256', 'sha384', 'sha512'];
if (!validAlgos.includes(algo)) {
return { success: false, error: `不支持的算法: ${algo}。支持: ${validAlgos.join(', ')}` };
}
const hash = crypto.createHash(algo).update(params.text, 'utf-8').digest('hex');
return { success: true, algorithm: algo, hash, input_length: params.text.length };
} catch (err) {
return { success: false, error: (err as Error).message };
}
}
+2 -10
View File
@@ -6,7 +6,7 @@
export { sendLog, resolvePath, type ToolResult } from './tool-handlers-shared.js'; export { sendLog, resolvePath, type ToolResult } from './tool-handlers-shared.js';
// 文件系统操作(15 个) // 文件系统操作(11 个)
export { export {
handleReadFile, handleReadFile,
handleWriteFile, handleWriteFile,
@@ -17,14 +17,11 @@ export {
handleMoveFile, handleMoveFile,
handleCopyFile, handleCopyFile,
handleEditFile, handleEditFile,
handleGetFileInfo,
handleTree, handleTree,
handleDiffFiles,
handleReplaceInFiles,
handleReadMultipleFiles, handleReadMultipleFiles,
} from './tool-handlers-fs.js'; } from './tool-handlers-fs.js';
// 系统与网络操作(8 个) // 系统与网络操作(7 个)
export { export {
handleRunCommand, handleRunCommand,
killToolProcess, killToolProcess,
@@ -32,12 +29,7 @@ export {
handleWebSearch, handleWebSearch,
handleDownloadFile, handleDownloadFile,
handleCompress, handleCompress,
handleDateTime,
handleCalculator, handleCalculator,
handleRandom,
handleUUID,
handleJsonFormat,
handleHash,
} from './tool-handlers-system.js'; } from './tool-handlers-system.js';
// Git 操作(1 个) // Git 操作(1 个)
+1 -1
View File
@@ -21,7 +21,7 @@ const MODE_NAMES: Record<string, string> = {
const MANAGED_TOOLS = [ const MANAGED_TOOLS = [
'run_command', 'run_command',
'write_file', 'create_directory', 'delete_file', 'write_file', 'create_directory', 'delete_file',
'edit_file', 'replace_in_files', 'move_file', 'copy_file', 'edit_file', 'move_file', 'copy_file',
'download_file', 'compress', 'download_file', 'compress',
]; ];
+4 -4
View File
@@ -996,8 +996,8 @@ function getToolDisplayName(name: string): string {
read_file: '读取文件', write_file: '写入文件', list_directory: '列出目录', read_file: '读取文件', write_file: '写入文件', list_directory: '列出目录',
search_files: '搜索文件', create_directory: '创建目录', delete_file: '删除文件', run_command: '执行命令', search_files: '搜索文件', create_directory: '创建目录', delete_file: '删除文件', run_command: '执行命令',
move_file: '移动文件', copy_file: '复制文件', web_fetch: '网页抓取', move_file: '移动文件', copy_file: '复制文件', web_fetch: '网页抓取',
edit_file: '编辑文件', get_file_info: '文件信息', tree: '目录树', download_file: '下载文件', edit_file: '编辑文件', tree: '目录树', download_file: '下载文件',
diff_files: '文件对比', replace_in_files: '批量替换', read_multiple_files: '批量读取', read_multiple_files: '批量读取',
git: 'Git 操作', compress: '压缩/解压', web_search: '联网搜索', git: 'Git 操作', compress: '压缩/解压', web_search: '联网搜索',
memory: '记忆管理', memory: '记忆管理',
@@ -1013,8 +1013,8 @@ function getToolIcon(name: string): string {
read_file: '📄', write_file: '✏️', list_directory: '📁', read_file: '📄', write_file: '✏️', list_directory: '📁',
search_files: '🔍', create_directory: '📂', delete_file: '🗑️', run_command: '💻', search_files: '🔍', create_directory: '📂', delete_file: '🗑️', run_command: '💻',
move_file: '📦', copy_file: '📋', web_fetch: '🌐', move_file: '📦', copy_file: '📋', web_fetch: '🌐',
edit_file: '✂️', get_file_info: '️', tree: '🌳', download_file: '⬇️', edit_file: '✂️', tree: '🌳', download_file: '⬇️',
diff_files: '🔀', replace_in_files: '🔄', read_multiple_files: '📚', read_multiple_files: '📚',
git: '🔖', compress: '🗜️', web_search: '🔍', git: '🔖', compress: '🗜️', web_search: '🔍',
memory: '🧠', memory: '🧠',
+2 -34
View File
@@ -28,7 +28,7 @@
<div class="header-left"> <div class="header-left">
<img class="logo" src="./assets/icons/llama.png" alt="logo" /> <img class="logo" src="./assets/icons/llama.png" alt="logo" />
<span class="app-title">Metona Ollama</span> <span class="app-title">Metona Ollama</span>
<span class="app-version">v0.16.11</span> <span class="app-version">v0.16.12</span>
<button class="icon-btn help-btn" id="btnHelp" title="使用帮助"> <button class="icon-btn help-btn" id="btnHelp" title="使用帮助">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/> <circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
@@ -438,7 +438,7 @@
<div class="modal-body"> <div class="modal-body">
<div class="help-section"><h4>🚀 快速开始</h4><ol><li>确保 Ollama 已启动(默认 <code>http://127.0.0.1:11434</code>,可在设置中修改)</li><li>顶部模型栏选择一个模型,右侧会显示模型能力徽章(🧠 Think / 👁️ Vision / 🔧 Tools</li><li>输入消息,按 <kbd>Enter</kbd> 发送,<kbd>Shift+Enter</kbd> 换行</li></ol></div> <div class="help-section"><h4>🚀 快速开始</h4><ol><li>确保 Ollama 已启动(默认 <code>http://127.0.0.1:11434</code>,可在设置中修改)</li><li>顶部模型栏选择一个模型,右侧会显示模型能力徽章(🧠 Think / 👁️ Vision / 🔧 Tools</li><li>输入消息,按 <kbd>Enter</kbd> 发送,<kbd>Shift+Enter</kbd> 换行</li></ol></div>
<div class="help-section"><h4>💬 聊天功能</h4><ul><li><strong>流式回复</strong> — 实时打字效果,随时点 ■ 停止</li><li><strong>Think 推理</strong> — 下方 Think 按钮切换,让模型展示深度思考过程(需模型支持,如 Qwen3)</li><li><strong>上下文长度手动控制</strong> — 设置面板下拉选择(128K / 256K / 512K / 1M),默认 128K。模型栏显示当前配置值,下拉框中每个模型旁显示其自身的上下文长度</li><li><strong>Qwen3 兼容</strong> — 自动保证 system 消息唯一且位于首位,避免 Qwen3 等模型 400 错误</li><li><strong>多模态</strong> — 上传图片或视频(≤10MB),模型需支持 Vision。图片自动压缩,视频 1fps 提取帧序列带时序标注</li><li><strong>文件分析</strong> — 支持 50+ 种文本/代码格式,单文件 ≤500KB,自动剥离注释并按上下文预算智能截断</li><li><strong>AI 回复顶部</strong> — 每条 AI 回复上方展示 📋 系统提示词折叠卡片,可点击查看实际发送给模型的完整上下文</li></ul></div> <div class="help-section"><h4>💬 聊天功能</h4><ul><li><strong>流式回复</strong> — 实时打字效果,随时点 ■ 停止</li><li><strong>Think 推理</strong> — 下方 Think 按钮切换,让模型展示深度思考过程(需模型支持,如 Qwen3)</li><li><strong>上下文长度手动控制</strong> — 设置面板下拉选择(128K / 256K / 512K / 1M),默认 128K。模型栏显示当前配置值,下拉框中每个模型旁显示其自身的上下文长度</li><li><strong>Qwen3 兼容</strong> — 自动保证 system 消息唯一且位于首位,避免 Qwen3 等模型 400 错误</li><li><strong>多模态</strong> — 上传图片或视频(≤10MB),模型需支持 Vision。图片自动压缩,视频 1fps 提取帧序列带时序标注</li><li><strong>文件分析</strong> — 支持 50+ 种文本/代码格式,单文件 ≤500KB,自动剥离注释并按上下文预算智能截断</li><li><strong>AI 回复顶部</strong> — 每条 AI 回复上方展示 📋 系统提示词折叠卡片,可点击查看实际发送给模型的完整上下文</li></ul></div>
<div class="help-section"><h4>🔧 Tool Calling(始终开启)</h4><ul><li>所有消息均通过 <strong>Agent Loop 自主调用本地工具</strong>,像一个本地 Agent,无普通聊天模式</li><li><strong>40+1 个工具</strong>,分为 9 类:<ul><li><strong>文件系统</strong>16 个):read_file / write_file / list_directory / search_files / create_directory / delete_file / move_file / copy_file / edit_file / get_file_info / tree / download_file / diff_files / replace_in_files / read_multiple_files / compress</li><li><strong>命令执行</strong>1 个):run_command(实时流式输出,支持自动/需确认/禁用三种模式,可配置超时)</li><li><strong>联网搜索</strong>2 个):web_search(支持 SearXNG 元搜索引擎 JSON API / 四引擎 HTML 解析,双模式可切换)/ web_fetch(反爬headers+UA自动切换+自动回退浏览器渲染)</li><li><strong>Git</strong>(1 个):git17 个子操作,push/pull/clone 内置60-120s超时保护)</li><li><strong>浏览器控制</strong>9 个):browser_open / browser_screenshot / browser_evaluate / browser_extract / browser_click / browser_type / browser_scroll / browser_wait / browser_close</li><li><strong>记忆 & 会话</strong>(5 个):memory(统一记忆管理,5 个 action/ session_list / session_read / spawn_task / plan_track</li><li><strong>系统工具</strong>6 个):datetime / calculator / random / uuid / json_format / hash</li></ul></li><li>read_file 支持 binary模式读取/base64解码/字节分页/2000行默认+截断hint自动续读</li><li>write_file 支持 base64写二进制文件/mode(overwrite+append)合并append_file功能/10MB</li><li>edit_file 支持 use_regex 正则替换</li><li>search_files 支持正则表达式搜索(use_regex=true</li><li>browser_screenshot 支持全页面截图+元素截图</li><li>browser_extract 支持CSS选择器提取特定区域</li><li>new browser_wait 等待元素出现或定时等待</li><li>所有写操作工具均支持三档开关:自动/需确认/禁用(浏览器工具始终自动执行)</li><li>危险命令(<code>rm -rf</code><code>mkfs</code>、反弹 shell 等)和系统路径(<code>/etc</code><code>~/.ssh</code> 等)被自动拦截</li><li>工具调用以<strong>可视化卡片</strong>展示状态(pending → running → success/error),在工作空间🔧工具页签中显示</li><li>默认最大 <strong>85 轮</strong>工具调用循环(设置面板可调),上下文使用率>80%时自动缩减到3轮</li><li>独立工具自动<strong>并行执行</strong>16个只读工具加入并行白名单),有依赖关系的工具串行执行</li></ul></div> <div class="help-section"><h4>🔧 Tool Calling(始终开启)</h4><ul><li>所有消息均通过 <strong>Agent Loop 自主调用本地工具</strong>,像一个本地 Agent,无普通聊天模式</li><li><strong>32 个工具</strong>,分为 9 类:<ul><li><strong>文件系统</strong>13 个):read_file / write_file / list_directory / search_files / create_directory / delete_file / move_file / copy_file / edit_file / tree / download_file / read_multiple_files / compress</li><li><strong>命令执行</strong>1 个):run_command(实时流式输出,支持自动/需确认/禁用三种模式,可配置超时)</li><li><strong>联网搜索</strong>2 个):web_search(支持 SearXNG 元搜索引擎 JSON API / 四引擎 HTML 解析,双模式可切换)/ web_fetch(反爬headers+UA自动切换+自动回退浏览器渲染)</li><li><strong>Git</strong>(1 个):git17 个子操作,push/pull/clone 内置60-120s超时保护)</li><li><strong>浏览器控制</strong>9 个):browser_open / browser_screenshot / browser_evaluate / browser_extract / browser_click / browser_type / browser_scroll / browser_wait / browser_close</li><li><strong>记忆 & 会话</strong>(5 个):memory(统一记忆管理,5 个 action/ session_list / session_read / spawn_task / plan_track</li><li><strong>系统工具</strong>1 个):calculator</li></ul></li><li>read_file 支持 binary模式读取/base64解码/字节分页/2000行默认+截断hint自动续读</li><li>write_file 支持 base64写二进制文件/mode(overwrite+append)合并append_file功能/10MB</li><li>edit_file 支持 use_regex 正则替换</li><li>search_files 支持正则表达式搜索(use_regex=true</li><li>browser_screenshot 支持全页面截图+元素截图</li><li>browser_extract 支持CSS选择器提取特定区域</li><li>new browser_wait 等待元素出现或定时等待</li><li>所有写操作工具均支持三档开关:自动/需确认/禁用(浏览器工具始终自动执行)</li><li>危险命令(<code>rm -rf</code><code>mkfs</code>、反弹 shell 等)和系统路径(<code>/etc</code><code>~/.ssh</code> 等)被自动拦截</li><li>工具调用以<strong>可视化卡片</strong>展示状态(pending → running → success/error),在工作空间🔧工具页签中显示</li><li>默认最大 <strong>85 轮</strong>工具调用循环(设置面板可调),上下文使用率>80%时自动缩减到3轮</li><li>独立工具自动<strong>并行执行</strong>11个只读工具加入并行白名单),有依赖关系的工具串行执行</li></ul></div>
<div class="help-section"><h4>🧠 Agent 记忆系统</h4><ul><li>记忆存储在工作空间 <strong>MEMORY.md</strong> 文件,受严格路径保护,仅 <code>memory</code> 工具可读写</li><li>新对话时自动检索相关记忆注入 AI 上下文,让 AI "记住"你</li><li>对话结束时 AI 自动提取有价值的用户信息保存(多层质量过滤,宁缺毋滥)</li><li><strong>memory 工具</strong>5 个 action):search(关键词搜索)/ add(添加)/ replace(替换)/ remove(删除)/ read_all(读取全部)</li><li>点击顶部 🧠 按钮打开记忆面板:查看、添加、删除记忆条目</li><li><strong>记忆容量上限 500 条</strong>,超限时自动清理低价值条目(规则类型受保护)</li><li>写入前自动安全扫描(prompt injection / 敏感信息 / 不可见字符检测)</li><li>应用启动时自动校验 MEMORY.md 格式,格式错误自动备份重建</li></ul></div> <div class="help-section"><h4>🧠 Agent 记忆系统</h4><ul><li>记忆存储在工作空间 <strong>MEMORY.md</strong> 文件,受严格路径保护,仅 <code>memory</code> 工具可读写</li><li>新对话时自动检索相关记忆注入 AI 上下文,让 AI "记住"你</li><li>对话结束时 AI 自动提取有价值的用户信息保存(多层质量过滤,宁缺毋滥)</li><li><strong>memory 工具</strong>5 个 action):search(关键词搜索)/ add(添加)/ replace(替换)/ remove(删除)/ read_all(读取全部)</li><li>点击顶部 🧠 按钮打开记忆面板:查看、添加、删除记忆条目</li><li><strong>记忆容量上限 500 条</strong>,超限时自动清理低价值条目(规则类型受保护)</li><li>写入前自动安全扫描(prompt injection / 敏感信息 / 不可见字符检测)</li><li>应用启动时自动校验 MEMORY.md 格式,格式错误自动备份重建</li></ul></div>
<div class="help-section"><h4>📋 Plan Mode(计划模式)</h4><ul><li>点击输入框上方 📋 按钮开启 <strong>Plan Mode</strong>(开关式)</li><li>开启后,AI <strong>先生成执行计划</strong>(Markdown 渲染弹窗),用户批准后才开始执行</li><li>计划批准后自动初始化追踪器,每个步骤完成后调用 <code>plan_track</code> 工具标记进度</li><li>系统提示词自动注入当前进度状态,AI 始终知道还剩多少步未完成</li><li><strong>多步骤任务防遗忘</strong>:自动检测用户请求中的动作动词,对比已完成步骤,注入提醒</li><li>关闭 Plan Mode 后恢复正常 Agent Loop 模式</li><li><strong>仅 Plan 模式</strong> 下 plan_track 工具可见,避免污染普通模式的工具列表</li></ul></div> <div class="help-section"><h4>📋 Plan Mode(计划模式)</h4><ul><li>点击输入框上方 📋 按钮开启 <strong>Plan Mode</strong>(开关式)</li><li>开启后,AI <strong>先生成执行计划</strong>(Markdown 渲染弹窗),用户批准后才开始执行</li><li>计划批准后自动初始化追踪器,每个步骤完成后调用 <code>plan_track</code> 工具标记进度</li><li>系统提示词自动注入当前进度状态,AI 始终知道还剩多少步未完成</li><li><strong>多步骤任务防遗忘</strong>:自动检测用户请求中的动作动词,对比已完成步骤,注入提醒</li><li>关闭 Plan Mode 后恢复正常 Agent Loop 模式</li><li><strong>仅 Plan 模式</strong> 下 plan_track 工具可见,避免污染普通模式的工具列表</li></ul></div>
<div class="help-section"><h4>🛡️ 抗幻觉 & 稳定性</h4><ul><li><strong>5 层防御体系</strong>:系统提示词加固 → 任务感知 → 中途幻觉检测(中英双语规则覆盖全部工具类别)→ 进度锚点 → 完成闸门(6 项检查,幻觉/注入 → 阻断,质量/效率 → 咨询)</li><li><strong>完成闸门</strong>:对话结束前自动审查 AI 回复,检测工具幻觉和 prompt injection,阻断有问题的回复</li><li><strong>中英双语检测规则</strong>:覆盖中英文模型输出,防止 AI 声称执行了未调用的工具</li><li><strong>智能重试机制</strong>:永久错误(文件不存在/权限拒绝)立即返回,瞬态错误(网络/超时)指数退避最多重试 2 次</li><li><strong>看门狗超时</strong>:设置面板可配置全局超时(默认 30 分钟),AI 卡死或无限循环时自动中止</li><li><strong>流式总超时</strong>:可配置超时(默认 300s),Ollama 卡死不再永久阻塞</li><li><strong>中止保护</strong>:所有状态处理器 + 重试循环均检查中止信号,点击 ■ 按钮立即生效</li><li><strong>上下文硬上限</strong>:200 条消息强制压缩 + 120 条增量压缩,防止 OOM</li></ul></div> <div class="help-section"><h4>🛡️ 抗幻觉 & 稳定性</h4><ul><li><strong>5 层防御体系</strong>:系统提示词加固 → 任务感知 → 中途幻觉检测(中英双语规则覆盖全部工具类别)→ 进度锚点 → 完成闸门(6 项检查,幻觉/注入 → 阻断,质量/效率 → 咨询)</li><li><strong>完成闸门</strong>:对话结束前自动审查 AI 回复,检测工具幻觉和 prompt injection,阻断有问题的回复</li><li><strong>中英双语检测规则</strong>:覆盖中英文模型输出,防止 AI 声称执行了未调用的工具</li><li><strong>智能重试机制</strong>:永久错误(文件不存在/权限拒绝)立即返回,瞬态错误(网络/超时)指数退避最多重试 2 次</li><li><strong>看门狗超时</strong>:设置面板可配置全局超时(默认 30 分钟),AI 卡死或无限循环时自动中止</li><li><strong>流式总超时</strong>:可配置超时(默认 300s),Ollama 卡死不再永久阻塞</li><li><strong>中止保护</strong>:所有状态处理器 + 重试循环均检查中止信号,点击 ■ 按钮立即生效</li><li><strong>上下文硬上限</strong>:200 条消息强制压缩 + 120 条增量压缩,防止 OOM</li></ul></div>
@@ -524,10 +524,6 @@
<div class="tool-card-header"><span class="tool-card-icon">💻</span><span class="tool-card-name">run_command</span><span class="tool-card-badge confirm">需确认</span></div> <div class="tool-card-header"><span class="tool-card-icon">💻</span><span class="tool-card-name">run_command</span><span class="tool-card-badge confirm">需确认</span></div>
<div class="tool-card-desc">执行 shell 命令,支持超时配置</div> <div class="tool-card-desc">执行 shell 命令,支持超时配置</div>
</div> </div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon"></span><span class="tool-card-name">get_file_info</span><span class="tool-card-badge auto">自动</span></div>
<div class="tool-card-desc">获取文件/目录详细信息:大小、日期、权限</div>
</div>
<div class="tool-card"> <div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">🌳</span><span class="tool-card-name">tree</span><span class="tool-card-badge auto">自动</span></div> <div class="tool-card-header"><span class="tool-card-icon">🌳</span><span class="tool-card-name">tree</span><span class="tool-card-badge auto">自动</span></div>
<div class="tool-card-desc">以树形结构展示目录内容</div> <div class="tool-card-desc">以树形结构展示目录内容</div>
@@ -536,14 +532,6 @@
<div class="tool-card-header"><span class="tool-card-icon">⬇️</span><span class="tool-card-name">download_file</span><span class="tool-card-badge confirm">需确认</span></div> <div class="tool-card-header"><span class="tool-card-icon">⬇️</span><span class="tool-card-name">download_file</span><span class="tool-card-badge confirm">需确认</span></div>
<div class="tool-card-desc">从 URL 下载文件到本地</div> <div class="tool-card-desc">从 URL 下载文件到本地</div>
</div> </div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">🔀</span><span class="tool-card-name">diff_files</span><span class="tool-card-badge auto">自动</span></div>
<div class="tool-card-desc">对比两个文件,显示差异</div>
</div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">🔄</span><span class="tool-card-name">replace_in_files</span><span class="tool-card-badge confirm">需确认</span></div>
<div class="tool-card-desc">按 glob 模式批量查找替换多个文件</div>
</div>
<div class="tool-card"> <div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">📚</span><span class="tool-card-name">read_multiple_files</span><span class="tool-card-badge auto">自动</span></div> <div class="tool-card-header"><span class="tool-card-icon">📚</span><span class="tool-card-name">read_multiple_files</span><span class="tool-card-badge auto">自动</span></div>
<div class="tool-card-desc">并行读取最多50个文件,每文件10KB</div> <div class="tool-card-desc">并行读取最多50个文件,每文件10KB</div>
@@ -612,30 +600,10 @@
<div class="tool-card-header"><span class="tool-card-icon"></span><span class="tool-card-name">browser_close</span><span class="tool-card-badge auto">自动</span></div> <div class="tool-card-header"><span class="tool-card-icon"></span><span class="tool-card-name">browser_close</span><span class="tool-card-badge auto">自动</span></div>
<div class="tool-card-desc">关闭 Agent 浏览器并释放资源</div> <div class="tool-card-desc">关闭 Agent 浏览器并释放资源</div>
</div> </div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">🕐</span><span class="tool-card-name">datetime</span><span class="tool-card-badge auto">自动</span></div>
<div class="tool-card-desc">获取系统精确时间(ISO/Unix/日期/时间/完整)</div>
</div>
<div class="tool-card"> <div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">🔢</span><span class="tool-card-name">calculator</span><span class="tool-card-badge auto">自动</span></div> <div class="tool-card-header"><span class="tool-card-icon">🔢</span><span class="tool-card-name">calculator</span><span class="tool-card-badge auto">自动</span></div>
<div class="tool-card-desc">安全数学计算(+ - * / ** % (),递归下降解析器)</div> <div class="tool-card-desc">安全数学计算(+ - * / ** % (),递归下降解析器)</div>
</div> </div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">🎲</span><span class="tool-card-name">random</span><span class="tool-card-badge auto">自动</span></div>
<div class="tool-card-desc">随机数/随机选择(整数/浮点/抽取/字符串)</div>
</div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">🆔</span><span class="tool-card-name">uuid</span><span class="tool-card-badge auto">自动</span></div>
<div class="tool-card-desc">生成 UUID v4 唯一标识符(批量最多20个)</div>
</div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">📋</span><span class="tool-card-name">json_format</span><span class="tool-card-badge auto">自动</span></div>
<div class="tool-card-desc">JSON 格式化 + 语法验证 + 键排序</div>
</div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">🔐</span><span class="tool-card-name">hash</span><span class="tool-card-badge auto">自动</span></div>
<div class="tool-card-desc">哈希计算(MD5/SHA1/SHA256/SHA384/SHA512</div>
</div>
<div class="tool-card"> <div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">📋</span><span class="tool-card-name">plan_track</span><span class="tool-card-badge confirm" id="badge_plan_track">需确认</span></div> <div class="tool-card-header"><span class="tool-card-icon">📋</span><span class="tool-card-name">plan_track</span><span class="tool-card-badge confirm" id="badge_plan_track">需确认</span></div>
<div class="tool-card-desc">Plan 模式专属:追踪执行进度,标记/查看步骤完成状态</div> <div class="tool-card-desc">Plan 模式专属:追踪执行进度,标记/查看步骤完成状态</div>
+1 -1
View File
@@ -458,7 +458,7 @@ async function init(): Promise<void> {
// 如果旧 toolModes 中所有被管理工具都是 auto,则迁移为 auto;否则 confirm // 如果旧 toolModes 中所有被管理工具都是 auto,则迁移为 auto;否则 confirm
const allAuto = [ const allAuto = [
'run_command', 'write_file', 'create_directory', 'delete_file', 'run_command', 'write_file', 'create_directory', 'delete_file',
'edit_file', 'replace_in_files', 'move_file', 'copy_file', 'edit_file', 'move_file', 'copy_file',
'download_file', 'compress', 'download_file', 'compress',
].every(t => savedModes[t] === 'auto'); ].every(t => savedModes[t] === 'auto');
globalMode = allAuto ? 'auto' : 'confirm'; globalMode = allAuto ? 'auto' : 'confirm';
+10 -46
View File
@@ -159,18 +159,16 @@ function getOSEnvironment() {
/** 始终可并行的只读/独立工具 */ /** 始终可并行的只读/独立工具 */
const ALWAYS_PARALLEL = new Set([ const ALWAYS_PARALLEL = new Set([
'read_file', 'list_directory', 'search_files', 'get_file_info', 'tree', 'read_file', 'list_directory', 'search_files', 'tree',
'web_search', 'browser_screenshot', 'browser_extract', 'web_search', 'browser_screenshot', 'browser_extract',
'memory', 'session_list', 'session_read', 'memory', 'session_list', 'session_read',
'diff_files', 'calculator',
'datetime', 'calculator',
'random', 'uuid', 'json_format', 'hash',
]); ]);
/** D4: 有副作用的工具 — 同轮次去重时不返回缓存,需实际执行 */ /** D4: 有副作用的工具 — 同轮次去重时不返回缓存,需实际执行 */
const SIDE_EFFECT_TOOLS = new Set([ const SIDE_EFFECT_TOOLS = new Set([
'write_file', 'edit_file', 'create_directory', 'delete_file', 'write_file', 'edit_file', 'create_directory', 'delete_file',
'move_file', 'copy_file', 'replace_in_files', 'download_file', 'move_file', 'copy_file', 'download_file',
'run_command', 'git', 'compress', 'run_command', 'git', 'compress',
'browser_open', 'browser_click', 'browser_type', 'browser_close', 'browser_open', 'browser_click', 'browser_type', 'browser_close',
]); ]);
@@ -190,17 +188,11 @@ const _pendingMemoryTimers: ReturnType<typeof setTimeout>[] = [];
/** R3/R108: 工具超时配置(基础超时 + 动态调整) */ /** R3/R108: 工具超时配置(基础超时 + 动态调整) */
const TOOL_TIMEOUT_MAP: Record<string, { base: number; grade: 'fast' | 'medium' | 'slow' | 'long'; description: string }> = { const TOOL_TIMEOUT_MAP: Record<string, { base: number; grade: 'fast' | 'medium' | 'slow' | 'long'; description: string }> = {
// fast (<5秒) // fast (<5秒)
datetime: { base: 1_000, grade: 'fast', description: '时间查询' },
random: { base: 1_000, grade: 'fast', description: '随机数' },
uuid: { base: 1_000, grade: 'fast', description: 'UUID' },
calculator: { base: 3_000, grade: 'fast', description: '计算器' }, calculator: { base: 3_000, grade: 'fast', description: '计算器' },
json_format: { base: 3_000, grade: 'fast', description: 'JSON 格式化' },
hash: { base: 3_000, grade: 'fast', description: '哈希计算' },
create_directory: { base: 5_000, grade: 'fast', description: '创建目录' }, create_directory: { base: 5_000, grade: 'fast', description: '创建目录' },
get_file_info: { base: 5_000, grade: 'fast', description: '获取文件信息' },
memory: { base: 5_000, grade: 'fast', description: '记忆操作' }, memory: { base: 5_000, grade: 'fast', description: '记忆操作' },
session_list: { base: 5_000, grade: 'fast', description: '会话列表' }, session_list: { base: 5_000, grade: 'fast', description: '会话列表' },
// medium (5-15秒) // medium (5-15秒)
list_directory: { base: 10_000, grade: 'medium', description: '列目录' }, list_directory: { base: 10_000, grade: 'medium', description: '列目录' },
delete_file: { base: 10_000, grade: 'medium', description: '删除文件' }, delete_file: { base: 10_000, grade: 'medium', description: '删除文件' },
@@ -208,15 +200,13 @@ const TOOL_TIMEOUT_MAP: Record<string, { base: number; grade: 'fast' | 'medium'
edit_file: { base: 15_000, grade: 'medium', description: '编辑文件' }, edit_file: { base: 15_000, grade: 'medium', description: '编辑文件' },
move_file: { base: 15_000, grade: 'medium', description: '移动文件' }, move_file: { base: 15_000, grade: 'medium', description: '移动文件' },
tree: { base: 15_000, grade: 'medium', description: '目录树' }, tree: { base: 15_000, grade: 'medium', description: '目录树' },
diff_files: { base: 15_000, grade: 'medium', description: '文件差异' },
session_read: { base: 10_000, grade: 'medium', description: '会话读取' }, session_read: { base: 10_000, grade: 'medium', description: '会话读取' },
web_search: { base: 30_000, grade: 'medium', description: '网页搜索' }, web_search: { base: 30_000, grade: 'medium', description: '网页搜索' },
git: { base: 30_000, grade: 'medium', description: 'Git 操作' }, git: { base: 30_000, grade: 'medium', description: 'Git 操作' },
// slow (15-60秒) // slow (15-60秒)
read_file: { base: 30_000, grade: 'slow', description: '读取文件' }, read_file: { base: 30_000, grade: 'slow', description: '读取文件' },
copy_file: { base: 30_000, grade: 'slow', description: '复制文件' }, copy_file: { base: 30_000, grade: 'slow', description: '复制文件' },
replace_in_files: { base: 30_000, grade: 'slow', description: '批量替换' },
search_files: { base: 60_000, grade: 'slow', description: '搜索文件' }, search_files: { base: 60_000, grade: 'slow', description: '搜索文件' },
read_multiple_files: { base: 60_000, grade: 'slow', description: '批量读取' }, read_multiple_files: { base: 60_000, grade: 'slow', description: '批量读取' },
web_fetch: { base: 60_000, grade: 'slow', description: '网页抓取' }, web_fetch: { base: 60_000, grade: 'slow', description: '网页抓取' },
@@ -375,9 +365,7 @@ function extractReadPath(call: ToolCall): string | null {
case 'read_file': return String(args.path || ''); case 'read_file': return String(args.path || '');
case 'list_directory': return String(args.path || ''); case 'list_directory': return String(args.path || '');
case 'search_files': return String(args.path || ''); case 'search_files': return String(args.path || '');
case 'get_file_info': return String(args.path || '');
case 'tree': return String(args.path || ''); case 'tree': return String(args.path || '');
case 'diff_files': return String(args.file1 || args.file2 || '');
case 'read_multiple_files': return null; // 多文件,难以精确判断 case 'read_multiple_files': return null; // 多文件,难以精确判断
default: return null; default: return null;
} }
@@ -412,10 +400,7 @@ function extractAffectedPath(call: ToolCall): string | null {
case 'create_directory': return String(args.path || ''); case 'create_directory': return String(args.path || '');
case 'move_file': return String(args.source || ''); case 'move_file': return String(args.source || '');
case 'copy_file': return String(args.source || ''); case 'copy_file': return String(args.source || '');
case 'get_file_info': return String(args.path || '');
case 'tree': return String(args.path || ''); case 'tree': return String(args.path || '');
case 'diff_files': return String(args.file1 || args.file2 || '');
case 'replace_in_files': return String(args.path || '');
case 'read_multiple_files': return null; // 多文件,难以精确判断 case 'read_multiple_files': return null; // 多文件,难以精确判断
case 'download_file': return null; // 下载不依赖本地文件 case 'download_file': return null; // 下载不依赖本地文件
case 'compress': return String(args.path || ''); case 'compress': return String(args.path || '');
@@ -444,7 +429,6 @@ function validateToolArgs(toolName: string, args: Record<string, unknown>): stri
case 'write_file': case 'write_file':
case 'delete_file': case 'delete_file':
case 'create_directory': case 'create_directory':
case 'get_file_info':
case 'tree': case 'tree':
case 'compress': case 'compress':
if (!getString('path')) return `${toolName} 缺少有效的 path 参数`; if (!getString('path')) return `${toolName} 缺少有效的 path 参数`;
@@ -478,10 +462,6 @@ function validateToolArgs(toolName: string, args: Record<string, unknown>): stri
if (args.new_text === undefined || args.new_text === null) if (args.new_text === undefined || args.new_text === null)
return 'edit_file 缺少 new_text 参数'; return 'edit_file 缺少 new_text 参数';
break; break;
case 'diff_files':
if (!getString('file1')) return 'diff_files 缺少 file1 参数';
if (!getString('file2')) return 'diff_files 缺少 file2 参数';
break;
} }
return null; return null;
} }
@@ -490,11 +470,10 @@ function validateToolArgs(toolName: string, args: Record<string, unknown>): stri
const VALID_TOOL_NAMES = new Set([ const VALID_TOOL_NAMES = new Set([
'read_file', 'write_file', 'list_directory', 'search_files', 'create_directory', 'read_file', 'write_file', 'list_directory', 'search_files', 'create_directory',
'delete_file', 'run_command', 'move_file', 'copy_file', 'web_fetch', 'web_search', 'delete_file', 'run_command', 'move_file', 'copy_file', 'web_fetch', 'web_search',
'edit_file', 'get_file_info', 'tree', 'download_file', 'diff_files', 'edit_file', 'tree', 'download_file',
'replace_in_files', 'read_multiple_files', 'git', 'compress', 'read_multiple_files', 'git', 'compress',
'memory', 'session_list', 'session_read', 'memory', 'session_list', 'session_read',
'datetime', 'calculator', 'calculator'
'random', 'uuid', 'json_format', 'hash'
]); ]);
/** /**
@@ -684,13 +663,10 @@ const CACHE_TTL_MAP: Record<string, number> = {
search_files: 2 * 60_000, search_files: 2 * 60_000,
browser_screenshot: 60_000, browser_screenshot: 60_000,
browser_extract: 5 * 60_000, browser_extract: 5 * 60_000,
get_file_info: 2 * 60_000,
tree: 2 * 60_000, tree: 2 * 60_000,
git: 30_000, // git status 30s 过期 git: 30_000, // git status 30s 过期
datetime: 5_000, // 时间 5s 过期
session_list: 60_000, session_list: 60_000,
session_read: 60_000, session_read: 60_000,
diff_files: 2 * 60_000,
memory: 10_000, // M2: 记忆搜索 10s 过期(短TTL,避免 add 后搜索过期) memory: 10_000, // M2: 记忆搜索 10s 过期(短TTL,避免 add 后搜索过期)
default: 60_000, // 默认 60s,不再永久缓存 default: 60_000, // 默认 60s,不再永久缓存
}; };
@@ -961,18 +937,6 @@ export function formatToolResultForModel(toolName: string, result: ToolResult):
}); });
} }
case 'get_file_info': {
return JSON.stringify({
success: true,
message: `${(result as any).type === 'directory' ? '目录' : '文件'}${result.path}`,
path: result.path,
type: (result as any).type,
size: (result as any).size,
modified: (result as any).modified,
created: (result as any).created,
});
}
case 'tree': { case 'tree': {
return JSON.stringify({ return JSON.stringify({
success: true, success: true,
@@ -1887,8 +1851,8 @@ async function handleExecuting(
// R79: 路径沙箱验证 — 确保文件操作不超出工作空间边界 // R79: 路径沙箱验证 — 确保文件操作不超出工作空间边界
const FILE_PATH_TOOLS = new Set([ const FILE_PATH_TOOLS = new Set([
'read_file', 'write_file', 'edit_file', 'delete_file', 'create_directory', 'read_file', 'write_file', 'edit_file', 'delete_file', 'create_directory',
'list_directory', 'search_files', 'get_file_info', 'tree', 'compress', 'list_directory', 'search_files', 'tree', 'compress',
'move_file', 'copy_file', 'replace_in_files', 'download_file', 'move_file', 'copy_file', 'download_file',
]); ]);
if (FILE_PATH_TOOLS.has(call.function.name)) { if (FILE_PATH_TOOLS.has(call.function.name)) {
const workspaceDir = getWorkspaceDirPath(); const workspaceDir = getWorkspaceDirPath();
-4
View File
@@ -319,10 +319,6 @@ export function scoreMessageImportance(msg: OllamaMessage): number {
if (/read_file|list_directory|tree/.test(msg.tool_name)) { if (/read_file|list_directory|tree/.test(msg.tool_name)) {
score += 1; score += 1;
} }
// 时间/计算类工具结果:低价值(时效性强,很快过期)
if (/datetime|random|uuid|hash/.test(msg.tool_name)) {
score -= 2;
}
} }
// 长度加分:长消息通常包含更多信息 // 长度加分:长消息通常包含更多信息
+265 -131
View File
@@ -196,20 +196,6 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
} }
} }
}, },
{
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', type: 'function',
function: { function: {
@@ -241,39 +227,6 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
} }
} }
}, },
{
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', type: 'function',
function: { function: {
@@ -590,17 +543,269 @@ TIP: Use remove_batch when deleting multiple entries — it's much more efficien
{ {
type: 'function', type: 'function',
function: { function: {
name: 'datetime', name: 'calculator',
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).', 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: '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"}
- remove_batch: {"action":"remove_batch","old_texts":["substring1","substring2","substring3"]} Batch delete multiple entries in one call
- 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.
TIP: Use remove_batch when deleting multiple entries it's much more efficient than calling remove repeatedly.`,
parameters: {
type: 'object',
required: ['action'],
properties: {
action: { type: 'string', enum: ['search', 'add', 'replace', 'remove', 'remove_batch', 'read_all'], description: 'Which operation to perform. Note: "add" requires "type"+"content", "replace" requires "old_text"+"new_content", "remove" requires "old_text", "remove_batch" requires "old_texts" (array), "search" requires "query".' },
query: { type: 'string', description: '[search REQUIRED] 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. Must be ≥5 chars. Use exact text from read_all for best results.' },
old_texts: { type: 'array', items: { type: 'string' }, description: '[remove_batch REQUIRED] Array of substrings to identify multiple entries for batch deletion. Each must be ≥5 chars.' },
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: { parameters: {
type: 'object', type: 'object',
properties: { properties: {
format: { type: 'string', enum: ['full', 'iso', 'unix', 'date', 'time'], description: 'Output format. Default: full (all fields).' }, limit: { type: 'integer', description: 'Max sessions to return. Default: 20.' },
timezone: { type: 'string', description: 'IANA timezone name (e.g., "Asia/Shanghai", "America/New_York"). Default: system timezone.' } 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_undone', 'mark_all_done'], description: 'status=查看当前进度, mark_done=标记步骤完成, mark_undone=撤销步骤完成标记, 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', type: 'function',
function: { function: {
@@ -614,68 +819,6 @@ TIP: Use remove_batch when deleting multiple entries — it's much more efficien
} }
} }
} }
},
{
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.' }
}
}
}
} }
]; ];
@@ -684,7 +827,7 @@ TIP: Use remove_batch when deleting multiple entries — it's much more efficien
const MODE_TOOLS = [ const MODE_TOOLS = [
'run_command', 'run_command',
'write_file', 'create_directory', 'delete_file', 'write_file', 'create_directory', 'delete_file',
'edit_file', 'replace_in_files', 'move_file', 'copy_file', 'edit_file', 'move_file', 'copy_file',
'download_file', 'compress', 'download_file', 'compress',
]; ];
@@ -730,14 +873,13 @@ let enabledTools: Set<string> = new Set([
'write_file', 'create_directory', 'delete_file', 'write_file', 'create_directory', 'delete_file',
'run_command', 'run_command',
'move_file', 'copy_file', 'web_fetch', 'web_search', 'edit_file', 'move_file', 'copy_file', 'web_fetch', 'web_search', 'edit_file',
'get_file_info', 'tree', 'download_file', 'diff_files', 'replace_in_files', 'tree', 'download_file',
'read_multiple_files', 'git', 'compress', 'read_multiple_files', 'git', 'compress',
'memory', 'memory',
'session_list', 'session_read', 'spawn_task', 'session_list', 'session_read', 'spawn_task',
'browser_open', 'browser_screenshot', 'browser_evaluate', 'browser_extract', 'browser_open', 'browser_screenshot', 'browser_evaluate', 'browser_extract',
'browser_click', 'browser_type', 'browser_scroll', 'browser_wait', 'browser_close', 'browser_click', 'browser_type', 'browser_scroll', 'browser_wait', 'browser_close',
'datetime', 'calculator', 'calculator'
'random', 'uuid', 'json_format', 'hash'
]); ]);
export function setToolEnabled(toolName: string, enabled: boolean): void { export function setToolEnabled(toolName: string, enabled: boolean): void {
@@ -784,11 +926,8 @@ const TOOL_KEYWORDS: Record<string, string[]> = {
web_fetch: ['fetch', 'url', '网页', '抓取', '获取'], web_fetch: ['fetch', 'url', '网页', '抓取', '获取'],
web_search: ['search', 'web', '搜索', '联网', '查', 'google', 'bing'], web_search: ['search', 'web', '搜索', '联网', '查', 'google', 'bing'],
edit_file: ['edit', 'replace', '编辑', '替换', '修改'], edit_file: ['edit', 'replace', '编辑', '替换', '修改'],
get_file_info: ['info', 'stat', '信息', '属性', '详情'],
tree: ['tree', '结构', '树', '目录结构'], tree: ['tree', '结构', '树', '目录结构'],
download_file: ['download', '下载'], download_file: ['download', '下载'],
diff_files: ['diff', 'compare', '对比', '差异'],
replace_in_files: ['replace', 'batch', '批量替换'],
read_multiple_files: ['read', 'multiple', '批量读取'], read_multiple_files: ['read', 'multiple', '批量读取'],
git: ['git', 'commit', 'push', 'pull', 'branch', '仓库'], git: ['git', 'commit', 'push', 'pull', 'branch', '仓库'],
compress: ['compress', 'zip', 'tar', '压缩', '解压', 'extract'], compress: ['compress', 'zip', 'tar', '压缩', '解压', 'extract'],
@@ -806,12 +945,7 @@ const TOOL_KEYWORDS: Record<string, string[]> = {
browser_scroll: ['scroll', '滚动'], browser_scroll: ['scroll', '滚动'],
browser_wait: ['wait', '等待'], browser_wait: ['wait', '等待'],
browser_close: ['close', '关闭浏览器'], browser_close: ['close', '关闭浏览器'],
datetime: ['time', 'date', '时间', '日期'],
calculator: ['calculate', 'math', '计算', '算'], calculator: ['calculate', 'math', '计算', '算'],
random: ['random', '随机'],
uuid: ['uuid', 'guid', '唯一'],
json_format: ['json', 'format', '格式化'],
hash: ['hash', 'sha', 'md5', '哈希'],
}; };
/** R55: 根据用户查询语义过滤工具定义,减少 token 占用 */ /** R55: 根据用户查询语义过滤工具定义,减少 token 占用 */
@@ -1611,8 +1745,8 @@ export function getToolIcon(name: string): string {
read_file: '📄', write_file: '✏️', list_directory: '📁', read_file: '📄', write_file: '✏️', list_directory: '📁',
search_files: '🔍', create_directory: '📂', delete_file: '🗑️', run_command: '💻', search_files: '🔍', create_directory: '📂', delete_file: '🗑️', run_command: '💻',
move_file: '📦', copy_file: '📋', web_fetch: '🌐', move_file: '📦', copy_file: '📋', web_fetch: '🌐',
edit_file: '✂️', get_file_info: '️', tree: '🌳', download_file: '⬇️', edit_file: '✂️', tree: '🌳', download_file: '⬇️',
diff_files: '🔀', replace_in_files: '🔄', read_multiple_files: '📚', read_multiple_files: '📚',
git: '🔖', compress: '🗜️', web_search: '🔍', git: '🔖', compress: '🗜️', web_search: '🔍',
memory: '🧠', memory: '🧠',
session_list: '📋', session_read: '📖', spawn_task: '🤖', session_list: '📋', session_read: '📖', spawn_task: '🤖',
@@ -1620,7 +1754,7 @@ export function getToolIcon(name: string): string {
browser_open: '🌐', browser_screenshot: '📸', browser_evaluate: '⚡', browser_extract: '📰', browser_open: '🌐', browser_screenshot: '📸', browser_evaluate: '⚡', browser_extract: '📰',
browser_click: '👆', browser_type: '⌨️', browser_scroll: '↕️', browser_close: '❌', browser_click: '👆', browser_type: '⌨️', browser_scroll: '↕️', browser_close: '❌',
browser_wait: '⏳', browser_wait: '⏳',
datetime: '🕐', calculator: '🔢', random: '🎲', uuid: '🔑', json_format: '📝', hash: '#️⃣' calculator: '🔢'
}; };
return icons[name] || '🔧'; return icons[name] || '🔧';
} }
@@ -1691,8 +1825,8 @@ export function formatToolName(name: string): string {
search_files: '搜索文件', create_directory: '创建目录', delete_file: '删除文件', search_files: '搜索文件', create_directory: '创建目录', delete_file: '删除文件',
run_command: '执行命令', move_file: '移动文件', copy_file: '复制文件', run_command: '执行命令', move_file: '移动文件', copy_file: '复制文件',
web_fetch: '网页抓取', edit_file: '编辑文件', web_fetch: '网页抓取', edit_file: '编辑文件',
get_file_info: '文件信息', tree: '目录树', download_file: '下载文件', tree: '目录树', download_file: '下载文件',
diff_files: '文件对比', replace_in_files: '批量替换', read_multiple_files: '批量读取', read_multiple_files: '批量读取',
git: 'Git 操作', compress: '压缩/解压', web_search: '联网搜索', git: 'Git 操作', compress: '压缩/解压', web_search: '联网搜索',
memory: '记忆管理', memory: '记忆管理',
session_list: '会话列表', session_read: '读取会话', spawn_task: '子代理委派', session_list: '会话列表', session_read: '读取会话', spawn_task: '子代理委派',
@@ -1700,7 +1834,7 @@ export function formatToolName(name: string): string {
browser_open: '打开网页', browser_screenshot: '浏览器截图', browser_evaluate: '执行JS', browser_extract: '提取内容', browser_open: '打开网页', browser_screenshot: '浏览器截图', browser_evaluate: '执行JS', browser_extract: '提取内容',
browser_click: '点击元素', browser_type: '输入文本', browser_scroll: '滚动页面', browser_close: '关闭浏览器', browser_click: '点击元素', browser_type: '输入文本', browser_scroll: '滚动页面', browser_close: '关闭浏览器',
browser_wait: '等待', browser_wait: '等待',
datetime: '日期时间', calculator: '计算器', random: '随机数', uuid: '生成UUID', json_format: 'JSON格式化', hash: '哈希计算' calculator: '计算器'
}; };
return names[name] || name; return names[name] || name;
} }