Komari 中文 Telegram 通知模板 v3 —— 全量支持官方 11 种事件,纯中文文案

这个模板解决了什么问题

  • 覆盖官方全部 11 种事件类型:offline / online / expire / renew / login / traffic / alert / dreport / wreport / mreport / test,每种都有独立的图标和标题
  • 英文文案自动翻译成中文,同时保留 CPU / RAM / IP / GB 等专有名词不被误翻
  • Alert 告警智能识别指标:从规则名称里判断是 CPU / 内存 / 硬盘 / 网络入站 / 网络出站中的哪一种,识别不出来时兜底显示通用告警,不会报错
  • 到期 / 续费信息结构化解析:自动把 名称 (7d)名称 until 日期 这类原始格式,转换成「剩余 N 天」「已到期 N 天」「续费至 XX」这样的自然语言
  • 登录通知详细拆解:登录方式、IP、地区、设备信息分行展示,方便一眼确认是不是本人操作
  • 多服务器场景友好:单台服务器和多台服务器(比如批量到期提醒)会自动切换不同的排版
  • 超长消息自动分段,避免触发 Telegram 4096 字符限制,还处理了不切断 Emoji / HTML 实体的细节
  • 限速重试:遇到 Telegram 429 限流会自动等待重试,不会白白丢消息
  • 统一使用北京时间展示触发时间

使用方法

  1. 复制下方完整代码
  2. 修改开头三处配置:
var BOT_TOKEN = "填写你的 Telegram 机器人令牌";
var CHAT_ID = "填写你的 Telegram 聊天 ID";
var PANEL_URL = "https://填写你的Komari面板域名";

  1. 粘贴到 Komari 面板的通知脚本设置里保存即可,sendMessagesendEvent 是固定入口函数名,不要改动
  2. 建议先在面板里点一下「测试通知」,确认能正常收到消息

效果举例

  • 服务器离线::red_circle: 服务器离线 + 触发时间(北京时间)
  • CPU 告警::fire: CPU 使用率告警,自动标出告警规则名称
  • 到期提醒::hourglass_not_done: 服务器到期提醒,自动算出「剩余 X 天」,多台服务器会列表展示
  • 面板登录::key: 登录提醒,登录方式 / IP / 地区 / 设备分行显示

完整代码

/*
 * Komari Telegram 中文通知模板 v3
 * 支持官方 11 种事件;Alert 无法确认指标时显示通用告警。
 * sendMessage 和 sendEvent 是固定入口,请勿改名。
 */


// 请填写以下配置

var BOT_TOKEN = "填写你的 Telegram 机器人令牌";

var CHAT_ID = "填写你的 Telegram 聊天 ID";

var PANEL_URL = "https://填写你的Komari面板域名";


// 基础工具

function escapeHtml(value) {
  return String(value == null ? "" : value)
    .replace(/&/g, "&")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;");
}

function padNumber(value) {
  return value < 10 ? "0" + value : String(value);
}

function formatBeijingTime(value) {
  var date = value ? new Date(value) : new Date();

  if (isNaN(date.getTime())) {
    date = new Date();
  }

  var beijingDate = new Date(
    date.getTime() + 8 * 60 * 60 * 1000
  );

  return (
    beijingDate.getUTCFullYear() +
    "-" +
    padNumber(beijingDate.getUTCMonth() + 1) +
    "-" +
    padNumber(beijingDate.getUTCDate()) +
    " " +
    padNumber(beijingDate.getUTCHours()) +
    ":" +
    padNumber(beijingDate.getUTCMinutes()) +
    ":" +
    padNumber(beijingDate.getUTCSeconds())
  );
}

function normalizeEventType(value) {
  return String(value == null ? "" : value)
    .trim()
    .toLowerCase()
    .replace(/[\s_-]+/g, "");
}

function isExactTitle(value, rules) {
  var text = String(value == null ? "" : value).trim();
  var i;

  for (i = 0; i < rules.length; i++) {
    if (rules[i].test(text)) {
      return true;
    }
  }

  return false;
}

function isTestNotification(eventName, title, message) {
  var eventType = normalizeEventType(eventName);

  if (eventType) {
    return eventType === "test";
  }

  if (
    isExactTitle(title, [
      /^test(?: notification)?$/i,
      /^notification test$/i,
      /^(?:Komari\s*)?(?:通知测试|测试通知)$/i
    ])
  ) {
    return true;
  }

  return isExactTitle(message, [
    /^this is a test message from komari[.!。]?$/i,
    /^this is a notification test from komari[.!。]?$/i,
    /^test message from komari[.!。]?$/i,
    /^komari test notification[.!。]?$/i,
    /^这是一条.*(?:测试消息|通知测试消息)[。.]?$/i
  ]);
}

function isExpireNotification(eventName, title, message) {
  var eventType = normalizeEventType(eventName);

  if (eventType) {
    return eventType === "expire";
  }

  if (
    isExactTitle(title, [
      /^(?:server\s*)?(?:expire|expired|expiry|expiration)(?: notification| reminder)?$/i,
      /^(?:服务器|服务)?(?:到期|过期)(?:通知|提醒)?$/i
    ])
  ) {
    return true;
  }

  return isExactTitle(message, [
    /^(?:server|client|node)\s+(?:will expire soon|is about to expire|has expired|is expired)[.!。]?$/i,
    /^(?:服务器|节点).*(?:即将到期|已经到期|已过期)[。.]?$/i
  ]);
}

function isRenewNotification(eventName, title, message) {
  var eventType = normalizeEventType(eventName);

  if (eventType) {
    return eventType === "renew";
  }

  if (
    isExactTitle(title, [
      /^(?:server\s*)?(?:renew|renewal|renewed)(?: notification)?$/i,
      /^(?:服务器)?续费(?:通知)?$/i
    ])
  ) {
    return true;
  }

  return isExactTitle(message, [
    /^(?:server|client|node)\s+(?:renewed successfully|has been renewed)[.!。]?$/i,
    /^(?:服务器|节点).*(?:续费成功|已经续费)[。.]?$/i
  ]);
}


// 文案翻译

function hasUntranslatedEnglish(text) {
  var checkText = String(text == null ? "" : text);

  checkText = checkText
    .replace(/Komari/gi, "")
    .replace(/CPU/gi, "")
    .replace(/RAM/gi, "")
    .replace(/IPv4/gi, "")
    .replace(/IPv6/gi, "")
    .replace(/\bIP\b/gi, "")
    .replace(/\bKB\b/gi, "")
    .replace(/\bMB\b/gi, "")
    .replace(/\bGB\b/gi, "")
    .replace(/\bTB\b/gi, "")
    .replace(/\bKbps\b/gi, "")
    .replace(/\bMbps\b/gi, "")
    .replace(/\bGbps\b/gi, "");

  return /[A-Za-z]{2,}/.test(checkText);
}


