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

107 lines
4.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 通知模块: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();
// url 支持 "env:变量名" —— 密钥不落在配置文件里(Obsidian 库会同步/进 Git)
const url = typeof hook.url === 'string' && hook.url.startsWith('env:')
? process.env[hook.url.slice(4)]
: hook.url;
if (!url) throw new Error('webhook.url 为空(或 env: 指定的环境变量未设置)');
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 };