feat: add external funnel monitoring and conversion alerts
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 7s

This commit is contained in:
OG T
2026-06-07 16:40:38 +08:00
parent 7c4841c27d
commit 71a765d1de
5 changed files with 271 additions and 2 deletions

View File

@@ -14,6 +14,7 @@ import { logAuditEvent } from "@/lib/audit";
import { redis } from "@/lib/redis";
import { authHold, capturePayment } from "@/lib/payment";
import { sendTrafficAlert } from "@/lib/traffic-alert";
import { evaluateExternalFunnelHealth } from "@/lib/traffic-conversion-monitor";
import crypto from "crypto";
import { z } from "zod";
@@ -182,6 +183,13 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
},
});
if (isPublicIp) {
void evaluateExternalFunnelHealth({
surface: "mcp/list_open_tasks",
periodMinutes: 10,
});
}
void prisma.auditEvent.count({
where: {
createdAt: {
@@ -288,6 +296,11 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
},
});
void evaluateExternalFunnelHealth({
surface: "mcp/claim_task",
periodMinutes: 10,
});
// Set Redis TTL key (3600 seconds)
await redis.set(`vw:task:${claim.task_id}:executing`, claim.claim_token, "EX", 3600);
@@ -364,6 +377,11 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
},
});
void evaluateExternalFunnelHealth({
surface: "mcp/submit_solution",
periodMinutes: 10,
});
// 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) {

View File

@@ -3,6 +3,7 @@ import { prisma } from "@/lib/prisma";
import { TaskStatus } from "@agent-bounty/contracts";
import { sendTrafficAlert } from "@/lib/traffic-alert";
import { isIP } from "node:net";
import { evaluateExternalFunnelHealth } from "@/lib/traffic-conversion-monitor";
export const dynamic = "force-dynamic";
@@ -235,6 +236,13 @@ export async function GET(request: Request) {
}
}).catch(() => {});
if (isPublicIp) {
void evaluateExternalFunnelHealth({
surface: "public-open-tasks",
periodMinutes: 10,
});
}
return NextResponse.json({
platform: "VibeWork",
version: "v1",

View File

@@ -162,6 +162,14 @@ export async function GET(request: NextRequest) {
return false;
};
const isInternalActor = (params: {
actorType: string | null | undefined;
actorId: string | null | undefined;
}) => {
if (params.actorType === "AGENT") return false;
return isInternalActorId(params.actorId);
};
const externalActorSummary = externalActorRows
.map((row) => ({
actorId: row.actorId || "unknown",
@@ -246,7 +254,11 @@ export async function GET(request: NextRequest) {
});
const recentExternalEvents = recentEvents.filter((event) =>
event.action.startsWith("EXTERNAL_") && !isInternalActorId(event.actorId)
event.action.startsWith("EXTERNAL_") &&
!isInternalActor({
actorType: event.actorType,
actorId: event.actorId,
})
);
const recentInternalEvents = recentEvents.filter(

View File

@@ -17,6 +17,10 @@ const EVENT_LABELS: Record<string, string> = {
EXTERNAL_SUBMIT_SOLUTION_ERROR: "外部提交失敗",
EXTERNAL_LIST_OPEN_TASKS_ERROR: "外部公開流量端點錯誤",
EXTERNAL_LIST_OPEN_TASKS_MCP_ERROR: "外部 MCP 流量端點錯誤",
EXTERNAL_FUNNEL_CLAIM_STALL: "外部曝光後未接案",
EXTERNAL_FUNNEL_SUBMIT_STALL: "外部接案後未提交",
EXTERNAL_FUNNEL_PASS_STALL: "外部提交後未 PASS",
EXTERNAL_FUNNEL_PAYOUT_STALL: "PASS 後未出金",
JUDGE_COMPLETE: "AI 交件判定完成",
};
@@ -61,6 +65,11 @@ function isInternalActorId(value: string | null | undefined) {
return false;
}
function isInternalActor(input: { actorType: string | null | undefined; actorId: string | null | undefined }) {
if (input.actorType === "AGENT") return false;
return isInternalActorId(input.actorId);
}
function isAuthorizedToken(token: string | undefined, tokenHeader: string | undefined) {
if (!token) return true;
return tokenHeader === token;
@@ -245,7 +254,13 @@ async function getTrafficSummary(minutes: number) {
externalActorSummary,
externalEventTypes,
internalEventTypes,
recentExternalEvents: recentEvents.filter((event) => event.action.startsWith("EXTERNAL_") && !isInternalActorId(event.actorId)),
recentExternalEvents: recentEvents.filter((event) =>
event.action.startsWith("EXTERNAL_") &&
!isInternalActor({
actorType: event.actorType,
actorId: event.actorId,
})
),
recentInternalEvents: recentEvents.filter((event) => !event.action.startsWith("EXTERNAL_")),
conversionSummary,
conversionRates,