269 lines
9.2 KiB
TypeScript
269 lines
9.2 KiB
TypeScript
// ============================================================
|
||
// 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<string, Record<string, string>>;
|
||
events?: {
|
||
on_save_success?: boolean;
|
||
on_save_fail?: boolean;
|
||
on_cookie_expire?: boolean;
|
||
on_cleanup?: boolean;
|
||
};
|
||
}
|
||
|
||
// ======================== 全局(系统级)通道管理 ========================
|
||
|
||
let _globalChannelsCache: { name: string; params: Record<string, string> }[] | null = null;
|
||
let _configHash: string = '';
|
||
|
||
function getGlobalNotifyConfigs(): { name: string; params: Record<string, string> }[] {
|
||
// 从 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<string, string> }[] = [];
|
||
if (globalConfig.channels) {
|
||
for (const [name, params] of Object.entries(globalConfig.channels)) {
|
||
if (params && typeof params === 'object') {
|
||
channels.push({ name, params: { ...(params as Record<string, string>), 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<string, string> }[],
|
||
title: string,
|
||
content: string,
|
||
level: string
|
||
): Promise<void> {
|
||
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<string, string>
|
||
): 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<string, string> = { ...(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, string>): string {
|
||
return template.replace(/\{([^}]+)\}/g, (_, key) => vars[key] || '{' + key + '}');
|
||
}
|
||
|
||
function getEventTemplates(): Record<string, { title: string; content: string }> {
|
||
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<string, string>
|
||
): void {
|
||
// Apply global template if available
|
||
const eventKey = 'on_' + eventName;
|
||
const templates = getEventTemplates();
|
||
const tmpl = templates[eventKey];
|
||
if (tmpl && tmpl.content) {
|
||
const vars: Record<string, string> = { ...(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<string, string> }[] = [];
|
||
if (notifyConfig.channels) {
|
||
for (const [name, params] of Object.entries(notifyConfig.channels)) {
|
||
userChannels.push({ name, params: { ...(params as Record<string, string>), 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<string, string>
|
||
): Promise<{ success: boolean; message: string }> {
|
||
let params: Record<string, string> = {};
|
||
|
||
// 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);
|
||
} |