// ============================================================ // notification.service.ts — 插件化消息推送 // 基于 notifiers/ 注册器,支持 14+ 个推送通道 // 支持:全局推送(系统配置)和按网盘配置推送 // ============================================================ import { getSystemConfig } from '../admin/system-config.service'; import { findPushUserForConfig } from './push-user.service'; import { getAllNotifiers, getNotifier, notifyWith } from './notifiers'; export { getAllNotifiers, getNotifier, getAllNotifierParams } from './notifiers'; export type { NotifyLevel, Notifier, NotifierParam, NotifyParams, NotifyResult } from './notifiers/notifier.types'; /** 用户级推送配置(存储在 cloud_configs.notify_config) */ export interface PerConfigNotify { // 每个通道的配置 = { channelName: { paramKey: value, ... } } channels?: Record>; events?: { on_save_success?: boolean; on_save_fail?: boolean; on_cookie_expire?: boolean; on_cleanup?: boolean; }; } // ======================== 全局(系统级)通道管理 ======================== let _globalChannelsCache: { name: string; params: Record }[] | null = null; let _configHash: string = ''; function getGlobalNotifyConfigs(): { name: string; params: Record }[] { // 从 global_notify_config JSON 读取(格式同 push user 的 notify_config) const raw = getSystemConfig('global_notify_config') || '{}'; let globalConfig: any = {}; try { globalConfig = JSON.parse(raw); } catch {} const channels: { name: string; params: Record }[] = []; if (globalConfig.channels) { for (const [name, params] of Object.entries(globalConfig.channels)) { if (params && typeof params === 'object') { channels.push({ name, params: { ...(params as Record), title: 'CloudSearch' } }); } } } return channels; } function checkEventEnabled(eventName: string): boolean { try { // 优先读 global_notify_config.events const raw = getSystemConfig('global_notify_config') || '{}'; let globalConfig: any = {}; try { globalConfig = JSON.parse(raw); } catch {} if (globalConfig.events && globalConfig.events[`on_${eventName}`] !== undefined) { return globalConfig.events[`on_${eventName}`] !== false; } // 降级到旧字段 const val = getSystemConfig(`notify_on_${eventName}`); return val !== 'false' && val !== '0'; } catch { return true; } } // ======================== 核心发送函数 ======================== async function sendToChannels( channels: { name: string; params: Record }[], title: string, content: string, level: string ): Promise { for (const ch of channels) { try { await notifyWith(ch.name, { ...ch.params, title, content, level, }); } catch (err: any) { console.error(`[Notify] ${ch.name} error:`, err.message); } } } // ======================== 导出 API ======================== /** 通过全局通道发送通知(不检查事件开关) */ export function notify(title: string, content: string, level: 'info' | 'warn' | 'error' = 'info'): void { const channels = getGlobalNotifyConfigs(); if (channels.length === 0) return; sendToChannels(channels, title, content, level).catch(() => {}); } export function notifyError(title: string, detail: string): void { notify(title, detail, 'error'); } export function notifyWarn(title: string, detail: string): void { notify(title, detail, 'warn'); } export function notifyInfo(title: string, detail: string): void { notify(title, detail, 'info'); } /** 事件通知(检查全局事件开关) */ export function notifyEvent( eventName: string, title: string, content: string, level: 'info' | 'warn' | 'error' = 'info', templateVars?: Record ): void { if (!checkEventEnabled(eventName)) return; // Apply global template if available const eventKey = 'on_' + eventName; const templates = getEventTemplates(); const tmpl = templates[eventKey]; if (tmpl && tmpl.content) { const vars: Record = { ...(templateVars || {}) }; if (!vars.content && content) vars.content = content; if (!vars.title && title) vars.title = title; title = applyTemplate(tmpl.title || title, vars); content = applyTemplate(tmpl.content, vars); } notify(title, content, level); } /** 按网盘配置发送通知(检查用户级推送配置,无配置则降级到全局) */ const DB_PATH = '/app/dist/database/database'; function getConfigNotifySettings(configId: number): PerConfigNotify { try { const { getDb } = require(DB_PATH); const db = getDb(); const row = db.prepare('SELECT notify_config FROM cloud_configs WHERE id = ?').get(configId) as any; if (row && row.notify_config) { return JSON.parse(row.notify_config); } } catch {} return {}; } function applyTemplate(template: string, vars: Record): string { return template.replace(/\{([^}]+)\}/g, (_, key) => vars[key] || '{' + key + '}'); } function getEventTemplates(): Record { try { const raw = getSystemConfig('global_notify_config') || '{}'; const cfg = JSON.parse(raw); return cfg.eventTemplates || {}; } catch { return {}; } } export function notifyConfigEvent( configId: number, eventName: string, title: string, content: string, level: 'info' | 'warn' | 'error' = 'info', templateVars?: Record ): void { // Apply global template if available const eventKey = 'on_' + eventName; const templates = getEventTemplates(); const tmpl = templates[eventKey]; if (tmpl && tmpl.content) { const vars: Record = { ...(templateVars || {}) }; if (!vars.content && content) vars.content = content; if (!vars.title && title) vars.title = title; const appliedTitle = applyTemplate(tmpl.title || title, vars); const appliedContent = applyTemplate(tmpl.content, vars); title = appliedTitle; content = appliedContent; } // Find matching push user by cloud_configs.promotion_account const pushUser = findPushUserForConfig(configId); if (!pushUser) { // No matching push user, fallback to global notifyEvent(eventName, title, content, level); return; } let notifyConfig: any = {}; try { notifyConfig = JSON.parse(pushUser.notify_config); } catch {} // Check event switch if (notifyConfig.events && notifyConfig.events[eventKey] === false) return; // Build channels from push user config const userChannels: { name: string; params: Record }[] = []; if (notifyConfig.channels) { for (const [name, params] of Object.entries(notifyConfig.channels)) { userChannels.push({ name, params: { ...(params as Record), title } }); } } if (userChannels.length > 0) { sendToChannels(userChannels, title, content, level).catch(() => {}); } else { // Fallback to global notifyEvent(eventName, title, content, level); } } /** 测试某个通道 */ export async function testChannel( channelName: string, account?: string, directParams?: Record ): Promise<{ success: boolean; message: string }> { let params: Record = {}; // If direct params provided, use them directly (for global panel test without saving first) if (directParams) { params = directParams; } else if (account) { const pushUser = findPushUserForConfig(undefined); // Use pushUser lookup by account instead const { getPushUserByAccount } = require('./push-user.service'); const user = getPushUserByAccount(account); if (user) { let notifyConfig: any = {}; try { notifyConfig = JSON.parse(user.notify_config); } catch {} const chParams = notifyConfig.channels?.[channelName]; if (!chParams) return { success: false, message: '该用户未配置此渠道' }; params = chParams; } else { return { success: false, message: '未找到该推送用户' }; } } else { const channels = getGlobalNotifyConfigs(); const ch = channels.find(c => c.name === channelName); if (!ch) return { success: false, message: '全局未配置此渠道' }; params = ch.params; } const result = await notifyWith(channelName, { ...params, title: '\ud83d\udd14 CloudSearch \u6d4b\u8bd5', content: `\u8fd9\u662f\u4e00\u6761\u6d4b\u8bd5\u6d88\u606f\n\u901a\u9053: ${channelName}\n\u65f6\u95f4: ${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}`, level: 'info', }); return result; } /** 保存用户级推送配置到数据库 */ export function saveConfigNotifySettings(configId: number, notify: PerConfigNotify): void { try { const { getDb } = require(DB_PATH); const db = getDb(); db.prepare('UPDATE cloud_configs SET notify_config = ? WHERE id = ?').run( JSON.stringify(notify), configId ); } catch (err: any) { console.error('[Notify] Failed to save config notify settings:', err.message); } } /** 获取用户级推送配置 */ export function getConfigNotifySettingsJSON(configId: number): PerConfigNotify { return getConfigNotifySettings(configId); }