From 0431ed56c04c942e6d4afc096db1ef738e854752 Mon Sep 17 00:00:00 2001 From: thzxx Date: Wed, 8 Apr 2026 13:12:18 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=91=BD=E4=BB=A4=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E8=B5=B0=E5=B7=A5=E4=BD=9C=E7=A9=BA=E9=97=B4=E7=BB=88?= =?UTF-8?q?=E7=AB=AF=EF=BC=8CAI=20=E4=B8=8D=E5=86=8D=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E6=89=A7=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 流程改为: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 --- src/main/ipc.ts | 25 ++---- src/main/preload.ts | 22 +++--- src/main/tool-handlers.ts | 91 +++++++++++++++++----- src/main/workspace.ts | 17 ---- src/renderer/components/workspace-panel.ts | 14 ++-- src/renderer/services/tool-registry.ts | 8 +- src/renderer/types.d.ts | 14 ++-- 7 files changed, 107 insertions(+), 84 deletions(-) diff --git a/src/main/ipc.ts b/src/main/ipc.ts index b53293d..a42c95c 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -19,10 +19,11 @@ import { handleSearchFiles, handleCreateDir, handleDeleteFile, - handleRunCommand + handleRunCommand, + killToolProcess } from './tool-handlers.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 { ipcMain.handle('dialog:openFile', async (_, options?: { filters?: Array<{ name: string; extensions: string[] }> }) => { @@ -172,24 +173,10 @@ export function setupIPC(): void { 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 工具命令 - ipcMain.handle('workspace:killTool', async () => { - const killed = terminateCurrentToolCommand(); - sendLog('info', `🔧 workspace:killTool`, killed ? '已终止' : '无正在运行的工具命令'); + ipcMain.handle('cmd:kill', async () => { + const killed = killToolProcess(); + sendLog('info', `🔧 cmd:kill`, killed ? '已终止' : '无正在运行的工具命令'); return { killed }; }); } diff --git a/src/main/preload.ts b/src/main/preload.ts index b4186fe..0ba6b14 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -59,17 +59,17 @@ contextBridge.exposeInMainWorld('metonaDesktop', { onExit: (callback: (data: { id: string; code: number | null }) => void) => { ipcRenderer.on('workspace:exit', (_: unknown, data: { id: string; code: number | null }) => callback(data)); }, - /** 无超时命令执行,用于 AI Tool Calling */ - 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; type: 'stdout' | 'stderr'; data: string }) => void) => { - ipcRenderer.on('workspace:toolOutput', (_: unknown, data: { command: string; type: 'stdout' | 'stderr'; data: string }) => callback(data)); + /** 无超时工具命令执行,用于 AI Tool Calling */ + execTool: (command: string, cwd?: string) => ipcRenderer.invoke('tool:execute', 'run_command', { command, cwd }), + /** AI 命令实时输出推送到工作空间终端 */ + onCmdOutput: (callback: (data: { command: string; type: 'stdout' | 'stderr'; data: string }) => void) => { + ipcRenderer.on('cmd:output', (_: 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)); - } + /** AI 命令执行完毕 */ + onCmdDone: (callback: (data: { command: string; exitCode: number | null }) => void) => { + ipcRenderer.on('cmd:done', (_: unknown, data: { command: string; exitCode: number | null }) => callback(data)); + }, + /** 终止当前 AI 命令 */ + cmdKill: () => ipcRenderer.invoke('cmd:kill') } }); diff --git a/src/main/tool-handlers.ts b/src/main/tool-handlers.ts index 30a308b..dbbd55d 100644 --- a/src/main/tool-handlers.ts +++ b/src/main/tool-handlers.ts @@ -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 | null = null; + export async function handleRunCommand(params: { command: string; cwd?: string; timeout?: number }): Promise { 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; +} diff --git a/src/main/workspace.ts b/src/main/workspace.ts index 3fd171b..c8c11eb 100644 --- a/src/main/workspace.ts +++ b/src/main/workspace.ts @@ -20,9 +20,6 @@ export const DEFAULT_WORKSPACE_DIR = path.join(app.getPath('userData'), 'workspa /** 活跃进程 Map */ const activeProcesses = new Map(); -/** 当前 AI 工具命令进程 ID(用于用户手动终止) */ -let _currentToolProcessId: string | null = null; - /** 确保工作空间目录存在 */ export function ensureWorkspaceDir(): void { if (!fs.existsSync(DEFAULT_WORKSPACE_DIR)) { @@ -257,7 +254,6 @@ export function execWorkspaceCommand( }); activeProcesses.set(cmdId, proc); - _currentToolProcessId = cmdId; proc.stdout.on('data', (data: Buffer) => { const str = data.toString(); @@ -291,7 +287,6 @@ export function execWorkspaceCommand( proc.on('close', (code) => { activeProcesses.delete(cmdId); - if (_currentToolProcessId === cmdId) _currentToolProcessId = null; const duration = Date.now() - startTime; sendLog( code === 0 ? 'success' : 'warn', @@ -311,7 +306,6 @@ export function execWorkspaceCommand( proc.on('error', (err) => { activeProcesses.delete(cmdId); - if (_currentToolProcessId === cmdId) _currentToolProcessId = null; const duration = Date.now() - startTime; sendLog('error', `🔧 execWorkspaceCommand 失败`, `${cmdId} | ${err.message}`); resolve({ @@ -347,14 +341,3 @@ export function execWorkspaceCommand( export function terminateWorkspaceCommand(commandId: string): boolean { return killProcess(commandId); } - -/** - * 终止当前正在运行的 AI 工具命令 - * @returns 是否成功终止 - */ -export function terminateCurrentToolCommand(): boolean { - if (!_currentToolProcessId) return false; - const id = _currentToolProcessId; - _currentToolProcessId = null; - return killProcess(id); -} diff --git a/src/renderer/components/workspace-panel.ts b/src/renderer/components/workspace-panel.ts index 7a8ca34..d69dc1b 100644 --- a/src/renderer/components/workspace-panel.ts +++ b/src/renderer/components/workspace-panel.ts @@ -182,14 +182,14 @@ export async function initWorkspacePanel(): Promise { handleProcessExit(data.id, data.code); }); - // 监听 AI Tool Calling 实时输出,流式推送到终端面板 - bridge.workspace.onToolOutput((data) => { + // 监听 AI 命令实时输出,流式推送到终端面板 + bridge.workspace.onCmdOutput((data) => { appendToolStreamOutput(data.command, data.type, data.data); }); - // 监听 AI Tool Calling 命令执行完毕 - bridge.workspace.onToolDone((data) => { - logDebug('工作空间工具命令完成', `command: ${data.command.slice(0, 50)} | exitCode: ${data.exitCode}`); + // 监听 AI 命令执行完毕 + bridge.workspace.onCmdDone((data) => { + logDebug('工作空间 AI 命令完成', `command: ${data.command.slice(0, 50)} | exitCode: ${data.exitCode}`); handleToolDone(data.command, data.exitCode); }); @@ -383,8 +383,8 @@ function killCurrentProcess(): void { bridge.workspace.kill(session.id); } - // 终止当前 AI 工具命令 - bridge.workspace.killTool(); + // 终止当前 AI 命令 + bridge.workspace.cmdKill(); const cmd = currentAiCommand; session.lines.push({ type: 'system', text: '■ 命令已手动终止' }); diff --git a/src/renderer/services/tool-registry.ts b/src/renderer/services/tool-registry.ts index b66c2fe..4213c5e 100644 --- a/src/renderer/services/tool-registry.ts +++ b/src/renderer/services/tool-registry.ts @@ -161,14 +161,14 @@ export async function executeTool(toolName: string, args: Record void) => void; onExit: (callback: (data: { id: string; code: number | null }) => void) => void; /** 无超时工具命令执行,用于 AI Tool Calling 集成 */ - 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; type: 'stdout' | 'stderr'; data: string }) => void) => void; - /** AI Tool Calling 命令执行完毕 */ - onToolDone: (callback: (data: { command: string; exitCode: number | null }) => void) => void; - /** 终止当前 AI 工具命令 */ - killTool: () => Promise<{ killed: boolean }>; + execTool: (command: string, cwd?: string) => Promise<{ success: boolean; stdout: string; stderr: string; exitCode: number | null; duration: number }>; + /** AI 命令实时输出推送到工作空间终端 */ + onCmdOutput: (callback: (data: { command: string; type: 'stdout' | 'stderr'; data: string }) => void) => void; + /** AI 命令执行完毕 */ + onCmdDone: (callback: (data: { command: string; exitCode: number | null }) => void) => void; + /** 终止当前 AI 命令 */ + cmdKill: () => Promise<{ killed: boolean }>; }; }