Files
ams-backend/src/routes/agenttasks.ts

282 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<AgentTask, "_id"> = {
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<AgentTask>("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<string, unknown> = {};
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<AgentTask>("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<AgentTask>("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<AgentTask>("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<AgentTask>("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<AgentTask>("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<AgentTask>("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<AgentTask>("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/<LINKED_TASK_ID> -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/<ID> -H "Authorization: Bearer ${serviceKey}" -H "Content-Type: application/json" -d '{"status":"done","result":"<Zusammenfassung>"}'
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<string, unknown> = { updatedAt: new Date() };
if (status) updateFields.status = status;
if (result !== undefined) updateFields.result = result;
try {
const res = await db.collection<AgentTask>("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" };
}
});