feat: AI 命令执行实时流式推送到工作空间终端

- execWorkspaceCommand 新增 onOutput 回调,stdout/stderr 实时推送
- ipc.ts workspace:execTool 实时转发输出 + 发送 toolDone 完成通知
- 渲染进程 appendToolStreamOutput 流式显示,handleToolDone 显示退出状态
- 去掉 tool-handlers.ts 中的一次性完整输出推送
- 完整流程:用户确认 → 实时输出 → 完成通知 → AI 回复结果
This commit is contained in:
thzxx
2026-04-08 13:06:57 +08:00
parent d150b53116
commit d5e5f5f58a
6 changed files with 70 additions and 51 deletions
+10 -2
View File
@@ -173,9 +173,17 @@ export function setupIPC(): void {
});
// 无超时工具命令执行(AI Tool Calling 集成)
ipcMain.handle('workspace:execTool', async (_, command: string, cwd?: string) => {
ipcMain.handle('workspace:execTool', async (event, command: string, cwd?: string) => {
sendLog('info', `🔧 workspace:execTool`, `${command.slice(0, 200)} | cwd: ${cwd || '默认'}`);
return execWorkspaceCommand(command, cwd);
const result = await execWorkspaceCommand(command, cwd, (type, data) => {
// 实时推送到工作空间终端
event.sender.send('workspace:toolOutput', { command, type, data });
});
// 通知命令执行完毕
event.sender.send('workspace:toolDone', { command, exitCode: result.exitCode });
return result;
});
// 终止当前 AI 工具命令
+7 -3
View File
@@ -63,9 +63,13 @@ contextBridge.exposeInMainWorld('metonaDesktop', {
execTool: (command: string, cwd?: string) => ipcRenderer.invoke('workspace:execTool', command, cwd),
/** 终止当前 AI 工具命令 */
killTool: () => ipcRenderer.invoke('workspace:killTool'),
/** 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));
/** AI Tool Calling 实时输出推送到工作空间终端 */
onToolOutput: (callback: (data: { command: string; type: 'stdout' | 'stderr'; data: string }) => void) => {
ipcRenderer.on('workspace:toolOutput', (_: unknown, data: { command: string; type: 'stdout' | 'stderr'; data: string }) => callback(data));
},
/** AI Tool Calling 命令执行完毕 */
onToolDone: (callback: (data: { command: string; exitCode: number | null }) => void) => {
ipcRenderer.on('workspace:toolDone', (_: unknown, data: { command: string; exitCode: number | null }) => callback(data));
}
}
});
-11
View File
@@ -6,7 +6,6 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import { checkPathAllowed, checkCommandAllowed } from './tool-security.js';
import { mainWindow } from './main.js';
import { execWorkspaceCommand } from './workspace.js';
export interface ToolResult {
@@ -271,16 +270,6 @@ export async function handleRunCommand(params: { command: string; cwd?: string;
// 通过 workspace 子进程执行,复用安全检查和进程管理
const result = await execWorkspaceCommand(params.command, params.cwd);
// 将命令和输出同步推送到工作空间终端面板
if (mainWindow) {
mainWindow.webContents.send('workspace:toolOutput', {
command: params.command,
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode
});
}
return {
success: result.success,
stdout: result.stdout,
+10 -3
View File
@@ -211,7 +211,8 @@ export function getActiveProcessCount(): number {
*/
export function execWorkspaceCommand(
command: string,
cwd?: string
cwd?: string,
onOutput?: (type: 'stdout' | 'stderr', data: string) => void
): Promise<{ success: boolean; stdout: string; stderr: string; exitCode: number | null; duration: number; userTerminated: boolean; truncated: boolean }> {
return new Promise((resolve) => {
const workDir = cwd ? path.resolve(cwd) : _workspaceDir;
@@ -259,8 +260,9 @@ export function execWorkspaceCommand(
_currentToolProcessId = cmdId;
proc.stdout.on('data', (data: Buffer) => {
const str = data.toString();
if (stdoutBuf.length < MAX_OUTPUT) {
stdoutBuf += data.toString();
stdoutBuf += str;
if (stdoutBuf.length > MAX_OUTPUT) {
stdoutBuf = stdoutBuf.slice(0, MAX_OUTPUT);
truncated = true;
@@ -268,11 +270,14 @@ export function execWorkspaceCommand(
} else {
truncated = true;
}
// 实时推送到渲染进程
onOutput?.('stdout', str);
});
proc.stderr.on('data', (data: Buffer) => {
const str = data.toString();
if (stderrBuf.length < MAX_OUTPUT) {
stderrBuf += data.toString();
stderrBuf += str;
if (stderrBuf.length > MAX_OUTPUT) {
stderrBuf = stderrBuf.slice(0, MAX_OUTPUT);
truncated = true;
@@ -280,6 +285,8 @@ export function execWorkspaceCommand(
} else {
truncated = true;
}
// 实时推送到渲染进程
onOutput?.('stderr', str);
});
proc.on('close', (code) => {