// 给管理员发提醒邮件(带节流)。 // // 用途:定时任务(证书/代理体检、用量告警等)需要给站长发提醒, // 但又不该每天重复刷屏。收件人固定为 MAIL_ADMIN,发件人默认「小赵」。 // // 节流:同一 dedupeKey 在 minGapHours 内只发一次,时间戳存 KV `notify:last:`。 import type { Env } from '../types'; import { sendMail } from './mail'; export interface AdminNotifyInput { subject: string; text: string; /** 同一 key 在 minGapHours 内只发一次;不传则不节流 */ dedupeKey?: string; minGapHours?: number; fromName?: string; } export interface AdminNotifyResult { ok: boolean; sent: boolean; throttled?: boolean; lastAt?: string; error?: string; } function escapeHtml(x: string): string { return x.replace(/&/g, '&').replace(//g, '>'); } export function textToHtml(subject: string, text: string): string { const paras = text .split('\n') .map((l) => l.trim() ? `

${escapeHtml(l)}

` : '

 

', ) .join(''); return ( `
` + `
${escapeHtml(subject)}
` + `
${paras}
` + `
来自 artalk-cf 定时提醒
` ); } export async function notifyAdmin(env: Env, input: AdminNotifyInput): Promise { const to = (env.MAIL_ADMIN || '').trim(); if (!to) return { ok: false, sent: false, error: 'MAIL_ADMIN not configured' }; const subject = String(input.subject || '').trim().slice(0, 160) || '提醒'; const text = String(input.text || '').trim().slice(0, 8000); if (!text) return { ok: false, sent: false, error: 'text required' }; const dedupeKey = String(input.dedupeKey || '').trim().slice(0, 60); const minGapHours = Math.min(Math.max(input.minGapHours || 0, 0), 24 * 30); const throttleKey = dedupeKey ? `notify:last:${dedupeKey}` : ''; if (throttleKey && minGapHours > 0) { const last = await env.RSS_KV.get(throttleKey); const lastAt = last ? parseInt(last, 10) : 0; if (lastAt && Date.now() - lastAt < minGapHours * 3600 * 1000) { return { ok: true, sent: false, throttled: true, lastAt: new Date(lastAt).toISOString() }; } } const sent = await sendMail(env, { to, subject, html: textToHtml(subject, text), text, fromName: input.fromName || '小赵', }); if (sent && throttleKey) { try { await env.RSS_KV.put(throttleKey, String(Date.now()), { expirationTtl: 60 * 60 * 24 * 90 }); } catch { /* 节流时间戳写失败不影响已发出的邮件 */ } } return { ok: sent, sent }; }