From 4099b5180f6c2bbf85a1d1b3fbcb1605ab7dbea0 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Thu, 16 Apr 2026 09:38:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B7=A5=E5=85=B7=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E6=89=A9=E5=B1=95=EF=BC=8C=E6=96=B0=E5=A2=9E=205=20=E4=B8=AA?= =?UTF-8?q?=E5=AE=9E=E7=94=A8=E5=B7=A5=E5=85=B7=EF=BC=88=E5=85=B1=2012=20?= =?UTF-8?q?=E4=B8=AA=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增工具: - move_file: 移动/重命名文件(需确认) - copy_file: 复制文件/目录(需确认) - web_fetch: 抓取网页内容,自动提取文本(自动) - append_file: 追加内容到文件末尾(需确认) - edit_file: 文件内查找替换,支持全部替换(需确认) 同步更新: - 主进程 IPC 分发 - 工具安全检查(路径白名单) - 设置面板工具列表 UI - 帮助文档工具说明 --- src/main/ipc.ts | 12 ++- src/main/tool-handlers.ts | 142 +++++++++++++++++++++++++ src/renderer/index.html | 7 +- src/renderer/services/tool-registry.ts | 99 ++++++++++++++++- 4 files changed, 254 insertions(+), 6 deletions(-) diff --git a/src/main/ipc.ts b/src/main/ipc.ts index da5e1cf..147fc15 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -20,7 +20,12 @@ import { handleCreateDir, handleDeleteFile, handleRunCommand, - killToolProcess + killToolProcess, + handleMoveFile, + handleCopyFile, + handleWebFetch, + handleAppendFile, + handleEditFile } from './tool-handlers.js'; import { getAllowedDirs, getBlockedDirs, setAllowedDirs } from './tool-security.js'; import { startProcess, killProcess, getWorkspaceDir, setWorkspaceDir, listWorkspaceDir } from './workspace.js'; @@ -108,6 +113,11 @@ export function setupIPC(): void { 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 }); default: return { success: false, error: `未知工具: ${toolName}` }; } } catch (err) { diff --git a/src/main/tool-handlers.ts b/src/main/tool-handlers.ts index 0b27a34..9a20a42 100644 --- a/src/main/tool-handlers.ts +++ b/src/main/tool-handlers.ts @@ -380,3 +380,145 @@ export function killToolProcess(): boolean { sendLog('info', `🔧 cmd:kill`, '工具命令已终止'); return true; } + +export async function handleMoveFile(params: { source: string; destination: string }): Promise { + try { + const src = resolvePath(params.source); + const dest = resolvePath(params.destination); + const srcCheck = checkPathAllowed(src, 'read'); + if (!srcCheck.ok) return { success: false, error: srcCheck.reason }; + const destCheck = checkPathAllowed(dest, 'write'); + if (!destCheck.ok) return { success: false, error: destCheck.reason }; + + await fs.rename(src, dest); + return { success: true, source: src, destination: dest }; + } catch (err) { + return { success: false, error: (err as Error).message }; + } +} + +export async function handleCopyFile(params: { source: string; destination: string; recursive?: boolean }): Promise { + try { + const src = resolvePath(params.source); + const dest = resolvePath(params.destination); + const srcCheck = checkPathAllowed(src, 'read'); + if (!srcCheck.ok) return { success: false, error: srcCheck.reason }; + const destCheck = checkPathAllowed(dest, 'write'); + if (!destCheck.ok) return { success: false, error: destCheck.reason }; + + const stat = await fs.stat(src); + if (stat.isDirectory()) { + await fs.cp(src, dest, { recursive: params.recursive !== false }); + } else { + const destDir = path.dirname(dest); + await fs.mkdir(destDir, { recursive: true }); + await fs.copyFile(src, dest); + } + return { success: true, source: src, destination: dest, isDirectory: stat.isDirectory() }; + } catch (err) { + return { success: false, error: (err as Error).message }; + } +} + +export async function handleWebFetch(params: { url: string; max_chars?: number; extract_mode?: string }): Promise { + try { + const url = params.url; + if (!url.startsWith('http://') && !url.startsWith('https://')) { + return { success: false, error: '仅支持 http/https 协议' }; + } + + const maxChars = params.max_chars || 10000; + + const resp = await fetch(url, { + headers: { 'User-Agent': 'MetonaOllama/1.0' }, + signal: AbortSignal.timeout(15000) + }); + + if (!resp.ok) { + return { success: false, error: `HTTP ${resp.status}: ${resp.statusText}` }; + } + + const contentType = resp.headers.get('content-type') || ''; + if (!contentType.includes('text') && !contentType.includes('html') && !contentType.includes('json') && !contentType.includes('xml')) { + return { success: false, error: `不支持的内容类型: ${contentType}` }; + } + + let text = await resp.text(); + + // 简单 HTML → 文本提取 + if (contentType.includes('html')) { + text = text + .replace(//gi, '') + .replace(//gi, '') + .replace(/<[^>]+>/g, ' ') + .replace(/ /g, ' ') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&') + .replace(/\s+/g, ' ') + .trim(); + } + + const truncated = text.length > maxChars; + if (truncated) text = text.slice(0, maxChars); + + return { + success: true, + url, + content: text, + content_type: contentType, + status: resp.status, + truncated, + length: text.length + }; + } catch (err) { + return { success: false, error: (err as Error).message }; + } +} + +export async function handleAppendFile(params: { path: string; content: string; newline?: boolean }): Promise { + try { + const filePath = resolvePath(params.path); + const allowed = checkPathAllowed(filePath, 'write'); + if (!allowed.ok) return { success: false, error: allowed.reason }; + + const prefix = params.newline !== false ? '\n' : ''; + await fs.appendFile(filePath, prefix + params.content, 'utf-8'); + + const stat = await fs.stat(filePath); + return { success: true, path: filePath, newSize: stat.size }; + } catch (err) { + return { success: false, error: (err as Error).message }; + } +} + +export async function handleEditFile(params: { path: string; old_text: string; new_text: string; all?: boolean }): Promise { + try { + const filePath = resolvePath(params.path); + const allowed = checkPathAllowed(filePath, 'write'); + if (!allowed.ok) return { success: false, error: allowed.reason }; + + const content = await fs.readFile(filePath, 'utf-8'); + + if (!content.includes(params.old_text)) { + return { success: false, error: '未找到要替换的文本' }; + } + + let newContent: string; + let replaceCount: number; + + if (params.all) { + const parts = content.split(params.old_text); + replaceCount = parts.length - 1; + newContent = parts.join(params.new_text); + } else { + replaceCount = 1; + newContent = content.replace(params.old_text, params.new_text); + } + + await fs.writeFile(filePath, newContent, 'utf-8'); + return { success: true, path: filePath, replaceCount, newSize: newContent.length }; + } catch (err) { + return { success: false, error: (err as Error).message }; + } +} diff --git a/src/renderer/index.html b/src/renderer/index.html index 40f0305..0b8b336 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -278,6 +278,11 @@
🔍 搜索文件自动
📂 创建目录需确认
🗑️ 删除文件需确认
+
📦 移动文件需确认
+
📋 复制文件需确认
+
🌐 网页抓取自动
+
➕ 追加文件需确认
+
✂️ 编辑文件需确认
💻 执行命令