Initial commit: AMS Backend - Deno + Oak + MongoDB
This commit is contained in:
281
src/routes/agenttasks.ts
Normal file
281
src/routes/agenttasks.ts
Normal 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" };
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user