diff --git a/internal/controllers/notification_controller.go b/internal/controllers/notification_controller.go index f4e3c90..bc31b74 100644 --- a/internal/controllers/notification_controller.go +++ b/internal/controllers/notification_controller.go @@ -84,7 +84,6 @@ func (nc *NotificationController) TestChannel(c *gin.Context) { utils.Success(c, result) } - // GetBindings 获取事件绑定列表 func (nc *NotificationController) GetBindings(c *gin.Context) { bindings := nc.notifyService.GetBindings() @@ -94,11 +93,12 @@ func (nc *NotificationController) GetBindings(c *gin.Context) { // SaveBinding 保存事件绑定 func (nc *NotificationController) SaveBinding(c *gin.Context) { var req struct { - ID string `json:"id"` - Type string `json:"type"` - Event string `json:"event"` - WayID string `json:"way_id"` - DataID string `json:"data_id"` + ID string `json:"id"` + Type string `json:"type"` + Event string `json:"event"` + WayID string `json:"way_id"` + DataID string `json:"data_id"` + Extra models.BigText `json:"extra"` } if err := c.ShouldBindJSON(&req); err != nil { utils.BadRequest(c, "参数错误") @@ -116,6 +116,7 @@ func (nc *NotificationController) SaveBinding(c *gin.Context) { Event: req.Event, WayID: req.WayID, DataID: req.DataID, + Extra: req.Extra, } if err := nc.notifyService.SaveBinding(binding); err != nil { @@ -142,6 +143,31 @@ func (nc *NotificationController) DeleteBinding(c *gin.Context) { utils.SuccessMsg(c, "删除成功") } +// BatchSaveBindings 批量保存事件绑定 +func (nc *NotificationController) BatchSaveBindings(c *gin.Context) { + var req struct { + Type string `json:"type"` + DataID string `json:"data_id"` + Bindings []models.NotifyBinding `json:"bindings"` + } + if err := c.ShouldBindJSON(&req); err != nil { + utils.BadRequest(c, "参数错误") + return + } + + if req.Type == "" { + utils.BadRequest(c, "类型不能为空") + return + } + + if err := nc.notifyService.BatchSaveBindings(req.Type, req.DataID, req.Bindings); err != nil { + utils.ServerError(c, err.Error()) + return + } + + utils.SuccessMsg(c, "保存成功") +} + // SendNotification API 发送通知(供脚本调用) func (nc *NotificationController) SendNotification(c *gin.Context) { var req struct { diff --git a/internal/models/notify_binding.go b/internal/models/notify_binding.go index b4b6e99..918067f 100644 --- a/internal/models/notify_binding.go +++ b/internal/models/notify_binding.go @@ -13,11 +13,18 @@ type NotifyBinding struct { Event string `json:"event" gorm:"size:50;not null;index"` // 事件类型 WayID string `json:"way_id" gorm:"size:20;not null;index"` // 通知渠道ID DataID string `json:"data_id" gorm:"size:20;index"` // 关联ID,系统事件为空,任务事件为任务ID + Extra BigText `json:"extra"` // 额外配置(如是否开启日志推送等,对应 BindingExtra 结构) CreatedAt LocalTime `json:"created_at"` UpdatedAt LocalTime `json:"updated_at"` DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` } +// BindingExtra 存储在 Extra 字段中的 JSON 配置 +type BindingExtra struct { + EnableLog bool `json:"enable_log"` + LogLimit int `json:"log_limit"` // 日志字数限制,默认 1000 +} + func (NotifyBinding) TableName() string { return constant.TablePrefix + "notify_bindings" } diff --git a/internal/router/api_routes.go b/internal/router/api_routes.go index 06b4826..ba38f84 100644 --- a/internal/router/api_routes.go +++ b/internal/router/api_routes.go @@ -231,6 +231,7 @@ func registerNotificationRoutes(g *gin.RouterGroup, c *Controllers) { notify.POST("/channels/test", c.Notification.TestChannel) notify.GET("/bindings", c.Notification.GetBindings) notify.POST("/bindings", c.Notification.SaveBinding) + notify.POST("/bindings/batch", c.Notification.BatchSaveBindings) notify.DELETE("/bindings/:id", c.Notification.DeleteBinding) } } diff --git a/internal/services/notification_service.go b/internal/services/notification_service.go index 7de53f8..79cd976 100644 --- a/internal/services/notification_service.go +++ b/internal/services/notification_service.go @@ -13,6 +13,8 @@ import ( "github.com/engigu/baihu-panel/internal/utils" "github.com/engigu/baihu-panel/internal/sdk/messenger" + "gorm.io/gorm" + "regexp" ) // NotifyChannel 通知渠道配置 @@ -155,9 +157,13 @@ func (s *NotificationService) SaveBinding(binding *models.NotifyBinding) error { res := database.DB.Where("type = ? AND event = ? AND way_id = ? AND data_id = ?", binding.Type, binding.Event, binding.WayID, binding.DataID).Limit(1).Find(&existing) if res.Error == nil && res.RowsAffected > 0 { - // 如果已存在且未删除,直接返回(或者更新它) - *binding = existing - return nil + // 如果已存在且未删除,更新现有记录(特别是 Extra 字段) + existing.Extra = binding.Extra + err := database.DB.Save(&existing).Error + if err == nil { + *binding = existing + } + return err } binding.ID = utils.GenerateID() @@ -166,6 +172,29 @@ func (s *NotificationService) SaveBinding(binding *models.NotifyBinding) error { return database.DB.Save(binding).Error } +// BatchSaveBindings 批量保存事件绑定 +func (s *NotificationService) BatchSaveBindings(bindingType, dataID string, bindings []models.NotifyBinding) error { + return database.DB.Transaction(func(tx *gorm.DB) error { + // 如果指定了 dataID,先清理该对象的所有现有绑定 + if dataID != "" { + if err := tx.Unscoped().Where("type = ? AND data_id = ?", bindingType, dataID).Delete(&models.NotifyBinding{}).Error; err != nil { + return err + } + } + + // 批量插入新绑定 + for i := range bindings { + bindings[i].ID = utils.GenerateID() + bindings[i].Type = bindingType + bindings[i].DataID = dataID + if err := tx.Create(&bindings[i]).Error; err != nil { + return err + } + } + return nil + }) +} + // DeleteBinding 删除事件绑定 func (s *NotificationService) DeleteBinding(id string) error { return database.DB.Unscoped().Where("id = ?", id).Delete(&models.NotifyBinding{}).Error @@ -288,6 +317,14 @@ func (s *NotificationService) SubscribeEvents(bus *eventbus.EventBus) { bus.Subscribe(constant.EventSystemNotice, s.handleEvent(constant.BindingTypeSystem)) } +var ansiRegexp = regexp.MustCompile(`[\x1b\x9b][\[()#;?]*([0-9]{1,4}(;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]`) + +// stripAnsi 移除字符串中的 ANSI 转义码(如颜色代码) +func stripAnsi(str string) string { + return ansiRegexp.ReplaceAllString(str, "") +} + +// handleEvent 处理事件订阅并发送通知 func (s *NotificationService) handleEvent(bindingType string) eventbus.Handler { return func(e eventbus.Event) { payload, ok := e.Payload.(map[string]interface{}) @@ -338,7 +375,6 @@ func (s *NotificationService) handleEvent(bindingType string) eventbus.Handler { return } - msg := &NotifyMessage{Title: title, Text: text} bindings := s.GetBindingsByEvent(bindingType, e.Type, dataID) if len(bindings) == 0 { return @@ -355,12 +391,38 @@ func (s *NotificationService) handleEvent(bindingType string) eventbus.Handler { if !ok || !ch.Enabled { continue } - go func(channel NotifyChannel) { - result := s.SendToChannel(channel, msg) + + // 克隆文本以便修改 + currentText := text + + // 解析额外配置 + var extra models.BindingExtra + if binding.Extra != "" { + _ = json.Unmarshal([]byte(binding.Extra), &extra) + } + // 默认日志限制为 1000 + if extra.LogLimit <= 0 { + extra.LogLimit = 1000 + } + + // 如果开启了日志推送 + if extra.EnableLog { + if output, ok := payload["output"].(string); ok && output != "" { + // 仅保留指定字数的日志内容并移除 ANSI 颜色代码 + logSnippet := stripAnsi(output) + if len(logSnippet) > extra.LogLimit { + logSnippet = "...\n" + logSnippet[len(logSnippet)-extra.LogLimit:] + } + currentText += "\n\n【执行日志】\n" + logSnippet + } + } + + go func(channel NotifyChannel, msgTitle, msgText string) { + result := s.SendToChannel(channel, &NotifyMessage{Title: msgTitle, Text: msgText}) if !result.Success { logger.Warnf("[Notify] 发送事件 %s 到渠道 %s(%s) 失败: %s", e.Type, channel.Name, channel.Type, result.Error) } - }(ch) + }(ch, title, currentText) } } } diff --git a/internal/services/tasks/executor_service.go b/internal/services/tasks/executor_service.go index b369bb4..f84a8b5 100644 --- a/internal/services/tasks/executor_service.go +++ b/internal/services/tasks/executor_service.go @@ -269,6 +269,8 @@ func (h *ServerSchedulerHandler) OnTaskCompleted(req *executor.ExecutionRequest, "task_name": task.Name, "status": result.Status, "duration": result.Duration, + "output": result.Output, + "error": result.Error, }, }) } @@ -346,6 +348,7 @@ func (h *ServerSchedulerHandler) OnTaskFailed(req *executor.ExecutionRequest, er "task_id": taskID, "task_name": taskName, "error": err.Error(), + "output": output, }, }) }() diff --git a/web/src/api/index.ts b/web/src/api/index.ts index f1c8f71..373d33d 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -299,6 +299,8 @@ export const api = { getBindings: () => request('/notify/bindings'), saveBinding: (data: Partial) => request('/notify/bindings', { method: 'POST', body: JSON.stringify(data) }), + saveBindingsBatch: (data: { type: string; data_id: string; bindings: Partial[] }) => + request('/notify/bindings/batch', { method: 'POST', body: JSON.stringify(data) }), deleteBinding: (id: string) => request('/notify/bindings/' + id, { method: 'DELETE' }), send: (data: { channel_id: string; title: string; text: string }) => request('/notify/send', { method: 'POST', body: JSON.stringify(data) }) @@ -595,10 +597,16 @@ export interface NotifyBinding { event: string way_id: string data_id: string + extra?: string created_at?: string updated_at?: string } +export interface BindingExtra { + enable_log: boolean + log_limit: number +} + export interface NotifyResult { success: boolean error?: string diff --git a/web/src/views/notify/components/EventBinding.vue b/web/src/views/notify/components/EventBinding.vue index 4866364..11bb7cb 100644 --- a/web/src/views/notify/components/EventBinding.vue +++ b/web/src/views/notify/components/EventBinding.vue @@ -10,7 +10,8 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' -import { X, Plus, Shield, Terminal, Search } from 'lucide-vue-next' +import { X, Plus, Shield, Terminal, Search, FileText } from 'lucide-vue-next' +import { Switch } from '@/components/ui/switch' import type { NotifyChannel, ChannelType, EventType, NotifyBinding, Task } from '@/api' const props = defineProps<{ @@ -87,7 +88,8 @@ function addChannelToEvent(eventType: string, bindingType: 'system' | 'task') { type: bindingType, event: eventType, way_id: channelId, - data_id: dataId + data_id: dataId, + extra: JSON.stringify({ enable_log: false, log_limit: 1000 }) } emit('save', [newBinding]) @@ -105,6 +107,49 @@ function getAvailableChannels(eventType: string, bindingType: 'system' | 'task') function removeBinding(binding: NotifyBinding) { emit('delete', binding.id) } + +// 日志推送开关逻辑 +function isLogEnabled(binding: NotifyBinding): boolean { + if (!binding.extra) return false + try { + const extra = JSON.parse(binding.extra) + return extra.enable_log === true + } catch { + return false + } +} + +function getLogLimit(binding: NotifyBinding): number { + if (!binding.extra) return 1000 + try { + const extra = JSON.parse(binding.extra) + return extra.log_limit || 1000 + } catch { + return 1000 + } +} + +function toggleLog(binding: NotifyBinding, enabled: boolean) { + const extra: any = binding.extra ? JSON.parse(binding.extra) : { log_limit: 1000 } + extra.enable_log = enabled + + const updatedBinding: Partial = { + ...binding, + extra: JSON.stringify(extra) + } + emit('save', [updatedBinding]) +} + +function setLogLimit(binding: NotifyBinding, limit: number) { + const extra: any = binding.extra ? JSON.parse(binding.extra) : { enable_log: false } + extra.log_limit = limit || 1000 + + const updatedBinding: Partial = { + ...binding, + extra: JSON.stringify(extra) + } + emit('save', [updatedBinding]) +}