function translateMessage(value) {
  var text = String(value == null ? "" : value).trim();

  if (!text) {
    return "";
  }

  var fullSentenceRules = [
    [
      /^this is a test message from komari[.!。]?$/i,
      "这是一条来自 Komari 的测试消息。"
    ],
    [
      /^this is a notification test from komari[.!。]?$/i,
      "这是一条来自 Komari 的通知测试消息。"
    ],
    [
      /^test message from komari[.!。]?$/i,
      "这是一条来自 Komari 的测试消息。"
    ],
    [
      /^komari test notification[.!。]?$/i,
      "这是一条 Komari 通知测试消息。"
    ],
    [
      /^notification test successful[.!。]?$/i,
      "通知测试成功。"
    ],
    [
      /^test notification sent successfully[.!。]?$/i,
      "测试通知发送成功。"
    ],
    [
      /^the notification channel is working correctly[.!。]?$/i,
      "通知渠道运行正常。"
    ]
  ];

  var i;

  for (i = 0; i < fullSentenceRules.length; i++) {
    if (fullSentenceRules[i][0].test(text)) {
      return fullSentenceRules[i][1];
    }
  }

  var rules = [
    [/this is a test message from komari/gi, "这是一条来自 Komari 的测试消息"],
    [/this is a notification test from komari/gi, "这是一条来自 Komari 的通知测试消息"],
    [/test notification/gi, "通知测试"],
    [/notification test/gi, "通知测试"],
    [/test message/gi, "测试消息"],
    [/from komari/gi, "来自 Komari"],
    [/this is a/gi, "这是一条"],

    [/\bserver is offline\b/gi, "服务器已经离线"],
    [/\bserver went offline\b/gi, "服务器已经离线"],
    [/\bserver offline\b/gi, "服务器离线"],
    [/\bclient is offline\b/gi, "服务器已经离线"],
    [/\bclient went offline\b/gi, "服务器已经离线"],
    [/\bclient offline\b/gi, "服务器离线"],
    [/\bnode is offline\b/gi, "节点已经离线"],
    [/\bnode went offline\b/gi, "节点已经离线"],
    [/\bnode offline\b/gi, "节点离线"],
    [/\bconnection lost\b/gi, "连接已经断开"],
    [/\blost connection\b/gi, "连接已经断开"],

    [/\bserver is online\b/gi, "服务器已经上线"],
    [/\bserver came online\b/gi, "服务器已经上线"],
    [/\bserver online\b/gi, "服务器上线"],
    [/\bclient is online\b/gi, "服务器已经上线"],
    [/\bclient came online\b/gi, "服务器已经上线"],
    [/\bclient online\b/gi, "服务器上线"],
    [/\bnode is online\b/gi, "节点已经上线"],
    [/\bnode came online\b/gi, "节点已经上线"],
    [/\bnode online\b/gi, "节点上线"],
    [/\bconnection restored\b/gi, "连接已经恢复"],

    [/cpu utilization rate/gi, "CPU 使用率"],
    [/cpu utilization/gi, "CPU 使用率"],
    [/cpu usage rate/gi, "CPU 使用率"],
    [/cpu usage/gi, "CPU 使用率"],
    [/processor utilization/gi, "处理器使用率"],
    [/processor usage/gi, "处理器使用率"],

    [/memory utilization rate/gi, "内存使用率"],
    [/memory utilization/gi, "内存使用率"],
    [/memory usage rate/gi, "内存使用率"],
    [/memory usage/gi, "内存使用率"],
    [/ram utilization/gi, "内存使用率"],
    [/ram usage/gi, "内存使用率"],
    [/available memory/gi, "可用内存"],
    [/used memory/gi, "已用内存"],
    [/free memory/gi, "空闲内存"],
    [/total memory/gi, "内存总量"],

    [/disk utilization rate/gi, "硬盘使用率"],
    [/disk utilization/gi, "硬盘使用率"],
    [/disk usage rate/gi, "硬盘使用率"],
    [/disk usage/gi, "硬盘使用率"],
    [/storage utilization/gi, "存储使用率"],
    [/storage usage/gi, "存储使用率"],
    [/available disk space/gi, "可用硬盘空间"],
    [/free disk space/gi, "空闲硬盘空间"],
    [/used disk space/gi, "已用硬盘空间"],
    [/total disk space/gi, "硬盘总空间"],
    [/disk space/gi, "硬盘空间"],

    [/net in/gi, "网络入站"],
    [/net out/gi, "网络出站"],
    [/download speed/gi, "下载速度"],
    [/upload speed/gi, "上传速度"],
    [/network speed/gi, "网络速度"],
    [/download traffic/gi, "下载流量"],
    [/upload traffic/gi, "上传流量"],
    [/network traffic/gi, "网络流量"],
    [/total traffic/gi, "总流量"],
    [/incoming traffic/gi, "入站流量"],
    [/outgoing traffic/gi, "出站流量"],

    [/expiration date/gi, "到期时间"],
    [/expiry date/gi, "到期时间"],
    [/expiration time/gi, "到期时间"],
    [/expiry time/gi, "到期时间"],
    [/expiration reminder/gi, "到期提醒"],
    [/expiry reminder/gi, "到期提醒"],
    [/will expire soon/gi, "即将到期"],
    [/is about to expire/gi, "即将到期"],
    [/has expired/gi, "已经到期"],
    [/is expired/gi, "已经到期"],
    [/expired/gi, "已到期"],
    [/renewed successfully/gi, "续费成功"],
    [/renewal successful/gi, "续费成功"],
    [/renewal notification/gi, "续费通知"],
    [/renewed/gi, "已经续费"],

    [/current value/gi, "当前值"],
    [/current usage/gi, "当前使用率"],
    [/exceeded the configured threshold/gi, "超过设定阈值"],
    [/exceeds the configured threshold/gi, "超过设定阈值"],
    [/exceeded the threshold/gi, "超过设定阈值"],
    [/exceeds the threshold/gi, "超过设定阈值"],
    [/exceeded threshold/gi, "超过设定阈值"],
    [/exceeds threshold/gi, "超过设定阈值"],
    [/above threshold/gi, "高于设定阈值"],
    [/below threshold/gi, "低于设定阈值"],
    [/within normal range/gi, "处于正常范围"],
    [/threshold value/gi, "告警阈值"],
    [/alert threshold/gi, "告警阈值"],
    [/warning threshold/gi, "警告阈值"],
    [/threshold/gi, "阈值"],

    [/\bserver name\b/gi, "服务器名称"],
    [/\bclient name\b/gi, "服务器名称"],
    [/\bnode name\b/gi, "节点名称"],
    [/\bhost name\b/gi, "主机名称"],
    [/\bhostname\b/gi, "主机名称"],
    [/\bservers?\b/gi, "服务器"],
    [/\bclients?\b/gi, "客户端"],
    [/\bnodes?\b/gi, "节点"],
    [/\bregions?\b/gi, "地区"],
    [/\blocations?\b/gi, "位置"],
    [/\bcountr(?:y|ies)\b/gi, "国家或地区"],
    [/\bstatus\b/gi, "状态"],
    [/\bdetails\b/gi, "详细信息"],
    [/\bdetail\b/gi, "详情"],
    [/\bdescriptions?\b/gi, "说明"],
    [/\bmessages?\b/gi, "消息"],
    [/\breasons?\b/gi, "原因"],
    [/\bdurations?\b/gi, "持续时间"],
    [/\buptime\b/gi, "运行时间"],
    [/\bdowntime\b/gi, "离线时间"],
    [/\blast seen\b/gi, "最后在线时间"],
    [/\bcreated at\b/gi, "创建时间"],
    [/\bupdated at\b/gi, "更新时间"],

    [/triggered/gi, "已经触发"],
    [/detected/gi, "已经检测到"],
    [/failed to connect/gi, "连接失败"],
    [/connection failed/gi, "连接失败"],
    [/timed out/gi, "连接超时"],
    [/timeout/gi, "超时"],

    [/\boffline\b/gi, "离线"],
    [/\bonline\b/gi, "在线"],
    [/\bwarning\b/gi, "警告"],
    [/\bcritical\b/gi, "严重告警"],
    [/\berror\b/gi, "错误"],
    [/\bfailed\b/gi, "失败"],
    [/\bfailure\b/gi, "失败"],
    [/\bsuccessfully\b/gi, "成功"],
    [/\bsuccessful\b/gi, "成功"],
    [/\bsuccess\b/gi, "成功"],
    [/\bhealthy\b/gi, "运行正常"],
    [/\bunhealthy\b/gi, "运行异常"],
    [/\bhigh\b/gi, "过高"],
    [/\blow\b/gi, "过低"],
    [/\bnormal\b/gi, "正常"],
    [/\babnormal\b/gi, "异常"],
    [/\bunknown\b/gi, "未知"],
    [/\bactive\b/gi, "活跃"],
    [/\binactive\b/gi, "未活跃"],
    [/\benabled\b/gi, "已启用"],
    [/\bdisabled\b/gi, "已禁用"],

    [/\bmilliseconds\b/gi, "毫秒"],
    [/\bmillisecond\b/gi, "毫秒"],
    [/\bms\b/gi, "毫秒"],
    [/\bseconds\b/gi, "秒"],
    [/\bsecond\b/gi, "秒"],
    [/\bminutes\b/gi, "分钟"],
    [/\bminute\b/gi, "分钟"],
    [/\bhours\b/gi, "小时"],
    [/\bhour\b/gi, "小时"],
    [/\bdays\b/gi, "天"],
    [/\bday\b/gi, "天"]
  ];

  for (i = 0; i < rules.length; i++) {
    text = text.replace(rules[i][0], rules[i][1]);
  }

  text = text
    .replace(
      /This is a\s*测试消息\s*from Komari[.!。]?/gi,
      "这是一条来自 Komari 的测试消息。"
    )
    .replace(
      /这是一条\s*测试消息\s*来自 Komari[.!。]?/gi,
      "这是一条来自 Komari 的测试消息。"
    )
    .replace(
      /This is a\s*通知测试\s*from Komari[.!。]?/gi,
      "这是一条来自 Komari 的通知测试消息。"
    )
    .replace(/\s*:\s*/g, ":")
    .replace(/\s*,\s*/g, ",")
    .replace(/\s*;\s*/g, ";")
    .replace(/[.]+$/g, "。")
    .trim();

  return text;
}


