Initial commit: AMS Backend - Deno + Oak + MongoDB

This commit is contained in:
FluxKit
2026-02-19 14:02:53 +00:00
commit 656a37efda
36 changed files with 7648 additions and 0 deletions

281
src/routes/agenttasks.ts Normal file
View File

@@ -0,0 +1,281 @@
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" };
}
});