feat: Support adding multiple alert methods (#12922)

* feat: Support adding multiple alert methods

* feat: Support adding multiple alert methods
This commit is contained in:
2026-06-02 22:35:21 +08:00
committed by GitHub
parent 654691543c
commit e49fa620c8
45 changed files with 2573 additions and 1152 deletions
+43 -3
View File
@@ -2,11 +2,16 @@ package v2
import (
"errors"
"net/url"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/gin-gonic/gin"
)
const defaultAuditUser = "system"
// @Tags Alert
// @Summary Page alert
// @Accept json
@@ -54,7 +59,7 @@ func (b *BaseApi) CreateAlert(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := alertService.CreateAlert(req)
err := alertService.CreateAlert(req, loadAuditUser(c))
if err != nil {
helper.InternalServer(c, err)
return
@@ -98,7 +103,7 @@ func (b *BaseApi) UpdateAlert(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := alertService.UpdateAlert(req); err != nil {
if err := alertService.UpdateAlert(req, loadAuditUser(c)); err != nil {
helper.InternalServer(c, err)
return
}
@@ -246,6 +251,30 @@ func (b *BaseApi) GetAlertConfig(c *gin.Context) {
helper.SuccessWithData(c, config)
}
// @Tags Alert
// @Summary Page alert config
// @Accept json
// @Param request body dto.PageInfo true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert/config/search [post]
func (b *BaseApi) PageAlertConfig(c *gin.Context) {
var req dto.PageInfo
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
total, configs, err := alertService.PageAlertConfig(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Total: total,
Items: configs,
})
}
// @Tags Alert
// @Summary Update alert config
// @Accept json
@@ -260,13 +289,24 @@ func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := alertService.UpdateAlertConfig(req); err != nil {
if err := alertService.UpdateAlertConfig(req, loadAuditUser(c)); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
func loadAuditUser(c *gin.Context) string {
userName := strings.TrimSpace(c.GetHeader("X-Panel-User"))
if userName == "" {
return defaultAuditUser
}
if decoded, err := url.QueryUnescape(userName); err == nil {
return decoded
}
return userName
}
// @Tags Alert
// @Summary Delete alert config
// @Accept json
+2 -2
View File
@@ -21,7 +21,7 @@ func (b *BaseApi) CreateClam(c *gin.Context) {
return
}
if err := clamService.Create(req); err != nil {
if err := clamService.Create(req, loadAuditUser(c)); err != nil {
helper.InternalServer(c, err)
return
}
@@ -43,7 +43,7 @@ func (b *BaseApi) UpdateClam(c *gin.Context) {
return
}
if err := clamService.Update(req); err != nil {
if err := clamService.Update(req, loadAuditUser(c)); err != nil {
helper.InternalServer(c, err)
return
}
+3 -3
View File
@@ -26,7 +26,7 @@ func (b *BaseApi) CreateCronjob(c *gin.Context) {
return
}
if err := cronjobService.Create(req); err != nil {
if err := cronjobService.Create(req, loadAuditUser(c)); err != nil {
helper.InternalServer(c, err)
return
}
@@ -91,7 +91,7 @@ func (b *BaseApi) ImportCronjob(c *gin.Context) {
return
}
if err := cronjobService.Import(req.Cronjobs); err != nil {
if err := cronjobService.Import(req.Cronjobs, loadAuditUser(c)); err != nil {
helper.InternalServer(c, err)
return
}
@@ -285,7 +285,7 @@ func (b *BaseApi) UpdateCronjob(c *gin.Context) {
return
}
if err := cronjobService.Update(req.ID, req); err != nil {
if err := cronjobService.Update(req.ID, req, loadAuditUser(c)); err != nil {
helper.InternalServer(c, err)
return
}
+2
View File
@@ -46,6 +46,8 @@ type AlertDTO struct {
Status string `json:"status"`
SendCount uint `json:"sendCount"`
AdvancedParams string `json:"advancedParams"`
CreateUser string `json:"createUser"`
UpdateUser string `json:"updateUser"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
+11 -7
View File
@@ -9,9 +9,11 @@ type Alert struct {
Count uint `gorm:"type:integer;not null" json:"count"`
Project string `gorm:"type:varchar(64)" json:"project"`
Status string `gorm:"type:varchar(64);not null" json:"status"`
Method string `gorm:"type:varchar(64);not null" json:"method"`
Method string `gorm:"type:text;not null" json:"method"`
SendCount uint `gorm:"type:integer" json:"sendCount"`
AdvancedParams string `gorm:"type:longText" json:"advancedParams"`
CreateUser string `gorm:"type:varchar(256)" json:"createUser"`
UpdateUser string `gorm:"type:varchar(256)" json:"updateUser"`
}
type AlertTask struct {
@@ -19,7 +21,7 @@ type AlertTask struct {
Type string `gorm:"type:varchar(64);not null" json:"type"`
Quota string `gorm:"type:varchar(64)" json:"quota"`
QuotaType string `gorm:"type:varchar(64)" json:"quotaType"`
Method string `gorm:"type:varchar(64);not null;default:'sms'" json:"method"`
Method string `gorm:"type:varchar(128);not null;default:'sms'" json:"method"`
}
type AlertLog struct {
@@ -34,15 +36,17 @@ type AlertLog struct {
Message string `gorm:"type:varchar(256);" json:"message"`
RecordId uint `gorm:"type:integer;" json:"recordId"`
LicenseId string `gorm:"type:varchar(256);not null;" json:"licenseId" `
Method string `gorm:"type:varchar(64);not null;default:'sms'" json:"method"`
Method string `gorm:"type:varchar(128);not null;default:'sms'" json:"method"`
}
type AlertConfig struct {
BaseModel
Type string `gorm:"type:varchar(64);not null" json:"type"`
Title string `gorm:"type:varchar(64);not null" json:"title"`
Status string `gorm:"type:varchar(64);not null" json:"status"`
Config string `gorm:"type:varchar(256);not null" json:"config"`
Type string `gorm:"type:varchar(64);not null" json:"type"`
Title string `gorm:"type:varchar(64);not null" json:"title"`
Status string `gorm:"type:varchar(64);not null" json:"status"`
Config string `gorm:"type:varchar(256);not null" json:"config"`
CreateUser string `gorm:"type:varchar(256)" json:"createUser"`
UpdateUser string `gorm:"type:varchar(256)" json:"updateUser"`
}
type LoginLog struct {
+78 -11
View File
@@ -6,6 +6,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/global"
"google.golang.org/genproto/googleapis/type/date"
"gorm.io/gorm"
"strconv"
"time"
)
@@ -21,6 +22,7 @@ type IAlertRepo interface {
WithByLicenseId(licenseId string) DBOption
WithByRecordId(recordId uint) DBOption
WithByMethod(method string) DBOption
WithByMethodConfigID(id uint) DBOption
Create(alert *model.Alert) error
Get(opts ...DBOption) (model.Alert, error)
@@ -47,11 +49,15 @@ type IAlertRepo interface {
GetLicensePushCount(method string) (uint, error)
GetConfig(opts ...DBOption) (model.AlertConfig, error)
GetConfigById(id uint) (model.AlertConfig, error)
AlertConfigList(opts ...DBOption) ([]model.AlertConfig, error)
UpdateAlertConfig(maps map[string]interface{}, opts ...DBOption) error
CreateAlertConfig(config *model.AlertConfig) error
DeleteAlertConfig(opts ...DBOption) error
WithByTypeNotIn(types []string) DBOption
PageAlertConfig(page, size int, opts ...DBOption) (int64, []model.AlertConfig, error)
SyncAll(data []model.AlertConfig) error
}
@@ -103,7 +109,14 @@ func (a *AlertRepo) WithByRecordId(recordId uint) DBOption {
func (a *AlertRepo) WithByMethod(method string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("method = ?", method)
return g.Where("(method = ? OR method LIKE ? OR method LIKE ? OR method LIKE ?)", method, method+",%", "%,"+method, "%,"+method+",%")
}
}
func (a *AlertRepo) WithByMethodConfigID(id uint) DBOption {
method := strconv.Itoa(int(id))
return func(g *gorm.DB) *gorm.DB {
return g.Where("(method = ? OR method LIKE ? OR method LIKE ? OR method LIKE ?)", method, method+",%", "%,"+method, "%,"+method+",%")
}
}
@@ -306,32 +319,86 @@ func (a *AlertRepo) GetConfig(opts ...DBOption) (model.AlertConfig, error) {
return alertConfig, err
}
func (a *AlertRepo) GetConfigById(id uint) (model.AlertConfig, error) {
var config model.AlertConfig
err := global.AlertDB.First(&config, id).Error
return config, err
}
func (a *AlertRepo) WithByTypeNotIn(types []string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("`type` NOT IN (?)", types)
}
}
func (a *AlertRepo) PageAlertConfig(page, size int, opts ...DBOption) (int64, []model.AlertConfig, error) {
var configs []model.AlertConfig
db := global.AlertDB.Model(&model.AlertConfig{})
for _, opt := range opts {
db = opt(db)
}
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&configs).Error
return count, configs, err
}
var singletonTypes = map[string]bool{
constant.CommonConfig: true,
}
func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
tx := global.AlertDB.Begin()
var oldConfigs []model.AlertConfig
_ = tx.Find(&oldConfigs).Error
oldConfigMap := make(map[string]uint)
nonSingletonTypes := make(map[string]struct{})
for _, item := range oldConfigs {
oldConfigMap[item.Type] = item.ID
if singletonTypes[item.Type] {
oldConfigMap[item.Type] = item.ID
continue
}
nonSingletonTypes[item.Type] = struct{}{}
}
for _, item := range data {
if val, ok := oldConfigMap[item.Type]; ok {
item.ID = val
delete(oldConfigMap, item.Type)
} else {
item.ID = 0
if !singletonTypes[item.Type] {
nonSingletonTypes[item.Type] = struct{}{}
}
if err := tx.Model(model.AlertConfig{}).Where("id = ?", item.ID).Save(&item).Error; err != nil {
}
for itemType := range nonSingletonTypes {
if err := tx.Where("type = ?", itemType).Delete(&model.AlertConfig{}).Error; err != nil {
tx.Rollback()
return err
}
}
for _, val := range oldConfigMap {
if err := tx.Where("id = ?", val).Delete(&model.AlertConfig{}).Error; err != nil {
for _, item := range data {
if singletonTypes[item.Type] {
if val, ok := oldConfigMap[item.Type]; ok {
item.ID = val
delete(oldConfigMap, item.Type)
} else {
item.ID = 0
}
if err := tx.Model(model.AlertConfig{}).Where("id = ?", item.ID).Save(&item).Error; err != nil {
tx.Rollback()
return err
}
continue
}
item.ID = 0
if err := tx.Create(&item).Error; err != nil {
tx.Rollback()
return err
}
}
tx.Commit()
for _, id := range oldConfigMap {
if err := tx.Where("id = ?", id).Delete(&model.AlertConfig{}).Error; err != nil {
tx.Rollback()
return err
}
}
if err := tx.Commit().Error; err != nil {
return err
}
return nil
}
+126 -10
View File
@@ -25,15 +25,18 @@ import (
type AlertService struct{}
var eeHiddenAlertTypes = []string{"licenseException", "panelUpdate", "panelPwdEndTime"}
var hiddenAlertConfigTypes = []string{"sms"}
type IAlertService interface {
PageAlert(req dto.AlertSearch) (int64, []dto.AlertDTO, error)
GetAlerts() ([]dto.AlertDTO, error)
CreateAlert(create dto.AlertCreate) error
UpdateAlert(req dto.AlertUpdate) error
CreateAlert(create dto.AlertCreate, operator string) error
UpdateAlert(req dto.AlertUpdate, operator string) error
DeleteAlert(id uint) error
GetAlert(id uint) (dto.AlertDTO, error)
UpdateStatus(id uint, status string) error
ExternalUpdateAlert(req dto.AlertCreate) error
ExternalUpdateAlert(req dto.AlertCreate, operator string) error
GetDisks() ([]dto.DiskDTO, error)
PageAlertLogs(req dto.AlertLogSearch) (int64, []dto.AlertLogDTO, error)
@@ -42,7 +45,8 @@ type IAlertService interface {
GetCronJobs(req dto.CronJobReq) ([]dto.CronJobDTO, error)
GetAlertConfig() ([]model.AlertConfig, error)
UpdateAlertConfig(req dto.AlertConfigUpdate) error
PageAlertConfig(req dto.PageInfo) (int64, []model.AlertConfig, error)
UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error
DeleteAlertConfig(id uint) error
TestAlertConfig(req dto.AlertConfigTest) (bool, error)
}
@@ -56,6 +60,9 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO,
opts []repo.DBOption
result []dto.AlertDTO
)
if global.CONF.Base.IsEnterprise {
opts = append(opts, alertRepo.WithByTypeNotIn(eeHiddenAlertTypes))
}
if search.Status != "" {
opts = append(opts, repo.WithByStatus(search.Status))
}
@@ -82,6 +89,8 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO,
Status: item.Status,
SendCount: item.SendCount,
AdvancedParams: item.AdvancedParams,
CreateUser: item.CreateUser,
UpdateUser: item.UpdateUser,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
})
@@ -113,6 +122,8 @@ func (a AlertService) GetAlerts() ([]dto.AlertDTO, error) {
Status: item.Status,
SendCount: item.SendCount,
AdvancedParams: item.AdvancedParams,
CreateUser: item.CreateUser,
UpdateUser: item.UpdateUser,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
})
@@ -121,7 +132,7 @@ func (a AlertService) GetAlerts() ([]dto.AlertDTO, error) {
return result, err
}
func (a AlertService) CreateAlert(create dto.AlertCreate) error {
func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error {
var alertID uint
var alertInfo model.Alert
if create.Project != "" {
@@ -138,7 +149,7 @@ func (a AlertService) CreateAlert(create dto.AlertCreate) error {
return buserr.WithErr("ErrStructTransform", err)
}
upAlert.ID = alertID
err := a.UpdateAlert(upAlert)
err := a.UpdateAlert(upAlert, operator)
if err != nil {
return err
}
@@ -147,6 +158,8 @@ func (a AlertService) CreateAlert(create dto.AlertCreate) error {
if err := copier.Copy(&alertInfo, &create); err != nil {
return buserr.WithErr("ErrStructTransform", err)
}
alertInfo.CreateUser = operator
alertInfo.UpdateUser = operator
if err := alertRepo.Create(&alertInfo); err != nil {
return err
@@ -157,7 +170,7 @@ func (a AlertService) CreateAlert(create dto.AlertCreate) error {
return nil
}
func (a AlertService) UpdateAlert(req dto.AlertUpdate) error {
func (a AlertService) UpdateAlert(req dto.AlertUpdate, operator string) error {
upMap := make(map[string]interface{})
upMap["id"] = req.ID
@@ -170,6 +183,7 @@ func (a AlertService) UpdateAlert(req dto.AlertUpdate) error {
upMap["status"] = req.Status
upMap["send_count"] = req.SendCount
upMap["advanced_params"] = req.AdvancedParams
upMap["update_user"] = operator
if err := alertRepo.Update(upMap, repo.WithByID(req.ID)); err != nil {
return err
@@ -460,12 +474,29 @@ func (a AlertService) GetAlertConfig() ([]model.AlertConfig, error) {
opts []repo.DBOption
configs []model.AlertConfig
)
if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "intl" {
opts = append(opts, alertRepo.WithByTypeNotIn(hiddenAlertConfigTypes))
}
opts = append(opts, repo.WithByStatus(constant.AlertEnable))
configs, err := alertRepo.AlertConfigList(opts...)
return configs, err
}
func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate) error {
func (a AlertService) PageAlertConfig(req dto.PageInfo) (int64, []model.AlertConfig, error) {
opts := []repo.DBOption{
alertRepo.WithByTypeNotIn([]string{"common"}),
repo.WithOrderDesc("created_at"),
}
if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "intl" {
opts = append(opts, alertRepo.WithByTypeNotIn(hiddenAlertConfigTypes))
}
return alertRepo.PageAlertConfig(req.Page, req.PageSize, opts...)
}
func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error {
if err := a.checkAlertConfigDisplayNameUnique(req); err != nil {
return err
}
if req.ID != 0 {
upMap := make(map[string]interface{})
upMap["id"] = req.ID
@@ -473,6 +504,7 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate) error {
upMap["title"] = req.Title
upMap["status"] = req.Status
upMap["config"] = req.Config
upMap["update_user"] = operator
if err := alertRepo.UpdateAlertConfig(upMap, repo.WithByID(req.ID)); err != nil {
return err
}
@@ -481,6 +513,8 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate) error {
if err := copier.Copy(&alertConfig, &req); err != nil {
return buserr.WithErr("ErrStructTransform", err)
}
alertConfig.CreateUser = operator
alertConfig.UpdateUser = operator
if err := alertRepo.CreateAlertConfig(&alertConfig); err != nil {
return err
}
@@ -489,7 +523,70 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate) error {
return nil
}
func (a AlertService) checkAlertConfigDisplayNameUnique(req dto.AlertConfigUpdate) error {
displayName := alertConfigDisplayName(req.Type, req.Config)
if displayName == "" {
return nil
}
configs, err := alertRepo.AlertConfigList(alertRepo.WithByType(req.Type))
if err != nil {
return err
}
for _, config := range configs {
if req.ID != 0 && config.ID == req.ID {
continue
}
if alertConfigDisplayName(config.Type, config.Config) == displayName {
return buserr.New("ErrNameIsExist")
}
}
return nil
}
func alertConfigDisplayName(configType, configData string) string {
switch configType {
case constant.Email, constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Bark:
var cfg struct {
DisplayName string `json:"displayName"`
}
if err := json.Unmarshal([]byte(configData), &cfg); err != nil {
return ""
}
return strings.TrimSpace(cfg.DisplayName)
default:
return ""
}
}
func (a AlertService) DeleteAlertConfig(id uint) error {
config, err := alertRepo.GetConfigById(id)
if err != nil {
return err
}
usedAlerts, err := alertRepo.List(alertRepo.WithByMethodConfigID(id))
if err != nil {
return err
}
if config.Type == constant.SMS {
legacyAlerts, err := alertRepo.List(alertRepo.WithByMethod(constant.SMS))
if err != nil {
return err
}
usedAlerts = append(usedAlerts, legacyAlerts...)
}
if legacyMethod := legacyAlertMethodByConfigType(config.Type); legacyMethod != "" {
legacyAlerts, err := alertRepo.List(alertRepo.WithByMethod(legacyMethod))
if err != nil {
return err
}
usedAlerts = append(usedAlerts, legacyAlerts...)
}
if len(usedAlerts) > 0 {
return fmt.Errorf("alert config is in use")
}
return alertRepo.DeleteAlertConfig(repo.WithByID(id))
}
@@ -522,7 +619,7 @@ func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
return true, nil
}
func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate) error {
func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator string) error {
upMap := make(map[string]interface{})
var newStatus string
if updateAlert.SendCount == 0 {
@@ -566,7 +663,7 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate) error {
} else {
if updateAlert.Method != "" && updateAlert.Title != "" {
updateAlert.Status = newStatus
if err := a.CreateAlert(updateAlert); err != nil {
if err := a.CreateAlert(updateAlert, operator); err != nil {
return err
}
}
@@ -574,3 +671,22 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate) error {
return nil
}
func legacyAlertMethodByConfigType(configType string) string {
switch configType {
case constant.Email:
return "mail"
case constant.SMS:
return constant.SMS
case constant.Bark:
return constant.Bark
case constant.WeCom:
return constant.WeCom
case constant.DingTalk:
return constant.DingTalk
case constant.FeiShu:
return constant.FeiShu
default:
return ""
}
}
+137 -83
View File
@@ -174,10 +174,16 @@ func baseTask(baseAlert []dto.AlertDTO) {
case "siteEndTime":
loadWebsiteInfo(alert)
case "panelPwdEndTime":
if global.CONF.Base.IsEnterprise {
continue
}
if global.IsMaster {
loadPanelPwd(alert)
}
case "panelUpdate":
if global.CONF.Base.IsEnterprise {
continue
}
if global.IsMaster {
loadPanelUpdate(alert)
}
@@ -210,6 +216,9 @@ func resourceTask(resourceAlert []dto.AlertDTO) {
loadNodeException(alert)
}
case "licenseException":
if global.CONF.Base.IsEnterprise {
continue
}
if execute && global.IsMaster {
loadLicenseException(alert)
}
@@ -590,94 +599,139 @@ func sendAlerts(alert dto.AlertDTO, alertType, quota, quotaType string, params [
if newDate.IsZero() || calculateMinutesDifference(newDate) > ResourceAlertInterval {
for _, m := range methods {
m = strings.TrimSpace(m)
switch m {
case constant.SMS:
if !alertUtil.CheckSMSSendLimit(constant.SMS) {
continue
}
todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, constant.SMS)
if !isValid {
continue
}
create := dto.AlertLogCreate{
Type: alertType,
AlertId: alert.ID,
Count: todayCount + 1,
}
alertErr := xpack.AlertProvider.CreateSMSAlertLog(alertType, alert, create, quotaType, params, constant.SMS)
if alertErr != nil {
global.LOG.Infof("%s alert sms push faild, err: %v", alertType, alertErr.Error())
continue
}
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, constant.SMS)
case constant.Email:
todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, constant.Email)
if !isValid {
continue
}
create := dto.AlertLogCreate{
Type: alertType,
AlertId: alert.ID,
Count: todayCount + 1,
}
alertInfo := alert
alertInfo.Type = alertType
create.AlertRule = alertUtil.ProcessAlertRule(alert)
create.AlertDetail = alertUtil.ProcessAlertDetail(alertInfo, quotaType, params, constant.Email)
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
alertErr := alertUtil.CreateEmailAlertLog(create, alertInfo, params, transport, agentInfo)
if alertErr != nil {
global.LOG.Infof("%s alert email push faild, err: %v", alertType, alertErr.Error())
continue
}
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, constant.Email)
case constant.WeCom, constant.DingTalk, constant.FeiShu:
todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, m)
if !isValid {
continue
}
var create = dto.AlertLogCreate{
Type: alertUtil.GetCronJobType(alert.Type),
AlertId: alert.ID,
Count: todayCount + 1,
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
err := xpack.AlertProvider.CreateWebhookAlertLog(alertType, alert, create, quotaType, params, m, transport, agentInfo)
if err != nil {
global.LOG.Infof("%s alert webhook %s push faild, err: %v", alertType, m, err)
continue
}
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, m)
case constant.Bark:
todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, m)
if !isValid {
continue
}
var create = dto.AlertLogCreate{
Type: alertUtil.GetCronJobType(alert.Type),
AlertId: alert.ID,
Count: todayCount + 1,
}
alertInfo := alert
alertInfo.Type = alertType
create.AlertRule = alertUtil.ProcessAlertRule(alert)
create.AlertDetail = alertUtil.ProcessAlertDetail(alertInfo, quotaType, params, m)
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
alertErr := alertUtil.CreateBarkAlertLog(create, alertInfo, params, transport, agentInfo)
if alertErr != nil {
global.LOG.Infof("%s alert %s push failed, err: %v", alertType, m, alertErr.Error())
continue
}
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, m)
default:
if configId, err := strconv.ParseUint(m, 10, 64); err == nil {
sendAlertsByConfigId(alert, alertType, quota, quotaType, params, uint(configId))
} else {
sendAlertsByLegacyMethod(alert, alertType, quota, quotaType, params, m)
}
}
}
}
func sendAlertsByConfigId(alert dto.AlertDTO, alertType, quota, quotaType string, params []dto.Param, configId uint) {
config, err := alertRepo.GetConfigById(configId)
if err != nil {
global.LOG.Errorf("alert config not found for id %d: %v", configId, err)
return
}
doSendAlert(alert, alertType, quota, quotaType, params, config)
}
func sendAlertsByLegacyMethod(alert dto.AlertDTO, alertType, quota, quotaType string, params []dto.Param, method string) {
typeMap := map[string]string{
"mail": constant.Email,
constant.Bark: constant.Bark,
constant.SMS: constant.SMS,
}
configType, ok := typeMap[method]
if !ok {
configType = method
}
config, err := alertRepo.GetConfig(alertRepo.WithByType(configType))
if err != nil {
global.LOG.Errorf("alert config not found for type %s: %v", configType, err)
return
}
doSendAlert(alert, alertType, quota, quotaType, params, config)
}
func doSendAlert(alert dto.AlertDTO, alertType, quota, quotaType string, params []dto.Param, config model.AlertConfig) {
if !alertUtil.IsAlertConfigEnabled(config) {
return
}
methodStr := strconv.Itoa(int(config.ID))
switch config.Type {
case constant.SMS:
if !alertUtil.CheckSMSSendLimit(config, methodStr) {
return
}
todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, methodStr)
if !isValid {
return
}
create := dto.AlertLogCreate{
Type: alertType,
AlertId: alert.ID,
Count: todayCount + 1,
Method: methodStr,
}
alertErr := xpack.AlertProvider.CreateSMSAlertLog(alertType, alert, create, quotaType, params, config, methodStr)
if alertErr != nil {
global.LOG.Infof("%s alert sms push faild, err: %v", alertType, alertErr.Error())
return
}
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
case constant.Email:
todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, methodStr)
if !isValid {
return
}
create := dto.AlertLogCreate{
Type: alertType,
AlertId: alert.ID,
Count: todayCount + 1,
Method: methodStr,
}
alertInfo := alert
alertInfo.Type = alertType
create.AlertRule = alertUtil.ProcessAlertRule(alert)
create.AlertDetail = alertUtil.ProcessAlertDetail(alertInfo, quotaType, params, constant.Email)
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
alertErr := alertUtil.CreateEmailAlertLog(create, alertInfo, params, transport, agentInfo, config)
if alertErr != nil {
global.LOG.Infof("%s alert email push faild, err: %v", alertType, alertErr.Error())
return
}
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
case constant.Bark:
todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, methodStr)
if !isValid {
return
}
create := dto.AlertLogCreate{
Type: alertType,
AlertId: alert.ID,
Count: todayCount + 1,
Method: methodStr,
}
alertInfo := alert
alertInfo.Type = alertType
create.AlertRule = alertUtil.ProcessAlertRule(alert)
create.AlertDetail = alertUtil.ProcessAlertDetail(alertInfo, quotaType, params, constant.Bark)
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
alertErr := alertUtil.CreateBarkAlertLog(create, alertInfo, params, transport, agentInfo, config)
if alertErr != nil {
global.LOG.Infof("%s alert %s push failed, err: %v", alertType, methodStr, alertErr.Error())
return
}
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
case constant.WeCom, constant.DingTalk, constant.FeiShu:
todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, methodStr)
if !isValid {
return
}
create := dto.AlertLogCreate{
Type: alertType,
AlertId: alert.ID,
Count: todayCount + 1,
Method: methodStr,
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
alertErr := xpack.AlertProvider.CreateWebhookAlertLog(alertType, alert, create, quotaType, params, config, transport, agentInfo)
if alertErr != nil {
global.LOG.Infof("%s alert webhook %s push faild, err: %v", alertType, methodStr, alertErr)
return
}
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
}
}
// ------------------------------
func getRepoOptionsByProject(project string) []repo.DBOption {
var opts []repo.DBOption
+273 -120
View File
@@ -1,12 +1,16 @@
package service
import (
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"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/xpack"
"strings"
)
type AlertSender struct {
@@ -22,115 +26,91 @@ func NewAlertSender(alert dto.AlertDTO, quotaType string) *AlertSender {
}
func (s *AlertSender) Send(quota string, params []dto.Param) {
methods := strings.Split(s.alert.Method, ",")
for _, method := range methods {
method = strings.TrimSpace(method)
switch method {
case constant.SMS:
s.sendSMS(quota, params)
case constant.Email:
s.sendEmail(quota, params)
case constant.Bark:
s.sendBark(quota, params)
case constant.WeCom, constant.DingTalk, constant.FeiShu:
s.sendWebhook(quota, params, method)
}
}
s.sendByConfigIds(s.alert.Method, quota, params, false)
}
func (s *AlertSender) ResourceSend(quota string, params []dto.Param) {
methods := strings.Split(s.alert.Method, ",")
for _, method := range methods {
method = strings.TrimSpace(method)
switch method {
case constant.SMS:
s.sendResourceSMS(quota, params)
case constant.Email:
s.sendResourceEmail(quota, params)
case constant.Bark:
s.sendResourceBark(quota, params)
case constant.WeCom, constant.DingTalk, constant.FeiShu:
s.sendResourceWebhook(quota, params, method)
s.sendByConfigIds(s.alert.Method, quota, params, true)
}
func (s *AlertSender) sendByConfigIds(methodStr string, quota string, params []dto.Param, isResource bool) {
alertRepo := repo.NewIAlertRepo()
configIds := strings.Split(methodStr, ",")
for _, idStr := range configIds {
idStr = strings.TrimSpace(idStr)
configId, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
s.sendByLegacyMethod(idStr, quota, params, isResource)
continue
}
config, err := alertRepo.GetConfigById(uint(configId))
if err != nil {
global.LOG.Errorf("alert config not found for id %d: %v", configId, err)
continue
}
s.sendByConfig(config, quota, params, isResource)
}
}
func (s *AlertSender) sendByConfig(config model.AlertConfig, quota string, params []dto.Param, isResource bool) {
if !alertUtil.IsAlertConfigEnabled(config) {
return
}
switch config.Type {
case constant.SMS:
if isResource {
s.sendResourceSMSWithConfig(config, quota, params)
} else {
s.sendSMSWithConfig(config, quota, params)
}
case constant.Email:
if isResource {
s.sendResourceEmailWithConfig(config, quota, params)
} else {
s.sendEmailWithConfig(config, quota, params)
}
case constant.Bark:
if isResource {
s.sendResourceBarkWithConfig(config, quota, params)
} else {
s.sendBarkWithConfig(config, quota, params)
}
case constant.WeCom, constant.DingTalk, constant.FeiShu:
if isResource {
s.sendResourceWebhookWithConfig(config, quota, params)
} else {
s.sendWebhookWithConfig(config, quota, params)
}
}
}
func (s *AlertSender) sendSMS(quota string, params []dto.Param) {
if !alertUtil.CheckSMSSendLimit(constant.SMS) {
return
func (s *AlertSender) sendByLegacyMethod(method string, quota string, params []dto.Param, isResource bool) {
alertRepo := repo.NewIAlertRepo()
typeMap := map[string]string{"mail": constant.Email, constant.Bark: constant.Bark, constant.SMS: constant.SMS}
configType := method
if mapped, ok := typeMap[method]; ok {
configType = mapped
}
totalCount, isValid := s.canSendAlert(constant.SMS)
if !isValid {
return
}
create := dto.AlertLogCreate{
Status: constant.AlertSuccess,
Count: totalCount + 1,
AlertId: s.alert.ID,
Type: s.alert.Type,
}
err := xpack.AlertProvider.CreateSMSAlertLog(s.alert.Type, s.alert, create, quota, params, constant.SMS)
config, err := alertRepo.GetConfig(alertRepo.WithByType(configType))
if err != nil {
global.LOG.Errorf("%s alert sms push failed: %v", s.alert.Type, err)
global.LOG.Errorf("alert config not found for type %s: %v", configType, err)
return
}
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.SMS)
if !alertUtil.IsAlertConfigEnabled(config) {
return
}
s.sendByConfig(config, quota, params, isResource)
}
func (s *AlertSender) sendEmail(quota string, params []dto.Param) {
totalCount, isValid := s.canSendAlert(constant.Email)
if !isValid {
func (s *AlertSender) sendSMSWithConfig(config model.AlertConfig, quota string, params []dto.Param) {
if !alertUtil.IsAlertConfigEnabled(config) {
return
}
method := strconv.Itoa(int(config.ID))
if !alertUtil.CheckSMSSendLimit(config, method) {
return
}
create := dto.AlertLogCreate{
Status: constant.AlertSuccess,
Count: totalCount + 1,
AlertId: s.alert.ID,
Type: s.alert.Type,
AlertRule: alertUtil.ProcessAlertRule(s.alert),
AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Email),
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
err := alertUtil.CreateEmailAlertLog(create, s.alert, params, transport, agentInfo)
if err != nil {
global.LOG.Errorf("%s alert email push failed: %v", s.alert.Type, err)
return
}
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.Email)
}
func (s *AlertSender) sendBark(quota string, params []dto.Param) {
totalCount, isValid := s.canSendAlert(constant.Bark)
if !isValid {
return
}
create := dto.AlertLogCreate{
Status: constant.AlertSuccess,
Count: totalCount + 1,
AlertId: s.alert.ID,
Type: s.alert.Type,
AlertRule: alertUtil.ProcessAlertRule(s.alert),
AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Bark),
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
err := alertUtil.CreateBarkAlertLog(create, s.alert, params, transport, agentInfo)
if err != nil {
global.LOG.Errorf("%s alert bark push failed: %v", s.alert.Type, err)
return
}
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.Bark)
}
func (s *AlertSender) sendWebhook(quota string, params []dto.Param, method string) {
totalCount, isValid := s.canSendAlert(method)
if !isValid {
return
@@ -141,43 +121,60 @@ func (s *AlertSender) sendWebhook(quota string, params []dto.Param, method strin
Count: totalCount + 1,
AlertId: s.alert.ID,
Type: s.alert.Type,
Method: method,
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
err := xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, method, transport, agentInfo)
err := xpack.AlertProvider.CreateSMSAlertLog(s.alert.Type, s.alert, create, quota, params, config, method)
if err != nil {
global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, method, err)
global.LOG.Errorf("%s alert sms push failed: %v", s.alert.Type, err)
return
}
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, method)
}
func (s *AlertSender) sendResourceSMS(quota string, params []dto.Param) {
if !alertUtil.CheckSMSSendLimit(constant.SMS) {
func (s *AlertSender) sendSMS(quota string, params []dto.Param) {
alertRepo := repo.NewIAlertRepo()
config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.SMS))
if err != nil {
return
}
s.sendSMSWithConfig(config, quota, params)
}
todayCount, isValid := s.canResourceSendAlert(constant.SMS)
func (s *AlertSender) sendEmailWithConfig(config model.AlertConfig, quota string, params []dto.Param) {
if !alertUtil.IsAlertConfigEnabled(config) {
return
}
totalCount, isValid := s.canSendAlert(strconv.Itoa(int(config.ID)))
if !isValid {
return
}
create := dto.AlertLogCreate{
Status: constant.AlertSuccess,
Count: todayCount + 1,
AlertId: s.alert.ID,
Type: s.alert.Type,
Status: constant.AlertSuccess,
Count: totalCount + 1,
AlertId: s.alert.ID,
Type: s.alert.Type,
AlertRule: alertUtil.ProcessAlertRule(s.alert),
AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Email),
Method: strconv.Itoa(int(config.ID)),
}
if err := xpack.AlertProvider.CreateSMSAlertLog(s.alert.Type, s.alert, create, quota, params, constant.SMS); err != nil {
global.LOG.Errorf("failed to send SMS alert: %v", err)
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
err := alertUtil.CreateEmailAlertLog(create, s.alert, params, transport, agentInfo, config)
if err != nil {
global.LOG.Errorf("%s alert email push failed: %v", s.alert.Type, err)
return
}
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.SMS)
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
}
func (s *AlertSender) sendResourceEmail(quota string, params []dto.Param) {
todayCount, isValid := s.canResourceSendAlert(constant.Email)
func (s *AlertSender) sendResourceEmailWithConfig(config model.AlertConfig, quota string, params []dto.Param) {
if !alertUtil.IsAlertConfigEnabled(config) {
return
}
todayCount, isValid := s.canResourceSendAlert(strconv.Itoa(int(config.ID)))
if !isValid {
return
}
@@ -189,19 +186,70 @@ func (s *AlertSender) sendResourceEmail(quota string, params []dto.Param) {
Type: s.alert.Type,
AlertRule: alertUtil.ProcessAlertRule(s.alert),
AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Email),
Method: strconv.Itoa(int(config.ID)),
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
if err := alertUtil.CreateEmailAlertLog(create, s.alert, params, transport, agentInfo); err != nil {
if err := alertUtil.CreateEmailAlertLog(create, s.alert, params, transport, agentInfo, config); err != nil {
global.LOG.Errorf("failed to send Email alert: %v", err)
return
}
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.Email)
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
}
func (s *AlertSender) sendResourceBark(quota string, params []dto.Param) {
todayCount, isValid := s.canResourceSendAlert(constant.Bark)
func (s *AlertSender) sendEmail(quota string, params []dto.Param) {
alertRepo := repo.NewIAlertRepo()
config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.EmailConfig))
if err != nil {
return
}
s.sendEmailWithConfig(config, quota, params)
}
func (s *AlertSender) sendResourceEmail(quota string, params []dto.Param) {
alertRepo := repo.NewIAlertRepo()
config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.EmailConfig))
if err != nil {
return
}
s.sendResourceEmailWithConfig(config, quota, params)
}
func (s *AlertSender) sendBarkWithConfig(config model.AlertConfig, quota string, params []dto.Param) {
if !alertUtil.IsAlertConfigEnabled(config) {
return
}
totalCount, isValid := s.canSendAlert(strconv.Itoa(int(config.ID)))
if !isValid {
return
}
create := dto.AlertLogCreate{
Status: constant.AlertSuccess,
Count: totalCount + 1,
AlertId: s.alert.ID,
Type: s.alert.Type,
AlertRule: alertUtil.ProcessAlertRule(s.alert),
AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Bark),
Method: strconv.Itoa(int(config.ID)),
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
err := alertUtil.CreateBarkAlertLog(create, s.alert, params, transport, agentInfo, config)
if err != nil {
global.LOG.Errorf("%s alert bark push failed: %v", s.alert.Type, err)
return
}
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
}
func (s *AlertSender) sendResourceBarkWithConfig(config model.AlertConfig, quota string, params []dto.Param) {
if !alertUtil.IsAlertConfigEnabled(config) {
return
}
todayCount, isValid := s.canResourceSendAlert(strconv.Itoa(int(config.ID)))
if !isValid {
return
}
@@ -213,18 +261,114 @@ func (s *AlertSender) sendResourceBark(quota string, params []dto.Param) {
Type: s.alert.Type,
AlertRule: alertUtil.ProcessAlertRule(s.alert),
AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Bark),
Method: strconv.Itoa(int(config.ID)),
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
if err := alertUtil.CreateBarkAlertLog(create, s.alert, params, transport, agentInfo); err != nil {
if err := alertUtil.CreateBarkAlertLog(create, s.alert, params, transport, agentInfo, config); err != nil {
global.LOG.Errorf("failed to send Bark alert: %v", err)
return
}
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.Bark)
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
}
func (s *AlertSender) sendBark(quota string, params []dto.Param) {
alertRepo := repo.NewIAlertRepo()
config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.Bark))
if err != nil {
return
}
s.sendBarkWithConfig(config, quota, params)
}
func (s *AlertSender) sendResourceBark(quota string, params []dto.Param) {
alertRepo := repo.NewIAlertRepo()
config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.Bark))
if err != nil {
return
}
s.sendResourceBarkWithConfig(config, quota, params)
}
func (s *AlertSender) sendWebhookWithConfig(config model.AlertConfig, quota string, params []dto.Param) {
if !alertUtil.IsAlertConfigEnabled(config) {
return
}
totalCount, isValid := s.canSendAlert(strconv.Itoa(int(config.ID)))
if !isValid {
return
}
create := dto.AlertLogCreate{
Status: constant.AlertSuccess,
Count: totalCount + 1,
AlertId: s.alert.ID,
Type: s.alert.Type,
Method: strconv.Itoa(int(config.ID)),
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
err := xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo)
if err != nil {
global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, config.Type, err)
return
}
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
}
func (s *AlertSender) sendResourceWebhookWithConfig(config model.AlertConfig, quota string, params []dto.Param) {
if !alertUtil.IsAlertConfigEnabled(config) {
return
}
todayCount, isValid := s.canResourceSendAlert(strconv.Itoa(int(config.ID)))
if !isValid {
return
}
create := dto.AlertLogCreate{
Status: constant.AlertSuccess,
Count: todayCount + 1,
AlertId: s.alert.ID,
Type: s.alert.Type,
Method: strconv.Itoa(int(config.ID)),
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
if err := xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo); err != nil {
global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, config.Type, err)
return
}
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
}
func (s *AlertSender) sendWebhook(quota string, params []dto.Param, method string) {
alertRepo := repo.NewIAlertRepo()
config, err := alertRepo.GetConfig(alertRepo.WithByType(method))
if err != nil {
return
}
s.sendWebhookWithConfig(config, quota, params)
}
func (s *AlertSender) sendResourceWebhook(quota string, params []dto.Param, method string) {
alertRepo := repo.NewIAlertRepo()
config, err := alertRepo.GetConfig(alertRepo.WithByType(method))
if err != nil {
return
}
s.sendResourceWebhookWithConfig(config, quota, params)
}
func (s *AlertSender) sendResourceSMSWithConfig(config model.AlertConfig, quota string, params []dto.Param) {
if !alertUtil.IsAlertConfigEnabled(config) {
return
}
method := strconv.Itoa(int(config.ID))
if !alertUtil.CheckSMSSendLimit(config, method) {
return
}
todayCount, isValid := s.canResourceSendAlert(method)
if !isValid {
return
@@ -235,16 +379,25 @@ func (s *AlertSender) sendResourceWebhook(quota string, params []dto.Param, meth
Count: todayCount + 1,
AlertId: s.alert.ID,
Type: s.alert.Type,
Method: method,
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
if err := xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, method, transport, agentInfo); err != nil {
global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, method, err)
if err := xpack.AlertProvider.CreateSMSAlertLog(s.alert.Type, s.alert, create, quota, params, config, method); err != nil {
global.LOG.Errorf("failed to send SMS alert: %v", err)
return
}
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, method)
}
func (s *AlertSender) sendResourceSMS(quota string, params []dto.Param) {
alertRepo := repo.NewIAlertRepo()
config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.SMSConfig))
if err != nil {
return
}
s.sendResourceSMSWithConfig(config, quota, params)
}
func (s *AlertSender) canSendAlert(method string) (uint, bool) {
todayCount, totalCount, err := alertRepo.LoadTaskCount(s.alert.Type, s.quotaType, method)
if err != nil {
+6 -6
View File
@@ -36,8 +36,8 @@ type IClamService interface {
LoadBaseInfo() (dto.ClamBaseInfo, error)
Operate(operate string) error
SearchWithPage(search dto.SearchClamWithPage) (int64, interface{}, error)
Create(req dto.ClamCreate) error
Update(req dto.ClamUpdate) error
Create(req dto.ClamCreate, operator string) error
Update(req dto.ClamUpdate, operator string) error
UpdateStatus(id uint, status string) error
Delete(req dto.ClamDelete) error
HandleOnce(id uint) error
@@ -165,7 +165,7 @@ func (c *ClamService) SearchWithPage(req dto.SearchClamWithPage) (int64, interfa
return total, datas, err
}
func (c *ClamService) Create(req dto.ClamCreate) error {
func (c *ClamService) Create(req dto.ClamCreate, operator string) error {
clam, _ := clamRepo.Get(repo.WithByName(req.Name))
if clam.ID != 0 {
return buserr.New("ErrRecordExist")
@@ -199,7 +199,7 @@ func (c *ClamService) Create(req dto.ClamCreate) error {
Project: strconv.Itoa(int(clam.ID)),
Status: constant.AlertEnable,
}
err := NewIAlertService().CreateAlert(createAlert)
err := NewIAlertService().CreateAlert(createAlert, operator)
if err != nil {
return err
}
@@ -207,7 +207,7 @@ func (c *ClamService) Create(req dto.ClamCreate) error {
return nil
}
func (c *ClamService) Update(req dto.ClamUpdate) error {
func (c *ClamService) Update(req dto.ClamUpdate, operator string) error {
if cmd.CheckIllegal(req.Path) {
return buserr.New("ErrCmdIllegal")
}
@@ -260,7 +260,7 @@ func (c *ClamService) Update(req dto.ClamUpdate) error {
Type: "clams",
Project: strconv.Itoa(int(clam.ID)),
}
err := NewIAlertService().ExternalUpdateAlert(updateAlert)
err := NewIAlertService().ExternalUpdateAlert(updateAlert, operator)
if err != nil {
return err
}
+9 -9
View File
@@ -27,10 +27,10 @@ type CronjobService struct{}
type ICronjobService interface {
SearchWithPage(search dto.PageCronjob) (int64, interface{}, error)
SearchRecords(search dto.SearchRecord) (int64, interface{}, error)
Create(cronjobDto dto.CronjobOperate) error
Create(cronjobDto dto.CronjobOperate, operator string) error
LoadNextHandle(spec string) ([]string, error)
HandleOnce(id uint) error
Update(id uint, req dto.CronjobOperate) error
Update(id uint, req dto.CronjobOperate, operator string) error
UpdateStatus(id uint, status string) error
UpdateGroup(req dto.ChangeGroup) error
Delete(req dto.CronjobBatchDelete) error
@@ -39,7 +39,7 @@ type ICronjobService interface {
HandleStop(id uint) error
Export(req dto.OperateByIDs) (string, error)
Import(req []dto.CronjobTrans) error
Import(req []dto.CronjobTrans, operator string) error
LoadScriptOptions() []dto.ScriptOptions
LoadInfo(req dto.OperateByID) (*dto.CronjobOperate, error)
@@ -212,7 +212,7 @@ func (u *CronjobService) Export(req dto.OperateByIDs) (string, error) {
return string(itemJson), nil
}
func (u *CronjobService) Import(req []dto.CronjobTrans) error {
func (u *CronjobService) Import(req []dto.CronjobTrans, operator string) error {
for _, item := range req {
cronjobItem, _ := cronjobRepo.Get(repo.WithByName(item.Name))
if cronjobItem.ID != 0 {
@@ -405,7 +405,7 @@ func (u *CronjobService) Import(req []dto.CronjobTrans) error {
Project: strconv.Itoa(int(cronjob.ID)),
Status: constant.AlertEnable,
}
_ = NewIAlertService().CreateAlert(createAlert)
_ = NewIAlertService().CreateAlert(createAlert, operator)
}
}
return nil
@@ -558,7 +558,7 @@ func (u *CronjobService) HandleOnce(id uint) error {
return nil
}
func (u *CronjobService) Create(req dto.CronjobOperate) error {
func (u *CronjobService) Create(req dto.CronjobOperate, operator string) error {
cronjob, _ := cronjobRepo.Get(repo.WithByName(req.Name))
if cronjob.ID != 0 {
return buserr.New("ErrRecordExist")
@@ -607,7 +607,7 @@ func (u *CronjobService) Create(req dto.CronjobOperate) error {
Project: strconv.Itoa(int(cronjob.ID)),
Status: constant.AlertEnable,
}
err := NewIAlertService().CreateAlert(createAlert)
err := NewIAlertService().CreateAlert(createAlert, operator)
if err != nil {
return err
}
@@ -678,7 +678,7 @@ func (u *CronjobService) Delete(req dto.CronjobBatchDelete) error {
return nil
}
func (u *CronjobService) Update(id uint, req dto.CronjobOperate) error {
func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string) error {
var cronjob model.Cronjob
if err := copier.Copy(&cronjob, &req); err != nil {
return buserr.WithDetail("ErrStructTransform", err.Error(), nil)
@@ -756,7 +756,7 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate) error {
Type: cronjob.Type,
Project: strconv.Itoa(int(cronModel.ID)),
}
err = NewIAlertService().ExternalUpdateAlert(updateAlert)
err = NewIAlertService().ExternalUpdateAlert(updateAlert, operator)
if err != nil {
return err
}
+1 -1
View File
@@ -20,7 +20,7 @@ const (
const (
WeChat = "wechat"
SMS = "sms"
Email = "mail"
Email = "email"
WeCom = "weCom"
DingTalk = "dingTalk"
FeiShu = "feiShu"
+2
View File
@@ -36,6 +36,8 @@ func InitAgentDB() {
migrations.UpdateMcpServer,
migrations.InitCronjobGroup,
migrations.AddColumnToAlert,
migrations.MigrateAlertMethodConfigIDs,
migrations.AddAlertAuditUser,
migrations.UpdateWebsiteSSL,
migrations.AddQuickJump,
migrations.UpdateMcpServerAddType,
+101 -8
View File
@@ -389,16 +389,20 @@ var InitAlertConfig = &gormigrate.Migration{
Migrate: func(tx *gorm.DB) error {
records := []model.AlertConfig{
{
Type: "sms",
Title: "xpack.alert.smsConfig",
Status: "Enable",
Config: `{"alertDailyNum":50}`,
Type: "sms",
Title: "xpack.alert.smsConfig",
Status: "Enable",
Config: `{"alertDailyNum":50}`,
CreateUser: "system",
UpdateUser: "system",
},
{
Type: "common",
Title: "xpack.alert.commonConfig",
Status: "Enable",
Config: `{"isOffline":"Disable","alertSendTimeRange":{"noticeAlert":{"sendTimeRange":"08:00:00 - 23:59:59","type":["ssl","siteEndTime","panelPwdEndTime","panelUpdate"]},"resourceAlert":{"sendTimeRange":"00:00:00 - 23:59:59","type":["clams","cronJob","cpu","memory","load","disk"]}}}`,
Type: "common",
Title: "xpack.alert.commonConfig",
Status: "Enable",
Config: `{"isOffline":"Disable","alertSendTimeRange":{"noticeAlert":{"sendTimeRange":"08:00:00 - 23:59:59","type":["ssl","siteEndTime","panelPwdEndTime","panelUpdate"]},"resourceAlert":{"sendTimeRange":"00:00:00 - 23:59:59","type":["clams","cronJob","cpu","memory","load","disk"]}}}`,
CreateUser: "system",
UpdateUser: "system",
},
}
for _, r := range records {
@@ -477,6 +481,95 @@ var AddColumnToAlert = &gormigrate.Migration{
},
}
var MigrateAlertMethodConfigIDs = &gormigrate.Migration{
ID: "20251001-migrate-alert-method-config-ids",
Migrate: func(tx *gorm.DB) error {
if err := global.AlertDB.AutoMigrate(&model.Alert{}, &model.AlertLog{}, &model.AlertTask{}, &model.AlertConfig{}); err != nil {
return err
}
if err := migrateAlertMethodConfigIDs(global.AlertDB); err != nil {
return err
}
return nil
},
}
var AddAlertAuditUser = &gormigrate.Migration{
ID: "20260602-add-alert-audit-user",
Migrate: func(tx *gorm.DB) error {
return global.AlertDB.AutoMigrate(&model.Alert{}, &model.AlertConfig{})
},
}
func migrateAlertMethodConfigIDs(tx *gorm.DB) error {
if err := tx.Model(&model.AlertConfig{}).Where("type = ?", "mail").Update("type", constant.EmailConfig).Error; err != nil {
return err
}
typeMap := map[string]string{
"mail": constant.Email,
constant.Email: constant.Email,
constant.SMS: constant.SMS,
constant.Bark: constant.Bark,
constant.WeCom: constant.WeCom,
constant.DingTalk: constant.DingTalk,
constant.FeiShu: constant.FeiShu,
}
configIDs := map[string]string{}
for _, configType := range typeMap {
if _, ok := configIDs[configType]; ok {
continue
}
var config model.AlertConfig
if err := tx.Where("type = ?", configType).Order("id ASC").First(&config).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
continue
}
return err
}
configIDs[configType] = strconv.Itoa(int(config.ID))
}
var alerts []model.Alert
if err := tx.Find(&alerts).Error; err != nil {
return err
}
for _, alert := range alerts {
method := migrateAlertMethodValue(alert.Method, typeMap, configIDs)
if method == alert.Method {
continue
}
if err := tx.Model(&model.Alert{}).Where("id = ?", alert.ID).Update("method", method).Error; err != nil {
return err
}
}
return nil
}
func migrateAlertMethodValue(method string, typeMap map[string]string, configIDs map[string]string) string {
items := strings.Split(method, ",")
next := make([]string, 0, len(items))
seen := map[string]struct{}{}
for _, item := range items {
item = strings.TrimSpace(item)
if item == "" {
continue
}
configType, ok := typeMap[item]
if ok {
if id, exists := configIDs[configType]; exists {
item = id
}
}
if _, exists := seen[item]; exists {
continue
}
seen[item] = struct{}{}
next = append(next, item)
}
return strings.Join(next, ",")
}
var UpdateWebsiteSSL = &gormigrate.Migration{
ID: "20250819-update-website-ssl",
Migrate: func(tx *gorm.DB) error {
+1
View File
@@ -25,6 +25,7 @@ func (a *AlertRouter) InitRouter(Router *gin.RouterGroup) {
alertRouter.POST("/config/update", baseApi.UpdateAlertConfig)
alertRouter.POST("/config/info", baseApi.GetAlertConfig)
alertRouter.POST("/config/search", baseApi.PageAlertConfig)
alertRouter.POST("/config/del", baseApi.DeleteAlertConfig)
alertRouter.POST("/config/test", baseApi.TestAlertConfig)
}
+23 -30
View File
@@ -4,6 +4,16 @@ import (
"encoding/json"
"errors"
"fmt"
"mime"
network "net"
"net/http"
"os"
"os/exec"
"strconv"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
@@ -16,29 +26,20 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/psutil"
"github.com/1Panel-dev/1Panel/agent/utils/re"
"github.com/jinzhu/copier"
"mime"
network "net"
"net/http"
"os"
"os/exec"
"strconv"
"strings"
"sync"
"time"
)
var cronJobAlertTypes = []string{"shell", "app", "website", "database", "directory", "log", "snapshot", "curl", "cutWebsiteLog", "clean", "ntp"}
func CreateTaskScanEmailAlertLog(alert dto.AlertDTO, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string, transport *http.Transport, agentInfo *dto.AgentInfo) error {
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))
alertDetail := ProcessAlertDetail(alert, pushAlert.TaskName, params, method)
alertRule := ProcessAlertRule(alert)
create.AlertRule = alertRule
create.AlertDetail = alertDetail
return CreateEmailAlertLog(create, alert, params, transport, agentInfo)
return CreateEmailAlertLog(create, alert, params, transport, agentInfo, emailConfig)
}
func CreateEmailAlertLog(create dto.AlertLogCreate, alert dto.AlertDTO, params []dto.Param, transport *http.Transport, agentInfo *dto.AgentInfo) error {
func CreateEmailAlertLog(create dto.AlertLogCreate, alert dto.AlertDTO, params []dto.Param, transport *http.Transport, agentInfo *dto.AgentInfo, emailConfig model.AlertConfig) error {
var alertLog model.AlertLog
alertRepo := repo.NewIAlertRepo()
config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.CommonConfig))
@@ -50,11 +51,6 @@ func CreateEmailAlertLog(create dto.AlertLogCreate, alert dto.AlertDTO, params [
if err != nil {
return err
}
create.Method = constant.Email
emailConfig, err := alertRepo.GetConfig(alertRepo.WithByType(constant.EmailConfig))
if err != nil {
return err
}
var emailInfo dto.AlertEmailConfig
err = json.Unmarshal([]byte(emailConfig.Config), &emailInfo)
if err != nil {
@@ -104,17 +100,11 @@ func CreateEmailAlertLog(create dto.AlertLogCreate, alert dto.AlertDTO, params [
}
}
func CreateBarkAlertLog(create dto.AlertLogCreate, alert dto.AlertDTO, params []dto.Param, transport *http.Transport, agentInfo *dto.AgentInfo) error {
func CreateBarkAlertLog(create dto.AlertLogCreate, alert dto.AlertDTO, params []dto.Param, transport *http.Transport, agentInfo *dto.AgentInfo, barkConfig model.AlertConfig) error {
var alertLog model.AlertLog
alertRepo := repo.NewIAlertRepo()
create.Method = constant.Bark
barkConfig, err := alertRepo.GetConfig(alertRepo.WithByType(constant.Bark))
if err != nil {
return err
}
var barkInfo dto.AlertWebhookConfig
err = json.Unmarshal([]byte(barkConfig.Config), &barkInfo)
err := json.Unmarshal([]byte(barkConfig.Config), &barkInfo)
if err != nil {
return err
}
@@ -244,14 +234,13 @@ func CreateAlertParams(param string) []dto.Param {
var checkTaskMutex sync.Mutex
func CheckSMSSendLimit(method string) bool {
alertRepo := repo.NewIAlertRepo()
config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.SMSConfig))
if err != nil {
func CheckSMSSendLimit(config model.AlertConfig, method string) bool {
if config.Type != constant.SMS {
return false
}
alertRepo := repo.NewIAlertRepo()
var cfg dto.AlertSmsConfig
cfg, err = ParseAlertSmsConfig(config.Config)
cfg, err := ParseAlertSmsConfig(config.Config)
if err != nil {
return false
}
@@ -273,6 +262,10 @@ func CheckSMSSendLimit(method string) bool {
return true
}
func IsAlertConfigEnabled(config model.AlertConfig) bool {
return config.Status == constant.AlertEnable
}
type Settings struct {
NoticeAlert Category `json:"noticeAlert"`
ResourceAlert Category `json:"resourceAlert"`
+129 -82
View File
@@ -1,15 +1,17 @@
package alert_push
import (
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"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/xpack"
"github.com/jinzhu/copier"
"strconv"
"strings"
)
func PushAlert(pushAlert dto.PushAlert) error {
@@ -28,87 +30,132 @@ func PushAlert(pushAlert dto.PushAlert) error {
methods := strings.Split(alert.Method, ",")
for _, m := range methods {
m = strings.TrimSpace(m)
switch m {
case constant.SMS:
if !alertUtil.CheckSMSSendLimit(constant.SMS) {
continue
}
todayCount, _, err := alertRepo.LoadTaskCount(alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), constant.SMS)
if err != nil || alert.SendCount <= todayCount {
continue
}
var create = dto.AlertLogCreate{
Type: alertUtil.GetCronJobType(alert.Type),
AlertId: alert.ID,
Count: todayCount + 1,
}
err = xpack.AlertProvider.CreateTaskScanSMSAlertLog(alert, alert.Type, create, pushAlert, constant.SMS)
if err != nil {
global.LOG.Errorf("%s alert sms push failed: %v", alert.Type, err)
continue
}
alertUtil.CreateNewAlertTask(strconv.Itoa(int(pushAlert.EntryID)), alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), constant.SMS)
case constant.Email:
todayCount, _, err := alertRepo.LoadTaskCount(alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), constant.Email)
if err != nil || alert.SendCount <= todayCount {
continue
}
var create = dto.AlertLogCreate{
Type: alertUtil.GetCronJobType(alert.Type),
AlertId: alert.ID,
Count: todayCount + 1,
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
err = alertUtil.CreateTaskScanEmailAlertLog(alert, create, pushAlert, constant.Email, transport, agentInfo)
if err != nil {
global.LOG.Errorf("%s alert email push failed: %v", alert.Type, err)
continue
}
alertUtil.CreateNewAlertTask(strconv.Itoa(int(pushAlert.EntryID)), alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), constant.Email)
case constant.Bark:
todayCount, _, err := alertRepo.LoadTaskCount(alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), constant.Bark)
if err != nil || alert.SendCount <= todayCount {
continue
}
var create = dto.AlertLogCreate{
Type: alertUtil.GetCronJobType(alert.Type),
AlertId: alert.ID,
Count: todayCount + 1,
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
params := alertUtil.CreateAlertParams(alertUtil.GetCronJobTypeName(pushAlert.Param))
alertDetail := alertUtil.ProcessAlertDetail(alert, pushAlert.TaskName, params, constant.Bark)
alertRule := alertUtil.ProcessAlertRule(alert)
create.AlertRule = alertRule
create.AlertDetail = alertDetail
err = alertUtil.CreateBarkAlertLog(create, alert, params, transport, agentInfo)
if err != nil {
global.LOG.Errorf("%s alert bark push failed: %v", alert.Type, err)
continue
}
alertUtil.CreateNewAlertTask(strconv.Itoa(int(pushAlert.EntryID)), alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), constant.Bark)
case constant.WeCom, constant.DingTalk, constant.FeiShu:
todayCount, _, err := alertRepo.LoadTaskCount(alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), m)
if err != nil || alert.SendCount <= todayCount {
continue
}
var create = dto.AlertLogCreate{
Type: alertUtil.GetCronJobType(alert.Type),
AlertId: alert.ID,
Count: todayCount + 1,
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
err = xpack.AlertProvider.CreateTaskScanWebhookAlertLog(alert, alert.Type, create, pushAlert, m, transport, agentInfo)
if err != nil {
global.LOG.Errorf("%s alert %s webhook push failed: %v", alert.Type, m, err)
continue
}
alertUtil.CreateNewAlertTask(strconv.Itoa(int(pushAlert.EntryID)), alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), m)
default:
if configId, err := strconv.ParseUint(m, 10, 64); err == nil {
pushByConfigId(alertRepo, alert, pushAlert, uint(configId))
} else {
pushByLegacyMethod(alertRepo, alert, pushAlert, m)
}
}
return nil
}
func pushByConfigId(alertRepo repo.IAlertRepo, alert dto.AlertDTO, pushAlert dto.PushAlert, configId uint) {
config, err := alertRepo.GetConfigById(configId)
if err != nil {
global.LOG.Errorf("alert config not found for id %d: %v", configId, err)
return
}
sendAlert(alertRepo, alert, pushAlert, config)
}
func pushByLegacyMethod(alertRepo repo.IAlertRepo, alert dto.AlertDTO, pushAlert dto.PushAlert, method string) {
typeMap := map[string]string{
"mail": constant.Email,
constant.Bark: constant.Bark,
constant.SMS: constant.SMS,
}
configType := method
if mapped, ok := typeMap[method]; ok {
configType = mapped
}
config, err := alertRepo.GetConfig(alertRepo.WithByType(configType))
if err != nil {
global.LOG.Errorf("alert config not found for type %s: %v", configType, err)
return
}
sendAlert(alertRepo, alert, pushAlert, config)
}
func sendAlert(alertRepo repo.IAlertRepo, alert dto.AlertDTO, pushAlert dto.PushAlert, config model.AlertConfig) {
if !alertUtil.IsAlertConfigEnabled(config) {
return
}
methodStr := strconv.Itoa(int(config.ID))
switch config.Type {
case constant.SMS:
if !alertUtil.CheckSMSSendLimit(config, methodStr) {
return
}
todayCount, _, err := alertRepo.LoadTaskCount(alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), methodStr)
if err != nil || alert.SendCount <= todayCount {
return
}
create := dto.AlertLogCreate{
Type: alertUtil.GetCronJobType(alert.Type),
AlertId: alert.ID,
Count: todayCount + 1,
Method: methodStr,
}
err = xpack.AlertProvider.CreateTaskScanSMSAlertLog(alert, alert.Type, create, pushAlert, config, methodStr)
if err != nil {
global.LOG.Errorf("%s alert sms push failed: %v", alert.Type, err)
return
}
alertUtil.CreateNewAlertTask(strconv.Itoa(int(pushAlert.EntryID)), alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), methodStr)
case constant.Email:
todayCount, _, err := alertRepo.LoadTaskCount(alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), methodStr)
if err != nil || alert.SendCount <= todayCount {
return
}
create := dto.AlertLogCreate{
Type: alertUtil.GetCronJobType(alert.Type),
AlertId: alert.ID,
Count: todayCount + 1,
Method: methodStr,
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
err = alertUtil.CreateTaskScanEmailAlertLog(alert, create, pushAlert, constant.Email, transport, agentInfo, config)
if err != nil {
global.LOG.Errorf("%s alert email push failed: %v", alert.Type, err)
return
}
alertUtil.CreateNewAlertTask(strconv.Itoa(int(pushAlert.EntryID)), alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), methodStr)
case constant.Bark:
todayCount, _, err := alertRepo.LoadTaskCount(alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), methodStr)
if err != nil || alert.SendCount <= todayCount {
return
}
create := dto.AlertLogCreate{
Type: alertUtil.GetCronJobType(alert.Type),
AlertId: alert.ID,
Count: todayCount + 1,
Method: methodStr,
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
params := alertUtil.CreateAlertParams(alertUtil.GetCronJobTypeName(pushAlert.Param))
alertDetail := alertUtil.ProcessAlertDetail(alert, pushAlert.TaskName, params, constant.Bark)
alertRule := alertUtil.ProcessAlertRule(alert)
create.AlertRule = alertRule
create.AlertDetail = alertDetail
err = alertUtil.CreateBarkAlertLog(create, alert, params, transport, agentInfo, config)
if err != nil {
global.LOG.Errorf("%s alert bark push failed: %v", alert.Type, err)
return
}
alertUtil.CreateNewAlertTask(strconv.Itoa(int(pushAlert.EntryID)), alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), methodStr)
case constant.WeCom, constant.DingTalk, constant.FeiShu:
todayCount, _, err := alertRepo.LoadTaskCount(alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), methodStr)
if err != nil || alert.SendCount <= todayCount {
return
}
create := dto.AlertLogCreate{
Type: alertUtil.GetCronJobType(alert.Type),
AlertId: alert.ID,
Count: todayCount + 1,
Method: methodStr,
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
err = xpack.AlertProvider.CreateTaskScanWebhookAlertLog(alert, alert.Type, create, pushAlert, config, transport, agentInfo)
if err != nil {
global.LOG.Errorf("%s alert %s webhook push failed: %v", alert.Type, methodStr, err)
return
}
alertUtil.CreateNewAlertTask(strconv.Itoa(int(pushAlert.EntryID)), alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), methodStr)
}
}
+57 -8
View File
@@ -1,9 +1,15 @@
package helper
import (
"encoding/json"
"fmt"
"net/http"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant"
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
"github.com/1Panel-dev/1Panel/agent/utils/bark"
"github.com/1Panel-dev/1Panel/agent/utils/xpack/providers"
)
@@ -13,20 +19,63 @@ func NewIAlertProvider() providers.AlertProvider {
return &alertHelper{}
}
func (a *alertHelper) CreateTaskScanSMSAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string) error {
return nil
func (a *alertHelper) CreateTaskScanSMSAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, config model.AlertConfig, method string) error {
params := alertUtil.CreateAlertParams(alertUtil.GetCronJobTypeName(pushAlert.Param))
create.AlertRule = alertUtil.ProcessAlertRule(alert)
create.AlertDetail = alertUtil.ProcessAlertDetail(alert, pushAlert.TaskName, params, method)
if create.Status == "" {
create.Status = constant.AlertSuccess
}
return alertUtil.SaveAlertLog(create, &model.AlertLog{})
}
func (a *alertHelper) CreateSMSAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, method string) error {
return nil
func (a *alertHelper) CreateSMSAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, config model.AlertConfig, method string) error {
create.AlertRule = alertUtil.ProcessAlertRule(info)
if create.AlertDetail == "" {
create.AlertDetail = alertUtil.ProcessAlertDetail(info, project, params, method)
}
if create.Status == "" {
create.Status = constant.AlertSuccess
}
return alertUtil.SaveAlertLog(create, &model.AlertLog{})
}
func (a *alertHelper) CreateTaskScanWebhookAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string, transport *http.Transport, agentInfo *dto.AgentInfo) error {
return nil
func (a *alertHelper) CreateTaskScanWebhookAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, config model.AlertConfig, transport *http.Transport, agentInfo *dto.AgentInfo) error {
params := alertUtil.CreateAlertParams(alertUtil.GetCronJobTypeName(pushAlert.Param))
create.AlertRule = alertUtil.ProcessAlertRule(alert)
create.AlertDetail = alertUtil.ProcessAlertDetail(alert, pushAlert.TaskName, params, config.Type)
return a.CreateWebhookAlertLog(alertType, alert, create, pushAlert.TaskName, params, config, transport, agentInfo)
}
func (a *alertHelper) CreateWebhookAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, method string, transport *http.Transport, agentInfo *dto.AgentInfo) error {
return nil
func (a *alertHelper) CreateWebhookAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, config model.AlertConfig, transport *http.Transport, agentInfo *dto.AgentInfo) error {
var webhookInfo dto.AlertWebhookConfig
if err := json.Unmarshal([]byte(config.Config), &webhookInfo); err != nil {
create.Message = err.Error()
create.Status = constant.AlertError
return alertUtil.SaveAlertLog(create, &model.AlertLog{})
}
if webhookInfo.Url == "" {
create.Message = "webhook url is required"
create.Status = constant.AlertError
return alertUtil.SaveAlertLog(create, &model.AlertLog{})
}
create.AlertRule = alertUtil.ProcessAlertRule(info)
if create.AlertDetail == "" {
create.AlertDetail = alertUtil.ProcessAlertDetail(info, project, params, config.Type)
}
content := alertUtil.GetSendContent(info.Type, params, agentInfo)
if content == "" {
content = fmt.Sprintf("%s: %s", info.Type, info.Title)
}
if err := bark.SendMessage(webhookInfo.Url, "1Panel Alert", content, transport); err != nil {
create.Message = err.Error()
create.Status = constant.AlertError
return alertUtil.SaveAlertLog(create, &model.AlertLog{})
}
if create.Status == "" {
create.Status = constant.AlertSuccess
}
return alertUtil.SaveAlertLog(create, &model.AlertLog{})
}
func (a *alertHelper) GetLicenseErrorAlert() (uint, error) {
+5 -4
View File
@@ -4,14 +4,15 @@ import (
"net/http"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
)
type AlertProvider interface {
GetNodeErrorAlert() (uint, error)
GetLicenseErrorAlert() (uint, error)
CreateTaskScanSMSAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string) error
CreateSMSAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, method string) error
CreateTaskScanWebhookAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string, transport *http.Transport, agentInfo *dto.AgentInfo) error
CreateWebhookAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, method string, transport *http.Transport, agentInfo *dto.AgentInfo) error
CreateTaskScanSMSAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, config model.AlertConfig, method string) error
CreateSMSAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, config model.AlertConfig, method string) error
CreateTaskScanWebhookAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, config model.AlertConfig, transport *http.Transport, agentInfo *dto.AgentInfo) error
CreateWebhookAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, config model.AlertConfig, transport *http.Transport, agentInfo *dto.AgentInfo) error
}
+1 -1
View File
@@ -3,7 +3,7 @@ package constant
const (
WeChat = "wechat"
SMS = "sms"
Email = "mail"
Email = "email"
WeCom = "weCom"
DingTalk = "dingTalk"
FeiShu = "feiShu"
+21
View File
@@ -0,0 +1,21 @@
package helper
type AlertAuditAlert struct {
ID uint `gorm:"primaryKey"`
CreateUser string `gorm:"type:varchar(256)"`
UpdateUser string `gorm:"type:varchar(256)"`
}
func (AlertAuditAlert) TableName() string {
return "alerts"
}
type AlertAuditConfig struct {
ID uint `gorm:"primaryKey"`
CreateUser string `gorm:"type:varchar(256)"`
UpdateUser string `gorm:"type:varchar(256)"`
}
func (AlertAuditConfig) TableName() string {
return "alert_configs"
}
+1
View File
@@ -46,6 +46,7 @@ func Init() {
migrations.AddOperationLogUser,
migrations.AddLoginLogUser,
migrations.AddIsOfflineSetting,
migrations.AddAlertAuditUser,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)
+42
View File
@@ -11,6 +11,7 @@ import (
"github.com/1Panel-dev/1Panel/core/app/dto"
"github.com/1Panel-dev/1Panel/core/app/model"
"github.com/1Panel-dev/1Panel/core/app/repo"
"github.com/1Panel-dev/1Panel/core/constant"
"github.com/1Panel-dev/1Panel/core/global"
"github.com/1Panel-dev/1Panel/core/init/migration/helper"
@@ -1284,3 +1285,44 @@ var AddIsOfflineSetting = &gormigrate.Migration{
return nil
},
}
var AddAlertAuditUser = &gormigrate.Migration{
ID: "20260602-add-alert-audit-user",
Migrate: func(tx *gorm.DB) error {
if !global.AlertDB.Migrator().HasTable(&helper.AlertAuditAlert{}) || !global.AlertDB.Migrator().HasTable(&helper.AlertAuditConfig{}) {
return nil
}
if err := global.AlertDB.AutoMigrate(&helper.AlertAuditAlert{}, &helper.AlertAuditConfig{}); err != nil {
return err
}
username, err := repo.NewISettingRepo().GetValueByKey("UserName")
if err != nil || strings.TrimSpace(username) == "" {
username = global.CONF.Base.Username
}
username = strings.TrimSpace(username)
if username == "" {
return nil
}
updates := []struct {
table string
field string
}{
{table: "alerts", field: "create_user"},
{table: "alerts", field: "update_user"},
{table: "alert_configs", field: "create_user"},
{table: "alert_configs", field: "update_user"},
}
for _, item := range updates {
if err := global.AlertDB.Exec(fmt.Sprintf(
"UPDATE %s SET %s = ? WHERE %s IS NULL OR %s = ''",
item.table,
item.field,
item.field,
item.field,
), username).Error; err != nil {
return err
}
}
return nil
},
}
+4
View File
@@ -47,6 +47,10 @@ func Proxy() gin.HandlerFunc {
return
}
if userName := middleware.LoadOperationUser(c); userName != "" {
c.Request.Header.Set("X-Panel-User", url.QueryEscape(userName))
}
if reqPath == "/api/v2/hosts/terminal/local" && (currentNode == "local" || len(currentNode) == 0) {
proxyLocalAgent(c)
return
+2 -2
View File
@@ -113,7 +113,7 @@ func OperationLog() gin.HandlerFunc {
c.Next()
record.User = loadOperationUser(c)
record.User = LoadOperationUser(c)
if len(operationDic.BeforeFunctions) != 0 {
if needAgentResolve {
@@ -181,7 +181,7 @@ func OperationLog() gin.HandlerFunc {
}
}
func loadOperationUser(c *gin.Context) string {
func LoadOperationUser(c *gin.Context) string {
sessionUser, ok := c.Get(psessionUtils.GinContextSessionUserKey)
if ok {
psession, ok := sessionUser.(psessionUtils.SessionUser)
+16
View File
@@ -14,6 +14,8 @@ export namespace Alert {
sendCount: number;
sendMethod: string[];
advancedParams: string;
createUser?: string;
updateUser?: string;
}
export interface AlertDetail {
@@ -138,6 +140,13 @@ export namespace Alert {
title: string;
config: string;
status: string;
createUser?: string;
updateUser?: string;
}
export interface AlertConfigPageReq {
page: number;
pageSize: number;
}
export interface AlertConfigUpdateReq {
@@ -188,6 +197,7 @@ export namespace Alert {
port?: number;
encryption?: string;
recipient?: string;
recipients?: string[];
};
}
@@ -210,6 +220,12 @@ export namespace Alert {
config: {
displayName?: string;
url?: string;
webhooks?: WebhookItem[];
};
}
export interface WebhookItem {
displayName: string;
url: string;
}
}
+9
View File
@@ -62,6 +62,15 @@ export const ListAlertConfigs = (currentNode?: string) => {
);
};
export const PageAlertConfigs = (req: Alert.AlertConfigPageReq, currentNode?: string) => {
return http.post<ResPage<Alert.AlertConfigInfo>>(
`/alert/config/search`,
req,
undefined,
currentNode ? { CurrentNode: currentNode } : undefined,
);
};
export const DeleteAlertConfig = (req: Alert.DelReq) => {
return http.post<any>(`/alert/config/del`, req);
};
+10
View File
@@ -123,9 +123,11 @@ const message = {
group: 'Group',
default: 'Default',
createdAt: 'Created',
creator: 'Creator',
publishedAt: 'Published',
date: 'Date',
updatedAt: 'Updated',
updater: 'Updater',
operate: 'Actions',
message: 'Message',
description: 'Description',
@@ -228,6 +230,7 @@ const message = {
requiredSelect: 'Select an item in the list',
illegalChar: 'Injection of characters & ; $ \' ` ( ) " > < | is currently not supported',
illegalInput: "This field mustn't contains illegal characters.",
duplicate: 'This value must be unique.',
commonName:
'This field must start with non-special characters and must consist of English, Chinese, numbers, ".", "-", and "_" characters with a length of 1-128.',
userName: 'This field must consist of English, Chinese, numbers and "_" characters with a length of 3-30.',
@@ -5453,6 +5456,13 @@ const message = {
webhookName: 'Bot name',
webhookUrl: 'Webhook URL',
alertConfigProHelper: 'Commercial Edition also supports WeCom, DingTalk, Feishu, and SMS alerts.',
recipientPlaceholder: 'Please enter recipient email address',
addRecipient: 'Add Recipient',
webhookItem: 'Webhook',
addWebhook: 'Add Webhook',
selectAlertType: 'Select Alert Type',
configDetail: 'Config Details',
webhookCount: ' Webhook(s)',
},
theme: {
lingXiaGold: 'LXware Gold',
+1
View File
@@ -229,6 +229,7 @@ const message = {
requiredSelect: 'Seleccione un elemento de la lista',
illegalChar: 'Actualmente no se admite la inyección de caracteres & ; $ \' ` ( ) " > < |',
illegalInput: 'Este campo no debe contener caracteres no permitidos.',
duplicate: 'Este valor debe ser único.',
commonName:
'Este campo debe comenzar con un carácter no especial y debe estar compuesto por letras, caracteres chinos, números, ".", "-", y "_" con una longitud de 1 a 128.',
userName:
+1
View File
@@ -228,6 +228,7 @@ const message = {
requiredSelect: 'リスト内のアイテムを選択します',
illegalChar: '現在文字 & ; $ \' ` ( ) " > < | の注入はサポートされていません',
illegalInput: 'このフィールドには違法なキャラクターが含まれてはなりません',
duplicate: 'この値は一意である必要があります',
commonName:
'このフィールドは特別なキャラクターではなく英語中国語数字で構成されている必要がありますおよび_文字が1128の文字で構成されている必要があります',
userName: '特殊文字で始まらない英字漢字数字および_をサポート長さ3-30',
+1
View File
@@ -226,6 +226,7 @@ const message = {
requiredSelect: '목록에서 항목을 선택하세요',
illegalChar: '현재 & ; $ \' ` ( ) " > < | 문자 주입은 지원되지 않습니다',
illegalInput: ' 필드에는 유효하지 않은 문자가 포함될 없습니다.',
duplicate: ' 값은 고유해야 합니다.',
commonName:
' 필드는 특수 문자로 시작할 없으며, 영어, 한자, 숫자, ".", "-", "_" 문자로 구성되어야 하며 길이는 1-128자여야 합니다.',
userName: '특수 문자로 시작하지 않고, 영어, 한국어, 숫자 _, 길이 3-30 지원',
+1
View File
@@ -229,6 +229,7 @@ const message = {
requiredSelect: 'Pilih satu item dalam senarai',
illegalChar: 'Suntikan aksara & ; $ \' ` ( ) " > < | tidak disokong buat masa ini',
illegalInput: 'Ruangan ini tidak boleh mengandungi aksara tidak sah.',
duplicate: 'Nilai ini mesti unik.',
commonName:
'Ruangan ini mesti bermula dengan aksara bukan khas dan mesti terdiri daripada aksara rumi, Cina, nombor, ".", "-", dan "_" dengan panjang 1-128 aksara.',
userName:
+1
View File
@@ -227,6 +227,7 @@ const message = {
requiredSelect: 'Selecione um item na lista',
illegalChar: 'Atualmente não suporte para injeção dos caracteres & ; $ \' ` ( ) " > < |',
illegalInput: 'Este campo não deve conter caracteres ilegais.',
duplicate: 'Este valor deve ser único.',
commonName:
'Este campo deve começar com caracteres não especiais e consistir em letras, números, ".", "-", e "_" com comprimento de 1-128.',
userName: 'Suporta não começar com caracteres especiais, inglês, chinês, números e _, comprimento 3-30',
+1
View File
@@ -227,6 +227,7 @@ const message = {
requiredSelect: 'Выберите элемент из списка',
illegalChar: 'В настоящее время не поддерживается вставка символов & ; $ \' ` ( ) " > < |',
illegalInput: 'Это поле не должно содержать недопустимых символов.',
duplicate: 'Значение должно быть уникальным.',
commonName:
'Это поле должно начинаться с неспециальных символов и должно состоять из английских букв, китайских иероглифов, цифр, ".", "-" и "_" длиной 1-128.',
userName: 'Поддерживает начало без специальных символов, английский, китайский, цифры и _, длина 3-30',
+1
View File
@@ -231,6 +231,7 @@ const message = {
requiredSelect: 'Listeden bir öğe seçin',
illegalChar: '& ; $ ` ( ) " > < | karakterlerinin enjekte edilmesi şu anda desteklenmiyor',
illegalInput: 'Bu alan yasadışı karakterler içermemelidir.',
duplicate: 'Bu değer benzersiz olmalıdır.',
commonName:
'Bu alan özel olmayan karakterlerle başlamalı ve İngilizce, Çince, rakam, ".", "-", ve "_" karakterlerinden oluşmalı, uzunluk 1-128 olmalıdır.',
userName: 'Bu alan İngilizce, Çince, rakam ve "_" karakterlerinden oluşmalı, uzunluk 3-30 olmalıdır.',
+11
View File
@@ -115,8 +115,10 @@ const message = {
group: '分组',
default: '默认',
createdAt: '创建时间',
creator: '创建人',
date: '时间',
updatedAt: '更新时间',
updater: '更新人',
operate: '操作',
message: '信息',
description: '描述',
@@ -212,6 +214,7 @@ const message = {
requiredSelect: '请选择必选项',
illegalChar: '暂不支持注入字符 & ; $ \' ` ( ) " > < |',
illegalInput: '输入框中存在不合法字符',
duplicate: '内容不能重复',
commonName: '支持非特殊字符开头,英文中文数字.-和_,长度1-128',
userName: '支持非特殊字符开头英文中文数字和_,长度3-30',
simpleName: '支持非下划线开头英文数字_,长度3-30',
@@ -4887,6 +4890,7 @@ const message = {
dingTalk: '钉钉通知',
feiShu: '飞书通知',
mail: '邮箱通知',
email: '邮箱通知',
weCom: '企业微信',
bark: 'Bark',
sendCountRulesHelper: '到期前发送告警的总数每日仅发送一次',
@@ -5059,6 +5063,13 @@ const message = {
webhookName: '机器人名称',
webhookUrl: 'Webhook 地址',
alertConfigProHelper: '商业版额外支持企业微信钉钉飞书及短信告警',
recipientPlaceholder: '请输入收件人邮箱地址',
addRecipient: '添加收件人',
webhookItem: 'Webhook',
addWebhook: '添加 Webhook',
selectAlertType: '选择告警类型',
configDetail: '配置详情',
webhookCount: ' Webhook',
},
theme: {
lingXiaGold: '凌霞金',
@@ -712,45 +712,44 @@
>
<el-select
class="selectClass"
popper-class="alert-config-method-dropdown"
v-model="form.alertMethodItems"
multiple
cleanable
collapse-tags
collapse-tags-tooltip
:max-collapse-tags="3"
>
<el-option value="mail" :label="$t('xpack.alert.mail')" />
<el-option
v-if="!isProductPro"
value="bark"
:label="$t('xpack.alert.bark')"
/>
<el-option
value="weCom"
v-if="!isIntl"
:disabled="!form.hasAlert || !isProductPro"
:label="$t('xpack.alert.weCom')"
/>
<el-option
value="dingTalk"
v-if="!isIntl"
:disabled="!form.hasAlert || !isProductPro"
:label="$t('xpack.alert.dingTalk')"
/>
<el-option
value="feiShu"
v-if="!isIntl"
:disabled="!form.hasAlert || !isProductPro"
:label="$t('xpack.alert.feiShu')"
/>
<el-option
v-if="isProductPro"
value="bark"
:label="$t('xpack.alert.bark')"
/>
<el-option
value="sms"
v-if="!isIntl || !isEE"
:disabled="!form.hasAlert || !isProductPro"
:label="$t('xpack.alert.sms')"
/>
<el-option-group
v-for="group in groupedAlertConfigOptions"
:key="group.type"
:label="
i18n.global.t(
'xpack.alert.' + (group.type === 'email' ? 'mail' : group.type),
)
"
>
<el-option
v-for="opt in group.options"
:key="opt.value"
:value="opt.value"
:label="opt.label"
>
<div class="alert-config-option">
<span class="alert-config-option__name" :title="opt.label">
{{ opt.label }}
</span>
<el-tag
class="alert-config-option__tag"
effect="light"
size="small"
round
>
{{ opt.typeLabel }}
</el-tag>
</div>
</el-option>
</el-option-group>
</el-select>
</el-form-item>
</LayoutCol>
@@ -837,7 +836,7 @@ import CodemirrorPro from '@/components/codemirror-pro/index.vue';
import InputTag from '@/components/input-tag/index.vue';
import LayoutCol from '@/components/layout-col/form.vue';
import CleanLogConfig from '@/views/cronjob/cronjob/config/clean-log.vue';
import { reactive, ref } from 'vue';
import { reactive, ref, computed, onMounted } from 'vue';
import { Rules } from '@/global/form-rules';
import { listBackupOptions } from '@/api/modules/backup';
import i18n from '@/lang';
@@ -851,6 +850,8 @@ import { useRouter } from 'vue-router';
import { listContainer } from '@/api/modules/container';
import { Database } from '@/api/interface/database';
import { listAppInstalled } from '@/api/modules/app';
import { Alert } from '@/api/interface/alert';
import { ListAlertConfigs } from '@/api/modules/alert';
import {
loadDefaultSpec,
loadDefaultSpecCustom,
@@ -883,6 +884,88 @@ const baseDir = ref();
const isCreate = ref();
const defaultGroupID = ref();
const alertConfigs = ref<Alert.AlertConfigInfo[]>([]);
const loadAlertConfigs = async () => {
try {
const res = await ListAlertConfigs();
alertConfigs.value = res.data || [];
} catch {}
};
onMounted(() => {
loadAlertConfigs();
});
const alertConfigOptions = computed(() => {
const hiddenTypes: string[] = [];
if (isIntl.value || isEE.value) hiddenTypes.push('sms');
return alertConfigs.value
.filter((c) => c.status === 'Enable' && c.type !== 'common' && !hiddenTypes.includes(c.type))
.map((c) => ({
value: String(c.id),
label: getAlertConfigOptionLabel(c),
type: c.type,
}));
});
const legacyAlertMethodTypeMap: Record<string, string> = {
mail: 'email',
email: 'email',
sms: 'sms',
bark: 'bark',
weCom: 'weCom',
dingTalk: 'dingTalk',
feiShu: 'feiShu',
};
const normalizeAlertMethodItems = (methods: string[]) => {
return methods.map((method) => {
if (/^\d+$/.test(method)) return method;
const configType = legacyAlertMethodTypeMap[method];
const matched = alertConfigOptions.value.find((item) => item.type === configType);
return matched?.value || method;
});
};
const groupedAlertConfigOptions = computed(() => {
const typeMap = new Map<string, { value: string; label: string }[]>();
for (const opt of alertConfigOptions.value) {
if (!typeMap.has(opt.type)) typeMap.set(opt.type, []);
typeMap.get(opt.type)!.push({ value: opt.value, label: opt.label });
}
const groups: {
type: string;
options: { value: string; label: string; typeLabel: string }[];
}[] = [];
const typeOrder = ['email', 'sms', 'weCom', 'dingTalk', 'feiShu', 'bark'];
for (const t of typeOrder) {
if (typeMap.has(t)) {
const typeLabel = getConfigTypeLabel(t);
groups.push({
type: t,
options: typeMap.get(t)!.map((item) => ({
...item,
typeLabel,
})),
});
}
}
return groups;
});
const getConfigTypeLabel = (type: string): string => {
return i18n.global.t(`xpack.alert.${type === 'email' ? 'mail' : type}`);
};
const getAlertConfigOptionLabel = (c: Alert.AlertConfigInfo): string => {
try {
const cfg = JSON.parse(c.config || '{}');
return cfg.displayName || i18n.global.t(`xpack.alert.${c.type === 'email' ? 'mail' : c.type}`);
} catch {
return i18n.global.t(`xpack.alert.${c.type === 'email' ? 'mail' : c.type}`);
}
};
const form = reactive<Cronjob.CronjobInfo>({
id: 0,
name: '',
@@ -1040,7 +1123,7 @@ const search = async () => {
form.alertCount = res.data.alertCount || 3;
form.alertTitle = res.data.alertTitle;
if (res.data.alertMethod) {
form.alertMethodItems = res.data.alertMethod.split(',') || [];
form.alertMethodItems = normalizeAlertMethodItems(res.data.alertMethod.split(',') || []);
} else {
form.alertMethodItems = [];
}
@@ -1644,6 +1727,36 @@ onMounted(() => {
font-size: 12px;
margin-top: 5px;
}
.alert-config-option {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
min-width: 0;
}
.alert-config-option__name {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.alert-config-option__tag {
flex: 0 0 auto;
}
:global(.alert-config-method-dropdown .el-select-dropdown__item) {
padding-right: 52px;
}
:global(.alert-config-method-dropdown .el-select-dropdown__item.is-selected::after) {
right: 16px;
}
.logText {
line-height: 22px;
font-size: 12px;
@@ -25,12 +25,12 @@
/>
<el-option value="panelLogin" :label="$t('xpack.alert.panelLogin')" />
<el-option
v-if="isProductPro"
v-if="isProductPro && !isEE"
value="licenseException"
:label="$t('xpack.alert.licenseException')"
/>
<el-option
v-if="isProductPro && !isEE.value"
v-if="isProductPro && !isEE"
value="nodeException"
:label="$t('xpack.alert.nodeException')"
/>
@@ -131,6 +131,17 @@
{{ formatRule(row) }}
</template>
</el-table-column>
<el-table-column
v-if="isEE"
:label="$t('commons.table.creator')"
prop="createUser"
width="100px"
show-overflow-tooltip
>
<template #default="{ row }">
{{ row.createUser || '-' }}
</template>
</el-table-column>
<fu-table-operations
:ellipsis="2"
width="130px"
@@ -154,7 +165,7 @@ import i18n from '@/lang';
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 } from '@/api/modules/alert';
import { UpdateAlertStatus, SearchAlerts, DeleteAlert, PageAlertConfigs } from '@/api/modules/alert';
const { isMobile, isMaster, isProductPro, isEE } = useGlobalStore();
@@ -261,13 +272,46 @@ const formatRule = (row: Alert.AlertInfo) => {
return ruleTemplates[row.type] ? ruleTemplates[row.type]() : '';
};
const configMap = ref<Map<string, Alert.AlertConfigInfo>>(new Map());
const loadConfigMap = async () => {
try {
const res = await PageAlertConfigs({ page: 1, pageSize: 1000 });
const map = new Map<string, Alert.AlertConfigInfo>();
for (const c of res.data?.items || []) {
map.set(String(c.id), c);
}
configMap.value = map;
} catch {}
};
const formatMethod = (row: Alert.AlertInfo) => {
if (!row.method) return '';
const sendMethod = row.method.split(',').filter(Boolean);
const methodStr = sendMethod.map((item) => t('xpack.alert.' + item)).join('');
const resolveMethodLabel = (method: string) => {
const config = configMap.value.get(method);
if (config) {
const typeLabel = i18n.global.t(`xpack.alert.${config.type === 'email' ? 'mail' : config.type}`);
try {
const cfg = JSON.parse(config.config || '{}');
const name = cfg.displayName || cfg.sender || cfg.phone || '';
return name ? `${name}(${typeLabel})` : typeLabel;
} catch {
return typeLabel;
}
}
const oldLabel = i18n.global.t(`xpack.alert.${method}`);
if (!oldLabel || oldLabel === `xpack.alert.${method}` || oldLabel.includes('.')) {
return /^\d+$/.test(method) ? `#${method}` : method;
}
return oldLabel;
};
return `${methodStr}`;
return `${row.method
.split(',')
.filter(Boolean)
.map((item) => resolveMethodLabel(item.trim()))
.join('')}`;
};
const search = async () => {
@@ -285,6 +329,7 @@ const search = async () => {
order: paginationConfig.order,
};
try {
await loadConfigMap();
const res = await SearchAlerts(params);
data.value = res.data.items || [];
paginationConfig.total = res.data.total || 0;
@@ -319,34 +319,40 @@
</el-form-item>
<el-form-item :label="$t('xpack.alert.alertMethod')" prop="sendMethod">
<el-select class="selectClass" v-model="dialogData.rowData!.sendMethod" multiple cleanable>
<el-option value="mail" :label="$t('xpack.alert.mail')" />
<el-option v-if="!isProductPro" value="bark" :label="$t('xpack.alert.bark')" />
<el-select
class="selectClass"
popper-class="alert-config-method-dropdown"
v-model="dialogData.rowData!.sendMethod"
multiple
cleanable
collapse-tags
collapse-tags-tooltip
:max-collapse-tags="3"
@change="handleSendMethodChange"
>
<el-option :value="ALL_SEND_METHOD" :label="$t('commons.table.all')">
<div class="alert-config-option alert-config-option--all">
<span class="alert-config-option__name">
{{ $t('commons.table.all') }}
</span>
</div>
</el-option>
<el-option
value="weCom"
v-if="!isIntl"
:disabled="!isProductPro"
:label="$t('xpack.alert.weCom')"
/>
<el-option
value="dingTalk"
v-if="!isIntl"
:disabled="!isProductPro"
:label="$t('xpack.alert.dingTalk')"
/>
<el-option
value="feiShu"
v-if="!isIntl"
:disabled="!isProductPro"
:label="$t('xpack.alert.feiShu')"
/>
<el-option v-if="isProductPro" value="bark" :label="$t('xpack.alert.bark')" />
<el-option
value="sms"
v-if="!isIntl || !isEE"
:disabled="!isProductPro"
:label="$t('xpack.alert.sms')"
/>
v-for="opt in configOptions"
:key="opt.value"
:value="opt.value"
:label="opt.label"
:disabled="opt.disabled"
>
<div class="alert-config-option">
<span class="alert-config-option__name" :title="opt.label">
{{ opt.label }}
</span>
<el-tag class="alert-config-option__tag" effect="light" size="small" round>
{{ opt.typeLabel }}
</el-tag>
</div>
</el-option>
</el-select>
</el-form-item>
<span class="input-help">
@@ -356,6 +362,10 @@
: ''
}}
</span>
<el-form-item v-if="dialogData.title === 'edit' && isEE" :label="$t('commons.table.updater')">
<el-input :model-value="dialogData.rowData?.updateUser || '-'" readonly />
</el-form-item>
</el-col>
</el-row>
</el-form>
@@ -376,11 +386,11 @@
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { reactive, ref, onMounted, computed } from 'vue';
import { ElForm } from 'element-plus';
import { Alert } from '@/api/interface/alert';
import { listSSL, listWebsites } from '@/api/modules/website';
import { CreateAlert, ListDisks, UpdateAlert, ListClams, ListCronJob } from '@/api/modules/alert';
import { CreateAlert, ListDisks, UpdateAlert, ListClams, ListCronJob, PageAlertConfigs } from '@/api/modules/alert';
import { MsgSuccess } from '@/utils/message';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
@@ -390,6 +400,95 @@ import { useGlobalStore } from '@/composables/useGlobalStore';
const { isMaster, isProductPro, isIntl, isEE } = useGlobalStore();
const alertConfigs = ref<Alert.AlertConfigInfo[]>([]);
const loadAlertConfigs = async () => {
try {
const res = await PageAlertConfigs({ page: 1, pageSize: 1000 });
alertConfigs.value = res.data?.items || [];
} catch {}
};
onMounted(() => {
loadAlertConfigs();
});
const ALL_SEND_METHOD = '__all__';
const configOptions = computed(() => {
const hiddenTypes: string[] = [];
if (isIntl.value || isEE.value) {
hiddenTypes.push('sms');
}
return alertConfigs.value
.filter((c) => c.type !== 'common' && !hiddenTypes.includes(c.type))
.map((c) => ({
value: String(c.id),
label: getConfigOptionLabel(c),
typeLabel: getConfigTypeLabel(c.type),
type: c.type,
disabled: c.status !== 'Enable',
}));
});
const allConfigValues = computed(() => configOptions.value.filter((c) => !c.disabled).map((c) => c.value));
const legacyMethodTypeMap: Record<string, string> = {
mail: 'email',
email: 'email',
sms: 'sms',
bark: 'bark',
weCom: 'weCom',
dingTalk: 'dingTalk',
feiShu: 'feiShu',
};
const normalizeMethodValues = (methods: string[]) => {
return methods.map((method) => {
if (/^\d+$/.test(method)) return method;
const configType = legacyMethodTypeMap[method];
const matched = configOptions.value.find((item) => item.type === configType);
return matched?.value || method;
});
};
const getConfigTypeLabel = (type: string): string => {
return i18n.global.t(`xpack.alert.${type}`);
};
const getConfigOptionLabel = (c: Alert.AlertConfigInfo): string => {
try {
const cfg = JSON.parse(c.config || '{}');
const name = cfg.displayName || cfg.sender || cfg.phone || '';
if (c.type === 'email') {
return name ? `${name}` : cfg.sender || getConfigTypeLabel(c.type);
}
if (cfg.webhooks && cfg.webhooks.length > 0) {
return cfg.webhooks
.map((w: { displayName: string }) => w.displayName || '')
.filter(Boolean)
.join(', ');
}
if (cfg.displayName) {
return cfg.displayName;
}
if (c.type === 'sms') {
return cfg.phone || getConfigTypeLabel(c.type);
}
} catch {}
return getConfigTypeLabel(c.type);
};
const handleSendMethodChange = (values: string[]) => {
if (!dialogData.value.rowData) return;
if (
allConfigValues.value.length > 0 &&
(values.includes(ALL_SEND_METHOD) || allConfigValues.value.every((item) => values.includes(item)))
) {
dialogData.value.rowData.sendMethod = [ALL_SEND_METHOD];
return;
}
dialogData.value.rowData.sendMethod = values.filter((item) => item !== ALL_SEND_METHOD);
};
interface DialogProps {
title: string;
rowData?: Alert.AlertInfo;
@@ -432,7 +531,16 @@ const acceptParams = (params: DialogProps): void => {
dialogData.value = params;
dialogData.value.rowData.sendMethod = [];
if (dialogData.value.rowData.method != '') {
dialogData.value.rowData.sendMethod = dialogData.value.rowData.method.split(',');
const sendMethods = normalizeMethodValues(dialogData.value.rowData.method.split(',').filter(Boolean));
if (
sendMethods.length > 0 &&
allConfigValues.value.length > 0 &&
allConfigValues.value.every((item) => sendMethods.includes(item))
) {
dialogData.value.rowData.sendMethod = [ALL_SEND_METHOD];
} else {
dialogData.value.rowData.sendMethod = sendMethods;
}
}
if (cronjobTypes.includes(dialogData.value.rowData.type)) {
dialogData.value.rowData.subType = dialogData.value.rowData.type;
@@ -457,7 +565,11 @@ const allTaskOptions = [
{ value: 'panelPwdEndTime', label: 'xpack.alert.panelPwdEndTime', show: isMaster.value && !isEE.value },
{ value: 'sshLogin', label: 'xpack.alert.sshLogin', show: true },
{ value: 'panelLogin', label: 'xpack.alert.panelLogin', show: isMaster.value },
{ value: 'licenseException', label: 'xpack.alert.licenseException', show: isMaster.value && isProductPro.value },
{
value: 'licenseException',
label: 'xpack.alert.licenseException',
show: isMaster.value && isProductPro.value && !isEE.value,
},
{ value: 'ssl', label: 'xpack.alert.ssl', show: true },
{ value: 'siteEndTime', label: 'xpack.alert.siteEndTime', show: true },
{
@@ -789,7 +901,10 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
await formEl.validate(async (valid) => {
if (!valid) return;
if (!dialogData.value.rowData) return;
dialogData.value.rowData.method = dialogData.value.rowData.sendMethod.join(',');
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;
@@ -832,4 +947,37 @@ defineExpose({
font-size: 12px;
margin-top: 5px;
}
.alert-config-option {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
min-width: 0;
}
.alert-config-option__name {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.alert-config-option__tag {
flex: 0 0 auto;
}
.alert-config-option--all {
justify-content: flex-start;
}
:global(.alert-config-method-dropdown .el-select-dropdown__item) {
padding-right: 52px;
}
:global(.alert-config-method-dropdown .el-select-dropdown__item.is-selected::after) {
right: 16px;
}
</style>
+67 -26
View File
@@ -21,12 +21,11 @@
</template>
</el-table-column>
<el-table-column :label="$t('xpack.alert.alertMethod')" prop="method" width="150px">
<el-table-column :label="$t('xpack.alert.alertMethod')" prop="method" width="200px">
<template #default="{ row }">
{{ formatMethod(row) }}
</template>
</el-table-column>
<el-table-column
:label="$t('commons.table.status')"
fix
@@ -90,6 +89,7 @@ import {
SyncAlertAll,
SyncOfflineAlert,
ListAlertConfigs,
PageAlertConfigs,
} from '@/api/modules/alert';
import { ElMessageBox } from 'element-plus';
import { useGlobalStore } from '@/composables/useGlobalStore';
@@ -99,6 +99,18 @@ const { t } = i18n.global;
const loading = ref(false);
const data = ref();
const isOffline = ref('Disable');
const configMap = ref<Map<string, Alert.AlertConfigInfo>>(new Map());
const loadConfigMap = async () => {
try {
const res = await PageAlertConfigs({ page: 1, pageSize: 1000 });
const map = new Map<string, Alert.AlertConfigInfo>();
for (const c of res.data?.items || []) {
map.set(String(c.id), c);
}
configMap.value = map;
} catch {}
};
const resourceTypes = [
'cpu',
'memory',
@@ -135,10 +147,14 @@ const buttons = [
syncAlert(row);
},
disabled: (row: Alert.AlertLog) => {
return (
(row.method != 'sms' && row.status != 'PushSuccess' && row.status != 'SyncError') ||
row.status == 'Success'
);
const isSms = row.method
.split(',')
.filter(Boolean)
.some((item) => {
const config = configMap.value.get(item.trim());
return config ? config.type === 'sms' : item.trim() === 'sms';
});
return (!isSms && row.status != 'PushSuccess' && row.status != 'SyncError') || row.status == 'Success';
},
},
];
@@ -213,26 +229,50 @@ const formatMessage = (row: Alert.AlertInfo) => {
};
const formatMethod = (row: Alert.AlertLog) => {
switch (row.method) {
case 'mail':
return t('xpack.alert.mail');
case 'dingTalk':
return t('xpack.alert.dingTalk');
case 'weCom':
return t('xpack.alert.weCom');
case 'feiShu':
return t('xpack.alert.feiShu');
case 'wechat':
return t('xpack.alert.wechat');
case 'sms':
return t('xpack.alert.sms');
case 'webhook':
return t('xpack.alert.webhook');
case 'bark':
return t('xpack.alert.bark');
default:
return t('xpack.alert.unknown');
}
if (!row.method) return '-';
const formatMethodPart = (method: string) => {
const config = configMap.value.get(method);
if (config) {
const typeKey = config.type === 'email' ? 'mail' : config.type;
const typeLabel = i18n.global.t('xpack.alert.' + typeKey);
try {
const cfg = JSON.parse(config.config || '{}');
const name = cfg.displayName || cfg.sender || cfg.phone || '';
return name ? `${name}(${typeLabel})` : typeLabel;
} catch {
return typeLabel;
}
}
switch (method) {
case 'mail':
case 'email':
return t('xpack.alert.mail');
case 'dingTalk':
return t('xpack.alert.dingTalk');
case 'weCom':
return t('xpack.alert.weCom');
case 'feiShu':
return t('xpack.alert.feiShu');
case 'wechat':
return t('xpack.alert.wechat');
case 'sms':
return t('xpack.alert.sms');
case 'webhook':
return t('xpack.alert.webhook');
case 'bark':
return t('xpack.alert.bark');
default:
return /^\d+$/.test(method) ? `#${method}` : method;
}
};
return `${row.method
.split(',')
.filter(Boolean)
.map((item) => formatMethodPart(item.trim()))
.join('')}`;
};
const formatCount = (row: Alert.AlertInfo) => {
@@ -253,6 +293,7 @@ const search = async () => {
status: req.status,
};
try {
await loadConfigMap();
const res = await SearchAlertLogs(params);
data.value = res.data.items || [];
paginationConfig.total = res.data.total || 0;
@@ -0,0 +1,444 @@
<template>
<DrawerPro v-model="drawerVisible" :header="drawerHeader" @close="handleClose" size="736">
<el-form
ref="formRef"
:rules="currentRules"
label-position="top"
:model="form"
@submit.prevent
v-loading="loading"
>
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item v-if="!isEdit" :label="$t('commons.table.type')" prop="type">
<el-select v-model="form.type" class="w-full" @change="onTypeChange">
<el-option
v-for="item in typeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<template v-if="form.type === 'email'">
<el-form-item :label="$t('xpack.alert.displayName')" prop="config.displayName">
<el-input v-model="form.config.displayName" />
<span class="input-help">{{ $t('xpack.alert.displayNameHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('xpack.alert.sender')" prop="config.sender">
<el-input v-model.trim="form.config.sender" />
<span class="input-help">{{ $t('xpack.alert.senderHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('commons.login.username')" prop="config.userName">
<el-input v-model.trim="form.config.userName" />
<span class="input-help">{{ $t('xpack.alert.userNameHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('xpack.alert.password')" prop="config.password">
<el-input v-model.trim="form.config.password" type="password" show-password />
<span class="input-help">{{ $t('xpack.alert.passwordHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('xpack.alert.host')" prop="config.host">
<el-input v-model.trim="form.config.host" placeholder="smtp.qq.com" />
<span class="input-help">{{ $t('xpack.alert.hostHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('xpack.alert.port')" prop="config.port">
<el-input v-model.number="form.config.port" :min="1" :max="65535" />
</el-form-item>
<el-form-item :label="$t('xpack.alert.encryption')" prop="config.encryption">
<div class="flex items-center gap-2">
<span class="el-form-item__label">SSL</span>
<el-switch
v-permission
v-model="form.config.encryption"
:active-value="'SSL'"
:inactive-value="form.config.encryption === 'SSL' ? 'NONE' : form.config.encryption"
/>
</div>
<span class="input-help">{{ $t('xpack.alert.sslHelper') }}</span>
<div class="flex items-center gap-2">
<span class="el-form-item__label">TLS</span>
<el-switch
v-permission
v-model="form.config.encryption"
:active-value="'TLS'"
:inactive-value="form.config.encryption === 'TLS' ? 'NONE' : form.config.encryption"
/>
</div>
<span class="input-help">{{ $t('xpack.alert.tlsHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('xpack.alert.recipient')" prop="recipient">
<el-input v-model.trim="form.recipient" placeholder="a@example.com,b@example.com" />
</el-form-item>
</template>
<template v-else-if="form.type === 'sms'">
<el-form-item :label="$t('xpack.alert.phone')" prop="smsPhone" :rules="[Rules.phone]">
<el-input clearable v-model="form.smsPhone" />
<span class="input-help">{{ $t('xpack.alert.phoneHelper') }}</span>
</el-form-item>
<el-form-item
:label="$t('xpack.alert.dailyAlertNum')"
prop="smsDailyAlertNum"
:rules="[Rules.integerNumber, checkNumberRange(20, 100)]"
>
<el-input clearable v-model.number="form.smsDailyAlertNum" min="20" max="100" />
<span class="input-help">{{ $t('xpack.alert.dailyAlertNumHelper') }}</span>
</el-form-item>
</template>
<template v-else>
<el-form-item :label="$t('xpack.alert.webhookName')" prop="webhookName">
<el-input v-model="form.webhookName" />
</el-form-item>
<el-form-item :label="$t('xpack.alert.webhookUrl')" prop="webhookUrl">
<el-input v-model.trim="form.webhookUrl" :rows="2" type="password" show-password />
</el-form-item>
</template>
<el-form-item v-if="isEdit && isEE" :label="$t('commons.table.updater')">
<el-input :model-value="form.updateUser || '-'" readonly />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div v-if="form.type === 'email'" class="flex items-center justify-between">
<el-button v-permission @click="onTest(formRef)" plain type="primary">
{{ $t('xpack.alert.test') }}
</el-button>
<div>
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button
v-permission
:disabled="loading || (form.type === 'email' && !isOK)"
type="primary"
@click="onSave(formRef)"
>
{{ $t('commons.button.confirm') }}
</el-button>
</div>
</div>
<div v-else class="flex justify-end gap-2">
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button v-permission :disabled="loading" type="primary" @click="onSave(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</div>
</template>
</DrawerPro>
</template>
<script lang="ts" setup>
import { computed, onMounted, reactive, ref, watch } from 'vue';
import i18n from '@/lang';
import { MsgError, MsgSuccess } from '@/utils/message';
import { FormInstance } from 'element-plus';
import { PageAlertConfigs, TestAlertConfig, UpdateAlertConfig } from '@/api/modules/alert';
import { Rules, checkNumberRange } from '@/global/form-rules';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { Alert } from '@/api/interface/alert';
const emit = defineEmits<{ (e: 'search'): void }>();
const { isProductPro, isIntl, isEE } = useGlobalStore();
const emailRules = {
'config.displayName': [Rules.requiredInput, { validator: checkDisplayNameDuplicate, trigger: 'blur' }],
'config.sender': [Rules.requiredInput],
'config.host': [Rules.requiredInput],
'config.port': [Rules.requiredInput],
recipient: [Rules.requiredInput],
};
const webhookRules = {
webhookName: [Rules.requiredInput, { validator: checkDisplayNameDuplicate, trigger: 'blur' }],
webhookUrl: [Rules.requiredInput],
};
const currentRules = computed(() => {
if (form.type === 'email') return emailRules;
if (form.type === 'sms') return {};
return webhookRules;
});
const typeOptions = computed(() => {
const options: { value: string; label: string }[] = [{ value: 'email', label: i18n.global.t('xpack.alert.mail') }];
if (isProductPro.value && !isIntl.value) {
options.push({ value: 'weCom', label: i18n.global.t('xpack.alert.weCom') });
options.push({ value: 'dingTalk', label: i18n.global.t('xpack.alert.dingTalk') });
options.push({ value: 'feiShu', label: i18n.global.t('xpack.alert.feiShu') });
}
options.push({ value: 'bark', label: i18n.global.t('xpack.alert.bark') });
if (!isEE.value && !isIntl.value) {
options.push({ value: 'sms', label: i18n.global.t('xpack.alert.sms') });
}
return options;
});
const defaultEmailForm = {
displayName: '',
sender: '',
userName: '',
password: '',
host: '',
port: 465,
encryption: 'NONE',
status: 'Enable',
recipient: '',
};
const drawerVisible = ref(false);
const loading = ref(false);
const isEdit = ref(false);
const isOK = ref(false);
const formRef = ref<FormInstance>();
const alertConfigs = ref<Alert.AlertConfigInfo[]>([]);
const loadAlertConfigs = async () => {
try {
const res = await PageAlertConfigs({ page: 1, pageSize: 1000 });
alertConfigs.value = res.data?.items?.filter((item: Alert.AlertConfigInfo) => item.type !== 'common') || [];
} catch {
alertConfigs.value = [];
}
};
const form = reactive({
id: undefined as number | undefined,
type: 'email',
title: '',
status: 'Enable',
updateUser: '',
config: { ...defaultEmailForm } as Record<string, any>,
recipient: '',
webhookName: '',
webhookUrl: '',
smsPhone: '',
smsDailyAlertNum: 50,
});
const drawerHeader = computed(() => {
if (isEdit.value) {
return i18n.global.t('xpack.alert.' + form.type);
}
return i18n.global.t('commons.button.create');
});
const normalizeDisplayName = (value?: string) => value?.trim() || '';
function checkDisplayNameDuplicate(_rule: unknown, value: string, callback: (error?: Error) => void) {
const currentValue = normalizeDisplayName(value);
if (!currentValue) {
callback();
return;
}
const duplicated = alertConfigs.value.some((item) => {
if (item.type !== form.type) {
return false;
}
if (form.id && item.id === form.id) {
return false;
}
try {
const config = JSON.parse(item.config || '{}') as { displayName?: string };
return normalizeDisplayName(config.displayName) === currentValue;
} catch {
return false;
}
});
if (duplicated) {
callback(new Error(i18n.global.t('commons.rule.duplicate')));
return;
}
callback();
}
const titleMap: Record<string, string> = {
email: 'xpack.alert.emailConfig',
weCom: 'xpack.alert.weCom',
dingTalk: 'xpack.alert.dingTalk',
feiShu: 'xpack.alert.feiShu',
bark: 'xpack.alert.bark',
sms: 'xpack.alert.smsConfig',
};
interface DrawerProps {
id?: number;
type?: string;
config?: Record<string, any>;
status?: string;
updateUser?: string;
}
const acceptParams = (params: DrawerProps): void => {
if (params.id && params.id > 0) {
isEdit.value = true;
form.id = params.id;
form.type = params.type || 'email';
form.title = titleMap[form.type] || '';
form.status = params.status || 'Enable';
form.updateUser = params.updateUser || '';
if (form.type === 'email') {
form.config = { ...defaultEmailForm, ...(params.config || {}) };
form.recipient = params.config?.recipient || '';
} else if (form.type === 'sms') {
form.smsPhone = params.config?.phone || '';
form.smsDailyAlertNum = params.config?.alertDailyNum || 50;
} else {
form.webhookName = params.config?.displayName || '';
form.webhookUrl = params.config?.url || '';
}
} else {
isEdit.value = false;
form.id = undefined;
form.type = params.type || 'email';
form.title = titleMap[form.type] || '';
form.status = 'Enable';
form.updateUser = '';
form.config = { ...defaultEmailForm };
form.recipient = '';
form.webhookName = '';
form.webhookUrl = '';
form.smsPhone = '';
form.smsDailyAlertNum = 50;
}
isOK.value = false;
drawerVisible.value = true;
void loadAlertConfigs();
};
const onTypeChange = (type: string) => {
form.title = titleMap[type] || '';
if (type === 'email') {
form.config = { ...defaultEmailForm };
form.recipient = '';
} else if (type === 'sms') {
form.smsPhone = '';
form.smsDailyAlertNum = 50;
} else {
form.config = {};
form.webhookName = '';
form.webhookUrl = '';
}
isOK.value = false;
formRef.value?.clearValidate();
};
const buildSavePayload = () => {
if (form.type === 'email') {
const configInfo = {
...form.config,
recipient: form.recipient,
};
return {
id: form.id,
type: 'email',
title: titleMap['email'],
status: form.status,
config: JSON.stringify(configInfo),
};
}
if (form.type === 'sms') {
const configInfo = { phone: form.smsPhone, alertDailyNum: form.smsDailyAlertNum };
return {
id: form.id,
type: 'sms',
title: titleMap['sms'],
status: form.status,
config: JSON.stringify(configInfo),
};
}
const configInfo = {
displayName: form.webhookName,
url: form.webhookUrl,
};
return {
id: form.id,
type: form.type,
title: form.title,
status: form.status,
config: JSON.stringify(configInfo),
};
};
const onSave = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate(async (valid) => {
if (!valid) return;
loading.value = true;
try {
await UpdateAlertConfig(buildSavePayload());
loading.value = false;
void loadAlertConfigs();
handleClose();
emit('search');
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
} catch (error) {
loading.value = false;
}
});
};
const onTest = async (formEl: FormInstance | undefined) => {
if (!formEl || form.type !== 'email') return;
await formEl.validate(async (valid) => {
if (!valid) return;
loading.value = true;
try {
const testConfig = {
...form.config,
recipient: form.recipient,
};
const res = await TestAlertConfig(testConfig);
loading.value = false;
if (res.data) {
isOK.value = true;
MsgSuccess(i18n.global.t('xpack.alert.alertTestOk'));
} else {
MsgError(i18n.global.t('xpack.alert.alertTestFailed'));
}
} catch {
loading.value = false;
MsgError(i18n.global.t('xpack.alert.alertTestFailed'));
}
});
};
watch(
() => form.config,
() => {
if (form.type === 'email') {
isOK.value = false;
}
},
{ deep: true },
);
watch(
() => form.recipient,
() => {
if (form.type === 'email') {
isOK.value = false;
}
},
);
const handleClose = () => {
isOK.value = false;
drawerVisible.value = false;
};
onMounted(() => {
void loadAlertConfigs();
});
defineExpose({
acceptParams,
});
</script>
@@ -69,8 +69,33 @@
{{ $t('xpack.alert.tlsHelper') }}
</span>
</el-form-item>
<el-form-item :label="$t('xpack.alert.recipient')" prop="recipient">
<el-input v-model="form.config.recipient" />
<el-form-item :label="$t('xpack.alert.recipient')" prop="recipients">
<div class="w-full">
<div
v-for="(item, index) in form.config.recipients"
:key="index"
class="flex items-center mb-2 gap-2"
>
<el-input
v-model="form.config.recipients[index]"
:placeholder="$t('xpack.alert.recipientPlaceholder')"
/>
<el-button
v-permission
type="danger"
plain
circle
@click="removeRecipient(index)"
:disabled="form.config.recipients.length <= 1"
>
<el-icon><Minus /></el-icon>
</el-button>
</div>
<el-button v-permission type="primary" plain @click="addRecipient">
<el-icon><Plus /></el-icon>
{{ $t('xpack.alert.addRecipient') }}
</el-button>
</div>
</el-form-item>
</el-col>
</el-row>
@@ -97,6 +122,7 @@ import { MsgError, MsgSuccess } from '@/utils/message';
import { FormInstance } from 'element-plus';
import { TestAlertConfig, UpdateAlertConfig } from '@/api/modules/alert';
import { Rules } from '@/global/form-rules';
import { Plus, Minus } from '@element-plus/icons-vue';
const emit = defineEmits<{ (e: 'search'): void }>();
@@ -105,7 +131,23 @@ const rules = {
sender: [Rules.requiredInput],
host: [Rules.requiredInput],
port: [Rules.requiredInput],
recipient: [Rules.requiredInput],
recipients: [
{
validator: (rule: any, value: string[], callback: any) => {
if (!value || value.length === 0) {
callback(new Error(i18n.global.t('commons.rule.requiredInput')));
} else {
const hasEmpty = value.some((item) => !item || !item.trim());
if (hasEmpty) {
callback(new Error(i18n.global.t('commons.rule.requiredInput')));
} else {
callback();
}
}
},
trigger: 'blur',
},
],
};
interface Config {
status: string;
@@ -116,7 +158,8 @@ interface Config {
host: string;
port: number;
encryption: string;
recipient: string;
recipient?: string;
recipients?: string[];
}
interface DialogProps {
id: number;
@@ -126,7 +169,7 @@ const drawerVisible = ref();
const loading = ref();
const form = reactive({
id: undefined,
id: undefined as number | undefined,
config: {
displayName: '',
sender: '',
@@ -136,7 +179,7 @@ const form = reactive({
port: 465,
encryption: 'NONE',
status: 'Enable',
recipient: '',
recipients: [''] as string[],
},
});
const isOK = ref(false);
@@ -144,10 +187,39 @@ const formRef = ref<FormInstance>();
const acceptParams = (params: DialogProps): void => {
form.id = params.id;
form.config = params.config;
form.config.displayName = params.config.displayName || '';
form.config.sender = params.config.sender || '';
form.config.password = params.config.password || '';
form.config.userName = params.config.userName || '';
form.config.host = params.config.host || '';
form.config.port = params.config.port || 465;
form.config.encryption = params.config.encryption || 'NONE';
form.config.status = params.config.status || 'Enable';
if (params.config.recipients && params.config.recipients.length > 0) {
form.config.recipients = [...params.config.recipients];
} else if (params.config.recipient) {
form.config.recipients = params.config.recipient
.split(',')
.map((r) => r.trim())
.filter((r) => r);
} else {
form.config.recipients = [''];
}
drawerVisible.value = true;
};
const addRecipient = () => {
form.config.recipients.push('');
};
const removeRecipient = (index: number) => {
if (form.config.recipients.length > 1) {
form.config.recipients.splice(index, 1);
}
};
const onSave = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
@@ -155,7 +227,10 @@ const onSave = async (formEl: FormInstance | undefined) => {
loading.value = true;
try {
form.config.status = 'Enable';
const configInfo = form.config;
const configInfo = {
...form.config,
recipient: form.config.recipients.filter((r) => r && r.trim()).join(','),
};
await UpdateAlertConfig({
id: form.id,
type: 'email',
@@ -180,7 +255,11 @@ const onTest = async (formEl: FormInstance | undefined) => {
if (!valid) return;
loading.value = true;
try {
await TestAlertConfig(form.config)
const testConfig = {
...form.config,
recipient: form.config.recipients.filter((r) => r && r.trim()).join(','),
};
await TestAlertConfig(testConfig)
.then((res) => {
loading.value = false;
if (res.data) {
File diff suppressed because it is too large Load Diff
@@ -41,6 +41,7 @@ interface DialogProps {
phone: string;
dailyAlertNum: number;
id: number;
status?: string;
}
const drawerVisible = ref();
const loading = ref();
@@ -49,6 +50,7 @@ const form = reactive({
phone: '',
dailyAlertNum: 50,
id: undefined,
status: 'Enable',
});
const formRef = ref<FormInstance>();
@@ -57,6 +59,7 @@ const acceptParams = (params: DialogProps): void => {
form.phone = params.phone;
form.id = params.id;
form.dailyAlertNum = Number(params.dailyAlertNum);
form.status = params.status || 'Enable';
drawerVisible.value = true;
};
@@ -71,7 +74,7 @@ const onSave = async (formEl: FormInstance | undefined) => {
id: form.id,
type: 'sms',
title: 'xpack.alert.smsConfig',
status: 'Enable',
status: form.status,
config: JSON.stringify(configInfo),
});
+144 -29
View File
@@ -153,37 +153,37 @@
>
<el-select
class="selectClass"
popper-class="alert-config-method-dropdown"
v-model="dialogData.rowData!.alertMethodItems"
multiple
cleanable
collapse-tags
collapse-tags-tooltip
:max-collapse-tags="3"
>
<el-option value="mail" :label="$t('xpack.alert.mail')" />
<el-option v-if="!isProductPro" value="bark" :label="$t('xpack.alert.bark')" />
<el-option
value="weCom"
v-if="!isIntl"
:disabled="!dialogData.rowData!.hasAlert || !isProductPro"
:label="$t('xpack.alert.weCom')"
/>
<el-option
value="dingTalk"
v-if="!isIntl"
:disabled="!dialogData.rowData!.hasAlert || !isProductPro"
:label="$t('xpack.alert.dingTalk')"
/>
<el-option
value="feiShu"
v-if="!isIntl"
:disabled="!dialogData.rowData!.hasAlert || !isProductPro"
:label="$t('xpack.alert.feiShu')"
/>
<el-option v-if="isProductPro" value="bark" :label="$t('xpack.alert.bark')" />
<el-option
value="sms"
v-if="!isIntl || !isEE"
:disabled="!dialogData.rowData!.hasAlert || !isProductPro"
:label="$t('xpack.alert.sms')"
/>
<el-option-group
v-for="group in groupedAlertConfigOptions"
:key="group.type"
:label="
i18n.global.t('xpack.alert.' + (group.type === 'email' ? 'mail' : group.type))
"
>
<el-option
v-for="opt in group.options"
:key="opt.value"
:value="opt.value"
:label="opt.label"
>
<div class="alert-config-option">
<span class="alert-config-option__name" :title="opt.label">
{{ opt.label }}
</span>
<el-tag class="alert-config-option__tag" effect="light" size="small" round>
{{ opt.typeLabel }}
</el-tag>
</div>
</el-option>
</el-option-group>
</el-select>
</el-form-item>
<el-form-item
@@ -233,7 +233,7 @@
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { reactive, ref, computed, onMounted } from 'vue';
import { Rules } from '@/global/form-rules';
import FileList from '@/components/file-list/index.vue';
import i18n from '@/lang';
@@ -243,9 +243,93 @@ import { MsgError, MsgSuccess } from '@/utils/message';
import { Toolbox } from '@/api/interface/toolbox';
import { createClam, updateClam } from '@/api/modules/toolbox';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { Alert } from '@/api/interface/alert';
import { ListAlertConfigs } from '@/api/modules/alert';
import { specOptions, transObjToSpec, transSpecToObj, weekOptions } from '@/views/cronjob/cronjob/helper';
import { splitTimeFromSecond, transferTimeToSecond } from '@/utils/validate';
const { isIntl, isProductPro, isEE } = useGlobalStore();
const alertConfigs = ref<Alert.AlertConfigInfo[]>([]);
const loadAlertConfigs = async () => {
try {
const res = await ListAlertConfigs();
alertConfigs.value = res.data || [];
} catch {}
};
onMounted(() => {
loadAlertConfigs();
});
const alertConfigOptions = computed(() => {
const hiddenTypes: string[] = [];
if (isIntl.value || isEE.value) hiddenTypes.push('sms');
return alertConfigs.value
.filter((c) => c.status === 'Enable' && c.type !== 'common' && !hiddenTypes.includes(c.type))
.map((c) => ({
value: String(c.id),
label: getAlertConfigOptionLabel(c),
type: c.type,
}));
});
const legacyAlertMethodTypeMap: Record<string, string> = {
mail: 'email',
email: 'email',
sms: 'sms',
bark: 'bark',
weCom: 'weCom',
dingTalk: 'dingTalk',
feiShu: 'feiShu',
};
const normalizeAlertMethodItems = (methods: string[]) => {
return methods.map((method) => {
if (/^\d+$/.test(method)) return method;
const configType = legacyAlertMethodTypeMap[method];
const matched = alertConfigOptions.value.find((item) => item.type === configType);
return matched?.value || method;
});
};
const groupedAlertConfigOptions = computed(() => {
const typeMap = new Map<string, { value: string; label: string }[]>();
for (const opt of alertConfigOptions.value) {
if (!typeMap.has(opt.type)) typeMap.set(opt.type, []);
typeMap.get(opt.type)!.push({ value: opt.value, label: opt.label });
}
const groups: {
type: string;
options: { value: string; label: string; typeLabel: string }[];
}[] = [];
const typeOrder = ['email', 'sms', 'weCom', 'dingTalk', 'feiShu', 'bark'];
for (const t of typeOrder) {
if (typeMap.has(t)) {
const typeLabel = getConfigTypeLabel(t);
groups.push({
type: t,
options: typeMap.get(t)!.map((item) => ({
...item,
typeLabel,
})),
});
}
}
return groups;
});
const getConfigTypeLabel = (type: string): string => {
return i18n.global.t(`xpack.alert.${type === 'email' ? 'mail' : type}`);
};
const getAlertConfigOptionLabel = (c: Alert.AlertConfigInfo): string => {
try {
const cfg = JSON.parse(c.config || '{}');
return cfg.displayName || i18n.global.t(`xpack.alert.${c.type === 'email' ? 'mail' : c.type}`);
} catch {
return i18n.global.t(`xpack.alert.${c.type === 'email' ? 'mail' : c.type}`);
}
};
const licenseRef = ref();
const scanDirRef = ref();
const infectedDirRef = ref();
@@ -284,7 +368,9 @@ const acceptParams = (params: DialogProps): void => {
dialogData.value.rowData.hasAlert = dialogData.value.rowData!.alertCount > 0;
dialogData.value.rowData!.alertCount = dialogData.value.rowData!.alertCount || 3;
if (dialogData.value.rowData!.alertMethod) {
dialogData.value.rowData!.alertMethodItems = dialogData.value.rowData!.alertMethod.split(',') || [];
dialogData.value.rowData!.alertMethodItems = normalizeAlertMethodItems(
dialogData.value.rowData!.alertMethod.split(',') || [],
);
} else {
dialogData.value.rowData!.alertMethodItems = [];
}
@@ -524,6 +610,35 @@ defineExpose({
});
</script>
<style scoped lang="scss">
.alert-config-option {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
min-width: 0;
}
.alert-config-option__name {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.alert-config-option__tag {
flex: 0 0 auto;
}
:global(.alert-config-method-dropdown .el-select-dropdown__item) {
padding-right: 52px;
}
:global(.alert-config-method-dropdown .el-select-dropdown__item.is-selected::after) {
right: 16px;
}
.logText {
line-height: 22px;
font-size: 12px;