mirror of https://github.com/certd/certd
perf: 通知支持vocechat、bark、telegram、discord、slack
parent
cbccd9e3d0
commit
642f57ff6d
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -39,7 +39,7 @@ export class AnPushNotification extends BaseNotification {
|
|||
},
|
||||
data: {
|
||||
title: body.title,
|
||||
content: body.content,
|
||||
content: body.content + '[查看详情](' + body.url + ')',
|
||||
channel: this.channel,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -25,7 +25,7 @@ export class IyuuNotification extends BaseNotification {
|
|||
method: 'POST',
|
||||
data: {
|
||||
text: body.title,
|
||||
desp: body.content,
|
||||
desp: body.content + '[查看详情](' + body.url + ')',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ export class ServerChanNotification extends BaseNotification {
|
|||
method: 'POST',
|
||||
data: {
|
||||
text: body.title,
|
||||
desp: body.content,
|
||||
desp: body.content + '[查看详情](' + body.url + ')',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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})`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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' 取决于需要的格式
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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})`,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue