diff --git a/backend/internal/api/handlers/alert_channels.go b/backend/internal/api/handlers/alert_channels.go index 710c26b..87a8af3 100644 --- a/backend/internal/api/handlers/alert_channels.go +++ b/backend/internal/api/handlers/alert_channels.go @@ -10,6 +10,7 @@ import ( "io" "net/http" "strconv" + "strings" "time" "ezops/internal/models" @@ -236,7 +237,7 @@ func sendFeishuTest(configJSON string) error { } title := "✅ AIOps 告警测试通知" - content := fmt.Sprintf("通知渠道连通性测试成功\\n测试时间:%s\\n来源:AIOps 智能运维平台", time.Now().Format("2006-01-02 15:04:05")) + content := fmt.Sprintf("**测试时间**:%s\\n**来源**:AIOps 智能运维平台\\n**状态**:连通性测试成功 🎉", time.Now().Format("2006-01-02 15:04:05")) return sendFeishuCard(cfg.URL, cfg.Secret, title, content, "green") } @@ -283,16 +284,70 @@ func sendFeishuRecovery(configJSON, title, content string) error { return sendFeishuCard(cfg.URL, cfg.Secret, title, content, "green") } -// sendFeishuCard 发送飞书消息(优先使用卡片格式,出错回退到文本) +// sendFeishuCard 发送飞书交互卡片消息 func sendFeishuCard(webhookURL, secret, title, content, color string) error { - // 先尝试文本格式(最可靠) - textMsg := map[string]interface{}{ - "msg_type": "text", - "content": map[string]interface{}{ - "text": title + "\n\n" + content, + // 解析 content 中的字段(格式: 多行 key: value) + lines := splitLines(content) + var fields []map[string]interface{} + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + fields = append(fields, map[string]interface{}{ + "is_short": false, + "text": map[string]interface{}{ + "tag": "lark_md", + "content": line, + }, + }) + } + + template := color + if template == "red" { + template = "red" + } else if template == "orange" || template == "warning" { + template = "orange" + } else if template == "green" { + template = "green" + } else if template == "blue" || template == "info" { + template = "blue" + } else { + template = "red" + } + + card := map[string]interface{}{ + "msg_type": "interactive", + "card": map[string]interface{}{ + "header": map[string]interface{}{ + "title": map[string]interface{}{ + "tag": "plain_text", + "content": title, + }, + "template": template, + }, + "elements": []map[string]interface{}{ + { + "tag": "div", + "fields": fields, + }, + { + "tag": "hr", + }, + { + "tag": "note", + "elements": []map[string]interface{}{ + { + "tag": "plain_text", + "content": fmt.Sprintf("⏰ %s | AIOps 智能运维平台", time.Now().Format("2006-01-02 15:04:05")), + }, + }, + }, + }, }, } - body, _ := json.Marshal(textMsg) + + body, _ := json.Marshal(card) req, err := http.NewRequest("POST", webhookURL, bytes.NewReader(body)) if err != nil { @@ -300,7 +355,7 @@ func sendFeishuCard(webhookURL, secret, title, content, color string) error { } req.Header.Set("Content-Type", "application/json") - // 如果有 secret,生成签名 + // 如果有 secret,追加签名到 URL query if secret != "" { timestamp := time.Now().Unix() sign := genFeishuSign(timestamp, secret) @@ -322,7 +377,6 @@ func sendFeishuCard(webhookURL, secret, title, content, color string) error { return fmt.Errorf("飞书返回错误 (HTTP %d): %s", resp.StatusCode, string(respBody)) } - // 检查飞书业务返回码 var result struct { Code int `json:"code"` Msg string `json:"msg"` @@ -335,6 +389,13 @@ func sendFeishuCard(webhookURL, secret, title, content, color string) error { return nil } +// splitLines splits a string by newlines, handling both actual \n and escaped \\n +func splitLines(s string) []string { + // First replace literal \n (from Go format strings) with actual newlines + s = strings.ReplaceAll(s, "\\n", "\n") + return strings.Split(s, "\n") +} + // buildFeishuCard 构建飞书卡片消息体(保留备用,当前使用文本格式) func buildFeishuCard(title, content, color string) models.FeishuCardMessage { return models.FeishuCardMessage{ diff --git a/frontend/src/components/monitor/alert-center/AlertCenter.tsx b/frontend/src/components/monitor/alert-center/AlertCenter.tsx index 0cfc92b..fc30426 100644 --- a/frontend/src/components/monitor/alert-center/AlertCenter.tsx +++ b/frontend/src/components/monitor/alert-center/AlertCenter.tsx @@ -262,11 +262,10 @@ const AlertCenter: React.FC = () => { - + 告警列表 告警规则 - 通知渠道 - 报警联系人 + 通知管理 报警联系组 @@ -502,62 +501,59 @@ const AlertCenter: React.FC = () => { )} - {/* 通知渠道 Tab — 自包含子组件 */} - - - - - {/* 报警联系人 Tab */} + {/* 通知管理 Tab — 渠道 + 联系人合并 */} -
- - + +
+
+

报警联系人

+ +
+ {contacts.length === 0 ? ( +
+ 暂无报警联系人,点击「添加联系人」创建 +
+ ) : ( +
+ {contacts.map((c) => ( + + +
+ {c.name} +
+
+ + {c.phone &&
📱 {c.phone}
} + {c.email &&
📧 {c.email}
} +
+ {c.dingtalk && 钉钉} + {c.feishu && 飞书} + {c.wechat && 企微} +
+
+ + +
+
+
+ ))} +
+ )}
- {contacts.length === 0 ? ( -
- 暂无报警联系人,点击「添加联系人」创建 -
- ) : ( -
- {contacts.map((c) => ( - - -
- {c.name} -
-
- - {c.phone &&
📱 {c.phone}
} - {c.email &&
📧 {c.email}
} - {c.dingtalk &&
🔷 钉钉已配置
} - {c.feishu &&
🟢 飞书已配置
} - {c.wechat &&
💚 企业微信已配置
} -
- - -
-
-
- ))} -
- )} {/* 报警联系组 Tab */}