Files

180 lines
6.1 KiB
JavaScript
Raw Permalink Normal View History

'use strict';
const { spawn } = require('child_process');
module.exports = function (RED) {
const agentSessions = new Map();
function AgentProcessNode(config) {
RED.nodes.createNode(this, config);
const node = this;
this.configNode = RED.nodes.getNode(config.agentConfig);
this.processId = config.processId || 'default';
this.action = config.action || 'start';
this.initialInput = config.initialInput || '';
this.workingDir = config.workingDir || '';
this.autoRestart = config.autoRestart !== false;
if (!this.configNode) {
node.error('No agent config node selected');
return;
}
node.on('input', async function (msg) {
const cfg = node.configNode;
const action = msg.action || node.action;
const processId = msg.processId || node.processId;
const workspaceDir = msg.workingDir || node.workingDir || cfg.workspaceDir || '/data/agent-workspace';
try {
switch (action) {
case 'start':
await startProcess(node, cfg, processId, workspaceDir, msg);
break;
case 'stop':
await stopProcess(node, processId);
break;
case 'restart':
await stopProcess(node, processId);
await startProcess(node, cfg, processId, workspaceDir, msg);
break;
case 'send':
await sendToProcess(node, processId, msg.payload || msg.initialInput);
break;
case 'status':
await getProcessStatus(node, processId);
break;
default:
throw new Error(`Unknown action: ${action}`);
}
} catch (err) {
node.error(`Process ${action} failed: ${err.message}`, msg);
}
});
node.on('close', function (removed, done) {
if (removed) {
stopProcess(node, node.processId).finally(done);
} else {
done();
}
});
}
async function startProcess(node, cfg, processId, workspaceDir, msg) {
if (agentSessions.has(processId)) {
node.warn(`Process ${processId} already running`);
msg.payload = { status: 'running', processId };
node.send(msg);
return;
}
const cliCmd = cfg.cliPath || 'agent';
const initialInput = msg.initialInput || msg.payload || node.initialInput;
const defaultArgs = cfg.defaultArgs ? cfg.defaultArgs.split(' ').filter(Boolean) : [];
const args = ['run', ...defaultArgs];
if (initialInput) args.push('--input', initialInput);
node.status({ fill: 'yellow', shape: 'dot', text: `Starting ${processId}...` });
const child = spawn(cliCmd, args, {
cwd: workspaceDir,
env: {
...process.env,
AGENT_WORKSPACE: workspaceDir,
AGENT_API_ENDPOINT: cfg.apiEndpoint,
AGENT_API_KEY: cfg.apiKey || ''
},
windowsHide: true
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => { stdout += data.toString(); });
child.stderr.on('data', (data) => { stderr += data.toString(); });
const session = {
id: processId,
process: child,
workspaceDir,
startTime: Date.now(),
stdout,
stderr
};
agentSessions.set(processId, session);
child.on('close', (code) => {
agentSessions.delete(processId);
node.status({ fill: 'red', shape: 'ring', text: `Stopped (${code})` });
node.send({
payload: { status: 'stopped', processId, exitCode: code, stdout, stderr },
processId
});
});
child.on('error', (err) => {
agentSessions.delete(processId);
node.status({ fill: 'red', shape: 'ring', text: 'Error' });
node.error(`Process ${processId} error: ${err.message}`);
});
await new Promise(r => setTimeout(r, 2000));
node.status({ fill: 'green', shape: 'dot', text: `Running: ${processId}` });
msg.payload = { status: 'started', processId, pid: child.pid };
msg.processId = processId;
node.send(msg);
}
async function stopProcess(node, processId) {
const session = agentSessions.get(processId);
if (!session) {
node.warn(`Process ${processId} not found`);
return;
}
session.process.kill('SIGTERM');
await new Promise(r => setTimeout(r, 1000));
if (!session.process.killed) {
session.process.kill('SIGKILL');
}
}
async function sendToProcess(node, processId, input) {
const session = agentSessions.get(processId);
if (!session) {
throw new Error(`Process ${processId} not running`);
}
if (session.process.stdin.writable) {
session.process.stdin.write(input + '\n');
}
node.status({ fill: 'blue', shape: 'dot', text: `Sent to ${processId}` });
node.send({ payload: { sent: true, processId, input }, processId });
}
async function getProcessStatus(node, processId) {
const session = agentSessions.get(processId);
if (!session) {
node.send({ payload: { status: 'not running', processId }, processId });
return;
}
node.send({
payload: {
status: 'running',
processId,
pid: session.process.pid,
uptime: Date.now() - session.startTime,
workspaceDir: session.workspaceDir
},
processId
});
}
RED.nodes.registerType('agent-process', AgentProcessNode);
};