// 事件显示

function containsAny(content, values) {
  var i;

  for (i = 0; i < values.length; i++) {
    if (content.indexOf(values[i]) !== -1) {
      return true;
    }
  }

  return false;
}

function matchesAlertRule(content, expression) {
  var text = String(content == null ? "" : content).toLowerCase();

  return Boolean(expression && expression.test(text));
}

// Alert 只提供规则名称,因此仅做保守匹配。
function getAlertInformation(content) {
  if (
    matchesAlertRule(
      content,
      /^\s*(?:net[\s_-]*in|network[\s_-]*(?:in|input)|inbound|incoming|download|网络入站|入站流量|下载速度|下载流量|接收速率)(?:\s*(?:traffic|speed|usage|流量|速率))?(?:\s*(?:(?:is\s+)?(?:high|above|over)|exceed(?:ed|s)?|达到|超过|高于)(?:\s*(?:the\s+)?(?:configured\s+)?threshold)?(?:\s*[:=]?\s*\d+(?:\.\d+)?\s*(?:%|mbps)?)?)?(?:\s*(?:alert|warning|告警|警告))?\s*$/i
    )
  ) {
    return {
      icon: "⬇️",
      title: "网络入站速率告警",
      detail: "服务器网络入站速率触发了告警规则。",
      metric: "net_in",
      metricLabel: "网络入站速率"
    };
  }

  if (
    matchesAlertRule(
      content,
      /^\s*(?:net[\s_-]*out|network[\s_-]*(?:out|output)|outbound|outgoing|upload|网络出站|出站流量|上传速度|上传流量|发送速率)(?:\s*(?:traffic|speed|usage|流量|速率))?(?:\s*(?:(?:is\s+)?(?:high|above|over)|exceed(?:ed|s)?|达到|超过|高于)(?:\s*(?:the\s+)?(?:configured\s+)?threshold)?(?:\s*[:=]?\s*\d+(?:\.\d+)?\s*(?:%|mbps)?)?)?(?:\s*(?:alert|warning|告警|警告))?\s*$/i
    )
  ) {
    return {
      icon: "⬆️",
      title: "网络出站速率告警",
      detail: "服务器网络出站速率触发了告警规则。",
      metric: "net_out",
      metricLabel: "网络出站速率"
    };
  }

  if (
    matchesAlertRule(
      content,
      /^\s*(?:cpu|processor|处理器)(?:\s*(?:usage|utilization|使用率))?(?:\s*(?:(?:is\s+)?(?:high|above|over)|exceed(?:ed|s)?|达到|超过|高于)(?:\s*(?:the\s+)?(?:configured\s+)?threshold)?(?:\s*[:=]?\s*\d+(?:\.\d+)?\s*%?)?)?(?:\s*(?:alert|warning|告警|警告))?\s*$/i
    )
  ) {
    return {
      icon: "🔥",
      title: "CPU 使用率告警",
      detail: "CPU 使用率触发了告警规则。",
      metric: "cpu",
      metricLabel: "CPU 使用率"
    };
  }

  if (
    matchesAlertRule(
      content,
      /^\s*(?:ram|memory|内存)(?:\s*(?:usage|utilization|使用率))?(?:\s*(?:(?:is\s+)?(?:high|above|over)|exceed(?:ed|s)?|达到|超过|高于)(?:\s*(?:the\s+)?(?:configured\s+)?threshold)?(?:\s*[:=]?\s*\d+(?:\.\d+)?\s*%?)?)?(?:\s*(?:alert|warning|告警|警告))?\s*$/i
    )
  ) {
    return {
      icon: "🧠",
      title: "内存使用率告警",
      detail: "内存使用率触发了告警规则。",
      metric: "ram",
      metricLabel: "内存使用率"
    };
  }

  if (
    matchesAlertRule(
      content,
      /^\s*(?:disk|storage|硬盘|磁盘|存储)(?:\s*(?:usage|utilization|使用率))?(?:\s*(?:(?:is\s+)?(?:high|above|over)|exceed(?:ed|s)?|达到|超过|高于)(?:\s*(?:the\s+)?(?:configured\s+)?threshold)?(?:\s*[:=]?\s*\d+(?:\.\d+)?\s*%?)?)?(?:\s*(?:alert|warning|告警|警告))?\s*$/i
    )
  ) {
    return {
      icon: "💾",
      title: "硬盘使用率告警",
      detail: "硬盘使用率触发了告警规则。",
      metric: "disk",
      metricLabel: "硬盘使用率"
    };
  }

  return {
    icon: "⚠️",
    title: "服务器状态告警",
    detail: "服务器触发了一项负载告警。",
    metric: "",
    metricLabel: ""
  };
}

function getEventInformation(eventName, title, message) {
  var eventType = normalizeEventType(eventName);

  var content = (
    String(title == null ? "" : title) +
    " " +
    String(message == null ? "" : message)
  ).toLowerCase();

  if (isTestNotification(eventName, title, message)) {
    return {
      icon: "🧪",
      title: "Komari 通知测试",
      detail: "通知渠道连接正常,测试消息发送成功。",
      isTest: true
    };
  }

  if (eventType === "offline") {
    return {
      icon: "🔴",
      title: "服务器离线",
      detail: "服务器连接已经断开,请及时检查运行状态。"
    };
  }

  if (eventType === "online") {
    return {
      icon: "🟢",
      title: "服务器上线",
      detail: "服务器已经恢复连接并正常上线。"
    };
  }

  if (isExpireNotification(eventName, title, message)) {
    return {
      icon: "⏳",
      title: "服务器到期提醒",
      detail: "服务器即将到期,请及时检查续费状态。"
    };
  }

  if (isRenewNotification(eventName, title, message)) {
    return {
      icon: "💳",
      title: "服务器续费通知",
      detail: "服务器续费状态已经更新。"
    };
  }

  if (eventType === "login") {
    return {
      icon: "🔑",
      title: "Komari 登录提醒",
      detail: "检测到一次 Komari 面板登录。"
    };
  }

  if (eventType === "traffic") {
    return {
      icon: "🚦",
      title: "服务器流量告警",
      detail: "服务器流量使用量已经达到提醒阈值。"
    };
  }

  if (eventType === "dreport") {
    return {
      icon: "📊",
      title: "每日流量报告",
      detail: "收到服务器每日流量统计。"
    };
  }

  if (eventType === "wreport") {
    return {
      icon: "📈",
      title: "每周流量报告",
      detail: "收到服务器每周流量统计。"
    };
  }

  if (eventType === "mreport") {
    return {
      icon: "📅",
      title: "每月流量报告",
      detail: "收到服务器每月流量统计。"
    };
  }

  if (eventType === "alert") {
    return getAlertInformation(message);
  }

  if (
    /\boffline\b/i.test(content) ||
    content.indexOf("离线") !== -1
  ) {
    return {
      icon: "🔴",
      title: "服务器离线",
      detail: "服务器连接已经断开,请及时检查运行状态。"
    };
  }

  if (
    /\bonline\b/i.test(content) ||
    content.indexOf("上线") !== -1 ||
    content.indexOf("connection restored") !== -1
  ) {
    return {
      icon: "🟢",
      title: "服务器上线",
      detail: "服务器已经恢复连接并正常上线。"
    };
  }

  if (
    containsAny(content, [
      "cpu",
      "memory",
      "ram",
      "内存",
      "disk",
      "storage",
      "硬盘",
      "存储",
      "net in",
      "net out",
      "network in",
      "network out",
      "inbound",
      "outbound",
      "入站",
      "出站",
      "上传",
      "下载",
      "alert",
      "warning",
      "critical",
      "告警",
      "警告"
    ])
  ) {
    return getAlertInformation(content);
  }

  return {
    icon: "🔔",
    title: "Komari 系统通知",
    detail: "收到一条服务器状态通知。"
  };
}


