v0.2.4: fix ad key-words comma split + deleteAdFiles entry for extensions only

This commit is contained in:
root
2026-05-16 19:49:58 +08:00
parent e38adee8ff
commit b5d3620273
30 changed files with 1458 additions and 335 deletions

View File

@@ -0,0 +1,44 @@
import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.types';
const params: NotifierParam[] = [
{ key: 'key', label: 'Bark Key', type: 'text', required: true, placeholder: 'xxxxxxxxxxxxxxxxx' },
{ key: 'server', label: '服务器', type: 'url', default: 'https://api.day.app', required: false, placeholder: 'https://api.day.app' },
{ key: 'title', label: '标题', type: 'text', default: 'CloudSearch 通知', required: false },
{ key: 'content', label: '内容', type: 'text', required: true },
{ key: 'level', label: '级别', type: 'text', default: 'info', required: false },
];
export const barkNotifier: Notifier = {
name: 'bark',
label: 'Bark',
params,
async notify(params: NotifyParams): Promise<NotifyResult> {
try {
const key = params.key;
const server = (params.server || 'https://api.day.app').replace(/\/+$/, '');
const title = params.title || 'CloudSearch';
const content = params.content || '';
const level: string = params.level || 'info';
const icon = level === 'error' ? '⚠️' : level === 'warn' ? '🔔' : '';
const resp = await fetch(`${server}/${key}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: `${icon} ${title}`,
body: content,
group: 'CloudSearch',
level: level === 'error' ? 'timeSensitive' : 'active',
icon: '',
}),
});
if (!resp.ok) {
const text = await resp.text();
return { success: false, message: `HTTP ${resp.status}: ${text.slice(0, 100)}` };
}
return { success: true, message: 'Bark 推送成功' };
} catch (err: any) {
return { success: false, message: err.message };
}
},
};

View File

@@ -0,0 +1,34 @@
import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.types';
const params: NotifierParam[] = [
{ key: 'webhook_url', label: 'Webhook URL', type: 'url', required: true, placeholder: 'https://oapi.dingtalk.com/robot/send?access_token=xxx' },
{ key: 'title', label: '标题', type: 'text', default: 'CloudSearch', required: false },
{ key: 'content', label: '内容', type: 'text', required: true },
{ key: 'level', label: '级别', type: 'text', default: 'info', required: false },
];
export const dingtalkNotifier: Notifier = {
name: 'dingtalk',
label: '钉钉机器人',
params,
async notify(params: NotifyParams): Promise<NotifyResult> {
try {
const level: string = params.level || 'info';
const text = `## ${params.title || 'CloudSearch'}\n${params.content || ''}`;
const resp = await fetch(params.webhook_url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
msgtype: 'markdown',
markdown: { title: params.title || 'CloudSearch', text },
at: { isAtAll: false },
}),
});
const data: any = await resp.json();
if (data.errcode === 0) return { success: true, message: '钉钉推送成功' };
return { success: false, message: data.errmsg || `HTTP ${resp.status}` };
} catch (err: any) {
return { success: false, message: err.message };
}
},
};

View File

@@ -0,0 +1,36 @@
import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.types';
const params: NotifierParam[] = [
{ key: 'webhook_url', label: 'Webhook URL', type: 'url', required: true, placeholder: 'https://discord.com/api/webhooks/...' },
{ key: 'title', label: '标题', type: 'text', default: 'CloudSearch', required: false },
{ key: 'content', label: '内容', type: 'text', required: true },
{ key: 'level', label: '级别', type: 'text', default: 'info', required: false },
];
export const discordNotifier: Notifier = {
name: 'discord',
label: 'Discord',
params,
async notify(params: NotifyParams): Promise<NotifyResult> {
try {
const level: string = params.level || 'info';
const color = level === 'error' ? 0xff0000 : level === 'warn' ? 0xffa500 : 0x3498db;
const resp = await fetch(params.webhook_url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
embeds: [{
title: params.title || 'CloudSearch',
description: params.content || '',
color,
footer: { text: 'CloudSearch \u00b7 ' + new Date().toLocaleString('zh-CN') },
}],
}),
});
if (resp.ok) return { success: true, message: 'Discord 推送成功' };
return { success: false, message: `HTTP ${resp.status}` };
} catch (err: any) {
return { success: false, message: err.message };
}
},
};

