Files
CloudSearch/packages/backend/src/cloud/notification.service.ts

269 lines
9.2 KiB
TypeScript
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.
// ============================================================
// 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);
}