diff --git a/README.md b/README.md index 72f97fa..2669b56 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,11 @@ API_KEY="your-secure-mcp-key" GITHUB_TOKEN="github_pat_..." # 監控告警:外部導流/外部操作事件 webhook(可留空) VIBEWORK_TRAFFIC_WEBHOOK_URL="https://your-webhook" +# 直接推送到 Discord(可留空) +DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/xxx" +# Telegram 告警(可留空) +TELEGRAM_BOT_TOKEN="123456:abcdef" +TELEGRAM_CHAT_ID="-1001234567890" # optional:只允許有 token 的 /api/traffic 查詢 TRAFFIC_MONITOR_TOKEN="your-monitor-token" # optional:Scout 掃描參數 diff --git a/apps/web/src/lib/traffic-alert.ts b/apps/web/src/lib/traffic-alert.ts index 6d8b070..6756b5f 100644 --- a/apps/web/src/lib/traffic-alert.ts +++ b/apps/web/src/lib/traffic-alert.ts @@ -13,6 +13,29 @@ export type TrafficAlertEvent = { }; 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) { @@ -55,25 +78,69 @@ async function writeTrafficAuditEvent(event: TrafficAlertEvent) { export async function sendTrafficAlert(event: TrafficAlertEvent): Promise { void writeTrafficAuditEvent(event); - if (!TRAFFIC_WEBHOOK_URL) return; - const payload = { platform: "agent-bounty-protocol", created_at: new Date().toISOString(), ...event, }; - try { - await fetch(TRAFFIC_WEBHOOK_URL, { - method: "POST", - headers: { - "content-type": "application/json", - "x-trace-source": "agent-bounty-protocol", + 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), }, - body: JSON.stringify(payload), - signal: AbortSignal.timeout(3000), - }); - } catch { - // Avoid affecting request flow if webhook fails. - } + }, + 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); + } + }) + ); }