feat: 工作空间与 AI Tool Calling 深度集成

核心变更:
- 新增 execWorkspaceCommand() 无超时进程执行(workspace.ts)
- 新增 workspace:execTool IPC 通道(无超时,5MB 输出上限)
- run_command 工具改用 workspace IPC,移除 30s 超时限制
- run_command 默认启用(需用户确认),AI 上下文注入工作空间目录
- Agent Loop 工具执行:run_command 无超时,其他工具保持 30s
- Agent 系统提示词新增工作空间规则(userTerminated/truncated 反馈)

UI 修复:
- Header z-index 提升至 50,Model Bar 提升至 40,Input Area 提升至 30
- 工作空间面板 top 调整为 90px(header + model-bar),不再遮挡菜单
- Input Area 增加 position: relative 确保 z-index 生效

其他:
- 新增 docs/DEVELOPMENT.md 开发规范文档
- 设置面板 run_command 开关文案优化,默认值改为 true
This commit is contained in:
thzxx
2026-04-07 22:24:58 +08:00
parent 4a85879b20
commit d9e9416d35
10 changed files with 492 additions and 25 deletions
+7 -1
View File
@@ -22,7 +22,7 @@ import {
handleRunCommand
} from './tool-handlers.js';
import { getAllowedDirs, getBlockedDirs, setAllowedDirs } from './tool-security.js';
import { startProcess, killProcess, getWorkspaceDir, setWorkspaceDir, listWorkspaceDir } from './workspace.js';
import { startProcess, killProcess, getWorkspaceDir, setWorkspaceDir, listWorkspaceDir, execWorkspaceCommand } from './workspace.js';
export function setupIPC(): void {
ipcMain.handle('dialog:openFile', async (_, options?: { filters?: Array<{ name: string; extensions: string[] }> }) => {
@@ -171,4 +171,10 @@ export function setupIPC(): void {
sendLog('info', `🖥️ workspace:kill`, `ID: ${id}`);
killProcess(id);
});
// 无超时工具命令执行(AI Tool Calling 集成)
ipcMain.handle('workspace:execTool', async (_, command: string, cwd?: string) => {
sendLog('info', `🔧 workspace:execTool`, `${command.slice(0, 200)} | cwd: ${cwd || '默认'}`);
return execWorkspaceCommand(command, cwd);
});
}
+3 -1
View File
@@ -58,6 +58,8 @@ 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)
}
});
+134
View File
@@ -200,3 +200,137 @@ export function isProcessAlive(id: string): boolean {
export function getActiveProcessCount(): number {
return activeProcesses.size;
}
/**
* 执行命令并收集完整输出(无超时限制)
* 用于 AI Tool Calling 集成,通过 IPC invoke/handle 调用
* 返回完整的 stdout + stderr + exitCode
*/
export function execWorkspaceCommand(
command: string,
cwd?: string
): 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;
const cmdId = `_tool_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
// 安全检查
const cmdCheck = checkCommandAllowed(command);
if (!cmdCheck.ok) {
sendLog('warn', `🔧 工具命令被拦截`, `${command.slice(0, 100)} | ${cmdCheck.reason}`);
resolve({ success: false, stdout: '', stderr: cmdCheck.reason!, exitCode: 1, duration: 0, userTerminated: false, truncated: false });
return;
}
const dirCheck = checkPathAllowed(workDir, 'read');
if (!dirCheck.ok) {
sendLog('warn', `🔧 工具目录被拦截`, `${workDir} | ${dirCheck.reason}`);
resolve({ success: false, stdout: '', stderr: dirCheck.reason!, exitCode: 1, duration: 0, userTerminated: false, truncated: false });
return;
}
if (!fs.existsSync(workDir)) {
resolve({ success: false, stdout: '', stderr: `工作目录不存在: ${workDir}`, exitCode: 1, duration: 0, userTerminated: false, truncated: false });
return;
}
sendLog('info', `🔧 execWorkspaceCommand`, `${command.slice(0, 200)} | cwd: ${workDir}`);
const startTime = Date.now();
const MAX_OUTPUT = 5 * 1024 * 1024; // 5MB 输出上限
let stdoutBuf = '';
let stderrBuf = '';
let truncated = false;
try {
const isWin = process.platform === 'win32';
const shell = isWin ? 'cmd.exe' : 'bash';
const shellArgs = isWin ? ['/c', command] : ['-c', command];
const proc = spawn(shell, shellArgs, {
cwd: workDir,
env: { ...process.env, ...(isWin ? {} : { TERM: 'xterm-256color' }) },
stdio: ['pipe', 'pipe', 'pipe']
});
activeProcesses.set(cmdId, proc);
proc.stdout.on('data', (data: Buffer) => {
if (stdoutBuf.length < MAX_OUTPUT) {
stdoutBuf += data.toString();
if (stdoutBuf.length > MAX_OUTPUT) {
stdoutBuf = stdoutBuf.slice(0, MAX_OUTPUT);
truncated = true;
}
} else {
truncated = true;
}
});
proc.stderr.on('data', (data: Buffer) => {
if (stderrBuf.length < MAX_OUTPUT) {
stderrBuf += data.toString();
if (stderrBuf.length > MAX_OUTPUT) {
stderrBuf = stderrBuf.slice(0, MAX_OUTPUT);
truncated = true;
}
} else {
truncated = true;
}
});
proc.on('close', (code) => {
activeProcesses.delete(cmdId);
const duration = Date.now() - startTime;
sendLog(
code === 0 ? 'success' : 'warn',
`🔧 execWorkspaceCommand 完成`,
`code: ${code} | ${duration}ms | stdout: ${stdoutBuf.length}B | stderr: ${stderrBuf.length}B`
);
resolve({
success: code === 0,
stdout: stdoutBuf,
stderr: stderrBuf,
exitCode: code,
duration,
userTerminated: false,
truncated
});
});
proc.on('error', (err) => {
activeProcesses.delete(cmdId);
const duration = Date.now() - startTime;
sendLog('error', `🔧 execWorkspaceCommand 失败`, `${cmdId} | ${err.message}`);
resolve({
success: false,
stdout: stdoutBuf,
stderr: stderrBuf + (stderrBuf ? '\n' : '') + `进程启动失败: ${err.message}`,
exitCode: 1,
duration,
userTerminated: false,
truncated
});
});
} catch (err) {
const duration = Date.now() - startTime;
sendLog('error', `🔧 execWorkspaceCommand 异常`, (err as Error).message);
resolve({
success: false,
stdout: stdoutBuf,
stderr: (err as Error).message,
exitCode: 1,
duration,
userTerminated: false,
truncated: false
});
}
});
}
/**
* 终止 execWorkspaceCommand 启动的进程
* @returns 是否成功终止
*/
export function terminateWorkspaceCommand(commandId: string): boolean {
return killProcess(commandId);
}