2025-03-11 00:29:18 +08:00
|
|
|
import { BaseNotification, IsNotification, NotificationBody, NotificationInput } from "@certd/pipeline";
|
|
|
|
|
|
|
|
|
|
@IsNotification({
|
2026-05-31 01:41:33 +08:00
|
|
|
name: "dingtalk",
|
|
|
|
|
title: "钉钉通知",
|
|
|
|
|
desc: "钉钉群聊通知",
|
2025-03-11 00:29:18 +08:00
|
|
|
needPlus: true,
|
|
|
|
|
})
|
|
|
|
|
// https://open.dingtalk.com/document/orgapp/the-creation-and-installation-of-the-application-robot-in-the?spm=ding_open_doc.document.0.0.242d1563cDgZz3
|
|
|
|
|
export class DingTalkNotification extends BaseNotification {
|
|
|
|
|
@NotificationInput({
|
2026-05-31 01:41:33 +08:00
|
|
|
title: "webhook地址",
|
2025-03-11 00:29:18 +08:00
|
|
|
component: {
|
2026-05-31 01:41:33 +08:00
|
|
|
placeholder: "https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxxxxxxxx",
|
2025-03-11 00:29:18 +08:00
|
|
|
},
|
2026-05-31 01:41:33 +08:00
|
|
|
helper: "钉钉APP->群聊->设置->机器人->添加机器人->自定义->[创建机器人->复制webhook地址](https://open.dingtalk.com/document/robots/custom-robot-access)",
|
2025-03-11 00:29:18 +08:00
|
|
|
required: true,
|
|
|
|
|
})
|
2026-05-31 01:41:33 +08:00
|
|
|
webhook = "";
|
2025-03-11 00:29:18 +08:00
|
|
|
|
|
|
|
|
@NotificationInput({
|
2026-05-31 01:41:33 +08:00
|
|
|
title: "加签密钥",
|
2025-03-11 00:29:18 +08:00
|
|
|
component: {
|
2026-05-31 01:41:33 +08:00
|
|
|
placeholder: "SECxxxxxxxxxxxxxxxxxxxxx",
|
2025-03-11 00:29:18 +08:00
|
|
|
},
|
2026-05-31 01:41:33 +08:00
|
|
|
helper: "必须选择一种安全设置,请选择加密密钥",
|
2025-03-11 00:29:18 +08:00
|
|
|
required: false,
|
|
|
|
|
})
|
2026-05-31 01:41:33 +08:00
|
|
|
secret = "";
|
2025-03-11 00:29:18 +08:00
|
|
|
|
|
|
|
|
@NotificationInput({
|
2026-05-31 01:41:33 +08:00
|
|
|
title: "@用户ID",
|
2025-03-11 00:29:18 +08:00
|
|
|
component: {
|
2026-05-31 01:41:33 +08:00
|
|
|
placeholder: "非必填,填写完一个按回车",
|
|
|
|
|
name: "a-select",
|
|
|
|
|
vModel: "value",
|
|
|
|
|
mode: "tags",
|
2025-03-11 00:29:18 +08:00
|
|
|
multiple: true,
|
|
|
|
|
open: false,
|
|
|
|
|
},
|
2026-05-31 01:41:33 +08:00
|
|
|
helper: "填写要@的用户ID",
|
2025-03-11 00:29:18 +08:00
|
|
|
required: false,
|
|
|
|
|
})
|
2026-05-31 01:41:33 +08:00
|
|
|
atUserIds: string[];
|
2025-03-11 00:29:18 +08:00
|
|
|
|
|
|
|
|
@NotificationInput({
|
2026-05-31 01:41:33 +08:00
|
|
|
title: "@用户手机号",
|
2025-03-11 00:29:18 +08:00
|
|
|
component: {
|
2026-05-31 01:41:33 +08:00
|
|
|
placeholder: "非必填,填写一个按回车",
|
|
|
|
|
name: "a-select",
|
|
|
|
|
vModel: "value",
|
|
|
|
|
mode: "tags",
|
2025-03-11 00:29:18 +08:00
|
|
|
multiple: true,
|
|
|
|
|
open: false,
|
|
|
|
|
},
|
2026-05-31 01:41:33 +08:00
|
|
|
helper: "填写要@的用户的手机号",
|
2025-03-11 00:29:18 +08:00
|
|
|
required: false,
|
|
|
|
|
})
|
2026-05-31 01:41:33 +08:00
|
|
|
atMobiles: string[];
|
2025-03-11 00:29:18 +08:00
|
|
|
|
|
|
|
|
@NotificationInput({
|
2026-05-31 01:41:33 +08:00
|
|
|
title: "@all",
|
2025-03-11 00:29:18 +08:00
|
|
|
component: {
|
2026-05-31 01:41:33 +08:00
|
|
|
placeholder: "非必填",
|
|
|
|
|
name: "a-switch",
|
|
|
|
|
vModel: "checked",
|
2025-03-11 00:29:18 +08:00
|
|
|
},
|
2026-05-31 01:41:33 +08:00
|
|
|
helper: "是否@所有人",
|
2025-03-11 00:29:18 +08:00
|
|
|
required: false,
|
|
|
|
|
})
|
2026-05-31 01:41:33 +08:00
|
|
|
isAtAll: boolean;
|
2025-03-11 00:29:18 +08:00
|
|
|
|
2026-05-31 01:41:33 +08:00
|
|
|
async sign() {
|
2025-03-11 00:29:18 +08:00
|
|
|
const timestamp = Date.now();
|
|
|
|
|
const secret = this.secret;
|
|
|
|
|
const stringToSign = `${timestamp}\n${secret}`;
|
|
|
|
|
/**
|
|
|
|
|
* string_to_sign = '{}\n{}'.format(timestamp, secret)
|
|
|
|
|
* string_to_sign_enc = string_to_sign.encode('utf-8')
|
|
|
|
|
* hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
|
|
|
|
|
* sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
|
|
|
|
|
*/
|
2026-05-31 01:41:33 +08:00
|
|
|
const crypto = await import("crypto");
|
|
|
|
|
const hmac = crypto.createHmac("sha256", secret);
|
2025-03-11 00:29:18 +08:00
|
|
|
hmac.update(stringToSign);
|
2026-05-31 01:41:33 +08:00
|
|
|
const sign = encodeURIComponent(Buffer.from(hmac.digest()).toString("base64"));
|
2025-03-11 00:29:18 +08:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
timestamp,
|
|
|
|
|
sign,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async send(body: NotificationBody) {
|
|
|
|
|
if (!this.webhook) {
|
2026-05-31 01:41:33 +08:00
|
|
|
throw new Error("webhook地址不能为空");
|
2025-03-11 00:29:18 +08:00
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* "msgtype": "text",
|
|
|
|
|
* "text": {
|
|
|
|
|
* "content": "hello world"
|
|
|
|
|
* }
|
|
|
|
|
* }
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
let webhook = this.webhook;
|
2026-05-31 01:41:33 +08:00
|
|
|
if (this.secret) {
|
2025-03-11 00:29:18 +08:00
|
|
|
const signRet = await this.sign();
|
|
|
|
|
webhook = `${webhook}×tamp=${signRet.timestamp}&sign=${signRet.sign}`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 01:41:33 +08:00
|
|
|
const at: any = {};
|
|
|
|
|
if (this.atUserIds && this.atUserIds.length > 0) {
|
|
|
|
|
at.atUserIds = this.atUserIds;
|
2025-03-11 00:29:18 +08:00
|
|
|
}
|
2026-05-31 01:41:33 +08:00
|
|
|
if (this.atMobiles && this.atMobiles.length > 0) {
|
|
|
|
|
at.atMobiles = this.atMobiles;
|
2025-03-11 00:29:18 +08:00
|
|
|
}
|
2026-05-31 01:41:33 +08:00
|
|
|
if (this.isAtAll) {
|
|
|
|
|
at.isAtAll = true;
|
2025-03-11 00:29:18 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-15 23:00:42 +08:00
|
|
|
const color = body.errorMessage ? "#CC0000": "#00CC66";
|
2025-03-11 00:29:18 +08:00
|
|
|
const res = await this.http.request({
|
|
|
|
|
url: webhook,
|
2026-05-31 01:41:33 +08:00
|
|
|
method: "POST",
|
2025-03-11 00:29:18 +08:00
|
|
|
data: {
|
|
|
|
|
at: at,
|
2025-05-17 01:13:36 +08:00
|
|
|
markdown: {
|
|
|
|
|
title: body.title,
|
|
|
|
|
text: `<font color='${color}'>${body.title}</font>\n\n\n ${body.content}\n\n\n[查看详情](${body.url})`,
|
2025-03-11 00:29:18 +08:00
|
|
|
},
|
2026-05-31 01:41:33 +08:00
|
|
|
msgtype: "markdown",
|
2025-03-11 00:29:18 +08:00
|
|
|
},
|
|
|
|
|
});
|
2026-05-31 01:41:33 +08:00
|
|
|
if (res.errcode > 100) {
|
2025-03-11 00:29:18 +08:00
|
|
|
throw new Error(`发送失败:${res.errmsg}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|