feat: AI 命令执行实时流式推送到工作空间终端
- execWorkspaceCommand 新增 onOutput 回调,stdout/stderr 实时推送 - ipc.ts workspace:execTool 实时转发输出 + 发送 toolDone 完成通知 - 渲染进程 appendToolStreamOutput 流式显示,handleToolDone 显示退出状态 - 去掉 tool-handlers.ts 中的一次性完整输出推送 - 完整流程:用户确认 → 实时输出 → 完成通知 → AI 回复结果
This commit is contained in:
+10
-2
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -182,10 +182,15 @@ export async function initWorkspacePanel(): Promise<void> {
|
||||
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 命令
|
||||
// 首次输出时显示命令头
|
||||
if (!currentAiCommand) {
|
||||
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 });
|
||||
}
|
||||
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();
|
||||
|
||||
Vendored
+4
-2
@@ -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 }>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user