fix: AI Tool Calling run_command 输出同步到工作空间终端面板

- handleRunCommand 改用 execWorkspaceCommand 替代 Node exec()
- 执行完成后通过 workspace:toolOutput IPC 事件推送命令和输出
- 渲染进程监听 onToolOutput,在活跃终端显示 AI 命令及结果
- 清理 tool-handlers.ts 中不再需要的 child_process 导入
This commit is contained in:
thzxx
2026-04-08 12:47:36 +08:00
parent 75a45de5f2
commit f9ad682f01
4 changed files with 69 additions and 25 deletions
+5 -1
View File
@@ -60,6 +60,10 @@ contextBridge.exposeInMainWorld('metonaDesktop', {
ipcRenderer.on('workspace:exit', (_: unknown, data: { id: string; code: number | null }) => callback(data)); ipcRenderer.on('workspace:exit', (_: unknown, data: { id: string; code: number | null }) => callback(data));
}, },
/** 无超时命令执行,用于 AI Tool Calling */ /** 无超时命令执行,用于 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));
}
} }
}); });
+21 -23
View File
@@ -5,11 +5,9 @@
import * as fs from 'fs/promises'; import * as fs from 'fs/promises';
import * as path from 'path'; import * as path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';
import { checkPathAllowed, checkCommandAllowed } from './tool-security.js'; import { checkPathAllowed, checkCommandAllowed } from './tool-security.js';
import { mainWindow } from './main.js';
const execAsync = promisify(exec); import { execWorkspaceCommand } from './workspace.js';
export interface ToolResult { export interface ToolResult {
success: boolean; success: boolean;
@@ -270,33 +268,33 @@ export async function handleRunCommand(params: { command: string; cwd?: string;
const cmdCheck = checkCommandAllowed(params.command); const cmdCheck = checkCommandAllowed(params.command);
if (!cmdCheck.ok) return { success: false, error: cmdCheck.reason }; if (!cmdCheck.ok) return { success: false, error: cmdCheck.reason };
const timeout = Math.min(params.timeout || 30000, 120000); // 通过 workspace 子进程执行,复用安全检查和进程管理
const cwd = params.cwd ? path.resolve(params.cwd) : process.env.HOME || '/'; const result = await execWorkspaceCommand(params.command, params.cwd);
const cwdAllowed = checkPathAllowed(cwd, 'read'); // 将命令和输出同步推送到工作空间终端面板
if (!cwdAllowed.ok) return { success: false, error: cwdAllowed.reason }; if (mainWindow) {
mainWindow.webContents.send('workspace:toolOutput', {
const start = Date.now(); command: params.command,
const { stdout, stderr } = await execAsync(params.command, { stdout: result.stdout,
cwd, stderr: result.stderr,
timeout, exitCode: result.exitCode
maxBuffer: 100 * 1024
}); });
}
return { return {
success: true, success: result.success,
stdout: stdout.slice(0, 100 * 1024), stdout: result.stdout,
stderr: stderr.slice(0, 100 * 1024), stderr: result.stderr,
exitCode: 0, exitCode: result.exitCode,
duration: Date.now() - start duration: result.duration
}; };
} catch (err) { } catch (err) {
const error = err as { stdout?: string; stderr?: string; code?: number; message: string }; const error = err as { message: string };
return { return {
success: false, success: false,
stdout: error.stdout?.slice(0, 100 * 1024) || '', stdout: '',
stderr: error.stderr?.slice(0, 100 * 1024) || error.message, stderr: error.message,
exitCode: error.code || 1, exitCode: 1,
error: error.message error: error.message
}; };
} }
@@ -171,6 +171,12 @@ export async function initWorkspacePanel(): Promise<void> {
handleProcessExit(data.id, data.code); 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 事件 // 绑定 UI 事件
bindEvents(); bindEvents();
@@ -293,6 +299,40 @@ function executeCommand(command: string): void {
bridge.workspace.exec({ id: session.id, command, cwd }); 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 { function appendTerminalOutput(sessionId: string, type: 'stdout' | 'stderr', data: string): void {
const session = terminalSessions.find(s => s.id === sessionId); const session = terminalSessions.find(s => s.id === sessionId);
if (!session) { if (!session) {
+2
View File
@@ -241,6 +241,8 @@ export interface MetonaDesktopAPI {
onExit: (callback: (data: { id: string; code: number | null }) => void) => void; onExit: (callback: (data: { id: string; code: number | null }) => void) => void;
/** 无超时工具命令执行,用于 AI Tool Calling 集成 */ /** 无超时工具命令执行,用于 AI Tool Calling 集成 */
execTool: (command: string, cwd?: string) => Promise<{ success: boolean; stdout: string; stderr: string; exitCode: number | null; duration: number; userTerminated: boolean; truncated: boolean }>; 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;
}; };
} }