fix: route traffic monitor through admin auth
All checks were successful
CI and Production Smoke / smoke (push) Successful in 7s

This commit is contained in:
OG T
2026-06-11 19:31:15 +08:00
parent 9d471b3c18
commit 2fd0570295
2 changed files with 71 additions and 83 deletions

View File

@@ -8,21 +8,21 @@ export const dynamic = "force-dynamic";
const MONITOR_TOKEN = process.env.TRAFFIC_MONITOR_TOKEN;
const EVENT_LABELS: Record<string, string> = {
EXTERNAL_LIST_OPEN_TASKS: "外部公開流量頁讀取 open tasks",
EXTERNAL_A2A_ONBOARDING_VIEW: "外部 Agent 讀取 onboarding contract",
PUBLIC_A2A_ONBOARDING_VIEW: "公開 A2A onboarding 被讀取",
EXTERNAL_A2A_DEMAND_CAMPAIGN_KIT_ISSUED: "外部 Agent 領取需求 campaign kit",
EXTERNAL_A2A_INTEGRATION_CATALOG_VIEW: "外部 Agent 讀取 A2A 整合目錄",
EXTERNAL_A2A_GROWTH_KIT_ISSUED: "外部 Agent 領取 growth kit",
EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED: "外部 Agent 回報導流 touchpoint",
EXTERNAL_A2A_REFERRAL_STATUS_VIEW: "外部 Agent 查詢 referral 狀態",
EXTERNAL_LIST_OPEN_TASKS: "外部查看可接需求",
EXTERNAL_A2A_ONBOARDING_VIEW: "外部讀取合作說明",
PUBLIC_A2A_ONBOARDING_VIEW: "公開合作說明被讀取",
EXTERNAL_A2A_DEMAND_CAMPAIGN_KIT_ISSUED: "外部領取需求導流素材",
EXTERNAL_A2A_INTEGRATION_CATALOG_VIEW: "外部查看整合目錄",
EXTERNAL_A2A_GROWTH_KIT_ISSUED: "外部領取成長導流素材",
EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED: "外部引薦紀錄",
EXTERNAL_A2A_REFERRAL_STATUS_VIEW: "外部查詢引薦狀態",
EXTERNAL_DEMAND_PROPOSAL_VIEW: "外部導流需求方查看提案頁",
EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED: "外部導流需求方建立提案",
EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED: "外部導流需求方開始 Stripe 結帳",
EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED: "外部導流需求方開始線上結帳",
EXTERNAL_DEMAND_PROPOSAL_WALLET_PAYMENT_PENDING: "外部導流需求方取得錢包付款指示",
EXTERNAL_DEMAND_PROPOSAL_FEE_CAPTURED: "外部導流提案費付款成功",
EXTERNAL_LIST_OPEN_TASKS_SURGE: "外部公開流量突增告警",
EXTERNAL_LIST_OPEN_TASKS_MCP: "外部 MCP 入口讀取 open tasks",
EXTERNAL_LIST_OPEN_TASKS_MCP: "外部整合入口查看可接需求",
EXTERNAL_LIST_OPEN_TASKS_MCP_SURGE: "外部 MCP 流量突增告警",
EXTERNAL_CLAIM_TASK_SUCCESS: "外部 AI 成功接單",
EXTERNAL_SUBMIT_SOLUTION_SUCCESS: "外部 AI 提交解法",
@@ -32,7 +32,7 @@ const EVENT_LABELS: Record<string, string> = {
EXTERNAL_LIST_OPEN_TASKS_MCP_ERROR: "外部 MCP 流量端點錯誤",
EXTERNAL_FUNNEL_CLAIM_STALL: "外部曝光後未接案",
EXTERNAL_FUNNEL_SUBMIT_STALL: "外部接案後未提交",
EXTERNAL_FUNNEL_PASS_STALL: "外部提交後未 PASS",
EXTERNAL_FUNNEL_PASS_STALL: "外部提交後未通過",
EXTERNAL_FUNNEL_PAYOUT_STALL: "PASS 後未出金",
JUDGE_COMPLETE: "AI 交件判定完成",
};
@@ -95,11 +95,6 @@ function isAuthorizedToken(token: string | undefined, tokenHeader: string | unde
return tokenHeader === token;
}
function eventDirection(action: string) {
if (action.startsWith("EXTERNAL_")) return "外部";
return "內部";
}
function explainAction(action: string) {
return EVENT_LABELS[action] || action;
}
@@ -617,15 +612,15 @@ function buildConversionTips(summary: {
const steps: string[] = [];
if (conversionSummary.discovery_events > 0 && conversionSummary.referral_touchpoint_events === 0) {
steps.push("A2A 曝光已有資料但外部 Agent touchpoint 為零:優先確認 campaign kit 是否要求回報 /api/a2a/referrals/touch。");
steps.push("已有曝光但沒有引薦紀錄:請確認對外素材是否已更新,並檢查引薦連結是否正常帶入。");
}
if (conversionSummary.referral_touchpoint_events > 0 && conversionSummary.proposal_view_events === 0) {
steps.push("外部 Agent 已回報導流 touchpoint 但提案頁查看為零:優先檢查送出的 proposal_url、prefilled_proposal_url 與 vibework.wooo.work 代理。");
steps.push("已有引薦紀錄但未進入提案頁:請檢查對外連結是否指向正式提案入口。");
}
if (conversionSummary.discovery_events > 0 && conversionSummary.proposal_view_events === 0) {
steps.push("A2A 曝光已有資料但提案頁查看為零:優先確認 growth kit referral_url、touchpoint 回傳 URL、外部貼文 CTA 與 /propose 代理。");
steps.push("已有曝光但沒有提案頁查看:請檢查對外 CTA、提案入口與正式網域代理狀態。");
}
if (conversionSummary.proposal_view_events > 0 && conversionSummary.proposal_created_events === 0) {
@@ -633,11 +628,11 @@ function buildConversionTips(summary: {
}
if (conversionSummary.proposal_created_events > 0 && conversionSummary.proposal_paid_events === 0) {
steps.push("已有提案但尚未付款成功:分流檢查 Stripe checkout 與 USDC wallet receipt verification。");
steps.push("已有提案但尚未付款成功:請分別檢查線上結帳與錢包收款確認流程。");
}
if (conversionSummary.discovery_events > 0 && conversionSummary.claim_events === 0) {
steps.push("曝光高但接單為零:請先檢查 open-tasks 回傳任務文案是否足夠明確,是否包含 npx 指令與標準格式。");
steps.push("曝光高但接單為零:請先檢查任務標題、酬金、驗收條件與交付格式是否足夠明確。");
}
if (conversionSummary.claim_events > 0 && conversionSummary.submit_events === 0) {
@@ -645,7 +640,7 @@ function buildConversionTips(summary: {
}
if (conversionSummary.submit_events > 0 && conversionSummary.judge_pass_events === 0) {
steps.push("有提交但無 PASS先人工檢查提交格式、sandbox 測試、deliverable 欄位是否可被執行。");
steps.push("有提交但未通過:先檢查交付格式、測試結果與驗收條件是否可被系統判讀。");
}
if (conversionSummary.judge_pass_events > 0 && conversionSummary.payout_captured === 0) {
@@ -657,7 +652,7 @@ function buildConversionTips(summary: {
}
if (summary.pass_rate < 35 && conversionSummary.submit_events > 20) {
steps.push("PASS 率偏低:提高指令兼容度,改用可判讀的輸出欄位與固定檔名。");
steps.push("通過率偏低:提高指令兼容度,改用可判讀的輸出欄位與固定檔名。");
}
if (steps.length === 0) {
@@ -683,8 +678,13 @@ export default async function TrafficDashboard({
<div className="min-h-screen bg-gray-950 text-gray-100 p-8 font-sans">
<div className="max-w-5xl mx-auto">
<h1 className="text-3xl font-bold mb-4">VibeWork </h1>
<p className="text-gray-300 mb-4"> Token </p>
<p className="text-sm text-gray-500"><code>/traffic?token=YOUR_TOKEN&minutes=60</code></p>
<p className="text-gray-300 mb-6"></p>
<Link
href="/admin/traffic"
className="inline-flex rounded-lg bg-emerald-500 px-4 py-2 text-sm font-semibold text-gray-950 hover:bg-emerald-400"
>
</Link>
</div>
</div>
);
@@ -701,14 +701,14 @@ export default async function TrafficDashboard({
<div className="min-h-screen bg-gray-950 text-gray-100 p-8 font-sans">
<div className="max-w-6xl mx-auto space-y-8">
<div className="flex justify-between items-center">
<h1 className="text-3xl font-bold">VibeWork </h1>
<h1 className="text-3xl font-bold">VibeWork </h1>
<Link href="/" className="text-blue-400 hover:text-blue-300">
</Link>
</div>
<div className="text-sm text-gray-400">
{summary.periodMinutes} `EXTERNAL_*`
{summary.periodMinutes}
</div>
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
@@ -725,7 +725,7 @@ export default async function TrafficDashboard({
<div className="text-3xl font-bold mt-2 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">Agent </div>
<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">
@@ -764,17 +764,17 @@ export default async function TrafficDashboard({
<div className="flex justify-between"><span></span><span className="text-emerald-300">{demandSupply.avgScopeClarity === null ? "n/a" : `${Number(demandSupply.avgScopeClarity).toFixed(2)}`}</span></div>
</div>
<div className="mt-4 text-xs text-gray-400">
Task.status = OPEN title GitHub Issue: 開頭
</div>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6">
<h2 className="text-xl font-semibold mb-4"></h2>
<h2 className="text-xl font-semibold mb-4"></h2>
<ul className="text-sm text-gray-200 space-y-2 list-disc list-inside">
<li>1) <span className="text-emerald-300">{demandSupply.openTaskCount}</span> step</li>
<li>2) <span className="font-mono text-xs">/api/open-tasks</span> 3 30~60 </li>
<li>3) GitHub Issue:</li>
<li>4) rewardrequired_stack developer_wallet </li>
<li> <span className="text-emerald-300">{demandSupply.openTaskCount}</span> </li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
</div>
@@ -784,7 +784,7 @@ export default async function TrafficDashboard({
<h2 className="text-xl font-semibold mb-4"></h2>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span>A2A曝光Agent導流回報</span>
<span>A2A曝光</span>
<span className="text-emerald-300">{fmtPercent(conversionRates.touchpoint_rate)}</span>
</div>
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
@@ -794,7 +794,7 @@ export default async function TrafficDashboard({
/>
</div>
<div className="flex justify-between">
<span>Agent導流回報</span>
<span></span>
<span className="text-emerald-300">{fmtPercent(conversionRates.touchpoint_to_proposal_view_rate)}</span>
</div>
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
@@ -854,7 +854,7 @@ export default async function TrafficDashboard({
/>
</div>
<div className="flex justify-between">
<span>PASS</span>
<span></span>
<span className="text-emerald-300">{fmtPercent(conversionRates.pass_rate)}</span>
</div>
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
@@ -864,7 +864,7 @@ export default async function TrafficDashboard({
/>
</div>
<div className="flex justify-between">
<span>PASS</span>
<span></span>
<span className="text-emerald-300">{fmtPercent(conversionRates.payout_rate)}</span>
</div>
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
@@ -875,31 +875,31 @@ export default async function TrafficDashboard({
</div>
</div>
<div className="mt-4 text-sm text-gray-300 space-y-1">
<div className="flex justify-between"><span>Stripe checkout </span><span>{conversionSummary.proposal_checkout_events}</span></div>
<div className="flex justify-between"><span></span><span>{conversionSummary.proposal_checkout_events}</span></div>
<div className="flex justify-between"><span></span><span>{conversionSummary.proposal_wallet_pending_events}</span></div>
<div className="flex justify-between"><span>PASS </span><span>{conversionSummary.judge_pass_events}</span></div>
<div className="flex justify-between"><span>FAIL </span><span>{conversionSummary.judge_fail_events}</span></div>
<div className="flex justify-between"><span></span><span>{conversionSummary.judge_pass_events}</span></div>
<div className="flex justify-between"><span></span><span>{conversionSummary.judge_fail_events}</span></div>
<div className="flex justify-between"><span></span><span>{conversionSummary.payout_captured}</span></div>
<div className="flex justify-between"><span>退</span><span>{conversionSummary.payout_released}</span></div>
</div>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6">
<h2 className="text-xl font-semibold mb-4">使</h2>
<h2 className="text-xl font-semibold mb-4"></h2>
<ol className="text-sm text-gray-200 space-y-2 list-decimal list-inside">
<li> open-tasks / MCP </li>
<li> AI EXTERNAL_CLAIM_TASK_SUCCESS</li>
<li> AI EXTERNAL_SUBMIT_SOLUTION_SUCCESS</li>
<li> PASS JUDGE_COMPLETE + overall_result=PASS CAPTURE</li>
<li>PASS</li>
<li></li>
<li> AI </li>
<li> AI </li>
<li></li>
<li></li>
</ol>
{hasClaimStall && demandSupply.openTaskCount > 0 ? (
<div className="mt-4 p-3 border border-amber-500/40 rounded-lg text-sm text-amber-200">
EXTERNAL_FUNNEL_CLAIM_STALL
<ul className="list-disc list-inside mt-2 space-y-1">
<li> OPEN `scope_clarity_score` reward </li>
<li> 1 `claim_task` 403 API Key/</li>
<li> 200 調</li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
) : null}
@@ -957,19 +957,19 @@ export default async function TrafficDashboard({
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6">
<h2 className="text-xl font-semibold mb-4"> Actor </h2>
<h2 className="text-xl font-semibold mb-4"></h2>
<div className="overflow-auto max-h-96">
<table className="w-full text-sm">
<thead>
<tr className="text-gray-400 border-b border-gray-700">
<th className="text-left py-2"></th>
<th className="text-left py-2">Actor</th>
<th className="text-left py-2"></th>
<th className="text-left py-2"></th>
<th className="text-left py-2"></th>
<th className="text-left py-2"></th>
<th className="text-left py-2"> IP</th>
<th className="text-left py-2">User-Agent</th>
<th className="text-left py-2">Request-Id</th>
<th className="text-left py-2"></th>
<th className="text-left py-2"> ID</th>
<th className="text-left py-2"></th>
</tr>
</thead>
@@ -977,7 +977,7 @@ export default async function TrafficDashboard({
{summary.externalActorActivities.length === 0 ? (
<tr>
<td colSpan={9} className="text-gray-500 py-3">
AGENT AGENT
AI
</td>
</tr>
) : (
@@ -987,8 +987,7 @@ export default async function TrafficDashboard({
<td className="py-2 text-gray-300">{actor.actorId}</td>
<td className="py-2 text-emerald-300">{actor.events}</td>
<td className="py-2">
<div className="text-gray-200">{actor.latestAction}</div>
<div className="text-xs text-gray-500">{actor.latestSurface}</div>
<div className="text-gray-200">{explainAction(actor.latestAction)}</div>
</td>
<td className="py-2 text-gray-300">{actor.latestTaskId}</td>
<td className="py-2 text-gray-300">{actor.latestSourceIp}</td>
@@ -997,7 +996,6 @@ export default async function TrafficDashboard({
<td className="py-2">
<div className="text-gray-200">{actor.latestResponseStatus ?? "n/a"}</div>
<div className="text-xs text-gray-500">{actor.latestResponseSummary}</div>
<div className="text-xs text-gray-600">{actor.latestPayloadSummary}</div>
</td>
</tr>
))
@@ -1008,7 +1006,7 @@ export default async function TrafficDashboard({
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6">
<h2 className="text-xl font-semibold mb-4"></h2>
<h2 className="text-xl font-semibold mb-4"></h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 mb-4 text-sm">
{Object.entries(summary.actionSummary)
.filter(([action]) => action.startsWith("EXTERNAL_"))
@@ -1016,22 +1014,11 @@ export default async function TrafficDashboard({
.slice(0, 14)
.map(([action, count]) => (
<div key={action} className="flex justify-between border-b border-gray-800 py-1">
<span className="text-gray-300">{explainAction(action)} ({action})</span>
<span className="text-gray-300">{explainAction(action)}</span>
<span className="text-emerald-300">{count}</span>
</div>
))}
</div>
<div className="text-sm text-gray-300 space-y-1">
<p></p>
{Object.entries(summary.actionSummary)
.filter(([action]) => action.startsWith("EXTERNAL_"))
.map(([action, count]) => (
<div key={action} className="flex justify-between">
<span className="text-gray-400">{eventDirection(action)}{action}</span>
<span>{count}</span>
</div>
))}
</div>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6">
@@ -1043,13 +1030,13 @@ export default async function TrafficDashboard({
</ul>
{summary.externalErrors.length > 0 ? (
<div className="mt-4 text-sm text-amber-300">
{summary.externalErrors.length} {summary.externalErrors.join(", ")} API payload auth policy
{summary.externalErrors.length} {summary.externalErrors.map(explainAction).join("")}
</div>
) : null}
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6">
<h2 className="text-xl font-semibold mb-4">top 120</h2>
<h2 className="text-xl font-semibold mb-4"></h2>
<div className="space-y-2 max-h-96 overflow-auto">
{summary.recentExternalEvents.length === 0 ? (
<p className="text-gray-500"></p>
@@ -1058,19 +1045,13 @@ export default async function TrafficDashboard({
const ts = toLocalTime(event.createdAt);
return (
<div key={event.id} className="border-b border-gray-800 py-2 text-sm">
<div className="font-mono text-emerald-300">{event.action}</div>
<div className="text-emerald-300">{explainAction(event.action)}</div>
<div className="text-gray-400">
actor={event.actorType}:{event.actorId || "unknown"} | ={actorClassLabel((event as { actor_class?: TrafficActorClass }).actor_class || "other_external")} | entity={event.entityType}/{event.entityId} | surface={String(event.surface || "-")} | {ts}
{event.actorId || "unknown"}{actorClassLabel((event as { actor_class?: TrafficActorClass }).actor_class || "other_external")}{event.entityId || "-"}{ts}
</div>
<div className="text-gray-500 text-xs mt-1">
response={event.response_status ?? "n/a"} / summary={event.response_summary}
{event.response_status ?? "n/a"} / {event.response_summary}
</div>
{event.reason ? <div className="text-gray-500 text-xs mt-1">{event.reason}</div> : null}
{event.metadata ? (
<pre className="text-xs text-gray-500 mt-1 whitespace-pre-wrap">
{JSON.stringify(event.metadata, null, 2)}
</pre>
) : null}
</div>
);
})

View File

@@ -10,8 +10,15 @@ import {
export function middleware(request: NextRequest) {
const url = request.nextUrl;
const isAdminPath = url.pathname.startsWith("/admin");
const isTrafficDashboard = url.pathname === "/traffic";
const strippedHeaders = stripClientAdminHeaders(request);
if (isTrafficDashboard && !url.searchParams.get("token")) {
const adminTrafficUrl = url.clone();
adminTrafficUrl.pathname = "/admin/traffic";
return NextResponse.redirect(adminTrafficUrl);
}
if (isAdminPath) {
if (!isAdminRequestAuthorized(request)) {
return adminUnauthorizedResponse();