// 解析 Expire 的“• 名称 (7d)”格式

function parseExpiryClientsFromMessage(message) {
  var text = String(message == null ? "" : message);
  var lines = text.split(/\r?\n/);
  var clients = [];
  var seen = {};
  var i;

  for (i = 0; i < lines.length; i++) {
    var line = lines[i].trim();

    if (!line) {
      continue;
    }

    var match = line.match(
      /^\s*(?:[•·*+\-]|\d+[.)、])?\s*(.+?)\s*[((]\s*(-?\d+)\s*(?:d|day|days|天)\s*[))]\s*$/i
    );

    if (!match) {
      continue;
    }

    var name = String(match[1] || "").trim();
    var daysLeft = parseInt(match[2], 10);

    if (!name || seen[name]) {
      continue;
    }

    seen[name] = true;

    clients.push({
      name: name,
      days_left: isNaN(daysLeft) ? null : daysLeft,
      parsed_from_expire_message: true
    });
  }

  return clients;
}

function normalizeClientList(value) {
  if (Array.isArray(value)) {
    return value;
  }

  if (value == null || value === "") {
    return [];
  }

  return [value];
}

function getClients(event) {
  if (!event) {
    return [];
  }

  var structuredCandidates = [
    event.clients,
    event.nodes,
    event.servers,
    event.affected_clients,
    event.affectedClients
  ];

  var i;

  for (i = 0; i < structuredCandidates.length; i++) {
    var currentList =
      normalizeClientList(structuredCandidates[i]);

    if (currentList.length > 0) {
      return currentList;
    }
  }

  var singleCandidates = [
    event.client,
    event.node,
    event.server,
    event.client_name,
    event.clientName,
    event.node_name,
    event.nodeName,
    event.server_name,
    event.serverName,
    event.hostname,
    event.host
  ];

  for (i = 0; i < singleCandidates.length; i++) {
    if (
      singleCandidates[i] != null &&
      singleCandidates[i] !== ""
    ) {
      return normalizeClientList(singleCandidates[i]);
    }
  }

  // Expire 没有 Clients 时从 message 回退解析。
  var eventName =
    event.event ||
    event.type ||
    event.name ||
    "";

  var eventTitle =
    event.title ||
    "";

  var originalMessage =
    getOriginalMessage(event);

  if (
    isExpireNotification(
      eventName,
      eventTitle,
      originalMessage
    )
  ) {
    var parsedClients =
      parseExpiryClientsFromMessage(originalMessage);

    if (parsedClients.length > 0) {
      return parsedClients;
    }
  }

  return [];
}


// 服务器信息

function getClientName(client) {
  if (
    typeof client === "string" ||
    typeof client === "number"
  ) {
    var directName = String(client).trim();
    return directName || "未知服务器";
  }

  if (!client) {
    return "未知服务器";
  }

  return (
    client.name ||
    client.client_name ||
    client.clientName ||
    client.node_name ||
    client.nodeName ||
    client.server_name ||
    client.serverName ||
    client.hostname ||
    client.host ||
    client.uuid ||
    "未知服务器"
  );
}

function getClientRegion(client) {
  if (!client || typeof client !== "object") {
    return "";
  }

  return (
    client.region ||
    client.location ||
    client.country ||
    client.area ||
    ""
  );
}

function getClientDaysLeft(client) {
  if (!client || typeof client !== "object") {
    return null;
  }

  var value =
    client.days_left != null
      ? client.days_left
      : client.daysLeft != null
        ? client.daysLeft
        : null;

  if (value == null || value === "") {
    return null;
  }

  var number = parseInt(value, 10);

  return isNaN(number) ? null : number;
}

function getClientExpiredAt(client) {
  if (!client || typeof client !== "object") {
    return null;
  }

  return (
    client.expired_at ||
    client.expiredAt ||
    client.expire_at ||
    client.expireAt ||
    null
  );
}

function buildExpirySuffix(client, showExpiry) {
  if (!showExpiry) {
    return "";
  }

  var daysLeft = getClientDaysLeft(client);

  if (daysLeft != null) {
    if (daysLeft < 0) {
      return " · 已到期 " + Math.abs(daysLeft) + " 天";
    }

    if (daysLeft === 0) {
      return " · 今天到期";
    }

    return " · 剩余 " + daysLeft + " 天";
  }

  var expiredAt = getClientExpiredAt(client);

  if (expiredAt) {
    return (
      " · 到期时间 " +
      formatBeijingTime(expiredAt)
    );
  }

  return "";
}

function buildClientInformation(clients, showExpiry) {
  var text = "";

  if (!clients || clients.length === 0) {
    return "";
  }

  if (clients.length === 1) {
    var client = clients[0] || {};
    var name = getClientName(client);
    var region = getClientRegion(client);

    text +=
      "🖥 <b>服务器</b>:" +
      escapeHtml(name) +
      "\n";

    if (region) {
      text +=
        "🌍 <b>所在地区</b>:" +
        escapeHtml(region) +
        "\n";
    }

    var singleExpirySuffix =
      buildExpirySuffix(client, showExpiry);

    if (singleExpirySuffix) {
      text +=
        "⏳ <b>到期状态</b>:" +
        escapeHtml(
          singleExpirySuffix.replace(/^\s*·\s*/, "")
        ) +
        "\n";
    }

    return text;
  }

  text +=
    "🖥 <b>影响服务器</b>:" +
    clients.length +
    " 台\n";

  var i;

  for (i = 0; i < clients.length; i++) {
    var currentClient = clients[i] || {};
    var currentName = getClientName(currentClient);
    var currentRegion = getClientRegion(currentClient);

    text +=
      " • " +
      escapeHtml(currentName);

    if (currentRegion) {
      text +=
        " · " +
        escapeHtml(currentRegion);
    }

    text += escapeHtml(
      buildExpirySuffix(currentClient, showExpiry)
    );

    text += "\n";
  }

  return text;
}


// 事件数据

function getOriginalMessage(event) {
  if (!event) {
    return "";
  }

  return (
    event.message ||
    event.description ||
    event.detail ||
    event.details ||
    event.reason ||
    ""
  );
}

function getEventTime(event) {
  if (!event) {
    return null;
  }

  return (
    event.time ||
    event.timestamp ||
    event.created_at ||
    event.createdAt ||
    null
  );
}

