- 品牌: Go 模块/数据文件/二进制/构建部署资产全部 opsx→lokistack;前端 localStorage 命名空间迁移(读旧写新,含按用户 id 前缀迁移);修复 nginx upstream 与 Makefile 引用已改名服务的断裂 - i18n: react-i18next 落地,common + 11 个页面域命名空间全站中英(菜单/顶栏/登录/列表页/表单/确认弹窗/状态徽章);设置页语言即时切换;修复词典自注册导致的循环导入白屏 - 用户管理: 批量删除(POST /users/batch-delete,不删自己/最后管理员,部分失败不阻断)+ 行复选/全选/批量工具条 - 主题: 浅色侧栏(中性灰激活态)、四态模式(浅色/深色/跟随系统/定时切换,按时区判定)、顶栏太阳月亮二态、设置页紧凑分段选择器 - UI: 登录记住用户名、CardActionButton 统一五页卡片操作钮、shadcn Badge 替换手写徽章、语义色修正 - 清理: 移除主机 Web 终端(前后端)、k8s-helm/Helm 遗留代码与字段、README 重写精简 - 修复: 侧栏与日志告警 order 重复、路由标题 i18n key 未翻译、词典双逗号等
147 lines
4.0 KiB
Go
147 lines
4.0 KiB
Go
package services
|
|
|
|
// 飞书/Lark 群机器人通知 — 卡片消息,
|
|
// webhook 存 SystemSetting(AES 加密),对外只回 hasWebhook。
|
|
import (
|
|
"bytes"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"lokistack/models"
|
|
"lokistack/pkg/crypto"
|
|
"lokistack/repository"
|
|
)
|
|
|
|
const (
|
|
notifySettingKey = "notify_settings"
|
|
notifyHTTPTimeout = 10 * time.Second
|
|
)
|
|
|
|
// NotifyConfig 通知配置(webhook/签名 加密存储)
|
|
type NotifyConfig struct {
|
|
Enabled bool `json:"enabled"`
|
|
Webhook string `json:"webhook"` // ENC: 密文存储
|
|
SigningSecret string `json:"signingSecret"` // 飞书签名校验(可选)
|
|
}
|
|
|
|
// NotifyPublicConfig 对外输出形态
|
|
type NotifyPublicConfig struct {
|
|
Enabled bool `json:"enabled"`
|
|
HasWebhook bool `json:"hasWebhook"`
|
|
HasSecret bool `json:"hasSecret"`
|
|
}
|
|
|
|
func GetNotifyConfig() *NotifyConfig {
|
|
db := repository.GetDB()
|
|
db.RLock()
|
|
var raw string
|
|
for i := range db.Settings {
|
|
if db.Settings[i].Key == notifySettingKey {
|
|
raw = db.Settings[i].Value
|
|
break
|
|
}
|
|
}
|
|
db.RUnlock()
|
|
cfg := &NotifyConfig{}
|
|
if raw != "" {
|
|
_ = json.Unmarshal([]byte(raw), cfg)
|
|
}
|
|
if c, err := DecryptCred(cfg.Webhook); err == nil {
|
|
cfg.Webhook = c
|
|
}
|
|
if c, err := DecryptCred(cfg.SigningSecret); err == nil {
|
|
cfg.SigningSecret = c
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
func SaveNotifyConfig(enabled bool, webhook, secret string) error {
|
|
// 留空 = 保留旧值
|
|
old := GetNotifyConfig()
|
|
if webhook == "" {
|
|
webhook = old.Webhook
|
|
} else if enc, err := crypto.EncryptField(webhook); err == nil {
|
|
webhook = enc
|
|
}
|
|
if secret == "" {
|
|
secret = old.SigningSecret
|
|
} else if enc, err := crypto.EncryptField(secret); err == nil {
|
|
secret = enc
|
|
}
|
|
data, _ := json.Marshal(&NotifyConfig{Enabled: enabled, Webhook: webhook, SigningSecret: secret})
|
|
|
|
db := repository.GetDB()
|
|
newID := db.NextID("settings")
|
|
db.Lock()
|
|
defer db.Unlock()
|
|
for i := range db.Settings {
|
|
if db.Settings[i].Key == notifySettingKey {
|
|
db.Settings[i].Value = string(data)
|
|
db.RequestSave()
|
|
return nil
|
|
}
|
|
}
|
|
db.Settings = append(db.Settings, models.SystemSetting{ID: newID, Key: notifySettingKey, Value: string(data)})
|
|
db.RequestSave()
|
|
return nil
|
|
}
|
|
|
|
// larkSign 飞书签名:timestamp + "\n" + secret 的 HMAC-SHA256,base64
|
|
func larkSign(secret string, ts int64) string {
|
|
stringToSign := fmt.Sprintf("%d\n%s", ts, secret)
|
|
h := hmac.New(sha256.New, []byte(stringToSign))
|
|
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
|
}
|
|
|
|
// SendLarkCard 发送飞书卡片;title 带颜色(red/orange),lines 为 markdown 内容行
|
|
func SendLarkCard(title, color string, lines []string) error {
|
|
cfg := GetNotifyConfig()
|
|
if !cfg.Enabled || cfg.Webhook == "" {
|
|
return fmt.Errorf("通知未启用或未配置 webhook")
|
|
}
|
|
content := strings.Join(lines, "\n")
|
|
payload := map[string]interface{}{
|
|
"msg_type": "interactive",
|
|
"card": map[string]interface{}{
|
|
"config": map[string]interface{}{"wide_screen_mode": true},
|
|
"header": map[string]interface{}{
|
|
"title": map[string]string{"tag": "plain_text", "content": title},
|
|
"template": color,
|
|
},
|
|
"elements": []interface{}{
|
|
map[string]interface{}{"tag": "div", "text": map[string]string{"tag": "lark_md", "content": content}},
|
|
map[string]interface{}{"tag": "note", "elements": []interface{}{map[string]string{"tag": "plain_text", "content": "LokiStack 日志告警"}}},
|
|
},
|
|
},
|
|
}
|
|
if cfg.SigningSecret != "" {
|
|
ts := time.Now().Unix()
|
|
payload["timestamp"] = fmt.Sprintf("%d", ts)
|
|
payload["sign"] = larkSign(cfg.SigningSecret, ts)
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
resp, err := (&http.Client{Timeout: notifyHTTPTimeout}).Post(cfg.Webhook, "application/json", bytes.NewReader(body))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != 200 {
|
|
return fmt.Errorf("飞书返回 HTTP %d", resp.StatusCode)
|
|
}
|
|
var out struct {
|
|
Code int `json:"code"`
|
|
Msg string `json:"msg"`
|
|
}
|
|
_ = json.NewDecoder(resp.Body).Decode(&out)
|
|
if out.Code != 0 {
|
|
return fmt.Errorf("飞书拒绝: %s", out.Msg)
|
|
}
|
|
return nil
|
|
}
|