logo NodeSeekbeta

备份一个Komari通知模板

可能要卸载了,留个存档
现在这个模板流量定时报告还有问题,昨日流量会显示0,期待大佬修复

const TELEGRAM_BOT_TOKEN = "";
const TELEGRAM_CHAT_ID = "";
const PANEL_URL = ""; 

// ==================== [核心数据字典] ====================
const VALID_COUNTRY_CODES = new Set([
  "AC","AD","AE","AF","AG","AI","AL","AM","AO","AQ","AR","AS","AT","AU","AW","AX","AZ",
  "BA","BB","BD","BE","BF","BG","BH","BI","BJ","BL","BM","BN","BO","BQ","BR","BS","BT",
  "BV","BW","BY","BZ","CA","CC","CD","CF","CG","CH","CI","CK","CL","CM","CN","CO","CP",
  "CR","CU","CV","CW","CX","CY","CZ","DE","DG","DJ","DK","DM","DO","DZ","EA","EC","EE",
  "EG","EH","ER","ES","ET","EU","FI","FJ","FK","FM","FO","FR","GA","GB","GD","GE","GF",
  "GG","GH","GI","GL","GM","GN","GP","GQ","GR","GS","GT","GU","GW","GY","HK","HM","HN",
  "HR","HT","HU","IC","ID","IE","IL","IM","IN","IO","IQ","IR","IS","IT","JE","JM","JO",
  "JP","KE","KG","KH","KI","KM","KN","KP","KR","KW","KY","KZ","LA","LB","LC","LI","LK",
  "LR","LS","LT","LU","LV","LY","MA","MC","MD","ME","MF","MG","MH","MK","ML","MM","MN",
  "MO","MP","MQ","MR","MS","MT","MU","MV","MW","MX","MY","MZ","NA","NC","NE","NF","NG",
  "NI","NL","NO","NP","NR","NU","NZ","OM","PA","PE","PF","PG","PH","PK","PL","PM","PN",
  "PR","PS","PT","PW","PY","QA","RE","RO","RS","RU","RW","SA","SB","SC","SD","SE","SG",
  "SH","SI","SJ","SK","SL","SM","SN","SO","SR","SS","ST","SV","SX","SY","SZ","TA","TC",
  "TD","TF","TG","TH","TJ","TK","TL","TM","TN","TO","TR","TT","TV","TW","TZ","UA","UG",
  "UM","US","UY","UZ","VA","VC","VE","VG","VI","VN","VU","WF","WS","XK","YE","YT","ZA",
  "ZM","ZW"
]);

