logo NodeSeekbeta

Komari 美化通知邮件模板 - 更新 3.0 版本

上一个文章:

2.0版本:https://www.nodeseek.com/post-582484-1


Komari 美化通知邮件模板 - 更新 3.0 版本

3.0展示:
image
image
image

🎉 本次更新:修复 Outlook 等客户端中文乱码问题 | 新增消息智能翻译 | 优化移动端显示 | 更丰富的服务器信息展示

✨ 3.0 版本新特性

特性 说明
🔧 编码修复 添加 UTF-8 字符集声明,彻底解决 Outlook 等客户端中文乱码
🌐 智能翻译 自动将英文系统消息翻译为中文(如 This is a test message → 这是一条来自 Komari 的测试通知)
📱 响应式优化 移动端胶囊布局自动适配,小屏幕下信息不重叠
💻 硬件信息 新增 CPU 核心数、内存、硬盘容量展示
🌍 地区识别 国旗 Emoji 自动转换为文字地区代码(如 🇨🇳 → CN)
📊 告警详情 流量/CPU/内存超限提示更直观,显示具体数值

🚀 快速开始

前置要求

  • 支持API的邮件发送服务(Mailgun / Resend / 自建 SMTP-to-API/其他的发给AI改就可以了很简单)
  • 推荐 Mailgun(每日 100 封免费)或 Resend(每月 3,000 封免费)

⚙️ 配置步骤

第一步:获取 API 凭证

📮 Mailgun 配置

配置项 获取位置 示例
key Settings → API Keys → Private API Key key-xxxxxxxxxx-xxxxxxxx
dom Sending → Domains mg.yourdomain.com
from 自定义发件人 Komari Monitor <[email protected]>

📧 Resend 配置(未经过测试,AI直接改的)

配置项 获取位置 示例
key Dashboard → API Keys re_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxx
from 需先验证域名或使用测试域名 Komari Monitor <[email protected]>

第二步:填入 Komari

  1. 打开 Komari 面板 → 设置 → 通知
  2. 开启通知开关
  3. 通知渠道选择 JavaScript 代码
  4. 粘贴下方对应版本的代码
  5. 点击保存,发送测试验证

📋 完整代码

🔷 Mailgun API 版本

