96 lines
2.7 KiB
TypeScript
96 lines
2.7 KiB
TypeScript
// Native fetch available in Node 20+
|
||
import { getSystemConfig } from '../admin/system-config.service';
|
||
|
||
type NotifyLevel = 'info' | 'warn' | 'error';
|
||
|
||
interface NotifyChannel {
|
||
send(title: string, content: string, level: NotifyLevel): Promise<void>;
|
||
}
|
||
|
||
// ---- Feishu Webhook Channel ----
|
||
class FeishuChannel implements NotifyChannel {
|
||
private webhookUrl: string;
|
||
|
||
constructor(webhookUrl: string) {
|
||
this.webhookUrl = webhookUrl;
|
||
}
|
||
|
||
async send(title: string, content: string, _level: NotifyLevel): Promise<void> {
|
||
try {
|
||
const body = JSON.stringify({
|
||
msg_type: 'interactive',
|
||
card: {
|
||
header: {
|
||
title: { tag: 'plain_text', content: title },
|
||
template: _level === 'error' ? 'red' : _level === 'warn' ? 'orange' : 'blue',
|
||
},
|
||
elements: [
|
||
{ tag: 'div', text: { tag: 'lark_md', content } },
|
||
{
|
||
tag: 'note',
|
||
elements: [
|
||
{ tag: 'plain_text', content: `CloudSearch · ${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}` },
|
||
],
|
||
},
|
||
],
|
||
},
|
||
});
|
||
|
||
const resp = await fetch(this.webhookUrl, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body,
|
||
});
|
||
|
||
if (!resp.ok) {
|
||
console.error(`[Notify] Feishu send failed: ${resp.status}`);
|
||
}
|
||
} catch (err: any) {
|
||
console.error('[Notify] Feishu send error:', err.message);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---- Notification Manager ----
|
||
let _channel: NotifyChannel | null = null;
|
||
|
||
function getChannel(): NotifyChannel | null {
|
||
const feishuUrl = process.env.FEISHU_WEBHOOK || getSystemConfig('feishu_webhook_url');
|
||
if (!feishuUrl) return null;
|
||
|
||
if (!_channel) {
|
||
_channel = new FeishuChannel(feishuUrl);
|
||
console.log('[Notify] Feishu webhook configured');
|
||
}
|
||
return _channel;
|
||
}
|
||
|
||
/**
|
||
* Send a notification through configured channels.
|
||
* Returns immediately — failures are logged silently.
|
||
*/
|
||
export function notify(title: string, content: string, level: NotifyLevel = 'info'): void {
|
||
const ch = getChannel();
|
||
if (!ch) return;
|
||
// Fire-and-forget — don't block the caller
|
||
ch.send(title, content, level).catch(() => {});
|
||
}
|
||
|
||
/**
|
||
* Notify on critical events:
|
||
* - Cookie expired / login failed
|
||
* - Save/transfer failed repeatedly
|
||
* - Storage below threshold
|
||
*/
|
||
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');
|
||
}
|