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:
+6
-19
@@ -19,10 +19,11 @@ import {
|
|||||||
handleSearchFiles,
|
handleSearchFiles,
|
||||||
handleCreateDir,
|
handleCreateDir,
|
||||||
handleDeleteFile,
|
handleDeleteFile,
|
||||||
handleRunCommand
|
handleRunCommand,
|
||||||
|
killToolProcess
|
||||||
} from './tool-handlers.js';
|
} from './tool-handlers.js';
|
||||||
import { getAllowedDirs, getBlockedDirs, setAllowedDirs } from './tool-security.js';
|
import { getAllowedDirs, getBlockedDirs, setAllowedDirs } from './tool-security.js';
|
||||||
import { startProcess, killProcess, getWorkspaceDir, setWorkspaceDir, listWorkspaceDir, execWorkspaceCommand, terminateCurrentToolCommand } from './workspace.js';
|
import { startProcess, killProcess, getWorkspaceDir, setWorkspaceDir, listWorkspaceDir } from './workspace.js';
|
||||||
|
|
||||||
export function setupIPC(): void {
|
export function setupIPC(): void {
|
||||||
ipcMain.handle('dialog:openFile', async (_, options?: { filters?: Array<{ name: string; extensions: string[] }> }) => {
|
ipcMain.handle('dialog:openFile', async (_, options?: { filters?: Array<{ name: string; extensions: string[] }> }) => {
|
||||||
@@ -172,24 +173,10 @@ export function setupIPC(): void {
|
|||||||
killProcess(id);
|
killProcess(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 无超时工具命令执行(AI Tool Calling 集成)
|
|
||||||
ipcMain.handle('workspace:execTool', async (event, command: string, cwd?: string) => {
|
|
||||||
sendLog('info', `🔧 workspace:execTool`, `${command.slice(0, 200)} | cwd: ${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 工具命令
|
// 终止当前 AI 工具命令
|
||||||
ipcMain.handle('workspace:killTool', async () => {
|
ipcMain.handle('cmd:kill', async () => {
|
||||||
const killed = terminateCurrentToolCommand();
|
const killed = killToolProcess();
|
||||||
sendLog('info', `🔧 workspace:killTool`, killed ? '已终止' : '无正在运行的工具命令');
|
sendLog('info', `🔧 cmd:kill`, killed ? '已终止' : '无正在运行的工具命令');
|
||||||
return { killed };
|
return { killed };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-11
@@ -59,17 +59,17 @@ contextBridge.exposeInMainWorld('metonaDesktop', {
|
|||||||
onExit: (callback: (data: { id: string; code: number | null }) => void) => {
|
onExit: (callback: (data: { id: string; code: number | null }) => void) => {
|
||||||
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('tool:execute', 'run_command', { command, cwd }),
|
||||||
/** 终止当前 AI 工具命令 */
|
/** AI 命令实时输出推送到工作空间终端 */
|
||||||
killTool: () => ipcRenderer.invoke('workspace:killTool'),
|
onCmdOutput: (callback: (data: { command: string; type: 'stdout' | 'stderr'; data: string }) => void) => {
|
||||||
/** AI Tool Calling 实时输出推送到工作空间终端 */
|
ipcRenderer.on('cmd:output', (_: unknown, data: { command: string; type: 'stdout' | 'stderr'; data: string }) => callback(data));
|
||||||
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 命令执行完毕 */
|
/** AI 命令执行完毕 */
|
||||||
onToolDone: (callback: (data: { command: string; exitCode: number | null }) => void) => {
|
onCmdDone: (callback: (data: { command: string; exitCode: number | null }) => void) => {
|
||||||
ipcRenderer.on('workspace:toolDone', (_: unknown, data: { command: string; exitCode: number | null }) => callback(data));
|
ipcRenderer.on('cmd:done', (_: unknown, data: { command: string; exitCode: number | null }) => callback(data));
|
||||||
}
|
},
|
||||||
|
/** 终止当前 AI 命令 */
|
||||||
|
cmdKill: () => ipcRenderer.invoke('cmd:kill')
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+72
-17
@@ -5,8 +5,10 @@
|
|||||||
|
|
||||||
import * as fs from 'fs/promises';
|
import * as fs from 'fs/promises';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
import { checkPathAllowed, checkCommandAllowed } from './tool-security.js';
|
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 {
|
export interface ToolResult {
|
||||||
success: boolean;
|
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> {
|
export async function handleRunCommand(params: { command: string; cwd?: string; timeout?: number }): Promise<ToolResult> {
|
||||||
try {
|
try {
|
||||||
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 };
|
||||||
|
|
||||||
// 通过 workspace 子进程执行,复用安全检查和进程管理
|
const cwd = params.cwd ? path.resolve(params.cwd) : getWorkspaceDir();
|
||||||
const result = await execWorkspaceCommand(params.command, params.cwd);
|
const dirCheck = checkPathAllowed(cwd, 'read');
|
||||||
|
if (!dirCheck.ok) return { success: false, error: dirCheck.reason };
|
||||||
|
|
||||||
return {
|
return new Promise((resolve) => {
|
||||||
success: result.success,
|
const isWin = process.platform === 'win32';
|
||||||
stdout: result.stdout,
|
const shell = isWin ? 'cmd.exe' : 'bash';
|
||||||
stderr: result.stderr,
|
const shellArgs = isWin ? ['/c', params.command] : ['-c', params.command];
|
||||||
exitCode: result.exitCode,
|
const startTime = Date.now();
|
||||||
duration: result.duration
|
|
||||||
};
|
_toolProc = spawn(shell, shellArgs, {
|
||||||
} catch (err) {
|
cwd,
|
||||||
const error = err as { message: string };
|
env: { ...process.env, ...(isWin ? {} : { TERM: 'xterm-256color' }) },
|
||||||
return {
|
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,
|
success: false,
|
||||||
stdout: '',
|
stdout,
|
||||||
stderr: error.message,
|
stderr: stderr + (stderr ? '\n' : '') + err.message,
|
||||||
exitCode: 1,
|
exitCode: 1,
|
||||||
error: error.message
|
error: err.message,
|
||||||
};
|
duration
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,9 +20,6 @@ export const DEFAULT_WORKSPACE_DIR = path.join(app.getPath('userData'), 'workspa
|
|||||||
/** 活跃进程 Map */
|
/** 活跃进程 Map */
|
||||||
const activeProcesses = new Map<string, ChildProcess>();
|
const activeProcesses = new Map<string, ChildProcess>();
|
||||||
|
|
||||||
/** 当前 AI 工具命令进程 ID(用于用户手动终止) */
|
|
||||||
let _currentToolProcessId: string | null = null;
|
|
||||||
|
|
||||||
/** 确保工作空间目录存在 */
|
/** 确保工作空间目录存在 */
|
||||||
export function ensureWorkspaceDir(): void {
|
export function ensureWorkspaceDir(): void {
|
||||||
if (!fs.existsSync(DEFAULT_WORKSPACE_DIR)) {
|
if (!fs.existsSync(DEFAULT_WORKSPACE_DIR)) {
|
||||||
@@ -257,7 +254,6 @@ export function execWorkspaceCommand(
|
|||||||
});
|
});
|
||||||
|
|
||||||
activeProcesses.set(cmdId, proc);
|
activeProcesses.set(cmdId, proc);
|
||||||
_currentToolProcessId = cmdId;
|
|
||||||
|
|
||||||
proc.stdout.on('data', (data: Buffer) => {
|
proc.stdout.on('data', (data: Buffer) => {
|
||||||
const str = data.toString();
|
const str = data.toString();
|
||||||
@@ -291,7 +287,6 @@ export function execWorkspaceCommand(
|
|||||||
|
|
||||||
proc.on('close', (code) => {
|
proc.on('close', (code) => {
|
||||||
activeProcesses.delete(cmdId);
|
activeProcesses.delete(cmdId);
|
||||||
if (_currentToolProcessId === cmdId) _currentToolProcessId = null;
|
|
||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
sendLog(
|
sendLog(
|
||||||
code === 0 ? 'success' : 'warn',
|
code === 0 ? 'success' : 'warn',
|
||||||
@@ -311,7 +306,6 @@ export function execWorkspaceCommand(
|
|||||||
|
|
||||||
proc.on('error', (err) => {
|
proc.on('error', (err) => {
|
||||||
activeProcesses.delete(cmdId);
|
activeProcesses.delete(cmdId);
|
||||||
if (_currentToolProcessId === cmdId) _currentToolProcessId = null;
|
|
||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
sendLog('error', `🔧 execWorkspaceCommand 失败`, `${cmdId} | ${err.message}`);
|
sendLog('error', `🔧 execWorkspaceCommand 失败`, `${cmdId} | ${err.message}`);
|
||||||
resolve({
|
resolve({
|
||||||
@@ -347,14 +341,3 @@ export function execWorkspaceCommand(
|
|||||||
export function terminateWorkspaceCommand(commandId: string): boolean {
|
export function terminateWorkspaceCommand(commandId: string): boolean {
|
||||||
return killProcess(commandId);
|
return killProcess(commandId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 终止当前正在运行的 AI 工具命令
|
|
||||||
* @returns 是否成功终止
|
|
||||||
*/
|
|
||||||
export function terminateCurrentToolCommand(): boolean {
|
|
||||||
if (!_currentToolProcessId) return false;
|
|
||||||
const id = _currentToolProcessId;
|
|
||||||
_currentToolProcessId = null;
|
|
||||||
return killProcess(id);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -182,14 +182,14 @@ export async function initWorkspacePanel(): Promise<void> {
|
|||||||
handleProcessExit(data.id, data.code);
|
handleProcessExit(data.id, data.code);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听 AI Tool Calling 实时输出,流式推送到终端面板
|
// 监听 AI 命令实时输出,流式推送到终端面板
|
||||||
bridge.workspace.onToolOutput((data) => {
|
bridge.workspace.onCmdOutput((data) => {
|
||||||
appendToolStreamOutput(data.command, data.type, data.data);
|
appendToolStreamOutput(data.command, data.type, data.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听 AI Tool Calling 命令执行完毕
|
// 监听 AI 命令执行完毕
|
||||||
bridge.workspace.onToolDone((data) => {
|
bridge.workspace.onCmdDone((data) => {
|
||||||
logDebug('工作空间工具命令完成', `command: ${data.command.slice(0, 50)} | exitCode: ${data.exitCode}`);
|
logDebug('工作空间 AI 命令完成', `command: ${data.command.slice(0, 50)} | exitCode: ${data.exitCode}`);
|
||||||
handleToolDone(data.command, data.exitCode);
|
handleToolDone(data.command, data.exitCode);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -383,8 +383,8 @@ function killCurrentProcess(): void {
|
|||||||
bridge.workspace.kill(session.id);
|
bridge.workspace.kill(session.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 终止当前 AI 工具命令
|
// 终止当前 AI 命令
|
||||||
bridge.workspace.killTool();
|
bridge.workspace.cmdKill();
|
||||||
|
|
||||||
const cmd = currentAiCommand;
|
const cmd = currentAiCommand;
|
||||||
session.lines.push({ type: 'system', text: '■ 命令已手动终止' });
|
session.lines.push({ type: 'system', text: '■ 命令已手动终止' });
|
||||||
|
|||||||
@@ -161,14 +161,14 @@ export async function executeTool(toolName: string, args: Record<string, unknown
|
|||||||
try {
|
try {
|
||||||
logToolStart(toolName, JSON.stringify(args));
|
logToolStart(toolName, JSON.stringify(args));
|
||||||
|
|
||||||
// run_command 走 workspace 无超时 IPC,其他工具走标准 IPC
|
// run_command 走 workspace IPC
|
||||||
if (toolName === 'run_command') {
|
if (toolName === 'run_command') {
|
||||||
const command = args.command as string;
|
const command = args.command as string;
|
||||||
const cwd = args.cwd as string | undefined;
|
const cwd = args.cwd as string | undefined;
|
||||||
if (!command) {
|
if (!command) {
|
||||||
return { success: false, error: '缺少 command 参数' };
|
return { success: false, error: '缺少 command 参数' };
|
||||||
}
|
}
|
||||||
logInfo(`Workspace 命令执行: ${command.slice(0, 100)}`);
|
logInfo(`工作空间命令执行: ${command.slice(0, 100)}`);
|
||||||
const result = await bridge.workspace.execTool(command, cwd);
|
const result = await bridge.workspace.execTool(command, cwd);
|
||||||
logToolResult('run_command', result.success, result.success ? undefined : result.stderr?.slice(0, 200));
|
logToolResult('run_command', result.success, result.success ? undefined : result.stderr?.slice(0, 200));
|
||||||
return {
|
return {
|
||||||
@@ -176,9 +176,7 @@ export async function executeTool(toolName: string, args: Record<string, unknown
|
|||||||
stdout: result.stdout,
|
stdout: result.stdout,
|
||||||
stderr: result.stderr,
|
stderr: result.stderr,
|
||||||
exitCode: result.exitCode,
|
exitCode: result.exitCode,
|
||||||
duration: result.duration,
|
duration: result.duration
|
||||||
userTerminated: result.userTerminated,
|
|
||||||
truncated: result.truncated
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+7
-7
@@ -240,13 +240,13 @@ export interface MetonaDesktopAPI {
|
|||||||
onOutput: (callback: (data: { id: string; type: 'stdout' | 'stderr'; data: string }) => void) => void;
|
onOutput: (callback: (data: { id: string; type: 'stdout' | 'stderr'; data: string }) => void) => void;
|
||||||
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 }>;
|
||||||
/** AI Tool Calling 实时输出推送到工作空间终端 */
|
/** AI 命令实时输出推送到工作空间终端 */
|
||||||
onToolOutput: (callback: (data: { command: string; type: 'stdout' | 'stderr'; data: string }) => void) => void;
|
onCmdOutput: (callback: (data: { command: string; type: 'stdout' | 'stderr'; data: string }) => void) => void;
|
||||||
/** AI Tool Calling 命令执行完毕 */
|
/** AI 命令执行完毕 */
|
||||||
onToolDone: (callback: (data: { command: string; exitCode: number | null }) => void) => void;
|
onCmdDone: (callback: (data: { command: string; exitCode: number | null }) => void) => void;
|
||||||
/** 终止当前 AI 工具命令 */
|
/** 终止当前 AI 命令 */
|
||||||
killTool: () => Promise<{ killed: boolean }>;
|
cmdKill: () => Promise<{ killed: boolean }>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user