From 7c22405386a4c68410d0386ed4755f30088eb231 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Thu, 16 Apr 2026 10:09:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=85=A8=E9=9D=A2=E8=A1=A5=E5=85=85?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E6=89=A7=E8=A1=8C=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IPC 分发层:所有工具执行后自动记录成功/失败日志 - 新增 summarizeResult 函数:为每个工具生成可读的结果摘要 - move_file/copy_file/web_fetch:新增操作前后的详细日志 - 日志面板实时显示:工具名 + 结果摘要 + 错误详情 示例: 🔧 tool:execute move_file → 📦 move_file 完成 🔧 web_fetch ✓ → 200 | 5432 chars 🔧 edit_file ✗ → 未找到要替换的文本 --- src/main/ipc.ts | 74 ++++++++++++++++++++++++++++----------- src/main/tool-handlers.ts | 8 +++++ 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 72182f4..34c267c 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -38,6 +38,32 @@ import { import { getAllowedDirs, getBlockedDirs, setAllowedDirs } from './tool-security.js'; import { startProcess, killProcess, getWorkspaceDir, setWorkspaceDir, listWorkspaceDir } from './workspace.js'; +/** 工具结果摘要(用于日志面板) */ +function summarizeResult(toolName: string, result: Record): string { + switch (toolName) { + case 'read_file': return `${result.path} (${result.lines} 行${result.truncated ? ', 已截断' : ''})`; + case 'write_file': return `${result.path} (${result.bytesWritten}B${result.created ? ', 新建' : ''})`; + case 'list_directory': return `${result.path} (${result.total} 项${result.truncated ? ', 已截断' : ''})`; + case 'search_files': return `"${result.query}" → ${result.total_matches} 匹配 / ${result.total_files} 文件`; + case 'create_directory': return String(result.path); + case 'delete_file': return String(result.path); + case 'move_file': return `${result.source} → ${result.destination}`; + case 'copy_file': return `${result.source} → ${result.destination}`; + case 'web_fetch': return `${result.status} | ${result.length} chars`; + case 'append_file': return `${result.path} (${result.newSize}B)`; + 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}`; + default: return '完成'; + } +} + export function setupIPC(): void { ipcMain.handle('dialog:openFile', async (_, options?: { filters?: Array<{ name: string; extensions: string[] }> }) => { const result = await dialog.showOpenDialog(mainWindow!, { @@ -113,29 +139,37 @@ export function setupIPC(): void { ipcMain.handle('tool:execute', async (_, toolName: string, args: Record) => { try { sendLog('debug', `🔧 tool:execute ${toolName}`, JSON.stringify(args || {}).slice(0, 200)); + let result; switch (toolName) { - case 'read_file': return await handleReadFile(args as { path: string; encoding?: string; start_line?: number; end_line?: number }); - case 'write_file': return await handleWriteFile(args as { path: string; content: string; encoding?: string }); - case 'list_directory': return await handleListDir(args as { path: string; recursive?: boolean; max_depth?: number; include_hidden?: boolean; filter_extension?: string }); - case 'search_files': return await handleSearchFiles(args as { path: string; query: string; search_type?: string; case_sensitive?: boolean; max_results?: number; file_extensions?: string[] }); - case 'create_directory': return await handleCreateDir(args as { path: string }); - case 'delete_file': return await handleDeleteFile(args as { path: string; recursive?: boolean }); - case 'run_command': return await handleRunCommand(args as { command: string; cwd?: string; timeout?: number }); - case 'move_file': return await handleMoveFile(args as { source: string; destination: string }); - case 'copy_file': return await handleCopyFile(args as { source: string; destination: string; recursive?: boolean }); - case 'web_fetch': return await handleWebFetch(args as { url: string; max_chars?: number; extract_mode?: string }); - case 'append_file': return await handleAppendFile(args as { path: string; content: string; newline?: boolean }); - case 'edit_file': return await handleEditFile(args as { path: string; old_text: string; new_text: string; all?: boolean }); - case 'get_file_info': return await handleGetFileInfo(args as { path: string }); - case 'tree': return await handleTree(args as { path: string; max_depth?: number; include_hidden?: boolean }); - case 'download_file': return await handleDownloadFile(args as { url: string; destination: string }); - case 'diff_files': return await handleDiffFiles(args as { file1: string; file2: string; context_lines?: number }); - case 'replace_in_files': return await handleReplaceInFiles(args as { path: string; glob: string; old_text: string; new_text: string }); - case 'read_multiple_files':return await handleReadMultipleFiles(args as { paths: string[]; max_chars_per_file?: number }); - case 'git': return await handleGit(args as any); - case 'compress': return await handleCompress(args as { action: string; path: string; destination?: string; format?: string }); + case 'read_file': result = await handleReadFile(args as { path: string; encoding?: string; start_line?: number; end_line?: number }); break; + case 'write_file': result = await handleWriteFile(args as { path: string; content: string; encoding?: string }); break; + case 'list_directory': result = await handleListDir(args as { path: string; recursive?: boolean; max_depth?: number; include_hidden?: boolean; filter_extension?: string }); break; + case 'search_files': result = await handleSearchFiles(args as { path: string; query: string; search_type?: string; case_sensitive?: boolean; max_results?: number; file_extensions?: string[] }); break; + case 'create_directory': result = await handleCreateDir(args as { path: string }); break; + case 'delete_file': result = await handleDeleteFile(args as { path: string; recursive?: boolean }); break; + case 'run_command': return await handleRunCommand(args as { command: string; cwd?: string; timeout?: number }); // run_command 自身管理日志 + case 'move_file': result = await handleMoveFile(args as { source: string; destination: string }); break; + case 'copy_file': result = await handleCopyFile(args as { source: string; destination: string; recursive?: boolean }); break; + case 'web_fetch': result = await handleWebFetch(args as { url: string; max_chars?: number; extract_mode?: string }); break; + case 'append_file': result = await handleAppendFile(args as { path: string; content: string; newline?: boolean }); break; + case 'edit_file': result = await handleEditFile(args as { path: string; old_text: string; new_text: string; all?: 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 any); break; + case 'compress': result = await handleCompress(args as { action: string; path: string; destination?: string; format?: string }); break; default: return { success: false, error: `未知工具: ${toolName}` }; } + // 结果日志 + if (result.success) { + sendLog('success', `🔧 ${toolName} ✓`, summarizeResult(toolName, result)); + } else { + sendLog('warn', `🔧 ${toolName} ✗`, (result.error || '未知错误').slice(0, 200)); + } + return result; } catch (err) { sendLog('error', `❌ tool:execute 异常: ${toolName}`, String(err)); return { success: false, error: (err as Error).message || '工具执行异常' }; diff --git a/src/main/tool-handlers.ts b/src/main/tool-handlers.ts index f056710..240cf3f 100644 --- a/src/main/tool-handlers.ts +++ b/src/main/tool-handlers.ts @@ -390,9 +390,12 @@ export async function handleMoveFile(params: { source: string; destination: stri const destCheck = checkPathAllowed(dest, 'write'); if (!destCheck.ok) return { success: false, error: destCheck.reason }; + sendLog('info', `📦 move_file`, `${src} → ${dest}`); await fs.rename(src, dest); + sendLog('success', `📦 move_file 完成`, dest); return { success: true, source: src, destination: dest }; } catch (err) { + sendLog('error', `📦 move_file 失败`, (err as Error).message); return { success: false, error: (err as Error).message }; } } @@ -407,6 +410,7 @@ export async function handleCopyFile(params: { source: string; destination: stri if (!destCheck.ok) return { success: false, error: destCheck.reason }; const stat = await fs.stat(src); + sendLog('info', `📋 copy_file`, `${src} → ${dest}${stat.isDirectory() ? ' (目录)' : ''}`); if (stat.isDirectory()) { await fs.cp(src, dest, { recursive: params.recursive !== false }); } else { @@ -414,8 +418,10 @@ export async function handleCopyFile(params: { source: string; destination: stri await fs.mkdir(destDir, { recursive: true }); await fs.copyFile(src, dest); } + sendLog('success', `📋 copy_file 完成`, dest); return { success: true, source: src, destination: dest, isDirectory: stat.isDirectory() }; } catch (err) { + sendLog('error', `📋 copy_file 失败`, (err as Error).message); return { success: false, error: (err as Error).message }; } } @@ -429,6 +435,7 @@ export async function handleWebFetch(params: { url: string; max_chars?: number; const maxChars = params.max_chars || 10000; + sendLog('info', `🌐 web_fetch`, `${url} (${maxChars} chars)`); const resp = await fetch(url, { headers: { 'User-Agent': 'MetonaOllama/1.0' }, signal: AbortSignal.timeout(15000) @@ -462,6 +469,7 @@ export async function handleWebFetch(params: { url: string; max_chars?: number; const truncated = text.length > maxChars; if (truncated) text = text.slice(0, maxChars); + sendLog('success', `🌐 web_fetch 完成`, `${resp.status} | ${text.length} chars${truncated ? ' (已截断)' : ''}`); return { success: true, url,