From f4e7f8e6951adf7da50012e557d6a38a0bff2b55 Mon Sep 17 00:00:00 2001 From: lan-yonghui Date: Thu, 24 Sep 2026 17:15:59 +0800 Subject: [PATCH] fix: add success alert message for cron jobs and enhance alert handling --- agent/app/dto/alert.go | 2 + agent/app/dto/cronjob.go | 17 ++- agent/app/service/alert.go | 98 +++++++++++++ agent/app/service/cronjob.go | 118 ++++++++++++---- agent/app/service/cronjob_backup.go | 1 + agent/app/service/cronjob_helper.go | 41 +++++- agent/i18n/lang/en.yaml | 1 + agent/i18n/lang/es-ES.yaml | 1 + agent/i18n/lang/fa.yaml | 2 + agent/i18n/lang/ja.yaml | 1 + agent/i18n/lang/ko.yaml | 1 + agent/i18n/lang/lo.yaml | 1 + agent/i18n/lang/ms.yaml | 1 + agent/i18n/lang/pt-BR.yaml | 1 + agent/i18n/lang/ru.yaml | 1 + agent/i18n/lang/tr.yaml | 1 + agent/i18n/lang/zh-Hant.yaml | 1 + agent/i18n/lang/zh.yaml | 1 + agent/utils/alert/alert.go | 55 +++++++- agent/utils/alert/cronjob.go | 130 ++++++++++++++++++ agent/utils/alert/custom_webhook.go | 4 +- agent/utils/alert_push/alert_push.go | 5 +- frontend/src/api/interface/alert.ts | 8 +- frontend/src/api/interface/cronjob.ts | 4 + frontend/src/lang/modules/en.ts | 9 ++ frontend/src/lang/modules/es-es.ts | 9 ++ frontend/src/lang/modules/fa.ts | 9 ++ frontend/src/lang/modules/ja.ts | 9 ++ frontend/src/lang/modules/ko.ts | 9 ++ frontend/src/lang/modules/lo.ts | 9 ++ frontend/src/lang/modules/ms.ts | 9 ++ frontend/src/lang/modules/pt-br.ts | 9 ++ frontend/src/lang/modules/ru.ts | 9 ++ frontend/src/lang/modules/tr.ts | 9 ++ frontend/src/lang/modules/zh-Hant.ts | 9 ++ frontend/src/lang/modules/zh.ts | 9 ++ frontend/src/utils/cronjob-alert.ts | 74 ++++++++++ .../views/cronjob/cronjob/operate/index.vue | 23 +++- .../src/views/setting/alert/dash/index.vue | 32 +++-- .../views/setting/alert/dash/task/index.vue | 83 ++++++----- .../src/views/setting/alert/log/index.vue | 23 ++-- .../setting/alert/setting/drawer/index.vue | 1 - 42 files changed, 726 insertions(+), 114 deletions(-) create mode 100644 agent/utils/alert/cronjob.go create mode 100644 frontend/src/utils/cronjob-alert.ts diff --git a/agent/app/dto/alert.go b/agent/app/dto/alert.go index 84203c674de6..8f493536d7e7 100644 --- a/agent/app/dto/alert.go +++ b/agent/app/dto/alert.go @@ -21,6 +21,7 @@ type AlertBase struct { } type PushAlert struct { + Result string `json:"result,omitempty"` TaskName string `json:"taskName"` AlertType string `json:"alertType"` EntryID uint `json:"entryID"` @@ -53,6 +54,7 @@ type AlertDTO struct { Method string `json:"method"` Title string `json:"title"` Project string `json:"project"` + TaskName string `json:"taskName,omitempty"` Status string `json:"status"` SendCount uint `json:"sendCount"` AdvancedParams string `json:"advancedParams"` diff --git a/agent/app/dto/cronjob.go b/agent/app/dto/cronjob.go index 5325381c7cc4..97e8dc8bf72e 100644 --- a/agent/app/dto/cronjob.go +++ b/agent/app/dto/cronjob.go @@ -51,9 +51,10 @@ type CronjobOperate struct { Secret string `json:"secret"` Args string `json:"args"` - AlertCount uint `json:"alertCount"` - AlertTitle string `json:"alertTitle"` - AlertMethod string `json:"alertMethod"` + AlertCount uint `json:"alertCount"` + AlertTitle string `json:"alertTitle"` + AlertMethod string `json:"alertMethod"` + AlertTriggerMode string `json:"alertTriggerMode" validate:"omitempty,oneof=failed success both"` CleanLogConfig } @@ -126,7 +127,8 @@ type CronjobInfo struct { Secret string `json:"secret"` Args string `json:"args"` - AlertCount uint `json:"alertCount"` + AlertCount uint `json:"alertCount"` + AlertTriggerMode string `json:"alertTriggerMode"` } type CronjobImport struct { @@ -169,9 +171,10 @@ type CronjobTrans struct { SourceAccounts []string `json:"sourceAccounts"` DownloadAccount string `json:"downloadAccount"` - AlertCount uint `json:"alertCount"` - AlertTitle string `json:"alertTitle"` - AlertMethod string `json:"alertMethod"` + AlertCount uint `json:"alertCount"` + AlertTitle string `json:"alertTitle"` + AlertMethod string `json:"alertMethod"` + AlertTriggerMode string `json:"alertTriggerMode" validate:"omitempty,oneof=failed success both"` } type TransHelper struct { Name string `json:"name"` diff --git a/agent/app/service/alert.go b/agent/app/service/alert.go index c981c88f9f84..1ccf62e1a86f 100644 --- a/agent/app/service/alert.go +++ b/agent/app/service/alert.go @@ -17,6 +17,7 @@ import ( "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/i18n" + alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert" alertconfig "github.com/1Panel-dev/1Panel/agent/utils/alert_config" alertwebhook "github.com/1Panel-dev/1Panel/agent/utils/alert_webhook" "github.com/1Panel-dev/1Panel/agent/utils/cmd" @@ -109,7 +110,38 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO, return 0, nil, err } + cronjobProjects := make(map[string]uint) + var cronjobIDs []uint for _, item := range alerts { + if alertUtil.GetCronJobType(item.Type) != "cronJob" { + continue + } + if _, exists := cronjobProjects[item.Project]; exists { + continue + } + id, parseErr := strconv.ParseUint(item.Project, 10, strconv.IntSize) + if parseErr != nil || id == 0 { + continue + } + cronjobProjects[item.Project] = uint(id) + cronjobIDs = append(cronjobIDs, uint(id)) + } + cronjobsByID := make(map[uint]model.Cronjob) + if len(cronjobIDs) > 0 { + cronjobs, err := cronjobRepo.List(repo.WithByIDs(cronjobIDs)) + if err != nil { + return 0, nil, err + } + for _, cronjob := range cronjobs { + cronjobsByID[cronjob.ID] = cronjob + } + } + + for _, item := range alerts { + var taskName string + if cronjob, exists := cronjobsByID[cronjobProjects[item.Project]]; exists && cronjob.Type == item.Type { + taskName = cronjob.Name + } result = append(result, dto.AlertDTO{ ID: item.ID, @@ -119,6 +151,7 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO, Method: item.Method, Title: item.Title, Project: item.Project, + TaskName: taskName, Status: item.Status, SendCount: item.SendCount, AdvancedParams: item.AdvancedParams, @@ -190,6 +223,16 @@ func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error return err } } else { + advanced, err := prepareCronJobAlertParams(create.Type, "", create.AdvancedParams) + if err != nil { + return err + } + create.AdvancedParams = advanced + if create.Status != constant.AlertDisable { + if err := a.validateCronJobAlertChannels(create.Type, advanced, create.Method); err != nil { + return err + } + } alertInfo.Status = constant.AlertEnable if err := copier.Copy(&alertInfo, &create); err != nil { return buserr.WithErr("ErrStructTransform", err) @@ -207,11 +250,24 @@ func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error } func (a AlertService) UpdateAlert(req dto.AlertUpdate, operator string) error { + if alertUtil.GetCronJobType(req.Type) == "cronJob" { + previous, err := alertRepo.Get(repo.WithByID(req.ID)) + if err != nil { + return err + } + req.AdvancedParams, err = prepareCronJobAlertParams(req.Type, previous.AdvancedParams, req.AdvancedParams) + if err != nil { + return err + } + } methodTypes, err := a.validateAlertMethodReferences(req.Method) if err != nil { return err } if req.Status != constant.AlertDisable { + if err := a.validateCronJobAlertChannels(req.Type, req.AdvancedParams, req.Method); err != nil { + return err + } if err := a.validateAlertMethodEntitlement(methodTypes); err != nil { return err } @@ -278,6 +334,9 @@ func (a AlertService) UpdateStatus(id uint, status string) error { return err } if status == constant.AlertEnable { + if err := a.validateCronJobAlertChannels(alertInfo.Type, alertInfo.AdvancedParams, alertInfo.Method); err != nil { + return err + } if err := a.validateAlertMethodEntitlement(methodTypes); err != nil { return err } @@ -1021,6 +1080,23 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator alertRepo.WithByType(updateAlert.Type), alertRepo.WithByProject(updateAlert.Project), ) + advanced, err := prepareCronJobAlertParams(updateAlert.Type, alertInfo.AdvancedParams, updateAlert.AdvancedParams) + if err != nil { + return err + } + updateAlert.AdvancedParams = advanced + if alertUtil.GetCronJobType(updateAlert.Type) == "cronJob" { + upMap["advanced_params"] = advanced + } + if newStatus == constant.AlertEnable { + method := updateAlert.Method + if method == "" { + method = alertInfo.Method + } + if err := a.validateCronJobAlertChannels(updateAlert.Type, advanced, method); err != nil { + return err + } + } if alertInfo.ID > 0 { shouldUpdate := false @@ -1034,6 +1110,9 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator if val, ok := upMap["method"]; ok && val != "" && val != alertInfo.Method { shouldUpdate = true } + if val, ok := upMap["advanced_params"]; ok && val != alertInfo.AdvancedParams { + shouldUpdate = true + } if shouldUpdate { if err := alertRepo.Update( @@ -1055,3 +1134,22 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator return nil } + +func prepareCronJobAlertParams(alertType, previous, incoming string) (string, error) { + if alertUtil.GetCronJobType(alertType) != "cronJob" { + return incoming, nil + } + return alertUtil.MergeCronJobAlertParams(previous, incoming) +} + +func (a AlertService) validateCronJobAlertChannels(alertType, advanced, method string) error { + if alertUtil.GetCronJobType(alertType) != "cronJob" { + return nil + } + mode, err := alertUtil.CronJobAlertTriggerMode(advanced) + if err != nil || mode != alertUtil.CronJobAlertSuccess { + return err + } + _, err = a.validateAlertMethodReferences(method) + return err +} diff --git a/agent/app/service/cronjob.go b/agent/app/service/cronjob.go index 896abbfbb75c..ddbc7d7a342a 100644 --- a/agent/app/service/cronjob.go +++ b/agent/app/service/cronjob.go @@ -16,6 +16,7 @@ import ( "github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" + alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert" "github.com/1Panel-dev/1Panel/agent/utils/docker" "github.com/jinzhu/copier" "github.com/pkg/errors" @@ -75,6 +76,7 @@ func (u *CronjobService) SearchWithPage(search dto.PageCronjob) (int64, interfac EntryID: cronjob.ID, } alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID))), repo.WithByStatus(constant.AlertEnable)) + item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams) if alertInfo.SendCount != 0 { item.AlertCount = alertInfo.SendCount } else { @@ -98,9 +100,11 @@ func (u *CronjobService) LoadInfo(req dto.OperateByID) (*dto.CronjobOperate, err AlertType: cronjob.Type, EntryID: cronjob.ID, } - alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID))), repo.WithByStatus(constant.AlertEnable)) + alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID)))) item.AlertMethod = alertInfo.Method - if alertInfo.SendCount != 0 { + item.AlertTitle = alertInfo.Title + item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams) + if alertInfo.Status == constant.AlertEnable { item.AlertCount = alertInfo.SendCount } else { item.AlertCount = 0 @@ -195,11 +199,12 @@ func (u *CronjobService) Export(req dto.OperateByIDs) (string, error) { } } item.SourceAccounts, item.DownloadAccount, _ = loadBackupNamesByID(cronjob.SourceAccountIDs, cronjob.DownloadAccountID) - alertInfo, _ := alertRepo.Get(alertRepo.WithByType(cronjob.Type), alertRepo.WithByProject(strconv.Itoa(int(cronjob.ID))), repo.WithByStatus(constant.AlertEnable)) - if alertInfo.SendCount != 0 { + alertInfo, _ := alertRepo.Get(alertRepo.WithByType(cronjob.Type), alertRepo.WithByProject(strconv.Itoa(int(cronjob.ID)))) + item.AlertTitle = alertInfo.Title + item.AlertMethod = alertInfo.Method + item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams) + if alertInfo.Status == constant.AlertEnable { item.AlertCount = alertInfo.SendCount - item.AlertTitle = alertInfo.Title - item.AlertMethod = alertInfo.Method } else { item.AlertCount = 0 } @@ -213,6 +218,17 @@ func (u *CronjobService) Export(req dto.OperateByIDs) (string, error) { } func (u *CronjobService) Import(req []dto.CronjobTrans, operator string) error { + for _, item := range req { + advanced, err := cronJobAlertAdvancedParams(item.AlertTriggerMode) + if err != nil { + return err + } + if item.AlertCount != 0 { + if err := (AlertService{}).validateCronJobAlertChannels(item.Type, advanced, item.AlertMethod); err != nil { + return err + } + } + } for _, item := range req { cronjobItem, _ := cronjobRepo.Get(repo.WithByName(item.Name)) if cronjobItem.ID != 0 { @@ -395,17 +411,27 @@ func (u *CronjobService) Import(req []dto.CronjobTrans, operator string) error { } else { cronjob.Status = constant.StatusDisable } - _ = cronjobRepo.Create(&cronjob) - if item.AlertCount != 0 && item.AlertTitle != "" && item.AlertMethod != "" { + if err := cronjobRepo.Create(&cronjob); err != nil { + return err + } + if item.AlertTitle != "" && item.AlertMethod != "" { + advanced, _ := cronJobAlertAdvancedParams(item.AlertTriggerMode) + status := constant.AlertEnable + if item.AlertCount == 0 { + status = constant.AlertDisable + } createAlert := dto.AlertCreate{ - Title: item.AlertTitle, - SendCount: item.AlertCount, - Method: item.AlertMethod, - Type: cronjob.Type, - Project: strconv.Itoa(int(cronjob.ID)), - Status: constant.AlertEnable, + Title: item.AlertTitle, + SendCount: item.AlertCount, + Method: item.AlertMethod, + Type: cronjob.Type, + Project: strconv.Itoa(int(cronjob.ID)), + Status: status, + AdvancedParams: advanced, + } + if err := NewIAlertService().CreateAlert(createAlert, operator); err != nil { + return err } - _ = NewIAlertService().CreateAlert(createAlert, operator) } } return nil @@ -562,6 +588,15 @@ func (u *CronjobService) HandleOnce(id uint) error { } func (u *CronjobService) Create(req dto.CronjobOperate, operator string) error { + advanced, err := cronJobAlertAdvancedParams(req.AlertTriggerMode) + if err != nil { + return err + } + if req.AlertCount != 0 { + if err := (AlertService{}).validateCronJobAlertChannels(req.Type, advanced, req.AlertMethod); err != nil { + return err + } + } cronjob, _ := cronjobRepo.Get(repo.WithByName(req.Name)) if cronjob.ID != 0 { return buserr.New("ErrRecordExist") @@ -603,12 +638,13 @@ func (u *CronjobService) Create(req dto.CronjobOperate, operator string) error { } if req.AlertCount != 0 && req.AlertTitle != "" && req.AlertMethod != "" { createAlert := dto.AlertCreate{ - Title: req.AlertTitle, - SendCount: req.AlertCount, - Method: req.AlertMethod, - Type: cronjob.Type, - Project: strconv.Itoa(int(cronjob.ID)), - Status: constant.AlertEnable, + Title: req.AlertTitle, + SendCount: req.AlertCount, + Method: req.AlertMethod, + Type: cronjob.Type, + Project: strconv.Itoa(int(cronjob.ID)), + Status: constant.AlertEnable, + AdvancedParams: advanced, } err := NewIAlertService().CreateAlert(createAlert, operator) if err != nil { @@ -682,6 +718,10 @@ func (u *CronjobService) Delete(req dto.CronjobBatchDelete) error { } func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string) error { + advanced, err := cronJobAlertAdvancedParams(req.AlertTriggerMode) + if err != nil { + return err + } var cronjob model.Cronjob if err := copier.Copy(&cronjob, &req); err != nil { return buserr.WithDetail("ErrStructTransform", err.Error(), nil) @@ -697,6 +737,20 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string if err != nil { return buserr.New("ErrRecordNotFound") } + if req.AlertCount != 0 { + previous, _ := alertRepo.Get(alertRepo.WithByType(cronModel.Type), alertRepo.WithByProject(strconv.Itoa(int(id)))) + merged, err := prepareCronJobAlertParams(cronModel.Type, previous.AdvancedParams, advanced) + if err != nil { + return err + } + method := req.AlertMethod + if method == "" { + method = previous.Method + } + if err := (AlertService{}).validateCronJobAlertChannels(cronModel.Type, merged, method); err != nil { + return err + } + } upMap := make(map[string]interface{}) cronjob.EntryIDs = cronModel.EntryIDs cronjob.Type = cronModel.Type @@ -753,11 +807,12 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string return err } updateAlert := dto.AlertCreate{ - Title: req.AlertTitle, - SendCount: req.AlertCount, - Method: req.AlertMethod, - Type: cronjob.Type, - Project: strconv.Itoa(int(cronModel.ID)), + Title: req.AlertTitle, + SendCount: req.AlertCount, + Method: req.AlertMethod, + Type: cronjob.Type, + Project: strconv.Itoa(int(cronModel.ID)), + AdvancedParams: advanced, } err = NewIAlertService().ExternalUpdateAlert(updateAlert, operator) if err != nil { @@ -766,6 +821,17 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string return nil } +func cronJobAlertAdvancedParams(mode string) (string, error) { + if mode == "" { + return "", nil + } + data, err := json.Marshal(map[string]string{"alertTriggerMode": mode}) + if err != nil { + return "", err + } + return alertUtil.MergeCronJobAlertParams("", string(data)) +} + func (u *CronjobService) UpdateStatus(id uint, status string) error { cronjob, _ := cronjobRepo.Get(repo.WithByID(id)) if cronjob.ID == 0 { diff --git a/agent/app/service/cronjob_backup.go b/agent/app/service/cronjob_backup.go index 1cd542d15039..34e5cb3a386c 100644 --- a/agent/app/service/cronjob_backup.go +++ b/agent/app/service/cronjob_backup.go @@ -412,6 +412,7 @@ func addSkipTask(source string, taskItem *task.Task) { taskItem.Log(i18n.GetMsgByKey("NoSuchResource")) return nil }, nil) + taskItem.SubTasks[len(taskItem.SubTasks)-1].StepAlias = cronJobSkippedStep } func loadDbsForJob(cronjob model.Cronjob) []DatabaseHelper { diff --git a/agent/app/service/cronjob_helper.go b/agent/app/service/cronjob_helper.go index 53edd56a2513..ce0945aaa3ff 100644 --- a/agent/app/service/cronjob_helper.go +++ b/agent/app/service/cronjob_helper.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -23,6 +24,7 @@ import ( "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/i18n" + alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert" "github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/files" "github.com/1Panel-dev/1Panel/agent/utils/ntp" @@ -56,10 +58,11 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) { _ = taskRepo.Save(context.Background(), taskItem.Task) } cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records) - handleCronJobAlert(cronjob) + handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err)) return } cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records) + handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, nil)) }() return } @@ -70,19 +73,20 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) { record.TaskID = "" } cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records) - handleCronJobAlert(cronjob) + handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err)) return } go func() { if err := taskItem.Execute(); err != nil { - taskItem, _ := taskRepo.GetFirst(taskRepo.WithByID(record.TaskID)) - if len(taskItem.ID) == 0 { + storedTask, _ := taskRepo.GetFirst(taskRepo.WithByID(record.TaskID)) + if len(storedTask.ID) == 0 { record.TaskID = "" } cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records) - handleCronJobAlert(cronjob) + handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err)) } else { cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records) + handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, nil)) } }() } @@ -482,8 +486,33 @@ func hasBackup(cronjobType string) bool { return cronjobType == "app" || cronjobType == "database" || cronjobType == "website" || cronjobType == "directory" || cronjobType == "snapshot" || cronjobType == "log" || cronjobType == "cutWebsiteLog" } -func handleCronJobAlert(cronjob *model.Cronjob) { +const cronJobSkippedStep = "cronjob-skipped" + +func cronJobAlertResult(taskItem *task.Task, err error) string { + if errors.Is(err, context.Canceled) || taskItem.Task.Status == constant.StatusCanceled || + (taskItem.TaskCtx != nil && taskItem.TaskCtx.Err() != nil) { + return "" + } + if err != nil { + return alertUtil.CronJobAlertFailed + } + if taskItem.Task.Status != constant.StatusSuccess { + return "" + } + for _, subTask := range taskItem.SubTasks { + if subTask.StepAlias != cronJobSkippedStep { + return alertUtil.CronJobAlertSuccess + } + } + return "" +} + +func handleCronJobAlert(cronjob *model.Cronjob, result string) { + if result == "" { + return + } pushAlert := dto.PushAlert{ + Result: result, TaskName: cronjob.Name, AlertType: cronjob.Type, EntryID: cronjob.ID, diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml index 92c0733334df..e208725c915c 100644 --- a/agent/i18n/lang/en.yaml +++ b/agent/i18n/lang/en.yaml @@ -649,6 +649,7 @@ CommonAlert: "Panel {{ .node }}{{ .ip }}: {{ .msg }}. Log in to view details." NodeExceptionAlert: "Panel {{ .node }}{{ .ip }}: {{ .num }} nodes are abnormal. Log in to view details." LicenseExceptionAlert: "Panel {{ .node }}{{ .ip }}: {{ .num }} licenses are abnormal. Log in to view details." SSHAndPanelLoginAlert: "Panel {{ .node }}{{ .ip }}: abnormal {{ .name }} login from {{ .loginIp }}. Log in to view details." +CronJobSuccessAlert: "Panel {{ .node }}{{ .ip }}: scheduled task {{ .name }} completed successfully. Log in to view details." # disk DeviceNotFound: "Device {{ .name }} not found" diff --git a/agent/i18n/lang/es-ES.yaml b/agent/i18n/lang/es-ES.yaml index f8688c58a857..91467f1b0b14 100644 --- a/agent/i18n/lang/es-ES.yaml +++ b/agent/i18n/lang/es-ES.yaml @@ -649,6 +649,7 @@ CommonAlert: 'Su Panel {{ .node }}{{ .ip }}, {{ .msg }}. Inicie sesión en el pa NodeExceptionAlert: 'Su Panel {{ .node }}{{ .ip }}, {{ .num }} nodos son anómalos. Inicie sesión en el panel para ver los detalles.' LicenseExceptionAlert: 'Su Panel {{ .node }}{{ .ip }}, {{ .num }} licencias son anómalas. Inicie sesión en el panel para ver los detalles.' SSHAndPanelLoginAlert: 'Su Panel {{ .node }}{{ .ip }}, el inicio de sesión {{ .name }} desde {{ .loginIp }} es anómalo. Inicie sesión en el panel para ver los detalles.' +CronJobSuccessAlert: 'Panel {{ .node }}{{ .ip }}: la tarea programada {{ .name }} se completó correctamente. Inicie sesión para ver los detalles.' # disco DeviceNotFound: 'Dispositivo {{ .name }} no encontrado' diff --git a/agent/i18n/lang/fa.yaml b/agent/i18n/lang/fa.yaml index 6283d72cf84a..78ff0019a4cf 100644 --- a/agent/i18n/lang/fa.yaml +++ b/agent/i18n/lang/fa.yaml @@ -649,6 +649,8 @@ CommonAlert: "پنل {{ .node }}{{ .ip }}: {{ .msg }}. برای مشاهده ج NodeExceptionAlert: "پنل {{ .node }}{{ .ip }}: {{ .num }} گره غیرعادی هستند. برای مشاهده جزئیات وارد شوید." LicenseExceptionAlert: "پنل {{ .node }}{{ .ip }}: {{ .num }} مجوز غیرعادی است. برای مشاهده جزئیات وارد شوید." SSHAndPanelLoginAlert: "پنل {{ .node }}{{ .ip }}: ورود غیرعادی {{ .name }} از {{ .loginIp }}. برای مشاهده جزئیات وارد شوید." +CronJobSuccessAlert: "پنل {{ .node }}{{ .ip }}: وظیفه زمان‌بندی‌شده {{ .name }} با موفقیت تکمیل شد. برای مشاهده جزئیات وارد شوید." + # دیسک DeviceNotFound: "دستگاه {{ .name }} یافت نشد" diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml index c9c6929bc80a..9653bfebbb7c 100644 --- a/agent/i18n/lang/ja.yaml +++ b/agent/i18n/lang/ja.yaml @@ -649,6 +649,7 @@ CommonAlert: 'あなたの {{ .node }}{{ .ip }} パネル、{{ .msg }}。詳細 NodeExceptionAlert: 'あなたの {{ .node }}{{ .ip }} パネル、{{ .num }} 個のノードに異常が発生しています。詳細はパネルにログインして' LicenseExceptionAlert: 'あなたの {{ .node }}{{ .ip }} パネル、{{ .num }} 個のライセンスに異常が発生しています。詳細はパネルにログインして' SSHAndPanelLoginAlert: 'あなたの {{ .node }}{{ .ip }} パネル、{{ .loginIp }} からの {{ .name }} ログインに異常があります。詳細はパネルにログインして' +CronJobSuccessAlert: 'パネル {{ .node }}{{ .ip }}: スケジュールタスク {{ .name }} が正常に完了しました。詳細はパネルにログインして確認してください。' # ディスク DeviceNotFound: 'デバイス {{ .name }} が見つかりません' diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml index d234f970cea2..bd9aef6128c3 100644 --- a/agent/i18n/lang/ko.yaml +++ b/agent/i18n/lang/ko.yaml @@ -649,6 +649,7 @@ CommonAlert: '귀하의 {{ .node }}{{ .ip }} 패널, {{ .msg }}。자세한 내 NodeExceptionAlert: '귀하의 {{ .node }}{{ .ip }} 패널, {{ .num }}개의 노드에 이상이 있습니다. 자세한 내용은 패널에 로그인하십시오.' LicenseExceptionAlert: '귀하의 {{ .node }}{{ .ip }} 패널, {{ .num }}개의 라이센스에 이상이 있습니다. 자세한 내용은 패널에 로그인하십시오.' SSHAndPanelLoginAlert: '귀하의 {{ .node }}{{ .ip }} 패널, {{ .loginIp }}에서의 {{ .name }} 로그인에 이상이 있습니다. 자세한 내용은 패널에 로그인하십시오.' +CronJobSuccessAlert: '패널 {{ .node }}{{ .ip }}: 예약 작업 {{ .name }}이(가) 성공적으로 완료되었습니다. 로그인하여 자세한 내용을 확인하십시오.' # 디스크 DeviceNotFound: '장치 {{ .name }} 을(를) 찾을 수 없습니다' diff --git a/agent/i18n/lang/lo.yaml b/agent/i18n/lang/lo.yaml index b917600a7f03..1875c8c7f6fb 100644 --- a/agent/i18n/lang/lo.yaml +++ b/agent/i18n/lang/lo.yaml @@ -639,6 +639,7 @@ CommonAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: {{ .msg }}. ເ NodeExceptionAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ໂນດ {{ .num }} ແຫ່ງຜິດປົກກະຕິ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ." LicenseExceptionAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ໃບອະນຸຍາດ {{ .num }} ສະບັບຜິດປົກກະຕິ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ." SSHAndPanelLoginAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ມີການເຂົ້າສູ່ລະບົບ {{ .name }} ທີ່ຜິດປົກກະຕິຈາກ {{ .loginIp }}. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ." +CronJobSuccessAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ວຽກທີ່ຕັ້ງເວລາ {{ .name }} ສຳເລັດແລ້ວ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ." #disk DeviceNotFound: "ບໍ່ພົບອຸປະກອນ {{ .name }}" diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml index 3c12ab8315e1..dd261ff9443e 100644 --- a/agent/i18n/lang/ms.yaml +++ b/agent/i18n/lang/ms.yaml @@ -649,6 +649,7 @@ CommonAlert: 'Panel {{ .node }}{{ .ip }} Anda, {{ .msg }}. Sila log masuk ke pan NodeExceptionAlert: 'Panel {{ .node }}{{ .ip }} Anda, {{ .num }} nod tidak normal. Sila log masuk ke panel untuk butiran lanjut.' LicenseExceptionAlert: 'Panel {{ .node }}{{ .ip }} Anda, {{ .num }} lesen tidak normal. Sila log masuk ke panel untuk butiran lanjut.' SSHAndPanelLoginAlert: 'Panel {{ .node }}{{ .ip }} Anda, log masuk {{ .name }} dari {{ .loginIp }} tidak normal. Sila log masuk ke panel untuk butiran lanjut.' +CronJobSuccessAlert: 'Panel {{ .node }}{{ .ip }}: tugas berjadual {{ .name }} berjaya diselesaikan. Log masuk untuk melihat butiran.' # cakera DeviceNotFound: 'Peranti {{ .name }} tidak ditemui' diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml index 4810606e2f6e..a8a2544a769c 100644 --- a/agent/i18n/lang/pt-BR.yaml +++ b/agent/i18n/lang/pt-BR.yaml @@ -649,6 +649,7 @@ CommonAlert: 'Seu Painel {{ .node }}{{ .ip }}, {{ .msg }}. Faça login no painel NodeExceptionAlert: 'Seu Painel {{ .node }}{{ .ip }}, {{ .num }} nós estão anormais. Faça login no painel para obter detalhes.' LicenseExceptionAlert: 'Seu Painel {{ .node }}{{ .ip }}, {{ .num }} licenças estão anormais. Faça login no painel para obter detalhes.' SSHAndPanelLoginAlert: 'Seu Painel {{ .node }}{{ .ip }}, o login {{ .name }} a partir de {{ .loginIp }} é anormal. Faça login no painel para obter detalhes.' +CronJobSuccessAlert: 'Painel {{ .node }}{{ .ip }}: a tarefa agendada {{ .name }} foi concluída com sucesso. Faça login para ver os detalhes.' # disco DeviceNotFound: 'Dispositivo {{ .name }} não encontrado' diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml index c88781c0bf1f..89ed5305ee1c 100644 --- a/agent/i18n/lang/ru.yaml +++ b/agent/i18n/lang/ru.yaml @@ -649,6 +649,7 @@ CommonAlert: 'Ваш панель {{ .node }}{{ .ip }}, {{ .msg }}. Войдит NodeExceptionAlert: 'Ваш панель {{ .node }}{{ .ip }}, {{ .num }} узлов работают с ошибками. Войдите в панель для получения деталей.' LicenseExceptionAlert: 'Ваш панель {{ .node }}{{ .ip }}, {{ .num }} лицензий имеют ошибки. Войдите в панель для получения деталей.' SSHAndPanelLoginAlert: 'Ваш панель {{ .node }}{{ .ip }}, вход {{ .name }} с адреса {{ .loginIp }} является аномальным. Войдите в панель для получения деталей.' +CronJobSuccessAlert: 'Панель {{ .node }}{{ .ip }}: запланированная задача {{ .name }} успешно завершена. Войдите для просмотра подробностей.' # диск DeviceNotFound: 'Устройство {{ .name }} не найдено' diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml index 7badedc7cec5..ca814031479b 100644 --- a/agent/i18n/lang/tr.yaml +++ b/agent/i18n/lang/tr.yaml @@ -649,6 +649,7 @@ CommonAlert: 'Paneliniz {{ .node }}{{ .ip }}, {{ .msg }}. Detaylar için panelin NodeExceptionAlert: 'Paneliniz {{ .node }}{{ .ip }}, {{ .num }} düğüm anormal durumda. Detaylar için paneline giriş yapın.' LicenseExceptionAlert: 'Paneliniz {{ .node }}{{ .ip }}, {{ .num }} lisans anormal durumda. Detaylar için paneline giriş yapın.' SSHAndPanelLoginAlert: 'Paneliniz {{ .node }}{{ .ip }}, {{ .loginIp }} adresinden {{ .name }} girişi anormal. Detaylar için paneline giriş yapın.' +CronJobSuccessAlert: 'Panel {{ .node }}{{ .ip }}: zamanlanmış görev {{ .name }} başarıyla tamamlandı. Ayrıntıları görmek için giriş yapın.' # disk DeviceNotFound: 'Cihaz {{ .name }} bulunamadı' diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml index 87fedfb24591..ae7d9b747ccd 100644 --- a/agent/i18n/lang/zh-Hant.yaml +++ b/agent/i18n/lang/zh-Hant.yaml @@ -649,6 +649,7 @@ CommonAlert: '您的 {{ .node }}{{ .ip }} 面板,{{ .msg }},詳情請登入 NodeExceptionAlert: '您的 {{ .node }}{{ .ip }} 面板,{{ .num }} 個節點出現異常,詳情請登入面板檢視。' LicenseExceptionAlert: '您的 {{ .node }}{{ .ip }} 面板,{{ .num }} 個授權出現異常,詳情請登入面板檢視。' SSHAndPanelLoginAlert: '您的 {{ .node }}{{ .ip }} 面板,來自 {{ .loginIp }} 的 {{ .name }} 登入出現異常,詳情請登入面板檢視。' +CronJobSuccessAlert: '您的 {{ .node }}{{ .ip }} 面板,排程任務 {{ .name }} 執行成功,詳情請登入面板檢視。' # 磁碟 DeviceNotFound: '裝置 {{ .name }} 未找到' diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml index a9e8129eadd0..00852883dfd6 100644 --- a/agent/i18n/lang/zh.yaml +++ b/agent/i18n/lang/zh.yaml @@ -649,6 +649,7 @@ CommonAlert: "您的 {{ .node }}{{ .ip }} 面板,{{ .msg }},请登录面板 NodeExceptionAlert: "您的 {{ .node }}{{ .ip }} 面板,{{ .num }} 个节点存在异常,请登录面板查看详情。" LicenseExceptionAlert: "您的 {{ .node }}{{ .ip }} 面板,{{ .num }} 个许可证存在异常,请登录面板查看详情。" SSHAndPanelLoginAlert: "您的 {{ .node }}{{ .ip }} 面板,面板 {{ .name }} 登录 {{ .loginIp }} 异常,请登录面板查看详情。" +CronJobSuccessAlert: "您的 {{ .node }}{{ .ip }} 面板,计划任务-{{ .name }}执行成功,请登录面板查看详情。" # 磁盘 DeviceNotFound: "设备 {{ .name }} 未找到" diff --git a/agent/utils/alert/alert.go b/agent/utils/alert/alert.go index 5688436bf12d..436046c5a1f9 100644 --- a/agent/utils/alert/alert.go +++ b/agent/utils/alert/alert.go @@ -24,10 +24,10 @@ import ( "github.com/jinzhu/copier" ) -var cronJobAlertTypes = []string{"shell", "app", "website", "database", "directory", "log", "snapshot", "curl", "cutWebsiteLog", "clean", "ntp"} +var cronJobAlertTypes = []string{"shell", "app", "website", "database", "directory", "log", "snapshot", "curl", "cutWebsiteLog", "clean", "ntp", "syncIpGroup", "cleanLog"} func CreateTaskScanEmailAlertLog(alert dto.AlertDTO, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string, transport *http.Transport, agentInfo *dto.AgentInfo, emailConfig model.AlertConfig) error { - params := CreateAlertParams(GetCronJobTypeName(pushAlert.Param)) + params := CreateTaskAlertParams(pushAlert) alertDetail := ProcessAlertDetail(alert, pushAlert.TaskName, params, method) alertRule := ProcessAlertRule(alert) create.AlertRule = alertRule @@ -76,14 +76,14 @@ func CreateEmailAlertLog(create dto.AlertLogCreate, alert dto.AlertDTO, params [ Encryption: emailInfo.Encryption, Recipient: emailInfo.Recipient, } - content := GetSendContent(alert.Type, params, agentInfo) + content := GetAlertLogContent(create, alert, params, agentInfo) if content == "" { content = i18n.GetMsgWithMap("CommonAlert", map[string]interface{}{"msg": alert.Title}) } msg := email.EmailMessage{ Subject: i18n.GetMsgByKey("PanelAlertTitle"), Body: content, - IsHTML: true, + IsHTML: GetCronJobType(alert.Type) != "cronJob", } if err = email.SendMail(smtpConfig, msg, transport); err != nil { @@ -110,7 +110,7 @@ func CreateBarkAlertLog(create dto.AlertLogCreate, alert dto.AlertDTO, params [] return SaveAlertLog(create, &alertLog) } - content := GetSendContent(alert.Type, params, agentInfo) + content := GetAlertLogContent(create, alert, params, agentInfo) if content == "" { content = i18n.GetMsgWithMap("CommonAlert", map[string]interface{}{"msg": alert.Title}) } @@ -268,7 +268,7 @@ func ProcessAlertDetail(alert dto.AlertDTO, project string, params []dto.Param, alertDetail := dto.AlertDetail{ Type: GetCronJobType(alert.Type), SubType: alert.Type, - Title: alert.Title, + Title: TaskAlertTitle(alert.Type, alert.Title, project, params), Method: method, Project: project, Params: params, @@ -281,6 +281,14 @@ func ProcessAlertDetail(alert dto.AlertDTO, project string, params []dto.Param, return string(marshal) } +func TaskAlertTitle(alertType, title, project string, params []dto.Param) string { + if GetCronJobType(alertType) != "cronJob" || CronJobAlertResultFromParams(params) != CronJobAlertSuccess { + return title + } + name := cronJobTaskName(project, params) + return i18n.GetMsgWithMap("TaskSuccess", map[string]interface{}{"name": name}) +} + func ProcessAlertRule(alert dto.AlertDTO) string { marshal, err := json.Marshal(alert) if err != nil { @@ -324,6 +332,10 @@ func GetCronJobTypeName(cronJobType string) string { module = "系统快照" case "ntp": module = "同步服务器时间" + case "syncIpGroup": + module = "同步 IP 组" + case "cleanLog": + module = "清理日志" default: } return module @@ -444,6 +456,30 @@ func isWithinTimeRange(savedTimeString string) bool { } func GetSendContent(alertType string, params []dto.Param, agentInfo *dto.AgentInfo) string { + return GetAlertDetailContent(dto.AlertDetail{Type: alertType, Params: params}, agentInfo) +} + +func GetAlertLogContent(create dto.AlertLogCreate, alert dto.AlertDTO, params []dto.Param, agentInfo *dto.AgentInfo) string { + detail := dto.AlertDetail{Type: alert.Type, Params: params} + var stored dto.AlertDetail + if json.Unmarshal([]byte(create.AlertDetail), &stored) == nil { + detail = stored + if detail.Type == "" { + detail.Type = alert.Type + } + if detail.Params == nil { + detail.Params = params + } + } + return GetAlertDetailContent(detail, agentInfo) +} + +func GetAlertDetailContent(detail dto.AlertDetail, agentInfo *dto.AgentInfo) string { + alertType := detail.SubType + if alertType == "" { + alertType = detail.Type + } + params := detail.Params switch GetCronJobType(alertType) { case "ssl": return i18n.GetMsgWithMap("SSLAlert", map[string]interface{}{"num": getValueByIndex(params, "1"), "day": getValueByIndex(params, "2"), "node": getNodeName(agentInfo), "ip": getNodeIp(agentInfo)}) @@ -464,7 +500,12 @@ func GetSendContent(alertType string, params []dto.Param, agentInfo *dto.AgentIn case "disk": return i18n.GetMsgWithMap("DiskUsedAlert", map[string]interface{}{"name": getValueByIndex(params, "1"), "used": getValueByIndex(params, "2"), "node": getNodeName(agentInfo), "ip": getNodeIp(agentInfo)}) case "cronJob": - return i18n.GetMsgWithMap("CronJobFailedAlert", map[string]interface{}{"name": getValueByIndex(params, "1"), "node": getNodeName(agentInfo), "ip": getNodeIp(agentInfo)}) + messageKey := "CronJobFailedAlert" + if CronJobAlertResultFromParams(params) == CronJobAlertSuccess { + messageKey = "CronJobSuccessAlert" + } + name := cronJobTaskName(detail.Project, params) + return i18n.GetMsgWithMap(messageKey, map[string]interface{}{"name": name, "node": getNodeName(agentInfo), "ip": getNodeIp(agentInfo)}) case "clams": return i18n.GetMsgWithMap("ClamAlert", map[string]interface{}{"num": getValueByIndex(params, "1"), "node": getNodeName(agentInfo), "ip": getNodeIp(agentInfo)}) case "panelLogin": diff --git a/agent/utils/alert/cronjob.go b/agent/utils/alert/cronjob.go new file mode 100644 index 000000000000..fb081e00f165 --- /dev/null +++ b/agent/utils/alert/cronjob.go @@ -0,0 +1,130 @@ +package alert + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/1Panel-dev/1Panel/agent/app/dto" +) + +const ( + CronJobAlertFailed = "failed" + CronJobAlertSuccess = "success" + CronJobAlertBoth = "both" +) + +func cronJobAlertParams(advanced string) (map[string]json.RawMessage, error) { + params := make(map[string]json.RawMessage) + if strings.TrimSpace(advanced) != "" { + if err := json.Unmarshal([]byte(advanced), ¶ms); err != nil { + return nil, fmt.Errorf("invalid cronjob alert advanced parameters: %w", err) + } + } + if params == nil { + params = make(map[string]json.RawMessage) + } + return params, nil +} + +func CronJobAlertTriggerMode(advanced string) (string, error) { + params, err := cronJobAlertParams(advanced) + if err != nil { + return CronJobAlertFailed, err + } + mode := CronJobAlertFailed + if raw, ok := params["alertTriggerMode"]; ok { + if err := json.Unmarshal(raw, &mode); err != nil { + return CronJobAlertFailed, fmt.Errorf("invalid cronjob alert trigger mode: %w", err) + } + } + switch mode { + case CronJobAlertFailed, CronJobAlertSuccess, CronJobAlertBoth: + return mode, nil + default: + return CronJobAlertFailed, fmt.Errorf("invalid cronjob alert trigger mode %q", mode) + } +} + +func MergeCronJobAlertParams(previous, incoming string) (string, error) { + params, err := cronJobAlertParams(previous) + if err != nil { + return "", err + } + updates, err := cronJobAlertParams(incoming) + if err != nil { + return "", err + } + for key, value := range updates { + params[key] = value + } + if _, ok := params["alertTriggerMode"]; !ok { + params["alertTriggerMode"] = json.RawMessage(`"failed"`) + } + data, err := json.Marshal(params) + if err != nil { + return "", err + } + if _, err := CronJobAlertTriggerMode(string(data)); err != nil { + return "", err + } + return string(data), nil +} + +func MatchCronJobAlertResult(advanced, result string) bool { + mode, err := CronJobAlertTriggerMode(advanced) + if err != nil { + return false + } + if result == "" { + result = CronJobAlertFailed + } + if result != CronJobAlertFailed && result != CronJobAlertSuccess { + return false + } + return mode == CronJobAlertBoth || mode == result +} + +func CreateTaskAlertParams(pushAlert dto.PushAlert) []dto.Param { + params := CreateAlertParams(GetCronJobTypeName(pushAlert.Param)) + if GetCronJobType(pushAlert.AlertType) != "cronJob" { + return params + } + params = append(params, CreateCronJobResultParam(pushAlert.Result)) + return params +} + +func cronJobTaskName(project string, params []dto.Param) string { + if project != "" { + return project + } + if name := getValueByIndex(params, "taskName"); name != "" { + return name + } + return getValueByIndex(params, "1") +} + +func CreateCronJobResultParam(result string) dto.Param { + value := "失败" + if result == CronJobAlertSuccess { + value = "成功" + } + return dto.Param{Index: "2", Key: "result", Value: value} +} + +func CronJobAlertResultFromParams(params []dto.Param) string { + for _, param := range params { + if param.Index == "result" { + if param.Value == CronJobAlertSuccess { + return CronJobAlertSuccess + } + return CronJobAlertFailed + } + } + for _, param := range params { + if param.Index == "2" && param.Key == "result" && param.Value == "成功" { + return CronJobAlertSuccess + } + } + return CronJobAlertFailed +} diff --git a/agent/utils/alert/custom_webhook.go b/agent/utils/alert/custom_webhook.go index a283936980ca..c25071b01eb7 100644 --- a/agent/utils/alert/custom_webhook.go +++ b/agent/utils/alert/custom_webhook.go @@ -46,7 +46,7 @@ func CreateTaskScanCustomWebhookAlertLog( transport *http.Transport, agentInfo *dto.AgentInfo, ) error { - params := CreateAlertParams(GetCronJobTypeName(pushAlert.Param)) + params := CreateTaskAlertParams(pushAlert) alertInfo := info alertInfo.Type = alertType create.Type = GetCronJobType(alertType) @@ -99,7 +99,7 @@ func customWebhookTemplateData(rawDetail string, agentInfo *dto.AgentInfo, occur if businessType == "" { return webhook_sender.TemplateData{}, errors.New("resolve custom webhook alert detail failed") } - content := GetSendContent(businessType, detail.Params, agentInfo) + content := GetAlertDetailContent(detail, agentInfo) if content == "" { content = i18n.GetMsgWithMap("CommonAlert", map[string]interface{}{"msg": detail.Title}) } diff --git a/agent/utils/alert_push/alert_push.go b/agent/utils/alert_push/alert_push.go index 5f9591c170ca..438c0d86f748 100644 --- a/agent/utils/alert_push/alert_push.go +++ b/agent/utils/alert_push/alert_push.go @@ -26,6 +26,9 @@ func PushAlert(pushAlert dto.PushAlert) error { } var alert dto.AlertDTO _ = copier.Copy(&alert, &alertInfo) + if alertUtil.GetCronJobType(pushAlert.AlertType) == "cronJob" && !alertUtil.MatchCronJobAlertResult(alert.AdvancedParams, pushAlert.Result) { + return nil + } methods := strings.Split(alert.Method, ",") for _, m := range methods { @@ -126,7 +129,7 @@ func sendAlert(alertRepo repo.IAlertRepo, alert dto.AlertDTO, pushAlert dto.Push } transport := xpack.MultiNodeProvider.LoadRequestTransport() agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() - params := alertUtil.CreateAlertParams(alertUtil.GetCronJobTypeName(pushAlert.Param)) + params := alertUtil.CreateTaskAlertParams(pushAlert) alertDetail := alertUtil.ProcessAlertDetail(alert, pushAlert.TaskName, params, constant.Bark) alertRule := alertUtil.ProcessAlertRule(alert) create.AlertRule = alertRule diff --git a/frontend/src/api/interface/alert.ts b/frontend/src/api/interface/alert.ts index 77d5bf5d9cbb..35895e792cd1 100644 --- a/frontend/src/api/interface/alert.ts +++ b/frontend/src/api/interface/alert.ts @@ -1,4 +1,5 @@ import { CommonModel, ReqPage } from '@/api/interface'; +import type { CronjobAlertTriggerMode } from '@/utils/cronjob-alert'; export namespace Alert { export interface AlertInfo extends CommonModel { @@ -9,22 +10,25 @@ export namespace Alert { interval: number; method: string; title: string; + taskName?: string; project: string; status: string; sendCount: number; sendMethod: string[]; advancedParams: string; + alertTriggerMode?: CronjobAlertTriggerMode; createUser?: string; updateUser?: string; } export interface AlertDetail { type: string; + subType?: string; licenseId: string; title: string; project: string; method: string; - params: string; + params: { index: string; key: string; value: string }[] | string; } export interface AlertUpdateStatusReq { @@ -74,6 +78,7 @@ export namespace Alert { project: string; status: string; sendCount: number; + advancedParams?: string; } export interface AlertUpdateReq extends CommonModel { @@ -86,6 +91,7 @@ export namespace Alert { project: string; status: string; sendCount: number; + advancedParams?: string; } export interface DelReq { diff --git a/frontend/src/api/interface/cronjob.ts b/frontend/src/api/interface/cronjob.ts index a5a0efdd1ab1..7e8c5acd999d 100644 --- a/frontend/src/api/interface/cronjob.ts +++ b/frontend/src/api/interface/cronjob.ts @@ -1,4 +1,5 @@ import { ReqPage } from '.'; +import type { CronjobAlertTriggerMode } from '@/utils/cronjob-alert'; export namespace Cronjob { export interface Search extends ReqPage { @@ -62,6 +63,7 @@ export namespace Cronjob { secret: string; hasAlert: boolean; alertCount: number; + alertTriggerMode?: CronjobAlertTriggerMode; alertTitle: string; alertMethod: string; alertMethodItems: Array; @@ -111,6 +113,7 @@ export namespace Cronjob { secret: string; alertCount: number; + alertTriggerMode?: CronjobAlertTriggerMode; alertTitle: string; alertMethod: string; @@ -154,6 +157,7 @@ export namespace Cronjob { downloadAccount: string; alertCount: number; + alertTriggerMode?: CronjobAlertTriggerMode; } export interface TransHelper { name: string; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index da776a32c29d..c1e7950af2c9 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -6723,6 +6723,15 @@ const message = { snapshot: 'System Snapshot', clamsRulesHelper: 'Virus scanning tasks that require alerts', cronJobRulesHelper: 'This type of scheduled task needs to be configured', + alertTriggerMode: 'Notify on', + alertTriggerFailed: 'Failure only', + alertTriggerSuccess: 'Success only', + alertTriggerBoth: 'Success or failure', + alertNotificationHelper: + 'Notify after the final execution result, not during retries. Success and failure notifications share the daily limit and sending hours.', + notificationTitle: 'Scheduled task - {0} 「{1}」 execution notification', + notificationSuccessTitle: 'Scheduled task - {0} 「{1}」 completed successfully', + notificationRule: '{0}: {1}, up to {2} notifications per day', clamsTitle: 'Virus scan task 「 {0} 」 detected infected file alert', cronJobAppTitle: 'Cronjob - Backup App 「 {0} 」 Task Failure Alert', cronJobWebsiteTitle: 'Cronjob - Backup Website「 {0} 」Task Failure Alert', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index 7b768cd745e0..909deeb365ea 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -6835,6 +6835,15 @@ const message = { snapshot: 'Snapshot del sistema', clamsRulesHelper: 'Tareas de escaneo antivirus que requieren alerta', cronJobRulesHelper: 'Este tipo de tareas programadas necesita configuración', + alertTriggerMode: 'Notificar cuando', + alertTriggerFailed: 'Solo falle', + alertTriggerSuccess: 'Solo tenga éxito', + alertTriggerBoth: 'Tenga éxito o falle', + alertNotificationHelper: + 'Notificar al finalizar la ejecución, no durante los reintentos. Las notificaciones de éxito y fallo comparten el límite diario y el horario de envío.', + notificationTitle: 'Tarea programada - {0} 「{1}」 notificación de ejecución', + notificationSuccessTitle: 'Tarea programada - {0} 「{1}」 completada correctamente', + notificationRule: '{0}: {1}, hasta {2} notificaciones al día', clamsTitle: 'Tarea antivirus 「 {0} 」 detectó archivo infectado', cronJobAppTitle: 'Cronjob - Backup de app 「 {0} 」 falló', cronJobWebsiteTitle: 'Cronjob - Backup de sitio 「 {0} 」 falló', diff --git a/frontend/src/lang/modules/fa.ts b/frontend/src/lang/modules/fa.ts index d6a84f8dfaf8..1ae1a727d167 100644 --- a/frontend/src/lang/modules/fa.ts +++ b/frontend/src/lang/modules/fa.ts @@ -6677,6 +6677,15 @@ const message = { snapshot: 'تصویر لحظه‌ای سیستم', clamsRulesHelper: 'وظایف اسکن ویروس که نیاز به هشدار دارند', cronJobRulesHelper: 'این نوع وظیفه زمان‌بندی باید پیکربندی شود', + alertTriggerMode: 'شرط اعلان', + alertTriggerFailed: 'فقط شکست', + alertTriggerSuccess: 'فقط موفقیت', + alertTriggerBoth: 'موفقیت یا شکست', + alertNotificationHelper: + 'اعلان پس از نتیجه نهایی اجرا ارسال می‌شود، نه هنگام تلاش مجدد. اعلان‌های موفقیت و شکست سقف روزانه و ساعات ارسال مشترک دارند.', + notificationTitle: 'وظیفه زمان‌بندی - {0} «{1}» اعلان اجرا', + notificationSuccessTitle: 'وظیفه زمان‌بندی - {0} «{1}» با موفقیت انجام شد', + notificationRule: '{0}: {1}، حداکثر {2} اعلان در روز', clamsTitle: 'وظیفه اسکن ویروس « {0} » هشدار فایل آلوده شناسایی شد', cronJobAppTitle: 'وظیفه زمان‌بندی - پشتیبان‌گیری از برنامه « {0} » هشدار شکست وظیفه', cronJobWebsiteTitle: 'وظیفه زمان‌بندی - پشتیبان‌گیری از وب‌سایت « {0} » هشدار شکست وظیفه', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index ba922ccfe5ea..0b72aac5bd5e 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -6717,6 +6717,15 @@ const message = { snapshot: 'システムスナップショット', clamsRulesHelper: 'アラートが必要なウイルススキャンタスク', cronJobRulesHelper: 'このタイプのスケジュールタスクには設定が必要です', + alertTriggerMode: '通知条件', + alertTriggerFailed: '失敗時のみ', + alertTriggerSuccess: '成功時のみ', + alertTriggerBoth: '成功時または失敗時', + alertNotificationHelper: + '再試行中ではなく、最終的な実行結果に基づいて通知します。成功と失敗の通知は、1日の上限と送信時間帯を共有します。', + notificationTitle: '定期タスク - {0}「{1}」実行通知', + notificationSuccessTitle: '定期タスク - {0}「{1}」が正常に完了しました', + notificationRule: '{0}:{1}、1日最大 {2} 回通知', clamsTitle: 'ウイルススキャンタスク「{0}」が感染ファイルを検出したアラート', cronJobAppTitle: 'Cronジョブ - アプリバックアップ「{0}」タスク失敗アラート', cronJobWebsiteTitle: 'Cronジョブ - ウェブサイトバックアップ「{0}」タスク失敗アラート', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 37d5a55f032b..d31b00b7c1bf 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -6582,6 +6582,15 @@ const message = { snapshot: '시스템 스냅샷', clamsRulesHelper: '알림이 필요한 바이러스 검사 작업', cronJobRulesHelper: '이 유형의 예약된 작업은 구성해야 합니다', + alertTriggerMode: '알림 조건', + alertTriggerFailed: '실패 시에만', + alertTriggerSuccess: '성공 시에만', + alertTriggerBoth: '성공 또는 실패 시', + alertNotificationHelper: + '재시도 중이 아닌 최종 실행 결과에 따라 알립니다. 성공 및 실패 알림은 일일 한도와 전송 시간대를 공유합니다.', + notificationTitle: '예약 작업 - {0} 「{1}」 실행 알림', + notificationSuccessTitle: '예약 작업 - {0} 「{1}」 실행 성공', + notificationRule: '{0}: {1}, 하루 최대 {2}회 알림', clamsTitle: '바이러스 검사 작업 「 {0} 」 감염된 파일 알림', cronJobAppTitle: '크론 작업 - 백업 애플리케이션 「 {0} 」 작업 실패 알림', cronJobWebsiteTitle: '크론 작업 - 백업 웹사이트「 {0} 」작업 실패 알림', diff --git a/frontend/src/lang/modules/lo.ts b/frontend/src/lang/modules/lo.ts index e74bc728e9da..7bc6018a9815 100644 --- a/frontend/src/lang/modules/lo.ts +++ b/frontend/src/lang/modules/lo.ts @@ -6493,6 +6493,15 @@ const message = { snapshot: 'System Snapshot', clamsRulesHelper: 'ງານສະແກນໄວຣັສທີ່ຕ້ອງການແຈ້ງເຕືອນ', cronJobRulesHelper: 'ຕ້ອງຕັ້ງຄ່າງານ Cronjob ປະເພດນີ້', + alertTriggerMode: 'ເງື່ອນໄຂແຈ້ງເຕືອນ', + alertTriggerFailed: 'ເມື່ອລົ້ມເຫຼວເທົ່ານັ້ນ', + alertTriggerSuccess: 'ເມື່ອສຳເລັດເທົ່ານັ້ນ', + alertTriggerBoth: 'ເມື່ອສຳເລັດ ຫຼື ລົ້ມເຫຼວ', + alertNotificationHelper: + 'ແຈ້ງເຕືອນຕາມຜົນສຸດທ້າຍ ບໍ່ແມ່ນລະຫວ່າງລອງໃໝ່. ການແຈ້ງເຕືອນສຳເລັດ ແລະ ລົ້ມເຫຼວໃຊ້ຂີດຈຳກັດລາຍວັນ ແລະ ເວລາສົ່ງຮ່ວມກັນ.', + notificationTitle: 'ວຽກຕາມກຳນົດເວລາ - {0} 「{1}」 ແຈ້ງເຕືອນການເຮັດວຽກ', + notificationSuccessTitle: 'ວຽກຕາມກຳນົດເວລາ - {0} 「{1}」 ສຳເລັດແລ້ວ', + notificationRule: '{0}: {1}, ສູງສຸດ {2} ຄັ້ງຕໍ່ມື້', clamsTitle: 'ງານສະແກນໄວຣັສ 「 {0} 」 ກວດພົບໄຟລ໌ອັນຕະລາຍ', cronJobAppTitle: 'ແຈ້ງເຕືອນງານສຳຮອງແອັບ 「 {0} 」 ລົ້ມເຫຼວ', cronJobWebsiteTitle: 'ແຈ້ງເຕືອນງານສຳຮອງເວັບໄຊ 「 {0} 」 ລົ້ມເຫຼວ', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 02a3c3b02272..b17a5489560b 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -6833,6 +6833,15 @@ const message = { snapshot: 'Snapshot Sistem', clamsRulesHelper: 'Tugas imbasan virus yang memerlukan amaran', cronJobRulesHelper: 'Jenis tugas berjadual ini perlu dikonfigurasikan', + alertTriggerMode: 'Syarat pemberitahuan', + alertTriggerFailed: 'Gagal sahaja', + alertTriggerSuccess: 'Berjaya sahaja', + alertTriggerBoth: 'Berjaya atau gagal', + alertNotificationHelper: + 'Maklumkan selepas keputusan akhir pelaksanaan, bukan semasa percubaan semula. Pemberitahuan berjaya dan gagal berkongsi had harian dan waktu penghantaran.', + notificationTitle: 'Tugas berjadual - {0} 「{1}」 pemberitahuan pelaksanaan', + notificationSuccessTitle: 'Tugas berjadual - {0} 「{1}」 berjaya diselesaikan', + notificationRule: '{0}: {1}, sehingga {2} pemberitahuan sehari', clamsTitle: 'Tugas imbasan virus 「 {0} 」 mengesan amaran fail dijangkiti', cronJobAppTitle: 'Cronjob - Sandaran Aplikasi 「 {0} 」 Amaran Kegagalan Tugas', cronJobWebsiteTitle: 'Cronjob - Sandaran Laman Web 「 {0} 」 Amaran Kegagalan Tugas', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 88fcf88fa313..033cb4f813b5 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -6867,6 +6867,15 @@ const message = { snapshot: 'Instantâneo do Sistema', clamsRulesHelper: 'Tarefas de varredura de vírus que exigem alertas por', cronJobRulesHelper: 'Este tipo de tarefa cron precisa ser configurado', + alertTriggerMode: 'Notificar em caso de', + alertTriggerFailed: 'Apenas falha', + alertTriggerSuccess: 'Apenas sucesso', + alertTriggerBoth: 'Sucesso ou falha', + alertNotificationHelper: + 'Notificar após o resultado final, não durante as novas tentativas. As notificações de sucesso e falha compartilham o limite diário e o horário de envio.', + notificationTitle: 'Tarefa agendada - {0} 「{1}」 notificação de execução', + notificationSuccessTitle: 'Tarefa agendada - {0} 「{1}」 concluída com sucesso', + notificationRule: '{0}: {1}, até {2} notificações por dia', clamsTitle: 'Tarefa de Varredura de Vírus 「 {0} 」 detectou arquivo infectado', cronJobAppTitle: 'Tarefa Cron - Falha no Backup do Aplicativo 「 {0} 」', cronJobWebsiteTitle: 'Tarefa Cron - Falha no Backup do Site 「 {0} 」', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index b212a3e0e5d1..993215f847db 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -6835,6 +6835,15 @@ const message = { snapshot: 'Системный снимок', clamsRulesHelper: 'Задачи сканирования на вирусы, требующие уведомлений', cronJobRulesHelper: 'Этот тип планируемой задачи требует конфигурации', + alertTriggerMode: 'Условие уведомления', + alertTriggerFailed: 'Только при ошибке', + alertTriggerSuccess: 'Только при успехе', + alertTriggerBoth: 'При успехе или ошибке', + alertNotificationHelper: + 'Уведомление отправляется по окончательному результату, а не во время повторных попыток. Уведомления об успехе и ошибке используют общий дневной лимит и часы отправки.', + notificationTitle: 'Плановая задача - {0} «{1}»: уведомление о выполнении', + notificationSuccessTitle: 'Плановая задача - {0} «{1}» успешно завершена', + notificationRule: '{0}: {1}, до {2} уведомлений в день', clamsTitle: 'Задача сканирования на вирусы 「 {0} 」 обнаружила заражённый файл', cronJobAppTitle: 'Задача Cron - ошибка задачи резервного копирования приложения 「 {0} 」', cronJobWebsiteTitle: 'Задача Cron - ошибка задачи резервного копирования сайта 「 {0} 」', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index f887ed4a50e2..a5ede94c0290 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -6841,6 +6841,15 @@ const message = { snapshot: 'Sistem Anlık Görüntüsü', clamsRulesHelper: 'Uyarısı gerektiren virüs tarama görevleri', cronJobRulesHelper: 'Bu tür zamanlanmış görevlerin yapılandırılması gerekir', + alertTriggerMode: 'Bildirim koşulu', + alertTriggerFailed: 'Yalnızca başarısızlık', + alertTriggerSuccess: 'Yalnızca başarı', + alertTriggerBoth: 'Başarı veya başarısızlık', + alertNotificationHelper: + 'Yeniden denemeler sırasında değil, son yürütme sonucuna göre bildirim gönderilir. Başarı ve başarısızlık bildirimleri günlük sınırı ve gönderim saatlerini paylaşır.', + notificationTitle: 'Zamanlanmış görev - {0} 「{1}」 yürütme bildirimi', + notificationSuccessTitle: 'Zamanlanmış görev - {0} 「{1}」 başarıyla tamamlandı', + notificationRule: '{0}: {1}, günde en fazla {2} bildirim', clamsTitle: 'Virüs tarama görevi 「 {0} 」 enfekte dosya algıladı uyarısı', cronJobAppTitle: 'Zamanlanmış Görev - Yedekleme Uygulaması 「 {0} 」 Görev Başarısızlık Uyarısı', cronJobWebsiteTitle: 'Zamanlanmış Görev - Yedekleme Web Sitesi 「 {0} 」 Görev Başarısızlık Uyarısı', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 2e1891ebf17a..24376da1595d 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -6238,6 +6238,15 @@ const message = { snapshot: '系統快照', clamsRulesHelper: '需要開啟告警的病毒掃描任務', cronJobRulesHelper: '需要設定此類型的計劃任務', + alertTriggerMode: '通知條件', + alertTriggerFailed: '僅失敗', + alertTriggerSuccess: '僅成功', + alertTriggerBoth: '成功或失敗', + alertNotificationHelper: + '任務最終執行完成後按結果通知,重試過程中不發送;成功與失敗共用每日次數及發送時段。', + notificationTitle: '計劃任務-{0}「{1}」執行告警', + notificationSuccessTitle: '計劃任務-{0}「{1}」執行成功通知', + notificationRule: '{0}:{1},每天最多發送 {2} 次', clamsTitle: '病毒掃描「{0}」任務偵測到感染檔案告警', cronJobAppTitle: '計劃任務-備份應用「{0}」任務失敗告警', cronJobWebsiteTitle: '計劃任務-備份網站「{0}」任務失敗告警', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 1387f660c46e..cdbfabd356f5 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -6294,6 +6294,15 @@ const message = { snapshot: '系统快照', clamsRulesHelper: '需要开启告警通知的病毒扫描任务', cronJobRulesHelper: '需要配置此类型的计划任务', + alertTriggerMode: '通知条件', + alertTriggerFailed: '仅失败', + alertTriggerSuccess: '仅成功', + alertTriggerBoth: '成功或失败', + alertNotificationHelper: + '任务最终执行完成后按结果通知,重试过程中不发送;成功与失败共用每日次数及发送时段。', + notificationTitle: '计划任务-{0}「{1}」执行告警', + notificationSuccessTitle: '计划任务-{0}「{1}」执行成功通知', + notificationRule: '{0}:{1},每天最多发送 {2} 次', clamsTitle: '病毒扫描「 {0} 」任务检测到感染文件告警', cronJobAppTitle: '计划任务-备份应用「 {0} 」任务失败告警', cronJobWebsiteTitle: '计划任务-备份网站「 {0} 」任务失败告警', diff --git a/frontend/src/utils/cronjob-alert.ts b/frontend/src/utils/cronjob-alert.ts new file mode 100644 index 000000000000..84ce485c5b01 --- /dev/null +++ b/frontend/src/utils/cronjob-alert.ts @@ -0,0 +1,74 @@ +export type CronjobAlertTriggerMode = 'failed' | 'success' | 'both'; + +export const cronjobAlertTypes = [ + 'shell', + 'app', + 'website', + 'database', + 'directory', + 'log', + 'snapshot', + 'curl', + 'cutWebsiteLog', + 'clean', + 'ntp', + 'syncIpGroup', + 'cleanLog', +]; + +export const cronjobAlertModes = [ + { value: 'failed', label: 'xpack.alert.alertTriggerFailed' }, + { value: 'success', label: 'xpack.alert.alertTriggerSuccess' }, + { value: 'both', label: 'xpack.alert.alertTriggerBoth' }, +] as const; + +export const normalizeCronjobAlertMode = (value: unknown): CronjobAlertTriggerMode => { + return value === 'success' || value === 'both' ? value : 'failed'; +}; + +const parseAdvancedParams = (value?: string): Record => { + try { + const params = JSON.parse(value || '{}'); + return params && typeof params === 'object' && !Array.isArray(params) ? params : {}; + } catch { + return {}; + } +}; + +export const getCronjobAlertMode = (advancedParams?: string): CronjobAlertTriggerMode => { + return normalizeCronjobAlertMode(parseAdvancedParams(advancedParams).alertTriggerMode); +}; + +export const setCronjobAlertMode = (advancedParams: string | undefined, mode: CronjobAlertTriggerMode): string => { + return JSON.stringify({ ...parseAdvancedParams(advancedParams), alertTriggerMode: mode }); +}; + +export const getCronjobAlertModeLabel = (mode: unknown): string => { + return cronjobAlertModes.find((item) => item.value === normalizeCronjobAlertMode(mode))!.label; +}; + +export const getCronjobAlertTitle = ( + row: { type: string; subType?: string; taskName?: string; title: string }, + renderTitle: (type: string, taskName: string) => string, +): string => { + const type = row.type === 'cronJob' ? row.subType : row.type; + if (type && cronjobAlertTypes.includes(type) && typeof row.taskName === 'string' && row.taskName.length > 0) { + return renderTitle(type, row.taskName); + } + return row.title; +}; + +export const getCronjobAlertResult = (params: unknown): 'success' | 'failed' => { + if (typeof params === 'string') { + try { + params = JSON.parse(params); + } catch { + return 'failed'; + } + } + if (!Array.isArray(params)) return 'failed'; + const result = params.find((item) => item?.index === 'result'); + if (result) return result.value === 'success' ? 'success' : 'failed'; + const numberedResult = params.find((item) => item?.index === '2' && item?.key === 'result'); + return numberedResult?.value === '成功' ? 'success' : 'failed'; +}; diff --git a/frontend/src/views/cronjob/cronjob/operate/index.vue b/frontend/src/views/cronjob/cronjob/operate/index.vue index 8f8a0d377d21..833446e3aaaf 100644 --- a/frontend/src/views/cronjob/cronjob/operate/index.vue +++ b/frontend/src/views/cronjob/cronjob/operate/index.vue @@ -689,10 +689,10 @@ - + - {{ $t('xpack.alert.cronJobHelper') }} + {{ $t('xpack.alert.alertNotificationHelper') }} {{ $t('xpack.alert.licenseHelper') }} @@ -703,6 +703,20 @@ + + + + + + + + + ({ secret: '', hasAlert: false, alertCount: 3, + alertTriggerMode: 'failed', alertTitle: '', alertMethod: '', alertMethodItems: [], @@ -1133,6 +1149,7 @@ const search = async () => { form.secret = res.data.secret; form.hasAlert = res.data.alertCount > 0; form.alertCount = res.data.alertCount || 3; + form.alertTriggerMode = normalizeCronjobAlertMode(res.data.alertTriggerMode); form.alertTitle = res.data.alertTitle; if (res.data.alertMethod) { form.alertMethodItems = normalizeAlertMethodItems(res.data.alertMethod.split(',') || []); @@ -1671,7 +1688,7 @@ const onSubmit = async (formEl: FormInstance | undefined) => { form.alertCount = form.hasAlert ? form.alertCount : 0; form.alertMethod = form.alertMethodItems.join(','); form.alertTitle = form.hasAlert - ? i18n.global.t('cronjob.alertTitle', [i18n.global.t('cronjob.' + form.type), form.name]) + ? i18n.global.t('xpack.alert.notificationTitle', [i18n.global.t('cronjob.' + form.type), form.name]) : ''; if (!form) return; diff --git a/frontend/src/views/setting/alert/dash/index.vue b/frontend/src/views/setting/alert/dash/index.vue index 10ac7f699c4a..f29e994266a6 100644 --- a/frontend/src/views/setting/alert/dash/index.vue +++ b/frontend/src/views/setting/alert/dash/index.vue @@ -84,6 +84,7 @@ @@ -167,6 +168,12 @@ import { ElMessageBox } from 'element-plus'; import AddTask from '@/views/setting/alert/dash/task/index.vue'; import { Alert } from '@/api/interface/alert'; import { UpdateAlertStatus, SearchAlerts, DeleteAlert, PageAlertConfigs } from '@/api/modules/alert'; +import { + cronjobAlertTypes, + getCronjobAlertMode, + getCronjobAlertModeLabel, + getCronjobAlertTitle, +} from '@/utils/cronjob-alert'; const { isMobile, isMaster, isProductPro, isEE } = useGlobalStore(); @@ -238,7 +245,21 @@ const changeSort = ({ prop, order }) => { search(); }; +const formatTitle = (row: Alert.AlertInfo) => { + return getCronjobAlertTitle(row, (type, taskName) => + t('xpack.alert.notificationTitle', [t('cronjob.' + type), taskName]), + ); +}; + const formatRule = (row: Alert.AlertInfo) => { + const type = row.type === 'cronJob' ? row.subType : row.type; + if (cronjobAlertTypes.includes(type)) { + return t('xpack.alert.notificationRule', [ + t('cronjob.' + type), + t(getCronjobAlertModeLabel(getCronjobAlertMode(row.advancedParams))), + row.sendCount, + ]); + } const ruleTemplates = { ssl: () => t('xpack.alert.timeRule', [row.cycle, row.sendCount]), siteEndTime: () => t('xpack.alert.timeRule', [row.cycle, row.sendCount]), @@ -253,17 +274,6 @@ const formatRule = (row: Alert.AlertInfo) => { : t('xpack.alert.diskRule', [row.project, row.count, row.cycle === 1 ? 'G' : '%', row.sendCount]); }, clams: () => t('xpack.alert.clamsRule', [row.sendCount]), - app: () => t('xpack.alert.cronJobAppRule', [row.sendCount]), - website: () => t('xpack.alert.cronJobWebsiteRule', [row.sendCount]), - database: () => t('xpack.alert.cronJobDatabaseRule', [row.sendCount]), - directory: () => t('xpack.alert.cronJobDirectoryRule', [row.sendCount]), - log: () => t('xpack.alert.cronJobLogRule', [row.sendCount]), - snapshot: () => t('xpack.alert.cronJobSnapshotRule', [row.sendCount]), - shell: () => t('xpack.alert.cronJobShellRule', [row.sendCount]), - curl: () => t('xpack.alert.cronJobCurlRule', [row.sendCount]), - cutWebsiteLog: () => t('xpack.alert.cronJobCutWebsiteLogRule', [row.sendCount]), - clean: () => t('xpack.alert.cronJobCleanRule', [row.sendCount]), - ntp: () => t('xpack.alert.cronJobNtpRule', [row.sendCount]), nodeException: () => t('xpack.alert.nodeExceptionRule', [row.sendCount]), licenseException: () => t('xpack.alert.licenseExceptionRule', [row.sendCount]), panelLogin: () => t('xpack.alert.panelLoginRule', [row.sendCount]), diff --git a/frontend/src/views/setting/alert/dash/task/index.vue b/frontend/src/views/setting/alert/dash/task/index.vue index ea2f76ee1392..cafe7cd1a0aa 100644 --- a/frontend/src/views/setting/alert/dash/task/index.vue +++ b/frontend/src/views/setting/alert/dash/task/index.vue @@ -60,6 +60,8 @@ + + @@ -305,6 +307,22 @@ {{ $t('xpack.alert.ipWhiteListHelper') }} + + + + + {{ $t('xpack.alert.alertNotificationHelper') }} + + @@ -398,6 +416,7 @@ import { routerToName } from '@/utils/router'; import { checkCidr, checkCidrV6, checkIpV4V6 } from '@/utils/validate'; import { useGlobalStore } from '@/composables/useGlobalStore'; import { getAlertConfigDisplayName } from '@/views/setting/alert/setting/drawer/secret-field'; +import { cronjobAlertTypes, cronjobAlertModes, getCronjobAlertMode, setCronjobAlertMode } from '@/utils/cronjob-alert'; const { isMaster, isProductPro, isEE, isIntl } = useGlobalStore(); @@ -524,19 +543,7 @@ const noParamTypes = ['panelUpdate']; const intervalTypes = ['cpu', 'memory', 'load', 'disk', 'sshLogin', 'panelLogin', 'nodeException', 'licenseException']; const diskTypes = ['disk']; -const cronjobTypes = [ - 'shell', - 'app', - 'website', - 'database', - 'directory', - 'log', - 'snapshot', - 'curl', - 'cutWebsiteLog', - 'clean', - 'ntp', -]; +const cronjobTypes = cronjobAlertTypes; const acceptParams = (params: DialogProps): void => { dialogData.value = params; @@ -556,6 +563,7 @@ const acceptParams = (params: DialogProps): void => { dialogData.value.rowData.subType = dialogData.value.rowData.type; dialogData.value.rowData.type = 'cronJob'; } + dialogData.value.rowData.alertTriggerMode = getCronjobAlertMode(dialogData.value.rowData.advancedParams); initOptions(dialogData.value.rowData.type, dialogData.value.rowData.subType); visible.value = true; }; @@ -655,6 +663,7 @@ function checkSendCount(rule: any, value: any, callback: any) { } function checkIPs(rule: any, value: any, callback: any) { + if (!ipTypes.includes(dialogData.value.rowData.type)) return callback(); if (typeof value === 'string' && value.trim() !== '') { let addr = value.split('\n'); for (const item of addr) { @@ -793,6 +802,12 @@ const changeType = () => { type = subType; rowData.subType = subType; } + if (cronjobTypes.includes(type)) { + rowData.project = ''; + rowData.cycle = 0; + rowData.count = 0; + rowData.alertTriggerMode = rowData.alertTriggerMode || 'failed'; + } rowData.project = typeof typeToProjectMap[type] !== 'undefined' ? typeToProjectMap[type] : rowData.project; rowData.cycle = typeof typeToCycleMap[type] !== 'undefined' ? typeToCycleMap[type] : rowData.cycle; rowData.count = typeof typeToCountMap[type] !== 'undefined' ? typeToCountMap[type] : rowData.count; @@ -838,12 +853,13 @@ const loadCronJob = async (jobType: string) => { status: '', }); cronJobOptions.value = res.data || []; - dialogData.value.rowData.project = dialogData.value.rowData.project || String(cronJobOptions.value[0].id); + dialogData.value.rowData.project = dialogData.value.rowData.project || String(cronJobOptions.value[0]?.id || ''); }; const formatTitle = (row: Alert.AlertInfo) => { - if (row.type === 'cronJob') { - row.type = row.subType; + const type = row.type === 'cronJob' ? row.subType : row.type; + if (cronjobTypes.includes(type)) { + return t('xpack.alert.notificationTitle', [t('cronjob.' + type), formatCronJobName(Number(row.project))]); } const titleTemplates = { ssl: () => { @@ -865,17 +881,6 @@ const formatTitle = (row: Alert.AlertInfo) => { return row.project === 'all' ? t('xpack.alert.allDiskTitle') : t('xpack.alert.diskTitle', [row.project]); }, clams: () => t('xpack.alert.clamsTitle', [formatClamName(Number(row.project))]), - app: () => t('xpack.alert.cronJobAppTitle', [formatCronJobName(Number(row.project))]), - website: () => t('xpack.alert.cronJobWebsiteTitle', [formatCronJobName(Number(row.project))]), - database: () => t('xpack.alert.cronJobDatabaseTitle', [formatCronJobName(Number(row.project))]), - directory: () => t('xpack.alert.cronJobDirectoryTitle', [formatCronJobName(Number(row.project))]), - log: () => t('xpack.alert.cronJobLogTitle', [formatCronJobName(Number(row.project))]), - snapshot: () => t('xpack.alert.cronJobSnapshotTitle', [formatCronJobName(Number(row.project))]), - shell: () => t('xpack.alert.cronJobShellTitle', [formatCronJobName(Number(row.project))]), - curl: () => t('xpack.alert.cronJobCurlTitle', [formatCronJobName(Number(row.project))]), - cutWebsiteLog: () => t('xpack.alert.cronJobCutWebsiteLogTitle', [formatCronJobName(Number(row.project))]), - clean: () => t('xpack.alert.cronJobCleanTitle', [formatCronJobName(Number(row.project))]), - ntp: () => t('xpack.alert.cronJobNtpTitle', [formatCronJobName(Number(row.project))]), nodeException: () => t('xpack.alert.nodeException'), licenseException: () => t('xpack.alert.licenseException'), panelLogin: () => t('xpack.alert.panelLogin'), @@ -909,25 +914,31 @@ const emit = defineEmits<{ (e: 'search'): void }>(); const onSubmit = async (formEl: FormInstance | undefined) => { if (loading.value) return; - loading.value = true; if (!formEl) return; + loading.value = true; await formEl.validate(async (valid) => { - if (!valid) return; - if (!dialogData.value.rowData) return; + if (!valid || !dialogData.value.rowData) { + loading.value = false; + return; + } const sendMethods = dialogData.value.rowData.sendMethod.includes(ALL_SEND_METHOD) ? allConfigValues.value : dialogData.value.rowData.sendMethod; - dialogData.value.rowData.method = sendMethods.join(','); - dialogData.value.rowData.title = formatTitle(dialogData.value.rowData); - if (dialogData.value.rowData.type === 'cronJob') { - dialogData.value.rowData.type = dialogData.value.rowData.subType; + const payload = { + ...dialogData.value.rowData, + method: sendMethods.join(','), + title: formatTitle(dialogData.value.rowData), + }; + if (payload.type === 'cronJob') { + payload.type = payload.subType; + payload.advancedParams = setCronjobAlertMode(payload.advancedParams, payload.alertTriggerMode || 'failed'); } try { if (dialogData.value.title === 'create') { - await CreateAlert(dialogData.value.rowData); + await CreateAlert(payload); MsgSuccess(i18n.global.t('commons.msg.createSuccess')); } else if (dialogData.value.title === 'edit') { - await UpdateAlert(dialogData.value.rowData); + await UpdateAlert(payload); MsgSuccess(i18n.global.t('commons.msg.updateSuccess')); } emit('search'); diff --git a/frontend/src/views/setting/alert/log/index.vue b/frontend/src/views/setting/alert/log/index.vue index 3dfcb4a2f69b..0268f64b3a5e 100644 --- a/frontend/src/views/setting/alert/log/index.vue +++ b/frontend/src/views/setting/alert/log/index.vue @@ -99,6 +99,7 @@ import { import { ElMessageBox } from 'element-plus'; import { useGlobalStore } from '@/composables/useGlobalStore'; import { getAlertConfigDisplayName } from '@/views/setting/alert/setting/drawer/secret-field'; +import { cronjobAlertTypes, getCronjobAlertResult } from '@/utils/cronjob-alert'; const { isMobile, isProductPro, isIntl, isMaster } = useGlobalStore(); const { t } = i18n.global; @@ -193,7 +194,15 @@ const syncAlert = (row: Alert.AlertLog) => { }); }; -const formatMessage = (row: Alert.AlertInfo) => { +const formatMessage = (row: Alert.AlertDetail) => { + const type = row.type === 'cronJob' ? row.subType : row.type; + if (cronjobAlertTypes.includes(type)) { + const title = + getCronjobAlertResult(row.params) === 'success' + ? 'xpack.alert.notificationSuccessTitle' + : 'cronjob.alertTitle'; + return t(title, [t('cronjob.' + type), row.project]); + } const messageTemplates = { ssl: () => { return row.project === 'all' ? t('xpack.alert.allSslTitle') : t('xpack.alert.sslTitle', [row.project]); @@ -212,17 +221,6 @@ const formatMessage = (row: Alert.AlertInfo) => { return row.project === 'all' ? t('xpack.alert.allDiskTitle') : t('xpack.alert.diskTitle', [row.project]); }, clams: () => t('xpack.alert.clamsTitle', [row.project]), - app: () => t('xpack.alert.cronJobAppTitle', [row.project]), - website: () => t('xpack.alert.cronJobWebsiteTitle', [row.project]), - database: () => t('xpack.alert.cronJobDatabaseTitle', [row.project]), - directory: () => t('xpack.alert.cronJobDirectoryTitle', [row.project]), - log: () => t('xpack.alert.cronJobLogTitle', [row.project]), - snapshot: () => t('xpack.alert.cronJobSnapshotTitle', [row.project]), - shell: () => t('xpack.alert.cronJobShellTitle', [row.project]), - curl: () => t('xpack.alert.cronJobCurlTitle', [row.project]), - cutWebsiteLog: () => t('xpack.alert.cronJobCutWebsiteLogTitle', [row.project]), - clean: () => t('xpack.alert.cronJobCleanTitle', [row.project]), - ntp: () => t('xpack.alert.cronJobNtpTitle', [row.project]), nodeException: () => t('xpack.alert.nodeException'), licenseException: () => t('xpack.alert.licenseException'), panelLogin: () => t('xpack.alert.panelLogin'), @@ -230,7 +228,6 @@ const formatMessage = (row: Alert.AlertInfo) => { panelIpLogin: () => t('xpack.alert.panelIpLogin'), sshIpLogin: () => t('xpack.alert.sshIpLogin'), }; - let type = row.type === 'cronJob' ? row.subType : row.type; return messageTemplates[type] ? messageTemplates[type]() : ''; }; diff --git a/frontend/src/views/setting/alert/setting/drawer/index.vue b/frontend/src/views/setting/alert/setting/drawer/index.vue index 5debe49afc0a..40b6bdd9ad29 100644 --- a/frontend/src/views/setting/alert/setting/drawer/index.vue +++ b/frontend/src/views/setting/alert/setting/drawer/index.vue @@ -78,7 +78,6 @@