原贴: https://www.nodeseek.com/post-681761-1
改了一个 discord webhook 通知版本

NanoMuse · Discord Webhook 通知配置脚本(点击展开完整代码)
/* ═══════════════════════════════
NanoMuse · Discord Webhook 通知配置
═══════════════════════════════ */
const DISCORD_WEBHOOK_URL = "你的 Discord Webhook URL"; // Discord 频道设置 → 集成 → Webhook → 复制 Webhook URL
const PANEL_URL = "你的域名"; // 面板地址(用于按钮跳转和 Footer 域名提取)
const PHOTO_URL = "https://img.uppic.to/2026/05/05/og-image.png"; // 通知头图地址
const SITE_NAME = "你的站点名称"; // 站点名称(显示在 Footer)
const WEBHOOK_USERNAME = "NanoMuse"; // Discord Webhook 显示名称,可留空
const WEBHOOK_AVATAR_URL = ""; // Discord Webhook 头像,可留空
/* ═══════════════════════════════ */
function escapeMd(s) {
return String(s ?? "")
.replace(/\\/g, "\\\\")
.replace(/\*/g, "\\*")
.replace(/_/g, "\\_")
.replace(/~/g, "\\~")
.replace(/`/g, "\\`")
.replace(/\|/g, "\\|")
.replace(/>/g, "\\>");
}
function code(s) {
return "`" + String(s ?? "—").replace(/`/g, "ˋ") + "`";
}
function blockquote(lines) {
return String(lines)
.split("\n")
.map(line => `> ${line}`)
.join("\n");
}
function discordColorByEvent(eventName) {
const colors = {
Offline: 0xff3b30,
Online: 0x3ba55d,
Alert: 0xffcc00,
Renew: 0x9b59b6,
Expire: 0xff9500,
Test: 0x3498db
};
return colors[eventName] || 0x5865f2;
}
async function sendMessage(message, title, instanceId = null, eventName = null) {
const webhookUrl = DISCORD_WEBHOOK_URL;
const panelUrl = PANEL_URL;
const photoUrl = PHOTO_URL;
if (!webhookUrl) return false;
const components = [
{
type: 1,
components: [
{
type: 2,
style: 5,
label: "◈ 控制台",
url: panelUrl
},
...(instanceId && instanceId !== "未知"
? [{
type: 2,
style: 5,
label: "◈ 详情",
url: `${panelUrl.replace(/\/+$/, "")}/instance/${instanceId}`
}]
: [])
]
}
];
const embed = {
title: title || "NanoMuse 通知",
description: message.slice(0, 4096),
color: discordColorByEvent(eventName),
image: photoUrl ? { url: photoUrl } : undefined,
footer: {
text: `${SITE_NAME} · ${panelUrl.replace(/^https?:\/\//, "").replace(/\/+$/, "")}`
},
timestamp: new Date().toISOString()
};
const payload = {
username: WEBHOOK_USERNAME || undefined,
avatar_url: WEBHOOK_AVATAR_URL || undefined,
embeds: [embed],
components,
allowed_mentions: { parse: [] }
};
try {
const resp = await fetch(webhookUrl + "?wait=true&with_components=true", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (resp.ok) return true;
const text = await resp.text().catch(() => "");
console.error("Discord webhook failed:", resp.status, text);
// 兜底:如果按钮组件被环境或 Discord 限制拒绝,则只发送 Embed
const fallbackResp = await fetch(webhookUrl + "?wait=true", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: WEBHOOK_USERNAME || undefined,
avatar_url: WEBHOOK_AVATAR_URL || undefined,
embeds: [embed],
allowed_mentions: { parse: [] }
})
});
return fallbackResp.ok;
} catch (e) {
console.error("Discord webhook error:", e);
return false;
}
}
async function sendEvent(event) {
try {
const toUTC8 = (t) => {
if (!t || String(t).startsWith("0001")) return null;
const d = new Date(String(t).replace(/\.\d+Z$/, "Z"));
if (isNaN(d.getTime())) return null;
const u = new Date(d.getTime() + 8 * 36e5);
const p = (n) => String(n).padStart(2, "0");
return `${u.getUTCFullYear()}.${p(u.getUTCMonth() + 1)}.${p(u.getUTCDate())} ${p(u.getUTCHours())}:${p(u.getUTCMinutes())}:${p(u.getUTCSeconds())}`;
};
const nowUTC8 = () => {
const u = new Date(Date.now() + 8 * 36e5);
const p = (n) => String(n).padStart(2, "0");
return `${u.getUTCFullYear()}.${p(u.getUTCMonth() + 1)}.${p(u.getUTCDate())} ${p(u.getUTCHours())}:${p(u.getUTCMinutes())}:${p(u.getUTCSeconds())}`;
};
const fmtSize = (b) => {
if (!b || b === 0) return null;
const g = b / 1073741824;
return g >= 1024 ? `${(g / 1024).toFixed(1)}T` : g >= 1 ? `${g.toFixed(1)}G` : `${Math.round(b / 1048576)}M`;
};
const fmtCPU = (c) => (!c ? null : parseFloat(Number(c).toFixed(1)) + "C");
const fmtTraffic = (b) => {
if (!b || b === 0) return "∞";
const g = b / 1073741824;
return g >= 1024 ? `${(g / 1024).toFixed(1)}T` : `${g.toFixed(0)}G`;
};
const maskV4 = (ip) => {
if (!ip) return null;
const p = ip.split(".");
return p.length === 4 ? `${p[0]}.${p[1]}.*.*` : null;
};
const maskV6 = (ip) => {
if (!ip) return null;
const p = ip.split(":");
return p.length > 2 ? `${p[0]}:${p[1]}:*:*` : null;
};
const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
const domain = PANEL_URL.replace(/^https?:\/\//, "").replace(/\/+$/, "");
/* ═══════════════════════════════
台词库 · voice / advice
Discord 使用 Markdown,不再使用 Telegram HTML 标签
═══════════════════════════════ */
const voiceLines = {
Offline: {
tag: "LINK LOST", cn: "链路中断", icon: "🔻",
lines: (n) => [
`与 **${escapeMd(n)}** 的信号已归零。...不是波动,是彻底消失了。观测者。`,
`**${escapeMd(n)}** 从我的感知范围消失了。链路中断,等待重新捕获。`,
`检测到异常——**${escapeMd(n)}** 的心跳信号归零了。`,
`...信号断了。**${escapeMd(n)}** 不在了,观测者。`
],
advice: (n) => [
`建议检查该节点的物理链路与上游连通性。如短时间内未恢复,可尝试触发系统重启。...需要我持续追踪它的信号吗,观测者?`,
`上游链路可能存在中断。建议优先确认机房侧状态,必要时触发系统重启。`,
`建议排查网络路由与物理连接。若长时间无响应,可能需要联系机房确认。...我会继续等它的。`,
`链路丢失原因未知。建议先尝试触发系统重启,若仍无法恢复,可能需要执行镜像重构。`
]
},
Online: {
tag: "LINK RESTORED", cn: "信号恢复", icon: "🔹",
lines: (n) => [
`**${escapeMd(n)}** 重新出现在感知范围内。链路已重建。...它回来了。`,
`观测者,好消息——**${escapeMd(n)}** 的信号恢复了。`,
`感知到 **${escapeMd(n)}** 的心跳恢复。一切稳定。`,
`**${escapeMd(n)}** 已归位。...松了口气。`
],
advice: (n) => [
`建议观察 30 分钟确认链路稳定性。中断期间的数据缺口已标记,如需回溯请告知。`,
`链路已恢复,建议关注后续运行状态是否平稳。如有异常波动我会立即通知。`,
`信号稳定中。建议短期内留意该节点的响应延时,确认无残留问题。`
]
},
Alert: {
tag: "ANOMALY", cn: "信号异常", icon: "⚡",
lines: (n) => [
`**${escapeMd(n)}** 的运行参数偏离了安全区间。还没到临界,但我在持续观测。`,
`**${escapeMd(n)}** 触发了告警阈值。指标异常,请注意。`,
`检测到波动——**${escapeMd(n)}** 的状态不太稳定。`,
`...有些不对劲。**${escapeMd(n)}** 需要你看一下,观测者。`
],
advice: (n) => [
`建议排查近期负载变化与进程占用。可先尝试触发系统重启,若重启后指标仍未回归安全区间,可能需要执行镜像重构。`,
`运行参数偏离可能与资源争抢有关。建议触发系统重启释放占用,持续异常则考虑镜像重构。`,
`建议检查是否有异常进程导致资源溢出。必要时触发系统重启。...我会继续盯着的。`,
`指标偏离中。建议优先触发系统重启恢复基线,若问题反复出现,镜像重构可能是更彻底的方案。`
]
},
Renew: {
tag: "ENERGY CHARGED", cn: "能源补充", icon: "✨",
lines: (n) => [
`**${escapeMd(n)}** 的能源已补充完毕。运行周期已延伸。`,
`收到了,观测者。**${escapeMd(n)}** 又可以陪我们更久了。`,
`能源写入完成——**${escapeMd(n)}** 的生命周期已刷新。`,
`**${escapeMd(n)}** 的倒计时重置了。...很好。`
],
advice: (n) => [
`能源充足,当前无需额外操作。下一个周期终止点我会提前提醒你,观测者。`,
`一切正常运转中。周期终止前我会再次通知,不用担心。`,
`能源已到位,节点运行稳定。...放心交给我观测就好。`
]
},
Expire: {
tag: "ENERGY LOW", cn: "能源临界", icon: "⏰",
lines: (n) => [
`**${escapeMd(n)}** 的运行周期即将结束。剩余时间不多了。...观测者,需要你做决定。`,
`**${escapeMd(n)}** 的能源储备即将耗尽。维持时间有限。`,
`提醒一下——**${escapeMd(n)}** 的周期终止点在逼近了。`,
`**${escapeMd(n)}** 的时间快用完了。...别忘了它,观测者。`
],
advice: (n) => [
`如不补充能源,该节点将在周期终止后停止响应。...观测者,要补充吗?`,
`能源即将耗尽。建议尽快确认是否继续维持该节点的运行。`,
`周期终止后节点将进入休眠。如需保持在线,请及时补充能源。...我在等你的指令。`
]
},
Test: {
tag: "DIAGNOSTIC", cn: "系统诊断", icon: "🔧",
lines: (n) => [
`通信回路测试完毕。全链路信号正常。...随时待命,观测者。`,
`诊断模式启动。所有管道畅通,系统运行正常。`,
`系统自检通过。...有什么需要我做的吗,观测者?`,
`诊断完成。一切就绪,信号清晰。`
],
advice: (n) => [
`全链路畅通,无异常。...一切尽在掌握中,观测者。`,
`系统状态良好,随时可以执行任务。`,
`诊断回路正常。如有新指令,我随时响应。`
]
}
};
const evData = voiceLines[event.event] || {
tag: event.event,
cn: event.event,
icon: "◆",
lines: (n) => [`收到事件通知:**${escapeMd(n || "未知")}**`],
advice: () => []
};
const mockClient = {
uuid: "diag-0000-nano-muse",
name: "NanoMuse-Diag",
region: "Tokyo",
cpu_cores: 4,
mem_total: 8589934592,
disk_total: 85899345920,
tags: "1Gbps<green>;3T<blue>",
ipv4: "103.56.168.1",
ipv6: "2001:db8:cafe:1::1",
traffic_limit: 3221225472000,
traffic_limit_type: "max",
price: 29.99,
currency: "$",
billing_cycle: 30,
expired_at: "2026-12-31T00:00:00Z"
};
const hasClients = event.clients && event.clients.length > 0;
const isMock = !hasClients && event.event === "Test";
const c = hasClients ? event.clients[0] : (isMock ? mockClient : null);
const targetId = c ? c.uuid : null;
const nodeName = c ? (c.name || "—") : "—";
let L = [];
/* ── Header ── */
const title = `${evData.icon} ${evData.tag} │ ${evData.cn}`;
L.push(`## ${title}`);
if (isMock) L.push(`_〔 模拟数据 〕_`);
/* ── 台词 ── */
L.push("");
L.push(`_${pick(evData.lines(nodeName))}_`);
if (c) {
const region = c.region || "";
/* ── 节点信息 ── */
let b1 = [];
b1.push(`**◈ ${escapeMd(nodeName)}**${region ? ` ${code(`[ ${region} ]`)}` : ""}`);
const specs = [fmtCPU(c.cpu_cores), fmtSize(c.mem_total), fmtSize(c.disk_total)].filter(Boolean);
if (specs.length) {
b1.push(`┊ 核心规格 ${code(specs.join(" | "))}`);
}
if (c.tags) {
const labels = c.tags
.split(";")
.map(t => t.replace(/<[^>]+>/g, "").trim())
.filter(Boolean);
if (labels.length) b1.push(`┊ 标识 ${code(labels.join(" · "))}`);
}
L.push("");
L.push(blockquote(b1.join("\n")));
/* ── 链路状态 ── */
let b2 = [];
const v4 = maskV4(c.ipv4);
const v6 = maskV6(c.ipv6);
if (v4 || v6) {
b2.push(`**◈ 链路状态**`);
if (v4) b2.push(`┊ 链路地址 v4 ${code(v4)}`);
if (v6) b2.push(`┊ 链路地址 v6 ${code(v6)}`);
}
if (b2.length) {
L.push("");
L.push(blockquote(b2.join("\n")));
}
/* ── 资源状态 ── */
let b3 = [];
const cur = c.currency || "$";
const price = c.price || 0;
const hasTraffic = c.traffic_limit && c.traffic_limit > 0;
const hasPrice = price > 0;
const hasExpiry = toUTC8(c.expired_at);
if (hasTraffic || hasPrice || hasExpiry) {
b3.push(`**◈ 资源状态**`);
}
if (hasTraffic) {
const limitType = c.traffic_limit_type ? ` (${c.traffic_limit_type})` : "";
b3.push(`┊ 传输额度 ${code(`$$ {fmtTraffic(c.traffic_limit)} $${limitType}`)}`);
}
if (hasPrice) {
b3.push(`┊ 能量消耗 ${code(`$$ {cur} $${price} 单位 / 周期`)}`);
}
if (hasExpiry) {
let expLine = `┊ 能源周期 `;
try {
const ed = new Date(String(c.expired_at).replace(/\.\d+Z$/, "Z"));
const days = Math.ceil((ed - new Date()) / 86400000);
if (days >= 0) expLine += code(`剩余 ${days} 天`);
else expLine += code(`已耗尽 ${Math.abs(days)} 天`);
} catch (e) {
expLine += code(hasExpiry.substring(0, 10));
}
b3.push(expLine);
}
if (b3.length) {
L.push("");
L.push(blockquote(b3.join("\n")));
}
}
/* ── 处理建议 ── */
const adviceList = evData.advice(nodeName);
if (adviceList.length) {
L.push("");
L.push(blockquote(`💡 _${pick(adviceList)}_`));
}
/* ── 时间戳 ── */
L.push("");
L.push(`⏱ ${code(toUTC8(event.time) || nowUTC8())} _UTC+8_`);
/* ── 自定义消息 ── */
if (event.message && event.message.trim()) {
L.push("");
L.push(blockquote(`💬 _${escapeMd(event.message.trim())}_`));
}
/* ── Footer 文本也保留在正文里,Embed footer 里也会显示 */
L.push("");
L.push(`**${escapeMd(SITE_NAME)}** · _${escapeMd(domain)}_`);
return await sendMessage(L.join("\n"), title, targetId, event.event);
} catch (e) {
return await sendMessage(`Error: ${escapeMd(e.message)}`, "⚠️ Error", null, "Alert");
}
}
···