From 94e0a36981da750b3be2a54894410a1dcc5f8bc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=B4=AB=E5=BD=B1233?= Date: Thu, 16 Jul 2026 17:47:02 +0800 Subject: [PATCH] =?UTF-8?q?v0.16.12:=20=E5=88=A0=E9=99=A4=208=20=E4=B8=AA?= =?UTF-8?q?=E9=9D=9E=E5=BF=85=E8=A6=81=E5=B7=A5=E5=85=B7=20=E2=80=94=20get?= =?UTF-8?q?=5Ffile=5Finfo/diff=5Ffiles/replace=5Fin=5Ffiles/random/uuid/js?= =?UTF-8?q?on=5Fformat/hash/datetime?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 38 +- package-lock.json | 4 +- package.json | 2 +- src/main/ipc.ts | 26 +- src/main/menu.ts | 2 +- src/main/tool-handlers-fs.ts | 203 ----------- src/main/tool-handlers-system.ts | 203 ----------- src/main/tool-handlers.ts | 12 +- src/renderer/components/tools-modal.ts | 2 +- src/renderer/components/workspace-panel.ts | 8 +- src/renderer/index.html | 36 +- src/renderer/main.ts | 2 +- src/renderer/services/agent-engine.ts | 56 +-- src/renderer/services/context-manager.ts | 4 - src/renderer/services/tool-registry.ts | 396 ++++++++++++++------- 15 files changed, 301 insertions(+), 693 deletions(-) diff --git a/README.md b/README.md index 9645f91..350ee38 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@

- version + version electron typescript 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 @@ ## 🔧 工具清单

-📁 文件系统(16 个) +📁 文件系统(13 个) | 工具 | 功能 | |------|------| @@ -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 @@
-🕐 系统工具(6 个) +🕐 系统工具(1 个) | 工具 | 功能 | |------|------| -| `datetime` | 获取系统精确时间(ISO/Unix/日期/时间/完整) | | `calculator` | 安全数学计算(+ - * / ** % (),递归下降解析器) | -| `random` | 随机生成(整数/浮点/抽取/随机字符串) | -| `uuid` | 生成 UUID v4 唯一标识符 | -| `json_format` | JSON 格式化 + 语法验证 + 键排序 | -| `hash` | 哈希计算(MD5/SHA1/SHA256/SHA384/SHA512) |
@@ -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
-📁 File System (16) +📁 File System (13) | 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 安装包
-🕐 System Tools (6) +🕐 System Tools (1) | 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) |
@@ -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 diff --git a/package-lock.json b/package-lock.json index ef6323a..701212d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index bd6906f..6155c54 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/main/ipc.ts b/src/main/ipc.ts index bcb1c4d..41e6949 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -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): 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 { 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; diff --git a/src/main/menu.ts b/src/main/menu.ts index c1da712..58d4f63 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -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() }); diff --git a/src/main/tool-handlers-fs.ts b/src/main/tool-handlers-fs.ts index 940fdda..83b2340 100644 --- a/src/main/tool-handlers-fs.ts +++ b/src/main/tool-handlers-fs.ts @@ -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 { - 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 { 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 { - 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 { - 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 { - 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 { try { const maxChars = params.max_chars_per_file || 0; // 0 = 不截断 diff --git a/src/main/tool-handlers-system.ts b/src/main/tool-handlers-system.ts index a15ba02..0505edb 100644 --- a/src/main/tool-handlers-system.ts +++ b/src/main/tool-handlers-system.ts @@ -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; - - 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 = {}; - 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 }; - } -} diff --git a/src/main/tool-handlers.ts b/src/main/tool-handlers.ts index 3ba5833..3881ace 100644 --- a/src/main/tool-handlers.ts +++ b/src/main/tool-handlers.ts @@ -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 个) diff --git a/src/renderer/components/tools-modal.ts b/src/renderer/components/tools-modal.ts index f7aaa40..9c01b28 100644 --- a/src/renderer/components/tools-modal.ts +++ b/src/renderer/components/tools-modal.ts @@ -21,7 +21,7 @@ const MODE_NAMES: Record = { 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', ]; diff --git a/src/renderer/components/workspace-panel.ts b/src/renderer/components/workspace-panel.ts index d4d6e98..8175fd4 100644 --- a/src/renderer/components/workspace-panel.ts +++ b/src/renderer/components/workspace-panel.ts @@ -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: '🧠', diff --git a/src/renderer/index.html b/src/renderer/index.html index 7be3b82..27381a3 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -28,7 +28,7 @@
Metona Ollama - v0.16.11 + v0.16.12