Botão Commit & Push nos projetos e correção do resume de sessão
- Adicionar botão de commit & push para cada projeto na página de arquivos - Criar rota POST /api/files/commit-push com git add, commit e push - Adicionar Modal.prompt reutilizável para inputs com valor padrão - Corrigir detecção de erro no executor (is_error/errors do CLI) - Fallback automático para nova execução quando sessão expira no resume
This commit is contained in:
@@ -232,6 +232,8 @@ function processChildOutput(child, executionId, callbacks, options = {}) {
|
||||
durationApiMs: parsed.duration_api_ms || 0,
|
||||
numTurns: parsed.num_turns || 0,
|
||||
sessionId: parsed.session_id || sessionIdOverride || '',
|
||||
isError: parsed.is_error || false,
|
||||
errors: parsed.errors || [],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -267,6 +269,13 @@ function processChildOutput(child, executionId, callbacks, options = {}) {
|
||||
activeExecutions.delete(executionId);
|
||||
if (hadError) return;
|
||||
if (outputBuffer.trim()) processEvent(parseStreamLine(outputBuffer));
|
||||
|
||||
if (resultMeta?.isError && resultMeta.errors?.length > 0) {
|
||||
const errorMsg = resultMeta.errors.join('; ');
|
||||
if (onError) onError(new Error(errorMsg), executionId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (onComplete) {
|
||||
onComplete({
|
||||
executionId,
|
||||
|
||||
@@ -420,42 +420,70 @@ export function continueConversation(agentId, sessionId, message, wsCallback) {
|
||||
parentSessionId: sessionId,
|
||||
});
|
||||
|
||||
const onData = (parsed, execId) => {
|
||||
if (cb) cb({ type: 'execution_output', executionId: execId, agentId, data: parsed });
|
||||
};
|
||||
|
||||
const 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,
|
||||
});
|
||||
try {
|
||||
const updated = executionsStore.getById(historyRecord.id);
|
||||
if (updated) {
|
||||
const report = generateAgentReport(updated);
|
||||
if (cb) cb({ type: 'report_generated', executionId: execId, agentId, reportFile: report.filename });
|
||||
}
|
||||
} catch (e) { console.error('[manager] Erro ao gerar relatório:', e.message); }
|
||||
if (cb) cb({ type: 'execution_complete', executionId: execId, agentId, agentName: agent.agent_name, data: result });
|
||||
};
|
||||
|
||||
const onError = (err, execId) => {
|
||||
const isSessionLost = err.message.includes('No conversation found') || err.message.includes('not a valid');
|
||||
|
||||
if (isSessionLost) {
|
||||
console.log(`[manager] Sessão perdida (${sessionId}), iniciando nova execução para agente ${agentId}`);
|
||||
if (cb) cb({ type: 'execution_output', executionId: execId, agentId, data: { type: 'system', content: 'Sessão anterior expirou. Iniciando nova execução...' } });
|
||||
|
||||
const secrets = secretsStore.getByAgent(agentId);
|
||||
const newExecId = executor.execute(
|
||||
agent.config,
|
||||
{ description: message },
|
||||
{ onData, onError: onErrorFinal, onComplete },
|
||||
Object.keys(secrets).length > 0 ? secrets : null,
|
||||
);
|
||||
|
||||
if (newExecId) {
|
||||
executionsStore.update(historyRecord.id, { executionId: newExecId, parentSessionId: null });
|
||||
} else {
|
||||
onErrorFinal(new Error('Falha ao iniciar nova execução'), execId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
onErrorFinal(err, execId);
|
||||
};
|
||||
|
||||
const onErrorFinal = (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 } });
|
||||
};
|
||||
|
||||
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,
|
||||
});
|
||||
try {
|
||||
const updated = executionsStore.getById(historyRecord.id);
|
||||
if (updated) {
|
||||
const report = generateAgentReport(updated);
|
||||
if (cb) cb({ type: 'report_generated', executionId: execId, agentId, reportFile: report.filename });
|
||||
}
|
||||
} catch (e) { console.error('[manager] Erro ao gerar relatório:', e.message); }
|
||||
if (cb) cb({ type: 'execution_complete', executionId: execId, agentId, data: result });
|
||||
},
|
||||
}
|
||||
{ onData, onError, onComplete }
|
||||
);
|
||||
|
||||
if (!executionId) {
|
||||
|
||||
@@ -1202,6 +1202,47 @@ router.get('/repos/:name/branches', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/files/commit-push', async (req, res) => {
|
||||
const { path: projectPath, message } = req.body;
|
||||
if (!projectPath) return res.status(400).json({ error: 'path é obrigatório' });
|
||||
|
||||
const targetPath = resolveProjectPath(projectPath);
|
||||
if (!targetPath) return res.status(400).json({ error: 'Caminho inválido' });
|
||||
if (!existsSync(targetPath)) return res.status(404).json({ error: 'Projeto não encontrado' });
|
||||
if (!statSync(targetPath).isDirectory()) return res.status(400).json({ error: 'Caminho não é uma pasta' });
|
||||
|
||||
const gitDir = `${targetPath}/.git`;
|
||||
if (!existsSync(gitDir)) return res.status(400).json({ error: 'Projeto não possui repositório git inicializado' });
|
||||
|
||||
const exec = (cmd, opts = {}) => new Promise((resolve, reject) => {
|
||||
const proc = spawnProcess('sh', ['-c', cmd], { cwd: opts.cwd || targetPath, env: { ...process.env, HOME: '/tmp', GIT_TERMINAL_PROMPT: '0' } });
|
||||
let stdout = '', stderr = '';
|
||||
proc.stdout.on('data', d => stdout += d);
|
||||
proc.stderr.on('data', d => stderr += d);
|
||||
proc.on('close', code => code === 0 ? resolve(stdout.trim()) : reject(new Error(stderr.trim() || `exit ${code}`)));
|
||||
});
|
||||
|
||||
try {
|
||||
const status = await exec('git status --porcelain');
|
||||
if (!status) return res.json({ status: 'clean', message: 'Nenhuma alteração para commitar', changes: 0 });
|
||||
|
||||
const changes = status.split('\n').filter(l => l.trim()).length;
|
||||
|
||||
await exec('git add -A');
|
||||
|
||||
const commitMsg = message || `Atualização automática - ${new Date().toLocaleDateString('pt-BR')} ${new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}`;
|
||||
await exec(`git -c user.name="Agents Orchestrator" -c user.email="agents@nitro-cloud" commit -m "${commitMsg.replace(/"/g, '\\"')}"`);
|
||||
|
||||
await exec('git push origin HEAD:main');
|
||||
|
||||
const log = await exec('git log -1 --format="%h %s"');
|
||||
|
||||
res.json({ status: 'pushed', message: `Commit e push realizados: ${log}`, changes, commit: log });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/files/publish', async (req, res) => {
|
||||
const { path: projectPath } = req.body;
|
||||
if (!projectPath) return res.status(400).json({ error: 'path é obrigatório' });
|
||||
|
||||
Reference in New Issue
Block a user