refactor: 命令执行走工作空间终端,AI 不再直接执行

流程改为:AI 提出命令 → 用户确认 → 工作空间终端执行(实时显示)→ 结果反馈 AI → AI 回复

- handleRunCommand 直接 spawn 子进程,stdout/stderr 实时通过 cmd:output 推送
- 进程结束后通过 cmd:done 通知渲染进程
- 停止按钮通过 cmd:kill 终止进程
- 清理 workspace.ts 中不再需要的 execWorkspaceCommand 工具集成代码
- 去掉 workspace:execTool/killTool,改为 cmd:output/cmd:done/cmd:kill
This commit is contained in:
thzxx
2026-04-08 13:12:18 +08:00
parent d5e5f5f58a
commit 0431ed56c0
7 changed files with 107 additions and 84 deletions
+73 -18
View File
@@ -5,8 +5,10 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import { spawn } from 'child_process';
import { checkPathAllowed, checkCommandAllowed } from './tool-security.js';
import { execWorkspaceCommand } from './workspace.js';
import { mainWindow } from './main.js';
import { getWorkspaceDir } from './workspace.js';
export interface ToolResult {
success: boolean;
@@ -262,29 +264,82 @@ export async function handleDeleteFile(params: { path: string; recursive?: boole
}
}
/** 当前工具命令进程(用于用户手动终止) */
let _toolProc: ReturnType<typeof spawn> | null = null;
export async function handleRunCommand(params: { command: string; cwd?: string; timeout?: number }): Promise<ToolResult> {
try {
const cmdCheck = checkCommandAllowed(params.command);
if (!cmdCheck.ok) return { success: false, error: cmdCheck.reason };
// 通过 workspace 子进程执行,复用安全检查和进程管理
const result = await execWorkspaceCommand(params.command, params.cwd);
const cwd = params.cwd ? path.resolve(params.cwd) : getWorkspaceDir();
const dirCheck = checkPathAllowed(cwd, 'read');
if (!dirCheck.ok) return { success: false, error: dirCheck.reason };
return {
success: result.success,
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode,
duration: result.duration
};
return new Promise((resolve) => {
const isWin = process.platform === 'win32';
const shell = isWin ? 'cmd.exe' : 'bash';
const shellArgs = isWin ? ['/c', params.command] : ['-c', params.command];
const startTime = Date.now();
_toolProc = spawn(shell, shellArgs, {
cwd,
env: { ...process.env, ...(isWin ? {} : { TERM: 'xterm-256color' }) },
stdio: ['pipe', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
// 实时推送到工作空间终端
_toolProc.stdout?.on('data', (data: Buffer) => {
const str = data.toString();
stdout += str;
mainWindow?.webContents.send('cmd:output', { command: params.command, type: 'stdout', data: str });
});
_toolProc.stderr?.on('data', (data: Buffer) => {
const str = data.toString();
stderr += str;
mainWindow?.webContents.send('cmd:output', { command: params.command, type: 'stderr', data: str });
});
_toolProc.on('close', (code) => {
_toolProc = null;
const duration = Date.now() - startTime;
mainWindow?.webContents.send('cmd:done', { command: params.command, exitCode: code });
resolve({
success: code === 0,
stdout,
stderr,
exitCode: code,
duration
});
});
_toolProc.on('error', (err) => {
_toolProc = null;
const duration = Date.now() - startTime;
mainWindow?.webContents.send('cmd:done', { command: params.command, exitCode: 1 });
resolve({
success: false,
stdout,
stderr: stderr + (stderr ? '\n' : '') + err.message,
exitCode: 1,
error: err.message,
duration
});
});
});
} catch (err) {
const error = err as { message: string };
return {
success: false,
stdout: '',
stderr: error.message,
exitCode: 1,
error: error.message
};
return { success: false, stdout: '', stderr: (err as Error).message, exitCode: 1, error: (err as Error).message };
}
}
/** 终止当前工具命令进程 */
export function killToolProcess(): boolean {
if (!_toolProc) return false;
try { _toolProc.kill('SIGTERM'); } catch { /* ignore */ }
_toolProc = null;
return true;
}