Corrigir E2BIG em pipelines, adicionar diretório de projeto e retomada
This commit is contained in:
@@ -2,6 +2,7 @@ FROM node:22-alpine
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm ci --omit=dev
|
RUN npm ci --omit=dev
|
||||||
|
RUN npm install -g @anthropic-ai/claude-code
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN mkdir -p data
|
RUN mkdir -p data
|
||||||
ENV HOST=0.0.0.0
|
ENV HOST=0.0.0.0
|
||||||
|
|||||||
@@ -3489,6 +3489,25 @@ tbody tr:hover td {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pipeline-workdir-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
background-color: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pipeline-workdir-badge code {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.pipeline-flow {
|
.pipeline-flow {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1138,6 +1138,14 @@
|
|||||||
<label class="form-label" for="pipeline-description">Descrição</label>
|
<label class="form-label" for="pipeline-description">Descrição</label>
|
||||||
<textarea class="textarea" id="pipeline-description" rows="2" placeholder="Descreva o objetivo do pipeline..."></textarea>
|
<textarea class="textarea" id="pipeline-description" rows="2" placeholder="Descreva o objetivo do pipeline..."></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="pipeline-workdir">
|
||||||
|
<i data-lucide="folder" style="width:14px;height:14px"></i>
|
||||||
|
Diretório do Projeto
|
||||||
|
</label>
|
||||||
|
<input class="input" type="text" id="pipeline-workdir" autocomplete="off">
|
||||||
|
<p class="form-hint">Caminho onde o projeto será criado/trabalhado. Todos os passos usarão este diretório.</p>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">
|
<label class="form-label">
|
||||||
Passos do Pipeline
|
Passos do Pipeline
|
||||||
@@ -1194,7 +1202,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
class="input"
|
class="input"
|
||||||
id="pipeline-execute-workdir"
|
id="pipeline-execute-workdir"
|
||||||
placeholder="/home/fred/projetos/meu-projeto"
|
placeholder="/home/projetos/meu-projeto"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
<p class="form-hint">Se vazio, cada agente usa seu próprio diretório configurado.</p>
|
<p class="form-hint">Se vazio, cada agente usa seu próprio diretório configurado.</p>
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ const API = {
|
|||||||
cancel(id) { return API.request('POST', `/pipelines/${id}/cancel`); },
|
cancel(id) { return API.request('POST', `/pipelines/${id}/cancel`); },
|
||||||
approve(id) { return API.request('POST', `/pipelines/${id}/approve`); },
|
approve(id) { return API.request('POST', `/pipelines/${id}/approve`); },
|
||||||
reject(id) { return API.request('POST', `/pipelines/${id}/reject`); },
|
reject(id) { return API.request('POST', `/pipelines/${id}/reject`); },
|
||||||
|
resume(executionId) { return API.request('POST', `/pipelines/resume/${executionId}`); },
|
||||||
},
|
},
|
||||||
|
|
||||||
webhooks: {
|
webhooks: {
|
||||||
|
|||||||
@@ -276,6 +276,7 @@ const App = {
|
|||||||
|
|
||||||
case 'pipeline_step_start':
|
case 'pipeline_step_start':
|
||||||
Terminal.stopProcessing();
|
Terminal.stopProcessing();
|
||||||
|
if (data.resumed) Terminal.addLine('(retomando execução anterior)', 'system');
|
||||||
Terminal.addLine(`Pipeline passo ${data.stepIndex + 1}/${data.totalSteps}: Executando agente "${data.agentName}"...`, 'system');
|
Terminal.addLine(`Pipeline passo ${data.stepIndex + 1}/${data.totalSteps}: Executando agente "${data.agentName}"...`, 'system');
|
||||||
Terminal.startProcessing(data.agentName);
|
Terminal.startProcessing(data.agentName);
|
||||||
break;
|
break;
|
||||||
@@ -732,6 +733,7 @@ const App = {
|
|||||||
case 'view-execution': HistoryUI.viewDetail(id); break;
|
case 'view-execution': HistoryUI.viewDetail(id); break;
|
||||||
case 'delete-execution': HistoryUI.deleteExecution(id); break;
|
case 'delete-execution': HistoryUI.deleteExecution(id); break;
|
||||||
case 'retry': HistoryUI.retryExecution(id); break;
|
case 'retry': HistoryUI.retryExecution(id); break;
|
||||||
|
case 'resume-pipeline': HistoryUI.resumePipeline(id); break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,11 @@ const HistoryUI = {
|
|||||||
<i data-lucide="eye"></i>
|
<i data-lucide="eye"></i>
|
||||||
Ver detalhes
|
Ver detalhes
|
||||||
</button>
|
</button>
|
||||||
|
${(exec.status === 'error' && exec.type === 'pipeline' && exec.failedAtStep !== undefined) ? `
|
||||||
|
<button class="btn btn-ghost btn-sm" data-action="resume-pipeline" data-id="${exec.id}" type="button" title="Retomar do passo ${(exec.failedAtStep || 0) + 1}">
|
||||||
|
<i data-lucide="play"></i>
|
||||||
|
Retomar
|
||||||
|
</button>` : ''}
|
||||||
${(exec.status === 'error' || exec.status === 'canceled') ? `
|
${(exec.status === 'error' || exec.status === 'canceled') ? `
|
||||||
<button class="btn btn-ghost btn-sm" data-action="retry" data-id="${exec.id}" type="button" title="Reexecutar">
|
<button class="btn btn-ghost btn-sm" data-action="retry" data-id="${exec.id}" type="button" title="Reexecutar">
|
||||||
<i data-lucide="refresh-cw"></i>
|
<i data-lucide="refresh-cw"></i>
|
||||||
@@ -421,6 +426,16 @@ const HistoryUI = {
|
|||||||
Toast.success('Download iniciado');
|
Toast.success('Download iniciado');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async resumePipeline(executionId) {
|
||||||
|
try {
|
||||||
|
await API.pipelines.resume(executionId);
|
||||||
|
Toast.info('Pipeline retomado');
|
||||||
|
App.navigateTo('terminal');
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error(`Erro ao retomar pipeline: ${err.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async retryExecution(id) {
|
async retryExecution(id) {
|
||||||
try {
|
try {
|
||||||
await API.executions.retry(id);
|
await API.executions.retry(id);
|
||||||
|
|||||||
@@ -107,6 +107,8 @@ const PipelinesUI = {
|
|||||||
|
|
||||||
${pipeline.description ? `<p class="agent-description">${Utils.escapeHtml(pipeline.description)}</p>` : ''}
|
${pipeline.description ? `<p class="agent-description">${Utils.escapeHtml(pipeline.description)}</p>` : ''}
|
||||||
|
|
||||||
|
${pipeline.workingDirectory ? `<div class="pipeline-workdir-badge"><i data-lucide="folder" style="width:12px;height:12px"></i> <code>${Utils.escapeHtml(pipeline.workingDirectory)}</code></div>` : ''}
|
||||||
|
|
||||||
<div class="pipeline-flow">
|
<div class="pipeline-flow">
|
||||||
${flowHtml || '<span class="agent-description">Nenhum passo configurado</span>'}
|
${flowHtml || '<span class="agent-description">Nenhum passo configurado</span>'}
|
||||||
</div>
|
</div>
|
||||||
@@ -133,6 +135,8 @@ const PipelinesUI = {
|
|||||||
`;
|
`;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_basePath: '/home/projetos/',
|
||||||
|
|
||||||
openCreateModal() {
|
openCreateModal() {
|
||||||
PipelinesUI._editingId = null;
|
PipelinesUI._editingId = null;
|
||||||
PipelinesUI._steps = [
|
PipelinesUI._steps = [
|
||||||
@@ -152,6 +156,9 @@ const PipelinesUI = {
|
|||||||
const descEl = document.getElementById('pipeline-description');
|
const descEl = document.getElementById('pipeline-description');
|
||||||
if (descEl) descEl.value = '';
|
if (descEl) descEl.value = '';
|
||||||
|
|
||||||
|
const workdirEl = document.getElementById('pipeline-workdir');
|
||||||
|
if (workdirEl) workdirEl.value = PipelinesUI._basePath;
|
||||||
|
|
||||||
PipelinesUI.renderSteps();
|
PipelinesUI.renderSteps();
|
||||||
Modal.open('pipeline-modal-overlay');
|
Modal.open('pipeline-modal-overlay');
|
||||||
},
|
},
|
||||||
@@ -183,6 +190,9 @@ const PipelinesUI = {
|
|||||||
const descEl = document.getElementById('pipeline-description');
|
const descEl = document.getElementById('pipeline-description');
|
||||||
if (descEl) descEl.value = pipeline.description || '';
|
if (descEl) descEl.value = pipeline.description || '';
|
||||||
|
|
||||||
|
const workdirEl = document.getElementById('pipeline-workdir');
|
||||||
|
if (workdirEl) workdirEl.value = pipeline.workingDirectory || PipelinesUI._basePath;
|
||||||
|
|
||||||
PipelinesUI.renderSteps();
|
PipelinesUI.renderSteps();
|
||||||
Modal.open('pipeline-modal-overlay');
|
Modal.open('pipeline-modal-overlay');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -391,9 +401,16 @@ const PipelinesUI = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const workingDirectory = document.getElementById('pipeline-workdir')?.value.trim() || '';
|
||||||
|
if (workingDirectory && !workingDirectory.startsWith('/')) {
|
||||||
|
Toast.warning('O diretório do projeto deve ser um caminho absoluto (começar com /)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
name,
|
name,
|
||||||
description: document.getElementById('pipeline-description')?.value.trim() || '',
|
description: document.getElementById('pipeline-description')?.value.trim() || '',
|
||||||
|
workingDirectory,
|
||||||
steps: PipelinesUI._steps.map((s, index) => {
|
steps: PipelinesUI._steps.map((s, index) => {
|
||||||
const isSimple = s.promptMode !== 'advanced';
|
const isSimple = s.promptMode !== 'advanced';
|
||||||
const inputTemplate = isSimple
|
const inputTemplate = isSimple
|
||||||
@@ -455,7 +472,7 @@ const PipelinesUI = {
|
|||||||
if (inputEl) inputEl.value = '';
|
if (inputEl) inputEl.value = '';
|
||||||
|
|
||||||
const workdirEl = document.getElementById('pipeline-execute-workdir');
|
const workdirEl = document.getElementById('pipeline-execute-workdir');
|
||||||
if (workdirEl) workdirEl.value = '';
|
if (workdirEl) workdirEl.value = (pipeline && pipeline.workingDirectory) || PipelinesUI._basePath;
|
||||||
|
|
||||||
if (App._pipelineDropzone) App._pipelineDropzone.reset();
|
if (App._pipelineDropzone) App._pipelineDropzone.reset();
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ function verifyWebhookSignature(req, res, next) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
app.set('trust proxy', 1);
|
||||||
const httpServer = createServer(app);
|
const httpServer = createServer(app);
|
||||||
const wss = new WebSocketServer({ server: httpServer });
|
const wss = new WebSocketServer({ server: httpServer });
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { existsSync } from 'fs';
|
import { existsSync, mkdirSync } from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
@@ -58,9 +58,9 @@ function cleanEnv(agentSecrets) {
|
|||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildArgs(agentConfig, prompt) {
|
function buildArgs(agentConfig) {
|
||||||
const model = agentConfig.model || 'claude-sonnet-4-6';
|
const model = agentConfig.model || 'claude-sonnet-4-6';
|
||||||
const args = ['-p', prompt, '--output-format', 'stream-json', '--verbose', '--model', model];
|
const args = ['--output-format', 'stream-json', '--verbose', '--model', model];
|
||||||
|
|
||||||
if (existsSync(AGENT_SETTINGS)) {
|
if (existsSync(AGENT_SETTINGS)) {
|
||||||
args.push('--settings', AGENT_SETTINGS);
|
args.push('--settings', AGENT_SETTINGS);
|
||||||
@@ -290,10 +290,14 @@ function validateWorkingDirectory(agentConfig, executionId, onError) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!existsSync(agentConfig.workingDirectory)) {
|
if (!existsSync(agentConfig.workingDirectory)) {
|
||||||
const err = new Error(`Diretório de trabalho não encontrado: ${agentConfig.workingDirectory}`);
|
try {
|
||||||
|
mkdirSync(agentConfig.workingDirectory, { recursive: true });
|
||||||
|
} catch (e) {
|
||||||
|
const err = new Error(`Não foi possível criar o diretório: ${agentConfig.workingDirectory} (${e.message})`);
|
||||||
if (onError) onError(err, executionId);
|
if (onError) onError(err, executionId);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -311,11 +315,11 @@ export function execute(agentConfig, task, callbacks = {}, secrets = null) {
|
|||||||
if (!validateWorkingDirectory(agentConfig, executionId, onError)) return null;
|
if (!validateWorkingDirectory(agentConfig, executionId, onError)) return null;
|
||||||
|
|
||||||
const prompt = buildPrompt(task.description || task, task.instructions);
|
const prompt = buildPrompt(task.description || task, task.instructions);
|
||||||
const args = buildArgs(agentConfig, prompt);
|
const args = buildArgs(agentConfig);
|
||||||
|
|
||||||
const spawnOptions = {
|
const spawnOptions = {
|
||||||
env: cleanEnv(secrets),
|
env: cleanEnv(secrets),
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (agentConfig.workingDirectory && agentConfig.workingDirectory.trim()) {
|
if (agentConfig.workingDirectory && agentConfig.workingDirectory.trim()) {
|
||||||
@@ -325,6 +329,8 @@ export function execute(agentConfig, task, callbacks = {}, secrets = null) {
|
|||||||
console.log(`[executor] Iniciando: ${executionId} | Modelo: ${agentConfig.model || 'claude-sonnet-4-6'}`);
|
console.log(`[executor] Iniciando: ${executionId} | Modelo: ${agentConfig.model || 'claude-sonnet-4-6'}`);
|
||||||
|
|
||||||
const child = spawn(CLAUDE_BIN, args, spawnOptions);
|
const child = spawn(CLAUDE_BIN, args, spawnOptions);
|
||||||
|
child.stdin.write(prompt);
|
||||||
|
child.stdin.end();
|
||||||
|
|
||||||
activeExecutions.set(executionId, {
|
activeExecutions.set(executionId, {
|
||||||
process: child,
|
process: child,
|
||||||
@@ -356,7 +362,6 @@ export function resume(agentConfig, sessionId, message, callbacks = {}) {
|
|||||||
const model = agentConfig.model || 'claude-sonnet-4-6';
|
const model = agentConfig.model || 'claude-sonnet-4-6';
|
||||||
const args = [
|
const args = [
|
||||||
'--resume', sessionId,
|
'--resume', sessionId,
|
||||||
'-p', sanitizeText(message),
|
|
||||||
'--output-format', 'stream-json',
|
'--output-format', 'stream-json',
|
||||||
'--verbose',
|
'--verbose',
|
||||||
'--model', model,
|
'--model', model,
|
||||||
@@ -373,7 +378,7 @@ export function resume(agentConfig, sessionId, message, callbacks = {}) {
|
|||||||
|
|
||||||
const spawnOptions = {
|
const spawnOptions = {
|
||||||
env: cleanEnv(),
|
env: cleanEnv(),
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (agentConfig.workingDirectory && agentConfig.workingDirectory.trim()) {
|
if (agentConfig.workingDirectory && agentConfig.workingDirectory.trim()) {
|
||||||
@@ -383,6 +388,8 @@ export function resume(agentConfig, sessionId, message, callbacks = {}) {
|
|||||||
console.log(`[executor] Resumindo sessão: ${sessionId} | Execução: ${executionId}`);
|
console.log(`[executor] Resumindo sessão: ${sessionId} | Execução: ${executionId}`);
|
||||||
|
|
||||||
const child = spawn(CLAUDE_BIN, args, spawnOptions);
|
const child = spawn(CLAUDE_BIN, args, spawnOptions);
|
||||||
|
child.stdin.write(sanitizeText(message));
|
||||||
|
child.stdin.end();
|
||||||
|
|
||||||
activeExecutions.set(executionId, {
|
activeExecutions.set(executionId, {
|
||||||
process: child,
|
process: child,
|
||||||
@@ -443,7 +450,6 @@ ${text}
|
|||||||
</conteudo_para_resumir>`;
|
</conteudo_para_resumir>`;
|
||||||
|
|
||||||
const args = [
|
const args = [
|
||||||
'-p', prompt,
|
|
||||||
'--output-format', 'text',
|
'--output-format', 'text',
|
||||||
'--model', 'claude-haiku-4-5-20251001',
|
'--model', 'claude-haiku-4-5-20251001',
|
||||||
'--max-turns', '1',
|
'--max-turns', '1',
|
||||||
@@ -456,8 +462,10 @@ ${text}
|
|||||||
|
|
||||||
const child = spawn(CLAUDE_BIN, args, {
|
const child = spawn(CLAUDE_BIN, args, {
|
||||||
env: cleanEnv(),
|
env: cleanEnv(),
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
});
|
});
|
||||||
|
child.stdin.write(prompt);
|
||||||
|
child.stdin.end();
|
||||||
|
|
||||||
let output = '';
|
let output = '';
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
|
|||||||
@@ -214,8 +214,9 @@ export async function executePipeline(pipelineId, initialInput, wsCallback, opti
|
|||||||
if (agent.status !== 'active') throw new Error(`Agente ${agent.agent_name} está inativo`);
|
if (agent.status !== 'active') throw new Error(`Agente ${agent.agent_name} está inativo`);
|
||||||
|
|
||||||
const stepConfig = { ...agent.config };
|
const stepConfig = { ...agent.config };
|
||||||
if (options.workingDirectory) {
|
const effectiveWorkdir = options.workingDirectory || pl.workingDirectory;
|
||||||
stepConfig.workingDirectory = options.workingDirectory;
|
if (effectiveWorkdir) {
|
||||||
|
stepConfig.workingDirectory = effectiveWorkdir;
|
||||||
}
|
}
|
||||||
|
|
||||||
const prompt = applyTemplate(step.inputTemplate, currentInput);
|
const prompt = applyTemplate(step.inputTemplate, currentInput);
|
||||||
@@ -318,6 +319,8 @@ export async function executePipeline(pipelineId, initialInput, wsCallback, opti
|
|||||||
executionsStore.update(historyRecord.id, {
|
executionsStore.update(historyRecord.id, {
|
||||||
status: 'error',
|
status: 'error',
|
||||||
error: err.message,
|
error: err.message,
|
||||||
|
failedAtStep: pipelineState.currentStep,
|
||||||
|
lastStepInput: currentInput.slice(0, 50000),
|
||||||
endedAt: new Date().toISOString(),
|
endedAt: new Date().toISOString(),
|
||||||
totalCostUsd: totalCost,
|
totalCostUsd: totalCost,
|
||||||
});
|
});
|
||||||
@@ -359,6 +362,175 @@ export function cancelPipeline(id) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function resumePipeline(executionId, wsCallback, options = {}) {
|
||||||
|
const prevExec = executionsStore.getById(executionId);
|
||||||
|
if (!prevExec) throw new Error('Execução não encontrada');
|
||||||
|
if (prevExec.status !== 'error') throw new Error('Só é possível retomar execuções com erro');
|
||||||
|
if (prevExec.type !== 'pipeline') throw new Error('Execução não é de pipeline');
|
||||||
|
|
||||||
|
const pl = pipelinesStore.getById(prevExec.pipelineId);
|
||||||
|
if (!pl) throw new Error(`Pipeline ${prevExec.pipelineId} não encontrado`);
|
||||||
|
|
||||||
|
const startStep = prevExec.failedAtStep || 0;
|
||||||
|
const initialInput = prevExec.lastStepInput || prevExec.input;
|
||||||
|
|
||||||
|
const newExecId = uuidv4();
|
||||||
|
const pipelineState = { pipelineId: prevExec.pipelineId, currentExecutionId: null, currentStep: startStep, canceled: false, pendingApproval: null };
|
||||||
|
activePipelines.set(newExecId, pipelineState);
|
||||||
|
|
||||||
|
const prevSteps = Array.isArray(prevExec.steps) ? [...prevExec.steps] : [];
|
||||||
|
const prevCost = prevExec.totalCostUsd || 0;
|
||||||
|
|
||||||
|
const historyRecord = executionsStore.create({
|
||||||
|
type: 'pipeline',
|
||||||
|
pipelineId: prevExec.pipelineId,
|
||||||
|
pipelineName: pl.name,
|
||||||
|
input: prevExec.input,
|
||||||
|
resumedFrom: executionId,
|
||||||
|
resumedAtStep: startStep,
|
||||||
|
status: 'running',
|
||||||
|
startedAt: new Date().toISOString(),
|
||||||
|
steps: prevSteps,
|
||||||
|
totalCostUsd: prevCost,
|
||||||
|
});
|
||||||
|
|
||||||
|
const steps = buildSteps(pl.steps);
|
||||||
|
const results = prevSteps.map(s => ({ stepId: s.stepId, agentId: s.agentId, agentName: s.agentName, result: s.result, sessionId: '' }));
|
||||||
|
let currentInput = initialInput;
|
||||||
|
let totalCost = prevCost;
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (let i = startStep; i < steps.length; i++) {
|
||||||
|
if (pipelineState.canceled) break;
|
||||||
|
|
||||||
|
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: prevExec.pipelineId, status: 'awaiting_approval', stepIndex: i });
|
||||||
|
const approved = await waitForApproval(newExecId, prevExec.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: prevExec.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 };
|
||||||
|
const effectiveWorkdir = options.workingDirectory || pl.workingDirectory;
|
||||||
|
if (effectiveWorkdir) stepConfig.workingDirectory = effectiveWorkdir;
|
||||||
|
|
||||||
|
const prompt = applyTemplate(step.inputTemplate, currentInput);
|
||||||
|
const stepStart = new Date().toISOString();
|
||||||
|
|
||||||
|
if (wsCallback) {
|
||||||
|
wsCallback({
|
||||||
|
type: 'pipeline_step_start',
|
||||||
|
pipelineId: prevExec.pipelineId,
|
||||||
|
stepIndex: i,
|
||||||
|
stepId: step.id,
|
||||||
|
agentName: agent.agent_name,
|
||||||
|
totalSteps: steps.length,
|
||||||
|
resumed: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepResult = await executeStepAsPromise(stepConfig, prompt, pipelineState, wsCallback, prevExec.pipelineId, i);
|
||||||
|
if (pipelineState.canceled) break;
|
||||||
|
|
||||||
|
totalCost += stepResult.costUsd;
|
||||||
|
currentInput = stepResult.text;
|
||||||
|
results.push({ stepId: step.id, agentId: step.agentId, agentName: agent.agent_name, result: stepResult.text, sessionId: stepResult.sessionId });
|
||||||
|
|
||||||
|
const current = executionsStore.getById(historyRecord.id);
|
||||||
|
const savedSteps = current ? (current.steps || []) : [];
|
||||||
|
savedSteps.push({
|
||||||
|
stepIndex: i,
|
||||||
|
agentId: step.agentId,
|
||||||
|
agentName: agent.agent_name,
|
||||||
|
prompt: prompt.slice(0, 5000),
|
||||||
|
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, totalCostUsd: totalCost });
|
||||||
|
|
||||||
|
if (wsCallback) {
|
||||||
|
wsCallback({
|
||||||
|
type: 'pipeline_step_complete',
|
||||||
|
pipelineId: prevExec.pipelineId,
|
||||||
|
stepIndex: i,
|
||||||
|
stepId: step.id,
|
||||||
|
result: stepResult.text.slice(0, 500),
|
||||||
|
costUsd: stepResult.costUsd,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i < steps.length - 1 && !pipelineState.canceled) {
|
||||||
|
if (wsCallback) wsCallback({ type: 'pipeline_summarizing', pipelineId: prevExec.pipelineId, stepIndex: i, originalLength: currentInput.length });
|
||||||
|
const summarized = await executor.summarize(currentInput);
|
||||||
|
if (summarized !== currentInput) {
|
||||||
|
if (wsCallback) wsCallback({ type: 'pipeline_summarized', pipelineId: prevExec.pipelineId, stepIndex: i, originalLength: currentInput.length, summarizedLength: summarized.length });
|
||||||
|
currentInput = summarized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activePipelines.delete(newExecId);
|
||||||
|
const finalStatus = pipelineState.canceled ? 'canceled' : 'completed';
|
||||||
|
executionsStore.update(historyRecord.id, { status: finalStatus, endedAt: new Date().toISOString(), totalCostUsd: totalCost });
|
||||||
|
|
||||||
|
if (!pipelineState.canceled) {
|
||||||
|
try {
|
||||||
|
const updated = executionsStore.getById(historyRecord.id);
|
||||||
|
if (updated) {
|
||||||
|
const report = generatePipelineReport(updated);
|
||||||
|
if (wsCallback) wsCallback({ type: 'report_generated', pipelineId: prevExec.pipelineId, reportFile: report.filename });
|
||||||
|
}
|
||||||
|
} catch (e) { console.error('[pipeline] Erro ao gerar relatório:', e.message); }
|
||||||
|
const lastResult = results.length > 0 ? results[results.length - 1] : null;
|
||||||
|
if (wsCallback) wsCallback({
|
||||||
|
type: 'pipeline_complete',
|
||||||
|
pipelineId: prevExec.pipelineId,
|
||||||
|
executionId: newExecId,
|
||||||
|
results,
|
||||||
|
totalCostUsd: totalCost,
|
||||||
|
lastAgentId: lastResult?.agentId || '',
|
||||||
|
lastAgentName: lastResult?.agentName || '',
|
||||||
|
lastSessionId: lastResult?.sessionId || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { executionId: newExecId, results };
|
||||||
|
} catch (err) {
|
||||||
|
activePipelines.delete(newExecId);
|
||||||
|
executionsStore.update(historyRecord.id, {
|
||||||
|
status: 'error',
|
||||||
|
error: err.message,
|
||||||
|
failedAtStep: pipelineState.currentStep,
|
||||||
|
lastStepInput: currentInput.slice(0, 50000),
|
||||||
|
endedAt: new Date().toISOString(),
|
||||||
|
totalCostUsd: totalCost,
|
||||||
|
});
|
||||||
|
if (wsCallback) wsCallback({ type: 'pipeline_error', pipelineId: prevExec.pipelineId, stepIndex: pipelineState.currentStep, error: err.message });
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function getActivePipelines() {
|
export function getActivePipelines() {
|
||||||
return Array.from(activePipelines.entries()).map(([id, state]) => ({
|
return Array.from(activePipelines.entries()).map(([id, state]) => ({
|
||||||
executionId: id,
|
executionId: id,
|
||||||
@@ -375,6 +547,7 @@ export function createPipeline(data) {
|
|||||||
return pipelinesStore.create({
|
return pipelinesStore.create({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
description: data.description || '',
|
description: data.description || '',
|
||||||
|
workingDirectory: data.workingDirectory || '',
|
||||||
steps: buildSteps(data.steps),
|
steps: buildSteps(data.steps),
|
||||||
status: data.status || 'active',
|
status: data.status || 'active',
|
||||||
});
|
});
|
||||||
@@ -386,6 +559,7 @@ export function updatePipeline(id, data) {
|
|||||||
const updateData = {};
|
const updateData = {};
|
||||||
if (data.name !== undefined) updateData.name = data.name;
|
if (data.name !== undefined) updateData.name = data.name;
|
||||||
if (data.description !== undefined) updateData.description = data.description;
|
if (data.description !== undefined) updateData.description = data.description;
|
||||||
|
if (data.workingDirectory !== undefined) updateData.workingDirectory = data.workingDirectory;
|
||||||
if (data.status !== undefined) updateData.status = data.status;
|
if (data.status !== undefined) updateData.status = data.status;
|
||||||
if (data.steps !== undefined) updateData.steps = buildSteps(data.steps);
|
if (data.steps !== undefined) updateData.steps = buildSteps(data.steps);
|
||||||
return pipelinesStore.update(id, updateData);
|
return pipelinesStore.update(id, updateData);
|
||||||
|
|||||||
@@ -506,6 +506,18 @@ router.post('/pipelines/:id/reject', (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post('/pipelines/resume/:executionId', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const clientId = req.headers['x-client-id'] || null;
|
||||||
|
const result = pipeline.resumePipeline(req.params.executionId, (msg) => wsCallback(msg, clientId));
|
||||||
|
result.catch(() => {});
|
||||||
|
res.status(202).json({ status: 'resumed' });
|
||||||
|
} catch (err) {
|
||||||
|
const status = err.message.includes('não encontrad') ? 404 : 400;
|
||||||
|
res.status(status).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.get('/webhooks', (req, res) => {
|
router.get('/webhooks', (req, res) => {
|
||||||
try {
|
try {
|
||||||
res.json(webhooksStore.getAll());
|
res.json(webhooksStore.getAll());
|
||||||
|
|||||||
Reference in New Issue
Block a user