agent-Specialization/easyagent/src/tools/run_command.js

129 lines
4.6 KiB
JavaScript

'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 };