perf: 通知支持vocechat、bark、telegram、discord、slack

pull/265/head
xiaojunnuo 2024-11-26 15:13:57 +08:00
parent cbccd9e3d0
commit 642f57ff6d
16 changed files with 315 additions and 19 deletions

View File

@ -367,16 +367,16 @@ export class Executor {
const errorMessage = error?.message;
if (when === "start") {
subject = `【Certd】开始执行${this.pipeline.id}${this.pipeline.title}`;
content = `buildId:${this.runtime.id}\n查看详情:${url}`;
content = `buildId:${this.runtime.id}`;
} else if (when === "success") {
subject = `【Certd】执行成功${this.pipeline.id}${this.pipeline.title}`;
content = `buildId:${this.runtime.id}\n查看详情:${url}`;
content = `buildId:${this.runtime.id}`;
} else if (when === "turnToSuccess") {
subject = `【Certd】执行成功失败转成功${this.pipeline.id}${this.pipeline.title}`;
content = `buildId:${this.runtime.id}\n查看详情:${url}`;
content = `buildId:${this.runtime.id}`;
} else if (when === "error") {
subject = `【Certd】执行失败${this.pipeline.id}${this.pipeline.title}`;
content = `buildId:${this.runtime.id}\n查看详情:${url}\nerror:${error.message}`;
content = `buildId:${this.runtime.id}\nerror:${error.message}`;
} else {
return;
}

View File

@ -98,14 +98,14 @@ export abstract class BaseNotification implements INotification {
await this.send({
userId: 0,
title: "【Certd】测试通知",
content: "测试通知\n\n查看详情http://www.baidu.com",
content: "测试通知",
pipeline: {
id: 1,
title: "测试流水线",
} as any,
pipelineId: 1,
historyId: 1,
url: "http://www.baidu.com",
url: "https://certd.docmirror.cn",
});
}
}

View File

