From ae18f64d35bcb9ba70932358f329bccf7301eb6d Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 30 Oct 2025 20:18:15 +0800 Subject: [PATCH] feat: support larkbot with secret --- internal/domain/access.go | 3 +- internal/notify/notifiers/sp_larkbot.go | 1 + .../notifier/providers/larkbot/larkbot.go | 30 +++++++++++++++---- .../providers/larkbot/larkbot_test.go | 11 +++++-- .../AccessConfigFieldsProviderDingTalkBot.tsx | 2 +- .../AccessConfigFieldsProviderLarkBot.tsx | 12 ++++++++ ui/src/i18n/locales/en/nls.access.json | 5 +++- ui/src/i18n/locales/zh/nls.access.json | 9 ++++-- 8 files changed, 59 insertions(+), 14 deletions(-) diff --git a/internal/domain/access.go b/internal/domain/access.go index 422ba921..991e55ab 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -159,7 +159,7 @@ type AccessConfigForDigitalOcean struct { type AccessConfigForDingTalkBot struct { WebhookUrl string `json:"webhookUrl"` - Secret string `json:"secret"` + Secret string `json:"secret,omitempty"` } type AccessConfigForDiscordBot struct { @@ -279,6 +279,7 @@ type AccessConfigForKubernetes struct { type AccessConfigForLarkBot struct { WebhookUrl string `json:"webhookUrl"` + Secret string `json:"secret,omitempty"` } type AccessConfigForLeCDN struct { diff --git a/internal/notify/notifiers/sp_larkbot.go b/internal/notify/notifiers/sp_larkbot.go index bce1c173..a5165d7d 100644 --- a/internal/notify/notifiers/sp_larkbot.go +++ b/internal/notify/notifiers/sp_larkbot.go @@ -18,6 +18,7 @@ func init() { provider, err := larkbot.NewNotifierProvider(&larkbot.NotifierProviderConfig{ WebhookUrl: credentials.WebhookUrl, + Secret: credentials.Secret, }) return provider, err }); err != nil { diff --git a/pkg/core/notifier/providers/larkbot/larkbot.go b/pkg/core/notifier/providers/larkbot/larkbot.go index c297cf5c..15db010d 100644 --- a/pkg/core/notifier/providers/larkbot/larkbot.go +++ b/pkg/core/notifier/providers/larkbot/larkbot.go @@ -2,11 +2,15 @@ package larkbot import ( "context" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" "encoding/json" "errors" "fmt" "log/slog" "net/url" + "time" "github.com/go-resty/resty/v2" @@ -16,6 +20,8 @@ import ( type NotifierProviderConfig struct { // 飞书机器人 Webhook 地址。 WebhookUrl string `json:"webhookUrl"` + // 飞书机器人的 Secret。 + Secret string `json:"secret"` } type NotifierProvider struct { @@ -62,6 +68,23 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s } } + payload := map[string]any{ + "msg_type": "text", + "content": map[string]string{ + "text": subject + "\n\n" + message, + }, + } + if n.config.Secret != "" { + timestamp := fmt.Sprintf("%d", time.Now().UnixMilli()) + + h := hmac.New(sha256.New, []byte(n.config.Secret)) + h.Write([]byte(fmt.Sprintf("%s\n%s", timestamp, n.config.Secret))) + sign := base64.StdEncoding.EncodeToString(h.Sum(nil)) + + payload["timestamp"] = timestamp + payload["sign"] = sign + } + // REF: https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot // REF: https://open.larksuite.com/document/client-docs/bot-v3/add-custom-bot var result struct { @@ -70,12 +93,7 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s } req := n.httpClient.R(). SetContext(ctx). - SetBody(map[string]any{ - "msg_type": "text", - "content": map[string]string{ - "text": subject + "\n\n" + message, - }, - }) + SetBody(payload) resp, err := req.Post(webhookUrl.String()) if err != nil { return nil, fmt.Errorf("lark api error: failed to send request: %w", err) diff --git a/pkg/core/notifier/providers/larkbot/larkbot_test.go b/pkg/core/notifier/providers/larkbot/larkbot_test.go index 8fd73bb8..b88c98b3 100644 --- a/pkg/core/notifier/providers/larkbot/larkbot_test.go +++ b/pkg/core/notifier/providers/larkbot/larkbot_test.go @@ -15,19 +15,24 @@ const ( mockMessage = "test_message" ) -var fWebhookUrl string +var ( + fWebhookUrl string + fSecret string +) func init() { argsPrefix := "CERTIMATE_NOTIFIER_LARKBOT_" flag.StringVar(&fWebhookUrl, argsPrefix+"WEBHOOKURL", "", "") + flag.StringVar(&fSecret, argsPrefix+"SECRET", "", "") } /* Shell command to run this test: go test -v ./larkbot_test.go -args \ - --CERTIMATE_NOTIFIER_LARKBOT_WEBHOOKURL="https://example.com/your-webhook-url" + --CERTIMATE_NOTIFIER_LARKBOT_WEBHOOKURL="https://example.com/your-webhook-url" \ + --CERTIMATE_NOTIFIER_LARKBOT_SECRET="your-secret" */ func TestNotify(t *testing.T) { flag.Parse() @@ -36,10 +41,12 @@ func TestNotify(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("WEBHOOKURL: %v", fWebhookUrl), + fmt.Sprintf("SECRET: %v", fSecret), }, "\n")) notifier, err := provider.NewNotifierProvider(&provider.NotifierProviderConfig{ WebhookUrl: fWebhookUrl, + Secret: fSecret, }) if err != nil { t.Errorf("err: %+v", err) diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderDingTalkBot.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderDingTalkBot.tsx index c9c8591d..528b3e5c 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProviderDingTalkBot.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderDingTalkBot.tsx @@ -52,7 +52,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) = return z.object({ webhookUrl: z.url(t("common.errmsg.url_invalid")), - secret: z.string().nonempty(t("access.form.dingtalkbot_secret.placeholder")), + secret: z.string().nullish(), }); }; diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderLarkBot.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderLarkBot.tsx index 6966a978..eacb2d8d 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProviderLarkBot.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderLarkBot.tsx @@ -26,6 +26,16 @@ const AccessConfigFormFieldsProviderLarkBot = () => { > + + } + > + + ); }; @@ -33,6 +43,7 @@ const AccessConfigFormFieldsProviderLarkBot = () => { const getInitialValues = (): Nullish>> => { return { webhookUrl: "", + secret: "", }; }; @@ -41,6 +52,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) = return z.object({ webhookUrl: z.url(t("common.errmsg.url_invalid")), + secret: z.string().nullish(), }); }; diff --git a/ui/src/i18n/locales/en/nls.access.json b/ui/src/i18n/locales/en/nls.access.json index a186bbcd..90cb5847 100644 --- a/ui/src/i18n/locales/en/nls.access.json +++ b/ui/src/i18n/locales/en/nls.access.json @@ -331,7 +331,10 @@ "access.form.kong_api_token.tooltip": "For more information, see https://developer.konghq.com/admin-api/", "access.form.larkbot_webhook_url.label": "Lark bot Webhook URL", "access.form.larkbot_webhook_url.placeholder": "Please enter Lark bot Webhook URL", - "access.form.larkbot_webhook_url.tooltip": "For more information, see https://www.feishu.cn/hc/en-US/articles/807992406756", + "access.form.larkbot_webhook_url.tooltip": "For more information, see https://open.larksuite.com/document/client-docs/bot-v3/add-custom-bot", + "access.form.larkbot_secret.label": "Lark bot secret", + "access.form.larkbot_secret.placeholder": "Please enter Lark bot secret", + "access.form.larkbot_secret.tooltip": "For more information, see https://open.larksuite.com/document/client-docs/bot-v3/add-custom-bot", "access.form.lecdn_server_url.label": "LeCDN server URL", "access.form.lecdn_server_url.placeholder": "Please enter LeCDN server URL", "access.form.lecdn_api_version.label": "LeCDN version", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index 0838810f..d394d680 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -204,8 +204,8 @@ "access.form.dingtalkbot_webhook_url.label": "钉钉群机器人 Webhook 地址", "access.form.dingtalkbot_webhook_url.placeholder": "请输入钉钉群机器人 Webhook 地址", "access.form.dingtalkbot_webhook_url.tooltip": "这是什么?请参阅 https://open.dingtalk.com/document/orgapp/obtain-the-webhook-address-of-a-custom-robot", - "access.form.dingtalkbot_secret.label": "钉钉群机器人加签密钥", - "access.form.dingtalkbot_secret.placeholder": "请输入钉钉群机器人加签密钥", + "access.form.dingtalkbot_secret.label": "钉钉群机器人签名密钥(可选)", + "access.form.dingtalkbot_secret.placeholder": "请输入钉钉群机器人签名密钥", "access.form.dingtalkbot_secret.tooltip": "这是什么?请参阅 https://open.dingtalk.com/document/orgapp/customize-robot-security-settings", "access.form.discordbot_token.label": "Discord 机器人 API Token", "access.form.discordbot_token.placeholder": "请输入 Discord 机器人 API Token", @@ -330,7 +330,10 @@ "access.form.kong_api_token.tooltip": "这是什么?请参阅 https://developer.konghq.com/admin-api/", "access.form.larkbot_webhook_url.label": "飞书群机器人 Webhook 地址", "access.form.larkbot_webhook_url.placeholder": "请输入飞书群机器人 Webhook 地址", - "access.form.larkbot_webhook_url.tooltip": "这是什么?请参阅 https://www.feishu.cn/hc/zh-CN/articles/807992406756", + "access.form.larkbot_webhook_url.tooltip": "这是什么?请参阅 https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot", + "access.form.larkbot_secret.label": "飞书群机器人签名密钥(可选)", + "access.form.larkbot_secret.placeholder": "请输入飞书群机器人签名密钥", + "access.form.larkbot_secret.tooltip": "这是什么?请参阅 https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot", "access.form.lecdn_server_url.label": "LeCDN 服务地址", "access.form.lecdn_server_url.placeholder": "请输入 LeCDN 服务地址", "access.form.lecdn_api_version.label": "LeCDN 版本",