const COUNTRY_ALIASES = {
  "中国": "CN", "大陆": "CN", "北京": "CN", "上海": "CN", "广州": "CN", "深圳": "CN", "杭州": "CN",
  "香港": "HK", "澳门": "MO", "台湾": "TW",
  "美国": "US", "洛杉矶": "US", "纽约": "US", "芝加哥": "US", "西雅图": "US", "达拉斯": "US", "圣何塞": "US", "硅谷": "US",
  "usa": "US", "united states": "US", "america": "US", "los angeles": "US", "new york": "US",
  "chicago": "US", "seattle": "US", "dallas": "US", "san jose": "US",
  "加拿大": "CA", "多伦多": "CA", "温哥华": "CA", "蒙特利尔": "CA",
  "canada": "CA", "toronto": "CA", "vancouver": "CA", "montreal": "CA",
  "日本": "JP", "东京": "JP", "大阪": "JP", "japan": "JP", "tokyo": "JP", "osaka": "JP",
  "韩国": "KR", "首尔": "KR", "korea": "KR", "south korea": "KR", "seoul": "KR",
  "新加坡": "SG", "singapore": "SG",
  "德国": "DE", "法兰克福": "DE", "柏林": "DE", "germany": "DE", "deutschland": "DE", "frankfurt": "DE", "berlin": "DE",
  "英国": "GB", "伦敦": "GB", "united kingdom": "GB", "britain": "GB", "london": "GB",
  "法国": "FR", "巴黎": "FR", "france": "FR", "paris": "FR",
  "荷兰": "NL", "阿姆斯特丹": "NL", "netherlands": "NL", "holland": "NL", "amsterdam": "NL",
  "俄罗斯": "RU", "莫斯科": "RU", "russia": "RU", "moscow": "RU", "乌克兰": "UA", "ukraine": "UA",
  "澳大利亚": "AU", "澳洲": "AU", "悉尼": "AU", "墨尔本": "AU", "australia": "AU", "sydney": "AU", "melbourne": "AU",
  "新西兰": "NZ", "new zealand": "NZ",
  "印度": "IN", "孟买": "IN", "德里": "IN", "india": "IN", "mumbai": "IN", "delhi": "IN",
  "泰国": "TH", "曼谷": "TH", "thailand": "TH", "bangkok": "TH",
  "越南": "VN", "河内": "VN", "胡志明": "VN", "vietnam": "VN", "hanoi": "VN", "ho chi minh": "VN",
  "马来西亚": "MY", "吉隆坡": "MY", "malaysia": "MY", "kuala lumpur": "MY",
  "菲律宾": "PH", "马尼拉": "PH", "philippines": "PH", "manila": "PH",
  "印度尼西亚": "ID", "印尼": "ID", "雅加达": "ID", "indonesia": "ID", "jakarta": "ID",
  "土耳其": "TR", "伊斯坦布尔": "TR", "turkey": "TR", "istanbul": "TR",
  "阿联酋": "AE", "迪拜": "AE", "united arab emirates": "AE", "uae": "AE", "dubai": "AE",
  "沙特": "SA", "沙特阿拉伯": "SA", "saudi arabia": "SA",
  "巴西": "BR", "圣保罗": "BR", "brazil": "BR", "sao paulo": "BR",
  "墨西哥": "MX", "mexico": "MX", "阿根廷": "AR", "argentina": "AR", "智利": "CL", "chile": "CL",
  "南非": "ZA", "south africa": "ZA", "埃及": "EG", "egypt": "EG", "以色列": "IL", "israel": "IL",
  "意大利": "IT", "米兰": "IT", "罗马": "IT", "italy": "IT", "milan": "IT", "rome": "IT",
  "西班牙": "ES", "马德里": "ES", "spain": "ES", "madrid": "ES",
  "葡萄牙": "PT", "portugal": "PT", "瑞士": "CH", "苏黎世": "CH", "switzerland": "CH", "zurich": "CH",
  "瑞典": "SE", "sweden": "SE", "挪威": "NO", "norway": "NO", "芬兰": "FI", "finland": "FI",
  "丹麦": "DK", "denmark": "DK", "波兰": "PL", "poland": "PL", "奥地利": "AT", "austria": "AT",
  "捷克": "CZ", "czech": "CZ", "爱尔兰": "IE", "ireland": "IE", "冰岛": "IS", "iceland": "IS"
};

// ==================== [基础工具函数] ====================
function escapeHtml(text) {
  return String(text ?? "")
    .replace(/&/g, "&")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
}

function getCSTTime(timeStr) {
  let date;
  if (!timeStr || timeStr.startsWith('0001')) {
    date = new Date();
  } else {
    date = new Date(timeStr.replace(/\.\d+Z$/, 'Z'));
  }
  const cst = new Date(date.getTime() + 8 * 60 * 60 * 1000);
  const f = (n) => n.toString().padStart(2, '0');
  return `${cst.getUTCFullYear()}-${f(cst.getUTCMonth() + 1)}-${f(cst.getUTCDate())} ${f(cst.getUTCHours())}:${f(cst.getUTCMinutes())}:${f(cst.getUTCSeconds())}`;
}

function formatTraffic(bytes) {
  if (!bytes || bytes === 0) return '无限制';
  const gb = bytes / (1024 ** 3);
  if (gb >= 1024) return `${(gb / 1024).toFixed(2)} TB`;
  return `${gb.toFixed(2)} GB`;
}

function hideIP(ip) {
  if (!ip) return '未知';
  const parts = ip.split('.');
  if (parts.length === 4) {
    return `${parts[0]}.${parts[1]}.xxx.xxx`;
  }
  const v6Parts = ip.split(':');
  return v6Parts.slice(0, 3).join(':') + ':xxxx:xxxx:xxxx';
}

function formatMemory(bytes) {
  if (!bytes || bytes === 0) return '0';
  const gb = bytes / (1024 ** 3);
  return gb < 1 ? `${Math.round(gb * 1024)}MB` : `${Math.round(gb)}G`;
}

