'use strict'; const { exec, spawn } = require('child_process'); const path = require('path'); const { getContainerContext, resolveContainerPath, buildEnvArgs } = require('./container_bridge'); function resolvePath(workspace, p) { if (!p) return workspace; if (path.isAbsolute(p)) return p; return path.join(workspace, p); } function runCommandTool(workspace, args, abortSignal) { return new Promise((resolve) => { const cmd = args.command; const timeoutSec = Number(args.timeout || 0); const cwd = resolvePath(workspace, args.working_dir || '.'); const ctx = getContainerContext(); if (!cmd) return resolve({ success: false, error: 'command 不能为空' }); if (abortSignal && abortSignal.aborted) { return resolve({ success: false, error: '任务被用户取消', cancelled: true }); } const timeoutMs = Math.max(0, timeoutSec) * 1000; let finished = false; if (ctx) { const resolved = resolveContainerPath(workspace, args.working_dir || '.', ctx); if (resolved.error) { return resolve({ success: false, error: resolved.error }); } const workdir = resolved.containerPath || ctx.mountPath; const execArgs = [ 'exec', '-i' ]; if (workdir) execArgs.push('-w', workdir); execArgs.push(...buildEnvArgs(ctx)); const venv = ctx.containerVenv || '/opt/agent-venv'; const basePath = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'; const exportPrefix = `export VIRTUAL_ENV=${venv}; export PATH=${venv}/bin:${basePath}; `; const wrappedCmd = `${exportPrefix}${cmd}`; execArgs.push(ctx.containerName, 'sh', '-c', wrappedCmd); const child = spawn(ctx.dockerBin, execArgs, { stdio: ['ignore', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; const onData = (chunk, buffer) => buffer.push(chunk); const stdoutBuf = []; const stderrBuf = []; child.stdout.on('data', (chunk) => onData(chunk, stdoutBuf)); child.stderr.on('data', (chunk) => onData(chunk, stderrBuf)); const done = (result) => { if (finished) return; finished = true; if (abortSignal) abortSignal.removeEventListener('abort', onAbort); return resolve(result); }; const onAbort = () => { if (finished) return; finished = true; try { child.kill('SIGTERM'); } catch (_) {} return resolve({ success: false, error: '任务被用户取消', cancelled: true }); }; if (abortSignal) abortSignal.addEventListener('abort', onAbort, { once: true }); let timer = null; if (timeoutMs) { timer = setTimeout(() => { try { child.kill('SIGTERM'); } catch (_) {} const output = Buffer.concat(stdoutBuf).toString() + Buffer.concat(stderrBuf).toString(); done({ success: false, status: 'timeout', error: '命令超时', return_code: null, output, timeout: timeoutSec, }); }, timeoutMs); } child.on('close', (code) => { if (timer) clearTimeout(timer); const output = Buffer.concat(stdoutBuf).toString() + Buffer.concat(stderrBuf).toString(); if (finished) return; if (code && code !== 0) { return done({ success: false, status: 'error', error: `exit ${code}`, return_code: code, output, }); } return done({ success: true, status: 'ok', output }); }); return; } const child = exec(cmd, { cwd, timeout: timeoutMs, maxBuffer: 10 * 1024 * 1024, shell: true }, (err, stdout, stderr) => { if (finished) return; finished = true; if (abortSignal) abortSignal.removeEventListener('abort', onAbort); const output = [stdout, stderr].filter(Boolean).join(''); if (err) { const isTimeout = err.killed && err.signal === 'SIGTERM'; return resolve({ success: false, status: isTimeout ? 'timeout' : 'error', error: err.message, return_code: err.code, output, timeout: timeoutSec, }); } resolve({ success: true, status: 'ok', output }); }); const onAbort = () => { if (finished) return; finished = true; try { child.kill('SIGTERM'); } catch (_) {} return resolve({ success: false, error: '任务被用户取消', cancelled: true }); }; if (abortSignal) abortSignal.addEventListener('abort', onAbort, { once: true }); }); } module.exports = { runCommandTool };