// Тема «Kvarta Cafe: Система»: сбои и служебные события без персональных данных. // Всё дублируется в stdout — это единственный канал, если Telegram недоступен или тема не задана. import type { TelegramClient } from './client.js'; export const SYSTEM_THROTTLE_MS = 10 * 60 * 1000; export type SystemReporter = { /** Никогда не бросает: отчёт о сбое не должен порождать новый сбой. */ report(kind: string, text: string): Promise; }; export function createSystemReporter({ client, chatId, threadId, now, log = (line: string) => console.log(line), }: { client?: Pick; chatId?: number; threadId?: number; now: () => Date; log?: (line: string) => void; }): SystemReporter { // Одинаковые события не чаще раза в 10 минут; пропущенные копим и показываем с ближайшим отправленным. const throttle = new Map(); return { async report(kind, text) { log(`[system] ${kind}: ${text}`); if (!client || chatId === undefined || threadId === undefined) return; const t = now().getTime(); const state = throttle.get(kind); if (state && t - state.sentAt < SYSTEM_THROTTLE_MS) { state.skipped++; return; } const count = (state?.skipped ?? 0) + 1; throttle.set(kind, { sentAt: t, skipped: 0 }); try { await client.sendMessage({ chat_id: chatId, message_thread_id: threadId, text: count > 1 ? `×${count} за период · ${text}` : text, }); } catch (err) { log(`[system] не отправлено в Telegram: ${errorMessage(err)}`); } }, }; } export function errorMessage(err: unknown): string { return err instanceof Error ? err.message : String(err); } /** Короткий stack без путей до node_modules: в «Систему» и stdout. */ export function shortError(err: unknown): string { if (!(err instanceof Error)) return String(err); const frames = (err.stack ?? '') .split('\n') .slice(1) .filter((l) => !l.includes('node_modules') && !l.includes('node:')) .slice(0, 3) .map((l) => l.trim()); return [`${err.name}: ${err.message}`, ...frames].join('\n'); }