From d5e5f5f58a8c0b3288917b105c9dc4f158da4ec8 Mon Sep 17 00:00:00 2001 From: thzxx Date: Wed, 8 Apr 2026 13:06:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20AI=20=E5=91=BD=E4=BB=A4=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E5=AE=9E=E6=97=B6=E6=B5=81=E5=BC=8F=E6=8E=A8=E9=80=81?= =?UTF-8?q?=E5=88=B0=E5=B7=A5=E4=BD=9C=E7=A9=BA=E9=97=B4=E7=BB=88=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - execWorkspaceCommand 新增 onOutput 回调,stdout/stderr 实时推送 - ipc.ts workspace:execTool 实时转发输出 + 发送 toolDone 完成通知 - 渲染进程 appendToolStreamOutput 流式显示,handleToolDone 显示退出状态 - 去掉 tool-handlers.ts 中的一次性完整输出推送 - 完整流程:用户确认 → 实时输出 → 完成通知 → AI 回复结果 --- src/main/ipc.ts | 12 +++- src/main/preload.ts | 10 +++- src/main/tool-handlers.ts | 11 ---- src/main/workspace.ts | 13 +++- src/renderer/components/workspace-panel.ts | 69 ++++++++++++---------- src/renderer/types.d.ts | 6 +- 6 files changed, 70 insertions(+), 51 deletions(-) diff --git a/src/main/ipc.ts b/src/main/ipc.ts index f4eba75..b53293d 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -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 工具命令 diff --git a/src/main/preload.ts b/src/main/preload.ts index 67fc4bd..b4186fe 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -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)); } } }); diff --git a/src/main/tool-handlers.ts b/src/main/tool-handlers.ts index 5189687..30a308b 100644 --- a/src/main/tool-handlers.ts +++ b/src/main/tool-handlers.ts @@ -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, diff --git a/src/main/workspace.ts b/src/main/workspace.ts index 3578964..3fd171b 100644 --- a/src/main/workspace.ts +++ b/src/main/workspace.ts @@ -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) => { diff --git a/src/renderer/components/workspace-panel.ts b/src/renderer/components/workspace-panel.ts index 697a3d9..7a8ca34 100644 --- a/src/renderer/components/workspace-panel.ts +++ b/src/renderer/components/workspace-panel.ts @@ -182,10 +182,15 @@ export async function initWorkspacePanel(): Promise { handleProcessExit(data.id, data.code); }); - // 监听 AI Tool Calling 命令输出,同步到终端面板 + // 监听 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); + appendToolStreamOutput(data.command, data.type, data.data); + }); + + // 监听 AI Tool Calling 命令执行完毕 + bridge.workspace.onToolDone((data) => { + logDebug('工作空间工具命令完成', `command: ${data.command.slice(0, 50)} | exitCode: ${data.exitCode}`); + handleToolDone(data.command, data.exitCode); }); // 绑定 UI 事件 @@ -263,46 +268,50 @@ function getActiveSession(): TerminalSession | undefined { return terminalSessions.find(s => s.id === activeTerminalId); } -/** AI Tool Calling 命令输出 → 追加到活跃终端 */ -function appendToolOutput(command: string, stdout: string, stderr: string, exitCode: number | null): void { +/** AI Tool Calling 实时输出 → 流式追加到活跃终端 */ +function appendToolStreamOutput(command: string, type: 'stdout' | 'stderr', data: string): void { const session = getActiveSession(); if (!session) return; - // 记录当前 AI 命令 - currentAiCommand = command; - session.running = true; - updateHint(); - - // 显示执行的命令 - 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 }); - } + // 首次输出时显示命令头 + if (!currentAiCommand) { + currentAiCommand = command; + session.running = true; + session.lines.push({ type: 'system', text: `$ ${command} (AI)` }); + updateHint(); + updateStopBtnState(); } - // 显示 stderr - if (stderr) { - for (const line of stderr.split('\n')) { - if (line) session.lines.push({ type: 'stderr', text: line }); + // 流式追加输出(按行分割,保留最后一行用于后续追加) + const lines = data.split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (i < lines.length - 1 || line) { + const cleanLine = line.replace(/\r/g, ''); + if (cleanLine) session.lines.push({ type, text: cleanLine }); } } - // 显示退出状态 - const exitMsg = exitCode === 0 ? '✓ AI 命令执行成功' : `✗ AI 命令退出 (code: ${exitCode})`; - session.lines.push({ type: 'system', text: exitMsg }); - - // 命令结束 - session.running = false; - currentAiCommand = null; - // 限制最大行数 if (session.lines.length > 3000) { session.lines = session.lines.slice(-2000); } + renderTerminal(); +} + +/** AI Tool Calling 命令执行完毕 */ +function handleToolDone(command: string, exitCode: number | null): void { + const session = getActiveSession(); + if (!session) return; + + // 显示退出状态 + const exitMsg = exitCode === 0 ? '✓ AI 命令执行成功' : `✗ AI 命令退出 (code: ${exitCode})`; + session.lines.push({ type: 'system', text: exitMsg }); + + session.running = false; + currentAiCommand = null; + renderTerminal(); updateStopBtnState(); updateHint(); diff --git a/src/renderer/types.d.ts b/src/renderer/types.d.ts index d2a1926..9caf7d2 100644 --- a/src/renderer/types.d.ts +++ b/src/renderer/types.d.ts @@ -241,8 +241,10 @@ export interface MetonaDesktopAPI { 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; stdout: string; stderr: string; exitCode: number | null }) => void) => void; + /** 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 }>; };