function escapeRegExp(value) {
  return String(value == null ? "" : value)
    .replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function protectClientNames(text, clients) {
  var protectedText =
    String(text == null ? "" : text);

  var names = [];
  var seen = {};
  var replacements = [];
  var i;

  for (
    i = 0;
    clients && i < clients.length;
    i++
  ) {
    var name =
      String(getClientName(clients[i]) || "").trim();

    if (
      !name ||
      name === "未知服务器" ||
      seen[name]
    ) {
      continue;
    }

    seen[name] = true;
    names.push(name);
  }

  names.sort(function (a, b) {
    return b.length - a.length;
  });

  for (i = 0; i < names.length; i++) {
    var token = "\uE000" + i + "\uE001";
    var pattern = new RegExp(
      escapeRegExp(names[i]),
      "gi"
    );

    if (protectedText.search(pattern) === -1) {
      continue;
    }

    protectedText =
      protectedText.replace(pattern, token);

    replacements.push({
      token: token,
      value: names[i]
    });
  }

  return {
    text: protectedText,
    replacements: replacements
  };
}

function restoreClientNames(text, replacements) {
  var result = String(text == null ? "" : text);
  var i;

  for (
    i = 0;
    replacements && i < replacements.length;
    i++
  ) {
    result = result
      .split(replacements[i].token)
      .join(replacements[i].value);
  }

  return result;
}

function translateLoginMethod(value) {
  var method =
    String(value == null ? "" : value).trim();

  var normalized = normalizeEventType(method);

  var methodMap = {
    password: "密码登录",
    passwd: "密码登录",
    local: "本地账户登录",
    github: "GitHub 登录",
    google: "Google 登录",
    oidc: "OIDC 登录",
    oauth: "OAuth 登录",
    email: "邮箱登录"
  };

  return (
    methodMap[normalized] ||
    method ||
    "未知方式"
  );
}

function buildLoginDetail(originalMessage) {
  var text =
    String(
      originalMessage == null ? "" : originalMessage
    ).trim();

  if (!text) {
    return "检测到一次 Komari 面板登录。";
  }

  var lines = text.split(/\r?\n/);
  var firstLine =
    String(lines.shift() || "").trim();

  var match = firstLine.match(
    /^([^::]+)[::]\s*(.+?)\s*[((](.*)[))]\s*$/
  );

  if (!match) {
    return "检测到一次 Komari 面板登录,请及时核对是否为本人操作。";
  }

  var method = translateLoginMethod(match[1]);
  var address = String(match[2] || "").trim();
  var location = String(match[3] || "").trim();
  var userAgent = lines.join(" ").trim();

  var detail =
    "登录方式:" +
    method;

  if (address) {
    detail +=
      "\n登录 IP:" +
      address;
  }

  if (
    location &&
    normalizeEventType(location) !== "unknown"
  ) {
    detail +=
      "\n登录地区:" +
      location;
  } else {
    detail += "\n登录地区:未知";
  }

  if (userAgent) {
    detail +=
      "\n设备信息:" +
      userAgent;
  }

  return detail;
}

// 解析 Renew 的“• 名称 until YYYY-MM-DD”格式
function buildRenewDetail(originalMessage, clients) {
  var text =
    String(
      originalMessage == null ? "" : originalMessage
    ).trim();

  if (!text) {
    return "服务器自动续费状态已经更新。";
  }

  var lines = text.split(/\r?\n/);
  var renewals = [];
  var i;

  for (i = 0; i < lines.length; i++) {
    var match = String(lines[i] || "").match(
      /^\s*(?:[•·*+\-]|\d+[.)、])?\s*(.+?)\s+until\s+(\d{4}-\d{2}-\d{2})\s*$/i
    );

    if (!match) {
      continue;
    }

    var parsedName = String(match[1] || "").trim();
    var knownName = "";
    var clientIndex;

    for (
      clientIndex = 0;
      clients && clientIndex < clients.length;
      clientIndex++
    ) {
      var candidateName = getClientName(clients[clientIndex]);

      if (
        candidateName &&
        (
          parsedName === candidateName ||
          parsedName.indexOf(candidateName + " ") === 0
        )
      ) {
        knownName = candidateName;
        break;
      }
    }

    renewals.push({
      name: knownName || parsedName,
      date: String(match[2] || "").trim()
    });
  }

  if (renewals.length === 1) {
    return (
      "服务器 " +
      renewals[0].name +
      " 已自动续费,新到期日期:" +
      renewals[0].date +
      "。"
    );
  }

  if (renewals.length > 1) {
    var detail = "以下服务器已自动续费:";

    for (i = 0; i < renewals.length; i++) {
      detail +=
        "\n• " +
        renewals[i].name +
        ":续费至 " +
        renewals[i].date;
    }

    return detail;
  }

  return (
    "服务器自动续费状态已经更新。" +
    "\n原始信息:" +
    text
  );
}

function buildAlertDetail(eventInformation, originalMessage) {
  var ruleName =
    String(
      originalMessage == null ? "" : originalMessage
    ).trim();

  if (!ruleName) {
    return eventInformation.detail;
  }

  var simpleOfficialName = /^(?:cpu|processor|ram|memory|disk|storage|net[\s_-]*(?:in|out)|network[\s_-]*(?:in|out|input|output)|inbound|outbound|incoming|outgoing|download|upload)(?:\s+(?:usage|utilization|traffic|speed|alert|warning))?$/i;

  if (
    eventInformation.metricLabel &&
    simpleOfficialName.test(ruleName)
  ) {
    ruleName = eventInformation.metricLabel;
  }

  var detail = eventInformation.metricLabel
    ? "告警类型:" + eventInformation.metricLabel + "\n"
    : "";

  detail += "告警规则:" + ruleName;

  return detail;
}

function getTrafficTypeName(value) {
  var type = normalizeEventType(value);

  var typeMap = {
    sum: "上传与下载总和",
    max: "上传和下载中的较大值",
    min: "上传和下载中的较小值",
    up: "上传流量",
    upload: "上传流量",
    down: "下载流量",
    download: "下载流量"
  };

  return (
    typeMap[type] ||
    "面板设定方式"
  );
}

function buildTrafficDetail(originalMessage) {
  var text =
    String(
      originalMessage == null ? "" : originalMessage
    ).trim();

  var match = text.match(
    /used\s+([\d.]+)%\s*[((]\s*([^/]+?)\s*\/\s*([^))]+?)\s*[))]\s*[,,]?\s*type\s*=\s*([a-z]+)/i
  );

  if (!match) {
    return "服务器流量使用量已经达到提醒阈值。";
  }

  return (
    "流量已使用 " +
    match[1] +
    "%(" +
    String(match[2]).trim() +
    " / " +
    String(match[3]).trim() +
    "),统计方式:" +
    getTrafficTypeName(match[4]) +
    "。"
  );
}

function buildChineseDetail(
  eventInformation,
  originalMessage,
  eventName,
  eventTitle,
  clients
) {
  var eventType =
    normalizeEventType(eventName);

  if (
    isExpireNotification(
      eventName,
      eventTitle,
      originalMessage
    )
  ) {
    if (clients && clients.length > 1) {
      return (
        "检测到 " +
        clients.length +
        " 台服务器即将到期,请及时检查续费状态。"
      );
    }

    if (clients && clients.length === 1) {
      var daysLeft =
        getClientDaysLeft(clients[0]);

      if (daysLeft != null) {
        if (daysLeft < 0) {
          return "该服务器已经到期,请及时检查续费状态。";
        }

        if (daysLeft === 0) {
          return "该服务器将在今天到期,请及时检查续费状态。";
        }

        return (
          "该服务器将在 " +
          daysLeft +
          " 天后到期,请及时检查续费状态。"
        );
      }
    }

    var unparsedExpireMessage =
      String(
        originalMessage == null ? "" : originalMessage
      ).trim();

    return unparsedExpireMessage
      ? eventInformation.detail + "\n原始信息:" + unparsedExpireMessage
      : eventInformation.detail;
  }

  if (
    isRenewNotification(
      eventName,
      eventTitle,
      originalMessage
    )
  ) {
    return buildRenewDetail(
      originalMessage,
      clients || []
    );
  }

  if (eventType === "login") {
    return buildLoginDetail(originalMessage);
  }

  if (eventType === "alert") {
    return buildAlertDetail(
      eventInformation,
      originalMessage
    );
  }

  if (eventType === "traffic") {
    return buildTrafficDetail(originalMessage);
  }

  if (
    eventType === "dreport" ||
    eventType === "wreport" ||
    eventType === "mreport"
  ) {
    return (
      String(
        originalMessage == null
          ? ""
          : originalMessage
      ).trim() ||
      eventInformation.detail
    );
  }

  var protectedMessage =
    protectClientNames(
      originalMessage,
      clients || []
    );

  var translatedMessage =
    translateMessage(protectedMessage.text);

  if (
    !translatedMessage ||
    hasUntranslatedEnglish(translatedMessage)
  ) {
    return eventInformation.detail;
  }

  return restoreClientNames(
    translatedMessage,
    protectedMessage.replacements
  );
}


