使用方法,还是komari 后台 —设置—通知 里面的 通知渠道 改成 JavaScript 。然后下面输入代码就行了,当然需要修改成自己的信息,需要申请邮件API
我这里API使用的是 https://resend.com/ 申请方便,如果你域名在CF,那么更简单,输入域名,授权CF 自动添加,弄好以后,增加个API 得到API KEY就完事了,免费版,1个月3000封


// ═══════════════════════════════════════════════════════════
// NanoMuse — Email Notification Script (Resend API)
// 对齐 Komari 通知结构,NanoMuse 风格邮件模板
// ═══════════════════════════════════════════════════════════
// ─── 用户配置(按需修改) ───
const SITE_NAME = "这里是很酷的名字"; // 探针站点名称,显示在邮件顶栏
const PANEL_URL = "https://你的域名"; // 探针面板地址,用于邮件内"查看详情"按钮跳转
const PHOTO_URL = "https://img.uppic.to/2026/03/31/charc8a606e78f00aab0.jpeg"; // 邮件内清晰角色图(建议1024px宽)默认是我自己的
const PHOTO_BLUR = "https://img.uppic.to/2026/03/31/char-blur-hq.jpeg"; // 邮件外层模糊角色图(同一张的模糊版)默认是我
const AUTHOR = "你的名字"; // 站长名,显示在邮件底栏
const RESEND_KEY = "你的api key"; // Resend API Key(https://resend.com/api-keys 获取)
const MAIL_FROM = "NanoMuse <[email protected]>"; // 发件人地址,域名必须在 Resend 验证过 这个我不改了,参考下格式
const MAIL_TO = "你的收件信箱"; // 收件人地址,改成你实际能收邮件的邮箱(QQ邮箱/Gmail/Outlook等)
// ─── 事件定义 ───
const EVENT_MAP = {
offline: {
tag: "OFFLINE", statusCn: "与目标失去联系 · 请尽快介入", statusIcon: "▲", statusIconColor: "#e15e57",
persona: "信号中断。我会在这里守着,等你回来。",
statusColor: "#cb8a30", tagColor: "#e15e57",
riskLevel: 4, riskLabel: "HIGH", riskColor: "#e15e57",
coreLine: { text: "ALERT ACTIVE", color: "rgba(255,90,90,0.8)" },
btnText: "查看详情 →", btnLink: "/instance/{uuid}",
metricsColors: { cpu: "#e15e57", ram: "#e15e57", disk: "#cb8a30" }
},
online: {
tag: "ONLINE", statusCn: "与目标重新建立联系", statusIcon: "◆", statusIconColor: "#2a8a5a",
persona: "链路重建完成。欢迎回来。",
statusColor: "#2a8a5a", tagColor: "#22c55e",
riskLevel: 1, riskLabel: "LOW", riskColor: "#22c55e",
coreLine: { text: "ALL CLEAR", color: "rgba(34,197,94,0.8)" },
btnText: "查看详情 →", btnLink: "/instance/{uuid}",
metricsColors: { cpu: "#3a5575", ram: "#3a5575", disk: "#3a5575" }
},
alert: {
tag: "OVERLOAD", statusCn: "目标核心压力过载", statusIcon: "▲", statusIconColor: "#c08830",
persona: "异常已锁定。我在这里,等你接管。",
statusColor: "#cb8a30", tagColor: "#e15e57",
riskLevel: 4, riskLabel: "HIGH", riskColor: "#e15e57",
coreLine: { text: "ALERT ACTIVE", color: "rgba(255,90,90,0.8)" },
btnText: "查看详情 →", btnLink: "/instance/{uuid}",
metricsColors: { cpu: "#e15e57", ram: "#e15e57", disk: "#cb8a30" }
},
expire: {
tag: "EXPIRE", statusCn: "能源即将耗尽", statusIcon: "▲", statusIconColor: "#ea8420",
persona: "能源即将耗尽。请尽快补充,维持核心运转。",
statusColor: "#cb8a30", tagColor: "#ea8420",
riskLevel: 3, riskLabel: "MED", riskColor: "#ea8420",
coreLine: { text: "EXPIRY WARN", color: "rgba(234,132,32,0.8)" },
btnText: "查看详情 →", btnLink: "/instance/{uuid}",
metricsColors: { cpu: "#3a5575", ram: "#3a5575", disk: "#3a5575" }
},
renew: {
tag: "RENEWED", statusCn: "能源已补充", statusIcon: "◆", statusIconColor: "#5f84be",
persona: "能源已补充。感谢你的守护。",
statusColor: "#5f84be", tagColor: "#5f84be",
riskLevel: 0, riskLabel: "NONE", riskColor: "#5f84be",
coreLine: { text: "RENEWED", color: "rgba(95,132,190,0.8)" },
btnText: "查看详情 →", btnLink: "/instance/{uuid}",
metricsColors: { cpu: "#3a5575", ram: "#3a5575", disk: "#3a5575" }
},
test: {
tag: "DIAG", statusCn: "诊断通道已开启", statusIcon: "◆", statusIconColor: "#7c6acd",
persona: "自检序列已完成。感知·链路·核心——全部正常。随时待命,等待你的指令。",
statusColor: "#7c6acd", tagColor: "#7c6acd",
riskLevel: 0, riskLabel: "NONE", riskColor: "#7c6acd",
coreLine: { text: "DIAG MODE", color: "rgba(124,106,205,0.8)" },
btnText: "打开面板 →", btnLink: "/",
metricsColors: { cpu: "#3a5575", ram: "#3a5575", disk: "#3a5575" }
}
};
// ─── 工具函数 ───
function toUTC8(ts) {
const d = ts ? new Date(ts) : new Date();
const u8 = new Date(d.getTime() + 8 * 3600000);
const Y = u8.getUTCFullYear();
const M = String(u8.getUTCMonth() + 1).padStart(2, "0");
const D = String(u8.getUTCDate()).padStart(2, "0");
const h = String(u8.getUTCHours()).padStart(2, "0");
const m = String(u8.getUTCMinutes()).padStart(2, "0");
const s = String(u8.getUTCSeconds()).padStart(2, "0");
return `${Y}-${M}-${D} ${h}:${m}:${s}`;
}
function toTimeOnly(ts) { return toUTC8(ts).split(" ")[1].slice(0, 5) + " UTC+8"; }
function fmtSize(bytes) {
if (!bytes || bytes <= 0) return "0 B";
const u = ["B", "KB", "MB", "GB", "TB"];
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), u.length - 1);
return (bytes / Math.pow(1024, i)).toFixed(i > 1 ? 2 : 0) + " " + u[i];
}
function fmtSizeShort(bytes) {
if (!bytes || bytes <= 0) return "0";
const u = ["B", "K", "M", "G", "T"];
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), u.length - 1);
const v = bytes / Math.pow(1024, i);
return (i > 1 ? v.toFixed(1) : Math.round(v)) + u[i];
}
function fmtCPU(v) { return v != null ? v.toFixed(1) + "%" : "--"; }
function fmtTraffic(bytes, limit) {
if (!limit || limit <= 0) return "∞";
return fmtSize(bytes) + " / " + fmtSize(limit);
}
function maskV4(ip) {
if (!ip) return "--";
const p = ip.split(".");
return p.length === 4 ? p[0] + "." + p[1] + ".*.*" : ip;
}
function maskV6(ip) {
if (!ip) return "--";
const p = ip.split(":");
return p.length >= 4 ? p.slice(0, 3).join(":") + ":****" : ip;
}
// ─── 风险等级方块 ───
function riskBlocksHTML(level, color) {
let cells = "";
for (let i = 0; i < 5; i++) {
const on = i < level;
const bg = on ? color : "rgba(180,195,215,0.25)";
cells += `<td style="width:16px;height:7px;background:${bg};border-radius:2px;${i < 4 ? "border-right:2px solid transparent;" : ""}font-size:0;"> </td>`;
}
return cells;
}
// ─── 断续能量色块 ───
const ENERGY_BAR = `<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%"><tr><td style="width:20px;height:3px;background:#00d8ff;font-size:0;"> </td><td> </td><td style="width:70px;height:3px;background:linear-gradient(90deg,#00d8ff,#2563eb);font-size:0;"> </td><td> </td><td style="width:20px;height:3px;background:#00d8ff;font-size:0;"> </td></tr></table>`;
// ─── LOGO SVG ───
const LOGO_SVG = `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" style="vertical-align:middle;"><path d="M12 2L2 7l10 5 10-5-10-5z" stroke="#00d8ff" stroke-width="1.5" fill="none"/><path d="M2 17l10 5 10-5" stroke="#00d8ff" stroke-width="1.5" fill="none"/><path d="M2 12l10 5 10-5" stroke="#00d8ff" stroke-width="1.5" fill="none"/></svg>`;
// ─── 生成邮件 HTML(v20 定稿结构) ───
function buildEmailHTML(event) {
const ev = (event.event || "test").toLowerCase();
const cfg = EVENT_MAP[ev] || EVENT_MAP.test;
const c = event.clients?.[0] || {};
const msg = event.message || "";
const time = toUTC8(event.time);
const timeShort = toTimeOnly(event.time);
const name = c.name || "Unknown";
const uuid = c.uuid || "";
const ipv4 = maskV4(c.ipv4);
const specParts = [];
if (c.cpu_cores) specParts.push(c.cpu_cores + "C");
if (c.mem_total) specParts.push(fmtSizeShort(c.mem_total));
if (c.disk_total) specParts.push(fmtSizeShort(c.disk_total));
const specStr = specParts.join(" / ") || "--";
const linkParts = [];
if (c.bandwidth) linkParts.push(c.bandwidth);
if (c.traffic_limit && c.traffic_limit > 0) linkParts.push(fmtSizeShort(c.traffic_limit));
const linkStr = linkParts.join(" · ") || "--";
const showMetrics = ["online", "alert", "test"].includes(ev);
const cpuVal = c.cpu != null ? fmtCPU(c.cpu) : "--";
const ramVal = (c.mem_total && c.mem_used) ? Math.round(c.mem_used / c.mem_total * 100) + "%" : "--";
const diskVal = (c.disk_total && c.disk_used) ? Math.round(c.disk_used / c.disk_total * 100) + "%" : "--";
const mc = cfg.metricsColors;
const showBilling = ["expire", "renew", "test"].includes(ev) && c.price > 0;
const costStr = c.price ? (c.currency || "$") + c.price : "--";
const expStr = c.expired_at ? toUTC8(c.expired_at).split(" ")[0] : "--";
const btnUrl = PANEL_URL + (cfg.btnLink || "/").replace("{uuid}", uuid);
const F = `'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif`;
const FM = `'Courier New',monospace`;
return `<!DOCTYPE html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1.0"/></head>
<body style="margin:0;padding:0;background:#dfe7f2;font-family:${F};">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#dfe7f2;"><tr><td align="center" style="padding:20px 4px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="960" style="max-width:960px;width:100%;border-radius:24px;overflow:hidden;border:1.5px solid rgba(255,255,255,0.5);box-shadow:0 0 40px rgba(130,180,255,0.08),0 16px 50px rgba(78,112,160,0.1);background-image:url('${PHOTO_BLUR}');background-size:cover;background-position:center;background-color:#dce4ef;">
<tr><td style="background:rgba(225,232,244,0.3);padding:0;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%">
<tr><td style="padding:10px 20px 6px 20px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%"><tr>
<td style="width:40px;vertical-align:middle;"><div style="height:1px;background:linear-gradient(90deg,rgba(0,180,220,0.6),transparent);"></div></td>
<td style="text-align:center;vertical-align:middle;padding:0 10px;"><span style="font-family:${FM};font-size:11px;letter-spacing:3px;color:#4a6585;">◇ NANO MUSE ALERT ◇</span></td>
<td style="width:40px;vertical-align:middle;"><div style="height:1px;background:linear-gradient(90deg,transparent,rgba(0,180,220,0.6));"></div></td>
</tr></table>
</td></tr>
<tr><td style="padding:0 16px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="border-radius:16px;overflow:hidden;border:1px solid rgba(255,255,255,0.45);box-shadow:0 16px 48px rgba(78,112,160,0.08),inset 0 0 0 1px rgba(255,255,255,0.12);background-image:url('${PHOTO_URL}');background-size:cover;background-position:center;">
<tr><td style="background:linear-gradient(105deg, rgba(238,242,252,0.96) 0%, rgba(238,242,252,0.93) 32%, rgba(238,242,252,0.65) 52%, rgba(238,242,252,0.3) 70%, rgba(238,242,252,0.18) 100%);padding:0;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%">
<tr><td style="padding:0;"><table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%">
<tr><td style="height:3px;font-size:0;">${ENERGY_BAR}</td></tr>
<tr><td style="padding:14px 28px 12px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%"><tr>
<td style="vertical-align:middle;">${LOGO_SVG}<span style="font-size:17px;font-weight:900;letter-spacing:4px;color:#1a2a40;margin-left:8px;vertical-align:middle;">${SITE_NAME}</span></td>
<td style="vertical-align:middle;text-align:right;"><span style="font-family:${FM};font-size:11px;color:rgba(100,120,150,0.6);letter-spacing:1.5px;">Server Fleet Monitor</span></td>
</tr></table></td></tr>
<tr><td style="height:1px;background:linear-gradient(90deg,rgba(148,170,200,0.2),rgba(148,170,200,0.1) 40%,transparent 70%);font-size:0;"> </td></tr>
</table></td></tr>
<tr><td style="padding:0;"><table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%"><tr>
<td style="width:52%;vertical-align:top;padding:18px 16px 20px 28px;">
<div style="font-size:13px;font-weight:700;color:#6c84a1;letter-spacing:0.1em;margin-bottom:12px;">NANO CORE SYSTEM</div>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-bottom:10px;"><tr><td style="width:20px;vertical-align:top;padding-top:1px;"><span style="font-size:14px;color:${cfg.statusIconColor};">${cfg.statusIcon}</span></td><td><div style="font-size:11px;font-weight:600;color:#8a9ab0;letter-spacing:1px;margin-bottom:2px;">系统状态</div><div style="font-size:16px;font-weight:800;color:${cfg.statusColor};">${cfg.statusCn}</div></td></tr></table>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-bottom:10px;"><tr><td style="width:20px;vertical-align:top;padding-top:1px;"><span style="font-size:12px;color:#5f84be;">◆</span></td><td><div style="font-size:11px;font-weight:600;color:#8a9ab0;letter-spacing:1px;margin-bottom:2px;">目标节点</div><div style="font-size:16px;font-weight:800;color:#3a5575;">${name} <span style="font-size:12px;font-weight:600;color:#7a8fa8;">主控关注中</span></div></td></tr></table>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-bottom:12px;"><tr><td style="width:20px;vertical-align:top;padding-top:1px;"><span style="font-size:12px;color:${cfg.riskColor};">●</span></td><td><div style="font-size:11px;font-weight:600;color:#8a9ab0;letter-spacing:1px;margin-bottom:3px;">威胁等级</div><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td style="vertical-align:middle;"><span style="font-family:${FM};font-size:14px;font-weight:800;color:${cfg.tagColor};letter-spacing:0.08em;">${cfg.tag}</span></td><td style="vertical-align:middle;padding-left:10px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0" style="display:inline-table;vertical-align:middle;"><tr>${riskBlocksHTML(cfg.riskLevel, cfg.riskColor)}</tr></table></td><td style="vertical-align:middle;padding-left:8px;"><span style="font-size:11px;font-weight:700;color:${cfg.riskColor};">${cfg.riskLabel}</span></td></tr></table></td></tr></table>
${msg ? `<div style="font-size:12px;color:#5a6a82;margin:0 0 10px;line-height:1.6;padding-left:20px;">${msg}</div>` : ""}
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="border-top:1px solid rgba(148,170,200,0.1);padding-top:8px;margin-bottom:8px;"><tr><td style="width:48px;padding:2px 0;"><span style="font-family:${FM};font-size:11px;color:#98a8bc;letter-spacing:1px;">SPEC</span></td><td style="padding:2px 0;"><span style="font-family:${FM};font-size:12px;color:#3a5070;">${specStr}</span></td></tr><tr><td style="padding:2px 0;"><span style="font-family:${FM};font-size:11px;color:#98a8bc;letter-spacing:1px;">ADDR</span></td><td style="padding:2px 0;"><span style="font-family:${FM};font-size:12px;color:#3a5070;">${ipv4}</span></td></tr><tr><td style="padding:2px 0;"><span style="font-family:${FM};font-size:11px;color:#98a8bc;letter-spacing:1px;">LINK</span></td><td style="padding:2px 0;"><span style="font-size:12px;color:#3a5070;">${linkStr}</span></td></tr></table>
${showMetrics ? `<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="border-top:1px solid rgba(148,170,200,0.1);padding-top:8px;margin-bottom:14px;"><tr><td style="width:33%;"><div style="font-family:${FM};font-size:11px;color:#98a8bc;letter-spacing:1px;margin-bottom:2px;">CPU</div><div style="font-size:16px;font-weight:800;color:${mc.cpu};">${cpuVal}</div></td><td style="width:33%;"><div style="font-family:${FM};font-size:11px;color:#98a8bc;letter-spacing:1px;margin-bottom:2px;">RAM</div><div style="font-size:16px;font-weight:800;color:${mc.ram};">${ramVal}</div></td><td style="width:34%;"><div style="font-family:${FM};font-size:11px;color:#98a8bc;letter-spacing:1px;margin-bottom:2px;">DISK</div><div style="font-size:16px;font-weight:800;color:${mc.disk};">${diskVal}</div></td></tr></table>` : ""}
${showBilling ? `<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="border-top:1px solid rgba(148,170,200,0.1);padding-top:8px;margin-bottom:14px;"><tr><td style="width:48px;padding:2px 0;"><span style="font-family:${FM};font-size:11px;color:#98a8bc;letter-spacing:1px;">COST</span></td><td style="padding:2px 0;"><span style="font-family:${FM};font-size:13px;font-weight:600;color:#2563eb;">${costStr}</span></td></tr><tr><td style="padding:2px 0;"><span style="font-family:${FM};font-size:11px;color:#98a8bc;letter-spacing:1px;">EXP</span></td><td style="padding:2px 0;"><span style="font-family:${FM};font-size:13px;font-weight:600;color:#cb8a30;">${expStr}</span></td></tr></table>` : ""}
<table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td style="border-radius:10px;background:linear-gradient(180deg,rgba(122,182,255,0.92),rgba(95,156,235,0.90));box-shadow:0 4px 14px rgba(95,156,235,0.15);text-align:center;"><a href="${btnUrl}" target="_blank" style="display:inline-block;padding:10px 22px;font-size:13px;font-weight:700;color:#fff;text-decoration:none;">${cfg.btnText}</a></td></tr></table>
</td>
<td style="width:48%;vertical-align:top;padding:18px 24px 20px 8px;">
<div style="text-align:right;margin-bottom:14px;"><span style="font-family:${FM};font-size:11px;color:rgba(90,115,145,0.5);letter-spacing:1.5px;">${timeShort}</span></div>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin-left:auto;background:rgba(240,245,255,0.78);border:1px solid rgba(200,215,235,0.5);border-radius:10px;box-shadow:0 4px 16px rgba(78,112,160,0.06);"><tr><td style="padding:12px 16px;"><div style="font-family:${FM};font-size:11px;font-weight:700;color:rgba(80,110,150,0.7);letter-spacing:2px;margin-bottom:8px;">[ NANO CORE ]</div><div style="font-family:${FM};font-size:11px;color:rgba(70,100,140,0.6);line-height:1.8;"><span style="color:rgba(34,197,94,0.75);">●</span> SYNC LOCKED<br/><span style="color:rgba(34,197,94,0.75);">●</span> SIGNAL STABLE<br/><span style="color:rgba(59,130,246,0.75);">●</span> AES-256<br/><span style="color:${cfg.coreLine.color};">●</span> ${cfg.coreLine.text}</div></td></tr></table>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin-top:55px;margin-left:auto;margin-right:12px;background:rgba(240,245,255,0.82);border:1px solid rgba(180,200,225,0.5);border-radius:10px;box-shadow:0 6px 20px rgba(78,112,160,0.06);"><tr><td style="padding:14px 18px;"><div style="font-size:14px;line-height:1.6;color:#4e6787;font-weight:700;">"${cfg.persona}"</div></td></tr></table>
<div style="margin-top:20px;text-align:right;"><span style="font-family:${FM};font-size:11px;color:rgba(90,115,145,0.4);letter-spacing:1px;">PERSONA · ACTIVE</span></div>
</td>
</tr></table></td></tr>
<tr><td><table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%">
<tr><td style="height:1px;background:linear-gradient(90deg,rgba(148,170,200,0.15),rgba(148,170,200,0.08) 40%,transparent 70%);font-size:0;"> </td></tr>
<tr><td style="padding:10px 28px;text-align:center;"><span style="font-family:${FM};font-size:11px;letter-spacing:3px;color:rgba(100,120,150,0.5);">SYSTEM MONITORING ACTIVE</span><span style="color:#95a8bc;margin:0 10px;">—</span><a href="${PANEL_URL}" target="_blank" style="font-family:${FM};font-size:11px;letter-spacing:1.5px;color:#2563eb;text-decoration:none;font-weight:600;">Komari</a><span style="color:#95a8bc;margin:0 5px;">·</span><span style="font-family:${FM};font-size:11px;color:rgba(100,120,150,0.5);">NanoMuse</span><span style="color:#95a8bc;margin:0 5px;">·</span><span style="font-family:${FM};font-size:11px;color:rgba(100,120,150,0.5);font-style:italic;">${AUTHOR}</span></td></tr>
<tr><td style="height:3px;font-size:0;">${ENERGY_BAR}</td></tr>
</table></td></tr>
</table>
</td></tr>
</table>
</td></tr>
<tr><td style="padding:6px 20px 10px 20px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%"><tr>
<td style="width:40px;vertical-align:middle;"><div style="height:1px;background:linear-gradient(90deg,rgba(0,180,220,0.55),transparent);"></div></td>
<td style="text-align:center;vertical-align:middle;padding:0 10px;"><span style="font-family:${FM};font-size:11px;letter-spacing:2px;color:#4a6585;">SECURE CHANNEL · ENCRYPTED</span></td>
<td style="width:40px;vertical-align:middle;"><div style="height:1px;background:linear-gradient(90deg,transparent,rgba(0,180,220,0.55));"></div></td>
</tr></table>
</td></tr>
</table>
</td></tr>
</table>
</td></tr></table></body></html>`;
}
// ─── 邮件主题行 ───
function buildSubject(event) {
const ev = (event.event || "test").toLowerCase();
const cfg = EVENT_MAP[ev] || EVENT_MAP.test;
const name = event.clients?.[0]?.name || "Unknown";
return `[${cfg.tag}] ${name} — ${cfg.statusCn}`;
}
// ─── Resend API ───
async function sendMessage(message, title) {
// Komari calls this with plain text (message, title)
// Wrap in minimal NanoMuse styled HTML
const isHTML = message.trim().startsWith("<!DOCTYPE") || message.trim().startsWith("<html");
const html = isHTML ? message : `<!DOCTYPE html><html><body style="margin:0;padding:20px;background:#dfe7f2;font-family:'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;"><div style="max-width:600px;margin:0 auto;background:#fff;border-radius:12px;padding:24px;box-shadow:0 2px 12px rgba(0,0,0,0.08);"><h2 style="color:#1a2a40;margin:0 0 16px;">${title || "NanoMuse"}</h2><pre style="white-space:pre-wrap;color:#3a5070;font-size:14px;line-height:1.6;margin:0;">${message}</pre><hr style="border:none;border-top:1px solid #e0e6ed;margin:16px 0 12px;"/><p style="font-size:11px;color:#8898b0;text-align:center;margin:0;">Komari · NanoMuse · ${AUTHOR}</p></div></body></html>`;
const subject = title || "NanoMuse Notification";
const resp = await fetch("https://api.resend.com/emails", {
method: "POST",
headers: { "Authorization": "Bearer " + RESEND_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ from: MAIL_FROM, to: MAIL_TO, subject, html })
});
const data = await resp.json();
if (!resp.ok) { console.error("[NanoMuse Email] Send failed:", data); return false; }
console.log("[NanoMuse Email] Sent:", data.id);
return true;
}
// ─── 主入口 ───
async function sendEvent(event) {
const html = buildEmailHTML(event);
const subject = buildSubject(event);
// sendMessage detects HTML and passes through directly
return await sendMessage(html, subject);
}
好看
牛
收藏。
好看帮顶
好看,收藏帮顶
好看绑定
邮件通知这个真好看,浏览器也要搭配背景
已严肃收藏
bd
已收藏,准备开搞