logo NodeSeekbeta

Komari TG通知模板样式分享

效果:

节点离线/上线:

image

到期提醒/续费通知:

image

面板登录/流量预警:

image

资源告警:

image

流量统计:

image

在 Komari 后台通知渠道中选择 Javascript,把整个脚本粘贴到 JavaScript 代码 并保存,在保存前配置好你自己的TG bot token和chat id与站点域名。

源码:

var KOMARI_TG_CONFIG = {
 endpoint: 'https://api.telegram.org/bot',
 bot_token: 'YOUR_BOT_TOKEN',
 chat_id: 'YOUR_CHAT_ID',
 message_thread_id: '' // 可选:Telegram 群组话题 ID;普通消息留空
};

// 你的 Komari 站点地址 比如https://monitor.example.com
var KOMARI_SITE_URL = '';

// Internal cache, populated only from notification events; no manual region configuration.
var learnedNodeRegions = Object.create(null);
var publicRegionState = {site: '', regions: Object.create(null), expiresAt: 0, retryAt: 0, pending: null};
var deliveredPages = Object.create(null);

function text(value) { return value === null || value === undefined ? '' : String(value); }
function esc(value) { return text(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
function strong(value) { return '<b>' + esc(value) + '</b>'; }
function pad(value) { return ('0' + value).slice(-2); }
function dateValue(value) {
 var result = new Date(text(value).replace(/(\.\d{3})\d+/, '$1'));
 return isNaN(result.getTime()) ? new Date() : result;
}
function beijing(value) { return new Date(dateValue(value).getTime() + 8 * 3600000); }
function dayString(date) { return date.getUTCFullYear() + '.' + pad(date.getUTCMonth() + 1) + '.' + pad(date.getUTCDate()); }
function clockString(value) {
 var d = beijing(value);
 return dayString(d) + ' - ' + pad(d.getUTCHours()) + ':' + pad(d.getUTCMinutes()) + ':' + pad(d.getUTCSeconds());
}
function countryFlag(region) {
 var raw = text(region).trim();
 if (/^[a-z]{2}$/i.test(raw)) {
   raw = raw.toUpperCase();
   return String.fromCodePoint(127397 + raw.charCodeAt(0), 127397 + raw.charCodeAt(1));
 }
 var points = Array.from(raw);
 return points.length === 2 && points.every(function (p) {
   var n = p.codePointAt(0); return n >= 127462 && n <= 127487;
 }) ? raw : '';
}
function clientName(client) { return text(client.name || client.uuid || '未命名节点'); }
function learnClients(clients) {
 clients.forEach(function (client) {
   var name = clientName(client);
   var flag = countryFlag(client.region);
   if (flag) learnedNodeRegions[name] = flag;
 });
}
function nodeName(clientOrName) {
 var client = typeof clientOrName === 'string' ? { name: clientOrName } : clientOrName;
 var name = clientName(client);
 var stored = Object.prototype.hasOwnProperty.call(learnedNodeRegions, name) ? learnedNodeRegions[name] : '';
 var publicFlag = publicRegionState.expiresAt > Date.now() ? publicRegionState.regions[name] : '';
 var flag = countryFlag(client.region) || countryFlag(stored) || countryFlag(publicFlag);
 return flag ? flag + ' ' + name : name;
}
function regionLookupSite() {
 var site = text(KOMARI_SITE_URL).trim().replace(/\/+$/, '');
 // A site URL only: no credentials, query parameters, or fragments.
 return /^https?:\/\/[^\s\/?#@]+(?:\/[^\s?#]*)?$/i.test(site) ? site : '';
}
function eventNodeNames(event) {
 var names = (Array.isArray(event.clients) ? event.clients : []).map(clientName);
 var kind = text(event.event);
 var pattern = kind === 'Expire' ? /^\s*[•·-]?\s*(.+?)\s+\(-?\d+d\)\s*$/ :
   kind === 'Renew' ? /^\s*[•·-]?\s*(.+?)\s+until\s+\d{4}-\d{2}-\d{2}\s*$/ :
   /^(DReport|WReport|MReport)$/.test(kind) ? /^(.*?)(?:昨日流量|上周流量|上个月流量)[::]/ : null;
 if (pattern) text(event.message).split(/\r?\n/).forEach(function (line) {
   var match = line.match(pattern);
   if (match) names.push(match[1]);
 });
 return names.filter(function (name, index) { return names.indexOf(name) === index; });
}
function prepareEventRegions(event) {
 learnClients(Array.isArray(event.clients) ? event.clients : []);
 var site = regionLookupSite();
 if (site !== publicRegionState.site) {
   publicRegionState = {site: site, regions: Object.create(null), expiresAt: 0, retryAt: 0, pending: null};
 }
 var state = publicRegionState;
 if (!site) return Promise.resolve();
 var now = Date.now();
 var missing = eventNodeNames(event).some(function (name) {
   return !countryFlag(learnedNodeRegions[name]) && !(state.expiresAt > now && countryFlag(state.regions[name]));
 });
 if (!missing) return Promise.resolve();
 if (state.pending) return state.pending;
 if (now < state.retryAt) return Promise.resolve();
 state.retryAt = now + 60000;
 state.pending = new Promise(function (resolve) {
   var finished = false;
   var controller = typeof AbortController === 'function' ? new AbortController() : null;
   var timer = setTimeout(function () {
     finish(null);
     if (controller) controller.abort();
   }, 2000);
   function finish(nodes) {
     if (finished) return;
     finished = true;
     clearTimeout(timer);
     var regions = Object.create(null);
     if (nodes) nodes.forEach(function (node) {
       if (!node || typeof node.name !== 'string') return;
       var flag = countryFlag(node.region);
       if (!flag) return;
       if (Object.prototype.hasOwnProperty.call(regions, node.name) && regions[node.name] !== flag) {
         regions[node.name] = ''; // Conflicting duplicate names cannot be resolved reliably.
       } else regions[node.name] = flag;
     });
     state.regions = regions;
     state.expiresAt = nodes ? Date.now() + 300000 : 0;
     if (!nodes) state.retryAt = Date.now() + 300000;
     resolve();
   }
   var options = {
     method: 'POST', credentials: 'omit',
     headers: {'Content-Type': 'application/json'},
     body: JSON.stringify({jsonrpc: '2.0', id: 'notification-regions', method: 'public:getNodesInformation', params: {}})
   };
   if (controller) options.signal = controller.signal;
   Promise.resolve().then(function () {
     return fetch(site + '/api/rpc2', options);
   }).then(function (response) {
     if (!response.ok) throw new Error('Public node lookup unavailable');
     return response.json();
   }).then(function (body) {
     if (!body || body.error || !Array.isArray(body.result)) throw new Error('Invalid public node response');
     finish(body.result);
   }).catch(function () { finish(null); });
 }).then(function () { state.pending = null; });
 return state.pending;
}
function modeLabel(mode) {
 return ({up: '仅出站', down: '仅入站', sum: '上下行合计', max: '上下行取较大值', min: '上下行取较小值'})[text(mode).toLowerCase()] || text(mode);
}
function clientBlock(client, detail) { return strong(nodeName(client)) + (detail ? '\n' + esc(detail) : ''); }
function plainBlocks(value) {
 var chars = Array.from(text(value));
 var result = [];
 for (var i = 0; i < chars.length; i += 450) result.push(esc(chars.slice(i, i + 450).join('')));
 return result;
}
function byteValue(number, unit) {
 var power = ({B: 0, KB: 1, KIB: 1, MB: 2, MIB: 2, GB: 3, GIB: 3, TB: 4, TIB: 4, PB: 5, PIB: 5, EB: 6, EIB: 6})[unit.toUpperCase()];
 return power === undefined ? NaN : Number(number) * Math.pow(1024, power);
}
function volume(bytes, scale) { return (bytes / scale.divisor).toFixed(2); }
function scaleFor(bytes) {
 var units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB'];
 var power = bytes > 0 ? Math.min(6, Math.floor(Math.log(bytes) / Math.log(1024))) : 0;
 return { divisor: Math.pow(1024, power), unit: units[Math.max(0, power)] };
}
function percentage(value) { return value.toFixed(1).replace(/\.0$/, ''); }
function billing(client) {
 var cycle = ({30: '月付', 92: '季付', 365: '年付'})[client.billing_cycle];
 if (!cycle && Number(client.billing_cycle) > 0) cycle = client.billing_cycle + ' 天周期';
 if (Number(client.price) === -1) return '免费' + (cycle ? ' / ' + cycle : '');
 if (client.price === null || client.price === undefined) return '';
 var currency = client.currency === '$' ? 'US$' : text(client.currency);
 return currency + Number(client.price).toFixed(2) + (cycle ? ' / ' + cycle : '');
}
function reportPeriod(kind, eventTime) {
 var current = beijing(eventTime);
 var end = new Date(Date.UTC(current.getUTCFullYear(), current.getUTCMonth(), current.getUTCDate()));
 if (kind === 'DReport') return dayString(new Date(end.getTime() - 86400000));
 if (kind === 'WReport') {
   var weekday = end.getUTCDay() || 7;
   var monday = new Date(end.getTime() - (weekday - 1 + 7) * 86400000);
   return dayString(monday) + ' — ' + dayString(new Date(monday.getTime() + 6 * 86400000));
 }
 var previous = new Date(Date.UTC(current.getUTCFullYear(), current.getUTCMonth() - 1, 1));
 return previous.getUTCFullYear() + ' 年 ' + (previous.getUTCMonth() + 1) + ' 月 · 自然月';
}
function loginDevice(userAgent) {
 var browser = /Edg\//.test(userAgent) ? 'Edge' : /Chrome\//.test(userAgent) ? 'Chrome' : /Firefox\//.test(userAgent) ? 'Firefox' : /Safari\//.test(userAgent) ? 'Safari' : '';
 var os = /Android/.test(userAgent) ? 'Android' : /iPhone|iPad/.test(userAgent) ? 'iOS' : /Windows/.test(userAgent) ? 'Windows' : /Macintosh|Mac OS X/.test(userAgent) ? 'macOS' : /Linux/.test(userAgent) ? 'Linux' : '';
 return browser && os ? browser + ' / ' + os : userAgent;
}
function formatEvent(event) {
 var kind = text(event.event);
 var clients = Array.isArray(event.clients) ? event.clients : [];
 learnClients(clients);
 var message = typeof event.message === 'object' && event.message !== null ? JSON.stringify(event.message) : text(event.message);
 var title = '📨 通知';
 var intro = '';
 var blocks = [];
 var note = '';
 if (kind === 'Offline' || kind === 'Online') {
   title = kind === 'Offline' ? '🔴 节点离线' : '🟢 节点上线';
   blocks = clients.map(function (c) { return clientBlock(c); });
 } else if (kind === 'Expire') {
   title = '⏳ 到期提醒';
   message.split(/\r?\n/).filter(Boolean).forEach(function (line) {
     var match = line.match(/^\s*[•·-]?\s*(.+?)\s+\((-?\d+)d\)\s*$/);
     if (!match) { blocks = blocks.concat(plainBlocks(line)); return; }
     var due = beijing(event.time);
     due.setUTCDate(due.getUTCDate() + Number(match[2]));
     blocks.push(strong(nodeName(match[1])) + '\n' + pad(due.getUTCMonth() + 1) + '月' + pad(due.getUTCDate()) + '日到期 · 剩余 ' + Number(match[2]) + ' 天');
   });
   intro = blocks.length ? '即将到期 · ' + blocks.length + ' 台服务器' : '';
 } else if (kind === 'Renew') {
   title = '🔄 续费通知';
   message.split(/\r?\n/).filter(Boolean).forEach(function (line) {
     var match = line.match(/^\s*[•·-]?\s*(.+?)\s+until\s+(\d{4}-\d{2}-\d{2})\s*$/);
     if (!match) { blocks = blocks.concat(plainBlocks(line)); return; }
     var client = clients.filter(function (c) { return clientName(c) === match[1]; })[0] || {name: match[1]};
     var next = match[2].replace(/-/g, '.');
     var previous = client.expired_at ? dayString(beijing(client.expired_at)) : '';
     var dates = previous ? previous + ' → ' + next : '新到期日期 · ' + next;
     blocks.push(clientBlock(client, dates) + (billing(client) ? '\n' + esc(billing(client)) : ''));
   });
 } else if (kind === 'Login') {
   title = '🔑 新的面板登录';
   var lines = message.split(/\r?\n/);
   var login = lines[0].match(/^([^:]+):\s*(.+?)\s+\((.*)\)$/);
   if (login) {
     var method = ({password: '密码登录', 'password+2fa': '密码与两步验证', oauth: 'OAuth 登录', sso: '单点登录'})[login[1].toLowerCase()] || login[1];
     blocks.push(strong(method) + '\n' + esc(login[2]) + (login[3] && login[3] !== 'unknown' ? ' · ' + esc(login[3]) : ''));
     if (lines.length > 1) blocks = blocks.concat(plainBlocks(loginDevice(lines.slice(1).join('\n'))));
   } else blocks = plainBlocks(message);
 } else if (kind === 'Alert') {
   title = '⚠️ 资源告警';
   blocks = clients.map(function (c) { return clientBlock(c); });
   blocks = blocks.concat(plainBlocks('触发规则 · ' + message));
 } else if (kind === 'Traffic') {
   title = '🟠 流量预警';
   var traffic = message.match(/used\s+([\d.]+)%\s*\(([\d.]+)\s*([KMGTPE]?i?B)\s*\/\s*([\d.]+)\s*([KMGTPE]?i?B)\),\s*type=(\w+)/i);
   if (traffic) {
     var used = byteValue(traffic[2], traffic[3]);
     var limit = byteValue(traffic[4], traffic[5]);
     if (clients[0] && Number(clients[0].traffic_limit) > 0) limit = Number(clients[0].traffic_limit);
     var scale = scaleFor(limit);
     var pct = limit > 0 ? used / limit * 100 : Number(traffic[1]);
     if (pct >= 100 || Number(traffic[1]) >= 100) title = used > limit ? '🔴 流量超额' : '🔴 流量已达上限';
     intro = clients.map(function (c) { return strong(nodeName(c)); }).join('\n');
     var difference = used > limit ? '超出 ' + volume(used - limit, scale) : '剩余 ' + volume(Math.max(0, limit - used), scale);
     blocks.push(strong(percentage(pct) + '% 已使用') + '\n' + volume(used, scale) + ' / ' + volume(limit, scale) + ' ' + scale.unit + '\n' + difference + ' ' + scale.unit + ' · ' + esc(modeLabel(traffic[6])));
   } else {
     blocks = clients.map(function (c) { return clientBlock(c); }).concat(plainBlocks(message));
   }
 } else if (kind === 'DReport' || kind === 'WReport' || kind === 'MReport') {
   title = ({DReport: '📊 昨日流量', WReport: '📈 上周流量', MReport: '📅 月度流量'})[kind];
   intro = reportPeriod(kind, event.time);
   var reportClients = clients.slice().sort(function (a, b) { return clientName(b).length - clientName(a).length; });
   // 1.4.3 joins report rows with a retirement notice and no trailing newline.
   var cleaned = message.replace(/Komari 内置的流量报告即将在[^\n]*\n?/g, '').replace(/Komari's built-in traffic reports will be removed[^\n]*?as soon as possible\./g, '');
   cleaned.split(/\r?\n/).filter(function (line) { return line.trim(); }).forEach(function (line) {
     var match = line.match(/^(.*?)(?:昨日流量|上周流量|上个月流量)[::]\s*([\d.]+)\s*([KMGTPE]?i?B)\s*$/i);
     if (!match) { blocks = blocks.concat(plainBlocks(line)); return; }
     var client = reportClients.filter(function (c) { return clientName(c) === match[1]; })[0] || {name: match[1]};
     var bytes = byteValue(match[2], match[3]);
     var unit = scaleFor(bytes);
     var amount = volume(bytes, unit) + ' ' + unit.unit;
     blocks.push(clientBlock(client, amount + (client.traffic_limit_type ? ' · ' + modeLabel(client.traffic_limit_type) : '')));
   });
 } else if (kind === 'Test') {
   title = '🧪 通知测试';
   blocks = clients.map(function (c) { return clientBlock(c); });
   blocks = blocks.concat(plainBlocks(message === 'This is a test message from Komari.' ? '通知发送正常。' : message));
 } else {
   title = text(event.emoji || '📨') + ' ' + (kind || '通知');
   blocks = clients.map(function (c) { return clientBlock(c); }).concat(plainBlocks(message));
 }
 if (!blocks.length) blocks.push(esc(message || '暂无附加信息'));
 return paginate(title, intro, blocks, note, clockString(event.time), kind !== 'Offline' && kind !== 'Online');
}
function paginate(title, intro, blocks, note, timestamp, quoted) {
 var groups = []; var current = [];
 blocks.forEach(function (block) {
   // Fields from existing nodes fit comfortably; preserve unbounded custom messages in safe text chunks.
   if (block.length > 2600) {
     var plain = block.replace(/<[^>]*>/g, '').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
     plainBlocks(plain).forEach(function (part) {
       if (current.join('\n\n').length + part.length > 2600) { groups.push(current); current = []; }
       current.push(part);
     });
   } else {
     if (current.length && current.join('\n\n').length + block.length > 2600) { groups.push(current); current = []; }
     current.push(block);
   }
 });
 if (current.length) groups.push(current);
 return groups.map(function (group, index) {
   var heading = strong(title) + (groups.length > 1 ? ' <i>· ' + (index + 1) + '/' + groups.length + '</i>' : '');
   var content = group.join('\n\n');
   if (quoted !== false) content = '<blockquote>' + content + '</blockquote>';
   return heading + (intro ? '\n\n' + intro : '') + '\n\n' + content + (note ? '\n\n<i>' + esc(note) + '</i>' : '') + '\n\n🕒 ' + esc(timestamp);
 });
}
function sendPages(pages) {
 var config = KOMARI_TG_CONFIG;
 if (!config.bot_token || !config.chat_id || !/^https:\/\//.test(config.endpoint || '')) throw new Error('Telegram configuration unavailable');
 var endpoint = config.endpoint + config.bot_token + '/sendMessage';
 Object.keys(deliveredPages).forEach(function (key) { if (Date.now() - deliveredPages[key] > 120000) delete deliveredPages[key]; });
 var chain = Promise.resolve(true);
 pages.forEach(function (page, index) {
   chain = chain.then(function () {
     if (deliveredPages[page]) return true;
     var payload = {chat_id: config.chat_id, text: page, parse_mode: 'HTML', disable_web_page_preview: true};
     if (config.message_thread_id) payload.message_thread_id = Number(config.message_thread_id);
     return new Promise(function (resolve) { setTimeout(resolve, index ? 1100 : 0); }).then(function () {
       return fetch(endpoint, {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload)}).then(function (response) {
         return response.json().then(function (body) {
           if (!response.ok || !body.ok) throw new Error('Telegram rejected notification; code=' + (body.error_code || response.status));
           deliveredPages[page] = Date.now();
           if (body.result && typeof console !== 'undefined') {
             var types = (body.result.entities || []).map(function (entity) { return entity.type; });
             console.log('KOMARI_NOTIFY_V1 ' + JSON.stringify({message_id: body.result.message_id, blockquote: types.indexOf('blockquote') >= 0, bold: types.indexOf('bold') >= 0}));
           }
           return true;
         });
       });
     }).catch(function () { throw new Error('Telegram notification delivery failed'); });
   });
 });
 return chain;
}
function sendEvent(event) {
 return prepareEventRegions(event).then(function () { return sendPages(formatEvent(event)); });
}
function sendMessage(message, title) {
 return sendEvent({event: title || '通知', message: message, time: new Date().toISOString(), clients: []});
}

  • xhj005 高级货

  • 好看捏

  • 感谢,让hermes agent去改我的komari去了

  • 据说下个版本大改,会有很多破坏性的“更新”

  • @xServ #4 留在1.4.3了,已关闭agent自动更新

  • 太好了,就你了

  • 感谢

  • 先收藏了,假期结束搞一搞

  • 这个好看啊,谢谢啊

  • 感谢分享,样式挺好看

你好啊,陌生人!

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

📈用户数目📈

目前论坛共有72181位seeker

🎉欢迎新用户🎉