Files
agent-bounty-protocol/apps/web/src/lib/traffic-alert.ts
OG T 54608a3376
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 7s
feat: expand traffic alerts to discord/telegram
2026-06-07 16:23:22 +08:00

147 lines
4.1 KiB
TypeScript

import { prisma } from "./prisma";
import { Prisma } from "../../prisma/generated/client";
export type TrafficAlertEvent = {
level: "info" | "warning" | "error";
action: string;
surface: string;
actorType: "SYSTEM" | "AGENT" | "USER";
actorId: string;
taskId?: string;
message: string;
metadata?: Record<string, unknown>;
};
const TRAFFIC_WEBHOOK_URL = process.env.VIBEWORK_TRAFFIC_WEBHOOK_URL?.trim();
const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN?.trim();
const TELEGRAM_CHAT_ID = process.env.TELEGRAM_CHAT_ID?.trim();
const DISCORD_WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL?.trim();
function escapeMarkdown(value: unknown) {
if (value === null || value === undefined) return "";
return String(value)
.replace(/([_*[\]()~`>#+=|{}.!-])/g, "\\$1")
.replace(/\n/g, "\\n");
}
function buildTelegramMessage(event: TrafficAlertEvent) {
return (
`*VibeWork 流量告警*` +
`\n- 平台: \`agent-bounty-protocol\`` +
`\n- 等級: \`${event.level}\`` +
`\n- 行為: \`${event.action}\`` +
`\n- 通道: \`${event.surface}\`` +
`\n- Actor: \`${event.actorType}/${event.actorId}\`` +
`\n- 任務: \`${event.taskId || "n/a"}\`` +
`\n- 訊息: ${escapeMarkdown(event.message)}`
);
}
function resolveEntityFromTrafficEvent(event: TrafficAlertEvent) {
if (event.taskId) {
return { entityType: "TASK", entityId: event.taskId };
}
return {
entityType: "TASK",
entityId: `surface:${event.surface}`,
};
}
async function writeTrafficAuditEvent(event: TrafficAlertEvent) {
const { entityType, entityId } = resolveEntityFromTrafficEvent(event);
try {
await prisma.auditEvent.create({
data: {
actorType: event.actorType,
actorId: event.actorId,
action: event.action,
entityType,
entityId,
beforeState: Prisma.JsonNull,
afterState: Prisma.JsonNull,
reason: event.message,
metadata: {
...event.metadata,
level: event.level,
surface: event.surface,
source: "traffic-alert",
},
},
});
} catch (error) {
console.error("[Traffic alert persistence error]", error);
}
}
export async function sendTrafficAlert(event: TrafficAlertEvent): Promise<void> {
void writeTrafficAuditEvent(event);
const payload = {
platform: "agent-bounty-protocol",
created_at: new Date().toISOString(),
...event,
};
const notifyTargets = [
TRAFFIC_WEBHOOK_URL && {
kind: "generic",
url: TRAFFIC_WEBHOOK_URL,
init: {
method: "POST",
headers: {
"content-type": "application/json",
"x-trace-source": "agent-bounty-protocol",
},
body: JSON.stringify(payload),
},
},
DISCORD_WEBHOOK_URL && {
kind: "discord",
url: DISCORD_WEBHOOK_URL,
init: {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
content: `\`\`\`\n${JSON.stringify(payload, null, 2)}\n\`\`\``,
}),
},
},
TELEGRAM_BOT_TOKEN && TELEGRAM_CHAT_ID && {
kind: "telegram",
url: `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`,
init: {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
chat_id: TELEGRAM_CHAT_ID,
text: buildTelegramMessage(event),
parse_mode: "MarkdownV2",
}),
},
},
].filter(Boolean) as Array<{ kind: string; url: string; init: RequestInit }>;
if (notifyTargets.length === 0) return;
await Promise.allSettled(
notifyTargets.map(async (target) => {
try {
const response = await fetch(target.url, { ...target.init, signal: AbortSignal.timeout(3000) });
if (!response.ok) {
console.warn(
`[Traffic alert notify failed] ${target.kind} ${response.status} ${await response.text()}`
);
}
} catch (error) {
console.warn(`[Traffic alert notify failed] ${target.kind}`, error);
}
})
);
}