// Telegram 发送

function splitLongTelegramLine(line, maxLength) {
  var result = [];

  var remaining =
    String(line == null ? "" : line)
      .replace(/<b>/gi, "")
      .replace(/<\/b>/gi, "");

  while (remaining.length > maxLength) {
    var cutAt = maxLength;

    // 避免切断 Emoji。
    if (
      cutAt > 0 &&
      cutAt < remaining.length &&
      /[\uD800-\uDBFF]/.test(remaining.charAt(cutAt - 1)) &&
      /[\uDC00-\uDFFF]/.test(remaining.charAt(cutAt))
    ) {
      cutAt--;
    }

    if (cutAt <= 0) {
      cutAt = Math.min(remaining.length, maxLength + 1);
    }

    var candidate =
      remaining.slice(0, cutAt);

    var lastAmpersand =
      candidate.lastIndexOf("&");

    var lastSemicolon =
      candidate.lastIndexOf(";");

    // 避免切断 HTML 实体。
    if (
      lastAmpersand > lastSemicolon &&
      lastAmpersand > 0
    ) {
      cutAt = lastAmpersand;
    }

    result.push(
      remaining.slice(0, cutAt)
    );

    remaining =
      remaining.slice(cutAt);
  }

  result.push(remaining);

  return result;
}

function splitTelegramHtmlMessage(text, maxLength) {
  var limit = maxLength || 3800;

  var sourceLines =
    String(text == null ? "" : text)
      .split("\n");

  var lines = [];
  var chunks = [];
  var current = "";
  var i;

  for (i = 0; i < sourceLines.length; i++) {
    var expandedLines =
      sourceLines[i].length > limit
        ? splitLongTelegramLine(
            sourceLines[i],
            limit
          )
        : [sourceLines[i]];

    var j;

    for (j = 0; j < expandedLines.length; j++) {
      lines.push(expandedLines[j]);
    }
  }

  for (i = 0; i < lines.length; i++) {
    var next =
      current
        ? current + "\n" + lines[i]
        : lines[i];

    if (next.length <= limit) {
      current = next;
      continue;
    }

    if (current) {
      chunks.push(current);
    }

    current = lines[i];
  }

  if (current || chunks.length === 0) {
    chunks.push(current);
  }

  return chunks;
}

function getPanelReplyMarkup() {
  if (
    PANEL_URL &&
    PANEL_URL.indexOf("填写") === -1 &&
    (
      PANEL_URL.indexOf("https://") === 0 ||
      PANEL_URL.indexOf("http://") === 0
    )
  ) {
    return {
      inline_keyboard: [
        [
          {
            text: "🖥 查看 Komari 面板",
            url: PANEL_URL
          }
        ]
      ]
    };
  }

  return null;
}

function waitMilliseconds(milliseconds) {
  return new Promise(function(resolve) {
    setTimeout(resolve, Math.max(0, milliseconds || 0));
  });
}

async function sendTelegramChunk(
  text,
  showPanelButton
) {
  var requestBody = {
    chat_id: String(CHAT_ID),
    text: text,
    parse_mode: "HTML",
    disable_web_page_preview: true
  };

  var replyMarkup =
    showPanelButton
      ? getPanelReplyMarkup()
      : null;

  if (replyMarkup) {
    requestBody.reply_markup = replyMarkup;
  }

  var attempt;

  for (attempt = 0; attempt < 2; attempt++) {
    try {
      var response = await fetch(
        "https://api.telegram.org/bot" +
        BOT_TOKEN +
        "/sendMessage",
        {
          method: "POST",
          headers: {
            "Content-Type": "application/json"
          },
          body: JSON.stringify(requestBody)
        }
      );

      var responseText =
        await response.text();

      var responseData;

      try {
        responseData =
          JSON.parse(responseText);
      } catch (parseError) {
        console.error(
          "无法解析 Telegram 返回内容:" +
          responseText
        );

        return false;
      }

      if (
        response.ok &&
        responseData.ok
      ) {
        return true;
      }

      var isRateLimited =
        response.status === 429 ||
        responseData.error_code === 429;

      if (isRateLimited && attempt === 0) {
        var retryAfter = Number(
          responseData.parameters &&
          responseData.parameters.retry_after
        );

        if (!isFinite(retryAfter) || retryAfter < 1) {
          retryAfter = 1;
        }

        // 保持在 Komari 的调用超时内。
        if (retryAfter > 15) {
          console.error(
            "Telegram 要求等待 " +
            retryAfter +
            " 秒,超过本模板的安全重试窗口。"
          );

          return false;
        }

        console.log(
          "Telegram 触发限速," +
          retryAfter +
          " 秒后重试。"
        );

        await waitMilliseconds(
          retryAfter * 1000 + 250
        );

        continue;
      }

      console.error(
        "Telegram 消息发送失败:" +
        responseText
      );

      return false;
    } catch (error) {
      console.error(
        "请求 Telegram 接口失败:" +
        String(error)
      );

      return false;
    }
  }

  return false;
}

async function sendTelegramMessage(text) {
  if (
    !BOT_TOKEN ||
    BOT_TOKEN.indexOf("填写") !== -1
  ) {
    console.error(
      "Telegram 机器人令牌尚未填写"
    );

    return false;
  }

  if (
    !CHAT_ID ||
    CHAT_ID.indexOf("填写") !== -1
  ) {
    console.error(
      "Telegram 聊天 ID 尚未填写"
    );

    return false;
  }

  var chunks =
    splitTelegramHtmlMessage(text, 3800);

  var i;

  for (i = 0; i < chunks.length; i++) {
    var success =
      await sendTelegramChunk(
        chunks[i],
        i === chunks.length - 1
      );

    if (!success) {
      console.error(
        "Telegram 分段消息发送失败:第 " +
        (i + 1) +
        " 条,共 " +
        chunks.length +
        " 条"
      );

      return false;
    }

    if (i < chunks.length - 1) {
      await waitMilliseconds(1100);
    }
  }

  console.log(
    chunks.length > 1
      ? "Telegram 消息发送成功,共 " +
        chunks.length +
        " 条"
      : "Telegram 消息发送成功"
  );

  return true;
}


// Komari 普通通知入口

async function sendMessage(message, title) {
  try {
    var eventInformation =
      getEventInformation(
        "",
        title,
        message
      );

    var detail;

    if (eventInformation.isTest) {
      detail =
        "通知渠道连接正常,测试消息发送成功。";
    } else {
      detail =
        buildChineseDetail(
          eventInformation,
          message,
          "",
          title,
          []
        );
    }

    var text =
      "<b>" +
      eventInformation.icon +
      " " +
      escapeHtml(eventInformation.title) +
      "</b>\n\n" +
      "📝 <b>通知内容</b>:" +
      escapeHtml(detail) +
      "\n\n" +
      "🕐 <b>触发时间</b>:" +
      formatBeijingTime() +
      "\n" +
      "🌏 <b>使用时区</b>:北京时间";

    return await sendTelegramMessage(text);
  } catch (error) {
    console.error(
      "处理普通通知失败:" +
      String(error)
    );

    return false;
  }
}


// 构建事件消息

function buildEventText(event) {
  var eventName =
    event.event ||
    event.type ||
    event.name ||
    "";

  var eventTitle =
    event.title ||
    "";

  var originalMessage =
    getOriginalMessage(event);

  var eventInformation =
    getEventInformation(
      eventName,
      eventTitle,
      originalMessage
    );

  var clients =
    getClients(event);

  var showExpiry =
    isExpireNotification(
      eventName,
      eventTitle,
      originalMessage
    );

  var detail;

  if (eventInformation.isTest) {
    detail =
      "通知渠道连接正常,测试消息发送成功。";
  } else {
    detail =
      buildChineseDetail(
        eventInformation,
        originalMessage,
        eventName,
        eventTitle,
        clients
      );
  }

  var text =
    "<b>" +
    eventInformation.icon +
    " " +
    escapeHtml(eventInformation.title) +
    "</b>\n\n";

  if (clients.length > 0) {
    text +=
      buildClientInformation(
        clients,
        showExpiry
      );

    text += "\n";
  }

  text +=
    "📝 <b>事件详情</b>:" +
    escapeHtml(detail) +
    "\n\n" +
    "🕐 <b>触发时间</b>:" +
    formatBeijingTime(getEventTime(event)) +
    "\n" +
    "🌏 <b>使用时区</b>:北京时间";

  return text;
}


