feat: 发布成功/失败飞书&Lark 群卡片通知
- internal/lark:飞书自定义机器人交互卡片(wide_screen_mode + 彩色头部 green/red/yellow + lark_md 字段区 + 查看构建详情按钮),支持签名加签 (timestamp+sign HMAC-SHA256,密钥 enc:v1: 加密存储) - 双引擎收口:原生模拟引擎与 Jenkins 真实引擎运行结束均推送卡片; 应用/分支/构建(#N 或 runId)/状态/时间;Jenkins 构建附 externalURL 按钮直达构建详情页 - Webhook 优先级:流水线「通知 Webhook」> 全局设置(需启用) - 系统设置新增飞书通知配置区(启用开关/Webhook/签名密钥只写/ 发送测试卡片);POST /api/settings/test-lark - E2E 已验证:本地模拟机器人收到完整卡片 JSON,测试卡片正常
This commit is contained in:
1 parent
6e97b55015
commit
d9d1dd5e99
10 files changed
+329
-9
No files matched your search
@@ -0,0 +1,96 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"flowstack/internal/crypto"
|
||||
"flowstack/internal/database"
|
||||
"flowstack/internal/lark"
|
||||
"flowstack/internal/model"
|
||||
)
|
||||
|
||||
// notifyLarkRelease 运行结束后推送飞书发布结果卡片:
|
||||
// Webhook 优先取流水线自带 notifyWebhookURL,否则用全局设置(需启用)。
|
||||
func notifyLarkRelease(run model.PipelineRun, status string) {
|
||||
if status != "success" && status != "failed" && status != "cancelled" {
|
||||
return
|
||||
}
|
||||
var p model.Pipeline
|
||||
database.DB.First(&p, run.PipelineID)
|
||||
|
||||
var s model.SystemSetting
|
||||
database.DB.First(&s, 1)
|
||||
|
||||
webhook := p.NotifyWebhook
|
||||
if webhook == "" {
|
||||
if !s.LarkEnabled || s.LarkWebhook == "" {
|
||||
return
|
||||
}
|
||||
webhook = s.LarkWebhook
|
||||
}
|
||||
secret := ""
|
||||
if s.LarkSecret != "" && crypto.IsEncrypted(s.LarkSecret) {
|
||||
if plain, err := crypto.DecryptField(s.LarkSecret); err == nil {
|
||||
secret = plain
|
||||
}
|
||||
}
|
||||
|
||||
build := run.RunID
|
||||
if run.BuildNumber > 0 {
|
||||
build = "#" + strconv.Itoa(run.BuildNumber)
|
||||
}
|
||||
card := lark.BuildReleaseCard(lark.ReleaseCardParams{
|
||||
AppName: run.PipelineName,
|
||||
Branch: run.Branch,
|
||||
Build: build,
|
||||
Status: status,
|
||||
Time: time.Now().Format("2006-01-02 15:04:05"),
|
||||
BuildURL: run.ExternalURL,
|
||||
})
|
||||
if err := lark.Send(webhook, secret, card); err != nil {
|
||||
log.Printf("[lark] 发布通知发送失败 (%s): %v", run.RunID, err)
|
||||
} else {
|
||||
log.Printf("[lark] 发布通知已发送: %s %s → %s", run.PipelineName, status, webhook)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLark POST /api/settings/test-lark —— 用已保存配置发测试卡片
|
||||
func TestLark(c *gin.Context) {
|
||||
var s model.SystemSetting
|
||||
if err := database.DB.First(&s, 1).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请先保存设置"})
|
||||
return
|
||||
}
|
||||
webhook := c.Query("webhook")
|
||||
if webhook == "" {
|
||||
webhook = s.LarkWebhook
|
||||
}
|
||||
if webhook == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "未配置飞书 Webhook"})
|
||||
return
|
||||
}
|
||||
secret := ""
|
||||
if s.LarkSecret != "" && crypto.IsEncrypted(s.LarkSecret) {
|
||||
if plain, err := crypto.DecryptField(s.LarkSecret); err == nil {
|
||||
secret = plain
|
||||
}
|
||||
}
|
||||
card := lark.BuildReleaseCard(lark.ReleaseCardParams{
|
||||
AppName: "flowstack-test",
|
||||
Branch: "test",
|
||||
Build: "#0",
|
||||
Status: "success",
|
||||
Time: time.Now().Format("2006-01-02 15:04:05"),
|
||||
BuildURL: "",
|
||||
})
|
||||
if err := lark.Send(webhook, secret, card); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "发送失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "测试卡片已发送到群"})
|
||||
}
|
||||
Reference in new issue
Block a user