From 5a58f445481cb78df643fc6732c7c06476a3735b Mon Sep 17 00:00:00 2001 From: guoyuqi Date: Tue, 29 Sep 2026 11:00:30 +0800 Subject: [PATCH] refactor: search bind --- agent/app/dto/alert.go | 2 + agent/app/dto/cronjob.go | 17 ++- agent/app/service/alert.go | 98 +++++++++++++ agent/app/service/alert_helper.go | 19 ++- 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 + .../src/composables/useSearchPersistence.ts | 17 +++ 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/store/modules/process.ts | 14 +- frontend/src/utils/cronjob-alert.ts | 74 ++++++++++ frontend/src/utils/search-persistence.ts | 63 +++++++++ .../ai/agents/agent/config/tabs/plugins.vue | 8 +- .../agent/config/tabs/skills/hermes.vue | 8 +- .../agent/config/tabs/skills/openclaw.vue | 6 + frontend/src/views/ai/agents/agent/index.vue | 2 + frontend/src/views/ai/agents/model/index.vue | 2 + frontend/src/views/ai/model/ollama/index.vue | 2 + frontend/src/views/app-store/apps/index.vue | 4 +- .../src/views/app-store/installed/index.vue | 4 +- .../src/views/container/compose/index.vue | 2 + .../src/views/container/container/index.vue | 2 + frontend/src/views/container/image/index.vue | 4 +- .../src/views/container/network/index.vue | 2 + frontend/src/views/container/repo/index.vue | 2 + .../src/views/container/template/index.vue | 2 + frontend/src/views/container/volume/index.vue | 2 + frontend/src/views/cronjob/cronjob/index.vue | 2 + .../views/cronjob/cronjob/operate/index.vue | 23 +++- frontend/src/views/cronjob/library/index.vue | 2 + frontend/src/views/database/mongodb/index.vue | 2 + .../views/database/mongodb/remote/index.vue | 2 + frontend/src/views/database/mysql/index.vue | 2 + .../src/views/database/mysql/remote/index.vue | 2 + .../src/views/database/postgresql/index.vue | 2 + .../database/postgresql/remote/index.vue | 2 + .../src/views/database/redis/remote/index.vue | 2 + .../src/views/host/file-management/index.vue | 4 +- .../src/views/host/firewall/docker/index.vue | 2 + .../src/views/host/firewall/forward/index.vue | 2 + .../src/views/host/firewall/rule/index.vue | 2 + frontend/src/views/host/ssh/log/log.vue | 2 + frontend/src/views/host/ssh/session/index.vue | 4 +- frontend/src/views/log/host-system/index.vue | 2 + frontend/src/views/log/login/index.vue | 2 + frontend/src/views/log/operation/index.vue | 2 + .../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 - .../views/setting/backup-account/index.vue | 4 +- frontend/src/views/setting/snapshot/index.vue | 2 + frontend/src/views/terminal/command/index.vue | 2 + frontend/src/views/terminal/host/index.vue | 2 + frontend/src/views/toolbox/clam/index.vue | 2 + frontend/src/views/toolbox/ftp/index.vue | 2 + .../php/extension-management/index.vue | 3 +- frontend/src/views/website/ssl/index.vue | 4 +- frontend/src/views/website/template/index.vue | 2 + frontend/src/views/website/website/index.vue | 4 +- 89 files changed, 943 insertions(+), 129 deletions(-) create mode 100644 agent/utils/alert/cronjob.go create mode 100644 frontend/src/composables/useSearchPersistence.ts create mode 100644 frontend/src/utils/cronjob-alert.ts create mode 100644 frontend/src/utils/search-persistence.ts diff --git a/agent/app/dto/alert.go b/agent/app/dto/alert.go index 84203c674..8f493536d 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 5325381c7..97e8dc8bf 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 c981c88f9..1ccf62e1a 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/alert_helper.go b/agent/app/service/alert_helper.go index d51c65317..f9ca2e791 100644 --- a/agent/app/service/alert_helper.go +++ b/agent/app/service/alert_helper.go @@ -530,11 +530,15 @@ func loadSSHLogin(alert dto.AlertDTO) { if err != nil { global.LOG.Errorf("Failed to load ssh login logs: %v", err) } + interfaceAddrs, err := net.InterfaceAddrs() + if err != nil { + global.LOG.Warnf("Failed to load local IP addresses for ssh login alert: %v", err) + } count, records := summarizeSSHLoginHistories( histories, now, failedWindow, - strings.Split(strings.TrimSpace(alert.AdvancedParams), "\n"), + sshSuccessLoginWhitelist(alert.AdvancedParams, interfaceAddrs), ) isAlert := count >= int(alert.Count) if isAlert { @@ -570,6 +574,19 @@ func loadSSHLogin(alert dto.AlertDTO) { } } +func sshSuccessLoginWhitelist(configured string, interfaceAddrs []net.Addr) []string { + whitelist := strings.Split(strings.TrimSpace(configured), "\n") + whitelist = append(whitelist, "127.0.0.0/8", "::1") + for _, addr := range interfaceAddrs { + ipNet, ok := addr.(*net.IPNet) + if !ok || ipNet.IP == nil || ipNet.IP.IsUnspecified() { + continue + } + whitelist = append(whitelist, ipNet.IP.String()) + } + return whitelist +} + func filterLoginLogsNotInWhitelist(records []model.LoginLog, whitelist []string) []model.LoginLog { filtered := make([]model.LoginLog, 0, len(records)) for _, record := range records { diff --git a/agent/app/service/cronjob.go b/agent/app/service/cronjob.go index 896abbfbb..ddbc7d7a3 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 1cd542d15..34e5cb3a3 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 53edd56a2..ce0945aaa 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 ab910746d..9041477b6 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 f8688c58a..91467f1b0 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 6283d72cf..78ff0019a 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 c9c6929bc..9653bfebb 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 d234f970c..bd9aef612 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 b917600a7..1875c8c7f 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 3c12ab831..dd261ff94 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 4810606e2..a8a2544a7 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 c88781c0b..89ed5305e 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 7badedc7c..ca8140314 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 3d1e33184..b7cb35b1f 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 cf5710182..0bf44a3bd 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 5688436bf..436046c5a 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 000000000..fb081e00f --- /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 a28393698..c25071b01 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 5f9591c17..438c0d86f 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 77d5bf5d9..35895e792 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 a5a0efdd1..7e8c5acd9 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/composables/useSearchPersistence.ts b/frontend/src/composables/useSearchPersistence.ts new file mode 100644 index 000000000..d317ead6c --- /dev/null +++ b/frontend/src/composables/useSearchPersistence.ts @@ -0,0 +1,17 @@ +import { storeToRefs } from 'pinia'; +import GlobalStore from '@/store/modules/global'; +import { bindSearchPersistence } from '@/utils/search-persistence'; + +/** Restore the parent's search fields synchronously, before its initial request. */ +export const useSearchPersistence = ( + page: string, + fields: Parameters[1], + scope?: () => string | number | undefined, +) => { + const { currentNode } = storeToRefs(GlobalStore()); + return bindSearchPersistence(() => { + const context = scope?.(); + if (scope && context === undefined) return undefined; + return JSON.stringify([currentNode.value, page, context ?? '']); + }, fields); +}; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index da776a32c..c1e7950af 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 7b768cd74..909deeb36 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 d6a84f8df..1ae1a727d 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 ba922ccfe..0b72aac5b 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 37d5a55f0..d31b00b7c 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 e74bc728e..7bc6018a9 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 02a3c3b02..b17a54895 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 88fcf88fa..033cb4f81 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 b212a3e0e..993215f84 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 f887ed4a5..a5ede94c0 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 2e1891ebf..24376da15 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 1387f660c..cdbfabd35 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/store/modules/process.ts b/frontend/src/store/modules/process.ts index f2795fd5f..13a5a4ece 100644 --- a/frontend/src/store/modules/process.ts +++ b/frontend/src/store/modules/process.ts @@ -1,5 +1,6 @@ +import { useSearchPersistence } from '@/composables/useSearchPersistence'; import { defineStore } from 'pinia'; -import { ref, reactive } from 'vue'; +import { ref, reactive, toRef } from 'vue'; import { MsgError } from '@/utils/message'; import { checkStreamAuth } from '@/utils/stream-auth'; @@ -46,6 +47,17 @@ export const ProcessStore = defineStore('ProcessStore', () => { port: undefined, }); + useSearchPersistence('host/process/process', { + pid: toRef(psSearch, 'pid'), + name: toRef(psSearch, 'name'), + username: toRef(psSearch, 'username'), + }); + useSearchPersistence('host/process/network', { + processID: toRef(netSearch, 'processID'), + processName: toRef(netSearch, 'processName'), + port: toRef(netSearch, 'port'), + }); + let pendingRequestType: 'ps' | 'net' | null = null; let queuedRequestType: 'ps' | 'net' | null = null; diff --git a/frontend/src/utils/cronjob-alert.ts b/frontend/src/utils/cronjob-alert.ts new file mode 100644 index 000000000..84ce485c5 --- /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/utils/search-persistence.ts b/frontend/src/utils/search-persistence.ts new file mode 100644 index 000000000..51689dcd0 --- /dev/null +++ b/frontend/src/utils/search-persistence.ts @@ -0,0 +1,63 @@ +import { watch, type Ref } from 'vue'; + +type SearchValue = string | number | undefined; +type SearchFields = Record>; + +export const bindSearchPersistence = (key: () => string | undefined, fields: SearchFields) => { + const defaults = Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, field.value])); + let initialized = false; + let restoring = false; + let activeKey: string | undefined; + + const stopRestore = watch( + key, + (value) => { + activeKey = value ? `1panel:search:v1:${value}` : undefined; + let saved: Record = {}; + try { + const parsed = activeKey ? JSON.parse(localStorage.getItem(activeKey) || '{}') : {}; + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) saved = parsed; + } catch { + // Invalid or unavailable storage must not prevent a page from loading. + } + restoring = true; + for (const [name, field] of Object.entries(fields)) { + const value = saved[name]; + const initial = defaults[name]; + const fallback = initialized ? (typeof initial === 'string' ? '' : undefined) : initial; + const valid = + (typeof value === 'string' || (typeof value === 'number' && Number.isFinite(value))) && + (fallback === undefined || typeof value === typeof fallback); + field.value = + value === null + ? typeof initial === 'string' + ? '' + : undefined + : valid + ? (value as SearchValue) + : fallback; + } + restoring = false; + initialized = true; + }, + { immediate: true, flush: 'sync' }, + ); + + const stopSave = watch( + () => Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, field.value ?? null])), + (value) => { + if (restoring || !activeKey) return; + try { + localStorage.setItem(activeKey, JSON.stringify(value)); + } catch { + // Search remains usable when browser storage is disabled or full. + } + }, + { flush: 'sync' }, + ); + + return () => { + stopRestore(); + stopSave(); + }; +}; diff --git a/frontend/src/views/ai/agents/agent/config/tabs/plugins.vue b/frontend/src/views/ai/agents/agent/config/tabs/plugins.vue index 5187d3580..1a7994868 100644 --- a/frontend/src/views/ai/agents/agent/config/tabs/plugins.vue +++ b/frontend/src/views/ai/agents/agent/config/tabs/plugins.vue @@ -172,6 +172,7 @@