arcadiasuite/server/blackboard/routes.ts

458 lines
14 KiB
TypeScript

/**
* Arcadia Suite - Blackboard Routes
*
* Rotas da API para o sistema de agentes colaborativos.
*/
import { Router, Request, Response } from "express";
import { blackboardService } from "./service";
import { agents, startAllAgents, stopAllAgents, getAgentsStatus } from "./agents";
const router = Router();
router.post("/task", async (req: Request, res: Response) => {
try {
const { title, description, context } = req.body;
const userId = (req.user as any)?.id || "anonymous";
if (!title || !description) {
return res.status(400).json({
success: false,
error: "Título e descrição são obrigatórios"
});
}
const mainTask = await blackboardService.createMainTask(
title,
description,
userId,
context
);
await blackboardService.createSubtask(
mainTask.id,
"Projetar solução",
`Analisar e criar especificação para: ${description}`,
"architect",
[],
{ phase: "design" }
);
res.json({
success: true,
task: mainTask,
message: "Tarefa criada. Os agentes vão processar automaticamente."
});
} catch (error: any) {
console.error("[Blackboard] Erro ao criar tarefa:", error);
res.status(500).json({ success: false, error: error.message });
}
});
router.get("/task/:id", async (req: Request, res: Response) => {
try {
const taskId = parseInt(req.params.id);
const details = await blackboardService.getTaskWithDetails(taskId);
if (!details) {
return res.status(404).json({ success: false, error: "Tarefa não encontrada" });
}
res.json({ success: true, ...details });
} catch (error: any) {
res.status(500).json({ success: false, error: error.message });
}
});
router.get("/tasks", async (req: Request, res: Response) => {
try {
const userId = (req.user as any)?.id;
const limit = parseInt(req.query.limit as string) || 20;
const tasks = await blackboardService.getRecentTasks(userId, limit);
res.json({ success: true, tasks });
} catch (error: any) {
res.status(500).json({ success: false, error: error.message });
}
});
router.get("/task/:id/logs", async (req: Request, res: Response) => {
try {
const taskId = parseInt(req.params.id);
const logs = await blackboardService.getTaskLogs(taskId);
res.json({ success: true, logs });
} catch (error: any) {
res.status(500).json({ success: false, error: error.message });
}
});
router.get("/task/:id/artifacts", async (req: Request, res: Response) => {
try {
const taskId = parseInt(req.params.id);
const type = req.query.type as string;
const artifacts = await blackboardService.getArtifactsForTask(taskId, type as any);
res.json({ success: true, artifacts });
} catch (error: any) {
res.status(500).json({ success: false, error: error.message });
}
});
router.get("/agents", async (_req: Request, res: Response) => {
try {
const status = getAgentsStatus();
res.json({ success: true, agents: status });
} catch (error: any) {
res.status(500).json({ success: false, error: error.message });
}
});
router.post("/agents/start", async (_req: Request, res: Response) => {
try {
startAllAgents();
res.json({ success: true, message: "Todos os agentes iniciados" });
} catch (error: any) {
res.status(500).json({ success: false, error: error.message });
}
});
router.post("/agents/stop", async (_req: Request, res: Response) => {
try {
stopAllAgents();
res.json({ success: true, message: "Todos os agentes parados" });
} catch (error: any) {
res.status(500).json({ success: false, error: error.message });
}
});
router.get("/stats", async (_req: Request, res: Response) => {
try {
const stats = await blackboardService.getStats();
const agentsStatus = getAgentsStatus();
res.json({
success: true,
stats,
agents: agentsStatus,
runningAgents: agentsStatus.filter(a => a.running).length
});
} catch (error: any) {
res.status(500).json({ success: false, error: error.message });
}
});
router.get("/history", async (req: Request, res: Response) => {
try {
const limit = Math.min(parseInt(req.query.limit as string) || 20, 50);
const offset = parseInt(req.query.offset as string) || 0;
const status = req.query.status as string;
const allTasks = await blackboardService.getRecentTasks(undefined, 200);
const filtered = status ? allTasks.filter(t => t.status === status) : allTasks;
const total = filtered.length;
const page = filtered.slice(offset, offset + limit);
const enriched = await Promise.all(
page.map(async (task) => {
const details = await blackboardService.getTaskWithDetails(task.id);
return {
...task,
subtaskCount: details?.subtasks?.length || 0,
artifactCount: details?.artifacts?.length || 0,
logCount: details?.logs?.length || 0,
subtasks: details?.subtasks?.map(s => ({
id: s.id,
title: s.title,
status: s.status,
assignedAgent: s.assignedAgent,
startedAt: s.startedAt,
completedAt: s.completedAt,
})) || [],
artifacts: details?.artifacts?.map(a => ({
id: a.id,
type: a.type,
name: a.name,
createdBy: a.createdBy,
createdAt: a.createdAt,
})) || [],
timeline: details?.logs?.map(l => ({
id: l.id,
agent: l.agentName,
action: l.action,
thought: l.thought,
observation: l.observation,
createdAt: l.createdAt,
})) || [],
};
})
);
res.json({ success: true, tasks: enriched, total, limit, offset });
} catch (error: any) {
res.status(500).json({ success: false, error: error.message });
}
});
router.post("/develop", async (req: Request, res: Response) => {
try {
const { description, autoCommit, targetBranch, images } = req.body;
const userId = (req.user as any)?.id || "anonymous";
if (!description) {
return res.status(400).json({
success: false,
error: "Descrição é obrigatória"
});
}
const hasImages = images && Array.isArray(images) && images.length > 0;
const enrichedDescription = hasImages
? `${description}\n\n[${images.length} imagem(ns) anexada(s) pelo usuário para referência visual]`
: description;
const mainTask = await blackboardService.createMainTask(
`Desenvolvimento: ${description.slice(0, 50)}...`,
enrichedDescription,
userId,
{
autoCommit: autoCommit || false,
targetBranch: targetBranch || "main",
source: "dev-center",
images: hasImages ? images.map((img: any) => ({ name: img.name })) : undefined
}
);
await blackboardService.createSubtask(
mainTask.id,
"Projetar solução",
enrichedDescription,
"architect",
[],
{ phase: "design", hasImages }
);
const pollResult = async (): Promise<any> => {
const maxWait = 60000;
const pollInterval = 1000;
let elapsed = 0;
while (elapsed < maxWait) {
const details = await blackboardService.getTaskWithDetails(mainTask.id);
if (details?.task.status === "completed" || details?.task.status === "failed") {
return details;
}
await new Promise(resolve => setTimeout(resolve, pollInterval));
elapsed += pollInterval;
}
return await blackboardService.getTaskWithDetails(mainTask.id);
};
const result = await pollResult();
res.json({
success: result?.task?.status === "completed",
phase: result?.task?.status,
taskId: mainTask.id,
spec: result?.artifacts?.find((a: any) => a.type === "spec")?.content,
files: result?.artifacts?.filter((a: any) => a.type === "code")?.map((a: any) => ({
path: a.name,
type: a.metadata?.type
})),
validation: result?.artifacts?.find((a: any) => a.name === "validation-report.json")?.content,
log: result?.logs?.map((l: any) => `[${l.agentName}] ${l.thought}`) || [],
error: result?.task?.errorMessage
});
} catch (error: any) {
console.error("[Blackboard] Erro no desenvolvimento:", error);
res.status(500).json({ success: false, error: error.message });
}
});
router.get("/staged", async (_req: Request, res: Response) => {
try {
const allTasks = await blackboardService.getRecentTasks(undefined, 100);
const stagedItems = [];
for (const task of allTasks) {
if (task.status !== "completed") continue;
const details = await blackboardService.getTaskWithDetails(task.id);
if (!details) continue;
const stagingReport = details.artifacts?.find(
(a: any) => a.name === "staging-report.json" && a.type === "doc"
);
if (!stagingReport?.content) continue;
let report: any;
try {
report = JSON.parse(stagingReport.content);
} catch { continue; }
if (report.status !== "awaiting_approval") continue;
const codeArtifacts = details.artifacts?.filter((a: any) => a.type === "code") || [];
const validFiles = codeArtifacts.filter((a: any) =>
report.stagedFiles?.includes(a.name)
);
stagedItems.push({
taskId: task.id,
mainTaskId: report.mainTaskId || task.parentId,
title: report.title || task.title,
description: task.description,
validationScore: report.validationScore,
stagedAt: report.stagedAt,
files: validFiles.map((a: any) => ({
artifactId: a.id,
path: a.name,
content: a.content,
lines: (a.content || "").split("\n").length,
})),
blockedFiles: report.blockedFiles || [],
});
}
res.json({ success: true, staged: stagedItems });
} catch (error: any) {
res.status(500).json({ success: false, error: error.message });
}
});
router.post("/publish/:taskId", async (req: Request, res: Response) => {
try {
const taskId = parseInt(req.params.taskId);
const { toolManager } = await import("../autonomous/tools");
const { PROTECTED_FILES } = await import("./agents/ExecutorAgent");
const details = await blackboardService.getTaskWithDetails(taskId);
if (!details) {
return res.status(404).json({ success: false, error: "Tarefa não encontrada" });
}
const stagingReport = details.artifacts?.find(
(a: any) => a.name === "staging-report.json" && a.type === "doc"
);
if (!stagingReport?.content) {
return res.status(400).json({ success: false, error: "Nenhum staging encontrado para esta tarefa" });
}
let report: any;
try {
report = JSON.parse(stagingReport.content);
} catch {
return res.status(400).json({ success: false, error: "Relatório de staging inválido" });
}
if (report.status !== "awaiting_approval") {
return res.status(400).json({ success: false, error: "Tarefa já foi publicada ou descartada" });
}
const codeArtifacts = details.artifacts?.filter((a: any) => a.type === "code") || [];
const validFiles = codeArtifacts.filter((a: any) =>
report.stagedFiles?.includes(a.name) && !PROTECTED_FILES.includes(a.name)
);
const appliedFiles: string[] = [];
const errors: string[] = [];
for (const artifact of validFiles) {
const result = await toolManager.execute("write_file", {
path: artifact.name,
content: artifact.content,
createDirs: true,
});
if (result.success) {
appliedFiles.push(artifact.name);
} else {
errors.push(`${artifact.name}: ${result.error}`);
}
}
if (appliedFiles.length > 0) {
const mainTask = await blackboardService.getMainTask(taskId);
const commitMessage = `[Arcadia Publicado] ${mainTask?.title || report.title || "Alterações aprovadas"}`;
await toolManager.execute("git_local_commit", {
message: commitMessage,
files: appliedFiles,
});
}
report.status = "published";
report.publishedAt = new Date().toISOString();
report.publishedFiles = appliedFiles;
report.publishErrors = errors.length > 0 ? errors : undefined;
await blackboardService.addArtifact(
taskId,
"doc",
"staging-report.json",
JSON.stringify(report, null, 2),
"executor"
);
res.json({
success: true,
applied: appliedFiles,
errors: errors.length > 0 ? errors : undefined,
message: `${appliedFiles.length} arquivo(s) publicado(s) com sucesso`
});
} catch (error: any) {
console.error("[Blackboard] Erro ao publicar:", error);
res.status(500).json({ success: false, error: error.message });
}
});
router.post("/discard/:taskId", async (req: Request, res: Response) => {
try {
const taskId = parseInt(req.params.taskId);
const details = await blackboardService.getTaskWithDetails(taskId);
if (!details) {
return res.status(404).json({ success: false, error: "Tarefa não encontrada" });
}
const stagingReport = details.artifacts?.find(
(a: any) => a.name === "staging-report.json" && a.type === "doc"
);
if (!stagingReport?.content) {
return res.status(400).json({ success: false, error: "Nenhum staging encontrado" });
}
let report: any;
try {
report = JSON.parse(stagingReport.content);
} catch {
return res.status(400).json({ success: false, error: "Relatório inválido" });
}
report.status = "discarded";
report.discardedAt = new Date().toISOString();
await blackboardService.addArtifact(
taskId,
"doc",
"staging-report.json",
JSON.stringify(report, null, 2),
"executor"
);
res.json({ success: true, message: "Alterações descartadas" });
} catch (error: any) {
res.status(500).json({ success: false, error: error.message });
}
});
export default router;