View File

@@ -0,0 +1,33 @@
import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.types';
const params: NotifierParam[] = [
{ key: 'server', label: '服务器地址', type: 'url', required: true, placeholder: 'https://gotify.example.com' },
{ key: 'token', label: 'App Token', type: 'password', required: true, placeholder: 'Gotify App Token' },
{ key: 'title', label: '标题', type: 'text', default: 'CloudSearch', required: false },
{ key: 'content', label: '内容', type: 'text', required: true },
{ key: 'priority', label: '优先级', type: 'number', default: 5, required: false },
];
export const gotifyNotifier: Notifier = {
name: 'gotify',
label: 'Gotify',
params,
async notify(params: NotifyParams): Promise<NotifyResult> {
try {
const server = params.server.replace(/\/+$/, '');
const resp = await fetch(`${server}/message?token=${params.token}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: params.title || 'CloudSearch',
message: params.content || '',
priority: params.priority || 5,
}),
});
if (resp.ok) return { success: true, message: 'Gotify 推送成功' };
return { success: false, message: `HTTP ${resp.status}` };
} catch (err: any) {
return { success: false, message: err.message };
}
},
};

View File

@@ -0,0 +1,72 @@
/**
* notifiers/index.ts — 注册器
* 统一管理所有通知渠道,支持按 name 查找和列表获取
*/
import { Notifier } from './notifier.types';
import { barkNotifier } from './bark.notifier';
import { serverchanNotifier } from './serverchan.notifier';
import { serverchanturboNotifier } from './serverchanturbo.notifier';
import { telegramNotifier } from './telegram.notifier';
import { larkNotifier } from './lark.notifier';
import { webhookNotifier } from './webhook.notifier';
import { wechatWorkBotNotifier } from './wechat_work_bot.notifier';
import { pushplusNotifier } from './pushplus.notifier';
import { dingtalkNotifier } from './dingtalk.notifier';
import { gotifyNotifier } from './gotify.notifier';
import { ntfyNotifier } from './ntfy.notifier';
import { discordNotifier } from './discord.notifier';
import { smtpNotifier } from './smtp.notifier';
import { qmsgNotifier } from './qmsg.notifier';
const registry = new Map<string, Notifier>();
function register(n: Notifier): void {
registry.set(n.name, n);
}
// ==================== 注册所有内置通知器 ====================
register(barkNotifier);
register(serverchanNotifier);
register(serverchanturboNotifier);
register(telegramNotifier);
register(larkNotifier);
register(webhookNotifier);
register(wechatWorkBotNotifier);
register(pushplusNotifier);
register(dingtalkNotifier);
register(gotifyNotifier);
register(ntfyNotifier);
register(discordNotifier);
register(smtpNotifier);
register(qmsgNotifier);
// ==================== API ====================
/** 根据名称获取通知器 */
export function getNotifier(name: string): Notifier | undefined {
return registry.get(name);
}
/** 获取所有已注册的通知器 */
export function getAllNotifiers(): Notifier[] {
return Array.from(registry.values());
}
/** 获取所有通知器的参数定义(用于前端动态生成表单) */
export function getAllNotifierParams(): Record<string, any> {
const result: Record<string, any> = {};
for (const [name, n] of registry) {
result[name] = { name: n.name, label: n.label, params: n.params };
}
return result;
}
/** 向指定通知器发送通知 */
export async function notifyWith(name: string, params: Record<string, any>): Promise<{ success: boolean; message: string }> {
const n = getNotifier(name);
if (!n) return { success: false, message: `未知的通知渠道: ${name}` };
return n.notify(params);
}

View File

@@ -0,0 +1,39 @@
import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.types';
const params: NotifierParam[] = [
{ key: 'webhook_url', label: 'Webhook URL', type: 'url', required: true, placeholder: 'https://open.feishu.cn/open-apis/bot/v2/hook/xxx' },
{ key: 'title', label: '标题', type: 'text', default: 'CloudSearch', required: false },
{ key: 'content', label: '内容', type: 'text', required: true },
{ key: 'level', label: '级别', type: 'text', default: 'info', required: false },
];
export const larkNotifier: Notifier = {
name: 'lark',
label: '飞书/Lark',
params,
async notify(params: NotifyParams): Promise<NotifyResult> {
try {
const level: string = params.level || 'info';
const template = level === 'error' ? 'red' : level === 'warn' ? 'orange' : 'blue';
const resp = await fetch(params.webhook_url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
msg_type: 'interactive',
card: {
header: { title: { tag: 'plain_text', content: params.title || 'CloudSearch' }, template },
elements: [
{ tag: 'div', text: { tag: 'lark_md', content: params.content || '' } },
{ tag: 'note', elements: [{ tag: 'plain_text', content: 'CloudSearch \u00b7 ' + new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }) }] },
],
},
}),
});
if (resp.ok) return { success: true, message: '飞书推送成功' };
const text = await resp.text();
return { success: false, message: text.slice(0, 150) };
} catch (err: any) {
return { success: false, message: err.message };
}
},
};

View File

@@ -0,0 +1,34 @@
/**
* 通知器类型定义 —— 参考 onepush 插件化设计
* 每个 provider 注册为一个 Notifier统一 notify(params) 接口
* 新增通道只需在这里加一个文件 + 在 index.ts 注册
*/
export type NotifyLevel = 'info' | 'warn' | 'error';
export interface NotifyParams {
[key: string]: any;
}
export interface NotifyResult {
success: boolean;
message: string;
}
/** 每个 provider 必须实现这个接口 */
export interface Notifier {
name: string;
label: string;
/** 参数描述(用于前端动态生成表单) */
params: NotifierParam[];
notify(params: NotifyParams): Promise<NotifyResult>;
}
export interface NotifierParam {
key: string;
label: string;
type: 'text' | 'password' | 'url' | 'switch' | 'number';
placeholder?: string;
default?: any;
required: boolean;
}

View File

@@ -0,0 +1,34 @@
import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.types';
const params: NotifierParam[] = [
{ key: 'topic', label: 'Topic', type: 'text', required: true, placeholder: 'my-notification-topic' },
{ key: 'server', label: '服务器', type: 'url', default: 'https://ntfy.sh', required: false, placeholder: 'https://ntfy.sh' },
{ key: 'title', label: '标题', type: 'text', default: 'CloudSearch', required: false },
{ key: 'content', label: '内容', type: 'text', required: true },
{ key: 'priority', label: '优先级(1-5)', type: 'number', default: 3, required: false },
];
export const ntfyNotifier: Notifier = {
name: 'ntfy',
label: 'ntfy',
params,
async notify(params: NotifyParams): Promise<NotifyResult> {
try {
const server = (params.server || 'https://ntfy.sh').replace(/\/+$/, '');
const resp = await fetch(`${server}/${params.topic}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: params.title || 'CloudSearch',
message: params.content || '',
priority: params.priority || 3,
tags: ['cloudsearch'],
}),
});
if (resp.ok) return { success: true, message: 'ntfy 推送成功' };
return { success: false, message: `HTTP ${resp.status}` };
} catch (err: any) {
return { success: false, message: err.message };
}
},
};