// Komari 事件通知入口

async function sendEvent(event) {
  try {
    if (!event) {
      console.error(
        "Komari 未提供事件数据"
      );

      return false;
    }

    return await sendTelegramMessage(
      buildEventText(event)
    );
  } catch (error) {
    console.error(
      "处理事件通知失败:" +
      String(error)
    );

    try {
      var fallbackText =
        "<b>🔔 Komari 系统通知</b>\n\n" +
        "📝 <b>事件详情</b>:收到一条服务器状态通知。\n\n" +
        "🕐 <b>触发时间</b>:" +
        formatBeijingTime() +
        "\n" +
        "🌏 <b>使用时区</b>:北京时间";

      return await sendTelegramMessage(
        fallbackText
      );
    } catch (fallbackError) {
      console.error(
        "备用通知发送失败:" +
        String(fallbackError)
      );

      return false;
    }
  }
}

这是V2版本的,也备份一下

/**
 * Komari 中文美化 Telegram 通知模板
 * 渠道选择:Javascript / JS
 * 功能:中文化通知 + 表情美化 + Telegram Bot 推送 + 直达 Komari 面板按钮 + 北京时间显示
 */

/* =========================
 * 1. 基础配置
 * ========================= */

// Telegram Bot Token
const TG_BOT_TOKEN = "";

// Telegram Chat ID
const TG_CHAT_ID = "";

// Komari 面板地址
const KOMARI_URL = "";

// Telegram 群组话题 ID,不使用话题就保持 null
const TG_TOPIC_ID = null;

// 是否关闭网页预览
const DISABLE_WEB_PREVIEW = true;

// 如果 Komari 传入的时间没有时区标记,是否按 UTC 时间处理
const ASSUME_NO_TIMEZONE_AS_UTC = true;


/* =========================
 * 2. 工具函数
 * ========================= */

function escapeHtml(text) {
  if (text === null || text === undefined) return "";

  return String(text)
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
}

function isEmpty(value) {
  return value === null || value === undefined || String(value).trim() === "";
}

function isInvalidTime(timeStr) {
  if (!timeStr) return true;

  const str = String(timeStr);

  if (
    str.includes("0001") ||
    str.startsWith("0001-01-01") ||
    str.startsWith("1-01-01")
  ) {
    return true;
  }

  const date = new Date(str);
  if (isNaN(date.getTime())) return true;
  if (date.getFullYear() < 2020) return true;

  return false;
}

/**
 * 固定显示北京时间 UTC+8
 * 输出格式:2026-06-25 16:34:27(北京时间)
 */
function formatTime(timeStr, eventType) {
  const eventKey = String(eventType || "").toLowerCase();

  if (eventKey === "test") {
    return "测试消息,无真实触发时间";
  }

  if (isInvalidTime(timeStr)) {
    return "未提供有效触发时间";
  }

  let str = String(timeStr).trim();

  // 兼容 2026-06-25 16:34:27 这种格式
  str = str.replace(" ", "T");

  // 判断是否自带时区,例如 Z、+08:00、-04:00
  const hasTimezone = /([zZ]|[+-]\d{2}:?\d{2})$/.test(str);

  let date;

  if (hasTimezone) {
    date = new Date(str);
  } else {
    if (ASSUME_NO_TIMEZONE_AS_UTC) {
      date = new Date(str + "Z");
    } else {
      date = new Date(str);
    }
  }

  if (isNaN(date.getTime())) {
    return "时间解析失败";
  }

  // 转换为北京时间 UTC+8
  const beijingTime = new Date(date.getTime() + 8 * 60 * 60 * 1000);

  const year = beijingTime.getUTCFullYear();
  const month = String(beijingTime.getUTCMonth() + 1).padStart(2, "0");
  const day = String(beijingTime.getUTCDate()).padStart(2, "0");
  const hour = String(beijingTime.getUTCHours()).padStart(2, "0");
  const minute = String(beijingTime.getUTCMinutes()).padStart(2, "0");
  const second = String(beijingTime.getUTCSeconds()).padStart(2, "0");

  return `${year}-${month}-${day} ${hour}:${minute}:${second}(北京时间)`;
}

function splitMessage(text, maxLength) {
  const limit = maxLength || 3900;
  const result = [];
  let current = "";

  const lines = String(text || "").split("\n");

  for (const line of lines) {
    if ((current + "\n" + line).length > limit) {
      result.push(current);
      current = line;
    } else {
      current += current ? "\n" + line : line;
    }
  }

  if (current) {
    result.push(current);
  }

  return result;
}

function isValidHttpUrl(url) {
  try {
    const u = new URL(url);
    return u.protocol === "http:" || u.protocol === "https:";
  } catch (e) {
    return false;
  }
}


/* =========================
 * 3. 事件类型中文化
 * ========================= */

function getEventInfo(eventType) {
  const key = String(eventType || "").toLowerCase();

  const map = {
    offline: {
      title: "🚨 服务器离线",
      level: "严重告警",
      icon: "🔴",
      desc: "服务器已离线,请尽快检查网络、探针或主机状态。"
    },
    online: {
      title: "✅ 服务器恢复上线",
      level: "恢复通知",
      icon: "🟢",
      desc: "服务器已恢复在线,当前状态正常。"
    },
    alert: {
      title: "⚠️ 监控告警",
      level: "异常提醒",
      icon: "🟡",
      desc: "监控指标触发告警,请检查服务器负载、流量、内存或磁盘状态。"
    },
    renew: {
      title: "🔄 自动续费通知",
      level: "续费通知",
      icon: "🧾",
      desc: "服务器或服务已完成自动续费。"
    },
    expire: {
      title: "⏰ 到期提醒",
      level: "请及时处理",
      icon: "🟠",
      desc: "服务器或服务即将到期,请及时续费或处理。"
    },
    test: {
      title: "🧪 测试通知",
      level: "测试消息",
      icon: "📨",
      desc: "这是一条来自 Komari 的测试消息,用于验证 Telegram 通知是否正常。"
    },
    recovery: {
      title: "✅ 服务恢复",
      level: "恢复通知",
      icon: "🟢",
      desc: "服务已恢复正常。"
    },
    recovered: {
      title: "✅ 服务恢复",
      level: "恢复通知",
      icon: "🟢",
      desc: "服务已恢复正常。"
    },
    load: {
      title: "📈 负载告警",
      level: "性能告警",
      icon: "🟡",
      desc: "服务器负载异常,请检查 CPU、内存或进程占用。"
    },
    cpu: {
      title: "🔥 CPU 告警",
      level: "性能告警",
      icon: "🟡",
      desc: "CPU 使用率异常,请检查进程占用情况。"
    },
    memory: {
      title: "🧠 内存告警",
      level: "性能告警",
      icon: "🟡",
      desc: "内存使用率异常,请检查服务占用情况。"
    },
    disk: {
      title: "💽 磁盘告警",
      level: "容量告警",
      icon: "🟠",
      desc: "磁盘使用率异常,请及时清理空间或扩容。"
    },
    traffic: {
      title: "📊 流量告警",
      level: "流量提醒",
      icon: "🟡",
      desc: "服务器流量触发提醒,请检查网络使用情况。"
    },
    network: {
      title: "🌐 网络异常",
      level: "网络告警",
      icon: "🟡",
      desc: "服务器网络状态异常,请检查线路、延迟或丢包情况。"
    },
    ping: {
      title: "📡 Ping 异常",
      level: "连通性告警",
      icon: "🟡",
      desc: "服务器 Ping 检测异常,请检查网络连通性。"
    }
  };

  return map[key] || {
    title: "📢 Komari 通知",
    level: "普通通知",
    icon: "🔔",
    desc: `收到未知类型事件:${eventType || "未知事件"}`
  };
}


