fix: add external traffic monitoring and webhook alerts
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 6s
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 6s
This commit is contained in:
@@ -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 });
|
||||
|
||||
@@ -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",
|
||||
|
||||
41
apps/web/src/app/api/traffic/route.ts
Normal file
41
apps/web/src/app/api/traffic/route.ts
Normal 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(),
|
||||
});
|
||||
}
|
||||
36
apps/web/src/lib/traffic-alert.ts
Normal file
36
apps/web/src/lib/traffic-alert.ts
Normal 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.
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user