Versão inicial do Agents Orchestrator
Painel administrativo web para orquestração de agentes Claude Code com suporte a execução de tarefas, agendamento cron, pipelines sequenciais e terminal com streaming em tempo real via WebSocket.
This commit is contained in:
189
src/agents/executor.js
Normal file
189
src/agents/executor.js
Normal file
@@ -0,0 +1,189 @@
|
||||
import { spawn } from 'child_process';
|
||||
import { existsSync } from 'fs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const CLAUDE_BIN = '/home/fred/.local/bin/claude';
|
||||
const DEFAULT_MODEL = 'claude-sonnet-4-6';
|
||||
const activeExecutions = new Map();
|
||||
|
||||
function cleanEnv() {
|
||||
const env = { ...process.env };
|
||||
delete env.CLAUDECODE;
|
||||
delete env.ANTHROPIC_API_KEY;
|
||||
return env;
|
||||
}
|
||||
|
||||
function buildArgs(agentConfig, prompt) {
|
||||
const model = agentConfig.model || DEFAULT_MODEL;
|
||||
const args = ['-p', prompt, '--output-format', 'stream-json', '--verbose', '--model', model];
|
||||
|
||||
if (agentConfig.systemPrompt) {
|
||||
args.push('--system-prompt', agentConfig.systemPrompt);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
function buildPrompt(task, instructions) {
|
||||
const parts = [];
|
||||
if (task) parts.push(task);
|
||||
if (instructions) parts.push(`\nInstruções adicionais:\n${instructions}`);
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
function parseStreamLine(line) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) return null;
|
||||
try {
|
||||
return JSON.parse(trimmed);
|
||||
} catch {
|
||||
return { type: 'text', content: trimmed };
|
||||
}
|
||||
}
|
||||
|
||||
function extractText(event) {
|
||||
if (!event) return null;
|
||||
|
||||
if (event.type === 'assistant' && event.message?.content) {
|
||||
return event.message.content
|
||||
.filter((b) => b.type === 'text')
|
||||
.map((b) => b.text)
|
||||
.join('');
|
||||
}
|
||||
|
||||
if (event.type === 'content_block_delta' && event.delta?.text) {
|
||||
return event.delta.text;
|
||||
}
|
||||
|
||||
if (event.type === 'content_block_start' && event.content_block?.text) {
|
||||
return event.content_block.text;
|
||||
}
|
||||
|
||||
if (event.type === 'result') {
|
||||
if (typeof event.result === 'string') return event.result;
|
||||
if (event.result?.content) {
|
||||
return event.result.content
|
||||
.filter((b) => b.type === 'text')
|
||||
.map((b) => b.text)
|
||||
.join('');
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === 'text') return event.content || null;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function execute(agentConfig, task, callbacks = {}) {
|
||||
const executionId = uuidv4();
|
||||
const { onData, onError, onComplete } = callbacks;
|
||||
|
||||
const prompt = buildPrompt(task.description || task, task.instructions);
|
||||
const args = buildArgs(agentConfig, prompt);
|
||||
|
||||
const spawnOptions = {
|
||||
env: cleanEnv(),
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
};
|
||||
|
||||
if (agentConfig.workingDirectory && agentConfig.workingDirectory.trim()) {
|
||||
if (!existsSync(agentConfig.workingDirectory)) {
|
||||
const err = new Error(`Diretório de trabalho não encontrado: ${agentConfig.workingDirectory}`);
|
||||
if (onError) onError(err, executionId);
|
||||
return executionId;
|
||||
}
|
||||
spawnOptions.cwd = agentConfig.workingDirectory;
|
||||
}
|
||||
|
||||
console.log(`[executor] Iniciando: ${executionId}`);
|
||||
console.log(`[executor] Modelo: ${agentConfig.model || DEFAULT_MODEL}`);
|
||||
console.log(`[executor] cwd: ${spawnOptions.cwd || process.cwd()}`);
|
||||
|
||||
const child = spawn(CLAUDE_BIN, args, spawnOptions);
|
||||
let hadError = false;
|
||||
|
||||
activeExecutions.set(executionId, {
|
||||
process: child,
|
||||
agentConfig,
|
||||
task,
|
||||
startedAt: new Date().toISOString(),
|
||||
executionId,
|
||||
});
|
||||
|
||||
let outputBuffer = '';
|
||||
let errorBuffer = '';
|
||||
let fullText = '';
|
||||
|
||||
child.stdout.on('data', (chunk) => {
|
||||
const raw = chunk.toString();
|
||||
const lines = (outputBuffer + raw).split('\n');
|
||||
outputBuffer = lines.pop();
|
||||
|
||||
for (const line of lines) {
|
||||
const parsed = parseStreamLine(line);
|
||||
if (!parsed) continue;
|
||||
|
||||
const text = extractText(parsed);
|
||||
if (text) {
|
||||
fullText += text;
|
||||
if (onData) onData({ type: 'chunk', content: text }, executionId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on('data', (chunk) => {
|
||||
errorBuffer += chunk.toString();
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
console.log(`[executor][error] ${err.message}`);
|
||||
hadError = true;
|
||||
activeExecutions.delete(executionId);
|
||||
if (onError) onError(err, executionId);
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
console.log(`[executor][close] code=${code} hadError=${hadError}`);
|
||||
activeExecutions.delete(executionId);
|
||||
if (hadError) return;
|
||||
|
||||
if (outputBuffer.trim()) {
|
||||
const parsed = parseStreamLine(outputBuffer);
|
||||
if (parsed) {
|
||||
const text = extractText(parsed);
|
||||
if (text) fullText += text;
|
||||
}
|
||||
}
|
||||
|
||||
if (onComplete) {
|
||||
onComplete(
|
||||
{
|
||||
executionId,
|
||||
exitCode: code,
|
||||
result: fullText,
|
||||
stderr: errorBuffer,
|
||||
},
|
||||
executionId,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return executionId;
|
||||
}
|
||||
|
||||
export function cancel(executionId) {
|
||||
const execution = activeExecutions.get(executionId);
|
||||
if (!execution) return false;
|
||||
|
||||
execution.process.kill('SIGTERM');
|
||||
activeExecutions.delete(executionId);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getActiveExecutions() {
|
||||
return Array.from(activeExecutions.entries()).map(([id, exec]) => ({
|
||||
executionId: id,
|
||||
startedAt: exec.startedAt,
|
||||
agentConfig: exec.agentConfig,
|
||||
}));
|
||||
}
|
||||
185
src/agents/manager.js
Normal file
185
src/agents/manager.js
Normal file
@@ -0,0 +1,185 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { agentsStore } from '../store/db.js';
|
||||
import * as executor from './executor.js';
|
||||
import * as scheduler from './scheduler.js';
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
model: 'claude-sonnet-4-6',
|
||||
systemPrompt: '',
|
||||
workingDirectory: '',
|
||||
maxTokens: 16000,
|
||||
temperature: 1,
|
||||
};
|
||||
|
||||
function validateAgent(data) {
|
||||
const errors = [];
|
||||
if (!data.agent_name || typeof data.agent_name !== 'string') {
|
||||
errors.push('agent_name é obrigatório e deve ser uma string');
|
||||
}
|
||||
if (data.config?.model && typeof data.config.model !== 'string') {
|
||||
errors.push('config.model deve ser uma string');
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function getAllAgents() {
|
||||
return agentsStore.getAll();
|
||||
}
|
||||
|
||||
export function getAgentById(id) {
|
||||
return agentsStore.getById(id);
|
||||
}
|
||||
|
||||
export function createAgent(data) {
|
||||
const errors = validateAgent(data);
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join('; '));
|
||||
}
|
||||
|
||||
const agentData = {
|
||||
agent_name: data.agent_name,
|
||||
description: data.description || '',
|
||||
tasks: data.tasks || [],
|
||||
config: { ...DEFAULT_CONFIG, ...(data.config || {}) },
|
||||
status: data.status || 'active',
|
||||
assigned_host: data.assigned_host || 'localhost',
|
||||
executions: [],
|
||||
};
|
||||
|
||||
return agentsStore.create(agentData);
|
||||
}
|
||||
|
||||
export function updateAgent(id, data) {
|
||||
const existing = agentsStore.getById(id);
|
||||
if (!existing) return null;
|
||||
|
||||
const updateData = {};
|
||||
if (data.agent_name !== undefined) updateData.agent_name = data.agent_name;
|
||||
if (data.description !== undefined) updateData.description = data.description;
|
||||
if (data.tasks !== undefined) updateData.tasks = data.tasks;
|
||||
if (data.status !== undefined) updateData.status = data.status;
|
||||
if (data.assigned_host !== undefined) updateData.assigned_host = data.assigned_host;
|
||||
if (data.config !== undefined) {
|
||||
updateData.config = { ...existing.config, ...data.config };
|
||||
}
|
||||
|
||||
return agentsStore.update(id, updateData);
|
||||
}
|
||||
|
||||
export function deleteAgent(id) {
|
||||
return agentsStore.delete(id);
|
||||
}
|
||||
|
||||
export function executeTask(agentId, task, instructions, wsCallback) {
|
||||
const agent = agentsStore.getById(agentId);
|
||||
if (!agent) throw new Error(`Agente ${agentId} não encontrado`);
|
||||
if (agent.status !== 'active') throw new Error(`Agente ${agentId} está inativo`);
|
||||
|
||||
const executionRecord = {
|
||||
executionId: null,
|
||||
agentId,
|
||||
task: typeof task === 'string' ? task : task.description,
|
||||
startedAt: new Date().toISOString(),
|
||||
status: 'running',
|
||||
};
|
||||
|
||||
const executionId = executor.execute(
|
||||
agent.config,
|
||||
{ description: task, instructions },
|
||||
{
|
||||
onData: (parsed, execId) => {
|
||||
if (wsCallback) {
|
||||
wsCallback({
|
||||
type: 'execution_output',
|
||||
executionId: execId,
|
||||
agentId,
|
||||
data: parsed,
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (err, execId) => {
|
||||
updateAgentExecution(agentId, execId, { status: 'error', error: err.message, endedAt: new Date().toISOString() });
|
||||
if (wsCallback) {
|
||||
wsCallback({
|
||||
type: 'execution_error',
|
||||
executionId: execId,
|
||||
agentId,
|
||||
data: { error: err.message },
|
||||
});
|
||||
}
|
||||
},
|
||||
onComplete: (result, execId) => {
|
||||
updateAgentExecution(agentId, execId, { status: 'completed', result, endedAt: new Date().toISOString() });
|
||||
if (wsCallback) {
|
||||
wsCallback({
|
||||
type: 'execution_complete',
|
||||
executionId: execId,
|
||||
agentId,
|
||||
data: result,
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
executionRecord.executionId = executionId;
|
||||
|
||||
const updatedAgent = agentsStore.getById(agentId);
|
||||
const executions = [...(updatedAgent.executions || []), executionRecord];
|
||||
agentsStore.update(agentId, { executions: executions.slice(-100) });
|
||||
|
||||
return executionId;
|
||||
}
|
||||
|
||||
function updateAgentExecution(agentId, executionId, updates) {
|
||||
const agent = agentsStore.getById(agentId);
|
||||
if (!agent) return;
|
||||
|
||||
const executions = (agent.executions || []).map((exec) => {
|
||||
if (exec.executionId === executionId) {
|
||||
return { ...exec, ...updates };
|
||||
}
|
||||
return exec;
|
||||
});
|
||||
|
||||
agentsStore.update(agentId, { executions });
|
||||
}
|
||||
|
||||
export function scheduleTask(agentId, taskDescription, cronExpression, wsCallback) {
|
||||
const agent = agentsStore.getById(agentId);
|
||||
if (!agent) throw new Error(`Agente ${agentId} não encontrado`);
|
||||
|
||||
const scheduleId = uuidv4();
|
||||
|
||||
scheduler.schedule(scheduleId, cronExpression, () => {
|
||||
executeTask(agentId, taskDescription, null, wsCallback);
|
||||
});
|
||||
|
||||
return { scheduleId, agentId, taskDescription, cronExpression };
|
||||
}
|
||||
|
||||
export function cancelExecution(executionId) {
|
||||
return executor.cancel(executionId);
|
||||
}
|
||||
|
||||
export function getActiveExecutions() {
|
||||
return executor.getActiveExecutions();
|
||||
}
|
||||
|
||||
export function exportAgent(agentId) {
|
||||
const agent = agentsStore.getById(agentId);
|
||||
if (!agent) return null;
|
||||
|
||||
return {
|
||||
id: agent.id,
|
||||
agent_name: agent.agent_name,
|
||||
description: agent.description,
|
||||
tasks: agent.tasks,
|
||||
config: agent.config,
|
||||
status: agent.status,
|
||||
assigned_host: agent.assigned_host,
|
||||
created_at: agent.created_at,
|
||||
updated_at: agent.updated_at,
|
||||
executions: agent.executions || [],
|
||||
};
|
||||
}
|
||||
204
src/agents/pipeline.js
Normal file
204
src/agents/pipeline.js
Normal file
@@ -0,0 +1,204 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { pipelinesStore } from '../store/db.js';
|
||||
import { agentsStore } from '../store/db.js';
|
||||
import * as executor from './executor.js';
|
||||
|
||||
const activePipelines = new Map();
|
||||
|
||||
function validatePipeline(data) {
|
||||
const errors = [];
|
||||
if (!data.name || typeof data.name !== 'string') {
|
||||
errors.push('name é obrigatório e deve ser uma string');
|
||||
}
|
||||
if (!Array.isArray(data.steps) || data.steps.length === 0) {
|
||||
errors.push('steps é obrigatório e deve ser um array não vazio');
|
||||
} else {
|
||||
data.steps.forEach((step, index) => {
|
||||
if (!step.agentId) errors.push(`steps[${index}].agentId é obrigatório`);
|
||||
});
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
function buildSteps(steps) {
|
||||
return steps
|
||||
.map((step, index) => ({
|
||||
id: step.id || uuidv4(),
|
||||
agentId: step.agentId,
|
||||
order: step.order !== undefined ? step.order : index,
|
||||
inputTemplate: step.inputTemplate || null,
|
||||
description: step.description || '',
|
||||
}))
|
||||
.sort((a, b) => a.order - b.order);
|
||||
}
|
||||
|
||||
function applyTemplate(template, input) {
|
||||
if (!template) return input;
|
||||
return template.replace(/\{\{input\}\}/g, input);
|
||||
}
|
||||
|
||||
function executeStepAsPromise(agentConfig, prompt, pipelineState) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const executionId = executor.execute(
|
||||
agentConfig,
|
||||
{ description: prompt },
|
||||
{
|
||||
onData: () => {},
|
||||
onError: (err) => {
|
||||
reject(err);
|
||||
},
|
||||
onComplete: (result) => {
|
||||
resolve(result.result || '');
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
pipelineState.currentExecutionId = executionId;
|
||||
});
|
||||
}
|
||||
|
||||
export async function executePipeline(pipelineId, initialInput, wsCallback) {
|
||||
const pipeline = pipelinesStore.getById(pipelineId);
|
||||
if (!pipeline) throw new Error(`Pipeline ${pipelineId} não encontrado`);
|
||||
|
||||
const pipelineState = {
|
||||
currentExecutionId: null,
|
||||
currentStep: 0,
|
||||
canceled: false,
|
||||
};
|
||||
|
||||
activePipelines.set(pipelineId, pipelineState);
|
||||
|
||||
const steps = buildSteps(pipeline.steps);
|
||||
const results = [];
|
||||
let currentInput = initialInput;
|
||||
|
||||
try {
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
if (pipelineState.canceled) break;
|
||||
|
||||
const step = steps[i];
|
||||
pipelineState.currentStep = i;
|
||||
|
||||
const agent = agentsStore.getById(step.agentId);
|
||||
if (!agent) throw new Error(`Agente ${step.agentId} não encontrado no passo ${i}`);
|
||||
if (agent.status !== 'active') throw new Error(`Agente ${agent.agent_name} está inativo`);
|
||||
|
||||
const prompt = applyTemplate(step.inputTemplate, currentInput);
|
||||
|
||||
if (wsCallback) {
|
||||
wsCallback({
|
||||
type: 'pipeline_step_start',
|
||||
pipelineId,
|
||||
stepIndex: i,
|
||||
stepId: step.id,
|
||||
agentName: agent.agent_name,
|
||||
totalSteps: steps.length,
|
||||
});
|
||||
}
|
||||
|
||||
const result = await executeStepAsPromise(agent.config, prompt, pipelineState);
|
||||
|
||||
if (pipelineState.canceled) break;
|
||||
|
||||
currentInput = result;
|
||||
results.push({ stepId: step.id, agentName: agent.agent_name, result });
|
||||
|
||||
if (wsCallback) {
|
||||
wsCallback({
|
||||
type: 'pipeline_step_complete',
|
||||
pipelineId,
|
||||
stepIndex: i,
|
||||
stepId: step.id,
|
||||
result: result.slice(0, 500),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
activePipelines.delete(pipelineId);
|
||||
|
||||
if (!pipelineState.canceled && wsCallback) {
|
||||
wsCallback({
|
||||
type: 'pipeline_complete',
|
||||
pipelineId,
|
||||
results,
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
} catch (err) {
|
||||
activePipelines.delete(pipelineId);
|
||||
|
||||
if (wsCallback) {
|
||||
wsCallback({
|
||||
type: 'pipeline_error',
|
||||
pipelineId,
|
||||
stepIndex: pipelineState.currentStep,
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export function cancelPipeline(pipelineId) {
|
||||
const state = activePipelines.get(pipelineId);
|
||||
if (!state) return false;
|
||||
|
||||
state.canceled = true;
|
||||
|
||||
if (state.currentExecutionId) {
|
||||
executor.cancel(state.currentExecutionId);
|
||||
}
|
||||
|
||||
activePipelines.delete(pipelineId);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getActivePipelines() {
|
||||
return Array.from(activePipelines.entries()).map(([id, state]) => ({
|
||||
pipelineId: id,
|
||||
currentStep: state.currentStep,
|
||||
currentExecutionId: state.currentExecutionId,
|
||||
}));
|
||||
}
|
||||
|
||||
export function createPipeline(data) {
|
||||
const errors = validatePipeline(data);
|
||||
if (errors.length > 0) throw new Error(errors.join('; '));
|
||||
|
||||
const pipelineData = {
|
||||
name: data.name,
|
||||
description: data.description || '',
|
||||
steps: buildSteps(data.steps),
|
||||
status: data.status || 'active',
|
||||
};
|
||||
|
||||
return pipelinesStore.create(pipelineData);
|
||||
}
|
||||
|
||||
export function updatePipeline(id, data) {
|
||||
const existing = pipelinesStore.getById(id);
|
||||
if (!existing) return null;
|
||||
|
||||
const updateData = {};
|
||||
if (data.name !== undefined) updateData.name = data.name;
|
||||
if (data.description !== undefined) updateData.description = data.description;
|
||||
if (data.status !== undefined) updateData.status = data.status;
|
||||
if (data.steps !== undefined) updateData.steps = buildSteps(data.steps);
|
||||
|
||||
return pipelinesStore.update(id, updateData);
|
||||
}
|
||||
|
||||
export function deletePipeline(id) {
|
||||
return pipelinesStore.delete(id);
|
||||
}
|
||||
|
||||
export function getPipeline(id) {
|
||||
return pipelinesStore.getById(id);
|
||||
}
|
||||
|
||||
export function getAllPipelines() {
|
||||
return pipelinesStore.getAll();
|
||||
}
|
||||
84
src/agents/scheduler.js
Normal file
84
src/agents/scheduler.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import cron from 'node-cron';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
const HISTORY_LIMIT = 50;
|
||||
const schedules = new Map();
|
||||
const history = [];
|
||||
const emitter = new EventEmitter();
|
||||
|
||||
function addToHistory(entry) {
|
||||
history.unshift(entry);
|
||||
if (history.length > HISTORY_LIMIT) {
|
||||
history.splice(HISTORY_LIMIT);
|
||||
}
|
||||
}
|
||||
|
||||
export function schedule(taskId, cronExpr, callback) {
|
||||
if (schedules.has(taskId)) {
|
||||
unschedule(taskId);
|
||||
}
|
||||
|
||||
if (!cron.validate(cronExpr)) {
|
||||
throw new Error(`Expressão cron inválida: ${cronExpr}`);
|
||||
}
|
||||
|
||||
const task = cron.schedule(
|
||||
cronExpr,
|
||||
() => {
|
||||
const firedAt = new Date().toISOString();
|
||||
addToHistory({ taskId, cronExpr, firedAt });
|
||||
emitter.emit('scheduled-task', { taskId, firedAt });
|
||||
if (callback) callback({ taskId, firedAt });
|
||||
},
|
||||
{ scheduled: true }
|
||||
);
|
||||
|
||||
schedules.set(taskId, {
|
||||
taskId,
|
||||
cronExpr,
|
||||
task,
|
||||
active: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return { taskId, cronExpr };
|
||||
}
|
||||
|
||||
export function unschedule(taskId) {
|
||||
const entry = schedules.get(taskId);
|
||||
if (!entry) return false;
|
||||
|
||||
entry.task.stop();
|
||||
schedules.delete(taskId);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function setActive(taskId, active) {
|
||||
const entry = schedules.get(taskId);
|
||||
if (!entry) return false;
|
||||
|
||||
if (active) {
|
||||
entry.task.start();
|
||||
} else {
|
||||
entry.task.stop();
|
||||
}
|
||||
|
||||
entry.active = active;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getSchedules() {
|
||||
return Array.from(schedules.values()).map(({ task: _, ...rest }) => rest);
|
||||
}
|
||||
|
||||
export function getHistory() {
|
||||
return [...history];
|
||||
}
|
||||
|
||||
export function on(event, listener) {
|
||||
emitter.on(event, listener);
|
||||
}
|
||||
|
||||
export function off(event, listener) {
|
||||
emitter.off(event, listener);
|
||||
}
|
||||
280
src/routes/api.js
Normal file
280
src/routes/api.js
Normal file
@@ -0,0 +1,280 @@
|
||||
import { Router } from 'express';
|
||||
import * as manager from '../agents/manager.js';
|
||||
import { tasksStore } from '../store/db.js';
|
||||
import * as scheduler from '../agents/scheduler.js';
|
||||
import * as pipeline from '../agents/pipeline.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
let wsbroadcast = null;
|
||||
|
||||
export function setWsBroadcast(fn) {
|
||||
wsbroadcast = fn;
|
||||
}
|
||||
|
||||
function wsCallback(message) {
|
||||
if (wsbroadcast) wsbroadcast(message);
|
||||
}
|
||||
|
||||
router.get('/agents', (req, res) => {
|
||||
try {
|
||||
res.json(manager.getAllAgents());
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/agents/:id', (req, res) => {
|
||||
try {
|
||||
const agent = manager.getAgentById(req.params.id);
|
||||
if (!agent) return res.status(404).json({ error: 'Agente não encontrado' });
|
||||
res.json(agent);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/agents', (req, res) => {
|
||||
try {
|
||||
const agent = manager.createAgent(req.body);
|
||||
res.status(201).json(agent);
|
||||
} catch (err) {
|
||||
res.status(400).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/agents/:id', (req, res) => {
|
||||
try {
|
||||
const agent = manager.updateAgent(req.params.id, req.body);
|
||||
if (!agent) return res.status(404).json({ error: 'Agente não encontrado' });
|
||||
res.json(agent);
|
||||
} catch (err) {
|
||||
res.status(400).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/agents/:id', (req, res) => {
|
||||
try {
|
||||
const deleted = manager.deleteAgent(req.params.id);
|
||||
if (!deleted) return res.status(404).json({ error: 'Agente não encontrado' });
|
||||
res.status(204).send();
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/agents/:id/execute', (req, res) => {
|
||||
try {
|
||||
const { task, instructions } = req.body;
|
||||
if (!task) return res.status(400).json({ error: 'task é obrigatório' });
|
||||
|
||||
const executionId = manager.executeTask(req.params.id, task, instructions, wsCallback);
|
||||
res.status(202).json({ executionId, status: 'started' });
|
||||
} catch (err) {
|
||||
const status = err.message.includes('não encontrado') ? 404 : 400;
|
||||
res.status(status).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/agents/:id/cancel/:executionId', (req, res) => {
|
||||
try {
|
||||
const cancelled = manager.cancelExecution(req.params.executionId);
|
||||
if (!cancelled) return res.status(404).json({ error: 'Execução não encontrada ou já finalizada' });
|
||||
res.json({ cancelled: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/agents/:id/export', (req, res) => {
|
||||
try {
|
||||
const exported = manager.exportAgent(req.params.id);
|
||||
if (!exported) return res.status(404).json({ error: 'Agente não encontrado' });
|
||||
res.json(exported);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/tasks', (req, res) => {
|
||||
try {
|
||||
res.json(tasksStore.getAll());
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/tasks', (req, res) => {
|
||||
try {
|
||||
if (!req.body.name) return res.status(400).json({ error: 'name é obrigatório' });
|
||||
const task = tasksStore.create(req.body);
|
||||
res.status(201).json(task);
|
||||
} catch (err) {
|
||||
res.status(400).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/tasks/:id', (req, res) => {
|
||||
try {
|
||||
const task = tasksStore.update(req.params.id, req.body);
|
||||
if (!task) return res.status(404).json({ error: 'Tarefa não encontrada' });
|
||||
res.json(task);
|
||||
} catch (err) {
|
||||
res.status(400).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/tasks/:id', (req, res) => {
|
||||
try {
|
||||
const deleted = tasksStore.delete(req.params.id);
|
||||
if (!deleted) return res.status(404).json({ error: 'Tarefa não encontrada' });
|
||||
res.status(204).send();
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/schedules', (req, res) => {
|
||||
try {
|
||||
const { agentId, taskDescription, cronExpression } = req.body;
|
||||
if (!agentId || !taskDescription || !cronExpression) {
|
||||
return res.status(400).json({ error: 'agentId, taskDescription e cronExpression são obrigatórios' });
|
||||
}
|
||||
const result = manager.scheduleTask(agentId, taskDescription, cronExpression, wsCallback);
|
||||
res.status(201).json(result);
|
||||
} catch (err) {
|
||||
const status = err.message.includes('não encontrado') ? 404 : 400;
|
||||
res.status(status).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/schedules', (req, res) => {
|
||||
try {
|
||||
res.json(scheduler.getSchedules());
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/schedules/:taskId', (req, res) => {
|
||||
try {
|
||||
const removed = scheduler.unschedule(req.params.taskId);
|
||||
if (!removed) return res.status(404).json({ error: 'Agendamento não encontrado' });
|
||||
res.status(204).send();
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/pipelines', (req, res) => {
|
||||
try {
|
||||
res.json(pipeline.getAllPipelines());
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/pipelines/:id', (req, res) => {
|
||||
try {
|
||||
const found = pipeline.getPipeline(req.params.id);
|
||||
if (!found) return res.status(404).json({ error: 'Pipeline não encontrado' });
|
||||
res.json(found);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/pipelines', (req, res) => {
|
||||
try {
|
||||
const created = pipeline.createPipeline(req.body);
|
||||
res.status(201).json(created);
|
||||
} catch (err) {
|
||||
res.status(400).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/pipelines/:id', (req, res) => {
|
||||
try {
|
||||
const updated = pipeline.updatePipeline(req.params.id, req.body);
|
||||
if (!updated) return res.status(404).json({ error: 'Pipeline não encontrado' });
|
||||
res.json(updated);
|
||||
} catch (err) {
|
||||
res.status(400).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/pipelines/:id', (req, res) => {
|
||||
try {
|
||||
const deleted = pipeline.deletePipeline(req.params.id);
|
||||
if (!deleted) return res.status(404).json({ error: 'Pipeline não encontrado' });
|
||||
res.status(204).send();
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/pipelines/:id/execute', (req, res) => {
|
||||
try {
|
||||
const { input } = req.body;
|
||||
if (!input) return res.status(400).json({ error: 'input é obrigatório' });
|
||||
|
||||
pipeline.executePipeline(req.params.id, input, wsCallback).catch(() => {});
|
||||
res.status(202).json({ pipelineId: req.params.id, status: 'started' });
|
||||
} catch (err) {
|
||||
const status = err.message.includes('não encontrado') ? 404 : 400;
|
||||
res.status(status).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/pipelines/:id/cancel', (req, res) => {
|
||||
try {
|
||||
const cancelled = pipeline.cancelPipeline(req.params.id);
|
||||
if (!cancelled) return res.status(404).json({ error: 'Pipeline não está em execução' });
|
||||
res.json({ cancelled: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/system/status', (req, res) => {
|
||||
try {
|
||||
const agents = manager.getAllAgents();
|
||||
const activeExecutions = manager.getActiveExecutions();
|
||||
const schedules = scheduler.getSchedules();
|
||||
const pipelines = pipeline.getAllPipelines();
|
||||
const activePipelines = pipeline.getActivePipelines();
|
||||
|
||||
res.json({
|
||||
agents: {
|
||||
total: agents.length,
|
||||
active: agents.filter((a) => a.status === 'active').length,
|
||||
inactive: agents.filter((a) => a.status === 'inactive').length,
|
||||
},
|
||||
executions: {
|
||||
active: activeExecutions.length,
|
||||
list: activeExecutions,
|
||||
},
|
||||
schedules: {
|
||||
total: schedules.length,
|
||||
active: schedules.filter((s) => s.active).length,
|
||||
},
|
||||
pipelines: {
|
||||
total: pipelines.length,
|
||||
active: pipelines.filter((p) => p.status === 'active').length,
|
||||
running: activePipelines.length,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/executions/active', (req, res) => {
|
||||
try {
|
||||
res.json(manager.getActiveExecutions());
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
89
src/store/db.js
Normal file
89
src/store/db.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const DATA_DIR = `${__dirname}/../../data`;
|
||||
const AGENTS_FILE = `${DATA_DIR}/agents.json`;
|
||||
const TASKS_FILE = `${DATA_DIR}/tasks.json`;
|
||||
const PIPELINES_FILE = `${DATA_DIR}/pipelines.json`;
|
||||
|
||||
function ensureDataDir() {
|
||||
if (!existsSync(DATA_DIR)) {
|
||||
mkdirSync(DATA_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function loadFile(filePath) {
|
||||
ensureDataDir();
|
||||
if (!existsSync(filePath)) {
|
||||
writeFileSync(filePath, JSON.stringify([]), 'utf8');
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
return JSON.parse(readFileSync(filePath, 'utf8'));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function saveFile(filePath, data) {
|
||||
ensureDataDir();
|
||||
writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
function createStore(filePath) {
|
||||
return {
|
||||
load: () => loadFile(filePath),
|
||||
|
||||
save: (data) => saveFile(filePath, data),
|
||||
|
||||
getAll: () => loadFile(filePath),
|
||||
|
||||
getById: (id) => {
|
||||
const items = loadFile(filePath);
|
||||
return items.find((item) => item.id === id) || null;
|
||||
},
|
||||
|
||||
create: (data) => {
|
||||
const items = loadFile(filePath);
|
||||
const newItem = {
|
||||
id: uuidv4(),
|
||||
...data,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
items.push(newItem);
|
||||
saveFile(filePath, items);
|
||||
return newItem;
|
||||
},
|
||||
|
||||
update: (id, data) => {
|
||||
const items = loadFile(filePath);
|
||||
const index = items.findIndex((item) => item.id === id);
|
||||
if (index === -1) return null;
|
||||
items[index] = {
|
||||
...items[index],
|
||||
...data,
|
||||
id,
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
saveFile(filePath, items);
|
||||
return items[index];
|
||||
},
|
||||
|
||||
delete: (id) => {
|
||||
const items = loadFile(filePath);
|
||||
const index = items.findIndex((item) => item.id === id);
|
||||
if (index === -1) return false;
|
||||
items.splice(index, 1);
|
||||
saveFile(filePath, items);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const agentsStore = createStore(AGENTS_FILE);
|
||||
export const tasksStore = createStore(TASKS_FILE);
|
||||
export const pipelinesStore = createStore(PIPELINES_FILE);
|
||||
Reference in New Issue
Block a user