// Package lark 飞书/Lark 群自定义机器人卡片通知。 package lark import ( "bytes" "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "strconv" "time" ) // ReleaseCardParams 发布结果卡片字段 type ReleaseCardParams struct { AppName string // 应用名 Branch string // 分支 Build string // 构建号(如 #172 或 pr-xxx) Status string // success | failed | 其他 Time string // 展示时间 BuildURL string // 「查看构建详情」按钮地址(空则不放按钮) } func statusMeta(status string) (template, text string) { switch status { case "success": return "green", "✅ 成功" case "failed": return "red", "❌ 失败" case "cancelled": return "yellow", "⚠️ 已取消" default: return "yellow", "⚠️ " + status } } // BuildReleaseCard 组装发布结果卡片(结构对齐现有 Jenkins 通知脚本) func BuildReleaseCard(p ReleaseCardParams) map[string]any { _, statusText := statusMeta(p.Status) template, _ := statusMeta(p.Status) elements := []any{ map[string]any{ "tag": "div", "text": map[string]any{ "tag": "lark_md", "content": fmt.Sprintf( "**应用:** %s\n**分支:** %s\n**构建:** %s\n**状态:** %s\n**时间:** %s", p.AppName, p.Branch, p.Build, statusText, p.Time, ), }, }, } if p.BuildURL != "" { elements = append(elements, map[string]any{ "tag": "action", "actions": []any{map[string]any{ "tag": "button", "text": map[string]any{"tag": "plain_text", "content": "查看构建详情"}, "url": p.BuildURL, "type": "default", }}, }) } _, statusOnly := statusMeta(p.Status) title := "发布结果通知 - " + statusOnly return map[string]any{ "msg_type": "interactive", "card": map[string]any{ "config": map[string]any{"wide_screen_mode": true}, "header": map[string]any{ "template": template, "title": map[string]any{"tag": "plain_text", "content": title}, }, "elements": elements, }, } } // Send 发送卡片;secret 非空时按飞书自定义机器人规范加签(timestamp+sign) func Send(webhook, secret string, card map[string]any) error { if webhook == "" { return fmt.Errorf("webhook 未配置") } if secret != "" { ts := strconv.FormatInt(time.Now().Unix(), 10) mac := hmac.New(sha256.New, []byte(ts+"\n"+secret)) mac.Write(nil) card["timestamp"] = ts card["sign"] = base64.StdEncoding.EncodeToString(mac.Sum(nil)) } body, err := json.Marshal(card) if err != nil { return err } resp, err := (&http.Client{Timeout: 10 * time.Second}).Post(webhook, "application/json", bytes.NewReader(body)) if err != nil { return err } defer resp.Body.Close() data, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) if resp.StatusCode != http.StatusOK { return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(data)) } // 飞书成功返回 {"code":0,...} / {"StatusCode":0} var out struct { Code int `json:"code"` StatusCode int `json:"StatusCode"` Msg string `json:"msg"` ErrorMessage string `json:"ErrorMessage"` } _ = json.Unmarshal(data, &out) if out.Code != 0 || out.StatusCode != 0 { msg := out.Msg if msg == "" { msg = out.ErrorMessage } return fmt.Errorf("飞书返回错误: %s", msg) } return nil }