import { Router } from "@oak/oak"; import { ObjectId } from "mongodb"; import { getDB } from "../db/mongo.ts"; import { authMiddleware } from "../middleware/auth.ts"; import { getNextNumber } from "./tasks.ts"; export const agentTasksRouter = new Router({ prefix: "/api/agent-tasks" }); interface AgentTask { _id: ObjectId; number?: number; message: string; projectId?: string; projectName?: string; agentId?: string; agentName?: string; linkedTaskIds: string[]; linkedTaskTitles: string[]; status: "pending" | "in_progress" | "done" | "rejected"; createdBy: string; createdByName: string; result?: string; createdAt: Date; updatedAt: Date; } // Neuen Auftrag erstellen agentTasksRouter.post("/", authMiddleware, async (ctx) => { const db = await getDB(); const body = await ctx.request.body.json(); const { message, projectId, projectName, agentId, agentName, linkedTaskIds, linkedTaskTitles } = body; if (!message || !message.trim()) { ctx.response.status = 400; ctx.response.body = { error: "Nachricht ist erforderlich" }; return; } const now = new Date(); const number = await getNextNumber("agent_task_number"); const doc: Omit = { number, message: message.trim(), projectId: projectId || undefined, projectName: projectName || undefined, agentId: agentId || undefined, agentName: agentName || undefined, linkedTaskIds: linkedTaskIds || [], linkedTaskTitles: linkedTaskTitles || [], status: "pending", createdBy: ctx.state.user.id, createdByName: ctx.state.user.username, createdAt: now, updatedAt: now, }; const result = await db.collection("agent_tasks").insertOne(doc as AgentTask); ctx.response.status = 201; ctx.response.body = { message: "Auftrag erstellt", id: result.insertedId.toString(), }; }); // Alle Aufträge laden (mit optionalen Filtern) agentTasksRouter.get("/", authMiddleware, async (ctx) => { const db = await getDB(); const url = ctx.request.url; const filter: Record = {}; const status = url.searchParams.get("status"); if (status) filter.status = status; const agentId = url.searchParams.get("agentId"); if (agentId) filter.agentId = agentId; const tasks = await db.collection("agent_tasks") .find(filter) .sort({ createdAt: -1 }) .limit(100) .toArray(); ctx.response.body = { tasks }; }); // Pending Aufträge für Agenten (Kurzform) agentTasksRouter.get("/pending", authMiddleware, async (ctx) => { const db = await getDB(); const tasks = await db.collection("agent_tasks") .find({ status: "pending" }) .sort({ createdAt: 1 }) .toArray(); ctx.response.body = { tasks }; }); // Auftrag nach Nummer laden agentTasksRouter.get("/by-number/:number", authMiddleware, async (ctx) => { const db = await getDB(); const num = parseInt(ctx.params.number); if (isNaN(num)) { ctx.response.status = 400; ctx.response.body = { error: "Ungültige Nummer" }; return; } const task = await db.collection("agent_tasks").findOne({ number: num }); if (!task) { ctx.response.status = 404; ctx.response.body = { error: "Auftrag nicht gefunden" }; return; } ctx.response.body = { task }; }); // Einzelnen Auftrag laden agentTasksRouter.get("/:id", authMiddleware, async (ctx) => { const db = await getDB(); const id = ctx.params.id; try { const task = await db.collection("agent_tasks") .findOne({ _id: new ObjectId(id) }); if (!task) { ctx.response.status = 404; ctx.response.body = { error: "Auftrag nicht gefunden" }; return; } ctx.response.body = { task }; } catch { ctx.response.status = 400; ctx.response.body = { error: "Ungültige ID" }; } }); // OpenClaw Agent-Proxy — startet isolierten Agent-Run über WireGuard VPN (hooks/agent endpoint) agentTasksRouter.post("/wake", authMiddleware, async (ctx) => { const openclawBaseUrl = Deno.env.get("OPENCLAW_WAKE_URL"); // e.g. http://10.10.0.3:18789/hooks/wake const openclawToken = Deno.env.get("OPENCLAW_WAKE_TOKEN"); if (!openclawBaseUrl || !openclawToken) { ctx.response.status = 503; ctx.response.body = { error: "OpenClaw Wake nicht konfiguriert" }; return; } // Pending Tasks laden für den Prompt const db = await getDB(); const pendingTasks = await db.collection("agent_tasks") .find({ status: "pending" }) .sort({ createdAt: 1 }) .toArray(); if (pendingTasks.length === 0) { ctx.response.body = { message: "Keine pending Tasks" }; return; } // Auto-Timeout: Tasks die >30 Minuten in_progress sind, auf pending zurücksetzen const TIMEOUT_MS = 30 * 60 * 1000; const timeoutThreshold = new Date(Date.now() - TIMEOUT_MS); await db.collection("agent_tasks").updateMany( { status: "in_progress", updatedAt: { $lt: timeoutThreshold } }, { $set: { status: "pending", updatedAt: new Date() } } ); // Lock: Nur ein Task gleichzeitig pro Agent const inProgressCount = await db.collection("agent_tasks") .countDocuments({ status: "in_progress" }); if (inProgressCount > 0) { ctx.response.body = { message: "Agent arbeitet bereits an einem Task", blocked: true }; return; } // Prompt mit Task-Details bauen const taskList = pendingTasks.map((t, i) => `${i + 1}. [${t._id}] "${t.message}" (Projekt: ${t.projectName || "–"}, von: ${t.createdByName})` ).join("\n"); const serviceKey = Deno.env.get("AMS_SERVICE_API_KEY") || ""; // Nur den ERSTEN pending Task bearbeiten (1 Task pro Run) const firstTask = pendingTasks[0]; const prompt = `WICHTIG: Lade zuerst CHECKLIST.md und WORKFLOW.md aus deinem Workspace und halte dich strikt daran! WICHTIG: KEINE Sub-Agenten spawnen (sessions_spawn)! Du bearbeitest Tasks SELBST, einzeln, nacheinander. Nur 1 Task pro Run! Dein Agent-Task: [${firstTask._id}] "${firstTask.message}" (Projekt: ${firstTask.projectName || "–"}, von: ${firstTask.createdByName}) Bearbeite diesen Task nach der CHECKLIST.md: 1. Task im AMS auf "in_progress" setzen: curl -X PUT https://api.ams.agentenbude.de/api/tasks/ -H "Authorization: Bearer ${serviceKey}" -H "Content-Type: application/json" -d '{"status":"in_progress"}' 2. Feature-Branch erstellen (feature/beschreibung) 3. Backend ZUERST implementieren (bei Full-Stack) 4. Kein \`any\`, kein \`console.log\`, keine Secrets im Code 5. Dark Theme beachten! Alle UI-Elemente müssen zum bestehenden Dark-Mode-Design passen (Farben: #1a1a2e Hintergrund, rgba(255,255,255,0.05-0.15) für Karten/Borders, helle Textfarben) 6. Self-Review: npm run build (FE) / deno check (BE) 7. Conventional Commits auf DEUTSCH 8. MR erstellen und mergen 9. Tag + Deploy 10. Agent-Task auf "done" setzen: curl -X PUT https://api.ams.agentenbude.de/api/agent-tasks/ -H "Authorization: Bearer ${serviceKey}" -H "Content-Type: application/json" -d '{"status":"done","result":""}' 11. Task-Kommentar mit Zusammenfassung + MR + Version 12. Melde dem Chef das Ergebnis. API-Auth: Bearer ${serviceKey} AMS-API: https://api.ams.agentenbude.de/api`; // /hooks/agent statt /hooks/wake aufrufen const agentUrl = openclawBaseUrl.replace("/hooks/wake", "/hooks/agent"); try { const response = await fetch(agentUrl, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${openclawToken}`, }, body: JSON.stringify({ message: prompt, name: "AMS-Task", sessionKey: "hook:ams-task", wakeMode: "now", deliver: true, channel: "telegram", to: "7150608398", }), signal: AbortSignal.timeout(5000), }); if (response.ok) { ctx.response.body = { message: "Agent gestartet", tasks: pendingTasks.length }; } else { ctx.response.status = 502; ctx.response.body = { error: "Agent-Start fehlgeschlagen", status: response.status }; } } catch { ctx.response.status = 502; ctx.response.body = { error: "OpenClaw nicht erreichbar" }; } }); // Auftrag-Status aktualisieren (Agent nimmt an / erledigt / lehnt ab) agentTasksRouter.put("/:id", authMiddleware, async (ctx) => { const db = await getDB(); const id = ctx.params.id; const body = await ctx.request.body.json(); const { status, result } = body; const validStatuses = ["pending", "in_progress", "done", "rejected"]; if (status && !validStatuses.includes(status)) { ctx.response.status = 400; ctx.response.body = { error: `Ungültiger Status. Erlaubt: ${validStatuses.join(", ")}` }; return; } const updateFields: Record = { updatedAt: new Date() }; if (status) updateFields.status = status; if (result !== undefined) updateFields.result = result; try { const res = await db.collection("agent_tasks").updateOne( { _id: new ObjectId(id) }, { $set: updateFields } ); if (res.matchedCount === 0) { ctx.response.status = 404; ctx.response.body = { error: "Auftrag nicht gefunden" }; return; } ctx.response.body = { message: "Auftrag aktualisiert" }; } catch { ctx.response.status = 400; ctx.response.body = { error: "Ungültige ID" }; } });