Files
ObsidianHome/小鱼/闲鱼消息通知/notify.js
T

103 lines
3.9 KiB
JavaScript
Raw Normal View History

2026-11-09 09:18:58 +08:00
// 通知模块:Windows Toast + 提示音 + 各种 webhook 转发
const path = require('path');
const { execFile } = require('child_process');
function toast(title, body, soundRepeat) {
const ps1 = path.join(__dirname, 'toast.ps1');
const exe = process.platform === 'win32' ? (process.env.DSH_PWSH || 'powershell') : null;
if (!exe) return;
const args = ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ps1, title, body, String(soundRepeat)];
try {
// stdio:'ignore' keeps this working even in restricted environments (no pipes needed)
const child = execFile(exe, args, { windowsHide: true, stdio: 'ignore' }, () => {});
if (child && child.unref) child.unref();
} catch (e) { /* 通知失败不影响监听 */ }
}
async function postJson(url, payload, headers = {}) {
const r = await fetch(url, {
method: 'POST',
headers: { 'content-type': 'application/json', ...headers },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(10000)
});
return { status: r.status, text: (await r.text()).slice(0, 300) };
}
async function sendWebhook(hook, title, text, extra) {
const type = (hook.type || 'custom').toLowerCase();
const url = hook.url;
if (!url) throw new Error('webhook.url 为空');
switch (type) {
case 'feishu': // 飞书自定义机器人
return postJson(url, { msg_type: 'text', content: { text: `${title}\n${text}` } });
case 'wecom': // 企业微信群机器人
case 'dingtalk': // 钉钉自定义机器人
return postJson(url, { msgtype: 'text', text: { content: `${title}\n${text}` } });
case 'bark': { // 手机推送 iOS: https://api.day.app/你的KEY
const base = url.replace(/\/+$/, '');
const r = await fetch(`${base}/${encodeURIComponent(title)}/${encodeURIComponent(text)}?group=Goofish&level=timeSensitive`, {
signal: AbortSignal.timeout(10000)
});
return { status: r.status, text: (await r.text()).slice(0, 300) };
}
case 'serverchan': { // Server酱 https://sctapi.ftqq.com/你的KEY.send
const body = new URLSearchParams({ title, desp: text });
const r = await fetch(url, {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body,
signal: AbortSignal.timeout(10000)
});
return { status: r.status, text: (await r.text()).slice(0, 300) };
}
case 'pushplus': { // http://www.pushplus.plus/send
return postJson(url, { token: hook.token || '', title, content: text, template: 'txt' });
}
case 'custom': {
const method = (hook.method || 'POST').toUpperCase();
let body;
if (method === 'GET') {
body = undefined;
} else if (typeof hook.body === 'string') {
body = hook.body.replaceAll('{{title}}', title).replaceAll('{{text}}', text).replaceAll('{{url}}', extra?.url || '');
} else {
body = JSON.stringify({ title, text, ...extra });
}
const r = await fetch(url, {
method,
headers: { 'content-type': typeof hook.body === 'string' ? 'text/plain; charset=utf-8' : 'application/json', ...(hook.headers || {}) },
body: method === 'GET' ? undefined : body,
signal: AbortSignal.timeout(10000)
});
return { status: r.status, text: (await r.text()).slice(0, 300) };
}
default:
throw new Error(`未知 webhook 类型: ${type}`);
}
}
async function notify(cfg, title, text, extra = {}, logger = console.log) {
if (cfg.notify?.toast) {
toast(title, text, cfg.notify?.repeatSound ?? 1);
}
const hooks = cfg.webhooks || [];
if (!hooks.length) return;
await Promise.all(hooks.map(async (h) => {
try {
const r = await sendWebhook(h, title, text, extra);
logger(`[webhook] ${h.type} ${h.url.slice(0, 60)} -> ${r.status}`);
} catch (e) {
logger(`[webhook] ${h.type} 失败: ${e.message}`);
}
}));
}
module.exports = { notify, toast, sendWebhook };