function hasFlagEmoji(text) {
  return /[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/.test(text || "");
}

// ==================== [国旗及地区识别核心逻辑] ====================
function countryCodeToFlag(code) {
  code = String(code || "").toUpperCase();
  if (!VALID_COUNTRY_CODES.has(code)) return "";
  return code
    .split("")
    .map(char => String.fromCodePoint(127397 + char.charCodeAt(0)))
    .join("");
}

function getCountryCodeFromClient(client) {
  const directFields = [
    client.country_code, client.countryCode, client.region_code, client.regionCode,
    client.iso2, client.cc, client.country_code2, client.countryCode2
  ];

  for (const field of directFields) {
    if (!field) continue;
    const code = String(field).trim().toUpperCase();
    if (VALID_COUNTRY_CODES.has(code)) return code;
  }

  const text = [
    client.name, client.region, client.country, client.location,
    client.remark, client.description, client.hostname, client.host
  ].filter(Boolean).join(" ").toLowerCase();

  for (const key in COUNTRY_ALIASES) {
    if (text.includes(key.toLowerCase())) {
      return COUNTRY_ALIASES[key];
    }
  }

  const tokens = text.toUpperCase().split(/[^A-Z]/).filter(Boolean);
  for (const token of tokens) {
    if (VALID_COUNTRY_CODES.has(token)) return token;
  }
  return "";
}

function getFlag(client) {
  if (hasFlagEmoji(client.region) || hasFlagEmoji(client.name)) {
    return "";
  }
  const code = getCountryCodeFromClient(client || {});
  return code ? countryCodeToFlag(code) : "";
}

// ==================== [翻译/事件词典处理逻辑] ====================
function translateMessage(message) {
  let msg = String(message || "").trim();
  if (!msg) return "";

  const lower = msg.toLowerCase();
  const exactMap = {
    "client is offline": "节点已离线", "client is online": "节点已恢复在线",
    "client offline": "节点离线", "client online": "节点在线",
    "server is offline": "服务器已离线", "server is online": "服务器已恢复在线",
    "node is offline": "节点已离线", "node is online": "节点已恢复在线",
    "host is offline": "主机已离线", "host is online": "主机已恢复在线",
    "heartbeat timeout": "心跳超时", "connection timeout": "连接超时",
    "request timeout": "请求超时", "response timeout": "响应超时",
    "ping timeout": "Ping 超时", "no response": "节点无响应",
    "test message": "测试消息", "test notification": "测试通知",
    "cpu usage is too high": "CPU 使用率过高", "memory usage is too high": "内存使用率过高",
    "ram usage is too high": "内存使用率过高", "disk usage is too high": "磁盘使用率过高",
    "load is too high": "系统负载过高", "network error": "网络异常",
    "network unreachable": "网络不可达", "connection refused": "连接被拒绝",
    "connection reset": "连接被重置", "service expired": "服务已到期",
    "service will expire": "服务即将到期", "renew success": "续费成功", "renew failed": "续费失败"
  };

  if (exactMap[lower]) return exactMap[lower];

  const rules = [
    [/\bclient is offline\b/gi, "节点已离线"], [/\bclient is online\b/gi, "节点已恢复在线"],
    [/\bserver is offline\b/gi, "服务器已离线"], [/\bserver is online\b/gi, "服务器已恢复在线"],
    [/\bnode is offline\b/gi, "节点已离线"], [/\bnode is online\b/gi, "节点已恢复在线"],
    [/\bhost is offline\b/gi, "主机已离线"], [/\bhost is online\b/gi, "主机已恢复在线"],
    [/\bheartbeat timeout\b/gi, "心跳超时"], [/\bconnection timeout\b/gi, "连接超时"],
    [/\brequest timeout\b/gi, "请求超时"], [/\bresponse timeout\b/gi, "响应超时"], [/\bping timeout\b/gi, "Ping 超时"],
    [/\bcpu usage is too high\b/gi, "CPU 使用率过高"], [/\bmemory usage is too high\b/gi, "内存使用率过高"],
    [/\bram usage is too high\b/gi, "内存使用率过高"], [/\bdisk usage is too high\b/gi, "磁盘使用率过高"],
    [/\bload is too high\b/gi, "系统负载过高"], [/\bnetwork unreachable\b/gi, "网络不可达"],
    [/\bnetwork error\b/gi, "网络异常"], [/\bconnection refused\b/gi, "连接被拒绝"], [/\bconnection reset\b/gi, "连接被重置"],
    [/\bno response\b/gi, "节点无响应"], [/\boffline\b/gi, "离线"], [/\bonline\b/gi, "在线"], [/\bdown\b/gi, "不可用"],
    [/\bup\b/gi, "可用"], [/\balert\b/gi, "告警"], [/\bwarning\b/gi, "警告"], [/\bcritical\b/gi, "严重"],
    [/\berror\b/gi, "错误"], [/\bfailed\b/gi, "失败"], [/\bfailure\b/gi, "故障"], [/\bsuccess\b/gi, "成功"],
    [/\btimeout\b/gi, "超时"], [/\bheartbeat\b/gi, "心跳"], [/\bconnection\b/gi, "连接"], [/\bconnect\b/gi, "连接"],
    [/\bdisconnect\b/gi, "断开连接"], [/\brequest\b/gi, "请求"], [/\bresponse\b/gi, "响应"], [/\bserver\b/gi, "服务器"],
    [/\bclient\b/gi, "节点"], [/\bnode\b/gi, "节点"], [/\bhost\b/gi, "主机"], [/\bcpu\b/gi, "CPU"], [/\bmemory\b/gi, "内存"],
    [/\bram\b/gi, "内存"], [/\bdisk\b/gi, "磁盘"], [/\bload\b/gi, "负载"], [/\btraffic\b/gi, "流量"], [/\bnetwork\b/gi, "网络"],
    [/\bupload\b/gi, "上传"], [/\bdownload\b/gi, "下载"], [/\busage\b/gi, "使用率"], [/\bhigh\b/gi, "过高"],
    [/\blow\b/gi, "过低"], [/\brenew\b/gi, "续费"], [/\bexpire\b/gi, "到期"], [/\bexpired\b/gi, "已到期"],
    [/\btest\b/gi, "测试"], [/\bmessage\b/gi, "消息"], [/\bnotification\b/gi, "通知"]
  ];

  for (const [pattern, replacement] of rules) {
    msg = msg.replace(pattern, replacement);
  }

  return msg;
}

const EVENT_MAP = {
  'online':    { icon: "🟢", title: "服务器上线", level: "正常" },
  'offline':   { icon: "🔴", title: "服务器离线", level: "异常" },
  'alert':     { icon: "⚠️", title: "异常警报",   level: "警告" },
  'renew':     { icon: "💰", title: "续费通知",   level: "提醒" },
  'expire':    { icon: "🚨", title: "到期预警",   level: "重要" },
  'expired':   { icon: "🚨", title: "服务到期",   level: "重要" },
  'test':      { icon: "🧪", title: "测试通知",   level: "测试" },
  'recover':   { icon: "🟢", title: "告警恢复",   level: "正常" },
  'recovered': { icon: "🟢", title: "告警恢复",   level: "正常" },
  'report':    { icon: "📊", title: "流量定时报告", level: "报告" }
};

// ==================== [网络发送模块] ====================
async function sendMessage(message, title, instanceId = null) {
  if (!TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) return false;

  const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`;
  
  let inline_keyboard = [];
  let row1 = [{ text: "📊 进入面板", url: PANEL_URL }];

  if (instanceId && instanceId !== '未知') {
    row1.push({ 
      text: "🌐 实例详情", 
      url: `${PANEL_URL}/instance/${instanceId}` 
    });
  }
  inline_keyboard.push(row1);

  const resp = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      chat_id: TELEGRAM_CHAT_ID,
      text: `<b>${title}</b>\n\n${message}`,
      parse_mode: 'HTML',
      reply_markup: { inline_keyboard: inline_keyboard },
      disable_web_page_preview: true
    }),
  });
  return resp.ok;
}

// ==================== [主事件解析处理函数] ====================
async function sendEvent(event) {
  try {
    let eventName = String(event.event || "").toLowerCase();
    
    if (eventName.includes('report') || (event.message && event.message.includes('流量报告'))) {
      eventName = 'report';
    }

    const info = EVENT_MAP[eventName] || { icon: "📌", title: "系统通知", level: "通知" };
    const title = `${info.icon} ${info.title}`;
    
    let clientInfo = '';
    let targetInstanceId = null;

    if (event.clients && event.clients.length > 0) {
      if (event.clients.length === 1) {
        const c = event.clients[0];
        targetInstanceId = c.uuid || null;
        
        const flag = getFlag(c);
        const flagPrefix = flag ? `${flag} ` : '';
        const region = c.region ? ` [${c.region}]` : '';
        
        const hiddenIPv4 = hideIP(c.ipv4);
        const hiddenIPv6 = hideIP(c.ipv6);
        
        const mem = c.mem_total ? formatMemory(c.mem_total) : '0';
        const swap = c.swap_total ? formatMemory(c.swap_total) : '0';
        const disk = c.disk_total ? formatMemory(c.disk_total) : '0';
        const trafficLimit = formatTraffic(c.traffic_limit);
        
        let trafficCycle = '';
        if (c.traffic_limit_type && c.traffic_limit !== 0) {
          const typeLower = String(c.traffic_limit_type).toLowerCase().trim();
          const typeMap = {
            'sum': '(总和)', 'max': '(取最大)', 'min': '(取最小)', 'upload': '(仅上传)', 'download': '(仅下载)'
          };
          trafficCycle = typeMap[typeLower] || `(${c.traffic_limit_type})`;
        }

        clientInfo += `🖥️ <b>服务器:</b>${flagPrefix}${escapeHtml(c.name || "未知节点")}${escapeHtml(region)}\n`;
        clientInfo += `📝 <b>配 置:</b>${c.cpu_cores || '0'}C / ${mem}${swap !== '0' ? `+${swap}` : ''} / ${disk}\n`;
        clientInfo += `🌐 <b>IPv4:</b><code>${escapeHtml(hiddenIPv4)}</code>\n`;
        clientInfo += `🌐 <b>IPv6:</b><code>${escapeHtml(hiddenIPv6)}</code>\n`;
        clientInfo += `📶 <b>流量限额:</b>${trafficLimit}${trafficCycle}\n`;
        
        if (eventName === 'renew' || eventName === 'expire' || eventName === 'expired') {
          clientInfo += `💰 <b>账 单:</b>${escapeHtml(c.currency || '$')}${c.price || '0'} (${c.billing_cycle || '0'}天/付)\n`;
        }
      } else {
        clientInfo += `📦 <b>关联节点:</b>${event.clients.length} 台\n\n`;
        for (let i = 0; i < event.clients.length; i++) {
          const c = event.clients[i];
          const flag = getFlag(c);
          
          let suffixRegion = '';
          if (c.region) {
            suffixRegion = ` | ${c.region}`;
          } else if (flag) {
            suffixRegion = ` | ${flag}`;
          } else {
            suffixRegion = ` | 🌐`;
          }

          clientInfo += `${i + 1}. <b>${escapeHtml(c.name || "未知节点")}</b>${escapeHtml(suffixRegion)}\n`;
        }
      }
    } else {
      if (eventName !== 'report') {
        clientInfo += `🖥️ <b>服务器:</b>全局系统级事件\n`;
      }
    }

    let message = clientInfo;
    message += `📊 <b>事件级别:</b>${info.level}\n`;
    message += `🕒 <b>北京时间:</b>${getCSTTime(event.time)}`;

    const translatedMessage = translateMessage(event.message);
    if (translatedMessage) {
      message += `\n\n📄 <b>详细描述:</b>\n${escapeHtml(translatedMessage)}`;
    }

    return await sendMessage(message, title, targetInstanceId);
  } catch (error) {
    console.error("事件通知发送异常:", error);
    return await sendMessage(`脚本解析出错: ${escapeHtml(error.message)}`, '❌ Error');
  }
}
  • @shen1e #1
    设置-通知-通知渠道选JavaScript,在JavaScript代码那里填

  • 这个怎么用

  • 贴通知模板里

  • 好东西,拿走学习了。谢谢楼主

  • 先收藏,谢谢哈

你好啊,陌生人!

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

📈用户数目📈

目前论坛共有61574位seeker

🎉欢迎新用户🎉