@ -199,11 +199,13 @@ function openUpgrade() {
const vipTypeDefine = {
free: {
title: "基础版",
desc: "免费使用",
type: "free",
privilege: ["证书申请功能无限制", "证书流水线数量10条", "常用的主机、cdn等部署插件"]
},
plus: {
title: "专业版",
desc: "功能增强,适用于个人企业内部使用",
type: "plus",
privilege: ["可加VIP群需求优先实现", "证书流水线数量无限制", "免配置发邮件功能", "支持宝塔、易盾、群晖、1Panel、cdnfly等部署插件"],
trial: {
@ -215,6 +217,7 @@ function openUpgrade() {
},
comm: {
title: "商业版",
desc: "商业授权,可对外运营",
type: "comm",
privilege: ["拥有专业版所有特权", "允许商用可修改logo、标题", "数据统计", "插件管理", "多用户无限制", "支持用户支付(敬请期待)"]
}
@ -268,6 +271,7 @@ function openUpgrade() {
</span>
)}
</h3>
<div>{item.desc}</div>
<ul>
{item.privilege.map((p: string) => (
<li>
@ -333,7 +337,7 @@ function openUpgrade() {
padding: 10px;
border: 1px solid #eee;
border-radius: 5px;
height: 170px;
height: 195px;
//background-color: rgba(250, 237, 167, 0.79);
&.current {
border-color: green;

View File

@ -77,6 +77,12 @@ h1, h2, h3, h4, h5, h6 {
display: flex;
flex-direction: column;
}
.align-left{
text-align: left;
}
.align-right{
text-align: right;
}
.scroll-y {
overflow-y: auto;

View File

@ -180,8 +180,10 @@
</div>
<div class="task">
<a-button shape="round" @click="notificationEdit(item, ii as number)">
<fs-icon icon="ion:notifications"></fs-icon>
通知 {{ item.title || item.type }}
<div class="flex-o w-100">
<fs-icon icon="ion:notifications"></fs-icon>
<span class="ellipsis flex-1 step-title align-left"> 通知 {{ item.title || item.type }} </span>
</div>
</a-button>
</div>
</div>
@ -198,8 +200,10 @@
</div>
<div class="task">
<a-button shape="round" @click="notificationEdit(item, index)">
<fs-icon icon="ion:notifications"></fs-icon>
通知 {{ item.title || item.type }}
<div class="flex-o w-100">
<fs-icon icon="ion:notifications"></fs-icon>
<span class="ellipsis flex-1 step-title align-left"> 通知 {{ item.title || item.type }} </span>
</div>
</a-button>
</div>
</div>

View File

@ -39,7 +39,7 @@ export class AnPushNotification extends BaseNotification {
},
data: {
title: body.title,
content: body.content,
content: body.content + '[查看详情](' + body.url + ')',
channel: this.channel,
},
};

View File

@ -0,0 +1,52 @@
/**
* curl -X "POST" "https://api.day.app/your_key" \
* -H 'Content-Type: application/json; charset=utf-8' \
* -d $'{
* "body": "Test Bark Server",
* "title": "Test Title",
* "badge": 1,
* "category": "myNotificationCategory",
* "sound": "minuet.caf",
* "icon": "https://day.app/assets/images/avatar.jpg",
* "group": "test",
* "url": "https://mritd.com"
* }'
*/
import { BaseNotification, IsNotification, NotificationBody, NotificationInput } from '@certd/pipeline';
@IsNotification({
name: 'bark',
title: 'Bark 通知',
desc: 'Bark 推送通知插件',
})
export class BarkNotification extends BaseNotification {
@NotificationInput({
title: '服务地址',
component: {
placeholder: 'https://api.day.app/your_key',
},
required: true,
helper: '你的bark服务地址+key',
})
webhook = '';
async send(body: NotificationBody) {
if (!this.webhook) {
throw new Error('服务器地址不能为空');
}
const payload = {
body: body.content, // 使用传入的内容或默认内容
title: body.title, // 使用传入的标题或默认标题
};
await this.http.request({
url: `${this.webhook}`,
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
data: payload,
});
}
}

View File

@ -0,0 +1,53 @@
import { BaseNotification, IsNotification, NotificationBody, NotificationInput } from '@certd/pipeline';
@IsNotification({
name: 'discord',
title: 'Discord 通知',
desc: 'Discord 机器人通知',
})
export class DiscordNotification extends BaseNotification {
@NotificationInput({
title: 'Webhook URL',
component: {
placeholder: 'https://discord.com/api/webhooks/xxxxx/xxxx',
},
helper: '[Discord Webhook 说明](https://discord.com/developers/docs/resources/webhook#execute-webhook)',
required: true,
})
webhook = '';
@NotificationInput({
title: '提醒指定成员',
component: {
name: 'a-select',
vModel: 'value',
mode: 'tags',
open: false,
},
required: false,
helper: '填写成员的Id或者角色Id&id或者everyone',
})
mentionedList!: string[];
async send(body: NotificationBody) {
if (!this.webhook) {
throw new Error('Webhook URL 不能为空');
}
// 创建 Discord 消息体
let content = `${body.title}\n${body.content}\n[查看详情](${body.url})`;
if (this.mentionedList && this.mentionedList.length > 0) {
content += `\n${this.mentionedList.map(item => `<@${item}> `).join('')}`;
}
const json = {
content: content,
};
await this.http.request({
url: this.webhook,
method: 'POST',
data: json,
});
}
}

View File

@ -23,7 +23,7 @@ export class EmailNotification extends BaseNotification {
await this.ctx.emailService.send({
userId: body.userId,
subject: body.title,
content: body.content,
content: body.content + '\n[查看详情](' + body.url + ')',
receivers: this.receivers,
});
}

View File

@ -4,3 +4,7 @@ export * from './iyuu/index.js';
export * from './webhook/index.js';
export * from './serverchan/index.js';
export * from './anpush/index.js';
export * from './telegram/index.js';
export * from './discord/index.js';
export * from './slack/index.js';
export * from './bark/index.js';

View File

@ -25,7 +25,7 @@ export class IyuuNotification extends BaseNotification {
method: 'POST',
data: {
text: body.title,
desp: body.content,
desp: body.content + '[查看详情](' + body.url + ')',
},
});

View File

@ -52,7 +52,7 @@ export class ServerChanNotification extends BaseNotification {
method: 'POST',
data: {
text: body.title,
desp: body.content,
desp: body.content + '[查看详情](' + body.url + ')',
},
});
}

View File

@ -0,0 +1,32 @@
import { BaseNotification, IsNotification, NotificationBody, NotificationInput } from '@certd/pipeline';
@IsNotification({
name: 'slack',
title: 'Slack通知',
desc: 'Slack消息推送通知',
})
export class SlackNotification extends BaseNotification {
@NotificationInput({
title: 'webhook地址',
component: {
placeholder: 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX',
},
helper: '[APPS](https://api.slack.com/apps/)->进入APP->incoming-webhooks->Add New Webhook to Workspace',
required: true,
})
webhook = '';
async send(body: NotificationBody) {
if (!this.webhook) {
throw new Error('token不能为空');
}
await this.http.request({
url: this.webhook,
method: 'POST',
data: {
text: `${body.title}\n${body.content}\n[查看详情](${body.url})`,
},
});
}
}

View File

@ -0,0 +1,51 @@
import { BaseNotification, IsNotification, NotificationBody, NotificationInput } from '@certd/pipeline';
@IsNotification({
name: 'telegram',
title: 'Telegram通知',
desc: 'Telegram Bot推送通知',
})
export class TelegramNotification extends BaseNotification {
@NotificationInput({
title: 'Bot Token',
component: {
placeholder: '123456789:ABCdefGhijklmnopqrstUVWXyz',
},
helper: '[token获取](https://core.telegram.org/bots/features#botfather)',
required: true,
})
botToken = '';
@NotificationInput({
title: '聊天ID',
component: {
placeholder: '聊天ID例如 -123456789 或 @channelusername',
},
helper: '聊天的唯一标识符或用户名',
required: true,
})
chatId = '';
async send(body: NotificationBody) {
if (!this.botToken || !this.chatId) {
throw new Error('Bot Token 和聊天ID不能为空');
}
// 构建消息内容
const messageContent = `*${body.title}*\n\n${body.content}\n[查看详情](${body.url})`;
// Telegram API URL
const url = `https://api.telegram.org/bot${this.botToken}/sendMessage`;
// 发送 HTTP 请求
await this.http.request({
url: url,
method: 'POST',
data: {
chat_id: this.chatId,
text: messageContent,
parse_mode: 'MarkdownV2', // 或使用 'HTML' 取决于需要的格式
},
});
}
}

View File

@ -0,0 +1,73 @@
import { BaseNotification, IsNotification, NotificationBody, NotificationInput } from '@certd/pipeline';
@IsNotification({
name: 'vocechat',
title: 'VoceChat通知',
desc: 'https://voce.chat',
})
export class VoceChatNotification extends BaseNotification {
@NotificationInput({
title: '服务地址',
component: {
placeholder: 'https://replace.your.domain',
},
required: true,
})
endpoint = '';
@NotificationInput({
title: 'apiKey',
component: {
placeholder: '',
},
helper: '[获取APIKEY](https://doc.voce.chat/bot/bot-and-webhook)',
required: true,
})
apiKey = '';
@NotificationInput({
title: '目标类型',
component: {
name: 'a-select',
options: [
{ value: 'user', label: '用户' },
{ value: 'channel', label: '频道' },
],
},
required: true,
helper: '发送消息的目标类型',
})
targetType = '';
@NotificationInput({
title: '目标ID',
component: {
placeholder: '发送消息的目标ID',
},
required: true,
helper: '目标ID可以是用户ID或频道ID',
})
targetId = '';
async send(body: NotificationBody) {
if (!this.apiKey) {
throw new Error('API Key不能为空');
}
if (!this.targetId) {
throw new Error('目标ID不能为空');
}
const url = this.targetType === 'user' ? '/api/bot/send_to_user/' : '/api/bot/send_to_group/';
await this.http.request({
url: url + this.targetId, // 这是示例API URL请根据实际API文档调整
baseURL: this.endpoint,
method: 'POST',
headers: {
'x-api-key': this.apiKey,
'Content-Type': 'text/markdown',
},
data: `# ${body.title}\n\n${body.content}\n[查看详情](${body.url})`,
});
}
}

View File

@ -44,11 +44,22 @@ export class WebhookNotification extends BaseNotification {
})
contentType = '';
@NotificationInput({
title: 'Headers',
component: {
name: 'a-textarea',
rows: 3,
},
helper: '一行一个格式为key:value',
required: true,
})
headers = '';
@NotificationInput({
title: '消息body模版',
value: `{
title:"{title}",
content:"{content}"
"text":"{title}",
"desp":"{content}\\n[查看详情]({url})"
}`,
component: {
name: 'a-textarea',
@ -57,7 +68,7 @@ export class WebhookNotification extends BaseNotification {
col: {
span: 24,
},
helper: `根据实际的webhook接口构建一个json对象作为参数支持{title}和{content}两个变量,变量用{}包裹,字符串需要双引号`,
helper: `根据实际的webhook接口构建一个json对象作为参数支持变量:{title}、{content}、{url},变量用{}包裹,字符串需要双引号`,
required: true,
})
template = '';
@ -67,15 +78,21 @@ export class WebhookNotification extends BaseNotification {
throw new Error('模版不能为空');
}
const bodyStr = this.template.replaceAll('{title}', body.title).replaceAll('{content}', body.content);
const bodyStr = this.template.replaceAll('{title}', body.title).replaceAll('{content}', body.content).replaceAll('{url}', body.url);
const data = JSON.parse(bodyStr);
const headers: any = {};
this.headers.split('\n').forEach(item => {
const [key, value] = item.trim().split(':');
headers[key] = value;
});
await this.http.request({
url: this.webhook,
method: this.method,
headers: {
'Content-Type': `${this.contentType}; charset=UTF-8`,
...headers,
},
data: data,
});