From 3b665e4cbe1a7dfb24a18b3a14f7afc4e054a367 Mon Sep 17 00:00:00 2001 From: OG T Date: Sun, 7 Jun 2026 20:35:09 +0800 Subject: [PATCH] fix traffic alert source mapping and claim-stall gating --- apps/web/src/app/api/open-tasks/route.ts | 4 ++ .../web/src/lib/traffic-conversion-monitor.ts | 58 +++++++++++++++++-- deploy_all.sh | 6 +- 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/apps/web/src/app/api/open-tasks/route.ts b/apps/web/src/app/api/open-tasks/route.ts index 4768573..f0bd857 100644 --- a/apps/web/src/app/api/open-tasks/route.ts +++ b/apps/web/src/app/api/open-tasks/route.ts @@ -287,6 +287,8 @@ export async function GET(request: Request) { actorType: actor.actorType, actorId: actor.actorId, taskId: "open-tasks", + sourceIp, + userAgent: request.headers.get("user-agent") ?? "unknown", message: `External discovery call for open tasks (${publicPayload.length} items)`, metadata: { ...trace, @@ -361,6 +363,8 @@ export async function GET(request: Request) { actorType: actor.actorType, actorId: actor.actorId, taskId: "open-tasks", + sourceIp, + userAgent: request.headers.get("user-agent") ?? "unknown", message: `open-tasks 查詢失敗: ${errorName}: ${errorMessage}`, metadata: { ...trace, diff --git a/apps/web/src/lib/traffic-conversion-monitor.ts b/apps/web/src/lib/traffic-conversion-monitor.ts index 0ac1c9a..d03fa46 100644 --- a/apps/web/src/lib/traffic-conversion-monitor.ts +++ b/apps/web/src/lib/traffic-conversion-monitor.ts @@ -8,6 +8,8 @@ type FunnelSummary = { submitEvents: number; judgePassEvents: number; judgeFailEvents: number; + openTaskCount: number; + sampleOpenTasks: string[]; externalOpenedActors: number; externalClaimingActors: number; externalSubmittingActors: number; @@ -25,6 +27,7 @@ type MonitorInput = { const DEFAULT_PERIOD_MINUTES = 10; const ALERT_TTL_SECONDS = 600; +let redisDedupeFailureLogged = false; function asRecordJson(value: unknown): Record | undefined { if (typeof value === "object" && value !== null && !Array.isArray(value)) { @@ -77,7 +80,7 @@ function isMissingLedgerTableError(error: unknown) { async function fetchFunnelSummary(minutes: number): Promise { const since = new Date(Date.now() - minutes * 60 * 1000); - const [summaryRows, actorRows, judgeRows] = await Promise.all([ + const [summaryRows, actorRows, judgeRows, openTaskRows, openTaskCount] = await Promise.all([ prisma.auditEvent.groupBy({ by: ["action"], where: { @@ -106,6 +109,33 @@ async function fetchFunnelSummary(minutes: number): Promise { }, select: { metadata: true }, }), + prisma.task.findMany({ + where: { + status: "OPEN", + title: { + not: { + startsWith: "GitHub Issue:", + }, + }, + }, + orderBy: { + created_at: "desc", + }, + select: { + id: true, + }, + take: 5, + }), + prisma.task.count({ + where: { + status: "OPEN", + title: { + not: { + startsWith: "GitHub Issue:", + }, + }, + }, + }), ]); let payoutCaptured = 0; @@ -192,6 +222,7 @@ async function fetchFunnelSummary(minutes: number): Promise { actorId: item.actorId, opens: item.opens, })); + const sampleOpenTasks = openTaskRows.map((task) => task.id); return { discoveryEvents, @@ -199,6 +230,8 @@ async function fetchFunnelSummary(minutes: number): Promise { submitEvents, judgePassEvents, judgeFailEvents, + openTaskCount, + sampleOpenTasks, externalOpenedActors, externalClaimingActors, externalSubmittingActors, @@ -218,6 +251,8 @@ function buildAlertMessage(rule: string, summary: FunnelSummary) { submitEvents, judgePassEvents, payoutCaptured, + openTaskCount, + sampleOpenTasks, externalOpenedActors, externalClaimingActors, externalSubmittingActors, @@ -227,7 +262,9 @@ function buildAlertMessage(rule: string, summary: FunnelSummary) { switch (rule) { case "EXTERNAL_FUNNEL_CLAIM_STALL": - return `外部曝光已達 ${discoveryEvents}(最近 ${periodMinutes} 分鐘),但尚無接案(EXTERNAL_CLAIM_TASK_SUCCESS = ${claimEvents})。請檢查任務是否包含可直接執行的 npx 指令與明確交付條件。`; + return `外部曝光已達 ${discoveryEvents}(最近 ${periodMinutes} 分鐘),待接任務 ${openTaskCount} 筆,但尚無接案(EXTERNAL_CLAIM_TASK_SUCCESS = ${claimEvents})。` + + `${sampleOpenTasks.length > 0 ? `可用任務樣本: ${sampleOpenTasks.join(", ")}。` : ""}` + + `請檢查任務是否包含可直接執行的 npx 指令與明確交付條件。`; case "EXTERNAL_FUNNEL_SUBMIT_STALL": return `外部已有 ${claimEvents} 筆接案,但近期 ${periodMinutes} 分鐘無任何提交(EXTERNAL_SUBMIT_SOLUTION_SUCCESS = ${submitEvents})。請先加速回傳格式與驗收測試規格。`; case "EXTERNAL_FUNNEL_PASS_STALL": @@ -248,14 +285,14 @@ function buildAlertMessage(rule: string, summary: FunnelSummary) { function alertRules(summary: FunnelSummary): Array<{ action: string; message: string }> { const alerts: Array<{ action: string; message: string }> = []; - if (summary.discoveryEvents > 0 && summary.claimEvents === 0) { + if (summary.discoveryEvents >= 3 && summary.openTaskCount > 0 && summary.claimEvents === 0) { alerts.push({ action: "EXTERNAL_FUNNEL_CLAIM_STALL", message: buildAlertMessage("EXTERNAL_FUNNEL_CLAIM_STALL", summary), }); } - if (summary.externalOnlyOpenActors >= 3 && summary.discoveryEvents >= 10) { + if (summary.externalOnlyOpenActors >= 3 && summary.discoveryEvents >= 10 && summary.openTaskCount > 0) { alerts.push({ action: "EXTERNAL_FUNNEL_OPEN_COLD_STANDBY", message: buildAlertMessage("EXTERNAL_FUNNEL_OPEN_COLD_STANDBY", summary), @@ -289,9 +326,18 @@ function alertRules(summary: FunnelSummary): Array<{ action: string; message: st async function shouldEmitAlert(key: string): Promise { try { const result = await redis.set(key, String(Date.now()), "EX", ALERT_TTL_SECONDS, "NX"); + if (redisDedupeFailureLogged) { + redisDedupeFailureLogged = false; + } return result === "OK"; } catch (error) { - console.warn("[traffic-monitor] redis dedupe failed", error); + if (!redisDedupeFailureLogged) { + console.warn( + "[traffic-monitor] redis dedupe failed, fallback no-dedupe mode", + error instanceof Error ? error.message : error + ); + redisDedupeFailureLogged = true; + } return true; } } @@ -322,6 +368,8 @@ export async function evaluateExternalFunnelHealth(input: MonitorInput): Promise submit_events: summary.submitEvents, judge_pass_events: summary.judgePassEvents, judge_fail_events: summary.judgeFailEvents, + open_task_count: summary.openTaskCount, + sample_open_tasks: summary.sampleOpenTasks, external_opened_actors: summary.externalOpenedActors, external_claiming_actors: summary.externalClaimingActors, external_submitting_actors: summary.externalSubmittingActors, diff --git a/deploy_all.sh b/deploy_all.sh index 3b822cd..477733c 100755 --- a/deploy_all.sh +++ b/deploy_all.sh @@ -1,6 +1,10 @@ #!/usr/bin/expect -f set timeout -1 -spawn ssh -J wooo@192.168.0.110 wooo@192.168.0.188 "cd /home/wooo/deployments/agent-bounty-protocol && git pull origin main && docker compose exec -T web npx tsx seed.ts" +set jump_host "wooo@192.168.0.110" +set target_host "ollama@192.168.0.188" +set repo_dir "/home/ollama/vibework-git" + +spawn ssh -J $jump_host $target_host "cd $repo_dir && git pull origin main && docker compose pull && docker compose up -d --build db web scout-bot && docker compose exec -T web npx prisma@6.4.1 db push --schema=apps/web/prisma/schema.prisma --skip-generate && docker compose exec -T web npx tsx apps/web/seed.ts" expect { "*assword:*" { send "0936223270\r"