View File

@@ -0,0 +1,31 @@
import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.types';
const params: NotifierParam[] = [
{ key: 'token', label: 'Token', type: 'password', required: true, placeholder: 'pushplus token' },
{ key: 'title', label: '标题', type: 'text', default: 'CloudSearch', required: false },
{ key: 'content', label: '内容', type: 'text', required: true },
];
export const pushplusNotifier: Notifier = {
name: 'pushplus',
label: 'PushPlus (微信)',
params,
async notify(params: NotifyParams): Promise<NotifyResult> {
try {
const resp = await fetch('https://www.pushplus.plus/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: params.token,
title: params.title || 'CloudSearch',
content: params.content || '',
}),
});
const data: any = await resp.json();
if (data.code === 200) return { success: true, message: 'PushPlus 推送成功' };
return { success: false, message: data.msg || `HTTP ${resp.status}` };
} catch (err: any) {
return { success: false, message: err.message };
}
},
};

View File

@@ -0,0 +1,33 @@
import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.types';
const params: NotifierParam[] = [
{ key: 'key', label: 'API Key', type: 'password', required: true, placeholder: 'Qmsg API Key' },
{ key: 'qq', label: 'QQ 号', type: 'text', required: false, placeholder: '留空则推送到所有绑定QQ' },
{ key: 'title', label: '标题', type: 'text', default: 'CloudSearch', required: false },
{ key: 'content', label: '内容', type: 'text', required: true },
];
export const qmsgNotifier: Notifier = {
name: 'qmsg',
label: 'Qmsg (QQ)',
params,
async notify(params: NotifyParams): Promise<NotifyResult> {
try {
const body: Record<string, string> = {
msg: `[${params.title || 'CloudSearch'}]\n${params.content || ''}`,
type: 'send',
};
if (params.qq) body.qq = params.qq;
const resp = await fetch(`https://qmsg.zendee.cn/api/${params.key}`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(body).toString(),
});
const data: any = await resp.json();
if (data.code === 0) return { success: true, message: 'Qmsg 推送成功' };
return { success: false, message: data.reason || `HTTP ${resp.status}` };
} catch (err: any) {
return { success: false, message: err.message };
}
},
};

