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:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "agents-orchestrator",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"description": "Painel administrativo para orquestração de agentes Claude Code",
|
||||
"main": "server.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -1029,6 +1029,17 @@ textarea {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.terminal-line.user-message {
|
||||
border-left: 3px solid var(--accent);
|
||||
padding-left: 8px;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.terminal-line.user-message .content {
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.terminal-line.info .content {
|
||||
color: var(--info);
|
||||
}
|
||||
@@ -2004,6 +2015,20 @@ tbody tr:hover td {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.metric-card-icon--yellow {
|
||||
background-color: rgba(250, 204, 21, 0.12);
|
||||
color: #facc15;
|
||||
}
|
||||
|
||||
.metric-card-icon--cyan {
|
||||
background-color: rgba(6, 182, 212, 0.12);
|
||||
color: #06b6d4;
|
||||
}
|
||||
|
||||
.metric-card-value--sm {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.metric-card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -2619,6 +2644,55 @@ tbody tr:hover td {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.terminal-input-bar {
|
||||
border-top: 1px solid var(--border);
|
||||
background-color: #0c0c14;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
|
||||
.terminal-input-context {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 6px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.terminal-input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.terminal-input-prompt {
|
||||
color: var(--accent);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.terminal-input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
color: var(--text-primary);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.terminal-input:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.terminal-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
@@ -3721,3 +3795,250 @@ tbody tr:hover td {
|
||||
padding: 5px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.approval-notification {
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(135deg, rgba(245, 158, 11, 0.12), rgba(245, 158, 11, 0.05));
|
||||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.approval-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.approval-icon {
|
||||
color: var(--warning);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.approval-icon svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.approval-text {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.approval-text strong {
|
||||
font-size: 13px;
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.approval-text span {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.approval-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.webhook-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.webhook-card:hover {
|
||||
border-color: var(--border-secondary);
|
||||
}
|
||||
|
||||
.webhook-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.webhook-card-identity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.webhook-card-name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.webhook-card-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.webhook-card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.webhook-card-target,
|
||||
.webhook-card-url {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.webhook-card-label {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.webhook-card-value {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.webhook-url-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.webhook-url-code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px;
|
||||
color: var(--accent);
|
||||
background: var(--bg-input);
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.webhook-card-curl {
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.webhook-card-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--border-primary);
|
||||
}
|
||||
|
||||
.webhook-meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.pipeline-step-approval {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--warning);
|
||||
cursor: pointer;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.pipeline-step-approval input[type="checkbox"] {
|
||||
accent-color: var(--warning);
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cost-value {
|
||||
color: #facc15;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.activity-item-cost {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px;
|
||||
color: #facc15;
|
||||
}
|
||||
|
||||
.history-card-cost {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: #facc15;
|
||||
}
|
||||
|
||||
.history-card-cost svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.history-card-duration-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pipeline-step-meta-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pipeline-step-cost {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: #facc15;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background-color: rgba(245, 158, 11, 0.15);
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.badge--yellow {
|
||||
background-color: rgba(245, 158, 11, 0.15);
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.form-hint-block {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.form-hint-block p {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.form-hint-block p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-hint-code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px;
|
||||
color: var(--accent);
|
||||
background: var(--bg-input);
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
@@ -53,6 +53,12 @@
|
||||
<span>Pipelines</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-nav-item">
|
||||
<a href="#" class="sidebar-nav-link" data-section="webhooks">
|
||||
<i data-lucide="webhook"></i>
|
||||
<span>Webhooks</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-nav-item">
|
||||
<a href="#" class="sidebar-nav-link" data-section="terminal">
|
||||
<i data-lucide="terminal"></i>
|
||||
@@ -148,6 +154,24 @@
|
||||
<span class="metric-card-value" id="metric-schedules">0</span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<div class="metric-card-icon metric-card-icon--yellow">
|
||||
<i data-lucide="dollar-sign"></i>
|
||||
</div>
|
||||
<div class="metric-card-body">
|
||||
<span class="metric-card-label">Custo Hoje</span>
|
||||
<span class="metric-card-value metric-card-value--sm" id="metric-cost-today">$0.0000</span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<div class="metric-card-icon metric-card-icon--cyan">
|
||||
<i data-lucide="webhook"></i>
|
||||
</div>
|
||||
<div class="metric-card-body">
|
||||
<span class="metric-card-label">Webhooks Ativos</span>
|
||||
<span class="metric-card-value" id="metric-webhooks">0</span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
@@ -361,6 +385,22 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="webhooks" class="section" aria-label="Webhooks" hidden>
|
||||
<div class="section-toolbar">
|
||||
<div class="search-field">
|
||||
<i data-lucide="search"></i>
|
||||
<input type="search" placeholder="Buscar webhooks..." id="webhooks-search" aria-label="Buscar webhooks" />
|
||||
</div>
|
||||
<div class="section-toolbar-actions">
|
||||
<button class="btn btn--primary btn--icon-text" type="button" id="webhooks-new-btn">
|
||||
<i data-lucide="plus"></i>
|
||||
<span>Novo Webhook</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="webhooks-list"></div>
|
||||
</section>
|
||||
|
||||
<section id="terminal" class="section" aria-label="Terminal" hidden>
|
||||
<div class="terminal-wrapper">
|
||||
<div class="terminal-toolbar">
|
||||
@@ -384,12 +424,30 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="approval-notification" id="approval-notification" hidden></div>
|
||||
<div class="terminal-output" id="terminal-output" role="log" aria-live="polite" aria-label="Saída do terminal">
|
||||
<div class="terminal-welcome">
|
||||
<span class="terminal-prompt">$</span>
|
||||
<span class="terminal-text">Aguardando execução de agente...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="terminal-input-bar" id="terminal-input-bar" hidden>
|
||||
<div class="terminal-input-context" id="terminal-input-context"></div>
|
||||
<div class="terminal-input-row">
|
||||
<span class="terminal-input-prompt">❯</span>
|
||||
<input
|
||||
type="text"
|
||||
class="terminal-input"
|
||||
id="terminal-input"
|
||||
placeholder="Continuar conversa com o agente..."
|
||||
aria-label="Enviar mensagem ao agente"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button class="btn btn--primary btn--sm" id="terminal-send-btn" type="button" aria-label="Enviar">
|
||||
<i data-lucide="send"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -890,6 +948,17 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="pipeline-execute-id">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="pipeline-execute-workdir">Diretório de Trabalho</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
id="pipeline-execute-workdir"
|
||||
placeholder="/home/fred/projetos/meu-projeto"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<p class="form-hint">Todos os agentes da pipeline vão trabalhar neste diretório. Se vazio, cada agente usa seu próprio.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="pipeline-execute-input">
|
||||
Input Inicial
|
||||
@@ -998,6 +1067,53 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="webhook-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="webhook-modal-title" hidden>
|
||||
<div class="modal modal--md">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="webhook-modal-title">Novo Webhook</h2>
|
||||
<button class="modal-close" type="button" aria-label="Fechar modal" data-modal-close="webhook-modal-overlay">
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="modal-form" id="webhook-form" novalidate>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="webhook-name">
|
||||
Nome
|
||||
<span class="form-required" aria-hidden="true">*</span>
|
||||
</label>
|
||||
<input type="text" class="input" id="webhook-name" placeholder="Ex: Deploy Trigger" required autocomplete="off" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="webhook-target-type">Tipo de Destino</label>
|
||||
<select class="select" id="webhook-target-type">
|
||||
<option value="agent">Agente</option>
|
||||
<option value="pipeline">Pipeline</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="webhook-target-id">
|
||||
Destino
|
||||
<span class="form-required" aria-hidden="true">*</span>
|
||||
</label>
|
||||
<select class="select" id="webhook-target-id" required>
|
||||
<option value="">Selecionar...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-hint-block">
|
||||
<p>Após criar, você receberá uma URL única para disparar este webhook via POST.</p>
|
||||
<p><strong>Payload aceito:</strong></p>
|
||||
<pre class="form-hint-code">{ "task": "descrição", "instructions": "..." }</pre>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn--ghost" type="button" data-modal-close="webhook-modal-overlay">Cancelar</button>
|
||||
<button class="btn btn--primary" type="button" id="webhook-form-submit">Criar Webhook</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast-container" id="toast-container" aria-live="assertive" aria-atomic="false" role="region" aria-label="Notificações"></div>
|
||||
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
|
||||
@@ -1012,6 +1128,7 @@
|
||||
<script src="js/components/pipelines.js"></script>
|
||||
<script src="js/components/settings.js"></script>
|
||||
<script src="js/components/history.js"></script>
|
||||
<script src="js/components/webhooks.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
<script>
|
||||
lucide.createIcons();
|
||||
|
||||
@@ -40,6 +40,7 @@ const API = {
|
||||
delete(id) { return API.request('DELETE', `/agents/${id}`); },
|
||||
execute(id, task, instructions) { return API.request('POST', `/agents/${id}/execute`, { task, instructions }); },
|
||||
cancel(id, executionId) { return API.request('POST', `/agents/${id}/cancel/${executionId}`); },
|
||||
continue(id, sessionId, message) { return API.request('POST', `/agents/${id}/continue`, { sessionId, message }); },
|
||||
export(id) { return API.request('GET', `/agents/${id}/export`); },
|
||||
import(data) { return API.request('POST', '/agents/import', data); },
|
||||
},
|
||||
@@ -65,8 +66,25 @@ const API = {
|
||||
create(data) { return API.request('POST', '/pipelines', data); },
|
||||
update(id, data) { return API.request('PUT', `/pipelines/${id}`, data); },
|
||||
delete(id) { return API.request('DELETE', `/pipelines/${id}`); },
|
||||
execute(id, input) { return API.request('POST', `/pipelines/${id}/execute`, { input }); },
|
||||
execute(id, input, workingDirectory) {
|
||||
const body = { input };
|
||||
if (workingDirectory) body.workingDirectory = workingDirectory;
|
||||
return API.request('POST', `/pipelines/${id}/execute`, body);
|
||||
},
|
||||
cancel(id) { return API.request('POST', `/pipelines/${id}/cancel`); },
|
||||
approve(id) { return API.request('POST', `/pipelines/${id}/approve`); },
|
||||
reject(id) { return API.request('POST', `/pipelines/${id}/reject`); },
|
||||
},
|
||||
|
||||
webhooks: {
|
||||
list() { return API.request('GET', '/webhooks'); },
|
||||
create(data) { return API.request('POST', '/webhooks', data); },
|
||||
update(id, data) { return API.request('PUT', `/webhooks/${id}`, data); },
|
||||
delete(id) { return API.request('DELETE', `/webhooks/${id}`); },
|
||||
},
|
||||
|
||||
stats: {
|
||||
costs(days) { return API.request('GET', `/stats/costs${days ? '?days=' + days : ''}`); },
|
||||
},
|
||||
|
||||
system: {
|
||||
|
||||
168
public/js/app.js
168
public/js/app.js
@@ -4,6 +4,7 @@ const App = {
|
||||
wsReconnectAttempts: 0,
|
||||
wsReconnectTimer: null,
|
||||
_initialized: false,
|
||||
_lastAgentName: '',
|
||||
|
||||
sectionTitles: {
|
||||
dashboard: 'Dashboard',
|
||||
@@ -11,6 +12,7 @@ const App = {
|
||||
tasks: 'Tarefas',
|
||||
schedules: 'Agendamentos',
|
||||
pipelines: 'Pipelines',
|
||||
webhooks: 'Webhooks',
|
||||
terminal: 'Terminal',
|
||||
history: 'Histórico',
|
||||
settings: 'Configurações',
|
||||
@@ -71,6 +73,7 @@ const App = {
|
||||
case 'tasks': await TasksUI.load(); break;
|
||||
case 'schedules': await SchedulesUI.load(); break;
|
||||
case 'pipelines': await PipelinesUI.load(); break;
|
||||
case 'webhooks': await WebhooksUI.load(); break;
|
||||
case 'history': await HistoryUI.load(); break;
|
||||
case 'settings': await SettingsUI.load(); break;
|
||||
}
|
||||
@@ -154,6 +157,23 @@ const App = {
|
||||
if (data.data?.stderr) {
|
||||
Terminal.addLine(data.data.stderr, 'error', data.executionId);
|
||||
}
|
||||
const costUsd = data.data?.costUsd || 0;
|
||||
const numTurns = data.data?.numTurns || 0;
|
||||
if (costUsd > 0) {
|
||||
Terminal.addLine(`Custo: $${costUsd.toFixed(4)} | Turnos: ${numTurns}`, 'info', data.executionId);
|
||||
}
|
||||
|
||||
const sessionId = data.data?.sessionId || '';
|
||||
if (sessionId && data.agentId) {
|
||||
if (Terminal.getChatSession()?.sessionId === sessionId || !Terminal.getChatSession()) {
|
||||
const agentName = App._lastAgentName || 'Agente';
|
||||
Terminal.enableChat(data.agentId, agentName, sessionId);
|
||||
}
|
||||
if (Terminal.getChatSession()) {
|
||||
Terminal.updateSessionId(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
Toast.success('Execução concluída');
|
||||
App.refreshCurrentSection();
|
||||
App._updateActiveBadge();
|
||||
@@ -200,6 +220,82 @@ const App = {
|
||||
Terminal.addLine(`Erro no passo ${data.stepIndex + 1}: ${data.error}`, 'error');
|
||||
Toast.error('Erro no pipeline');
|
||||
break;
|
||||
|
||||
case 'pipeline_approval_required':
|
||||
Terminal.stopProcessing();
|
||||
Terminal.addLine(`Passo ${data.stepIndex + 1} requer aprovação antes de executar.`, 'system');
|
||||
if (data.previousOutput) {
|
||||
Terminal.addLine(`Output do passo anterior:\n${data.previousOutput.slice(0, 1000)}`, 'info');
|
||||
}
|
||||
App._showApprovalNotification(data.pipelineId, data.stepIndex, data.agentName);
|
||||
Toast.warning('Pipeline aguardando aprovação');
|
||||
break;
|
||||
|
||||
case 'pipeline_rejected':
|
||||
Terminal.stopProcessing();
|
||||
Terminal.addLine(`Pipeline rejeitado no passo ${data.stepIndex + 1}.`, 'error');
|
||||
App._hideApprovalNotification();
|
||||
Toast.info('Pipeline rejeitado');
|
||||
App.refreshCurrentSection();
|
||||
break;
|
||||
|
||||
case 'pipeline_status':
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
_showApprovalNotification(pipelineId, stepIndex, agentName) {
|
||||
const container = document.getElementById('approval-notification');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="approval-content">
|
||||
<div class="approval-icon"><i data-lucide="shield-alert"></i></div>
|
||||
<div class="approval-text">
|
||||
<strong>Aprovação necessária</strong>
|
||||
<span>Passo ${stepIndex + 1} (${agentName || 'agente'}) aguardando autorização</span>
|
||||
</div>
|
||||
<div class="approval-actions">
|
||||
<button class="btn btn--primary btn--sm" id="approval-approve-btn" type="button">Aprovar</button>
|
||||
<button class="btn btn--danger btn--sm" id="approval-reject-btn" type="button">Rejeitar</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.hidden = false;
|
||||
container.dataset.pipelineId = pipelineId;
|
||||
|
||||
if (window.lucide) lucide.createIcons({ nodes: [container] });
|
||||
|
||||
document.getElementById('approval-approve-btn')?.addEventListener('click', () => {
|
||||
App._handleApproval(pipelineId, true);
|
||||
});
|
||||
document.getElementById('approval-reject-btn')?.addEventListener('click', () => {
|
||||
App._handleApproval(pipelineId, false);
|
||||
});
|
||||
},
|
||||
|
||||
_hideApprovalNotification() {
|
||||
const container = document.getElementById('approval-notification');
|
||||
if (container) {
|
||||
container.hidden = true;
|
||||
container.innerHTML = '';
|
||||
}
|
||||
},
|
||||
|
||||
async _handleApproval(pipelineId, approve) {
|
||||
try {
|
||||
if (approve) {
|
||||
await API.pipelines.approve(pipelineId);
|
||||
Terminal.addLine('Passo aprovado. Continuando pipeline...', 'success');
|
||||
Toast.success('Passo aprovado');
|
||||
} else {
|
||||
await API.pipelines.reject(pipelineId);
|
||||
Terminal.addLine('Pipeline rejeitado pelo usuário.', 'error');
|
||||
Toast.info('Pipeline rejeitado');
|
||||
}
|
||||
App._hideApprovalNotification();
|
||||
} catch (err) {
|
||||
Toast.error(`Erro: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -306,6 +402,17 @@ const App = {
|
||||
SchedulesUI.save();
|
||||
});
|
||||
|
||||
on('webhooks-new-btn', 'click', () => WebhooksUI.openCreateModal());
|
||||
|
||||
on('webhook-form-submit', 'click', (e) => {
|
||||
e.preventDefault();
|
||||
WebhooksUI.save();
|
||||
});
|
||||
|
||||
on('webhook-target-type', 'change', (e) => {
|
||||
WebhooksUI._updateTargetSelect(e.target.value);
|
||||
});
|
||||
|
||||
on('pipelines-new-btn', 'click', () => PipelinesUI.openCreateModal());
|
||||
|
||||
on('pipeline-form-submit', 'click', (e) => {
|
||||
@@ -317,7 +424,19 @@ const App = {
|
||||
|
||||
on('pipeline-execute-submit', 'click', () => PipelinesUI._executeFromModal());
|
||||
|
||||
on('terminal-clear-btn', 'click', () => Terminal.clear());
|
||||
on('terminal-clear-btn', 'click', () => {
|
||||
Terminal.clear();
|
||||
Terminal.disableChat();
|
||||
});
|
||||
|
||||
on('terminal-send-btn', 'click', () => App._sendChatMessage());
|
||||
|
||||
on('terminal-input', 'keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
App._sendChatMessage();
|
||||
}
|
||||
});
|
||||
|
||||
on('export-copy-btn', 'click', () => App._copyExportJson());
|
||||
|
||||
@@ -374,6 +493,10 @@ const App = {
|
||||
);
|
||||
});
|
||||
|
||||
on('webhooks-search', 'input', () => {
|
||||
WebhooksUI.filter(document.getElementById('webhooks-search')?.value);
|
||||
});
|
||||
|
||||
on('pipelines-search', 'input', () => {
|
||||
PipelinesUI.filter(document.getElementById('pipelines-search')?.value);
|
||||
});
|
||||
@@ -443,6 +566,13 @@ const App = {
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('schedules-history')?.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-action]');
|
||||
if (!btn) return;
|
||||
const { action, id } = btn.dataset;
|
||||
if (action === 'view-schedule-exec') HistoryUI.viewDetail(id);
|
||||
});
|
||||
|
||||
document.getElementById('pipelines-grid')?.addEventListener('click', (e) => {
|
||||
if (e.target.closest('#pipelines-empty-new-btn')) {
|
||||
PipelinesUI.openCreateModal();
|
||||
@@ -471,6 +601,18 @@ const App = {
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('webhooks-list')?.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-action]');
|
||||
if (!btn) return;
|
||||
const { action, id, url } = btn.dataset;
|
||||
switch (action) {
|
||||
case 'toggle-webhook': WebhooksUI.toggleActive(id); break;
|
||||
case 'delete-webhook': WebhooksUI.delete(id); break;
|
||||
case 'copy-webhook-url': WebhooksUI.copyUrl(url); break;
|
||||
case 'copy-webhook-curl': WebhooksUI.copyCurl(id); break;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('pipeline-steps-container')?.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-step-action]');
|
||||
if (!btn) return;
|
||||
@@ -572,6 +714,9 @@ const App = {
|
||||
const selectEl = document.getElementById('execute-agent-select');
|
||||
const agentName = selectEl?.selectedOptions[0]?.text || 'Agente';
|
||||
|
||||
Terminal.disableChat();
|
||||
App._lastAgentName = agentName;
|
||||
|
||||
await API.agents.execute(agentId, task, instructions);
|
||||
|
||||
Modal.close('execute-modal-overlay');
|
||||
@@ -583,6 +728,27 @@ const App = {
|
||||
}
|
||||
},
|
||||
|
||||
async _sendChatMessage() {
|
||||
const session = Terminal.getChatSession();
|
||||
if (!session) return;
|
||||
|
||||
const input = document.getElementById('terminal-input');
|
||||
const message = input?.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
input.value = '';
|
||||
|
||||
Terminal.addLine(`❯ ${message}`, 'user-message', null);
|
||||
|
||||
try {
|
||||
await API.agents.continue(session.agentId, session.sessionId, message);
|
||||
Terminal.startProcessing(session.agentName);
|
||||
} catch (err) {
|
||||
Terminal.addLine(`Erro: ${err.message}`, 'error');
|
||||
Toast.error(`Erro ao continuar conversa: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async _copyExportJson() {
|
||||
const jsonEl = document.getElementById('export-code-content');
|
||||
if (!jsonEl) return;
|
||||
|
||||
@@ -29,6 +29,18 @@ const DashboardUI = {
|
||||
const current = parseInt(el.textContent, 10) || 0;
|
||||
DashboardUI._animateCount(el, current, target);
|
||||
}
|
||||
|
||||
const costEl = document.getElementById('metric-cost-today');
|
||||
if (costEl) {
|
||||
const cost = status.costs?.today ?? 0;
|
||||
costEl.textContent = `$${cost.toFixed(4)}`;
|
||||
}
|
||||
|
||||
const webhooksEl = document.getElementById('metric-webhooks');
|
||||
if (webhooksEl) {
|
||||
const current = parseInt(webhooksEl.textContent, 10) || 0;
|
||||
DashboardUI._animateCount(webhooksEl, current, status.webhooks?.active ?? 0);
|
||||
}
|
||||
},
|
||||
|
||||
_animateCount(el, from, to) {
|
||||
@@ -77,6 +89,10 @@ const DashboardUI = {
|
||||
const date = exec.startedAt
|
||||
? new Date(exec.startedAt).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' })
|
||||
: '';
|
||||
const cost = exec.costUsd || exec.totalCostUsd || 0;
|
||||
const costHtml = cost > 0
|
||||
? `<span class="activity-item-cost">$${cost.toFixed(4)}</span>`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<li class="activity-item">
|
||||
@@ -85,6 +101,7 @@ const DashboardUI = {
|
||||
<span class="activity-item-task">${taskText.length > 80 ? taskText.slice(0, 80) + '...' : taskText}</span>
|
||||
</div>
|
||||
<div class="activity-item-meta">
|
||||
${costHtml}
|
||||
<span class="badge ${statusClass}">${statusLabel}</span>
|
||||
<span class="activity-item-time">${date} ${time}</span>
|
||||
</div>
|
||||
@@ -107,7 +124,9 @@ const DashboardUI = {
|
||||
running: 'badge--blue',
|
||||
completed: 'badge--green',
|
||||
error: 'badge--red',
|
||||
cancelled: 'badge--gray',
|
||||
canceled: 'badge--gray',
|
||||
awaiting_approval: 'badge--yellow',
|
||||
rejected: 'badge--red',
|
||||
};
|
||||
return map[status] || 'badge--gray';
|
||||
},
|
||||
@@ -117,7 +136,9 @@ const DashboardUI = {
|
||||
running: 'Em execução',
|
||||
completed: 'Concluído',
|
||||
error: 'Erro',
|
||||
cancelled: 'Cancelado',
|
||||
canceled: 'Cancelado',
|
||||
awaiting_approval: 'Aguardando',
|
||||
rejected: 'Rejeitado',
|
||||
};
|
||||
return map[status] || status || 'Desconhecido';
|
||||
},
|
||||
|
||||
@@ -60,6 +60,10 @@ const HistoryUI = {
|
||||
: (exec.task || '');
|
||||
const date = HistoryUI._formatDate(exec.startedAt);
|
||||
const duration = HistoryUI._formatDuration(exec.startedAt, exec.endedAt);
|
||||
const cost = exec.costUsd || exec.totalCostUsd || 0;
|
||||
const costHtml = cost > 0
|
||||
? `<span class="history-card-cost"><i data-lucide="dollar-sign" aria-hidden="true"></i>$${cost.toFixed(4)}</span>`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<article class="history-card">
|
||||
@@ -75,9 +79,12 @@ const HistoryUI = {
|
||||
</div>
|
||||
<div class="history-card-meta">
|
||||
<span class="history-card-task">${HistoryUI._escapeHtml(task)}</span>
|
||||
<span class="history-card-duration">
|
||||
<i data-lucide="clock" aria-hidden="true"></i>
|
||||
${duration}
|
||||
<span class="history-card-duration-group">
|
||||
<span class="history-card-duration">
|
||||
<i data-lucide="clock" aria-hidden="true"></i>
|
||||
${duration}
|
||||
</span>
|
||||
${costHtml}
|
||||
</span>
|
||||
</div>
|
||||
<div class="history-card-actions">
|
||||
@@ -222,6 +229,16 @@ const HistoryUI = {
|
||||
<span class="execution-detail-label">Exit Code</span>
|
||||
<span class="execution-detail-value font-mono">${exec.exitCode}</span>
|
||||
</div>` : ''}
|
||||
${exec.costUsd || exec.totalCostUsd ? `
|
||||
<div class="execution-detail-row">
|
||||
<span class="execution-detail-label">Custo</span>
|
||||
<span class="execution-detail-value cost-value">$${(exec.costUsd || exec.totalCostUsd || 0).toFixed(4)}</span>
|
||||
</div>` : ''}
|
||||
${exec.numTurns ? `
|
||||
<div class="execution-detail-row">
|
||||
<span class="execution-detail-label">Turnos</span>
|
||||
<span class="execution-detail-value font-mono">${exec.numTurns}</span>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
${exec.task ? `
|
||||
<div class="execution-detail-section">
|
||||
@@ -265,9 +282,12 @@ const HistoryUI = {
|
||||
<span class="pipeline-step-agent">${HistoryUI._escapeHtml(step.agentName || step.agentId || 'Agente')}</span>
|
||||
${HistoryUI._statusBadge(step.status)}
|
||||
</div>
|
||||
<span class="pipeline-step-duration">
|
||||
<i data-lucide="clock" aria-hidden="true"></i>
|
||||
${stepDuration}
|
||||
<span class="pipeline-step-meta-group">
|
||||
<span class="pipeline-step-duration">
|
||||
<i data-lucide="clock" aria-hidden="true"></i>
|
||||
${stepDuration}
|
||||
</span>
|
||||
${step.costUsd ? `<span class="pipeline-step-cost">$${step.costUsd.toFixed(4)}</span>` : ''}
|
||||
</span>
|
||||
</div>
|
||||
${step.prompt ? `
|
||||
@@ -314,6 +334,11 @@ const HistoryUI = {
|
||||
<span class="execution-detail-label">Duração</span>
|
||||
<span class="execution-detail-value">${duration}</span>
|
||||
</div>
|
||||
${exec.totalCostUsd ? `
|
||||
<div class="execution-detail-row">
|
||||
<span class="execution-detail-label">Custo Total</span>
|
||||
<span class="execution-detail-value cost-value">$${(exec.totalCostUsd || 0).toFixed(4)}</span>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
${exec.input ? `
|
||||
<div class="execution-detail-section">
|
||||
@@ -373,6 +398,9 @@ const HistoryUI = {
|
||||
running: ['badge-running', 'Em execução'],
|
||||
completed: ['badge-active', 'Concluído'],
|
||||
error: ['badge-error', 'Erro'],
|
||||
awaiting_approval: ['badge-warning', 'Aguardando'],
|
||||
rejected: ['badge-error', 'Rejeitado'],
|
||||
canceled: ['badge-inactive', 'Cancelado'],
|
||||
};
|
||||
const [cls, label] = map[status] || ['badge-inactive', status || 'Desconhecido'];
|
||||
return `<span class="badge ${cls}">${label}</span>`;
|
||||
|
||||
@@ -3,6 +3,7 @@ const PipelinesUI = {
|
||||
agents: [],
|
||||
_editingId: null,
|
||||
_steps: [],
|
||||
_pendingApprovals: new Map(),
|
||||
|
||||
async load() {
|
||||
try {
|
||||
@@ -84,10 +85,13 @@ const PipelinesUI = {
|
||||
const flowHtml = steps.map((step, index) => {
|
||||
const agentName = step.agentName || step.agentId || 'Agente';
|
||||
const isLast = index === steps.length - 1;
|
||||
const approvalIcon = step.requiresApproval && index > 0
|
||||
? '<i data-lucide="shield-check" style="width:10px;height:10px;color:var(--warning)"></i> '
|
||||
: '';
|
||||
return `
|
||||
<span class="pipeline-step-badge">
|
||||
<span class="pipeline-step-number">${index + 1}</span>
|
||||
${agentName}
|
||||
${approvalIcon}${agentName}
|
||||
</span>
|
||||
${!isLast ? '<span class="pipeline-flow-arrow">→</span>' : ''}
|
||||
`;
|
||||
@@ -130,8 +134,8 @@ const PipelinesUI = {
|
||||
openCreateModal() {
|
||||
PipelinesUI._editingId = null;
|
||||
PipelinesUI._steps = [
|
||||
{ agentId: '', inputTemplate: '' },
|
||||
{ agentId: '', inputTemplate: '' },
|
||||
{ agentId: '', inputTemplate: '', requiresApproval: false },
|
||||
{ agentId: '', inputTemplate: '', requiresApproval: false },
|
||||
];
|
||||
|
||||
const titleEl = document.getElementById('pipeline-modal-title');
|
||||
@@ -156,7 +160,7 @@ const PipelinesUI = {
|
||||
|
||||
PipelinesUI._editingId = pipelineId;
|
||||
PipelinesUI._steps = Array.isArray(pipeline.steps)
|
||||
? pipeline.steps.map((s) => ({ agentId: s.agentId || '', inputTemplate: s.inputTemplate || '' }))
|
||||
? pipeline.steps.map((s) => ({ agentId: s.agentId || '', inputTemplate: s.inputTemplate || '', requiresApproval: !!s.requiresApproval }))
|
||||
: [];
|
||||
|
||||
const titleEl = document.getElementById('pipeline-modal-title');
|
||||
@@ -198,6 +202,15 @@ const PipelinesUI = {
|
||||
? '<div class="pipeline-step-connector"><i data-lucide="arrow-down" style="width:14px;height:14px"></i></div>'
|
||||
: '';
|
||||
|
||||
const approvalChecked = step.requiresApproval ? 'checked' : '';
|
||||
const approvalHtml = index > 0
|
||||
? `<label class="pipeline-step-approval">
|
||||
<input type="checkbox" data-step-field="requiresApproval" data-step-index="${index}" ${approvalChecked} />
|
||||
<i data-lucide="shield-check" style="width:12px;height:12px"></i>
|
||||
<span>Requer aprovação</span>
|
||||
</label>`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="pipeline-step-row" data-step-index="${index}">
|
||||
<span class="pipeline-step-number-lg">${index + 1}</span>
|
||||
@@ -213,6 +226,7 @@ const PipelinesUI = {
|
||||
data-step-field="inputTemplate"
|
||||
data-step-index="${index}"
|
||||
>${step.inputTemplate || ''}</textarea>
|
||||
${approvalHtml}
|
||||
</div>
|
||||
<div class="pipeline-step-actions">
|
||||
<button class="btn btn-ghost btn-icon btn-sm" type="button" data-step-action="move-up" data-step-index="${index}" title="Mover para cima" ${isFirst ? 'disabled' : ''}>
|
||||
@@ -246,14 +260,18 @@ const PipelinesUI = {
|
||||
const index = parseInt(el.dataset.stepIndex, 10);
|
||||
const field = el.dataset.stepField;
|
||||
if (PipelinesUI._steps[index] !== undefined) {
|
||||
PipelinesUI._steps[index][field] = el.value;
|
||||
if (el.type === 'checkbox') {
|
||||
PipelinesUI._steps[index][field] = el.checked;
|
||||
} else {
|
||||
PipelinesUI._steps[index][field] = el.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
addStep() {
|
||||
PipelinesUI._syncStepsFromDOM();
|
||||
PipelinesUI._steps.push({ agentId: '', inputTemplate: '' });
|
||||
PipelinesUI._steps.push({ agentId: '', inputTemplate: '', requiresApproval: false });
|
||||
PipelinesUI.renderSteps();
|
||||
},
|
||||
|
||||
@@ -299,6 +317,7 @@ const PipelinesUI = {
|
||||
steps: PipelinesUI._steps.map((s) => ({
|
||||
agentId: s.agentId,
|
||||
inputTemplate: s.inputTemplate || '',
|
||||
requiresApproval: !!s.requiresApproval,
|
||||
})),
|
||||
};
|
||||
|
||||
@@ -347,12 +366,16 @@ const PipelinesUI = {
|
||||
const inputEl = document.getElementById('pipeline-execute-input');
|
||||
if (inputEl) inputEl.value = '';
|
||||
|
||||
const workdirEl = document.getElementById('pipeline-execute-workdir');
|
||||
if (workdirEl) workdirEl.value = '';
|
||||
|
||||
Modal.open('pipeline-execute-modal-overlay');
|
||||
},
|
||||
|
||||
async _executeFromModal() {
|
||||
const pipelineId = document.getElementById('pipeline-execute-id')?.value;
|
||||
const input = document.getElementById('pipeline-execute-input')?.value.trim();
|
||||
const workingDirectory = document.getElementById('pipeline-execute-workdir')?.value.trim() || '';
|
||||
|
||||
if (!input) {
|
||||
Toast.warning('O input inicial é obrigatório');
|
||||
@@ -360,7 +383,7 @@ const PipelinesUI = {
|
||||
}
|
||||
|
||||
try {
|
||||
await API.pipelines.execute(pipelineId, input);
|
||||
await API.pipelines.execute(pipelineId, input, workingDirectory);
|
||||
Modal.close('pipeline-execute-modal-overlay');
|
||||
App.navigateTo('terminal');
|
||||
Toast.info('Pipeline iniciado');
|
||||
|
||||
@@ -47,8 +47,7 @@ const SchedulesUI = {
|
||||
<td>${schedule.agentName || '—'}</td>
|
||||
<td class="schedule-task-cell" title="${schedule.taskDescription || ''}">${schedule.taskDescription || '—'}</td>
|
||||
<td>
|
||||
<span title="${cronExpr}">${humanCron}</span>
|
||||
<small class="font-mono">${cronExpr}</small>
|
||||
<code class="font-mono">${cronExpr}</code>
|
||||
</td>
|
||||
<td>${nextRun}</td>
|
||||
<td><span class="badge ${statusClass}">${statusLabel}</span></td>
|
||||
@@ -214,19 +213,84 @@ const SchedulesUI = {
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<ul class="activity-list">
|
||||
${history.slice(0, 20).map((h) => `
|
||||
<li class="activity-item">
|
||||
<div class="activity-item-info">
|
||||
<span class="activity-item-agent">${h.cronExpr}</span>
|
||||
</div>
|
||||
<div class="activity-item-meta">
|
||||
<span class="activity-item-time">${new Date(h.firedAt).toLocaleString('pt-BR')}</span>
|
||||
</div>
|
||||
</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
<div class="table-wrapper">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Agente</th>
|
||||
<th scope="col">Tarefa</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Data</th>
|
||||
<th scope="col">Duração</th>
|
||||
<th scope="col">Custo</th>
|
||||
<th scope="col" aria-label="Ações"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${history.map((exec) => {
|
||||
const status = SchedulesUI._statusBadge(exec.status);
|
||||
const date = exec.startedAt ? new Date(exec.startedAt).toLocaleString('pt-BR') : '—';
|
||||
const duration = SchedulesUI._formatDuration(exec.startedAt, exec.endedAt);
|
||||
const cost = exec.costUsd || exec.totalCostUsd || 0;
|
||||
const costStr = cost > 0 ? `$${cost.toFixed(4)}` : '—';
|
||||
const taskStr = SchedulesUI._escapeHtml(SchedulesUI._truncate(exec.task || '', 60));
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>${SchedulesUI._escapeHtml(exec.agentName || '—')}</td>
|
||||
<td title="${SchedulesUI._escapeHtml(exec.task || '')}">${taskStr}</td>
|
||||
<td>${status}</td>
|
||||
<td>${date}</td>
|
||||
<td>${duration}</td>
|
||||
<td class="font-mono">${costStr}</td>
|
||||
<td>
|
||||
<button class="btn btn-ghost btn-sm" data-action="view-schedule-exec" data-id="${exec.id}" type="button" title="Ver resultado">
|
||||
<i data-lucide="eye"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (window.lucide) lucide.createIcons({ nodes: [container] });
|
||||
},
|
||||
|
||||
_statusBadge(status) {
|
||||
const map = {
|
||||
running: ['badge-running', 'Executando'],
|
||||
completed: ['badge-active', 'Concluído'],
|
||||
error: ['badge-error', 'Erro'],
|
||||
};
|
||||
const [cls, label] = map[status] || ['badge-inactive', status || '—'];
|
||||
return `<span class="badge ${cls}">${label}</span>`;
|
||||
},
|
||||
|
||||
_formatDuration(start, end) {
|
||||
if (!start) return '—';
|
||||
const startMs = new Date(start).getTime();
|
||||
const endMs = end ? new Date(end).getTime() : Date.now();
|
||||
const totalSeconds = Math.floor((endMs - startMs) / 1000);
|
||||
if (totalSeconds < 0) return '—';
|
||||
if (totalSeconds < 60) return `${totalSeconds}s`;
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
if (hours > 0) return `${hours}h ${minutes}m`;
|
||||
return `${minutes}m ${seconds}s`;
|
||||
},
|
||||
|
||||
_escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
},
|
||||
|
||||
_truncate(str, max) {
|
||||
if (!str || str.length <= max) return str;
|
||||
return str.slice(0, max) + '…';
|
||||
},
|
||||
|
||||
cronToHuman(expression) {
|
||||
@@ -262,8 +326,10 @@ const SchedulesUI = {
|
||||
|
||||
if (minute.startsWith('*/')) return `A cada ${minute.slice(2)} minutos`;
|
||||
if (hour.startsWith('*/') && minute === '0') return `A cada ${hour.slice(2)} horas`;
|
||||
if (minute === '0' && hour !== '*' && day === '*' && month === '*' && weekday === '*') {
|
||||
return `Todo dia às ${hour.padStart(2, '0')}h`;
|
||||
if (hour !== '*' && day === '*' && month === '*' && weekday === '*') {
|
||||
const h = hour.padStart(2, '0');
|
||||
if (minute === '0') return `Todo dia às ${h}h`;
|
||||
return `Todo dia às ${h}:${minute.padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
return expression;
|
||||
|
||||
@@ -4,6 +4,31 @@ const Terminal = {
|
||||
autoScroll: true,
|
||||
executionFilter: null,
|
||||
_processingInterval: null,
|
||||
_chatSession: null,
|
||||
|
||||
enableChat(agentId, agentName, sessionId) {
|
||||
Terminal._chatSession = { agentId, agentName, sessionId };
|
||||
const bar = document.getElementById('terminal-input-bar');
|
||||
const ctx = document.getElementById('terminal-input-context');
|
||||
const input = document.getElementById('terminal-input');
|
||||
if (bar) bar.hidden = false;
|
||||
if (ctx) ctx.textContent = `Conversando com: ${agentName}`;
|
||||
if (input) { input.value = ''; input.focus(); }
|
||||
},
|
||||
|
||||
disableChat() {
|
||||
Terminal._chatSession = null;
|
||||
const bar = document.getElementById('terminal-input-bar');
|
||||
if (bar) bar.hidden = true;
|
||||
},
|
||||
|
||||
getChatSession() {
|
||||
return Terminal._chatSession;
|
||||
},
|
||||
|
||||
updateSessionId(sessionId) {
|
||||
if (Terminal._chatSession) Terminal._chatSession.sessionId = sessionId;
|
||||
},
|
||||
|
||||
addLine(content, type = 'default', executionId = null) {
|
||||
const time = new Date();
|
||||
|
||||
257
public/js/components/webhooks.js
Normal file
257
public/js/components/webhooks.js
Normal file
@@ -0,0 +1,257 @@
|
||||
const WebhooksUI = {
|
||||
webhooks: [],
|
||||
agents: [],
|
||||
pipelines: [],
|
||||
|
||||
async load() {
|
||||
try {
|
||||
const [webhooks, agents, pipelines] = await Promise.all([
|
||||
API.webhooks.list(),
|
||||
API.agents.list(),
|
||||
API.pipelines.list(),
|
||||
]);
|
||||
WebhooksUI.webhooks = Array.isArray(webhooks) ? webhooks : [];
|
||||
WebhooksUI.agents = Array.isArray(agents) ? agents : [];
|
||||
WebhooksUI.pipelines = Array.isArray(pipelines) ? pipelines : [];
|
||||
WebhooksUI.render();
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao carregar webhooks: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
filter(searchText) {
|
||||
const search = (searchText || '').toLowerCase();
|
||||
const filtered = WebhooksUI.webhooks.filter((w) => {
|
||||
const name = (w.name || '').toLowerCase();
|
||||
return !search || name.includes(search);
|
||||
});
|
||||
WebhooksUI.render(filtered);
|
||||
},
|
||||
|
||||
render(filteredWebhooks) {
|
||||
const container = document.getElementById('webhooks-list');
|
||||
if (!container) return;
|
||||
|
||||
const webhooks = filteredWebhooks || WebhooksUI.webhooks;
|
||||
|
||||
if (webhooks.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">
|
||||
<i data-lucide="webhook"></i>
|
||||
</div>
|
||||
<h3 class="empty-state-title">Nenhum webhook cadastrado</h3>
|
||||
<p class="empty-state-desc">Crie webhooks para disparar agentes ou pipelines via HTTP.</p>
|
||||
</div>
|
||||
`;
|
||||
if (window.lucide) lucide.createIcons({ nodes: [container] });
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = webhooks.map((w) => WebhooksUI._renderCard(w)).join('');
|
||||
if (window.lucide) lucide.createIcons({ nodes: [container] });
|
||||
},
|
||||
|
||||
_renderCard(webhook) {
|
||||
const typeBadge = webhook.targetType === 'pipeline'
|
||||
? '<span class="badge badge--purple">Pipeline</span>'
|
||||
: '<span class="badge badge--blue">Agente</span>';
|
||||
|
||||
const statusBadge = webhook.active
|
||||
? '<span class="badge badge-active">Ativo</span>'
|
||||
: '<span class="badge badge-inactive">Inativo</span>';
|
||||
|
||||
const targetName = WebhooksUI._resolveTargetName(webhook);
|
||||
const hookUrl = `${window.location.origin}/hook/${webhook.token}`;
|
||||
const lastTrigger = webhook.lastTriggeredAt
|
||||
? new Date(webhook.lastTriggeredAt).toLocaleString('pt-BR')
|
||||
: 'Nunca';
|
||||
|
||||
return `
|
||||
<article class="webhook-card">
|
||||
<div class="webhook-card-header">
|
||||
<div class="webhook-card-identity">
|
||||
<span class="webhook-card-name">${WebhooksUI._escapeHtml(webhook.name)}</span>
|
||||
${typeBadge}
|
||||
${statusBadge}
|
||||
</div>
|
||||
<div class="webhook-card-actions">
|
||||
<button class="btn btn-ghost btn-sm btn-icon" data-action="toggle-webhook" data-id="${webhook.id}" title="${webhook.active ? 'Desativar' : 'Ativar'}">
|
||||
<i data-lucide="${webhook.active ? 'pause' : 'play'}"></i>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm btn-icon btn-danger" data-action="delete-webhook" data-id="${webhook.id}" title="Excluir">
|
||||
<i data-lucide="trash-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="webhook-card-body">
|
||||
<div class="webhook-card-target">
|
||||
<span class="webhook-card-label">Destino</span>
|
||||
<span class="webhook-card-value">${WebhooksUI._escapeHtml(targetName)}</span>
|
||||
</div>
|
||||
<div class="webhook-card-url">
|
||||
<span class="webhook-card-label">URL</span>
|
||||
<div class="webhook-url-field">
|
||||
<code class="webhook-url-code">${hookUrl}</code>
|
||||
<button class="btn btn-ghost btn-sm btn-icon" data-action="copy-webhook-url" data-url="${hookUrl}" title="Copiar URL">
|
||||
<i data-lucide="copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="webhook-card-curl">
|
||||
<button class="btn btn-ghost btn-sm btn-icon-text" data-action="copy-webhook-curl" data-id="${webhook.id}" title="Copiar comando cURL">
|
||||
<i data-lucide="terminal"></i>
|
||||
<span>Copiar cURL</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="webhook-card-meta">
|
||||
<span class="webhook-meta-item">
|
||||
<i data-lucide="activity" style="width:12px;height:12px"></i>
|
||||
${webhook.triggerCount || 0} disparos
|
||||
</span>
|
||||
<span class="webhook-meta-item">
|
||||
<i data-lucide="clock" style="width:12px;height:12px"></i>
|
||||
Último: ${lastTrigger}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
},
|
||||
|
||||
_resolveTargetName(webhook) {
|
||||
if (webhook.targetType === 'agent') {
|
||||
const agent = WebhooksUI.agents.find((a) => a.id === webhook.targetId);
|
||||
return agent ? (agent.agent_name || agent.name) : webhook.targetId;
|
||||
}
|
||||
const pl = WebhooksUI.pipelines.find((p) => p.id === webhook.targetId);
|
||||
return pl ? pl.name : webhook.targetId;
|
||||
},
|
||||
|
||||
openCreateModal() {
|
||||
const titleEl = document.getElementById('webhook-modal-title');
|
||||
if (titleEl) titleEl.textContent = 'Novo Webhook';
|
||||
|
||||
const nameEl = document.getElementById('webhook-name');
|
||||
if (nameEl) nameEl.value = '';
|
||||
|
||||
const typeEl = document.getElementById('webhook-target-type');
|
||||
if (typeEl) {
|
||||
typeEl.value = 'agent';
|
||||
WebhooksUI._updateTargetSelect('agent');
|
||||
}
|
||||
|
||||
Modal.open('webhook-modal-overlay');
|
||||
},
|
||||
|
||||
_updateTargetSelect(targetType) {
|
||||
const selectEl = document.getElementById('webhook-target-id');
|
||||
if (!selectEl) return;
|
||||
|
||||
if (targetType === 'agent') {
|
||||
selectEl.innerHTML = '<option value="">Selecionar agente...</option>' +
|
||||
WebhooksUI.agents.map((a) => `<option value="${a.id}">${a.agent_name || a.name}</option>`).join('');
|
||||
} else {
|
||||
selectEl.innerHTML = '<option value="">Selecionar pipeline...</option>' +
|
||||
WebhooksUI.pipelines.map((p) => `<option value="${p.id}">${p.name}</option>`).join('');
|
||||
}
|
||||
},
|
||||
|
||||
async save() {
|
||||
const name = document.getElementById('webhook-name')?.value.trim();
|
||||
const targetType = document.getElementById('webhook-target-type')?.value;
|
||||
const targetId = document.getElementById('webhook-target-id')?.value;
|
||||
|
||||
if (!name) { Toast.warning('Nome do webhook é obrigatório'); return; }
|
||||
if (!targetId) { Toast.warning('Selecione um destino'); return; }
|
||||
|
||||
try {
|
||||
await API.webhooks.create({ name, targetType, targetId });
|
||||
Modal.close('webhook-modal-overlay');
|
||||
Toast.success('Webhook criado com sucesso');
|
||||
await WebhooksUI.load();
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao criar webhook: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async toggleActive(webhookId) {
|
||||
const webhook = WebhooksUI.webhooks.find((w) => w.id === webhookId);
|
||||
if (!webhook) return;
|
||||
|
||||
try {
|
||||
await API.webhooks.update(webhookId, { active: !webhook.active });
|
||||
Toast.success(webhook.active ? 'Webhook desativado' : 'Webhook ativado');
|
||||
await WebhooksUI.load();
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao atualizar webhook: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async delete(webhookId) {
|
||||
const confirmed = await Modal.confirm(
|
||||
'Excluir webhook',
|
||||
'Tem certeza que deseja excluir este webhook? Integrações que usam esta URL deixarão de funcionar.'
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await API.webhooks.delete(webhookId);
|
||||
Toast.success('Webhook excluído');
|
||||
await WebhooksUI.load();
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao excluir webhook: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async copyUrl(url) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
Toast.success('URL copiada');
|
||||
} catch {
|
||||
Toast.error('Não foi possível copiar a URL');
|
||||
}
|
||||
},
|
||||
|
||||
async copyCurl(webhookId) {
|
||||
const webhook = WebhooksUI.webhooks.find((w) => w.id === webhookId);
|
||||
if (!webhook) return;
|
||||
|
||||
const hookUrl = `${window.location.origin}/hook/${webhook.token}`;
|
||||
const targetName = WebhooksUI._resolveTargetName(webhook);
|
||||
|
||||
let payload;
|
||||
if (webhook.targetType === 'pipeline') {
|
||||
payload = JSON.stringify({
|
||||
input: 'Texto de entrada para o pipeline',
|
||||
workingDirectory: '/caminho/do/projeto (opcional)',
|
||||
}, null, 2);
|
||||
} else {
|
||||
payload = JSON.stringify({
|
||||
task: 'Descreva a tarefa a ser executada',
|
||||
instructions: 'Instruções adicionais (opcional)',
|
||||
}, null, 2);
|
||||
}
|
||||
|
||||
const curl = `curl -X POST '${hookUrl}' \\\n -H 'Content-Type: application/json' \\\n -d '${payload}'`;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(curl);
|
||||
Toast.success('cURL copiado');
|
||||
} catch {
|
||||
Toast.error('Não foi possível copiar o cURL');
|
||||
}
|
||||
},
|
||||
|
||||
_escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
},
|
||||
};
|
||||
|
||||
window.WebhooksUI = WebhooksUI;
|
||||
@@ -4,7 +4,7 @@ import { WebSocketServer } from 'ws';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import apiRouter, { setWsBroadcast, setWsBroadcastTo } from './src/routes/api.js';
|
||||
import apiRouter, { setWsBroadcast, setWsBroadcastTo, hookRouter } from './src/routes/api.js';
|
||||
import * as manager from './src/agents/manager.js';
|
||||
import { setGlobalBroadcast } from './src/agents/manager.js';
|
||||
import { cancelAllExecutions } from './src/agents/executor.js';
|
||||
@@ -40,6 +40,7 @@ if (AUTH_TOKEN) {
|
||||
}
|
||||
|
||||
app.use(express.json());
|
||||
app.use('/hook', hookRouter);
|
||||
app.use(express.static(join(__dirname, 'public')));
|
||||
app.use('/api', apiRouter);
|
||||
|
||||
|
||||
@@ -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