logo NodeSeekbeta

发一个适配MeoW的Komari通知模板

// ==================== [推送配置] ====================
const MEOW_NICKNAME = "";
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 getCSTTime(timeStr) {
  let date;
  if (!timeStr || typeof timeStr !== 'string' || timeStr.startsWith('0001')) {
    date = new Date();
  } else {
    date = new Date(timeStr.replace(/\.\d+Z$/, 'Z'));
  }
  if (isNaN(date.getTime())) date = new Date(); 
  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 || typeof ip !== 'string') return '未知';
  const parts = ip.split('.');
  if (parts.length === 4) {
    return `${parts[0]}.${parts[1]}.xxx.xxx`;
  }
  const v6Parts = ip.split(':');
  if (v6Parts.length >= 3) {
    return v6Parts.slice(0, 3).join(':') + ':xxxx:xxxx:xxxx';
  }
  return '未知';
}

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 parseBillingDesc(price, currency, cycleStr) {
  if (price === undefined || price === null || price === -1 || String(price).toLowerCase() === 'free') {
    return '🎁 免费';
  }
  const symbol = currency || '$';
  let cycle = String(cycleStr || '').toLowerCase().trim();
  
  if (!cycle || cycle === '0') {
    cycle = '';
  } else if (!isNaN(cycle)) {
    cycle = `/${cycle}天`;
  } else {
    const alias = { 'month': '/月', 'year': '/年', 'quarter': '/季', 'weekly': '/周' };
    cycle = alias[cycle] || `/${cycleStr}`;
  }
  return `${symbol}${price}${cycle}`;
}

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

function getCountryCodeFromClient(client) {
  if (!client) return "";
  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(token => token && token.length === 2);
  for (const token of tokens) {
    if (VALID_COUNTRY_CODES.has(token)) return token;
  }
  return "";
}

function getFlag(client) {
  if (!client) return "";
  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: "报告" }
};

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

  let jumpUrl = PANEL_URL;
  if (instanceId && instanceId !== '未知') {
    jumpUrl = `${PANEL_URL}/instance/${instanceId}`;
  }

  const apiUrl = `https://api.chuckfang.com/${encodeURIComponent(MEOW_NICKNAME)}`;

  try {
    const resp = await fetch(apiUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        title: title,
        msg: message,
        url: jumpUrl,
        imgUrl: "https://avatars.githubusercontent.com/u/208285284?v=4&s=216"
      }),
    });
    const result = await resp.json();
    return resp.ok && result.status === 200;
  } catch (error) {
    console.error("Error:", error);
    return false;
  }
}

// ==================== [主事件解析处理函数] ====================
async function sendEvent(event) {
  if (!event) 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 && Array.isArray(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': '(取最小)', 'up': '(仅上传)', 'down': '(仅下载)'
          };
          trafficCycle = typeMap[typeLower] || `(${c.traffic_limit_type})`;
        }

        clientInfo += `🖥️ 服务器:${flagPrefix}${c.name || "未知节点"}${region}\n`;
        clientInfo += `📝 配 置:${c.cpu_cores || '0'}C / ${mem}${swap !== '0' ? `+${swap}` : ''} / ${disk}\n`;
        clientInfo += `🌐 IPv4:${hiddenIPv4}\n`;
        clientInfo += `🌐 IPv6:${hiddenIPv6}\n`;
        clientInfo += `📶 流量限额:${trafficLimit}${trafficCycle}\n`;
        
        const billingDesc = parseBillingDesc(c.price, c.currency, c.billing_cycle);
        clientInfo += `💰 账 单:${billingDesc}\n`;
        
        if (c.uuid) {
          clientInfo += `🔗 链接:${PANEL_URL}/instance/${c.uuid}\n`;
        }

      } else {
        const totalClients = event.clients.length;
        clientInfo += `📦 关联节点:${totalClients} 台受影响\n`;
        
        const displayCount = Math.min(totalClients, 3);
        
        for (let i = 0; i < totalClients; i++) {
          const c = event.clients[i];
          if (!c) continue;
          
          if (i < displayCount) {
            const flag = getFlag(c);
            const flagPrefix = flag ? `${flag} ` : '';
            const region = c.region ? ` [${c.region}]` : '';
            
            const mem = c.mem_total ? formatMemory(c.mem_total) : '0';
            const disk = c.disk_total ? formatMemory(c.disk_total) : '0';
            
            const billingDesc = parseBillingDesc(c.price, c.currency, c.billing_cycle);

            clientInfo += `\n┌── 🖥️ ${i + 1}. ${flagPrefix}${c.name || "未知节点"}${region}`;
            clientInfo += `\n│   ⚙️ 配置: ${c.cpu_cores || '0'}C / ${mem} / ${disk}`;
            clientInfo += `\n└── 💰 账单: ${billingDesc}`;
            
            if (c.uuid) {
              clientInfo += `\n🔗 链接: ${PANEL_URL}/instance/${c.uuid}\n`;
            } else {
              clientInfo += `\n`;
            }
          }
        }

        if (totalClients > 3) {
          clientInfo += `\n📌 仅展示前 3 台核心卡片,其余 ${totalClients - 3} 台请前往面板查看。\n`;
        }
      }
    } else {
      if (eventName !== 'report') {
        clientInfo += `🖥️ 服务器:全局系统级事件\n`;
      }
    }

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

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

    return await sendMessage(message, title, targetInstanceId);
  } catch (error) {
    console.error("Error:", error);
    
    const fallbackTitle = '⚠️ 通知 - 系统异常';
    const fallbackMessage = `🚨 解析脚本出错\n\n📢 原始事件: ${event.event || '未知'}\n📅 发生时间: ${getCSTTime(event.time)}\n📝 错误详情: ${error.message}`;

    try {
      return await sendMessage(fallbackMessage, fallbackTitle);
    } catch (fallbackError) {
      return false;
    }
  }
}

m8tyGoLsX8Axy34H0OY8X6NephtxiD1M.png

  • 我用上了 xhj003

你好啊,陌生人!

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

📈用户数目📈

目前论坛共有63124位seeker

🎉欢迎新用户🎉