Continuação de conversa no terminal, histórico de agendamentos, webhooks e melhorias gerais
- Terminal com input de chat: após execução, permite continuar conversa com o agente via --resume do CLI, mantendo contexto da sessão (sessionId persistido) - Nova rota POST /api/agents/:id/continue para retomar sessões - Executor com função resume() para spawnar claude com --resume <sessionId> - Histórico de agendamentos agora busca do executionsStore (persistente) com dados completos: agente, tarefa, status, duração, custo e link para detalhes no modal - Execuções de agendamento tagueadas com source:'schedule' e scheduleId - Correção da expressão cron duplicada na UI de agendamentos - cronToHuman trata expressões com minuto específico (ex: 37 3 * * * → Todo dia às 03:37) - Botão "Copiar cURL" nos cards de webhook com payload de exemplo contextual - Webhooks component (webhooks.js) adicionado ao repositório
This commit is contained in:
@@ -38,6 +38,9 @@ function cleanEnv() {
|
||||
const env = { ...process.env };
|
||||
delete env.CLAUDECODE;
|
||||
delete env.ANTHROPIC_API_KEY;
|
||||
if (!env.CLAUDE_CODE_MAX_OUTPUT_TOKENS) {
|
||||
env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = '128000';
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
@@ -151,20 +154,31 @@ export function execute(agentConfig, task, callbacks = {}) {
|
||||
let outputBuffer = '';
|
||||
let errorBuffer = '';
|
||||
let fullText = '';
|
||||
let resultMeta = null;
|
||||
|
||||
function processEvent(parsed) {
|
||||
if (!parsed) return;
|
||||
const text = extractText(parsed);
|
||||
if (text) {
|
||||
fullText += text;
|
||||
if (onData) onData({ type: 'chunk', content: text }, executionId);
|
||||
}
|
||||
if (parsed.type === 'result') {
|
||||
resultMeta = {
|
||||
costUsd: parsed.cost_usd || 0,
|
||||
totalCostUsd: parsed.total_cost_usd || 0,
|
||||
durationMs: parsed.duration_ms || 0,
|
||||
durationApiMs: parsed.duration_api_ms || 0,
|
||||
numTurns: parsed.num_turns || 0,
|
||||
sessionId: parsed.session_id || '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
child.stdout.on('data', (chunk) => {
|
||||
const lines = (outputBuffer + chunk.toString()).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);
|
||||
}
|
||||
}
|
||||
for (const line of lines) processEvent(parseStreamLine(line));
|
||||
});
|
||||
|
||||
child.stderr.on('data', (chunk) => {
|
||||
@@ -182,16 +196,126 @@ export function execute(agentConfig, task, callbacks = {}) {
|
||||
activeExecutions.delete(executionId);
|
||||
if (hadError) return;
|
||||
|
||||
if (outputBuffer.trim()) {
|
||||
const parsed = parseStreamLine(outputBuffer);
|
||||
if (parsed) {
|
||||
const text = extractText(parsed);
|
||||
if (text) fullText += text;
|
||||
}
|
||||
}
|
||||
if (outputBuffer.trim()) processEvent(parseStreamLine(outputBuffer));
|
||||
|
||||
if (onComplete) {
|
||||
onComplete({ executionId, exitCode: code, result: fullText, stderr: errorBuffer }, executionId);
|
||||
onComplete({
|
||||
executionId,
|
||||
exitCode: code,
|
||||
result: fullText,
|
||||
stderr: errorBuffer,
|
||||
...(resultMeta || {}),
|
||||
}, executionId);
|
||||
}
|
||||
});
|
||||
|
||||
return executionId;
|
||||
}
|
||||
|
||||
export function resume(agentConfig, sessionId, message, callbacks = {}) {
|
||||
if (activeExecutions.size >= maxConcurrent) {
|
||||
const err = new Error(`Limite de ${maxConcurrent} execuções simultâneas atingido`);
|
||||
if (callbacks.onError) callbacks.onError(err, uuidv4());
|
||||
return null;
|
||||
}
|
||||
|
||||
const executionId = uuidv4();
|
||||
const { onData, onError, onComplete } = callbacks;
|
||||
|
||||
const model = agentConfig.model || 'claude-sonnet-4-6';
|
||||
const args = [
|
||||
'--resume', sessionId,
|
||||
'-p', sanitizeText(message),
|
||||
'--output-format', 'stream-json',
|
||||
'--verbose',
|
||||
'--model', model,
|
||||
'--permission-mode', agentConfig.permissionMode || 'bypassPermissions',
|
||||
];
|
||||
|
||||
if (agentConfig.maxTurns && agentConfig.maxTurns > 0) {
|
||||
args.push('--max-turns', String(agentConfig.maxTurns));
|
||||
}
|
||||
|
||||
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] Resumindo sessão: ${sessionId} | Execução: ${executionId}`);
|
||||
|
||||
const child = spawn(CLAUDE_BIN, args, spawnOptions);
|
||||
let hadError = false;
|
||||
|
||||
activeExecutions.set(executionId, {
|
||||
process: child,
|
||||
agentConfig,
|
||||
task: { description: message },
|
||||
startedAt: new Date().toISOString(),
|
||||
executionId,
|
||||
});
|
||||
|
||||
let outputBuffer = '';
|
||||
let errorBuffer = '';
|
||||
let fullText = '';
|
||||
let resultMeta = null;
|
||||
|
||||
function processEvent(parsed) {
|
||||
if (!parsed) return;
|
||||
const text = extractText(parsed);
|
||||
if (text) {
|
||||
fullText += text;
|
||||
if (onData) onData({ type: 'chunk', content: text }, executionId);
|
||||
}
|
||||
if (parsed.type === 'result') {
|
||||
resultMeta = {
|
||||
costUsd: parsed.cost_usd || 0,
|
||||
totalCostUsd: parsed.total_cost_usd || 0,
|
||||
durationMs: parsed.duration_ms || 0,
|
||||
durationApiMs: parsed.duration_api_ms || 0,
|
||||
numTurns: parsed.num_turns || 0,
|
||||
sessionId: parsed.session_id || sessionId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
child.stdout.on('data', (chunk) => {
|
||||
const lines = (outputBuffer + chunk.toString()).split('\n');
|
||||
outputBuffer = lines.pop();
|
||||
for (const line of lines) processEvent(parseStreamLine(line));
|
||||
});
|
||||
|
||||
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) => {
|
||||
activeExecutions.delete(executionId);
|
||||
if (hadError) return;
|
||||
if (outputBuffer.trim()) processEvent(parseStreamLine(outputBuffer));
|
||||
if (onComplete) {
|
||||
onComplete({
|
||||
executionId,
|
||||
exitCode: code,
|
||||
result: fullText,
|
||||
stderr: errorBuffer,
|
||||
...(resultMeta || {}),
|
||||
}, executionId);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ export function deleteAgent(id) {
|
||||
return agentsStore.delete(id);
|
||||
}
|
||||
|
||||
export function executeTask(agentId, task, instructions, wsCallback) {
|
||||
export function executeTask(agentId, task, instructions, wsCallback, metadata = {}) {
|
||||
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`);
|
||||
@@ -116,6 +116,7 @@ export function executeTask(agentId, task, instructions, wsCallback) {
|
||||
|
||||
const historyRecord = executionsStore.create({
|
||||
type: 'agent',
|
||||
...metadata,
|
||||
agentId,
|
||||
agentName: agent.agent_name,
|
||||
task: taskText,
|
||||
@@ -154,6 +155,11 @@ export function executeTask(agentId, task, instructions, wsCallback) {
|
||||
result: result.result || '',
|
||||
exitCode: result.exitCode,
|
||||
endedAt,
|
||||
costUsd: result.costUsd || 0,
|
||||
totalCostUsd: result.totalCostUsd || 0,
|
||||
durationMs: result.durationMs || 0,
|
||||
numTurns: result.numTurns || 0,
|
||||
sessionId: result.sessionId || '',
|
||||
});
|
||||
if (cb) cb({ type: 'execution_complete', executionId: execId, agentId, data: result });
|
||||
},
|
||||
@@ -216,7 +222,7 @@ export function scheduleTask(agentId, taskDescription, cronExpression, wsCallbac
|
||||
schedulesStore.save(items);
|
||||
|
||||
scheduler.schedule(scheduleId, cronExpression, () => {
|
||||
executeTask(agentId, taskDescription, null, null);
|
||||
executeTask(agentId, taskDescription, null, null, { source: 'schedule', scheduleId });
|
||||
}, false);
|
||||
|
||||
return { scheduleId, agentId, agentName: agent.agent_name, taskDescription, cronExpression };
|
||||
@@ -234,13 +240,71 @@ export function updateScheduleTask(scheduleId, data, wsCallback) {
|
||||
const cronExpression = data.cronExpression || stored.cronExpression;
|
||||
|
||||
scheduler.updateSchedule(scheduleId, cronExpression, () => {
|
||||
executeTask(agentId, taskDescription, null, null);
|
||||
executeTask(agentId, taskDescription, null, null, { source: 'schedule', scheduleId });
|
||||
});
|
||||
|
||||
schedulesStore.update(scheduleId, { agentId, agentName: agent.agent_name, taskDescription, cronExpression });
|
||||
return schedulesStore.getById(scheduleId);
|
||||
}
|
||||
|
||||
export function continueConversation(agentId, sessionId, message, wsCallback) {
|
||||
const agent = agentsStore.getById(agentId);
|
||||
if (!agent) throw new Error(`Agente ${agentId} não encontrado`);
|
||||
|
||||
const cb = getWsCallback(wsCallback);
|
||||
const startedAt = new Date().toISOString();
|
||||
|
||||
const historyRecord = executionsStore.create({
|
||||
type: 'agent',
|
||||
agentId,
|
||||
agentName: agent.agent_name,
|
||||
task: message,
|
||||
status: 'running',
|
||||
startedAt,
|
||||
parentSessionId: sessionId,
|
||||
});
|
||||
|
||||
const executionId = executor.resume(
|
||||
agent.config,
|
||||
sessionId,
|
||||
message,
|
||||
{
|
||||
onData: (parsed, execId) => {
|
||||
if (cb) cb({ type: 'execution_output', executionId: execId, agentId, data: parsed });
|
||||
},
|
||||
onError: (err, execId) => {
|
||||
const endedAt = new Date().toISOString();
|
||||
executionsStore.update(historyRecord.id, { status: 'error', error: err.message, endedAt });
|
||||
if (cb) cb({ type: 'execution_error', executionId: execId, agentId, data: { error: err.message } });
|
||||
},
|
||||
onComplete: (result, execId) => {
|
||||
const endedAt = new Date().toISOString();
|
||||
executionsStore.update(historyRecord.id, {
|
||||
status: 'completed',
|
||||
result: result.result || '',
|
||||
exitCode: result.exitCode,
|
||||
endedAt,
|
||||
costUsd: result.costUsd || 0,
|
||||
totalCostUsd: result.totalCostUsd || 0,
|
||||
durationMs: result.durationMs || 0,
|
||||
numTurns: result.numTurns || 0,
|
||||
sessionId: result.sessionId || sessionId,
|
||||
});
|
||||
if (cb) cb({ type: 'execution_complete', executionId: execId, agentId, data: result });
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!executionId) {
|
||||
executionsStore.update(historyRecord.id, { status: 'error', error: 'Limite de execuções simultâneas atingido', endedAt: new Date().toISOString() });
|
||||
throw new Error('Limite de execuções simultâneas atingido');
|
||||
}
|
||||
|
||||
executionsStore.update(historyRecord.id, { executionId });
|
||||
incrementDailyCount();
|
||||
return executionId;
|
||||
}
|
||||
|
||||
export function cancelExecution(executionId) {
|
||||
return executor.cancel(executionId);
|
||||
}
|
||||
@@ -278,9 +342,9 @@ export function importAgent(data) {
|
||||
}
|
||||
|
||||
export function restoreSchedules() {
|
||||
scheduler.restoreSchedules((agentId, taskDescription) => {
|
||||
scheduler.restoreSchedules((agentId, taskDescription, scheduleId) => {
|
||||
try {
|
||||
executeTask(agentId, taskDescription, null, null);
|
||||
executeTask(agentId, taskDescription, null, null, { source: 'schedule', scheduleId });
|
||||
} catch (err) {
|
||||
console.log(`[manager] Erro ao executar tarefa agendada: ${err.message}`);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ function buildSteps(steps) {
|
||||
order: step.order !== undefined ? step.order : index,
|
||||
inputTemplate: step.inputTemplate || null,
|
||||
description: step.description || '',
|
||||
requiresApproval: index === 0 ? false : !!step.requiresApproval,
|
||||
}))
|
||||
.sort((a, b) => a.order - b.order);
|
||||
}
|
||||
@@ -79,7 +80,12 @@ function executeStepAsPromise(agentConfig, prompt, pipelineState, wsCallback, pi
|
||||
reject(new Error(result.stderr || `Processo encerrado com código ${result.exitCode}`));
|
||||
return;
|
||||
}
|
||||
resolve(result.result || '');
|
||||
resolve({
|
||||
text: result.result || '',
|
||||
costUsd: result.costUsd || 0,
|
||||
durationMs: result.durationMs || 0,
|
||||
numTurns: result.numTurns || 0,
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -93,11 +99,53 @@ function executeStepAsPromise(agentConfig, prompt, pipelineState, wsCallback, pi
|
||||
});
|
||||
}
|
||||
|
||||
export async function executePipeline(pipelineId, initialInput, wsCallback) {
|
||||
function waitForApproval(pipelineId, stepIndex, previousOutput, agentName, wsCallback) {
|
||||
return new Promise((resolve) => {
|
||||
const state = activePipelines.get(pipelineId);
|
||||
if (!state) { resolve(false); return; }
|
||||
|
||||
state.pendingApproval = {
|
||||
stepIndex,
|
||||
previousOutput: previousOutput.slice(0, 3000),
|
||||
agentName,
|
||||
resolve,
|
||||
};
|
||||
|
||||
if (wsCallback) {
|
||||
wsCallback({
|
||||
type: 'pipeline_approval_required',
|
||||
pipelineId,
|
||||
stepIndex,
|
||||
agentName,
|
||||
previousOutput: previousOutput.slice(0, 3000),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function approvePipelineStep(pipelineId) {
|
||||
const state = activePipelines.get(pipelineId);
|
||||
if (!state?.pendingApproval) return false;
|
||||
const { resolve } = state.pendingApproval;
|
||||
state.pendingApproval = null;
|
||||
resolve(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function rejectPipelineStep(pipelineId) {
|
||||
const state = activePipelines.get(pipelineId);
|
||||
if (!state?.pendingApproval) return false;
|
||||
const { resolve } = state.pendingApproval;
|
||||
state.pendingApproval = null;
|
||||
resolve(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function executePipeline(pipelineId, initialInput, wsCallback, options = {}) {
|
||||
const pl = pipelinesStore.getById(pipelineId);
|
||||
if (!pl) throw new Error(`Pipeline ${pipelineId} não encontrado`);
|
||||
|
||||
const pipelineState = { currentExecutionId: null, currentStep: 0, canceled: false };
|
||||
const pipelineState = { currentExecutionId: null, currentStep: 0, canceled: false, pendingApproval: null };
|
||||
activePipelines.set(pipelineId, pipelineState);
|
||||
|
||||
const historyRecord = executionsStore.create({
|
||||
@@ -108,11 +156,13 @@ export async function executePipeline(pipelineId, initialInput, wsCallback) {
|
||||
status: 'running',
|
||||
startedAt: new Date().toISOString(),
|
||||
steps: [],
|
||||
totalCostUsd: 0,
|
||||
});
|
||||
|
||||
const steps = buildSteps(pl.steps);
|
||||
const results = [];
|
||||
let currentInput = initialInput;
|
||||
let totalCost = 0;
|
||||
|
||||
try {
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
@@ -121,10 +171,40 @@ export async function executePipeline(pipelineId, initialInput, wsCallback) {
|
||||
const step = steps[i];
|
||||
pipelineState.currentStep = i;
|
||||
|
||||
if (step.requiresApproval && i > 0) {
|
||||
const prevAgentName = results.length > 0 ? results[results.length - 1].agentName : '';
|
||||
|
||||
executionsStore.update(historyRecord.id, { status: 'awaiting_approval' });
|
||||
|
||||
if (wsCallback) {
|
||||
wsCallback({ type: 'pipeline_status', pipelineId, status: 'awaiting_approval', stepIndex: i });
|
||||
}
|
||||
|
||||
const approved = await waitForApproval(pipelineId, i, currentInput, prevAgentName, wsCallback);
|
||||
|
||||
if (!approved) {
|
||||
pipelineState.canceled = true;
|
||||
executionsStore.update(historyRecord.id, { status: 'rejected', endedAt: new Date().toISOString(), totalCostUsd: totalCost });
|
||||
if (wsCallback) {
|
||||
wsCallback({ type: 'pipeline_rejected', pipelineId, stepIndex: i });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
executionsStore.update(historyRecord.id, { status: 'running' });
|
||||
}
|
||||
|
||||
if (pipelineState.canceled) break;
|
||||
|
||||
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 stepConfig = { ...agent.config };
|
||||
if (options.workingDirectory) {
|
||||
stepConfig.workingDirectory = options.workingDirectory;
|
||||
}
|
||||
|
||||
const prompt = applyTemplate(step.inputTemplate, currentInput);
|
||||
const stepStart = new Date().toISOString();
|
||||
|
||||
@@ -139,12 +219,13 @@ export async function executePipeline(pipelineId, initialInput, wsCallback) {
|
||||
});
|
||||
}
|
||||
|
||||
const result = await executeStepAsPromise(agent.config, prompt, pipelineState, wsCallback, pipelineId, i);
|
||||
const stepResult = await executeStepAsPromise(stepConfig, prompt, pipelineState, wsCallback, pipelineId, i);
|
||||
|
||||
if (pipelineState.canceled) break;
|
||||
|
||||
currentInput = result;
|
||||
results.push({ stepId: step.id, agentName: agent.agent_name, result });
|
||||
totalCost += stepResult.costUsd;
|
||||
currentInput = stepResult.text;
|
||||
results.push({ stepId: step.id, agentName: agent.agent_name, result: stepResult.text });
|
||||
|
||||
const current = executionsStore.getById(historyRecord.id);
|
||||
const savedSteps = current ? (current.steps || []) : [];
|
||||
@@ -153,12 +234,15 @@ export async function executePipeline(pipelineId, initialInput, wsCallback) {
|
||||
agentId: step.agentId,
|
||||
agentName: agent.agent_name,
|
||||
prompt: prompt.slice(0, 5000),
|
||||
result,
|
||||
result: stepResult.text,
|
||||
startedAt: stepStart,
|
||||
endedAt: new Date().toISOString(),
|
||||
status: 'completed',
|
||||
costUsd: stepResult.costUsd,
|
||||
durationMs: stepResult.durationMs,
|
||||
numTurns: stepResult.numTurns,
|
||||
});
|
||||
executionsStore.update(historyRecord.id, { steps: savedSteps });
|
||||
executionsStore.update(historyRecord.id, { steps: savedSteps, totalCostUsd: totalCost });
|
||||
|
||||
if (wsCallback) {
|
||||
wsCallback({
|
||||
@@ -166,19 +250,23 @@ export async function executePipeline(pipelineId, initialInput, wsCallback) {
|
||||
pipelineId,
|
||||
stepIndex: i,
|
||||
stepId: step.id,
|
||||
result: result.slice(0, 500),
|
||||
result: stepResult.text.slice(0, 500),
|
||||
costUsd: stepResult.costUsd,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
activePipelines.delete(pipelineId);
|
||||
|
||||
const finalStatus = pipelineState.canceled ? 'canceled' : 'completed';
|
||||
executionsStore.update(historyRecord.id, {
|
||||
status: pipelineState.canceled ? 'canceled' : 'completed',
|
||||
status: finalStatus,
|
||||
endedAt: new Date().toISOString(),
|
||||
totalCostUsd: totalCost,
|
||||
});
|
||||
|
||||
if (!pipelineState.canceled && wsCallback) {
|
||||
wsCallback({ type: 'pipeline_complete', pipelineId, results });
|
||||
wsCallback({ type: 'pipeline_complete', pipelineId, results, totalCostUsd: totalCost });
|
||||
}
|
||||
|
||||
return results;
|
||||
@@ -188,6 +276,7 @@ export async function executePipeline(pipelineId, initialInput, wsCallback) {
|
||||
status: 'error',
|
||||
error: err.message,
|
||||
endedAt: new Date().toISOString(),
|
||||
totalCostUsd: totalCost,
|
||||
});
|
||||
if (wsCallback) {
|
||||
wsCallback({
|
||||
@@ -205,6 +294,10 @@ export function cancelPipeline(pipelineId) {
|
||||
const state = activePipelines.get(pipelineId);
|
||||
if (!state) return false;
|
||||
state.canceled = true;
|
||||
if (state.pendingApproval) {
|
||||
state.pendingApproval.resolve(false);
|
||||
state.pendingApproval = null;
|
||||
}
|
||||
if (state.currentExecutionId) executor.cancel(state.currentExecutionId);
|
||||
activePipelines.delete(pipelineId);
|
||||
return true;
|
||||
@@ -215,6 +308,7 @@ export function getActivePipelines() {
|
||||
pipelineId: id,
|
||||
currentStep: state.currentStep,
|
||||
currentExecutionId: state.currentExecutionId,
|
||||
pendingApproval: !!state.pendingApproval,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ export function restoreSchedules(executeFn) {
|
||||
if (!s.active) continue;
|
||||
const cronExpr = s.cronExpression || s.cronExpr;
|
||||
try {
|
||||
schedule(s.id, cronExpr, () => executeFn(s.agentId, s.taskDescription), false);
|
||||
schedule(s.id, cronExpr, () => executeFn(s.agentId, s.taskDescription, s.id), false);
|
||||
restored++;
|
||||
} catch (err) {
|
||||
console.log(`[scheduler] Falha ao restaurar ${s.id}: ${err.message}`);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Router } from 'express';
|
||||
import { execSync } from 'child_process';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import crypto from 'crypto';
|
||||
import os from 'os';
|
||||
import * as manager from '../agents/manager.js';
|
||||
import { tasksStore, settingsStore, executionsStore } from '../store/db.js';
|
||||
import { tasksStore, settingsStore, executionsStore, webhooksStore } from '../store/db.js';
|
||||
import * as scheduler from '../agents/scheduler.js';
|
||||
import * as pipeline from '../agents/pipeline.js';
|
||||
import { getBinPath, updateMaxConcurrent } from '../agents/executor.js';
|
||||
@@ -10,6 +12,7 @@ import { invalidateAgentMapCache } from '../agents/pipeline.js';
|
||||
import { cached } from '../cache/index.js';
|
||||
|
||||
const router = Router();
|
||||
export const hookRouter = Router();
|
||||
|
||||
let wsbroadcast = null;
|
||||
let wsBroadcastTo = null;
|
||||
@@ -126,6 +129,20 @@ router.post('/agents/:id/execute', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/agents/:id/continue', (req, res) => {
|
||||
try {
|
||||
const { sessionId, message } = req.body;
|
||||
if (!sessionId) return res.status(400).json({ error: 'sessionId é obrigatório' });
|
||||
if (!message) return res.status(400).json({ error: 'message é obrigatório' });
|
||||
const clientId = req.headers['x-client-id'] || null;
|
||||
const executionId = manager.continueConversation(req.params.id, sessionId, message, (msg) => wsCallback(msg, clientId));
|
||||
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);
|
||||
@@ -200,7 +217,12 @@ router.post('/schedules', (req, res) => {
|
||||
|
||||
router.get('/schedules/history', (req, res) => {
|
||||
try {
|
||||
res.json(scheduler.getHistory());
|
||||
const limit = parseInt(req.query.limit) || 20;
|
||||
const items = executionsStore.getAll()
|
||||
.filter((e) => e.source === 'schedule')
|
||||
.sort((a, b) => new Date(b.startedAt) - new Date(a.startedAt))
|
||||
.slice(0, limit);
|
||||
res.json(items);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
@@ -284,10 +306,12 @@ router.delete('/pipelines/:id', (req, res) => {
|
||||
|
||||
router.post('/pipelines/:id/execute', (req, res) => {
|
||||
try {
|
||||
const { input } = req.body;
|
||||
const { input, workingDirectory } = req.body;
|
||||
if (!input) return res.status(400).json({ error: 'input é obrigatório' });
|
||||
const clientId = req.headers['x-client-id'] || null;
|
||||
pipeline.executePipeline(req.params.id, input, (msg) => wsCallback(msg, clientId)).catch(() => {});
|
||||
const options = {};
|
||||
if (workingDirectory) options.workingDirectory = workingDirectory;
|
||||
pipeline.executePipeline(req.params.id, input, (msg) => wsCallback(msg, clientId), options).catch(() => {});
|
||||
res.status(202).json({ pipelineId: req.params.id, status: 'started' });
|
||||
} catch (err) {
|
||||
const status = err.message.includes('não encontrado') ? 404 : 400;
|
||||
@@ -305,6 +329,174 @@ router.post('/pipelines/:id/cancel', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/pipelines/:id/approve', (req, res) => {
|
||||
try {
|
||||
const approved = pipeline.approvePipelineStep(req.params.id);
|
||||
if (!approved) return res.status(404).json({ error: 'Nenhuma aprovação pendente para este pipeline' });
|
||||
res.json({ approved: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/pipelines/:id/reject', (req, res) => {
|
||||
try {
|
||||
const rejected = pipeline.rejectPipelineStep(req.params.id);
|
||||
if (!rejected) return res.status(404).json({ error: 'Nenhuma aprovação pendente para este pipeline' });
|
||||
res.json({ rejected: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/webhooks', (req, res) => {
|
||||
try {
|
||||
res.json(webhooksStore.getAll());
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/webhooks', (req, res) => {
|
||||
try {
|
||||
const { name, targetType, targetId } = req.body;
|
||||
if (!name || !targetType || !targetId) {
|
||||
return res.status(400).json({ error: 'name, targetType e targetId são obrigatórios' });
|
||||
}
|
||||
if (!['agent', 'pipeline'].includes(targetType)) {
|
||||
return res.status(400).json({ error: 'targetType deve ser "agent" ou "pipeline"' });
|
||||
}
|
||||
|
||||
const token = crypto.randomBytes(24).toString('hex');
|
||||
const webhook = webhooksStore.create({
|
||||
name,
|
||||
targetType,
|
||||
targetId,
|
||||
token,
|
||||
active: true,
|
||||
lastTriggeredAt: null,
|
||||
triggerCount: 0,
|
||||
});
|
||||
|
||||
res.status(201).json(webhook);
|
||||
} catch (err) {
|
||||
res.status(400).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/webhooks/:id', (req, res) => {
|
||||
try {
|
||||
const existing = webhooksStore.getById(req.params.id);
|
||||
if (!existing) return res.status(404).json({ error: 'Webhook não encontrado' });
|
||||
|
||||
const updateData = {};
|
||||
if (req.body.name !== undefined) updateData.name = req.body.name;
|
||||
if (req.body.active !== undefined) updateData.active = !!req.body.active;
|
||||
|
||||
const updated = webhooksStore.update(req.params.id, updateData);
|
||||
res.json(updated);
|
||||
} catch (err) {
|
||||
res.status(400).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/webhooks/:id', (req, res) => {
|
||||
try {
|
||||
const deleted = webhooksStore.delete(req.params.id);
|
||||
if (!deleted) return res.status(404).json({ error: 'Webhook não encontrado' });
|
||||
res.status(204).send();
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
hookRouter.post('/:token', (req, res) => {
|
||||
try {
|
||||
const webhooks = webhooksStore.getAll();
|
||||
const webhook = webhooks.find((w) => w.token === req.params.token);
|
||||
|
||||
if (!webhook) return res.status(404).json({ error: 'Webhook não encontrado' });
|
||||
if (!webhook.active) return res.status(403).json({ error: 'Webhook desativado' });
|
||||
|
||||
webhooksStore.update(webhook.id, {
|
||||
lastTriggeredAt: new Date().toISOString(),
|
||||
triggerCount: (webhook.triggerCount || 0) + 1,
|
||||
});
|
||||
|
||||
const payload = req.body || {};
|
||||
|
||||
if (webhook.targetType === 'agent') {
|
||||
const task = payload.task || payload.message || payload.input || 'Webhook trigger';
|
||||
const instructions = payload.instructions || '';
|
||||
const executionId = manager.executeTask(webhook.targetId, task, instructions, (msg) => {
|
||||
if (wsbroadcast) wsbroadcast(msg);
|
||||
});
|
||||
res.status(202).json({ executionId, status: 'started', webhook: webhook.name });
|
||||
} else if (webhook.targetType === 'pipeline') {
|
||||
const input = payload.input || payload.task || payload.message || 'Webhook trigger';
|
||||
const options = {};
|
||||
if (payload.workingDirectory) options.workingDirectory = payload.workingDirectory;
|
||||
pipeline.executePipeline(webhook.targetId, input, (msg) => {
|
||||
if (wsbroadcast) wsbroadcast(msg);
|
||||
}, options).catch(() => {});
|
||||
res.status(202).json({ pipelineId: webhook.targetId, status: 'started', webhook: webhook.name });
|
||||
}
|
||||
} catch (err) {
|
||||
const status = err.message.includes('não encontrado') ? 404 : 500;
|
||||
res.status(status).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/stats/costs', (req, res) => {
|
||||
try {
|
||||
const days = parseInt(req.query.days) || 30;
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - days);
|
||||
|
||||
const items = executionsStore.getAll().filter((e) => {
|
||||
if (!e.startedAt) return false;
|
||||
return new Date(e.startedAt) >= cutoff;
|
||||
});
|
||||
|
||||
let totalCost = 0;
|
||||
let totalExecutions = 0;
|
||||
const byAgent = {};
|
||||
const byDay = {};
|
||||
|
||||
for (const item of items) {
|
||||
const cost = item.costUsd || item.totalCostUsd || 0;
|
||||
if (cost <= 0) continue;
|
||||
|
||||
totalCost += cost;
|
||||
totalExecutions++;
|
||||
|
||||
const agentName = item.agentName || item.pipelineName || 'Desconhecido';
|
||||
if (!byAgent[agentName]) byAgent[agentName] = { cost: 0, count: 0 };
|
||||
byAgent[agentName].cost += cost;
|
||||
byAgent[agentName].count++;
|
||||
|
||||
const day = item.startedAt.slice(0, 10);
|
||||
if (!byDay[day]) byDay[day] = 0;
|
||||
byDay[day] += cost;
|
||||
}
|
||||
|
||||
const topAgents = Object.entries(byAgent)
|
||||
.map(([name, data]) => ({ name, ...data }))
|
||||
.sort((a, b) => b.cost - a.cost)
|
||||
.slice(0, 10);
|
||||
|
||||
res.json({
|
||||
totalCost: Math.round(totalCost * 10000) / 10000,
|
||||
totalExecutions,
|
||||
period: days,
|
||||
topAgents,
|
||||
dailyCosts: byDay,
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
const SYSTEM_STATUS_TTL = 5_000;
|
||||
|
||||
router.get('/system/status', (req, res) => {
|
||||
@@ -315,6 +507,15 @@ router.get('/system/status', (req, res) => {
|
||||
const schedules = scheduler.getSchedules();
|
||||
const pipelines = pipeline.getAllPipelines();
|
||||
const activePipelines = pipeline.getActivePipelines();
|
||||
const webhooks = webhooksStore.getAll();
|
||||
|
||||
const todayCost = (() => {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
return executionsStore.getAll()
|
||||
.filter((e) => e.startedAt && e.startedAt.startsWith(today))
|
||||
.reduce((sum, e) => sum + (e.costUsd || e.totalCostUsd || 0), 0);
|
||||
})();
|
||||
|
||||
return {
|
||||
agents: {
|
||||
total: agents.length,
|
||||
@@ -335,6 +536,13 @@ router.get('/system/status', (req, res) => {
|
||||
active: pipelines.filter((p) => p.status === 'active').length,
|
||||
running: activePipelines.length,
|
||||
},
|
||||
webhooks: {
|
||||
total: webhooks.length,
|
||||
active: webhooks.filter((w) => w.active).length,
|
||||
},
|
||||
costs: {
|
||||
today: Math.round(todayCost * 10000) / 10000,
|
||||
},
|
||||
};
|
||||
});
|
||||
res.json(status);
|
||||
@@ -355,7 +563,7 @@ router.get('/system/info', (req, res) => {
|
||||
}
|
||||
}
|
||||
res.json({
|
||||
serverVersion: '1.0.0',
|
||||
serverVersion: '1.1.0',
|
||||
nodeVersion: process.version,
|
||||
claudeVersion: claudeVersionCache,
|
||||
platform: `${os.platform()} ${os.arch()}`,
|
||||
|
||||
@@ -185,4 +185,5 @@ export const tasksStore = createStore(`${DATA_DIR}/tasks.json`);
|
||||
export const pipelinesStore = createStore(`${DATA_DIR}/pipelines.json`);
|
||||
export const schedulesStore = createStore(`${DATA_DIR}/schedules.json`);
|
||||
export const executionsStore = createStore(`${DATA_DIR}/executions.json`);
|
||||
export const webhooksStore = createStore(`${DATA_DIR}/webhooks.json`);
|
||||
export const settingsStore = createSettingsStore(`${DATA_DIR}/settings.json`);
|
||||
|
||||
Reference in New Issue
Block a user