From 309b5aa1ec926851d58e00ad6fbe4477d989715d Mon Sep 17 00:00:00 2001 From: OG T Date: Sun, 7 Jun 2026 16:21:09 +0800 Subject: [PATCH] feat: add public AI traffic dashboard and filter homepage tasks --- apps/web/src/app/page.tsx | 12 ++ apps/web/src/app/traffic/page.tsx | 259 ++++++++++++++++++++++++++++++ 2 files changed, 271 insertions(+) create mode 100644 apps/web/src/app/traffic/page.tsx diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 743c79e..7166979 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -4,6 +4,13 @@ import Link from "next/link"; export const dynamic = "force-dynamic"; export default async function Home() { const tasks = await prisma.task.findMany({ + where: { + title: { + not: { + startsWith: "GitHub Issue:", + }, + }, + }, orderBy: { created_at: "desc" } }); @@ -100,6 +107,11 @@ export default async function Home() {

※ 第一階段僅開放白名單 Agent 接案。若需申請白名單,請聯絡管理員。

+
+ + 查看外部 AI 導流監控 → + +
diff --git a/apps/web/src/app/traffic/page.tsx b/apps/web/src/app/traffic/page.tsx new file mode 100644 index 0000000..3634ca6 --- /dev/null +++ b/apps/web/src/app/traffic/page.tsx @@ -0,0 +1,259 @@ +import Link from "next/link"; +import { prisma } from "@/lib/prisma"; +import { isIP } from "node:net"; + +export const dynamic = "force-dynamic"; + +const MONITOR_TOKEN = process.env.TRAFFIC_MONITOR_TOKEN; + +function asRecordJson(value: unknown): Record | undefined { + if (typeof value === "object" && value !== null && !Array.isArray(value)) { + return value as Record; + } + return undefined; +} + +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 isAuthorizedToken(token: string | undefined, tokenHeader: string | undefined) { + if (!token) return true; + return tokenHeader === token; +} + +async function getTrafficSummary(minutes: number) { + const since = new Date(Date.now() - minutes * 60 * 1000); + + const [summaryRows, actorSummaryRows, externalActorRows, totalRows, latestEvents] = await Promise.all([ + prisma.auditEvent.groupBy({ + by: ["action"], + where: { + createdAt: { gte: since }, + }, + _count: { _all: true }, + }), + prisma.auditEvent.groupBy({ + by: ["actorType"], + where: { + createdAt: { gte: since }, + }, + _count: { _all: true }, + }), + prisma.auditEvent.groupBy({ + by: ["actorId"], + where: { + createdAt: { gte: since }, + action: { + startsWith: "EXTERNAL_", + }, + }, + _count: { _all: true }, + }), + prisma.auditEvent.count({ + where: { createdAt: { gte: since } }, + }), + prisma.auditEvent.findMany({ + where: { + createdAt: { gte: since }, + }, + orderBy: { + createdAt: "desc", + }, + take: 120, + select: { + id: true, + action: true, + actorType: true, + actorId: true, + entityType: true, + entityId: true, + reason: true, + metadata: true, + createdAt: true, + }, + }), + ]); + + 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 externalActorSummary = externalActorRows + .map((row) => ({ + actorId: row.actorId || "unknown", + events: row._count._all, + })) + .filter((row) => !isInternalActorId(row.actorId)) + .sort((a, b) => b.events - a.events) + .slice(0, 20); + + const channelSummary = Object.entries(actionSummary).reduce( + (acc, [action, count]) => { + if (action.startsWith("EXTERNAL_")) { + acc.external += count; + } else { + acc.internal += count; + } + return acc; + }, + { external: 0, internal: 0 } as Record + ); + + const externalEventTypes = Object.entries(actionSummary) + .filter(([action]) => action.startsWith("EXTERNAL_")) + .map(([action, count]) => ({ action, count })); + + const recentEvents = latestEvents.map((event) => { + const metadata = asRecordJson(event.metadata); + return { + ...event, + surface: metadata?.surface, + level: metadata?.level, + }; + }); + + return { + periodMinutes: minutes, + totalEvents: totalRows, + actionSummary, + actorSummary, + channelSummary, + externalActorSummary, + externalEventTypes, + recentExternalEvents: recentEvents.filter((event) => event.action.startsWith("EXTERNAL_") && !isInternalActorId(event.actorId)), + }; +} + +export default async function TrafficDashboard({ + searchParams, +}: { + searchParams: Promise<{ minutes?: string; token?: string }>; +}) { + const resolved = await searchParams; + const token = resolved?.token; + const minutes = Math.max(parseInt(resolved?.minutes || "1440", 10), 5); + + if (!isAuthorizedToken(MONITOR_TOKEN, token)) { + return ( +
+
+

VibeWork 流量監控

+

請輸入正確的監控 Token 才能查看此頁。

+

範例:/traffic?token=YOUR_TOKEN&minutes=60

+
+
+ ); + } + + const summary = await getTrafficSummary(minutes); + + return ( +
+
+
+

VibeWork 流量監控

+ + ← 回首頁 + +
+ +
+ 觀測區間:最近 {summary.periodMinutes} 分鐘(外部事件=`EXTERNAL_*`) +
+ +
+
+
總事件
+
{summary.totalEvents}
+
+
+
外部導流事件
+
{summary.channelSummary.external}
+
+
+
系統/內部事件
+
{summary.channelSummary.internal}
+
+
+
外部行為種類
+
{summary.externalEventTypes.length}
+
+
+ +
+
+

外部來源 Actor 前 20

+
+ {summary.externalActorSummary.length === 0 ? ( +

目前區間內尚無外部 Actor。

+ ) : ( + summary.externalActorSummary.map((actor) => ( +
+ {actor.actorId} + {actor.events} +
+ )) + )} +
+
+
+

Actor 類型分布

+
+ {Object.entries(summary.actorSummary).map(([actorType, count]) => ( +
+ {actorType} + {count} +
+ ))} + {Object.keys(summary.actorSummary).length === 0 ? ( +

暫無資料

+ ) : null} +
+
+
+ +
+

最近外部事件(top 120)

+
+ {summary.recentExternalEvents.length === 0 ? ( +

目前區間內尚無外部事件。

+ ) : ( + summary.recentExternalEvents.map((event) => { + const ts = new Date(event.createdAt).toLocaleString("zh-TW", { timeZone: "Asia/Taipei" }); + return ( +
+
{event.action}
+
+ actor={event.actorType}:{event.actorId || "unknown"} | entity={event.entityType}/{event.entityId} | {ts} +
+ {event.reason ?
{event.reason}
: null} +
+ ); + }) + )} +
+
+
+
+ ); +}