Deploy to Production / pre-check (push) Successful in 58s
Deploy to Production / build (push) Successful in 4m3s
Deploy to Production / deploy-edgeone (push) Successful in 3m48s
Deploy to Production / finalize (push) Successful in 26s
Deploy to Production / notify-failure (push) Skipped
86 lines
3.0 KiB
TypeScript
86 lines
3.0 KiB
TypeScript
// 给管理员发提醒邮件(带节流)。
|
|
//
|
|
// 用途:定时任务(证书/代理体检、用量告警等)需要给站长发提醒,
|
|
// 但又不该每天重复刷屏。收件人固定为 MAIL_ADMIN,发件人默认「小赵」。
|
|
//
|
|
// 节流:同一 dedupeKey 在 minGapHours 内只发一次,时间戳存 KV `notify:last:<key>`。
|
|
|
|
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, '<').replace(/>/g, '>');
|
|
}
|
|
|
|
export function textToHtml(subject: string, text: string): string {
|
|
const paras = text
|
|
.split('\n')
|
|
.map((l) =>
|
|
l.trim()
|
|
? `<p style="margin:0 0 8px">${escapeHtml(l)}</p>`
|
|
: '<p style="margin:0 0 8px"> </p>',
|
|
)
|
|
.join('');
|
|
return (
|
|
`<div style="max-width:560px;margin:0 auto;padding:20px;font-family:'PingFang SC','Microsoft YaHei',sans-serif;color:#253830">` +
|
|
`<div style="font-size:15px;font-weight:600;color:#2f9e63;margin-bottom:12px">${escapeHtml(subject)}</div>` +
|
|
`<div style="background:#fff;border:1px solid #e7e9ee;border-radius:10px;padding:16px;font-size:14px;line-height:1.7">${paras}</div>` +
|
|
`<div style="font-size:12px;color:#8a94a6;margin-top:12px">来自 artalk-cf 定时提醒</div></div>`
|
|
);
|
|
}
|
|
|
|
export async function notifyAdmin(env: Env, input: AdminNotifyInput): Promise<AdminNotifyResult> {
|
|
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 };
|
|
}
|