feat: add message push function call

This commit is contained in:
engigu
2026-03-04 21:45:43 +08:00
parent d4663cb492
commit 81b8d2a93d
53 changed files with 4161 additions and 33 deletions
+323
View File
@@ -0,0 +1,323 @@
package services
import (
"encoding/json"
"fmt"
"sync"
"github.com/engigu/baihu-panel/internal/constant"
"github.com/engigu/baihu-panel/internal/database"
"github.com/engigu/baihu-panel/internal/logger"
"github.com/engigu/baihu-panel/internal/models"
"github.com/engigu/baihu-panel/internal/utils"
"github.com/engigu/baihu-panel/internal/sdk/messenger"
)
// NotifyChannel 通知渠道配置
type NotifyChannel struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Enabled bool `json:"enabled"`
Config map[string]string `json:"config"`
}
// NotifyMessage 通知消息
type NotifyMessage struct {
Title string `json:"title"`
Text string `json:"text"`
}
// NotifyResult 发送结果
type NotifyResult struct {
Success bool `json:"success"`
Error string `json:"error,omitempty"`
}
// SupportedChannelTypes 支持的渠道类型
var SupportedChannelTypes = []map[string]string{
{"type": messenger.ChannelTelegram, "label": "Telegram"},
{"type": messenger.ChannelBark, "label": "Bark"},
{"type": messenger.ChannelDtalk, "label": "钉钉"},
{"type": messenger.ChannelQyWeiXin, "label": "企业微信"},
{"type": messenger.ChannelFeishu, "label": "飞书"},
{"type": messenger.ChannelEmail, "label": "邮件"},
{"type": messenger.ChannelCustom, "label": "自定义Webhook"},
{"type": messenger.ChannelNtfy, "label": "Ntfy"},
{"type": messenger.ChannelGotify, "label": "Gotify"},
{"type": messenger.ChannelPushMe, "label": "PushMe"},
// {"type": messenger.ChannelWeChatOFAccount, "label": "微信公众号"},
{"type": messenger.ChannelAliyunSMS, "label": "阿里云短信"},
}
// SupportedEvents 支持的事件类型
var SupportedEvents = []map[string]string{
{"type": constant.EventUserLogin, "label": "用户登录", "binding_type": constant.BindingTypeSystem},
{"type": constant.EventBruteForceLogin, "label": "密码多次错误", "binding_type": constant.BindingTypeSystem},
{"type": constant.EventPasswordChanged, "label": "密码修改", "binding_type": constant.BindingTypeSystem},
{"type": constant.EventTaskSuccess, "label": "任务成功", "binding_type": constant.BindingTypeTask},
{"type": constant.EventTaskFailed, "label": "任务失败", "binding_type": constant.BindingTypeTask},
{"type": constant.EventTaskTimeout, "label": "任务超时", "binding_type": constant.BindingTypeTask},
}
type NotificationService struct {
settingsService *SettingsService
mu sync.RWMutex
}
func NewNotificationService() *NotificationService {
return &NotificationService{
settingsService: NewSettingsService(),
}
}
// GetChannels 获取所有渠道
func (s *NotificationService) GetChannels() []NotifyChannel {
s.mu.RLock()
defer s.mu.RUnlock()
return s.getChannelsInternal()
}
// SaveChannel 保存/更新渠道
func (s *NotificationService) SaveChannel(channel NotifyChannel) error {
s.mu.Lock()
defer s.mu.Unlock()
configJSON, err := json.Marshal(channel.Config)
if err != nil {
return err
}
if channel.ID == "" {
// 新建
channel.ID = utils.GenerateID()
notifyWay := &models.NotifyWay{
ID: channel.ID,
Name: channel.Name,
Type: channel.Type,
Config: string(configJSON),
Enabled: channel.Enabled,
}
return database.DB.Create(notifyWay).Error
}
// 更新
updates := map[string]interface{}{
"name": channel.Name,
"type": channel.Type,
"config": string(configJSON),
"enabled": channel.Enabled,
}
return database.DB.Model(&models.NotifyWay{}).Where("id = ?", channel.ID).Updates(updates).Error
}
// DeleteChannel 删除渠道
func (s *NotificationService) DeleteChannel(id string) error {
s.mu.Lock()
defer s.mu.Unlock()
// 检查渠道是否存在
var count int64
database.DB.Model(&models.NotifyWay{}).Where("id = ?", id).Count(&count)
if count == 0 {
return fmt.Errorf("渠道 %s 不存在", id)
}
// 删除渠道
if err := database.DB.Unscoped().Where("id = ?", id).Delete(&models.NotifyWay{}).Error; err != nil {
return err
}
// 同时清理事件绑定中引用此渠道的配置
if err := database.DB.Unscoped().Where("way_id = ?", id).Delete(&models.NotifyBinding{}).Error; err != nil {
logger.Errorf("[Notify] 清理事件绑定失败: %v", err)
}
return nil
}
// GetBindings 获取事件绑定列表(新接口,用于前端展示)
func (s *NotificationService) GetBindings() []models.NotifyBinding {
var bindings []models.NotifyBinding
database.DB.Find(&bindings)
return bindings
}
// SaveBinding 保存事件绑定
func (s *NotificationService) SaveBinding(binding *models.NotifyBinding) error {
if binding.ID == "" {
// 检查是否已经存在相同的绑定(避免重复点击导致多个记录)
var existing models.NotifyBinding
err := database.DB.Where("type = ? AND event = ? AND way_id = ? AND data_id = ?",
binding.Type, binding.Event, binding.WayID, binding.DataID).First(&existing).Error
if err == nil {
// 如果已存在且未删除,直接返回(或者更新它)
*binding = existing
return nil
}
binding.ID = utils.GenerateID()
return database.DB.Create(binding).Error
}
return database.DB.Save(binding).Error
}
// DeleteBinding 删除事件绑定
func (s *NotificationService) DeleteBinding(id string) error {
return database.DB.Unscoped().Where("id = ?", id).Delete(&models.NotifyBinding{}).Error
}
// GetBindingsByEvent 根据事件类型和数据ID获取绑定
func (s *NotificationService) GetBindingsByEvent(bindingType, event, dataID string) []models.NotifyBinding {
var bindings []models.NotifyBinding
// 如果是任务事件且带有 dataID,只获取特定任务的绑定(禁用全局任务配置)
if bindingType == constant.BindingTypeTask && dataID != "" {
database.DB.Where("type = ? AND event = ? AND data_id = ?", constant.BindingTypeTask, event, dataID).Find(&bindings)
return bindings
}
// 对于系统事件或其他情况
query := database.DB.Where("event = ?", event)
if bindingType != "" {
query = query.Where("type = ?", bindingType)
}
if dataID != "" {
query = query.Where("data_id = ?", dataID)
} else {
query = query.Where("data_id = ? OR data_id IS NULL", "")
}
query.Find(&bindings)
return bindings
}
// SendToChannel 使用 messenger SDK 发送通知到指定渠道
func (s *NotificationService) SendToChannel(channel NotifyChannel, msg *NotifyMessage) *NotifyResult {
result, err := messenger.Send(channel.Type, messenger.ChannelConfig(channel.Config), &messenger.Message{
Title: msg.Title,
Text: msg.Text,
})
if err != nil {
return &NotifyResult{Success: false, Error: err.Error()}
}
if !result.Success {
return &NotifyResult{Success: false, Error: result.Error}
}
return &NotifyResult{Success: true}
}
// SendByChannelID 根据渠道ID发送通知
func (s *NotificationService) SendByChannelID(channelID string, msg *NotifyMessage) *NotifyResult {
s.mu.RLock()
defer s.mu.RUnlock()
var notifyWay models.NotifyWay
if err := database.DB.Where("id = ?", channelID).First(&notifyWay).Error; err != nil {
return &NotifyResult{Success: false, Error: "渠道不存在"}
}
if !notifyWay.Enabled {
return &NotifyResult{Success: false, Error: "渠道已禁用"}
}
var config map[string]string
if err := json.Unmarshal([]byte(notifyWay.Config), &config); err != nil {
return &NotifyResult{Success: false, Error: "渠道配置解析失败"}
}
ch := NotifyChannel{
ID: notifyWay.ID,
Name: notifyWay.Name,
Type: notifyWay.Type,
Enabled: notifyWay.Enabled,
Config: config,
}
return s.SendToChannel(ch, msg)
}
// TriggerEvent 触发事件通知(实现 tasks.Notifier 接口)
func (s *NotificationService) TriggerEvent(bindingType string, eventType string, dataID string, templateData map[string]interface{}) {
var title, text string
switch eventType {
case constant.EventUserLogin:
title = "用户登录通知"
text = fmt.Sprintf("用户 %v 在 IP %v 登录成功", templateData["username"], templateData["ip"])
case constant.EventBruteForceLogin:
title = "系统安全警告"
text = fmt.Sprintf("检测到 IP %v 正在尝试暴力破解用户 %v", templateData["ip"], templateData["username"])
case constant.EventPasswordChanged:
title = "账户安全通知"
text = fmt.Sprintf("用户 %v 刚刚修改了密码", templateData["username"])
case constant.EventTaskSuccess:
title = fmt.Sprintf("任务[%v] 成功", templateData["task_name"])
text = fmt.Sprintf("任务 #%v %v\n状态: 成功\n耗时: %vms", templateData["task_id"], templateData["task_name"], templateData["duration"])
case constant.EventTaskFailed:
title = fmt.Sprintf("任务[%v] 失败", templateData["task_name"])
if errStr, ok := templateData["error"]; ok {
text = fmt.Sprintf("任务 #%v %v\n执行失败\n错误: %v", templateData["task_id"], templateData["task_name"], errStr)
} else {
text = fmt.Sprintf("任务 #%v %v\n执行失败\n状态: %v\n耗时: %vms", templateData["task_id"], templateData["task_name"], templateData["status"], templateData["duration"])
}
case constant.EventTaskTimeout:
title = fmt.Sprintf("任务[%v] 超时", templateData["task_name"])
text = fmt.Sprintf("任务 #%v %v\n执行超时\n耗时: %vms", templateData["task_id"], templateData["task_name"], templateData["duration"])
default:
title = "系统通知"
text = "收到未知事件"
}
msg := &NotifyMessage{Title: title, Text: text}
bindings := s.GetBindingsByEvent(bindingType, eventType, dataID)
if len(bindings) == 0 {
return
}
channels := s.GetChannels()
channelMap := make(map[string]NotifyChannel)
for _, ch := range channels {
channelMap[ch.ID] = ch
}
for _, binding := range bindings {
ch, ok := channelMap[binding.WayID]
if !ok || !ch.Enabled {
continue
}
go func(channel NotifyChannel) {
result := s.SendToChannel(channel, msg)
if !result.Success {
logger.Warnf("[Notify] 发送事件 %s 到渠道 %s(%s) 失败: %s", eventType, channel.Name, channel.Type, result.Error)
}
}(ch)
}
}
// --- 内部方法 ---
// getChannelsInternal 从 notify_ways 表中读取所有渠道配置
func (s *NotificationService) getChannelsInternal() []NotifyChannel {
var notifyWays []models.NotifyWay
database.DB.Find(&notifyWays)
channels := make([]NotifyChannel, 0, len(notifyWays))
for _, nw := range notifyWays {
var config map[string]string
if err := json.Unmarshal([]byte(nw.Config), &config); err != nil {
logger.Warnf("[Notify] 解析渠道 %s 配置失败: %v", nw.ID, err)
continue
}
channels = append(channels, NotifyChannel{
ID: nw.ID,
Name: nw.Name,
Type: nw.Type,
Enabled: nw.Enabled,
Config: config,
})
}
return channels
}
+5
View File
@@ -89,6 +89,11 @@ func (s *SettingsService) Set(section, key, value string) error {
return database.DB.Model(&setting).Update("value", value).Error
}
// Delete 删除单个设置
func (s *SettingsService) Delete(section, key string) error {
return database.DB.Where("section = ? AND `key` = ?", section, key).Delete(&models.Setting{}).Error
}
// GetSection 获取整个 section 的设置
func (s *SettingsService) GetSection(section string) map[string]string {
if section == constant.SectionSite {
@@ -39,6 +39,11 @@ type EnvService interface {
GetEnvVarsByIDs(ids string) []string
}
// Notifier 通知服务接口定义(避免循环依赖)
type Notifier interface {
TriggerEvent(bindingType string, eventType string, dataID string, templateData map[string]interface{})
}
// ExecutorService handles task execution and scheduling
type ExecutorService struct {
taskService *TaskService
@@ -46,6 +51,7 @@ type ExecutorService struct {
agentWSManager AgentWSManager
settingsService SettingsService
envService EnvService
notifier Notifier
scheduler *executor.Scheduler
cronManager *executor.CronManager
results []executor.ExecutionResult
@@ -65,6 +71,7 @@ func NewExecutorService(
agentWSManager AgentWSManager,
settingsService SettingsService,
envService EnvService,
notifier Notifier,
) *ExecutorService {
es := &ExecutorService{
taskService: taskService,
@@ -72,6 +79,7 @@ func NewExecutorService(
agentWSManager: agentWSManager,
settingsService: settingsService,
envService: envService,
notifier: notifier,
results: make([]executor.ExecutionResult, 0, 100),
stopCh: make(chan struct{}),
}
@@ -246,6 +254,29 @@ func (h *ServerSchedulerHandler) OnTaskCompleted(req *executor.ExecutionRequest,
// ======= 重试逻辑 =======
h.es.HandleTaskRetry(task, req, result.Success, result.Status, result.ExitCode)
// ======= 通知触发 =======
if h.es.notifier != nil {
go func() {
var eventType string
switch result.Status {
case constant.TaskStatusSuccess:
eventType = constant.EventTaskSuccess
case constant.TaskStatusFailed:
eventType = constant.EventTaskFailed
case constant.TaskStatusTimeout:
eventType = constant.EventTaskTimeout
}
if eventType != "" {
h.es.notifier.TriggerEvent(constant.BindingTypeTask, eventType, task.ID, map[string]interface{}{
"task_id": task.ID,
"task_name": task.Name,
"status": result.Status,
"duration": result.Duration,
})
}
}()
}
}
func (h *ServerSchedulerHandler) OnTaskFailed(req *executor.ExecutionRequest, err error) {
@@ -305,6 +336,21 @@ func (h *ServerSchedulerHandler) OnTaskFailed(req *executor.ExecutionRequest, er
// ======= 重试逻辑 =======
h.es.HandleTaskRetry(task, req, false, constant.TaskStatusFailed, 1)
// ======= 通知触发 =======
if h.es.notifier != nil {
go func() {
taskName := "未知任务"
if task != nil {
taskName = task.Name
}
h.es.notifier.TriggerEvent(constant.BindingTypeTask, constant.EventTaskFailed, taskID, map[string]interface{}{
"task_id": taskID,
"task_name": taskName,
"error": err.Error(),
})
}()
}
}
// HandleTaskRetry 处理任务失败重试逻辑
+3
View File
@@ -124,6 +124,9 @@ func (ts *TaskService) UpdateTask(id string, name, command, schedule string, tim
}
func (ts *TaskService) DeleteTask(id string) bool {
// 同时删除关联的通知推送设置
database.DB.Where("type = ? AND data_id = ?", constant.BindingTypeTask, id).Delete(&models.NotifyBinding{})
result := database.DB.Where("id = ?", id).Delete(&models.Task{})
return result.RowsAffected > 0
}