可能要卸载了,留个存档
现在这个模板流量定时报告还有问题,昨日流量会显示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, "<")
.replace(/>/g, ">");
}
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代码那里填
这个怎么用
贴通知模板里
好东西,拿走学习了。谢谢楼主
先收藏,谢谢哈