归档 artalk-cf 评论后端 + rss-robot 到 blog-admin(含技术选型/模块分布 README)
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
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
This commit is contained in:
1 parent
855a001046
commit
a74b3c7127
98 files changed
+15657
No files matched your search
@@ -0,0 +1,85 @@
|
||||
// 给管理员发提醒邮件(带节流)。
|
||||
//
|
||||
// 用途:定时任务(证书/代理体检、用量告警等)需要给站长发提醒,
|
||||
// 但又不该每天重复刷屏。收件人固定为 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 };
|
||||
}
|
||||
Reference in new issue
Block a user