- Tarefas agora são templates executáveis com botão play e seleção de agente - Dropdown de tarefas salvas no modal de execução para reutilização rápida - Broadcast global no manager para execuções agendadas via cron aparecerem no terminal - Dashboard atividade recente agora consulta executionsStore persistente - Suporte a exibição de pipelines e agentes na atividade recente
448 lines
13 KiB
JavaScript
448 lines
13 KiB
JavaScript
import { Router } from 'express';
|
|
import { execSync } from 'child_process';
|
|
import os from 'os';
|
|
import * as manager from '../agents/manager.js';
|
|
import { tasksStore, settingsStore, executionsStore } 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';
|
|
import { invalidateAgentMapCache } from '../agents/pipeline.js';
|
|
import { cached } from '../cache/index.js';
|
|
|
|
const router = Router();
|
|
|
|
let wsbroadcast = null;
|
|
let wsBroadcastTo = null;
|
|
|
|
export function setWsBroadcast(fn) {
|
|
wsbroadcast = fn;
|
|
}
|
|
|
|
export function setWsBroadcastTo(fn) {
|
|
wsBroadcastTo = fn;
|
|
}
|
|
|
|
function wsCallback(message, clientId) {
|
|
if (clientId && wsBroadcastTo) wsBroadcastTo(clientId, message);
|
|
else if (wsbroadcast) wsbroadcast(message);
|
|
}
|
|
|
|
router.get('/settings', (req, res) => {
|
|
try {
|
|
res.json(settingsStore.get());
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.put('/settings', (req, res) => {
|
|
try {
|
|
const allowed = ['defaultModel', 'defaultWorkdir', 'maxConcurrent'];
|
|
const data = {};
|
|
for (const key of allowed) {
|
|
if (req.body[key] !== undefined) data[key] = req.body[key];
|
|
}
|
|
if (data.maxConcurrent !== undefined) {
|
|
data.maxConcurrent = Math.max(1, Math.min(20, parseInt(data.maxConcurrent) || 5));
|
|
updateMaxConcurrent(data.maxConcurrent);
|
|
}
|
|
const saved = settingsStore.save(data);
|
|
res.json(saved);
|
|
} catch (err) {
|
|
res.status(400).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.get('/agents', (req, res) => {
|
|
try {
|
|
res.json(manager.getAllAgents());
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.get('/agents/:id', (req, res) => {
|
|
try {
|
|
const agent = manager.getAgentById(req.params.id);
|
|
if (!agent) return res.status(404).json({ error: 'Agente não encontrado' });
|
|
res.json(agent);
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.post('/agents', (req, res) => {
|
|
try {
|
|
const agent = manager.createAgent(req.body);
|
|
invalidateAgentMapCache();
|
|
res.status(201).json(agent);
|
|
} catch (err) {
|
|
res.status(400).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.post('/agents/import', (req, res) => {
|
|
try {
|
|
const agent = manager.importAgent(req.body);
|
|
invalidateAgentMapCache();
|
|
res.status(201).json(agent);
|
|
} catch (err) {
|
|
res.status(400).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.put('/agents/:id', (req, res) => {
|
|
try {
|
|
const agent = manager.updateAgent(req.params.id, req.body);
|
|
if (!agent) return res.status(404).json({ error: 'Agente não encontrado' });
|
|
invalidateAgentMapCache();
|
|
res.json(agent);
|
|
} catch (err) {
|
|
res.status(400).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.delete('/agents/:id', (req, res) => {
|
|
try {
|
|
const deleted = manager.deleteAgent(req.params.id);
|
|
if (!deleted) return res.status(404).json({ error: 'Agente não encontrado' });
|
|
invalidateAgentMapCache();
|
|
res.status(204).send();
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.post('/agents/:id/execute', (req, res) => {
|
|
try {
|
|
const { task, instructions } = req.body;
|
|
if (!task) return res.status(400).json({ error: 'task é obrigatório' });
|
|
const clientId = req.headers['x-client-id'] || null;
|
|
const executionId = manager.executeTask(req.params.id, task, instructions, (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);
|
|
if (!cancelled) return res.status(404).json({ error: 'Execução não encontrada ou já finalizada' });
|
|
res.json({ cancelled: true });
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.get('/agents/:id/export', (req, res) => {
|
|
try {
|
|
const exported = manager.exportAgent(req.params.id);
|
|
if (!exported) return res.status(404).json({ error: 'Agente não encontrado' });
|
|
res.json(exported);
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.get('/tasks', (req, res) => {
|
|
try {
|
|
res.json(tasksStore.getAll());
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.post('/tasks', (req, res) => {
|
|
try {
|
|
if (!req.body.name) return res.status(400).json({ error: 'name é obrigatório' });
|
|
res.status(201).json(tasksStore.create(req.body));
|
|
} catch (err) {
|
|
res.status(400).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.put('/tasks/:id', (req, res) => {
|
|
try {
|
|
const task = tasksStore.update(req.params.id, req.body);
|
|
if (!task) return res.status(404).json({ error: 'Tarefa não encontrada' });
|
|
res.json(task);
|
|
} catch (err) {
|
|
res.status(400).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.delete('/tasks/:id', (req, res) => {
|
|
try {
|
|
const deleted = tasksStore.delete(req.params.id);
|
|
if (!deleted) return res.status(404).json({ error: 'Tarefa não encontrada' });
|
|
res.status(204).send();
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.post('/schedules', (req, res) => {
|
|
try {
|
|
const { agentId, taskDescription, cronExpression } = req.body;
|
|
if (!agentId || !taskDescription || !cronExpression) {
|
|
return res.status(400).json({ error: 'agentId, taskDescription e cronExpression são obrigatórios' });
|
|
}
|
|
const clientId = req.headers['x-client-id'] || null;
|
|
const result = manager.scheduleTask(agentId, taskDescription, cronExpression, (msg) => wsCallback(msg, clientId));
|
|
res.status(201).json(result);
|
|
} catch (err) {
|
|
const status = err.message.includes('não encontrado') ? 404 : 400;
|
|
res.status(status).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.get('/schedules/history', (req, res) => {
|
|
try {
|
|
res.json(scheduler.getHistory());
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.get('/schedules', (req, res) => {
|
|
try {
|
|
res.json(scheduler.getSchedules());
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.put('/schedules/:id', (req, res) => {
|
|
try {
|
|
const clientId = req.headers['x-client-id'] || null;
|
|
const updated = manager.updateScheduleTask(req.params.id, req.body, (msg) => wsCallback(msg, clientId));
|
|
if (!updated) return res.status(404).json({ error: 'Agendamento não encontrado' });
|
|
res.json(updated);
|
|
} catch (err) {
|
|
const status = err.message.includes('não encontrado') ? 404 : 400;
|
|
res.status(status).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.delete('/schedules/:taskId', (req, res) => {
|
|
try {
|
|
const removed = scheduler.unschedule(req.params.taskId);
|
|
if (!removed) return res.status(404).json({ error: 'Agendamento não encontrado' });
|
|
res.status(204).send();
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.get('/pipelines', (req, res) => {
|
|
try {
|
|
res.json(pipeline.getAllPipelines());
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.get('/pipelines/:id', (req, res) => {
|
|
try {
|
|
const found = pipeline.getPipeline(req.params.id);
|
|
if (!found) return res.status(404).json({ error: 'Pipeline não encontrado' });
|
|
res.json(found);
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.post('/pipelines', (req, res) => {
|
|
try {
|
|
res.status(201).json(pipeline.createPipeline(req.body));
|
|
} catch (err) {
|
|
res.status(400).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.put('/pipelines/:id', (req, res) => {
|
|
try {
|
|
const updated = pipeline.updatePipeline(req.params.id, req.body);
|
|
if (!updated) return res.status(404).json({ error: 'Pipeline não encontrado' });
|
|
res.json(updated);
|
|
} catch (err) {
|
|
res.status(400).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.delete('/pipelines/:id', (req, res) => {
|
|
try {
|
|
const deleted = pipeline.deletePipeline(req.params.id);
|
|
if (!deleted) return res.status(404).json({ error: 'Pipeline não encontrado' });
|
|
res.status(204).send();
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.post('/pipelines/:id/execute', (req, res) => {
|
|
try {
|
|
const { input } = 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(() => {});
|
|
res.status(202).json({ pipelineId: req.params.id, status: 'started' });
|
|
} catch (err) {
|
|
const status = err.message.includes('não encontrado') ? 404 : 400;
|
|
res.status(status).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.post('/pipelines/:id/cancel', (req, res) => {
|
|
try {
|
|
const cancelled = pipeline.cancelPipeline(req.params.id);
|
|
if (!cancelled) return res.status(404).json({ error: 'Pipeline não está em execução' });
|
|
res.json({ cancelled: true });
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
const SYSTEM_STATUS_TTL = 5_000;
|
|
|
|
router.get('/system/status', (req, res) => {
|
|
try {
|
|
const status = cached('system:status', SYSTEM_STATUS_TTL, () => {
|
|
const agents = manager.getAllAgents();
|
|
const activeExecutions = manager.getActiveExecutions();
|
|
const schedules = scheduler.getSchedules();
|
|
const pipelines = pipeline.getAllPipelines();
|
|
const activePipelines = pipeline.getActivePipelines();
|
|
return {
|
|
agents: {
|
|
total: agents.length,
|
|
active: agents.filter((a) => a.status === 'active').length,
|
|
inactive: agents.filter((a) => a.status === 'inactive').length,
|
|
},
|
|
executions: {
|
|
active: activeExecutions.length,
|
|
today: manager.getDailyExecutionCount(),
|
|
list: activeExecutions,
|
|
},
|
|
schedules: {
|
|
total: schedules.length,
|
|
active: schedules.filter((s) => s.active).length,
|
|
},
|
|
pipelines: {
|
|
total: pipelines.length,
|
|
active: pipelines.filter((p) => p.status === 'active').length,
|
|
running: activePipelines.length,
|
|
},
|
|
};
|
|
});
|
|
res.json(status);
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
let claudeVersionCache = null;
|
|
|
|
router.get('/system/info', (req, res) => {
|
|
try {
|
|
if (claudeVersionCache === null) {
|
|
try {
|
|
claudeVersionCache = execSync(`${getBinPath()} --version`, { timeout: 5000 }).toString().trim();
|
|
} catch {
|
|
claudeVersionCache = 'N/A';
|
|
}
|
|
}
|
|
res.json({
|
|
serverVersion: '1.0.0',
|
|
nodeVersion: process.version,
|
|
claudeVersion: claudeVersionCache,
|
|
platform: `${os.platform()} ${os.arch()}`,
|
|
uptime: Math.floor(process.uptime()),
|
|
});
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.get('/executions/history', (req, res) => {
|
|
try {
|
|
const limit = parseInt(req.query.limit) || 50;
|
|
const offset = parseInt(req.query.offset) || 0;
|
|
const typeFilter = req.query.type || '';
|
|
const statusFilter = req.query.status || '';
|
|
const search = (req.query.search || '').toLowerCase();
|
|
|
|
let items = executionsStore.getAll();
|
|
|
|
if (typeFilter) items = items.filter((e) => e.type === typeFilter);
|
|
if (statusFilter) items = items.filter((e) => e.status === statusFilter);
|
|
if (search) {
|
|
items = items.filter((e) => {
|
|
const name = (e.agentName || e.pipelineName || '').toLowerCase();
|
|
const task = (e.task || e.input || '').toLowerCase();
|
|
return name.includes(search) || task.includes(search);
|
|
});
|
|
}
|
|
|
|
items.sort((a, b) => new Date(b.startedAt) - new Date(a.startedAt));
|
|
const total = items.length;
|
|
const paged = items.slice(offset, offset + limit);
|
|
|
|
res.json({ items: paged, total });
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.get('/executions/history/:id', (req, res) => {
|
|
try {
|
|
const exec = executionsStore.getById(req.params.id);
|
|
if (!exec) return res.status(404).json({ error: 'Execução não encontrada' });
|
|
res.json(exec);
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.delete('/executions/history/:id', (req, res) => {
|
|
try {
|
|
const deleted = executionsStore.delete(req.params.id);
|
|
if (!deleted) return res.status(404).json({ error: 'Execução não encontrada' });
|
|
res.status(204).send();
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.delete('/executions/history', (req, res) => {
|
|
try {
|
|
executionsStore.save([]);
|
|
res.status(204).send();
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.get('/executions/active', (req, res) => {
|
|
try {
|
|
res.json(manager.getActiveExecutions());
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.get('/executions/recent', (req, res) => {
|
|
try {
|
|
const limit = parseInt(req.query.limit) || 20;
|
|
const items = executionsStore.getAll();
|
|
items.sort((a, b) => new Date(b.startedAt) - new Date(a.startedAt));
|
|
res.json(items.slice(0, limit));
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
export default router;
|