From f9ad682f011491f1786d41ae53b0b966fbd1d740 Mon Sep 17 00:00:00 2001 From: thzxx Date: Wed, 8 Apr 2026 12:47:28 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20AI=20Tool=20Calling=20run=5Fcommand=20?= =?UTF-8?q?=E8=BE=93=E5=87=BA=E5=90=8C=E6=AD=A5=E5=88=B0=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E7=A9=BA=E9=97=B4=E7=BB=88=E7=AB=AF=E9=9D=A2=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - handleRunCommand 改用 execWorkspaceCommand 替代 Node exec() - 执行完成后通过 workspace:toolOutput IPC 事件推送命令和输出 - 渲染进程监听 onToolOutput,在活跃终端显示 AI 命令及结果 - 清理 tool-handlers.ts 中不再需要的 child_process 导入 --- src/main/preload.ts | 6 ++- src/main/tool-handlers.ts | 46 +++++++++++----------- src/renderer/components/workspace-panel.ts | 40 +++++++++++++++++++ src/renderer/types.d.ts | 2 + 4 files changed, 69 insertions(+), 25 deletions(-) diff --git a/src/main/preload.ts b/src/main/preload.ts index b3146ac..1c6293a 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -60,6 +60,10 @@ contextBridge.exposeInMainWorld('metonaDesktop', { ipcRenderer.on('workspace:exit', (_: unknown, data: { id: string; code: number | null }) => callback(data)); }, /** 无超时命令执行,用于 AI Tool Calling */ - execTool: (command: string, cwd?: string) => ipcRenderer.invoke('workspace:execTool', command, cwd) + execTool: (command: string, cwd?: string) => ipcRenderer.invoke('workspace:execTool', command, cwd), + /** AI Tool Calling 命令输出 → 同步到工作空间终端 */ + onToolOutput: (callback: (data: { command: string; stdout: string; stderr: string; exitCode: number | null }) => void) => { + ipcRenderer.on('workspace:toolOutput', (_: unknown, data: { command: string; stdout: string; stderr: string; exitCode: number | null }) => callback(data)); + } } }); diff --git a/src/main/tool-handlers.ts b/src/main/tool-handlers.ts index 05f8be6..5189687 100644 --- a/src/main/tool-handlers.ts +++ b/src/main/tool-handlers.ts @@ -5,11 +5,9 @@ import * as fs from 'fs/promises'; import * as path from 'path'; -import { exec } from 'child_process'; -import { promisify } from 'util'; import { checkPathAllowed, checkCommandAllowed } from './tool-security.js'; - -const execAsync = promisify(exec); +import { mainWindow } from './main.js'; +import { execWorkspaceCommand } from './workspace.js'; export interface ToolResult { success: boolean; @@ -270,33 +268,33 @@ export async function handleRunCommand(params: { command: string; cwd?: string; const cmdCheck = checkCommandAllowed(params.command); if (!cmdCheck.ok) return { success: false, error: cmdCheck.reason }; - const timeout = Math.min(params.timeout || 30000, 120000); - const cwd = params.cwd ? path.resolve(params.cwd) : process.env.HOME || '/'; + // 通过 workspace 子进程执行,复用安全检查和进程管理 + const result = await execWorkspaceCommand(params.command, params.cwd); - const cwdAllowed = checkPathAllowed(cwd, 'read'); - if (!cwdAllowed.ok) return { success: false, error: cwdAllowed.reason }; - - const start = Date.now(); - const { stdout, stderr } = await execAsync(params.command, { - cwd, - timeout, - maxBuffer: 100 * 1024 - }); + // 将命令和输出同步推送到工作空间终端面板 + if (mainWindow) { + mainWindow.webContents.send('workspace:toolOutput', { + command: params.command, + stdout: result.stdout, + stderr: result.stderr, + exitCode: result.exitCode + }); + } return { - success: true, - stdout: stdout.slice(0, 100 * 1024), - stderr: stderr.slice(0, 100 * 1024), - exitCode: 0, - duration: Date.now() - start + success: result.success, + stdout: result.stdout, + stderr: result.stderr, + exitCode: result.exitCode, + duration: result.duration }; } catch (err) { - const error = err as { stdout?: string; stderr?: string; code?: number; message: string }; + const error = err as { message: string }; return { success: false, - stdout: error.stdout?.slice(0, 100 * 1024) || '', - stderr: error.stderr?.slice(0, 100 * 1024) || error.message, - exitCode: error.code || 1, + stdout: '', + stderr: error.message, + exitCode: 1, error: error.message }; } diff --git a/src/renderer/components/workspace-panel.ts b/src/renderer/components/workspace-panel.ts index 8d9b9a6..6162681 100644 --- a/src/renderer/components/workspace-panel.ts +++ b/src/renderer/components/workspace-panel.ts @@ -171,6 +171,12 @@ export async function initWorkspacePanel(): Promise { handleProcessExit(data.id, data.code); }); + // 监听 AI Tool Calling 命令输出,同步到终端面板 + bridge.workspace.onToolOutput((data) => { + logDebug('工作空间收到工具输出', `command: ${data.command.slice(0, 50)} | exitCode: ${data.exitCode}`); + appendToolOutput(data.command, data.stdout, data.stderr, data.exitCode); + }); + // 绑定 UI 事件 bindEvents(); @@ -293,6 +299,40 @@ function executeCommand(command: string): void { bridge.workspace.exec({ id: session.id, command, cwd }); } +/** AI Tool Calling 命令输出 → 追加到活跃终端 */ +function appendToolOutput(command: string, stdout: string, stderr: string, exitCode: number | null): void { + const session = getActiveSession(); + if (!session) return; + + // 显示执行的命令 + session.lines.push({ type: 'system', text: `$ ${command} (AI)` }); + + // 显示 stdout + if (stdout) { + for (const line of stdout.split('\n')) { + if (line) session.lines.push({ type: 'stdout', text: line }); + } + } + + // 显示 stderr + if (stderr) { + for (const line of stderr.split('\n')) { + if (line) session.lines.push({ type: 'stderr', text: line }); + } + } + + // 显示退出状态 + const exitMsg = exitCode === 0 ? '✓ AI 命令执行成功' : `✗ AI 命令退出 (code: ${exitCode})`; + session.lines.push({ type: 'system', text: exitMsg }); + + // 限制最大行数 + if (session.lines.length > 3000) { + session.lines = session.lines.slice(-2000); + } + + renderTerminal(); +} + function appendTerminalOutput(sessionId: string, type: 'stdout' | 'stderr', data: string): void { const session = terminalSessions.find(s => s.id === sessionId); if (!session) { diff --git a/src/renderer/types.d.ts b/src/renderer/types.d.ts index 475d749..2bf89ec 100644 --- a/src/renderer/types.d.ts +++ b/src/renderer/types.d.ts @@ -241,6 +241,8 @@ export interface MetonaDesktopAPI { onExit: (callback: (data: { id: string; code: number | null }) => void) => void; /** 无超时工具命令执行,用于 AI Tool Calling 集成 */ execTool: (command: string, cwd?: string) => Promise<{ success: boolean; stdout: string; stderr: string; exitCode: number | null; duration: number; userTerminated: boolean; truncated: boolean }>; + /** AI Tool Calling 命令输出 → 同步到工作空间终端 */ + onToolOutput: (callback: (data: { command: string; stdout: string; stderr: string; exitCode: number | null }) => void) => void; }; }