fix: harden traffic telegram alert delivery and fallback chat id
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 6s

This commit is contained in:
OG T
2026-06-07 19:28:00 +08:00
parent d1765a78d9
commit 69cb463cbf

View File

@@ -1,7 +1,12 @@
import { request } from "node:https";
import { prisma } from "./prisma";
import { Prisma } from "../../prisma/generated/client";
const TELEGRAM_BOT_ID_FROM_TOKEN = (() => {
if (!process.env.TELEGRAM_BOT_TOKEN) return undefined;
const [candidate] = process.env.TELEGRAM_BOT_TOKEN.split(":", 1);
return candidate?.trim() || undefined;
})();
export type TrafficAlertEvent = {
level: "info" | "warning" | "error";
action: string;
@@ -26,7 +31,95 @@ const TELEGRAM_CHAT_ID = (
process.env.VIBEWORKAIAGENTBOT_CHAT_ID ||
process.env.VIBEWORK_AI_BOT_CHAT_ID
)?.trim();
const TELEGRAM_FALLBACK_FROM_UPDATES =
process.env.TELEGRAM_FALLBACK_FROM_UPDATES?.trim().toLowerCase() === "true";
const DISCORD_WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL?.trim();
const TELEGRAM_NOTIFY_TIMEOUT_MS = Math.max(
1,
Number.parseInt(process.env.TELEGRAM_NOTIFY_TIMEOUT_MS?.trim() || "3000", 10) || 3000
);
const TELEGRAM_NOTIFY_MAX_ATTEMPTS = Math.max(
1,
Number.parseInt(process.env.TELEGRAM_NOTIFY_MAX_ATTEMPTS?.trim() || "2", 10) || 2
);
const TELEGRAM_NOTIFY_RETRY_BASE_DELAY_MS = Math.max(
100,
Number.parseInt(process.env.TELEGRAM_NOTIFY_RETRY_BASE_DELAY_MS?.trim() || "400", 10) || 400
);
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
let cachedTelegramFallbackChatId: string | null | undefined = undefined;
function normalizeChatId(rawChatId: string | undefined) {
if (!rawChatId) return undefined;
const normalized = rawChatId.trim();
if (!normalized) return undefined;
if (TELEGRAM_BOT_ID_FROM_TOKEN && normalized === TELEGRAM_BOT_ID_FROM_TOKEN) {
return undefined;
}
return normalized;
}
async function resolveTelegramFallbackChatId(): Promise<string | undefined> {
if (!TELEGRAM_BOT_TOKEN) {
return undefined;
}
if (cachedTelegramFallbackChatId !== undefined) {
return cachedTelegramFallbackChatId || undefined;
}
try {
const response = await fetch(`https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getUpdates?limit=20`, {
method: "GET",
signal: AbortSignal.timeout(TELEGRAM_NOTIFY_TIMEOUT_MS),
});
if (!response.ok) {
return undefined;
}
const data = (await response.json()) as {
result?: Array<Record<string, unknown>>;
};
const updates = Array.isArray(data.result) ? data.result : [];
for (let index = updates.length - 1; index >= 0; index -= 1) {
const update = updates[index];
const chatCandidate =
(update?.message as { chat?: { id?: number | string } } | undefined)?.chat?.id ??
(update?.edited_message as { chat?: { id?: number | string } } | undefined)?.chat?.id ??
(update?.callback_query as { message?: { chat?: { id?: number | string } } } | undefined)?.message?.chat?.id ??
(update?.channel_post as { chat?: { id?: number | string } } | undefined)?.chat?.id;
const normalized = normalizeChatId(String(chatCandidate ?? ""));
if (normalized) {
cachedTelegramFallbackChatId = normalized;
console.log(`[Traffic alert] Resolved Telegram chat_id from updates: ${normalized}`);
return normalized;
}
}
cachedTelegramFallbackChatId = null;
return undefined;
} catch (error) {
console.warn("[Traffic alert] resolveTelegramFallbackChatId failed", error);
cachedTelegramFallbackChatId = null;
return undefined;
}
}
async function resolveTelegramChatId(): Promise<string | undefined> {
const explicitChatId = normalizeChatId(TELEGRAM_CHAT_ID);
if (explicitChatId) return explicitChatId;
if (!TELEGRAM_FALLBACK_FROM_UPDATES) {
return undefined;
}
return resolveTelegramFallbackChatId();
}
function escapeMarkdown(value: unknown) {
if (value === null || value === undefined) return "";
@@ -55,43 +148,34 @@ function buildTelegramMessage(event: TrafficAlertEvent) {
async function sendViaHttps(url: string, body: Record<string, unknown>) {
return new Promise<void>((resolve, reject) => {
try {
const parsed = new URL(url);
const payload = JSON.stringify(body);
let responseBody = "";
const requestHandle = request(
{
method: "POST",
hostname: parsed.hostname,
path: `${parsed.pathname}${parsed.search}`,
port: 443,
protocol: "https:",
headers: {
"content-type": "application/json",
"content-length": Buffer.byteLength(payload),
},
fetch(url, {
method: "POST",
headers: {
"content-type": "application/json",
"content-length": String(Buffer.byteLength(payload)),
},
(response) => {
let responseBody = "";
response.on("data", (chunk) => {
responseBody += chunk;
});
body: payload,
signal: AbortSignal.timeout(TELEGRAM_NOTIFY_TIMEOUT_MS),
})
.then(async (response) => {
responseBody = await response.text();
if (response.status >= 200 && response.status < 300) {
resolve();
return;
}
response.on("end", () => {
if (response.statusCode && response.statusCode >= 200 && response.statusCode < 300) {
return resolve();
}
reject(new Error(`Telegram API ${response.statusCode}: ${responseBody.slice(0, 200)}`));
});
}
);
requestHandle.on("error", reject);
requestHandle.setTimeout(3000, () => {
requestHandle.destroy(new Error("Telegram request timeout"));
});
requestHandle.write(payload);
requestHandle.end();
reject(new Error(`Telegram API ${response.status}: ${responseBody.slice(0, 200)}`));
})
.catch((error) => {
if (error && error.name === "TimeoutError") {
reject(new Error(`Telegram API timeout ${TELEGRAM_NOTIFY_TIMEOUT_MS}ms`));
return;
}
reject(error);
});
} catch (error) {
reject(error);
}
@@ -153,6 +237,8 @@ export async function sendTrafficAlert(event: TrafficAlertEvent): Promise<void>
...event,
};
const resolvedTelegramChatId = await resolveTelegramChatId();
const notifyTargets = [
TRAFFIC_WEBHOOK_URL && {
kind: "generic",
@@ -179,7 +265,8 @@ export async function sendTrafficAlert(event: TrafficAlertEvent): Promise<void>
}),
},
},
TELEGRAM_BOT_TOKEN && TELEGRAM_CHAT_ID && {
TELEGRAM_BOT_TOKEN &&
resolvedTelegramChatId && {
kind: "telegram",
url: `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`,
init: {
@@ -187,11 +274,11 @@ export async function sendTrafficAlert(event: TrafficAlertEvent): Promise<void>
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
chat_id: TELEGRAM_CHAT_ID,
text: buildTelegramMessage(event),
parse_mode: "MarkdownV2",
}),
body: JSON.stringify({
chat_id: resolvedTelegramChatId,
text: buildTelegramMessage(event),
parse_mode: "MarkdownV2",
}),
},
},
].filter(Boolean) as Array<{ kind: string; url: string; init: RequestInit }>;
@@ -205,7 +292,20 @@ export async function sendTrafficAlert(event: TrafficAlertEvent): Promise<void>
const payload = target.init.body
? JSON.parse(typeof target.init.body === "string" ? target.init.body : "{}")
: {};
await sendViaHttps(target.url, payload);
let attempt = 0;
while (true) {
try {
await sendViaHttps(target.url, payload);
return;
} catch (error) {
attempt += 1;
if (attempt >= TELEGRAM_NOTIFY_MAX_ATTEMPTS) {
throw error;
}
await sleep(TELEGRAM_NOTIFY_RETRY_BASE_DELAY_MS * attempt);
}
}
} else {
const response = await fetch(target.url, { ...target.init, signal: AbortSignal.timeout(3000) });
if (!response.ok) {