View File

@@ -0,0 +1,28 @@
import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.types';
const params: NotifierParam[] = [
{ key: 'sendkey', label: 'SendKey', type: 'password', required: true, placeholder: 'Server酱 SendKey' },
{ key: 'title', label: '标题', type: 'text', default: 'CloudSearch', required: false },
{ key: 'content', label: '内容', type: 'text', required: true },
];
export const serverchanNotifier: Notifier = {
name: 'serverchan',
label: 'Server酱',
params,
async notify(params: NotifyParams): Promise<NotifyResult> {
try {
const sendkey = params.sendkey;
const resp = await fetch(`https://sctapi.ftqq.com/${sendkey}.send`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ title: params.title || 'CloudSearch', desp: params.content || '' }).toString(),
});
const data: any = await resp.json();
if (data.code === 0) return { success: true, message: 'Server酱 推送成功' };
return { success: false, message: data.message || `HTTP ${resp.status}` };
} catch (err: any) {
return { success: false, message: err.message };
}
},
};

View File

@@ -0,0 +1,27 @@
import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.types';
const params: NotifierParam[] = [
{ key: 'sendkey', label: 'SendKey', type: 'password', required: true, placeholder: 'Server酱 Turbo SendKey' },
{ key: 'title', label: '标题', type: 'text', default: 'CloudSearch', required: false },
{ key: 'content', label: '内容', type: 'text', required: true },
];
export const serverchanturboNotifier: Notifier = {
name: 'serverchanturbo',
label: 'Server酱 Turbo',
params,
async notify(params: NotifyParams): Promise<NotifyResult> {
try {
const resp = await fetch(`https://sctapi.ftqq.com/${params.sendkey}.send`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ title: params.title || 'CloudSearch', desp: params.content || '' }).toString(),
});
const data: any = await resp.json();
if (data.code === 0) return { success: true, message: 'Server酱 Turbo 推送成功' };
return { success: false, message: data.message || `HTTP ${resp.status}` };
} catch (err: any) {
return { success: false, message: err.message };
}
},
};

View File

@@ -0,0 +1,40 @@
import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.types';
/** SMTP 邮件推送 — 使用 nodemailer需要安装 */
const params: NotifierParam[] = [
{ key: 'host', label: 'SMTP 服务器', type: 'text', required: true, placeholder: 'smtp.qq.com' },
{ key: 'port', label: '端口', type: 'number', default: 465, required: false },
{ key: 'secure', label: 'SSL', type: 'switch', default: true, required: false },
{ key: 'user', label: '用户名', type: 'text', required: true, placeholder: 'user@example.com' },
{ key: 'pass', label: '密码/授权码', type: 'password', required: true, placeholder: 'SMTP 授权码' },
{ key: 'from', label: '发件人', type: 'text', required: true, placeholder: 'sender@example.com' },
{ key: 'to', label: '收件人', type: 'text', required: true, placeholder: 'receiver@example.com' },
{ key: 'title', label: '标题', type: 'text', default: 'CloudSearch', required: false },
{ key: 'content', label: '内容', type: 'text', required: true },
];
export const smtpNotifier: Notifier = {
name: 'smtp',
label: 'SMTP 邮件',
params,
async notify(params: NotifyParams): Promise<NotifyResult> {
try {
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport({
host: params.host,
port: params.port || 465,
secure: params.secure !== false,
auth: { user: params.user, pass: params.pass },
});
await transporter.sendMail({
from: params.from,
to: params.to,
subject: `[CloudSearch] ${params.title || ''}`,
text: params.content || '',
});
return { success: true, message: 'SMTP 邮件发送成功' };
} catch (err: any) {
return { success: false, message: err.message };
}
},
};

