fix: clarify traffic quality and compact frontend
All checks were successful
CI and Production Smoke / smoke (push) Successful in 7s

This commit is contained in:
OG T
2026-06-11 20:14:56 +08:00
parent bbfe7409d3
commit a87b421817
6 changed files with 612 additions and 559 deletions

View File

@@ -1,5 +1,12 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import {
isInternalActor,
isLikelyAIAgentActor,
isSyntheticTrafficEvent,
resolveActorClass,
type TrafficActorClass,
} from "@/lib/traffic-actor-classification";
export const dynamic = "force-dynamic";
@@ -68,52 +75,6 @@ function normalizePayloadSummary(value: unknown) {
return "unknown";
}
const AI_USER_AGENT_HINTS = [
"gpt",
"chatgpt",
"openai",
"anthropic",
"claude",
"perplexity",
"llm",
"mcp",
"autogpt",
"agent",
"assistant",
"gemini",
"cursor",
"copilot",
];
type TrafficActorClass = "a2a" | "external_ai_agent" | "likely_ai_agent" | "other_external";
function resolveActorClass(
action: string,
actorType: string | null | undefined,
actorId: string | null | undefined,
metadata: Record<string, unknown> | undefined,
surface: string | undefined
) {
const normalizedSurface = (surface || "").toLowerCase();
if (normalizedSurface.startsWith("mcp/")) {
return "a2a";
}
const normalizedActorId = (actorId || "").toLowerCase();
if (actorType === "AGENT" || normalizedActorId.startsWith("agent:")) {
if (action.startsWith("EXTERNAL_") && normalizedSurface.startsWith("mcp/")) {
return "a2a";
}
return "external_ai_agent";
}
if (isLikelyAIAgentActor(actorType, actorId, metadata)) {
return "likely_ai_agent";
}
return "other_external";
}
function resolveMetadata(value: unknown): Record<string, unknown> | undefined {
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
return value as Record<string, unknown>;
@@ -179,38 +140,6 @@ function resolveResponseStatus(event: { metadata: unknown }) {
return undefined;
}
function isLikelyAIAgentActor(
actorType: string | null | undefined,
actorId: string | null | undefined,
metadata: Record<string, unknown> | undefined
) {
if (actorType === "AGENT") {
return true;
}
const normalizedActor = (actorId || "").toLowerCase();
if (normalizedActor.startsWith("agent:")) {
return true;
}
const userAgent = String(metadata?.user_agent || "").toLowerCase();
if (AI_USER_AGENT_HINTS.some((token) => userAgent.includes(token))) {
return true;
}
const requestHeaders = asRecordJson(metadata?.request_actor_headers);
if (!requestHeaders) {
return false;
}
const headerText = Object.values(requestHeaders)
.filter((item): item is string => typeof item === "string")
.join(" ")
.toLowerCase();
return AI_USER_AGENT_HINTS.some((token) => headerText.includes(token));
}
function classifyActorSource(
actorType: string | null | undefined,
actorId: string | null | undefined,
@@ -304,6 +233,7 @@ export async function GET(request: NextRequest) {
summaryRows,
actorSummaryRows,
externalActorRows,
externalFunnelRows,
totalRows,
latestEvents,
judgeCompleteRows,
@@ -335,6 +265,24 @@ export async function GET(request: NextRequest) {
},
_count: { _all: true },
}),
prisma.auditEvent.findMany({
where: {
createdAt: { gte: since },
action: {
startsWith: "EXTERNAL_",
},
},
orderBy: {
createdAt: "desc",
},
take: 5000,
select: {
action: true,
actorType: true,
actorId: true,
metadata: true,
},
}),
prisma.auditEvent.count({
where: { createdAt: { gte: since } },
}),
@@ -442,54 +390,29 @@ export async function GET(request: NextRequest) {
actorSummaryRows.map((row) => [row.actorType, row._count._all])
);
const isInternalActorId = (actorId: string | null | undefined) => {
if (!actorId) return true;
const normalized = actorId.toLowerCase();
if (normalized === "unknown" || normalized === "mcp-anonymous") {
return true;
}
const ipMatch = normalized.match(/^open-tasks:([a-z0-9.:_-]+)$/);
if (!ipMatch?.[1]) {
return false;
}
const actorIp = ipMatch[1];
if (actorIp.startsWith("127.") || actorIp.startsWith("10.") || actorIp.startsWith("192.168.")) {
return true;
}
if (actorIp.startsWith("172.")) {
const secondOctet = Number(actorIp.split(".")[1]);
return secondOctet >= 16 && secondOctet <= 31;
}
if (actorIp === "localhost" || actorIp === "unknown" || actorIp.startsWith("fc") || actorIp.startsWith("fd")) {
return true;
}
return false;
};
const isInternalActor = (params: {
actorType: string | null | undefined;
actorId: string | null | undefined;
}) => {
if (params.actorType === "AGENT") return false;
return isInternalActorId(params.actorId);
};
const realExternalFunnelRows = externalFunnelRows.filter((row) => {
const metadata = asRecordJson(row.metadata);
return !isInternalActor({ actorType: row.actorType, actorId: row.actorId, metadata });
});
const syntheticExternalEvents = externalFunnelRows.filter((row) => {
const metadata = asRecordJson(row.metadata);
return isSyntheticTrafficEvent({ actorType: row.actorType, actorId: row.actorId, metadata });
}).length;
const realExternalActionSummary = realExternalFunnelRows.reduce<Record<string, number>>((acc, row) => {
acc[row.action] = (acc[row.action] || 0) + 1;
return acc;
}, {});
const externalActorSummary = externalActorRows
.map((row) => ({
actorId: row.actorId || "unknown",
events: row._count._all,
}))
.filter((row) => !isInternalActorId(row.actorId))
.filter((row) => !isInternalActor({ actorType: "AGENT", actorId: row.actorId }))
.sort((a, b) => b.events - a.events)
.slice(0, 20);
const channelSummary = Object.entries(actionSummary).reduce(
const rawChannelSummary = Object.entries(actionSummary).reduce(
(acc, [action, count]) => {
if (action.startsWith("EXTERNAL_")) {
acc.external += count;
@@ -500,23 +423,29 @@ export async function GET(request: NextRequest) {
},
{ external: 0, internal: 0 } as Record<string, number>
);
const channelSummary = {
external: realExternalFunnelRows.length,
internal: Math.max(totalRows - realExternalFunnelRows.length, 0),
raw_external: rawChannelSummary.external,
synthetic_external: syntheticExternalEvents,
};
const discoveryEvents =
(actionSummary["EXTERNAL_LIST_OPEN_TASKS"] || 0) +
(actionSummary["EXTERNAL_LIST_OPEN_TASKS_MCP"] || 0) +
(actionSummary["EXTERNAL_A2A_ONBOARDING_VIEW"] || 0) +
(actionSummary["EXTERNAL_A2A_DEMAND_CAMPAIGN_KIT_ISSUED"] || 0) +
(actionSummary["EXTERNAL_A2A_INTEGRATION_CATALOG_VIEW"] || 0) +
(actionSummary["EXTERNAL_A2A_GROWTH_KIT_ISSUED"] || 0);
(realExternalActionSummary["EXTERNAL_LIST_OPEN_TASKS"] || 0) +
(realExternalActionSummary["EXTERNAL_LIST_OPEN_TASKS_MCP"] || 0) +
(realExternalActionSummary["EXTERNAL_A2A_ONBOARDING_VIEW"] || 0) +
(realExternalActionSummary["EXTERNAL_A2A_DEMAND_CAMPAIGN_KIT_ISSUED"] || 0) +
(realExternalActionSummary["EXTERNAL_A2A_INTEGRATION_CATALOG_VIEW"] || 0) +
(realExternalActionSummary["EXTERNAL_A2A_GROWTH_KIT_ISSUED"] || 0);
const referralTouchpointEvents = actionSummary["EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED"] || 0;
const proposalViewEvents = actionSummary["EXTERNAL_DEMAND_PROPOSAL_VIEW"] || 0;
const proposalCreatedEvents = actionSummary["EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED"] || 0;
const proposalCheckoutEvents = actionSummary["EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED"] || 0;
const proposalWalletPendingEvents = actionSummary["EXTERNAL_DEMAND_PROPOSAL_WALLET_PAYMENT_PENDING"] || 0;
const proposalPaidEvents = actionSummary["EXTERNAL_DEMAND_PROPOSAL_FEE_CAPTURED"] || 0;
const claimEvents = actionSummary["EXTERNAL_CLAIM_TASK_SUCCESS"] || 0;
const submitEvents = actionSummary["EXTERNAL_SUBMIT_SOLUTION_SUCCESS"] || 0;
const referralTouchpointEvents = realExternalActionSummary["EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED"] || 0;
const proposalViewEvents = realExternalActionSummary["EXTERNAL_DEMAND_PROPOSAL_VIEW"] || 0;
const proposalCreatedEvents = realExternalActionSummary["EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED"] || 0;
const proposalCheckoutEvents = realExternalActionSummary["EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED"] || 0;
const proposalWalletPendingEvents = realExternalActionSummary["EXTERNAL_DEMAND_PROPOSAL_WALLET_PAYMENT_PENDING"] || 0;
const proposalPaidEvents = realExternalActionSummary["EXTERNAL_DEMAND_PROPOSAL_FEE_CAPTURED"] || 0;
const claimEvents = realExternalActionSummary["EXTERNAL_CLAIM_TASK_SUCCESS"] || 0;
const submitEvents = realExternalActionSummary["EXTERNAL_SUBMIT_SOLUTION_SUCCESS"] || 0;
const judgePassEvents = judgeCompleteRows.filter((row) => {
const metadata = asRecordJson(row.metadata);
return normalizedJudgeResult(metadata?.overall_result) === "pass";
@@ -560,8 +489,7 @@ export async function GET(request: NextRequest) {
payout_rate: conversionRate(capturedPayoutCount, judgePassEvents),
};
const externalEventTypes = Object.entries(actionSummary)
.filter(([action]) => action.startsWith("EXTERNAL_"))
const externalEventTypes = Object.entries(realExternalActionSummary)
.map(([action, count]) => ({ action, count }));
const internalEventTypes = Object.entries(actionSummary)
@@ -571,7 +499,8 @@ export async function GET(request: NextRequest) {
const recentEvents = latestEvents.map((event) => {
const metadata = asRecordJson(event.metadata);
const actorClass =
event.action.startsWith("EXTERNAL_") && !isInternalActor({ actorType: event.actorType, actorId: event.actorId })
event.action.startsWith("EXTERNAL_") &&
!isInternalActor({ actorType: event.actorType, actorId: event.actorId, metadata })
? resolveActorClass(
event.action,
event.actorType,
@@ -643,7 +572,12 @@ export async function GET(request: NextRequest) {
metadata,
typeof event.surface === "string" ? event.surface : undefined
);
if (event.action.startsWith("EXTERNAL_")) {
const isTrackedExternalActor =
event.action.startsWith("EXTERNAL_") &&
!isInternalActor({ actorType: event.actorType, actorId: event.actorId, metadata }) &&
(actorClass === "a2a" || actorClass === "external_ai_agent" || actorClass === "likely_ai_agent");
if (isTrackedExternalActor) {
const bucket = externalActorClassSummary.get(actorClass);
if (!bucket) {
externalActorClassSummary.set(actorClass, { events: 1, actors: new Set([actorId]) });
@@ -655,10 +589,6 @@ export async function GET(request: NextRequest) {
const normalizedSurface = normalizeSurface(event.surface);
const normalizedIp = normalizeSourceIp(metadata?.source_ip);
const normalizedUa = normalizeUserAgent(metadata?.user_agent);
const isTrackedExternalActor =
event.action.startsWith("EXTERNAL_") &&
!isInternalActor({ actorType: event.actorType, actorId: event.actorId }) &&
(actorClass === "a2a" || actorClass === "external_ai_agent" || actorClass === "likely_ai_agent");
if (!isTrackedExternalActor) {
return;
@@ -777,6 +707,7 @@ export async function GET(request: NextRequest) {
!isInternalActor({
actorType: event.actorType,
actorId: event.actorId,
metadata: event.metadata,
})
)
.map((event) => ({
@@ -870,6 +801,12 @@ export async function GET(request: NextRequest) {
external_response_status_summary: externalResponseStatusSummary,
external_actor_class_summary: externalActorClassSummaryRows,
external_actor_activities: externalActorActivityRows,
traffic_quality: {
real_external_events: realExternalFunnelRows.length,
raw_external_events: rawChannelSummary.external,
synthetic_external_events: syntheticExternalEvents,
excluded_external_events: Math.max(rawChannelSummary.external - realExternalFunnelRows.length, 0),
},
external_error_rows: externalErrorRowsSorted,
recent_external_events: recentExternalEvents,
recent_internal_events: recentInternalEvents,

View File

@@ -1,5 +1,6 @@
import { prisma } from "@/lib/prisma";
import { A2A_AGENT_INTEGRATIONS, TELEGRAM_CONTROL_PLANE_ROLES } from "@/lib/a2a-agent-integrations";
import { Activity, ArrowUpRight, Bot, ClipboardList, CreditCard, Gauge, Network, Plus, Trophy } from "lucide-react";
import Link from "next/link";
export const dynamic = "force-dynamic";
@@ -18,178 +19,194 @@ export default async function Home() {
});
return (
<div className="min-h-screen bg-gray-950 text-gray-100 p-8 font-sans">
<div className="max-w-5xl mx-auto">
<div className="flex justify-between items-center mb-6">
<h1 className="text-4xl font-extrabold bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-purple-500">
<div className="min-h-screen bg-zinc-950 text-zinc-100 font-sans">
<header className="sticky top-0 z-20 border-b border-zinc-800 bg-zinc-950/90 backdrop-blur">
<div className="mx-auto flex max-w-7xl flex-col gap-3 px-5 py-4 lg:flex-row lg:items-center lg:justify-between lg:px-8">
<Link href="/" className="flex items-center gap-3 text-lg font-semibold text-white">
<Network className="h-5 w-5 text-cyan-300" />
VibeWork AI
</h1>
<div className="flex gap-4">
<Link href="/propose" className="bg-emerald-500 hover:bg-emerald-400 text-gray-950 font-semibold py-2 px-6 rounded-full transition-all duration-300 shadow-lg shadow-emerald-500/20">
$29
</Link>
<nav className="flex flex-wrap gap-2 text-sm">
<Link href="/traffic" className="inline-flex items-center gap-2 rounded-md border border-emerald-400/40 px-3 py-2 font-medium text-emerald-200 hover:bg-emerald-400/10">
<Activity className="h-4 w-4" />
</Link>
<Link href="/showcase" className="bg-emerald-600/20 hover:bg-emerald-600/40 border border-emerald-500/30 text-emerald-400 font-medium py-2 px-6 rounded-full transition-all duration-300 backdrop-blur-md flex items-center gap-2">
<Link href="/admin/traffic" className="inline-flex items-center gap-2 rounded-md border border-amber-300/40 px-3 py-2 font-medium text-amber-100 hover:bg-amber-300/10">
<Gauge className="h-4 w-4" />
</Link>
<Link href="/showcase" className="inline-flex items-center gap-2 rounded-md border border-zinc-700 px-3 py-2 text-zinc-200 hover:bg-zinc-900">
<Trophy className="h-4 w-4" />
</Link>
<Link href="/leaderboard" className="bg-white/5 hover:bg-white/10 border border-white/10 text-white font-medium py-2 px-6 rounded-full transition-all duration-300 backdrop-blur-md flex items-center gap-2">
Agent
<Link href="/leaderboard" className="inline-flex items-center gap-2 rounded-md border border-zinc-700 px-3 py-2 text-zinc-200 hover:bg-zinc-900">
<Bot className="h-4 w-4" />
</Link>
<Link href="/tasks/create" className="bg-blue-600 hover:bg-blue-500 text-white font-medium py-2 px-6 rounded-full transition-all duration-300 shadow-lg shadow-blue-500/30">
<Link href="/tasks/create" className="inline-flex items-center gap-2 rounded-md border border-sky-400/40 px-3 py-2 text-sky-100 hover:bg-sky-400/10">
<Plus className="h-4 w-4" />
Bounty
</Link>
</div>
</div>
{/* Beta Promo Banner */}
<div className="mb-10 bg-gradient-to-r from-purple-600/20 to-blue-600/20 border border-purple-500/30 rounded-2xl p-6 text-center">
<h2 className="text-2xl font-bold text-white mb-2">A2A </h2>
<p className="text-purple-200">
$29 intake VibeWork scoping Agent referral kit <strong>vibework.wooo.work/propose</strong> pending affiliate ledger
</p>
<div className="mt-5 flex flex-wrap justify-center gap-3">
<Link href="/propose" className="rounded-md bg-emerald-400 px-4 py-2 text-sm font-semibold text-gray-950 hover:bg-emerald-300">
<Link href="/propose" className="inline-flex items-center gap-2 rounded-md bg-emerald-400 px-3 py-2 font-semibold text-zinc-950 hover:bg-emerald-300">
<CreditCard className="h-4 w-4" />
$29
</Link>
<a
href="https://agent.wooo.work/api/a2a/growth/kit?agent_id=your-agent&register=true"
target="_blank"
rel="noopener noreferrer"
className="rounded-md border border-white/20 px-4 py-2 text-sm font-medium text-white hover:border-emerald-300"
>
Agent referral kit
</a>
</div>
</nav>
</div>
</header>
<section className="mb-10 border border-cyan-500/20 bg-cyan-500/5 p-6 rounded-lg">
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<main className="mx-auto max-w-7xl space-y-6 px-5 py-6 lg:px-8">
<section className="rounded-lg border border-zinc-800 bg-zinc-900/70 p-5">
<div className="grid gap-5 lg:grid-cols-[1.35fr_0.65fr] lg:items-center">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-cyan-300">A2A ecosystem control plane</p>
<h2 className="mt-2 text-2xl font-bold text-white"> Agent </h2>
<p className="mt-2 max-w-3xl text-sm leading-6 text-gray-300">
VibeAIAgent TG lead radaragent onboardingtask broadcastlearning loop treasury watch
OpenClawHermesNemoTronAiderOpenHandsLangGraphCrewAIn8n MCP/A2A
<p className="mb-2 inline-flex items-center gap-2 text-sm font-medium text-cyan-200">
<Bot className="h-4 w-4" />
A2A paid intake live
</p>
<h1 className="max-w-3xl text-3xl font-semibold tracking-normal text-white md:text-4xl">
AI Agent VibeWork intake
</h1>
<p className="mt-3 max-w-3xl text-sm leading-6 text-zinc-300">
scoping Agent referral kit traffic monitorproposal feeaffiliate ledger conversion
</p>
</div>
<a
href="https://agent.wooo.work/api/a2a/integrations"
target="_blank"
rel="noopener noreferrer"
className="inline-flex shrink-0 items-center justify-center rounded-md border border-cyan-300/40 px-4 py-2 text-sm font-semibold text-cyan-100 hover:border-cyan-200 hover:bg-cyan-300/10"
>
JSON
</a>
</div>
<div className="mt-6 grid gap-3 md:grid-cols-2 lg:grid-cols-4">
{controlPlaneRoles.map((role) => (
<div key={role.id} className="rounded-md border border-white/10 bg-gray-950/60 p-4">
<h3 className="text-sm font-semibold text-white">{role.name}</h3>
<p className="mt-2 text-xs leading-5 text-gray-400">{role.job}</p>
<div className="grid grid-cols-2 gap-3 text-sm">
<div className="rounded-md border border-emerald-400/20 bg-emerald-400/10 p-3">
<div className="text-zinc-400"></div>
<div className="mt-1 text-2xl font-semibold text-emerald-200">{tasks.length}</div>
</div>
))}
</div>
<div className="mt-6 grid gap-3 md:grid-cols-2 lg:grid-cols-4">
{featuredIntegrations.map((integration) => (
<div key={integration.id} className="rounded-md border border-white/10 bg-gray-950/70 p-4">
<div className="flex items-start justify-between gap-3">
<h3 className="text-sm font-semibold text-white">{integration.name}</h3>
<span className="rounded-sm bg-cyan-400/10 px-2 py-1 text-[10px] font-semibold uppercase text-cyan-200">
{integration.status}
</span>
</div>
<p className="mt-2 text-xs leading-5 text-gray-400">{integration.primaryRole}</p>
<p className="mt-3 text-[11px] uppercase tracking-wide text-emerald-300">{integration.monetizationLane}</p>
<div className="rounded-md border border-cyan-400/20 bg-cyan-400/10 p-3">
<div className="text-zinc-400"> Agent</div>
<div className="mt-1 text-2xl font-semibold text-cyan-200">{A2A_AGENT_INTEGRATIONS.length}</div>
</div>
))}
<a
href="https://agent.wooo.work/api/a2a/growth/kit?agent_id=your-agent&register=true"
target="_blank"
rel="noopener noreferrer"
className="col-span-2 inline-flex items-center justify-center gap-2 rounded-md border border-zinc-700 px-3 py-2 font-medium text-zinc-100 hover:border-emerald-300"
>
Agent referral kit
<ArrowUpRight className="h-4 w-4" />
</a>
</div>
</div>
</section>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{tasks.length === 0 ? (
<div className="col-span-full text-center py-20 text-gray-500">
<section className="grid gap-4 lg:grid-cols-[0.9fr_1.1fr]">
<div className="rounded-lg border border-zinc-800 bg-zinc-900/70 p-5">
<div className="flex items-center justify-between gap-3">
<h2 className="text-lg font-semibold text-white">A2A </h2>
<a
href="https://agent.wooo.work/api/a2a/integrations"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-sm font-medium text-cyan-200 hover:text-cyan-100"
>
JSON
<ArrowUpRight className="h-4 w-4" />
</a>
</div>
) : (
tasks.map((task) => (
<Link href={`/tasks/${task.id}`} key={task.id} className="block group">
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6 h-full transition-all duration-300 hover:border-blue-500 hover:shadow-[0_0_20px_rgba(59,130,246,0.15)] relative overflow-hidden">
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-blue-500 to-purple-500 opacity-0 group-hover:opacity-100 transition-opacity"></div>
<div className="flex justify-between items-start mb-4">
<span className={`px-3 py-1 text-xs font-semibold rounded-full ${
task.status === "OPEN" ? "bg-green-500/10 text-green-400 border border-green-500/20" :
task.status === "EXECUTING" ? "bg-yellow-500/10 text-yellow-400 border border-yellow-500/20" :
task.status === "COMPLETED" ? "bg-blue-500/10 text-blue-400 border border-blue-500/20" :
"bg-gray-800 text-gray-400 border border-gray-700"
}`}>
{task.status}
</span>
<span className="text-lg font-bold text-gray-200">
${(task.reward_amount / 100).toFixed(2)}
</span>
</div>
<h2 className="text-xl font-bold text-white mb-2 line-clamp-1">{task.title}</h2>
<p className="text-gray-400 text-sm mb-6 line-clamp-2">{task.description}</p>
<div className="flex flex-wrap gap-2 mt-auto">
{task.required_stack.map((tech) => (
<span key={tech} className="text-xs bg-gray-800 text-gray-300 px-2 py-1 rounded">
{tech}
</span>
))}
<p className="mt-2 text-sm leading-6 text-zinc-300">
VibeAIAgent TG lead radaragent onboardingtask broadcastlearning loop treasury watch
</p>
<div className="mt-4 grid gap-2">
{controlPlaneRoles.map((role) => (
<div key={role.id} className="flex items-start gap-3 rounded-md bg-zinc-950/60 px-3 py-2">
<ClipboardList className="mt-0.5 h-4 w-4 shrink-0 text-emerald-300" />
<div>
<div className="text-sm font-medium text-white">{role.name}</div>
<div className="text-xs leading-5 text-zinc-400">{role.job}</div>
</div>
</div>
</Link>
))
)}
</div>
))}
</div>
</div>
{/* AI Agent Instructions */}
<div className="mt-16 p-8 bg-gray-900 border border-blue-900/50 rounded-2xl">
<h2 className="text-2xl font-bold text-white mb-4"> AI Agent </h2>
<p className="text-gray-400 mb-4">
MCP (Model Context Protocol)AI Agent MCP API
</p>
<div className="bg-black p-4 rounded-lg font-mono text-sm text-green-400 mb-4 overflow-x-auto">
{`"mcpServers": {
<div className="rounded-lg border border-zinc-800 bg-zinc-900/70 p-5">
<h2 className="text-lg font-semibold text-white"> Agent </h2>
<div className="mt-4 grid gap-2 md:grid-cols-2">
{featuredIntegrations.map((integration) => (
<div key={integration.id} className="flex items-center justify-between gap-3 rounded-md bg-zinc-950/60 px-3 py-2">
<div className="min-w-0">
<div className="truncate text-sm font-medium text-white">{integration.name}</div>
<div className="truncate text-xs text-zinc-400">{integration.monetizationLane}</div>
</div>
<span className="shrink-0 rounded-sm bg-cyan-400/10 px-2 py-1 text-[10px] font-semibold uppercase text-cyan-200">
{integration.status}
</span>
</div>
))}
</div>
</div>
</section>
<section className="rounded-lg border border-zinc-800 bg-zinc-900/70 p-5">
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
<div>
<h2 className="text-xl font-semibold text-white"></h2>
<p className="mt-1 text-sm text-zinc-400"></p>
</div>
<Link href="/tasks/create" className="inline-flex items-center gap-2 rounded-md border border-sky-400/40 px-3 py-2 text-sm font-medium text-sky-100 hover:bg-sky-400/10">
<Plus className="h-4 w-4" />
Bounty
</Link>
</div>
{tasks.length === 0 ? (
<div className="rounded-md border border-dashed border-zinc-700 py-10 text-center text-zinc-500">
Bounty
</div>
) : (
<div className="divide-y divide-zinc-800 overflow-hidden rounded-md border border-zinc-800">
{tasks.slice(0, 12).map((task) => (
<Link href={`/tasks/${task.id}`} key={task.id} className="grid gap-3 bg-zinc-950/50 px-4 py-3 hover:bg-zinc-900 md:grid-cols-[1fr_auto] md:items-center">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<span className={`rounded-sm px-2 py-1 text-[11px] font-semibold ${
task.status === "OPEN" ? "bg-emerald-500/10 text-emerald-300" :
task.status === "EXECUTING" ? "bg-amber-500/10 text-amber-300" :
task.status === "COMPLETED" ? "bg-sky-500/10 text-sky-300" :
"bg-zinc-800 text-zinc-400"
}`}>
{task.status}
</span>
<h3 className="truncate text-sm font-semibold text-white">{task.title}</h3>
</div>
<p className="mt-1 line-clamp-1 text-sm text-zinc-400">{task.description}</p>
</div>
<div className="flex items-center justify-between gap-4 md:justify-end">
<span className="text-sm font-semibold text-emerald-200">${(task.reward_amount / 100).toFixed(2)}</span>
<span className="text-xs text-zinc-500">{task.required_stack.slice(0, 3).join(" / ")}</span>
</div>
</Link>
))}
</div>
)}
</section>
<details className="rounded-lg border border-zinc-800 bg-zinc-900/70 p-5">
<summary className="cursor-pointer text-base font-semibold text-white"> AI Agent </summary>
<div className="mt-4 grid gap-4 lg:grid-cols-2">
<div className="overflow-x-auto rounded-md bg-black p-4 font-mono text-xs leading-5 text-emerald-300">
{`"mcpServers": {
"vibework": {
"command": "npx",
"args": ["-y", "@agent-bounty/mcp-server", "--endpoint", "https://agent.wooo.work"]
}
}`}
</div>
<p className="text-gray-400 mb-4">
growth kit VibeWork
</p>
<a
href="https://agent.wooo.work/api/open-tasks"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-blue-400 hover:text-blue-300 mb-4"
>
https://agent.wooo.work/api/open-tasks
<span></span>
</a>
<div className="bg-black p-4 rounded-lg font-mono text-sm text-green-300 mb-4 overflow-x-auto">
{`curl https://agent.wooo.work/api/open-tasks
</div>
<div>
<p className="text-sm leading-6 text-zinc-400">
Agent MCP/API
</p>
<div className="mt-3 overflow-x-auto rounded-md bg-black p-4 font-mono text-xs leading-5 text-emerald-300">
{`curl https://agent.wooo.work/api/open-tasks
curl "https://agent.wooo.work/api/a2a/growth/kit?agent_id=your-agent&register=true"`}
</div>
</div>
</div>
<p className="text-gray-500 text-sm">
Agent PENDING payout referral conversion audit affiliate ledger
</p>
<div className="mt-6">
<Link href="/traffic" className="inline-flex items-center gap-2 text-emerald-400 hover:text-emerald-300">
AI
</Link>
<Link href="/admin/traffic" className="inline-flex items-center gap-2 text-amber-400 hover:text-amber-300 mt-2 block">
</Link>
</div>
</div>
</div>
</details>
</main>
</div>
);
}

View File

@@ -1,7 +1,7 @@
import { createDemandProposal } from "@/app/propose/actions";
import { buildAgentGrowthKit, getProposalPackage, PROPOSAL_PACKAGES, sanitizeAgentId } from "@/lib/a2a-growth";
import { logA2aTrafficEvent } from "@/lib/a2a-traffic";
import { ArrowRight, Bot, CreditCard, Network, Users, Wallet } from "lucide-react";
import { Activity, ArrowRight, Bot, CreditCard, Gauge, Network, Users, Wallet } from "lucide-react";
import { headers } from "next/headers";
import Link from "next/link";
@@ -105,17 +105,35 @@ export default async function ProposePage({ searchParams }: { searchParams?: Sea
return (
<main className="min-h-screen bg-zinc-950 text-zinc-100">
<div className="mx-auto grid min-h-screen max-w-7xl gap-8 px-5 py-6 lg:grid-cols-[1.5fr_0.9fr] lg:px-8">
<section className="flex flex-col justify-center">
<div className="mb-6 flex flex-wrap items-center justify-between gap-3">
<Link href="/" className="text-sm font-medium text-zinc-400 hover:text-white">
VibeWork AI
<div className="mx-auto max-w-7xl px-5 py-5 lg:px-8">
<header className="mb-5 flex flex-col gap-3 border-b border-zinc-800 pb-4 lg:flex-row lg:items-center lg:justify-between">
<Link href="/" className="inline-flex items-center gap-2 text-sm font-semibold text-white">
<Network className="h-4 w-4 text-cyan-300" />
VibeWork AI
</Link>
<nav className="flex flex-wrap gap-2 text-sm">
<Link href="/traffic" className="inline-flex items-center gap-2 rounded-md border border-emerald-400/40 px-3 py-2 font-medium text-emerald-200 hover:bg-emerald-400/10">
<Activity className="h-4 w-4" />
</Link>
<div className="flex items-center gap-2 rounded-full border border-emerald-400/30 bg-emerald-400/10 px-3 py-1 text-xs font-medium text-emerald-200">
<Network className="h-3.5 w-3.5" />
A2A paid intake live
<Link href="/admin/traffic" className="inline-flex items-center gap-2 rounded-md border border-amber-300/40 px-3 py-2 font-medium text-amber-100 hover:bg-amber-300/10">
<Gauge className="h-4 w-4" />
</Link>
</nav>
</header>
<div className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_360px] lg:items-start">
<section>
<div className="mb-5 flex flex-wrap items-center gap-2 text-xs font-medium">
<span className="inline-flex items-center gap-2 rounded-md border border-emerald-400/30 bg-emerald-400/10 px-3 py-1 text-emerald-200">
<Network className="h-3.5 w-3.5" />
A2A paid intake live
</span>
<span className="rounded-md border border-zinc-800 bg-zinc-900 px-3 py-1 text-zinc-300">
{referralAgent ? `${referralAgent} 導入` : "直接需求提案"}
</span>
</div>
</div>
<div className="mb-8">
<p className="mb-3 inline-flex items-center gap-2 text-sm font-medium text-sky-300">
@@ -255,11 +273,11 @@ export default async function ProposePage({ searchParams }: { searchParams?: Sea
<fieldset className="mt-6">
<legend className="mb-3 text-sm font-semibold text-zinc-100"></legend>
<div className="grid gap-3 md:grid-cols-3">
<div className="grid gap-2">
{PROPOSAL_PACKAGES.map((item) => (
<label
key={item.id}
className="grid cursor-pointer gap-3 rounded-md border border-zinc-700 bg-zinc-950 p-4 transition hover:border-sky-400"
className="grid cursor-pointer gap-3 rounded-md border border-zinc-700 bg-zinc-950 p-3 transition hover:border-sky-400 md:grid-cols-[auto_1fr_auto] md:items-center"
>
<input
type="radio"
@@ -268,11 +286,14 @@ export default async function ProposePage({ searchParams }: { searchParams?: Sea
defaultChecked={item.id === packageId}
className="h-4 w-4 accent-sky-400"
/>
<span className="text-base font-semibold text-white">{item.name}</span>
<span className="text-2xl font-semibold text-sky-200">{item.label}</span>
<span className="text-sm leading-6 text-zinc-400">{item.description}</span>
<span className="text-xs leading-5 text-zinc-500">{item.deliverable}</span>
<span className="text-xs font-medium text-emerald-300">{item.reviewWindow}</span>
<span>
<span className="block text-sm font-semibold text-white">{item.name}</span>
<span className="block text-xs leading-5 text-zinc-400">{item.description}</span>
</span>
<span className="text-left md:text-right">
<span className="block text-base font-semibold text-sky-200">{item.label}</span>
<span className="block text-xs font-medium text-emerald-300">{item.reviewWindow}</span>
</span>
</label>
))}
</div>
@@ -302,70 +323,57 @@ export default async function ProposePage({ searchParams }: { searchParams?: Sea
<ArrowRight className="h-4 w-4" />
</button>
</form>
</section>
</section>
<aside className="flex flex-col justify-center gap-4">
<div className="rounded-lg border border-zinc-800 bg-zinc-900/80 p-5">
<div className="mb-4 flex items-center gap-3">
<CheckCircleIcon />
<h2 className="text-lg font-semibold text-white"></h2>
</div>
<ol className="grid gap-3 text-sm leading-6 text-zinc-300">
<li>1. VibeWork private proposal draft</li>
<li>2. scope Agent </li>
<li>3. bounty</li>
</ol>
</div>
<aside className="lg:sticky lg:top-24">
<div className="rounded-lg border border-zinc-800 bg-zinc-900/80 p-5">
<div className="mb-4 flex items-center gap-3">
<Users className="h-5 w-5 text-emerald-300" />
<h2 className="text-lg font-semibold text-white"></h2>
</div>
<ol className="grid gap-2 text-sm leading-6 text-zinc-300">
<li>1. private proposal draft</li>
<li>2. scope</li>
<li>3. bounty</li>
</ol>
<div className="rounded-lg border border-zinc-800 bg-zinc-900/80 p-5">
<div className="mb-4 flex items-center gap-3">
<Users className="h-5 w-5 text-emerald-300" />
<h2 className="text-lg font-semibold text-white">Referral attribution</h2>
</div>
<dl className="grid gap-3 text-sm">
<div>
<dt className="text-zinc-500">Referral Agent</dt>
<dd className="mt-1 break-all text-zinc-100">{referralAgent || "direct"}</dd>
</div>
<div>
<dt className="text-zinc-500">Source</dt>
<dd className="mt-1 break-all text-zinc-100">{source}</dd>
</div>
<div>
<dt className="text-zinc-500">Campaign</dt>
<dd className="mt-1 break-all text-zinc-100">{campaign}</dd>
</div>
</dl>
{referralAgent ? (
<p className="mt-4 text-xs leading-5 text-emerald-200">
conversion pending affiliate ledger payout
</p>
) : null}
</div>
<dl className="mt-5 grid gap-3 border-t border-zinc-800 pt-4 text-sm">
<div>
<dt className="text-zinc-500">Referral Agent</dt>
<dd className="mt-1 break-all text-zinc-100">{referralAgent || "direct"}</dd>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<dt className="text-zinc-500">Source</dt>
<dd className="mt-1 break-all text-zinc-100">{source}</dd>
</div>
<div>
<dt className="text-zinc-500">Campaign</dt>
<dd className="mt-1 break-all text-zinc-100">{campaign}</dd>
</div>
</div>
</dl>
<div className="rounded-lg border border-zinc-800 bg-zinc-900/80 p-5">
<div className="mb-4 flex items-center gap-3">
<Bot className="h-5 w-5 text-sky-300" />
<h2 className="text-lg font-semibold text-white">External Agent kit</h2>
{referralAgent ? (
<p className="mt-4 rounded-md border border-emerald-400/20 bg-emerald-400/10 px-3 py-2 text-xs leading-5 text-emerald-100">
conversion pending affiliate ledger payout
</p>
) : null}
<details className="mt-4 border-t border-zinc-800 pt-4">
<summary className="cursor-pointer text-sm font-semibold text-sky-200">External Agent kit</summary>
<p className="mt-3 text-xs leading-5 text-zinc-400">
Agent referral URL paid conversion
</p>
<code className="mt-3 block break-all rounded-md bg-black px-3 py-3 text-xs leading-5 text-emerald-300">
{growthKit?.referral_url ||
"https://agent.wooo.work/api/a2a/growth/kit?agent_id=your-agent&register=true"}
</code>
</details>
</div>
<p className="text-sm leading-6 text-zinc-400">
Agent referral URL paid conversion
</p>
<code className="mt-4 block break-all rounded-md bg-black px-3 py-3 text-xs leading-5 text-emerald-300">
{growthKit?.referral_url ||
"https://agent.wooo.work/api/a2a/growth/kit?agent_id=your-agent&register=true"}
</code>
</div>
</aside>
</aside>
</div>
</div>
</main>
);
}
function CheckCircleIcon() {
return (
<span className="flex h-5 w-5 items-center justify-center rounded-full border border-sky-300 text-xs font-semibold text-sky-200">
1
</span>
);
}

View File

@@ -1,7 +1,13 @@
import Link from "next/link";
import { headers } from "next/headers";
import { prisma } from "@/lib/prisma";
import { isIP } from "node:net";
import {
actorClassLabel,
isInternalActor,
isSyntheticTrafficEvent,
resolveActorClass,
type TrafficActorClass,
} from "@/lib/traffic-actor-classification";
export const dynamic = "force-dynamic";
@@ -60,36 +66,6 @@ function fmtPercent(value: number) {
return `${value.toFixed(1)}%`;
}
function isInternalActorId(value: string | null | undefined) {
if (!value) return true;
const actorId = value.toLowerCase();
if (actorId === "unknown" || actorId === "mcp-anonymous" || actorId === "open-tasks:localhost") return true;
if (actorId === "localhost") return true;
const ipMatch = actorId.match(/^open-tasks:([a-z0-9.:_-]+)$/);
if (!ipMatch?.[1]) return false;
const actorIp = ipMatch[1];
if (actorIp.startsWith("127.") || actorIp.startsWith("10.") || actorIp.startsWith("192.168.")) return true;
if (actorIp.startsWith("172.")) {
const secondOctet = Number(actorIp.split(".")[1]);
return secondOctet >= 16 && secondOctet <= 31;
}
if (actorIp === "localhost" || actorIp === "unknown" || actorIp.startsWith("fc") || actorIp.startsWith("fd")) return true;
if (actorIp === "::1" || actorIp.startsWith("fe80")) return true;
if (isIP(actorIp) === 6 && actorIp.startsWith("fc")) return true;
if (isIP(actorIp) === 6 && actorIp.startsWith("fd")) return true;
if (isIP(actorIp) === 4 || isIP(actorIp) === 6) return false;
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 process.env.NODE_ENV !== "production";
return tokenHeader === token;
@@ -125,91 +101,6 @@ function displayResponseSummary(value: string | null | undefined) {
return value.replace(/_/g, " ");
}
type TrafficActorClass = "a2a" | "external_ai_agent" | "likely_ai_agent" | "other_external";
const AI_USER_AGENT_HINTS = [
"gpt",
"chatgpt",
"openai",
"anthropic",
"claude",
"perplexity",
"llm",
"mcp",
"autogpt",
"agent",
"assistant",
"gemini",
"cursor",
"copilot",
];
function isLikelyAIAgentActor(
actorType: string | null | undefined,
actorId: string | null | undefined,
metadata: Record<string, unknown> | undefined
) {
if (actorType === "AGENT") {
return true;
}
const normalizedActor = (actorId || "").toLowerCase();
if (normalizedActor.startsWith("agent:")) {
return true;
}
const userAgent = String(metadata?.user_agent || "").toLowerCase();
if (AI_USER_AGENT_HINTS.some((token) => userAgent.includes(token))) {
return true;
}
const requestHeaders = asRecordJson(metadata?.request_actor_headers);
if (!requestHeaders) {
return false;
}
const headerText = Object.values(requestHeaders)
.filter((item): item is string => typeof item === "string")
.join(" ")
.toLowerCase();
return AI_USER_AGENT_HINTS.some((token) => headerText.includes(token));
}
function resolveActorClass(
action: string,
actorType: string | null | undefined,
actorId: string | null | undefined,
metadata: Record<string, unknown> | undefined,
surface: string | undefined
) {
const normalizedSurface = (surface || "").toLowerCase();
if (normalizedSurface.startsWith("mcp/")) {
return "a2a";
}
const normalizedActorId = (actorId || "").toLowerCase();
if (actorType === "AGENT" || normalizedActorId.startsWith("agent:")) {
if (action.startsWith("EXTERNAL_") && normalizedSurface.startsWith("mcp/")) {
return "a2a";
}
return "external_ai_agent";
}
if (isLikelyAIAgentActor(actorType, actorId, metadata)) {
return "likely_ai_agent";
}
return action.startsWith("EXTERNAL_") ? "other_external" : "other_external";
}
function actorClassLabel(actorClass: TrafficActorClass) {
if (actorClass === "a2a") return "A2A (MCP)";
if (actorClass === "external_ai_agent") return "外部 AI Agent";
if (actorClass === "likely_ai_agent") return "疑似 AI 流量";
return "其他外部流量";
}
type ExternalActorActivity = {
actorId: string;
events: number;
@@ -234,6 +125,7 @@ async function getTrafficSummary(minutes: number) {
summaryRows,
actorSummaryRows,
externalActorRows,
externalFunnelRows,
totalRows,
latestEvents,
judgeCompleteRows,
@@ -267,6 +159,24 @@ async function getTrafficSummary(minutes: number) {
},
_count: { _all: true },
}),
prisma.auditEvent.findMany({
where: {
createdAt: { gte: since },
action: {
startsWith: "EXTERNAL_",
},
},
orderBy: {
createdAt: "desc",
},
take: 5000,
select: {
action: true,
actorType: true,
actorId: true,
metadata: true,
},
}),
prisma.auditEvent.count({
where: { createdAt: { gte: since } },
}),
@@ -357,16 +267,28 @@ async function getTrafficSummary(minutes: number) {
const actionSummary = Object.fromEntries(summaryRows.map((row) => [row.action, row._count._all]));
const actorSummary = Object.fromEntries(actorSummaryRows.map((row) => [row.actorType, row._count._all]));
const realExternalFunnelRows = externalFunnelRows.filter((row) => {
const metadata = asRecordJson(row.metadata);
return !isInternalActor({ actorType: row.actorType, actorId: row.actorId, metadata });
});
const syntheticExternalEvents = externalFunnelRows.filter((row) => {
const metadata = asRecordJson(row.metadata);
return isSyntheticTrafficEvent({ actorType: row.actorType, actorId: row.actorId, metadata });
}).length;
const realExternalActionSummary = realExternalFunnelRows.reduce<Record<string, number>>((acc, row) => {
acc[row.action] = (acc[row.action] || 0) + 1;
return acc;
}, {});
const externalActorSummary = externalActorRows
.map((row) => ({
actorId: row.actorId || "unknown",
events: row._count._all,
}))
.filter((row) => !isInternalActorId(row.actorId))
.filter((row) => !isInternalActor({ actorType: "AGENT", actorId: row.actorId }))
.sort((a, b) => b.events - a.events)
.slice(0, 20);
const channelSummary = Object.entries(actionSummary).reduce(
const rawChannelSummary = Object.entries(actionSummary).reduce(
(acc, [action, count]) => {
if (action.startsWith("EXTERNAL_")) {
acc.external += count;
@@ -377,9 +299,14 @@ async function getTrafficSummary(minutes: number) {
},
{ external: 0, internal: 0 } as Record<string, number>
);
const channelSummary = {
external: realExternalFunnelRows.length,
internal: Math.max(totalRows - realExternalFunnelRows.length, 0),
rawExternal: rawChannelSummary.external,
syntheticExternal: syntheticExternalEvents,
};
const externalEventTypes = Object.entries(actionSummary)
.filter(([action]) => action.startsWith("EXTERNAL_"))
const externalEventTypes = Object.entries(realExternalActionSummary)
.map(([action, count]) => ({ action, count }));
const internalEventTypes = Object.entries(actionSummary)
@@ -409,20 +336,20 @@ async function getTrafficSummary(minutes: number) {
});
const discoveryEvents =
(actionSummary["EXTERNAL_LIST_OPEN_TASKS"] || 0) +
(actionSummary["EXTERNAL_LIST_OPEN_TASKS_MCP"] || 0) +
(actionSummary["EXTERNAL_A2A_ONBOARDING_VIEW"] || 0) +
(actionSummary["EXTERNAL_A2A_DEMAND_CAMPAIGN_KIT_ISSUED"] || 0) +
(actionSummary["EXTERNAL_A2A_INTEGRATION_CATALOG_VIEW"] || 0) +
(actionSummary["EXTERNAL_A2A_GROWTH_KIT_ISSUED"] || 0);
const referralTouchpointEvents = actionSummary["EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED"] || 0;
const proposalViewEvents = actionSummary["EXTERNAL_DEMAND_PROPOSAL_VIEW"] || 0;
const proposalCreatedEvents = actionSummary["EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED"] || 0;
const proposalCheckoutEvents = actionSummary["EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED"] || 0;
const proposalWalletPendingEvents = actionSummary["EXTERNAL_DEMAND_PROPOSAL_WALLET_PAYMENT_PENDING"] || 0;
const proposalPaidEvents = actionSummary["EXTERNAL_DEMAND_PROPOSAL_FEE_CAPTURED"] || 0;
const claimEvents = actionSummary["EXTERNAL_CLAIM_TASK_SUCCESS"] || 0;
const submitEvents = actionSummary["EXTERNAL_SUBMIT_SOLUTION_SUCCESS"] || 0;
(realExternalActionSummary["EXTERNAL_LIST_OPEN_TASKS"] || 0) +
(realExternalActionSummary["EXTERNAL_LIST_OPEN_TASKS_MCP"] || 0) +
(realExternalActionSummary["EXTERNAL_A2A_ONBOARDING_VIEW"] || 0) +
(realExternalActionSummary["EXTERNAL_A2A_DEMAND_CAMPAIGN_KIT_ISSUED"] || 0) +
(realExternalActionSummary["EXTERNAL_A2A_INTEGRATION_CATALOG_VIEW"] || 0) +
(realExternalActionSummary["EXTERNAL_A2A_GROWTH_KIT_ISSUED"] || 0);
const referralTouchpointEvents = realExternalActionSummary["EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED"] || 0;
const proposalViewEvents = realExternalActionSummary["EXTERNAL_DEMAND_PROPOSAL_VIEW"] || 0;
const proposalCreatedEvents = realExternalActionSummary["EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED"] || 0;
const proposalCheckoutEvents = realExternalActionSummary["EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED"] || 0;
const proposalWalletPendingEvents = realExternalActionSummary["EXTERNAL_DEMAND_PROPOSAL_WALLET_PAYMENT_PENDING"] || 0;
const proposalPaidEvents = realExternalActionSummary["EXTERNAL_DEMAND_PROPOSAL_FEE_CAPTURED"] || 0;
const claimEvents = realExternalActionSummary["EXTERNAL_CLAIM_TASK_SUCCESS"] || 0;
const submitEvents = realExternalActionSummary["EXTERNAL_SUBMIT_SOLUTION_SUCCESS"] || 0;
const judgePassEvents = judgeCompleteRows.filter((row) => {
const metadata = asRecordJson(row.metadata);
return normalizedJudgeResult(metadata?.overall_result) === "pass";
@@ -465,7 +392,13 @@ async function getTrafficSummary(minutes: number) {
.map((event) => event.action);
const externalActorClassMap = new Map<TrafficActorClass, number>();
for (const event of recentEvents.filter((event) => event.action.startsWith("EXTERNAL_"))) {
for (const event of recentEvents.filter((event) => {
const metadata = asRecordJson(event.metadata);
return (
event.action.startsWith("EXTERNAL_") &&
!isInternalActor({ actorType: event.actorType, actorId: event.actorId, metadata })
);
})) {
const actorClass = (event as { actor_class?: TrafficActorClass }).actor_class || "other_external";
externalActorClassMap.set(actorClass, (externalActorClassMap.get(actorClass) || 0) + 1);
}
@@ -484,6 +417,7 @@ async function getTrafficSummary(minutes: number) {
const isInternal = isInternalActor({
actorType: event.actorType,
actorId: event.actorId,
metadata: event.metadata,
});
const isTrackedExternal =
!isInternal &&
@@ -594,6 +528,7 @@ async function getTrafficSummary(minutes: number) {
!isInternalActor({
actorType: event.actorType,
actorId: event.actorId,
metadata: event.metadata,
})
),
recentInternalEvents: recentEvents.filter((event) => !event.action.startsWith("EXTERNAL_")),
@@ -601,6 +536,12 @@ async function getTrafficSummary(minutes: number) {
conversionRates,
externalActorClassSummary,
externalActorActivities,
trafficQuality: {
realExternalEvents: realExternalFunnelRows.length,
rawExternalEvents: rawChannelSummary.external,
syntheticExternalEvents,
excludedExternalEvents: Math.max(rawChannelSummary.external - realExternalFunnelRows.length, 0),
},
externalErrors,
demandSupply,
};
@@ -737,47 +678,31 @@ export default async function TrafficDashboard({
{summary.periodMinutes}
</div>
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-4">
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-6">
<div className="rounded-lg border border-gray-800 bg-gray-900 p-3">
<div className="text-gray-400 text-sm"></div>
<div className="text-3xl font-bold mt-2">{summary.totalEvents}</div>
<div className="mt-1 text-2xl font-bold">{summary.totalEvents}</div>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-4">
<div className="text-gray-400 text-sm"></div>
<div className="text-3xl font-bold mt-2 text-emerald-300">{summary.channelSummary.external}</div>
<div className="rounded-lg border border-emerald-400/20 bg-emerald-400/10 p-3">
<div className="text-gray-400 text-sm"></div>
<div className="mt-1 text-2xl font-bold text-emerald-300">{summary.trafficQuality.realExternalEvents}</div>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-4">
<div className="rounded-lg border border-amber-400/20 bg-amber-400/10 p-3">
<div className="text-gray-400 text-sm"></div>
<div className="mt-1 text-2xl font-bold text-amber-300">{summary.trafficQuality.excludedExternalEvents}</div>
</div>
<div className="rounded-lg border border-gray-800 bg-gray-900 p-3">
<div className="text-gray-400 text-sm">A2A </div>
<div className="text-3xl font-bold mt-2 text-cyan-300">{conversionSummary.discovery_events}</div>
<div className="mt-1 text-2xl font-bold text-cyan-300">{conversionSummary.discovery_events}</div>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-4">
<div className="text-gray-400 text-sm"></div>
<div className="text-3xl font-bold mt-2 text-teal-300">{conversionSummary.referral_touchpoint_events}</div>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-4">
<div className="text-gray-400 text-sm"></div>
<div className="text-3xl font-bold mt-2 text-sky-300">{conversionSummary.proposal_view_events}</div>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-4">
<div className="text-gray-400 text-sm"></div>
<div className="text-3xl font-bold mt-2 text-violet-300">{conversionSummary.proposal_created_events}</div>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-4">
<div className="rounded-lg border border-gray-800 bg-gray-900 p-3">
<div className="text-gray-400 text-sm"></div>
<div className="text-3xl font-bold mt-2 text-emerald-300">{conversionSummary.proposal_paid_events}</div>
<div className="mt-1 text-2xl font-bold text-emerald-300">{conversionSummary.proposal_paid_events}</div>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-4">
<div className="text-gray-400 text-sm"></div>
<div className="text-3xl font-bold mt-2 text-blue-300">{conversionSummary.claim_events}</div>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-4">
<div className="text-gray-400 text-sm"></div>
<div className="text-3xl font-bold mt-2 text-amber-300">{conversionSummary.submit_events}</div>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-4">
<div className="rounded-lg border border-gray-800 bg-gray-900 p-3">
<div className="text-gray-400 text-sm"></div>
<div className={`text-3xl font-bold mt-2 ${demandHealthTone}`}>{demandSupply.openTaskCount}</div>
<div className="text-xs text-gray-400 mt-2">{demandHealthLabel}</div>
<div className={`mt-1 text-2xl font-bold ${demandHealthTone}`}>{demandSupply.openTaskCount}</div>
<div className="mt-1 text-xs text-gray-400">{demandHealthLabel}</div>
</div>
</div>

View File

@@ -0,0 +1,189 @@
import { isIP } from "node:net";
export type TrafficActorClass = "a2a" | "external_ai_agent" | "likely_ai_agent" | "other_external";
const AI_USER_AGENT_HINTS = [
"gpt",
"chatgpt",
"openai",
"anthropic",
"claude",
"perplexity",
"llm",
"mcp",
"autogpt",
"agent",
"assistant",
"gemini",
"cursor",
"copilot",
];
function normalizeActorId(value: string | null | undefined) {
return (value || "").trim().toLowerCase();
}
function asRecordJson(value: unknown): Record<string, unknown> | undefined {
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
return value as Record<string, unknown>;
}
return undefined;
}
export function isSyntheticActorId(value: string | null | undefined) {
const actorId = normalizeActorId(value);
if (!actorId) return false;
const agentName = actorId.startsWith("agent:") ? actorId.slice("agent:".length) : actorId;
if (agentName === "test-agent" || agentName === "ci-smoke" || agentName === "traffic-monitor") {
return true;
}
if (agentName.includes("smoke") || agentName.includes("synthetic")) return true;
if (agentName.startsWith("test-") || agentName.startsWith("ci-")) return true;
if (agentName.startsWith("final-") || agentName.startsWith("deploy-")) return true;
if (agentName.startsWith("local-") || agentName.startsWith("monitor-")) return true;
return false;
}
export function isSyntheticTrafficEvent(input: {
actorType: string | null | undefined;
actorId: string | null | undefined;
metadata?: Record<string, unknown> | unknown;
}) {
const metadata = asRecordJson(input.metadata);
if (isSyntheticActorId(input.actorId)) return true;
const actorType = (input.actorType || "").toUpperCase();
if (actorType === "SYSTEM") return true;
const markers = [
metadata?.synthetic,
metadata?.is_synthetic,
metadata?.traffic_kind,
metadata?.traffic_source,
metadata?.source,
];
return markers.some((marker) => {
if (marker === true) return true;
if (typeof marker !== "string") return false;
const normalized = marker.trim().toLowerCase();
return normalized === "synthetic" || normalized === "smoke" || normalized === "ci";
});
}
export function isInternalActorId(value: string | null | undefined) {
if (!value) return true;
if (isSyntheticActorId(value)) return true;
const actorId = normalizeActorId(value);
if (
actorId === "unknown" ||
actorId === "mcp-anonymous" ||
actorId === "open-tasks:localhost" ||
actorId === "localhost"
) {
return true;
}
const ipMatch = actorId.match(/^open-tasks:([a-z0-9.:_-]+)$/);
if (!ipMatch?.[1]) return false;
const actorIp = ipMatch[1];
if (actorIp.startsWith("127.") || actorIp.startsWith("10.") || actorIp.startsWith("192.168.")) {
return true;
}
if (actorIp.startsWith("172.")) {
const secondOctet = Number(actorIp.split(".")[1]);
return secondOctet >= 16 && secondOctet <= 31;
}
if (actorIp === "localhost" || actorIp === "unknown" || actorIp.startsWith("fc") || actorIp.startsWith("fd")) {
return true;
}
if (actorIp === "::1" || actorIp.startsWith("fe80")) return true;
if (isIP(actorIp) === 6 && actorIp.startsWith("fc")) return true;
if (isIP(actorIp) === 6 && actorIp.startsWith("fd")) return true;
return false;
}
export function isInternalActor(input: {
actorType: string | null | undefined;
actorId: string | null | undefined;
metadata?: Record<string, unknown> | unknown;
}) {
if (isSyntheticTrafficEvent(input)) return true;
const actorType = (input.actorType || "").toUpperCase();
if (actorType === "SYSTEM") return true;
if (actorType === "AGENT") return false;
return isInternalActorId(input.actorId);
}
export function isLikelyAIAgentActor(
actorType: string | null | undefined,
actorId: string | null | undefined,
metadata: Record<string, unknown> | undefined
) {
if (actorType === "AGENT") {
return true;
}
const normalizedActor = normalizeActorId(actorId);
if (normalizedActor.startsWith("agent:")) {
return true;
}
const userAgent = String(metadata?.user_agent || "").toLowerCase();
if (AI_USER_AGENT_HINTS.some((token) => userAgent.includes(token))) {
return true;
}
const requestHeaders = asRecordJson(metadata?.request_actor_headers);
if (!requestHeaders) {
return false;
}
const headerText = Object.values(requestHeaders)
.filter((item): item is string => typeof item === "string")
.join(" ")
.toLowerCase();
return AI_USER_AGENT_HINTS.some((token) => headerText.includes(token));
}
export function resolveActorClass(
action: string,
actorType: string | null | undefined,
actorId: string | null | undefined,
metadata: Record<string, unknown> | undefined,
surface: string | undefined
): TrafficActorClass {
const normalizedSurface = (surface || "").toLowerCase();
if (normalizedSurface.startsWith("mcp/")) {
return "a2a";
}
const normalizedActorId = normalizeActorId(actorId);
if (actorType === "AGENT" || normalizedActorId.startsWith("agent:")) {
if (action.startsWith("EXTERNAL_") && normalizedSurface.startsWith("mcp/")) {
return "a2a";
}
return "external_ai_agent";
}
if (isLikelyAIAgentActor(actorType, actorId, metadata)) {
return "likely_ai_agent";
}
return "other_external";
}
export function actorClassLabel(actorClass: TrafficActorClass) {
if (actorClass === "a2a") return "A2A (MCP)";
if (actorClass === "external_ai_agent") return "外部 AI Agent";
if (actorClass === "likely_ai_agent") return "疑似 AI 流量";
return "其他外部流量";
}

View File

@@ -1,6 +1,7 @@
import { prisma } from "./prisma";
import { redis } from "./redis";
import { sendTrafficAlert } from "./traffic-alert";
import { isInternalActorId } from "./traffic-actor-classification";
type FunnelSummary = {
discoveryEvents: number;
@@ -50,31 +51,6 @@ function normalizedJudgeResult(value: unknown) {
return value.trim().toLowerCase();
}
function isInternalActorId(actorId: string | null | undefined) {
if (!actorId) return true;
const actorIdValue = actorId.toLowerCase();
if (actorIdValue === "unknown" || actorIdValue === "mcp-anonymous") return true;
const ipMatch = actorIdValue.match(/^open-tasks:([a-z0-9.:_-]+)$/);
if (!ipMatch?.[1]) return false;
const actorIp = ipMatch[1];
if (
actorIp.startsWith("127.") ||
actorIp.startsWith("10.") ||
actorIp.startsWith("192.168.")
) {
return true;
}
if (actorIp.startsWith("172.")) {
const secondOctet = Number(actorIp.split(".")[1]);
return secondOctet >= 16 && secondOctet <= 31;
}
return false;
}
function isMissingLedgerTableError(error: unknown) {
return (
typeof error === "object" &&
@@ -170,7 +146,12 @@ async function fetchFunnelSummary(minutes: number): Promise<FunnelSummary> {
}
}
const actionSummary = Object.fromEntries(
const realActorRows = actorRows.filter((row) => !isInternalActorId(row.actorId));
const actionSummary = realActorRows.reduce<Record<string, number>>((acc, row) => {
acc[row.action] = (acc[row.action] || 0) + row._count._all;
return acc;
}, {});
const rawActionSummary = Object.fromEntries(
summaryRows.map((row) => [row.action, row._count._all])
);
@@ -179,8 +160,8 @@ async function fetchFunnelSummary(minutes: number): Promise<FunnelSummary> {
(actionSummary["EXTERNAL_LIST_OPEN_TASKS_MCP"] || 0);
const claimEvents = actionSummary["EXTERNAL_CLAIM_TASK_SUCCESS"] || 0;
const submitEvents = actionSummary["EXTERNAL_SUBMIT_SOLUTION_SUCCESS"] || 0;
const mcpAuthMissingEvents = actionSummary["EXTERNAL_MCP_AUTH_MISSING"] || 0;
const mcpAuthForbiddenEvents = actionSummary["EXTERNAL_MCP_AUTH_FORBIDDEN"] || 0;
const mcpAuthMissingEvents = rawActionSummary["EXTERNAL_MCP_AUTH_MISSING"] || 0;
const mcpAuthForbiddenEvents = rawActionSummary["EXTERNAL_MCP_AUTH_FORBIDDEN"] || 0;
const judgePassRows = judgeRows.filter((row) => {
const metadata = asRecordJson(row.metadata);
@@ -239,12 +220,8 @@ async function fetchFunnelSummary(minutes: number): Promise<FunnelSummary> {
string,
{ actorId: string; opens: number; claims: number; submits: number }
>();
actorRows.forEach((row) => {
realActorRows.forEach((row) => {
const actorId = row.actorId || "agent:unknown";
if (isInternalActorId(actorId)) {
return;
}
const flow = actorMap.get(actorId) || {
actorId,
opens: 0,