fix: add external traffic monitoring and webhook alerts
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 6s

This commit is contained in:
OG T
2026-06-07 14:53:45 +08:00
parent 29482e1ee8
commit 23fa73c895
4 changed files with 163 additions and 2 deletions

View File

@@ -12,6 +12,7 @@ import { runSubmissionInSandbox } from "@/lib/sandbox";
import { logAuditEvent } from "@/lib/audit";
import { redis } from "@/lib/redis";
import { authHold, capturePayment } from "@/lib/payment";
import { sendTrafficAlert } from "@/lib/traffic-alert";
import crypto from "crypto";
import { z } from "zod";
@@ -29,8 +30,9 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
return NextResponse.json({ error: "Forbidden: Invalid API Key" }, { status: 403 });
}
let body: unknown = null;
try {
const body = await request.json();
body = await request.json();
switch (tool) {
case "list_open_tasks": {
@@ -56,6 +58,20 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
description_preview: t.description.substring(0, 100) + (t.description.length > 100 ? "..." : ""),
}));
void sendTrafficAlert({
level: "info",
action: "EXTERNAL_LIST_OPEN_TASKS_MCP",
surface: "mcp/list_open_tasks",
actorType: "USER",
actorId: "mcp-anonymous",
taskId: "open-tasks",
message: "外部 MCP 查詢任務列表",
metadata: {
count: formattedTasks.length,
source_tool: tool,
},
});
return NextResponse.json({
tasks: formattedTasks,
total_open: formattedTasks.length,
@@ -121,6 +137,21 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
return newClaim;
});
void sendTrafficAlert({
level: "info",
action: "EXTERNAL_CLAIM_TASK_SUCCESS",
surface: "mcp/claim_task",
actorType: "AGENT",
actorId: parsed.developer_wallet,
taskId: claim.task_id,
message: `Agent 成功接案: ${parsed.task_id}`,
metadata: {
agent_id: parsed.agent_id,
reward: claim.held_amount,
currency: claim.held_currency,
},
});
// Set Redis TTL key (3600 seconds)
await redis.set(`vw:task:${claim.task_id}:executing`, claim.claim_token, "EX", 3600);
@@ -179,6 +210,20 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
return newSubmission;
});
void sendTrafficAlert({
level: "info",
action: "EXTERNAL_SUBMIT_SOLUTION_SUCCESS",
surface: "mcp/submit_solution",
actorType: "AGENT",
actorId: parsed.developer_wallet,
taskId: submission.task_id,
message: `Agent 提交解法: ${parsed.task_id}`,
metadata: {
claim_id: submission.claim_id,
deliverable_count: Object.keys(parsed.deliverables ?? {}).length,
},
});
// Async trigger E2B Sandbox evaluation
const taskObj = await prisma.task.findUnique({ where: { id: submission.task_id }});
if (taskObj && typeof taskObj.acceptance_criteria === "object" && taskObj.acceptance_criteria !== null) {
@@ -288,6 +333,21 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
}
} catch (error: any) {
console.error(`[API Gateway] Error handling ${tool}:`, error);
void sendTrafficAlert({
level: "error",
action: `EXTERNAL_${tool.toUpperCase()}_ERROR`,
surface: `mcp/${tool}`,
actorType: "AGENT",
actorId: typeof body === "object" && body !== null && "developer_wallet" in body
? String((body as { developer_wallet?: string }).developer_wallet ?? "unknown")
: "unknown",
taskId:
typeof body === "object" && body !== null && "task_id" in body
? String((body as { task_id?: string }).task_id ?? "unknown")
: "unknown",
message: `${error instanceof Error ? error.message : String(error)}`,
});
if (error.name === "ZodError") {
return NextResponse.json({ error_type: "InvalidParams", message: error.errors }, { status: 400 });

View File

@@ -18,7 +18,7 @@ const getPayoutMode = (task: {
return "PAYMENT_PENDING";
};
export async function GET() {
export async function GET(request: Request) {
const tasks = await prisma.task.findMany({
where: { status: TaskStatus.OPEN },
orderBy: { created_at: "desc" },
@@ -56,6 +56,30 @@ export async function GET() {
task_url: `https://agent.wooo.work/tasks/${task.id}`,
}));
const sourceIp =
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ??
request.headers.get("x-real-ip") ??
"unknown";
void prisma.auditEvent.create({
data: {
actorType: "USER",
actorId: `open-tasks:${sourceIp}`,
action: "EXTERNAL_LIST_OPEN_TASKS",
entityType: "TASK",
entityId: "open-tasks",
beforeState: null,
afterState: {
total_open: publicPayload.length,
},
reason: "external-discovery",
metadata: {
source: "public-open-tasks",
user_agent: request.headers.get("user-agent") ?? "unknown",
},
},
}).catch(() => {});
return NextResponse.json({
platform: "VibeWork",
version: "v1",

View File

@@ -0,0 +1,41 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
export const dynamic = "force-dynamic";
const MONITOR_TOKEN = process.env.TRAFFIC_MONITOR_TOKEN;
export async function GET(request: NextRequest) {
if (MONITOR_TOKEN) {
const token = request.headers.get("x-traffic-token");
if (token !== MONITOR_TOKEN) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
}
const since = new Date(Date.now() - 24 * 60 * 60 * 1000);
const [summaryRows, totalRows] = await Promise.all([
prisma.auditEvent.groupBy({
by: ["action"],
where: {
createdAt: { gte: since },
},
_count: { _all: true },
}),
prisma.auditEvent.count({
where: { createdAt: { gte: since } },
}),
]);
const actionSummary = Object.fromEntries(
summaryRows.map((row) => [row.action, row._count._all])
);
return NextResponse.json({
period_hours: 24,
total_events: totalRows,
action_summary: actionSummary,
updated_at: new Date().toISOString(),
});
}

View File

@@ -0,0 +1,36 @@
export type TrafficAlertEvent = {
level: "info" | "warning" | "error";
action: string;
surface: string;
actorType: "SYSTEM" | "AGENT" | "USER";
actorId: string;
taskId?: string;
message: string;
metadata?: Record<string, unknown>;
};
const TRAFFIC_WEBHOOK_URL = process.env.VIBEWORK_TRAFFIC_WEBHOOK_URL?.trim();
export async function sendTrafficAlert(event: TrafficAlertEvent): Promise<void> {
if (!TRAFFIC_WEBHOOK_URL) return;
const payload = {
platform: "agent-bounty-protocol",
created_at: new Date().toISOString(),
...event,
};
try {
await fetch(TRAFFIC_WEBHOOK_URL, {
method: "POST",
headers: {
"content-type": "application/json",
"x-trace-source": "agent-bounty-protocol",
},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(3000),
});
} catch {
// Avoid affecting request flow if webhook fails.
}
}