147 lines
4.1 KiB
TypeScript
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);
|
|
}
|
|
})
|
|
);
|
|
}
|