/* =========================
 * 4. 事件说明中文化
 * ========================= */

function translateEventMessage(message, eventType) {
  const raw = String(message || "").trim();
  const key = String(eventType || "").toLowerCase();

  if (!raw) {
    return getEventInfo(eventType).desc;
  }

  const msgMap = {
    "This is a test message from Komari.": "这是一条来自 Komari 的测试消息,用于验证 Telegram 通知是否正常。",
    "test message": "这是一条测试消息。",
    "server offline": "服务器已离线。",
    "server online": "服务器已恢复在线。",
    "server recovered": "服务器已恢复正常。",
    "service recovered": "服务已恢复正常。",
    "recovered": "服务已恢复正常。",
    "offline": "服务器已离线。",
    "online": "服务器已恢复在线。",
    "cpu usage too high": "CPU 使用率过高。",
    "high cpu usage": "CPU 使用率过高。",
    "memory usage too high": "内存使用率过高。",
    "high memory usage": "内存使用率过高。",
    "disk usage too high": "磁盘使用率过高。",
    "high disk usage": "磁盘使用率过高。",
    "load too high": "服务器负载过高。",
    "high load": "服务器负载过高。",
    "traffic too high": "服务器流量使用过高。",
    "high traffic": "服务器流量使用过高。",
    "ping timeout": "服务器 Ping 超时。",
    "connection timeout": "连接超时。",
    "timeout": "连接超时。",
    "network error": "网络异常。",
    "packet loss": "网络丢包异常。",
    "renew": "服务已续费。",
    "expired": "服务已到期。",
    "expire": "服务即将到期。"
  };

  if (msgMap[raw]) {
    return msgMap[raw];
  }

  const lower = raw.toLowerCase();

  for (const k in msgMap) {
    if (lower.includes(k.toLowerCase())) {
      return msgMap[k];
    }
  }

  if (key === "test") {
    return "这是一条来自 Komari 的测试消息,用于验证 Telegram 通知是否正常。";
  }

  if (/[a-zA-Z]/.test(raw)) {
    return `系统返回了一条原始说明:${raw}`;
  }

  return raw;
}


/* =========================
 * 5. 服务器信息格式化
 * ========================= */

function formatClient(client, index) {
  const name = escapeHtml(
    client.name ||
    client.client_name ||
    client.hostname ||
    client.host ||
    "未命名服务器"
  );

  const region = escapeHtml(
    client.region ||
    client.location ||
    client.area ||
    "未知地区"
  );

  const uuid = escapeHtml(
    client.uuid ||
    client.id ||
    ""
  );

  let text = "";

  text += `\n<b>🖥️ 节点 ${index + 1}</b>\n`;
  text += `├ 名称:<b>${name}</b>\n`;
  text += `├ 地区:${region}\n`;

  if (!isEmpty(uuid)) {
    text += `└ 标识:<code>${uuid}</code>\n`;
  } else {
    text += `└ 标识:暂无\n`;
  }

  return text;
}


/* =========================
 * 6. Telegram 发送函数
 * ========================= */

async function sendTelegramMessage(text, title) {
  if (!TG_BOT_TOKEN || TG_BOT_TOKEN === "这里填TG机器人TOKEN") {
    console.error("❌ Telegram Bot Token 未填写");
    return false;
  }

  if (!TG_CHAT_ID || TG_CHAT_ID === "这里填TG用户ID或群组ID") {
    console.error("❌ Telegram Chat ID 未填写");
    return false;
  }

  const url = `https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage`;
  const messages = splitMessage(text, 3900);

  for (let i = 0; i < messages.length; i++) {
    const body = {
      chat_id: TG_CHAT_ID,
      parse_mode: "HTML",
      disable_web_page_preview: DISABLE_WEB_PREVIEW,
      text: messages[i]
    };

    if (i === 0 && isValidHttpUrl(KOMARI_URL)) {
      body.reply_markup = {
        inline_keyboard: [
          [
            {
              text: "🚀 直达 Komari 面板",
              url: KOMARI_URL
            }
          ]
        ]
      };
    }

    if (TG_TOPIC_ID) {
      body.message_thread_id = TG_TOPIC_ID;
    }

    const resp = await fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify(body)
    });

    if (!resp.ok) {
      const errText = await resp.text();
      console.error("❌ Telegram 消息发送失败:", resp.status, errText);
      return false;
    }
  }

  console.log("✅ Telegram 消息发送成功");
  return true;
}


/* =========================
 * 7. Komari 普通消息入口
 * ========================= */

async function sendMessage(message, title) {
  try {
    const safeTitle = escapeHtml(title || "📢 Komari 消息通知");
    const safeMessage = escapeHtml(message || "暂无消息内容");

    const text =
      `<b>${safeTitle}</b>\n\n` +
      `📨 <b>消息内容</b>\n` +
      `━━━━━━━━━━━━━━\n` +
      `${safeMessage}\n` +
      `━━━━━━━━━━━━━━\n` +
      `🤖 来源:Komari 监控通知`;

    return await sendTelegramMessage(text, safeTitle);
  } catch (error) {
    console.error("❌ 普通消息处理失败:", error);
    return false;
  }
}


/* =========================
 * 8. Komari 事件通知入口
 * ========================= */

async function sendEvent(event) {
  try {
    event = event || {};

    const eventType = event.event || event.type || "Unknown";
    const info = getEventInfo(eventType);

    const title = `${info.title} | Komari`;

    const eventName = escapeHtml(info.title);
    const eventLevel = escapeHtml(info.level);
    const eventTime = escapeHtml(formatTime(event.time, eventType));
    const eventMessage = escapeHtml(translateEventMessage(event.message, eventType));

    let msg = "";

    msg += `<b>${escapeHtml(title)}</b>\n\n`;
    msg += `${info.icon} <b>${eventLevel}</b>\n`;
    msg += `━━━━━━━━━━━━━━\n`;
    msg += `📌 事件类型:${eventName}\n`;
    msg += `🕒 触发时间:${eventTime}\n`;
    msg += `📝 事件说明:${eventMessage}\n`;
    msg += `━━━━━━━━━━━━━━\n`;

    if (event.clients && Array.isArray(event.clients) && event.clients.length > 0) {
      msg += `📡 影响节点:<b>${event.clients.length} 台</b>\n`;

      for (let i = 0; i < event.clients.length; i++) {
        msg += formatClient(event.clients[i], i);
      }
    } else {
      msg += `📡 影响节点:暂无服务器信息\n`;
    }

    msg += `\n━━━━━━━━━━━━━━\n`;

    const key = String(eventType || "").toLowerCase();

    if (key === "test") {
      msg += `💡 提示:这是测试通知,说明 Telegram 推送渠道已正常工作。\n`;
    } else if (key === "online" || key === "recovery" || key === "recovered") {
      msg += `💡 提示:服务已恢复,建议继续观察一段时间确认稳定性。\n`;
    } else if (key === "offline") {
      msg += `💡 提示:请优先检查服务器电源、网络、探针进程和防火墙规则。\n`;
    } else if (key === "expire") {
      msg += `💡 提示:请及时续费或迁移,避免服务中断。\n`;
    } else {
      msg += `💡 提示:请及时检查服务器状态、探针运行情况与网络连通性。\n`;
    }

    msg += `🤖 来源:Komari 监控通知`;

    return await sendTelegramMessage(msg, title);
  } catch (error) {
    console.error("❌ Komari 事件通知处理失败:", error);

    const fallbackTitle = "📢 Komari 通知异常";
    const fallbackMsg =
      `<b>📢 Komari 通知异常</b>\n\n` +
      `⚠️ <b>模板执行异常</b>\n` +
      `━━━━━━━━━━━━━━\n` +
      `📝 异常说明:通知模板执行失败,请检查 JS 配置。\n` +
      `🔧 建议操作:检查 Bot Token、Chat ID、面板地址和通知模板语法。\n` +
      `━━━━━━━━━━━━━━\n` +
      `🤖 来源:Komari 监控通知`;

    return await sendTelegramMessage(fallbackMsg, fallbackTitle);
  }
}