async function sendEvent(event) {
  const C = {
    key: "YOUR_MAILGUN_API_KEY",           // ← 替换为你的 Private API Key
    dom: "YOUR_MAILGUN_DOMAIN",            // ← 替换为你的发送域名
    from: "Komari Monitor <[email protected]>",  // ← 自定义发件人
    to: "[email protected]"           // ← 接收通知的邮箱
  };

  // Base64 编码(兼容浏览器/Node.js)
  const b64 = s => {
    try { if (typeof btoa != 'undefined') return btoa(s); } catch (e) {}
    try { if (typeof Buffer != 'undefined') return Buffer.from(s).toString('base64'); } catch (e) {}
    let o = "", c = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", i = 0, a, b, d;
    while (i < s.length) {
      a = s.charCodeAt(i++); b = s.charCodeAt(i++); d = s.charCodeAt(i++);
      o += c.charAt(a >> 2) + c.charAt((a & 3) << 4 | b >> 4) + c.charAt((b & 15) << 2 | d >> 6) + c.charAt(d & 63).replace(64, '=');
    }
    return o;
  };

  // 字节格式化(自动转换 MB/GB)
  const fmt = bytes => {
    if (!bytes) return '0';
    const k = 1024, gb = bytes / (k**3);
    return gb < 1 ? Math.round(bytes / (k**2)) + 'M' : Math.round(gb) + 'G';
  };

  // 国旗 Emoji 转地区代码
  const flagToTxt = str => {
    if (!str) return 'UNK';
    return Array.from(str).map(char => {
      const code = char.codePointAt(0);
      return (code >= 0x1F1E6 && code <= 0x1F1FF) ? String.fromCharCode(code - 127397) : char;
    }).join('').toUpperCase();
  };

  // 智能翻译系统消息
  const translateMsg = msg => {
    if (!msg) return '';
    return msg
      .replace(/This is a test message from Komari\.?/ig, "这是一条来自 Komari 的测试通知。")
      .replace(/used\s+([0-9.]+.*?%)\s+\((.*?)\),\s*type=sum/ig, "流量或硬盘总量超限!已使用 $1 (具体数值:$2)")
      .replace(/used\s+([0-9.]+.*?%)\s+\((.*?)\),\s*type=cpu/ig, "CPU 负载过高!当前占用 $1 ($2)")
      .replace(/used\s+([0-9.]+.*?%)\s+\((.*?)\),\s*type=memory/ig, "内存占用过高!当前已用 $1 ($2)")
      .replace(/type=sum/ig, "触发类型:总量累计")
      .replace(/used/ig, "已使用");
  };

  // 发送邮件
  const send = async (html, sub) => {
    try {
      const resp = await fetch(`https://api.mailgun.net/v3/${C.dom}/messages`, {
        method: 'POST',
        headers: {
          'Authorization': `Basic ${b64('api:' + C.key)}`,
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: `from=${encodeURIComponent(C.from)}&to=${encodeURIComponent(C.to)}&subject=${encodeURIComponent(sub)}&html=${encodeURIComponent(html)}`
      });
      return resp.ok;
    } catch (e) { return false; }
  };

  try {
    const c = event.clients || [];
    const map = {
      'Offline': ['#e11d48', '离线警告'],
      'Online': ['#059669', '恢复上线'],
      'Alert': ['#d97706', '系统告警'],
      'Renew': ['#2563eb', '自动续费'],
      'Expire': ['#dc2626', '到期提醒'],
      'Test': ['#7c3aed', '测试通知']
    };
    const [color, title] = map[event.event] || ['#4b5563', '系统通知'];

    // 北京时间转换
    const d = new Date(new Date(event.time).getTime() + 28800000);
    const date = `${d.getUTCFullYear()}/${d.getUTCMonth() + 1}/${d.getUTCDate()} ${d.getUTCHours().toString().padStart(2, '0')}:${d.getUTCMinutes().toString().padStart(2, '0')}`;
    const fontBase = "font-family:'MiSans',system-ui,-apple-system,BlinkMacSystemFont,sans-serif";
  
    const translatedMessage = translateMsg(event.message);

    // 服务器信息行
    const rows = c.map(x => {
      const cpu = x.cpu_cores ? x.cpu_cores + 'C' : '?';
      const specs = `${cpu} · ${fmt(x.mem_total)} · ${fmt(x.disk_total)}`;
      const ip = x.ipv4 || x.ipv6 || 'No IP';
      const region = flagToTxt(x.region || 'UNK');
    
      return `<tr>
        <td style="padding:10px 0;border-bottom:1px solid #f3f4f6">
          <div style="display:flex;flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:6px;">
            <div style="flex:1 1 230px;max-width:100%;">
              <div style="display:inline-block;background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px;padding:3px 10px;line-height:1.2;margin-bottom:4px;max-width:100%;box-sizing:border-box;">
                <span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:${color};margin-right:6px;vertical-align:middle"></span>
                <span style="font-weight:500;color:#1f2937;font-size:13.5px;letter-spacing:-0.2px;vertical-align:middle;word-break:break-all;">${x.name || '?'}</span>
              </div>
              <div style="padding-left:4px;font-size:10px;color:#9ca3af;${fontBase};letter-spacing:0.3px;">${specs}</div>
            </div>
            <div class="capsule-box" style="flex:0 1 auto;text-align:right;">
              <div style="display:inline-block;background:#f9fafb;border:1px solid #e5e7eb;border-radius:20px;padding:4px 10px;line-height:1.2;white-space:nowrap;">
                <span style="font-size:11px;color:#6b7280;vertical-align:middle;position:relative;top:-0.5px;${fontBase}">${ip}</span>
                <span style="color:#e5e7eb;margin:0 1px;font-size:11px;vertical-align:middle;position:relative;top:-1px">|</span>
                <span style="font-size:11px;font-weight:600;color:#4b5563;vertical-align:middle;position:relative;top:-0.5px;${fontBase}">${region}</span>
              </div>
            </div>
          </div>
        </td>
      </tr>`;
    }).join('');

    // ✅ 3.0 关键修复:添加 charset="UTF-8" 防止乱码
    const html = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<style>@media only screen and (max-width:480px){.capsule-box{text-align:left!important;margin-top:6px!important;flex-basis:100%!important}}</style>
</head>
<body style="margin:0;padding:0;background-color:#fff">
<div style="font-family:'MiSans','MiLanPro','MiLan',ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;background:#fff;padding:20px 0">
<div style="width:94%;max-width:480px;margin:0 auto;background:#fff;border-radius:12px;border:0;border-top:4px solid ${color};box-shadow:inset 0 3px 6px rgba(0,0,0,0.06),0 0 0 1px #e5e7eb,0 10px 25px -5px rgba(0,0,0,0.18),0 8px 10px -6px rgba(0,0,0,0.1);overflow:hidden">
<div style="padding:24px 5%">
<h1 style="font-size:18px;margin:0;color:#111827;font-weight:600;letter-spacing:-0.5px">${title}</h1>
<p style="color:#6b7280;font-size:11px;margin:4px 0 2px 0;font-weight:500">${date}</p>
${translatedMessage ? `<div style="margin:14px 0;background:#f9fafb;border-radius:0 6px 6px 0;padding:12px 16px;color:#374151;font-size:13px;font-weight:normal;line-height:1.6;border-left:4px solid ${color};word-wrap:break-word">${translatedMessage}</div>` : ''}
${rows ? `<div style="margin-top:8px"><table style="width:100%;border-collapse:collapse">${rows}</table></div>` : ''}
</div>
<div style="background:#fafafa;padding:10px;text-align:center;font-size:10px;color:#9ca3af;border-top:1px solid #f3f4f6;font-weight:500;letter-spacing:0.5px">Komari Monitoring</div>
</div>
</div>
</body>
</html>`;

    // 邮件主题
    let sub = `[${title}] ` + (!c.length ? 'System' : (c.length === 1 ? c[0].name : `${c.length} Servers Update`));
    if (sub.length > 80) sub = sub.slice(0, 77) + '...';

    return await send(html, sub);
  } catch (e) {
    return await send(`Error: ${e.message}`, 'Script Error');
  }
}

🔶 Resend API 版本

async function sendEvent(event) {
  const C = {
    key: "re_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxx",  // ← 替换为 Resend API Key
    from: "Komari Monitor <[email protected]>",  // ← 替换为验证后的发件地址
    to: "[email protected]"                 // ← 接收通知的邮箱
  };

  // 字节格式化(自动转换 MB/GB)
  const fmt = bytes => {
    if (!bytes) return '0';
    const k = 1024, gb = bytes / (k**3);
    return gb < 1 ? Math.round(bytes / (k**2)) + 'M' : Math.round(gb) + 'G';
  };

  // 国旗 Emoji 转地区代码
  const flagToTxt = str => {
    if (!str) return 'UNK';
    return Array.from(str).map(char => {
      const code = char.codePointAt(0);
      return (code >= 0x1F1E6 && code <= 0x1F1FF) ? String.fromCharCode(code - 127397) : char;
    }).join('').toUpperCase();
  };

  // 智能翻译系统消息
  const translateMsg = msg => {
    if (!msg) return '';
    return msg
      .replace(/This is a test message from Komari\.?/ig, "这是一条来自 Komari 的测试通知。")
      .replace(/used\s+([0-9.]+.*?%)\s+\((.*?)\),\s*type=sum/ig, "流量或硬盘总量超限!已使用 $1 (具体数值:$2)")
      .replace(/used\s+([0-9.]+.*?%)\s+\((.*?)\),\s*type=cpu/ig, "CPU 负载过高!当前占用 $1 ($2)")
      .replace(/used\s+([0-9.]+.*?%)\s+\((.*?)\),\s*type=memory/ig, "内存占用过高!当前已用 $1 ($2)")
      .replace(/type=sum/ig, "触发类型:总量累计")
      .replace(/used/ig, "已使用");
  };

  // 发送邮件
  const send = async (html, sub) => {
    try {
      const resp = await fetch('https://api.resend.com/emails', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${C.key}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          from: C.from,
          to: C.to,
          subject: sub,
          html: html
        })
      });
      return resp.ok;
    } catch (e) { return false; }
  };

  try {
    const c = event.clients || [];
    const map = {
      'Offline': ['#e11d48', '离线警告'],
      'Online': ['#059669', '恢复上线'],
      'Alert': ['#d97706', '系统告警'],
      'Renew': ['#2563eb', '自动续费'],
      'Expire': ['#dc2626', '到期提醒'],
      'Test': ['#7c3aed', '测试通知']
    };
    const [color, title] = map[event.event] || ['#4b5563', '系统通知'];

    // 北京时间转换
    const d = new Date(new Date(event.time).getTime() + 28800000);
    const date = `${d.getUTCFullYear()}/${d.getUTCMonth() + 1}/${d.getUTCDate()} ${d.getUTCHours().toString().padStart(2, '0')}:${d.getUTCMinutes().toString().padStart(2, '0')}`;
    const fontBase = "font-family:'MiSans',system-ui,-apple-system,BlinkMacSystemFont,sans-serif";
  
    const translatedMessage = translateMsg(event.message);

    // 服务器信息行
    const rows = c.map(x => {
      const cpu = x.cpu_cores ? x.cpu_cores + 'C' : '?';
      const specs = `${cpu} · ${fmt(x.mem_total)} · ${fmt(x.disk_total)}`;
      const ip = x.ipv4 ||
12
12

你好啊,陌生人!

我的朋友,看起来你是新来的,如果想参与到讨论中,点击下面的按钮!

📈用户数目📈

目前论坛共有72287位seeker

🎉欢迎新用户🎉