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:
Frederico Castro
2026-02-26 04:01:12 -03:00
parent 22a3ce9262
commit 93d9027e2c
18 changed files with 1609 additions and 75 deletions

View File

@@ -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);
}
});

View File

@@ -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}`);
}

View File

@@ -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,
}));
}

View File

@@ -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}`);

View File

@@ -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()}`,

View File

@@ -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`);