v0.16.12: 删除 8 个非必要工具 — get_file_info/diff_files/replace_in_files/random/uuid/json_format/hash/datetime
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
</p>
|
||||
|
||||
<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/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">
|
||||
@@ -37,7 +37,7 @@
|
||||
| 🤖 | **ReAct Agent Loop** | 始终开启的唯一对话模式。8 状态机(INIT→THINKING→PARSING→EXECUTING→OBSERVING→REFLECTING→COMPRESSING→TERMINATED),最大 85 轮(可配置),智能重试(永久错误立即返回、瞬态错误指数退避),工具去重,智能路径依赖串行化,看门狗超时保护 |
|
||||
| 🛡️ | **5 层抗幻觉系统** | 提示词加固 → 任务感知 → 中途检测(中英双语规则覆盖全部工具类别)→ 进度锚点 → 完成闸门(6 项检查,幻觉/注入 → 阻断,质量/效率 → 咨询) |
|
||||
| 📋 | **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 条,对话结束自动提取,严格格式校验 |
|
||||
| 📋 | **自定义文件** | SOUL.md(人格,不可压缩)+ AGENT.md(行为准则,内置 fallback)+ USER.md(用户画像,仅工作空间读取,不存在则不注入) |
|
||||
| 🌐 | **MCP 协议扩展** | JSON-RPC 2.0 over stdio,动态工具发现,Shadowing 防护 |
|
||||
@@ -59,7 +59,7 @@
|
||||
## 🔧 工具清单
|
||||
|
||||
<details>
|
||||
<summary><strong>📁 文件系统(16 个)</strong></summary>
|
||||
<summary><strong>📁 文件系统(13 个)</strong></summary>
|
||||
|
||||
| 工具 | 功能 |
|
||||
|------|------|
|
||||
@@ -72,11 +72,8 @@
|
||||
| `move_file` | 移动/重命名文件 |
|
||||
| `copy_file` | 复制文件 |
|
||||
| `edit_file` | 编辑文件(支持正则替换) |
|
||||
| `get_file_info` | 获取文件元信息 |
|
||||
| `tree` | 目录树结构(默认5层深度) |
|
||||
| `download_file` | 下载文件 |
|
||||
| `diff_files` | 文件差异对比 |
|
||||
| `replace_in_files` | 批量替换 |
|
||||
| `read_multiple_files` | 批量读取(50文件/10KB) |
|
||||
| `compress` | 压缩文件/目录 |
|
||||
|
||||
@@ -150,16 +147,11 @@
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>🕐 系统工具(6 个)</strong></summary>
|
||||
<summary><strong>🕐 系统工具(1 个)</strong></summary>
|
||||
|
||||
| 工具 | 功能 |
|
||||
|------|------|
|
||||
| `datetime` | 获取系统精确时间(ISO/Unix/日期/时间/完整) |
|
||||
| `calculator` | 安全数学计算(+ - * / ** % (),递归下降解析器) |
|
||||
| `random` | 随机生成(整数/浮点/抽取/随机字符串) |
|
||||
| `uuid` | 生成 UUID v4 唯一标识符 |
|
||||
| `json_format` | JSON 格式化 + 语法验证 + 键排序 |
|
||||
| `hash` | 哈希计算(MD5/SHA1/SHA256/SHA384/SHA512) |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -185,7 +177,7 @@
|
||||
↓
|
||||
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) → 观察结果 → 反思 → 循环 / 最终回答
|
||||
```
|
||||
@@ -253,7 +245,7 @@ npm start
|
||||
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 |
|
||||
| 🛡️ | **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) |
|
||||
| 🔧 | **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 |
|
||||
| 📋 | **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 |
|
||||
@@ -308,7 +300,7 @@ npm run dist # 构建 Windows 安装包
|
||||
## 🔧 Tool List
|
||||
|
||||
<details>
|
||||
<summary><strong>📁 File System (16)</strong></summary>
|
||||
<summary><strong>📁 File System (13)</strong></summary>
|
||||
|
||||
| Tool | Function |
|
||||
|------|------|
|
||||
@@ -321,11 +313,8 @@ npm run dist # 构建 Windows 安装包
|
||||
| `move_file` | Move/rename file |
|
||||
| `copy_file` | Copy file |
|
||||
| `edit_file` | Edit file (regex support) |
|
||||
| `get_file_info` | Get file metadata |
|
||||
| `tree` | Directory tree (default 5 levels) |
|
||||
| `download_file` | Download file |
|
||||
| `diff_files` | File diff comparison |
|
||||
| `replace_in_files` | Batch replace |
|
||||
| `read_multiple_files` | Batch read (50 files/10KB) |
|
||||
| `compress` | Compress file/directory |
|
||||
|
||||
@@ -398,16 +387,11 @@ npm run dist # 构建 Windows 安装包
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>🕐 System Tools (6)</strong></summary>
|
||||
<summary><strong>🕐 System Tools (1)</strong></summary>
|
||||
|
||||
| Tool | Function |
|
||||
|------|------|
|
||||
| `datetime` | Get precise system time (ISO/Unix/date/time/full) |
|
||||
| `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>
|
||||
|
||||
@@ -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)
|
||||
↓
|
||||
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
|
||||
```
|
||||
@@ -501,7 +485,7 @@ npm start
|
||||
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
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "metona-ollama-desktop",
|
||||
"version": "0.16.11",
|
||||
"version": "0.16.12",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "metona-ollama-desktop",
|
||||
"version": "0.16.11",
|
||||
"version": "0.16.12",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "metona-ollama-desktop",
|
||||
"version": "0.16.11",
|
||||
"version": "0.16.12",
|
||||
"description": "Metona Ollama - TypeScript + Electron 桌面 AI 聊天客户端",
|
||||
"main": "dist/main/main.js",
|
||||
"author": "thzxx",
|
||||
|
||||
+1
-25
@@ -36,20 +36,12 @@ import {
|
||||
handleWebFetch,
|
||||
handleWebSearch,
|
||||
handleEditFile,
|
||||
handleGetFileInfo,
|
||||
handleTree,
|
||||
handleDownloadFile,
|
||||
handleDiffFiles,
|
||||
handleReplaceInFiles,
|
||||
handleReadMultipleFiles,
|
||||
handleGit,
|
||||
handleCompress,
|
||||
handleDateTime,
|
||||
handleCalculator,
|
||||
handleRandom,
|
||||
handleUUID,
|
||||
handleJsonFormat,
|
||||
handleHash
|
||||
handleCalculator
|
||||
} from './tool-handlers.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';
|
||||
@@ -80,20 +72,12 @@ function summarizeResult(toolName: string, result: Record<string, unknown>): str
|
||||
case 'web_fetch': return `${result.status} | ${result.length} chars`;
|
||||
case 'web_search': return `[${String(result._mode) === 'searxng' ? 'SearXNG' : '内置'}] "${result.query}" → ${result.total} 条结果`;
|
||||
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 '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 'git': return `${result.action} ✓`;
|
||||
case 'compress': return `${result.action} → ${result.archive || result.destination}`;
|
||||
case 'datetime': return `${result.date || result.iso}`;
|
||||
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 '完成';
|
||||
}
|
||||
}
|
||||
@@ -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_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 '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 '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 '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 'datetime': result = handleDateTime(args as { format?: string; timezone?: 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 控制(增强版)
|
||||
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;
|
||||
|
||||
+1
-1
@@ -101,7 +101,7 @@ export function createMenu(): void {
|
||||
dialog.showMessageBox(mainWindow!, {
|
||||
type: 'info',
|
||||
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',
|
||||
icon: getIconPath()
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { spawn } from 'child_process';
|
||||
import { checkPathAllowed } from './tool-security.js';
|
||||
import { sendLog, resolvePath, type ToolResult } from './tool-handlers-shared.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> {
|
||||
try {
|
||||
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> {
|
||||
try {
|
||||
const maxChars = params.max_chars_per_file || 0; // 0 = 不截断
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { spawn } from 'child_process';
|
||||
import * as crypto from 'crypto';
|
||||
import { checkPathAllowed, checkCommandAllowed } from './tool-security.js';
|
||||
import { mainWindow } from './main.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) ──
|
||||
export function handleCalculator(params: { expression: string }): ToolResult {
|
||||
try {
|
||||
@@ -1710,126 +1630,3 @@ function safeCalc(expr: string): number {
|
||||
if (!isFinite(result)) throw new Error('计算结果为无穷大');
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
export { sendLog, resolvePath, type ToolResult } from './tool-handlers-shared.js';
|
||||
|
||||
// 文件系统操作(15 个)
|
||||
// 文件系统操作(11 个)
|
||||
export {
|
||||
handleReadFile,
|
||||
handleWriteFile,
|
||||
@@ -17,14 +17,11 @@ export {
|
||||
handleMoveFile,
|
||||
handleCopyFile,
|
||||
handleEditFile,
|
||||
handleGetFileInfo,
|
||||
handleTree,
|
||||
handleDiffFiles,
|
||||
handleReplaceInFiles,
|
||||
handleReadMultipleFiles,
|
||||
} from './tool-handlers-fs.js';
|
||||
|
||||
// 系统与网络操作(8 个)
|
||||
// 系统与网络操作(7 个)
|
||||
export {
|
||||
handleRunCommand,
|
||||
killToolProcess,
|
||||
@@ -32,12 +29,7 @@ export {
|
||||
handleWebSearch,
|
||||
handleDownloadFile,
|
||||
handleCompress,
|
||||
handleDateTime,
|
||||
handleCalculator,
|
||||
handleRandom,
|
||||
handleUUID,
|
||||
handleJsonFormat,
|
||||
handleHash,
|
||||
} from './tool-handlers-system.js';
|
||||
|
||||
// Git 操作(1 个)
|
||||
|
||||
@@ -21,7 +21,7 @@ const MODE_NAMES: Record<string, string> = {
|
||||
const MANAGED_TOOLS = [
|
||||
'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',
|
||||
];
|
||||
|
||||
|
||||
@@ -996,8 +996,8 @@ function getToolDisplayName(name: 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: '批量读取',
|
||||
edit_file: '编辑文件', tree: '目录树', download_file: '下载文件',
|
||||
read_multiple_files: '批量读取',
|
||||
git: 'Git 操作', compress: '压缩/解压', web_search: '联网搜索',
|
||||
memory: '记忆管理',
|
||||
|
||||
@@ -1013,8 +1013,8 @@ function getToolIcon(name: 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: '📚',
|
||||
edit_file: '✂️', tree: '🌳', download_file: '⬇️',
|
||||
read_multiple_files: '📚',
|
||||
git: '🔖', compress: '🗜️', web_search: '🔍',
|
||||
memory: '🧠',
|
||||
|
||||
|
||||
+2
-34
@@ -28,7 +28,7 @@
|
||||
<div class="header-left">
|
||||
<img class="logo" src="./assets/icons/llama.png" alt="logo" />
|
||||
<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="使用帮助">
|
||||
<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"/>
|
||||
@@ -438,7 +438,7 @@
|
||||
<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><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 个):git(17 个子操作,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 个):git(17 个子操作,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>📋 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>
|
||||
@@ -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-desc">执行 shell 命令,支持超时配置</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-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>
|
||||
@@ -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-desc">从 URL 下载文件到本地</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-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>
|
||||
@@ -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-desc">关闭 Agent 浏览器并释放资源</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-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>
|
||||
<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-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>
|
||||
|
||||
@@ -458,7 +458,7 @@ async function init(): Promise<void> {
|
||||
// 如果旧 toolModes 中所有被管理工具都是 auto,则迁移为 auto;否则 confirm
|
||||
const allAuto = [
|
||||
'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',
|
||||
].every(t => savedModes[t] === 'auto');
|
||||
globalMode = allAuto ? 'auto' : 'confirm';
|
||||
|
||||
@@ -159,18 +159,16 @@ function getOSEnvironment() {
|
||||
|
||||
/** 始终可并行的只读/独立工具 */
|
||||
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',
|
||||
'memory', 'session_list', 'session_read',
|
||||
'diff_files',
|
||||
'datetime', 'calculator',
|
||||
'random', 'uuid', 'json_format', 'hash',
|
||||
'calculator',
|
||||
]);
|
||||
|
||||
/** D4: 有副作用的工具 — 同轮次去重时不返回缓存,需实际执行 */
|
||||
const SIDE_EFFECT_TOOLS = new Set([
|
||||
'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',
|
||||
'browser_open', 'browser_click', 'browser_type', 'browser_close',
|
||||
]);
|
||||
@@ -190,14 +188,8 @@ const _pendingMemoryTimers: ReturnType<typeof setTimeout>[] = [];
|
||||
/** R3/R108: 工具超时配置(基础超时 + 动态调整) */
|
||||
const TOOL_TIMEOUT_MAP: Record<string, { base: number; grade: 'fast' | 'medium' | 'slow' | 'long'; description: string }> = {
|
||||
// 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: '计算器' },
|
||||
json_format: { base: 3_000, grade: 'fast', description: 'JSON 格式化' },
|
||||
hash: { base: 3_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: '记忆操作' },
|
||||
session_list: { base: 5_000, grade: 'fast', description: '会话列表' },
|
||||
|
||||
@@ -208,7 +200,6 @@ const TOOL_TIMEOUT_MAP: Record<string, { base: number; grade: 'fast' | 'medium'
|
||||
edit_file: { base: 15_000, grade: 'medium', description: '编辑文件' },
|
||||
move_file: { 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: '会话读取' },
|
||||
web_search: { base: 30_000, grade: 'medium', description: '网页搜索' },
|
||||
git: { base: 30_000, grade: 'medium', description: 'Git 操作' },
|
||||
@@ -216,7 +207,6 @@ const TOOL_TIMEOUT_MAP: Record<string, { base: number; grade: 'fast' | 'medium'
|
||||
// slow (15-60秒)
|
||||
read_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: '搜索文件' },
|
||||
read_multiple_files: { 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 'list_directory': 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 'diff_files': return String(args.file1 || args.file2 || '');
|
||||
case 'read_multiple_files': return null; // 多文件,难以精确判断
|
||||
default: return null;
|
||||
}
|
||||
@@ -412,10 +400,7 @@ function extractAffectedPath(call: ToolCall): string | null {
|
||||
case 'create_directory': return String(args.path || '');
|
||||
case 'move_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 'diff_files': return String(args.file1 || args.file2 || '');
|
||||
case 'replace_in_files': return String(args.path || '');
|
||||
case 'read_multiple_files': return null; // 多文件,难以精确判断
|
||||
case 'download_file': return null; // 下载不依赖本地文件
|
||||
case 'compress': return String(args.path || '');
|
||||
@@ -444,7 +429,6 @@ function validateToolArgs(toolName: string, args: Record<string, unknown>): stri
|
||||
case 'write_file':
|
||||
case 'delete_file':
|
||||
case 'create_directory':
|
||||
case 'get_file_info':
|
||||
case 'tree':
|
||||
case 'compress':
|
||||
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)
|
||||
return 'edit_file 缺少 new_text 参数';
|
||||
break;
|
||||
case 'diff_files':
|
||||
if (!getString('file1')) return 'diff_files 缺少 file1 参数';
|
||||
if (!getString('file2')) return 'diff_files 缺少 file2 参数';
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -490,11 +470,10 @@ function validateToolArgs(toolName: string, args: Record<string, unknown>): stri
|
||||
const VALID_TOOL_NAMES = new Set([
|
||||
'read_file', 'write_file', 'list_directory', 'search_files', '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',
|
||||
'edit_file', 'tree', 'download_file',
|
||||
'read_multiple_files', 'git', 'compress',
|
||||
'memory', 'session_list', 'session_read',
|
||||
'datetime', 'calculator',
|
||||
'random', 'uuid', 'json_format', 'hash'
|
||||
'calculator'
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -684,13 +663,10 @@ const CACHE_TTL_MAP: Record<string, number> = {
|
||||
search_files: 2 * 60_000,
|
||||
browser_screenshot: 60_000,
|
||||
browser_extract: 5 * 60_000,
|
||||
get_file_info: 2 * 60_000,
|
||||
tree: 2 * 60_000,
|
||||
git: 30_000, // git status 30s 过期
|
||||
datetime: 5_000, // 时间 5s 过期
|
||||
session_list: 60_000,
|
||||
session_read: 60_000,
|
||||
diff_files: 2 * 60_000,
|
||||
memory: 10_000, // M2: 记忆搜索 10s 过期(短TTL,避免 add 后搜索过期)
|
||||
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': {
|
||||
return JSON.stringify({
|
||||
success: true,
|
||||
@@ -1887,8 +1851,8 @@ async function handleExecuting(
|
||||
// R79: 路径沙箱验证 — 确保文件操作不超出工作空间边界
|
||||
const FILE_PATH_TOOLS = new Set([
|
||||
'read_file', 'write_file', 'edit_file', 'delete_file', 'create_directory',
|
||||
'list_directory', 'search_files', 'get_file_info', 'tree', 'compress',
|
||||
'move_file', 'copy_file', 'replace_in_files', 'download_file',
|
||||
'list_directory', 'search_files', 'tree', 'compress',
|
||||
'move_file', 'copy_file', 'download_file',
|
||||
]);
|
||||
if (FILE_PATH_TOOLS.has(call.function.name)) {
|
||||
const workspaceDir = getWorkspaceDirPath();
|
||||
|
||||
@@ -319,10 +319,6 @@ export function scoreMessageImportance(msg: OllamaMessage): number {
|
||||
if (/read_file|list_directory|tree/.test(msg.tool_name)) {
|
||||
score += 1;
|
||||
}
|
||||
// 时间/计算类工具结果:低价值(时效性强,很快过期)
|
||||
if (/datetime|random|uuid|hash/.test(msg.tool_name)) {
|
||||
score -= 2;
|
||||
}
|
||||
}
|
||||
|
||||
// 长度加分:长消息通常包含更多信息
|
||||
|
||||
@@ -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',
|
||||
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',
|
||||
function: {
|
||||
@@ -590,17 +543,269 @@ TIP: Use remove_batch when deleting multiple entries — it's much more efficien
|
||||
{
|
||||
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).',
|
||||
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: '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: {
|
||||
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.' }
|
||||
limit: { type: 'integer', description: 'Max sessions to return. Default: 20.' },
|
||||
search: { type: 'string', description: 'Filter sessions by title keyword.' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'session_read',
|
||||
description: 'Read the messages from a previous chat session. Use when the user references something from a past conversation.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
required: ['session_id'],
|
||||
properties: {
|
||||
session_id: { type: 'string', description: 'The session ID to read.' },
|
||||
max_messages: { type: 'integer', description: 'Max messages to return. Default: 50.' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// ══════════════════════════════════════════════
|
||||
// v4.3 新增工具:子代理委派
|
||||
// ══════════════════════════════════════════════
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'spawn_task',
|
||||
description: 'Spawn a sub-agent to independently execute a task using read-only tools (file reading, web search, browser viewing, memory/session queries). Use this to parallelize independent research or analysis sub-tasks. The model is configured in Settings and cannot be overridden per call.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
required: ['task'],
|
||||
properties: {
|
||||
task: { type: 'string', description: 'The task description for the sub-agent to execute.' },
|
||||
context: { type: 'string', description: 'Optional additional context or reference data for the sub-agent.' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// ══════════════════════════════════════════════
|
||||
// 新增工具:Plan Mode 执行追踪
|
||||
// ══════════════════════════════════════════════
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'plan_track',
|
||||
description: 'Manage Plan Mode execution progress. Use this to mark plan steps as completed so you always know what remains. REQUIRED in Plan Mode — call plan_track after completing each step.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
required: ['action'],
|
||||
properties: {
|
||||
action: { type: 'string', enum: ['status', 'mark_done', 'mark_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',
|
||||
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 = [
|
||||
'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',
|
||||
];
|
||||
|
||||
@@ -730,14 +873,13 @@ let enabledTools: Set<string> = new Set([
|
||||
'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',
|
||||
'tree', 'download_file',
|
||||
'read_multiple_files', 'git', 'compress',
|
||||
'memory',
|
||||
'session_list', 'session_read', 'spawn_task',
|
||||
'browser_open', 'browser_screenshot', 'browser_evaluate', 'browser_extract',
|
||||
'browser_click', 'browser_type', 'browser_scroll', 'browser_wait', 'browser_close',
|
||||
'datetime', 'calculator',
|
||||
'random', 'uuid', 'json_format', 'hash'
|
||||
'calculator'
|
||||
]);
|
||||
|
||||
export function setToolEnabled(toolName: string, enabled: boolean): void {
|
||||
@@ -784,11 +926,8 @@ const TOOL_KEYWORDS: Record<string, string[]> = {
|
||||
web_fetch: ['fetch', 'url', '网页', '抓取', '获取'],
|
||||
web_search: ['search', 'web', '搜索', '联网', '查', 'google', 'bing'],
|
||||
edit_file: ['edit', 'replace', '编辑', '替换', '修改'],
|
||||
get_file_info: ['info', 'stat', '信息', '属性', '详情'],
|
||||
tree: ['tree', '结构', '树', '目录结构'],
|
||||
download_file: ['download', '下载'],
|
||||
diff_files: ['diff', 'compare', '对比', '差异'],
|
||||
replace_in_files: ['replace', 'batch', '批量替换'],
|
||||
read_multiple_files: ['read', 'multiple', '批量读取'],
|
||||
git: ['git', 'commit', 'push', 'pull', 'branch', '仓库'],
|
||||
compress: ['compress', 'zip', 'tar', '压缩', '解压', 'extract'],
|
||||
@@ -806,12 +945,7 @@ const TOOL_KEYWORDS: Record<string, string[]> = {
|
||||
browser_scroll: ['scroll', '滚动'],
|
||||
browser_wait: ['wait', '等待'],
|
||||
browser_close: ['close', '关闭浏览器'],
|
||||
datetime: ['time', 'date', '时间', '日期'],
|
||||
calculator: ['calculate', 'math', '计算', '算'],
|
||||
random: ['random', '随机'],
|
||||
uuid: ['uuid', 'guid', '唯一'],
|
||||
json_format: ['json', 'format', '格式化'],
|
||||
hash: ['hash', 'sha', 'md5', '哈希'],
|
||||
};
|
||||
|
||||
/** R55: 根据用户查询语义过滤工具定义,减少 token 占用 */
|
||||
@@ -1611,8 +1745,8 @@ export function getToolIcon(name: 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: '📚',
|
||||
edit_file: '✂️', tree: '🌳', download_file: '⬇️',
|
||||
read_multiple_files: '📚',
|
||||
git: '🔖', compress: '🗜️', web_search: '🔍',
|
||||
memory: '🧠',
|
||||
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_click: '👆', browser_type: '⌨️', browser_scroll: '↕️', browser_close: '❌',
|
||||
browser_wait: '⏳',
|
||||
datetime: '🕐', calculator: '🔢', random: '🎲', uuid: '🔑', json_format: '📝', hash: '#️⃣'
|
||||
calculator: '🔢'
|
||||
};
|
||||
return icons[name] || '🔧';
|
||||
}
|
||||
@@ -1691,8 +1825,8 @@ export function formatToolName(name: string): string {
|
||||
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: '批量读取',
|
||||
tree: '目录树', download_file: '下载文件',
|
||||
read_multiple_files: '批量读取',
|
||||
git: 'Git 操作', compress: '压缩/解压', web_search: '联网搜索',
|
||||
memory: '记忆管理',
|
||||
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_click: '点击元素', browser_type: '输入文本', browser_scroll: '滚动页面', browser_close: '关闭浏览器',
|
||||
browser_wait: '等待',
|
||||
datetime: '日期时间', calculator: '计算器', random: '随机数', uuid: '生成UUID', json_format: 'JSON格式化', hash: '哈希计算'
|
||||
calculator: '计算器'
|
||||
};
|
||||
return names[name] || name;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user