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:
+5
-1
@@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+22
-24
@@ -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) {
|
||||||
|
|||||||
Vendored
+2
@@ -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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user