View File

@@ -0,0 +1,38 @@
import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.types';
const params: NotifierParam[] = [
{ key: 'token', label: 'Bot Token', type: 'password', required: true, placeholder: '123456:ABC-def' },
{ key: 'chat_id', label: 'Chat ID', type: 'text', required: true, placeholder: '@频道 或 -1001234567890' },
{ key: 'title', label: '标题', type: 'text', default: 'CloudSearch', required: false },
{ key: 'content', label: '内容', type: 'text', required: true },
{ key: 'level', label: '级别', type: 'text', default: 'info', required: false },
];
export const telegramNotifier: Notifier = {
name: 'telegram',
label: 'Telegram',
params,
async notify(params: NotifyParams): Promise<NotifyResult> {
try {
const iconMap: Record<string, string> = { error: '\ud83d\udea8', warn: '\u26a0\ufe0f', info: '\u2139\ufe0f' };
const level: string = params.level || 'info';
const text = `${iconMap[level] || iconMap.info} *${params.title || 'CloudSearch'}*\n\n${params.content || ''}`;
const resp = await fetch(`https://api.telegram.org/bot${params.token}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: params.chat_id,
text,
parse_mode: 'Markdown',
disable_web_page_preview: true,
}),
});
const data: any = await resp.json();
if (data.ok) return { success: true, message: 'Telegram 推送成功' };
return { success: false, message: data.description || `HTTP ${resp.status}` };
} catch (err: any) {
return { success: false, message: err.message };
}
},
};

View File

@@ -0,0 +1,33 @@
import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.types';
const params: NotifierParam[] = [
{ key: 'url', label: 'Webhook URL', type: 'url', required: true, placeholder: 'https://example.com/webhook' },
{ key: 'title', label: '标题', type: 'text', default: 'CloudSearch', required: false },
{ key: 'content', label: '内容', type: 'text', required: true },
{ key: 'level', label: '级别', type: 'text', default: 'info', required: false },
];
export const webhookNotifier: Notifier = {
name: 'webhook',
label: '自定义 Webhook',
params,
async notify(params: NotifyParams): Promise<NotifyResult> {
try {
const resp = await fetch(params.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: params.title || 'CloudSearch',
content: params.content || '',
level: params.level || 'info',
source: 'CloudSearch',
timestamp: new Date().toISOString(),
}),
});
if (resp.ok) return { success: true, message: 'Webhook 推送成功' };
return { success: false, message: `HTTP ${resp.status}` };
} catch (err: any) {
return { success: false, message: err.message };
}
},
};

View File

@@ -0,0 +1,32 @@
import { Notifier, NotifyParams, NotifyResult, NotifierParam } from './notifier.types';
const params: NotifierParam[] = [
{ key: 'webhook_url', label: 'Webhook URL', type: 'url', required: true, placeholder: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx' },
{ key: 'title', label: '标题', type: 'text', default: 'CloudSearch', required: false },
{ key: 'content', label: '内容', type: 'text', required: true },
{ key: 'level', label: '级别', type: 'text', default: 'info', required: false },
];
export const wechatWorkBotNotifier: Notifier = {
name: 'wechat_work_bot',
label: '企业微信机器人',
params,
async notify(params: NotifyParams): Promise<NotifyResult> {
try {
const content = `## ${params.title || 'CloudSearch'}\n${params.content || ''}`;
const resp = await fetch(params.webhook_url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
msgtype: 'markdown',
markdown: { content },
}),
});
const data: any = await resp.json();
if (data.errcode === 0) return { success: true, message: '企业微信推送成功' };
return { success: false, message: data.errmsg || `HTTP ${resp.status}` };
} catch (err: any) {
return { success: false, message: err.message };
}
},
};