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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user