From 19f3d5d49b000979278bd6cdbd3b1b75b298f3cd Mon Sep 17 00:00:00 2001 From: lan-yonghui Date: Tue, 9 Jun 2026 18:49:55 +0800 Subject: [PATCH] fix: refine alert methods --- agent/app/api/v2/alert.go | 2 +- agent/app/service/alert.go | 60 ++++++++++++++++++ agent/app/service/alert_helper.go | 63 ++++++++++++++++++- agent/i18n/lang/en.yaml | 1 + agent/i18n/lang/es-ES.yaml | 1 + agent/i18n/lang/ja.yaml | 1 + agent/i18n/lang/ko.yaml | 1 + agent/i18n/lang/ms.yaml | 1 + agent/i18n/lang/pt-BR.yaml | 1 + agent/i18n/lang/ru.yaml | 1 + agent/i18n/lang/tr.yaml | 1 + agent/i18n/lang/zh-Hant.yaml | 1 + agent/i18n/lang/zh.yaml | 1 + frontend/src/api/modules/alert.ts | 2 +- .../views/setting/alert/dash/task/index.vue | 52 +++++++++++---- .../src/views/setting/alert/setting/index.vue | 3 + 16 files changed, 174 insertions(+), 18 deletions(-) diff --git a/agent/app/api/v2/alert.go b/agent/app/api/v2/alert.go index 5faa9d4ae273..30aee5f08106 100644 --- a/agent/app/api/v2/alert.go +++ b/agent/app/api/v2/alert.go @@ -287,7 +287,7 @@ func (b *BaseApi) PageAlertConfig(c *gin.Context) { // @Security ApiKeyAuth // @Security Timestamp // @Router /alert/config/update [post] -// @x-panel-log {"bodyKeys":["title"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置 [title]","formatEN":"update alert config [title]"} +// @x-panel-log {"bodyKeys":["id","type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置 [id][type]","formatEN":"update alert config [id][type]"} func (b *BaseApi) UpdateAlertConfig(c *gin.Context) { var req dto.AlertConfigUpdate if err := helper.CheckBindAndValidate(&req, c); err != nil { diff --git a/agent/app/service/alert.go b/agent/app/service/alert.go index 18c3de59c518..30d2104515c7 100644 --- a/agent/app/service/alert.go +++ b/agent/app/service/alert.go @@ -5,6 +5,7 @@ import ( "fmt" "mime" "sort" + "strconv" "strings" "sync" "time" @@ -26,6 +27,12 @@ import ( type AlertService struct{} var eeHiddenAlertTypes = []string{"licenseException", "panelUpdate", "panelPwdEndTime"} +var communityAlertMethodTypeNames = map[string]string{ + constant.WeCom: "WeCom", + constant.DingTalk: "DingTalk", + constant.FeiShu: "FeiShu", + constant.SMS: "SMS", +} type IAlertService interface { PageAlert(req dto.AlertSearch) (int64, []dto.AlertDTO, error) @@ -132,6 +139,9 @@ func (a AlertService) GetAlerts() ([]dto.AlertDTO, error) { } func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error { + if err := a.validateCommunityAlertMethod(create.Method); err != nil { + return err + } var alertID uint var alertInfo model.Alert if create.Project != "" { @@ -170,6 +180,9 @@ func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error } func (a AlertService) UpdateAlert(req dto.AlertUpdate, operator string) error { + if err := a.validateCommunityAlertMethod(req.Method); err != nil { + return err + } upMap := make(map[string]interface{}) upMap["id"] = req.ID @@ -493,6 +506,9 @@ func (a AlertService) PageAlertConfig(req dto.AlertConfigPageReq) (int64, []mode } func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error { + if err := a.validateCommunityAlertConfigType(req.Type); err != nil { + return err + } if err := a.checkAlertConfigDisplayNameUnique(req); err != nil { return err } @@ -545,6 +561,47 @@ func (a AlertService) checkAlertConfigDisplayNameUnique(req dto.AlertConfigUpdat return nil } +func (a AlertService) validateCommunityAlertMethod(method string) error { + if global.CONF.Base.IsEnterprise { + return nil + } + if strings.TrimSpace(method) == "" { + return nil + } + + for _, item := range strings.Split(method, ",") { + item = strings.TrimSpace(item) + if item == "" { + continue + } + if configID, err := strconv.ParseUint(item, 10, 64); err == nil { + config, err := alertRepo.GetConfigById(uint(configID)) + if err != nil { + return err + } + if name, ok := communityAlertMethodTypeNames[config.Type]; ok { + return buserr.WithMap("ErrAlertMethodNotSupported", map[string]interface{}{"name": name}, nil) + } + continue + } + if name, ok := communityAlertMethodTypeNames[item]; ok { + return buserr.WithMap("ErrAlertMethodNotSupported", map[string]interface{}{"name": name}, nil) + } + } + + return nil +} + +func (a AlertService) validateCommunityAlertConfigType(configType string) error { + if global.CONF.Base.IsEnterprise { + return nil + } + if name, ok := communityAlertMethodTypeNames[configType]; ok { + return buserr.WithMap("ErrAlertMethodNotSupported", map[string]interface{}{"name": name}, nil) + } + return nil +} + func alertConfigDisplayName(configType, configData string) string { switch configType { case constant.Email, constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Bark: @@ -605,6 +662,9 @@ func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) { } func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator string) error { + if err := a.validateCommunityAlertMethod(updateAlert.Method); err != nil { + return err + } upMap := make(map[string]interface{}) var newStatus string if updateAlert.SendCount == 0 { diff --git a/agent/app/service/alert_helper.go b/agent/app/service/alert_helper.go index 25a445e030c4..0b200594ae57 100644 --- a/agent/app/service/alert_helper.go +++ b/agent/app/service/alert_helper.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "math" + "net" "sort" "strconv" "strings" @@ -24,7 +25,7 @@ import ( "github.com/shirou/gopsutil/v4/disk" "github.com/shirou/gopsutil/v4/load" "github.com/shirou/gopsutil/v4/mem" - "github.com/shirou/gopsutil/v4/net" + gnet "github.com/shirou/gopsutil/v4/net" ) const ( @@ -35,7 +36,7 @@ const ( type AlertTaskHelper struct { DiskIO chan []disk.IOCountersStat - NetIO chan []net.IOCountersStat + NetIO chan []gnet.IOCountersStat } type IAlertTaskHelper interface { @@ -55,7 +56,7 @@ var resourceTypes = map[string]bool{"cpu": true, "memory": true, "disk": true, " func NewIAlertTaskHelper() IAlertTaskHelper { return &AlertTaskHelper{ DiskIO: make(chan []disk.IOCountersStat, 1), - NetIO: make(chan []net.IOCountersStat, 1), + NetIO: make(chan []gnet.IOCountersStat, 1), } } func (m *AlertTaskHelper) StartTask() { @@ -485,6 +486,7 @@ func loadPanelLogin(alert dto.AlertDTO) { if err != nil { global.LOG.Errorf("Failed to check recent failed ip login logs: %v", err) } + records = filterLoginLogsNotInWhitelist(records, whitelist) if len(records) > 0 { quota := strings.Join(func() []string { var ips []string @@ -534,6 +536,7 @@ func loadSSHLogin(alert dto.AlertDTO) { if err != nil { global.LOG.Errorf("Failed to check recent failed ip ssh login logs: %v", err) } + records = filterSSHLoginEntriesNotInWhitelist(records, whitelist) if len(records) > 0 { quota := strings.Join(records, "\n") params := []dto.Param{ @@ -552,6 +555,60 @@ func loadSSHLogin(alert dto.AlertDTO) { } } +func filterLoginLogsNotInWhitelist(records []model.LoginLog, whitelist []string) []model.LoginLog { + filtered := make([]model.LoginLog, 0, len(records)) + for _, record := range records { + if !isIPInWhitelist(record.IP, whitelist) { + filtered = append(filtered, record) + } + } + return filtered +} + +func filterSSHLoginEntriesNotInWhitelist(records []string, whitelist []string) []string { + filtered := make([]string, 0, len(records)) + for _, record := range records { + ip := record + if idx := strings.Index(record, "-"); idx >= 0 { + ip = record[:idx] + } + if !isIPInWhitelist(ip, whitelist) { + filtered = append(filtered, record) + } + } + return filtered +} + +func isIPInWhitelist(ip string, whitelist []string) bool { + targetIP := net.ParseIP(strings.TrimSpace(ip)) + if targetIP == nil { + return false + } + for _, item := range whitelist { + item = strings.TrimSpace(item) + if item == "" { + continue + } + if item == ip { + return true + } + if whiteIP := net.ParseIP(item); whiteIP != nil { + if whiteIP.Equal(targetIP) { + return true + } + continue + } + _, ipNet, err := net.ParseCIDR(item) + if err != nil { + continue + } + if ipNet.Contains(targetIP) { + return true + } + } + return false +} + func loadNodeException(alert dto.AlertDTO) { // only master alert failCount, err := xpack.AlertProvider.GetNodeErrorAlert() diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml index 8e054b78565e..f3ebed759385 100644 --- a/agent/i18n/lang/en.yaml +++ b/agent/i18n/lang/en.yaml @@ -14,6 +14,7 @@ ErrStructTransform: 'Type conversion failed: {{ .err }}' ErrNotLogin: 'Not logged in: {{ .detail }}' ErrPasswordExpired: 'Password expired: {{ .detail }}' ErrNotSupportType: 'Unsupported type: {{ .name }}' +ErrAlertMethodNotSupported: 'This version does not support this alert method: {{ .name }}' ErrProxy: 'Request failed: {{ .detail }}' ErrApiConfigStatusInvalid: 'API access disabled: {{ .detail }}' ErrApiConfigKeyInvalid: 'Invalid API key: {{ .detail }}' diff --git a/agent/i18n/lang/es-ES.yaml b/agent/i18n/lang/es-ES.yaml index c55e322a17c3..ce639c7f1a95 100644 --- a/agent/i18n/lang/es-ES.yaml +++ b/agent/i18n/lang/es-ES.yaml @@ -14,6 +14,7 @@ ErrStructTransform: 'Error de conversión: {{ .err }}' ErrNotLogin: 'No has iniciado sesión: {{ .detail }}' ErrPasswordExpired: 'Contraseña expirada: {{ .detail }}' ErrNotSupportType: 'Tipo no soportado: {{ .name }}' +ErrAlertMethodNotSupported: 'Esta versión no admite este método de alerta: {{ .name }}' ErrProxy: 'Solicitud fallida: {{ .detail }}' ErrApiConfigStatusInvalid: 'API desactivada: {{ .detail }}' ErrApiConfigKeyInvalid: 'Clave API inválida: {{ .detail }}' diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml index 3f7df7352079..3942e542e756 100644 --- a/agent/i18n/lang/ja.yaml +++ b/agent/i18n/lang/ja.yaml @@ -14,6 +14,7 @@ ErrStructTransform: '型変換エラー: {{ .err }}' ErrNotLogin: 'ログインしていません: {{ .detail }}' ErrPasswordExpired: 'パスワード期限切れ: {{ .detail }}' ErrNotSupportType: '未対応のタイプ: {{ .name }}' +ErrAlertMethodNotSupported: 'このバージョンではこの通知方法は利用できません: {{ .name }}' ErrProxy: 'リクエストに失敗しました: {{ .detail }}' ErrApiConfigStatusInvalid: 'API利用不可: {{ .detail }}' ErrApiConfigKeyInvalid: 'APIキーが無効です: {{ .detail }}' diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml index 7f574f751cbc..c5f4c078e121 100644 --- a/agent/i18n/lang/ko.yaml +++ b/agent/i18n/lang/ko.yaml @@ -14,6 +14,7 @@ ErrStructTransform: '형 변환 실패: {{ .err }}' ErrNotLogin: '로그인하지 않음: {{ .detail }}' ErrPasswordExpired: '비밀번호 만료: {{ .detail }}' ErrNotSupportType: '지원하지 않는 타입: {{ .name }}' +ErrAlertMethodNotSupported: '현재 버전에서는 이 알림 방식을 지원하지 않습니다: {{ .name }}' ErrProxy: '요청 실패: {{ .detail }}' ErrApiConfigStatusInvalid: 'API 사용 중지: {{ .detail }}' ErrApiConfigKeyInvalid: 'API 키가 잘못됨: {{ .detail }}' diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml index 582e62f62026..fe18e94b739c 100644 --- a/agent/i18n/lang/ms.yaml +++ b/agent/i18n/lang/ms.yaml @@ -14,6 +14,7 @@ ErrStructTransform: 'Ralat penukaran: {{ .err }}' ErrNotLogin: 'Belum log masuk: {{ .detail }}' ErrPasswordExpired: 'Kata laluan tamat: {{ .detail }}' ErrNotSupportType: 'Jenis tidak disokong: {{ .name }}' +ErrAlertMethodNotSupported: 'Versi ini tidak menyokong kaedah amaran ini: {{ .name }}' ErrProxy: 'Permintaan gagal: {{ .detail }}' ErrApiConfigStatusInvalid: 'API dilumpuhkan: {{ .detail }}' ErrApiConfigKeyInvalid: 'Kunci API tidak sah: {{ .detail }}' diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml index 309f538baf03..4664614d37a8 100644 --- a/agent/i18n/lang/pt-BR.yaml +++ b/agent/i18n/lang/pt-BR.yaml @@ -14,6 +14,7 @@ ErrStructTransform: 'Erro de conversão: {{ .err }}' ErrNotLogin: 'Sem sessão: {{ .detail }}' ErrPasswordExpired: 'Senha expirada: {{ .detail }}' ErrNotSupportType: 'Tipo não suportado: {{ .name }}' +ErrAlertMethodNotSupported: 'Esta versão não oferece suporte a este método de alerta: {{ .name }}' ErrProxy: 'Requisição falhou: {{ .detail }}' ErrApiConfigStatusInvalid: 'API desativada: {{ .detail }}' ErrApiConfigKeyInvalid: 'Chave API inválida: {{ .detail }}' diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml index f7c75eb6ee52..3d940175cfe8 100644 --- a/agent/i18n/lang/ru.yaml +++ b/agent/i18n/lang/ru.yaml @@ -14,6 +14,7 @@ ErrStructTransform: 'Ошибка преобразования: {{ .err }}' ErrNotLogin: 'Не авторизован: {{ .detail }}' ErrPasswordExpired: 'Пароль истёк: {{ .detail }}' ErrNotSupportType: 'Тип не поддерживается: {{ .name }}' +ErrAlertMethodNotSupported: 'Эта версия не поддерживает данный способ оповещения: {{ .name }}' ErrProxy: 'Запрос не удался: {{ .detail }}' ErrApiConfigStatusInvalid: 'API отключено: {{ .detail }}' ErrApiConfigKeyInvalid: 'Неверный API-ключ: {{ .detail }}' diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml index 289729b412e7..354f46a4319d 100644 --- a/agent/i18n/lang/tr.yaml +++ b/agent/i18n/lang/tr.yaml @@ -14,6 +14,7 @@ ErrStructTransform: 'Dönüşüm hatası: {{ .err }}' ErrNotLogin: 'Oturum açılmamış: {{ .detail }}' ErrPasswordExpired: 'Şifre süresi doldu: {{ .detail }}' ErrNotSupportType: 'Bu tür desteklenmiyor: {{ .name }}' +ErrAlertMethodNotSupported: 'Bu sürüm bu uyarı yöntemini desteklemiyor: {{ .name }}' ErrProxy: 'İstek başarısız: {{ .detail }}' ErrApiConfigStatusInvalid: 'API kapalı: {{ .detail }}' ErrApiConfigKeyInvalid: 'Geçersiz API anahtarı: {{ .detail }}' diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml index a656e4b607e9..5573735e62a6 100644 --- a/agent/i18n/lang/zh-Hant.yaml +++ b/agent/i18n/lang/zh-Hant.yaml @@ -14,6 +14,7 @@ ErrStructTransform: '型別轉換失敗: {{ .err }}' ErrNotLogin: '使用者未登入: {{ .detail }}' ErrPasswordExpired: '目前密碼已過期: {{ .detail }}' ErrNotSupportType: '系統暫不支援目前類型: {{ .name }}' +ErrAlertMethodNotSupported: '當前版本不支援此告警方式: {{ .name }}' ErrProxy: '請求錯誤,請檢查該節點狀態: {{ .detail }}' ErrApiConfigStatusInvalid: 'API 介面禁止存取: {{ .detail }}' ErrApiConfigKeyInvalid: 'API 介面金鑰錯誤: {{ .detail }}' diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml index 4b656d35e3e2..48438b3d4250 100644 --- a/agent/i18n/lang/zh.yaml +++ b/agent/i18n/lang/zh.yaml @@ -14,6 +14,7 @@ ErrStructTransform: "类型转换失败: {{ .err }}" ErrNotLogin: "用户未登录: {{ .detail }}" ErrPasswordExpired: "当前密码已过期: {{ .detail }}" ErrNotSupportType: "不支持当前类型: {{ .name }}" +ErrAlertMethodNotSupported: "当前版本不支持此告警方式: {{ .name }}" ErrProxy: "请求失败,请检查节点状态: {{ .detail }}" ErrApiConfigStatusInvalid: "API 禁止访问: {{ .detail }}" ErrApiConfigKeyInvalid: "API 接口密钥错误: {{ .detail }}" diff --git a/frontend/src/api/modules/alert.ts b/frontend/src/api/modules/alert.ts index cd9c7d0c08c0..181187f53a07 100644 --- a/frontend/src/api/modules/alert.ts +++ b/frontend/src/api/modules/alert.ts @@ -9,7 +9,7 @@ const alertConfigHiddenTypes = ['sms']; const resolveAlertConfigExcludeTypes = (excludeTypes: string[] = []) => { const globalStore = GlobalStore(); const types = new Set(excludeTypes); - if (!(globalStore.isProductPro && !globalStore.isIntl && !globalStore.isEE)) { + if (globalStore.isIntl || globalStore.isEE) { alertConfigHiddenTypes.forEach((type) => types.add(type)); } return Array.from(types); diff --git a/frontend/src/views/setting/alert/dash/task/index.vue b/frontend/src/views/setting/alert/dash/task/index.vue index d5382c9b298b..f319e8440d97 100644 --- a/frontend/src/views/setting/alert/dash/task/index.vue +++ b/frontend/src/views/setting/alert/dash/task/index.vue @@ -342,7 +342,7 @@ :key="opt.value" :value="opt.value" :label="opt.label" - :disabled="opt.disabled" + :disabled="isLockedMethodOption(opt)" >
@@ -421,10 +421,17 @@ const configOptions = computed(() => { label: getConfigOptionLabel(c), typeLabel: getConfigTypeLabel(c.type), type: c.type, - disabled: c.status !== 'Enable', - })); + disabled: + c.status !== 'Enable' || + (!isProductPro.value && ['weCom', 'dingTalk', 'feiShu', 'sms'].includes(c.type)), + })) + .sort((a, b) => Number(a.disabled) - Number(b.disabled)); }); +const isLockedMethodOption = (opt: { value: string; disabled: boolean }) => { + return opt.disabled && !dialogData.value.rowData?.sendMethod.includes(opt.value); +}; + const allConfigValues = computed(() => configOptions.value.filter((c) => !c.disabled).map((c) => c.value)); const legacyMethodTypeMap: Record = { @@ -446,6 +453,14 @@ const normalizeMethodValues = (methods: string[]) => { }); }; +const isAllEnabledMethodsSelected = (methods: string[]) => { + return ( + methods.length > 0 && + methods.every((item) => allConfigValues.value.includes(item)) && + allConfigValues.value.every((item) => methods.includes(item)) + ); +}; + const getConfigTypeLabel = (type: string): string => { return i18n.global.t(`xpack.alert.${type}`); }; @@ -473,16 +488,28 @@ const getConfigOptionLabel = (c: Alert.AlertConfigInfo): string => { return getConfigTypeLabel(c.type); }; +const lastSendMethod = ref([]); + 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))) - ) { + if (values.includes(ALL_SEND_METHOD)) { + dialogData.value.rowData.sendMethod = [ALL_SEND_METHOD]; + lastSendMethod.value = [...allConfigValues.value]; + return; + } + const disabledValues = new Set(configOptions.value.filter((c) => c.disabled).map((c) => c.value)); + const prev = new Set(lastSendMethod.value); + const filteredValues = values.filter((value) => !disabledValues.has(value) || prev.has(value)); + const nonAllValues = filteredValues.filter((item) => item !== ALL_SEND_METHOD); + const hasLockedValues = nonAllValues.some((item) => disabledValues.has(item)); + + if (!hasLockedValues && isAllEnabledMethodsSelected(nonAllValues)) { dialogData.value.rowData.sendMethod = [ALL_SEND_METHOD]; + lastSendMethod.value = [...allConfigValues.value]; return; } - dialogData.value.rowData.sendMethod = values.filter((item) => item !== ALL_SEND_METHOD); + dialogData.value.rowData.sendMethod = nonAllValues; + lastSendMethod.value = [...nonAllValues]; }; interface DialogProps { @@ -526,16 +553,15 @@ const cronjobTypes = [ const acceptParams = (params: DialogProps): void => { dialogData.value = params; dialogData.value.rowData.sendMethod = []; + lastSendMethod.value = []; if (dialogData.value.rowData.method != '') { 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)) - ) { + if (isAllEnabledMethodsSelected(sendMethods)) { dialogData.value.rowData.sendMethod = [ALL_SEND_METHOD]; + lastSendMethod.value = [...allConfigValues.value]; } else { dialogData.value.rowData.sendMethod = sendMethods; + lastSendMethod.value = [...sendMethods]; } } if (cronjobTypes.includes(dialogData.value.rowData.type)) { diff --git a/frontend/src/views/setting/alert/setting/index.vue b/frontend/src/views/setting/alert/setting/index.vue index 80986750288a..2b51bda7dc56 100644 --- a/frontend/src/views/setting/alert/setting/index.vue +++ b/frontend/src/views/setting/alert/setting/index.vue @@ -107,6 +107,7 @@ v-model="row.status" active-value="Enable" inactive-value="Disable" + :disabled="!isProductPro && ['weCom', 'dingTalk', 'feiShu', 'sms'].includes(row.type)" @change="onStatusChange(row)" /> @@ -478,6 +479,8 @@ const buttons = computed(() => [ click: (row: Alert.AlertConfigInfo) => { openEditDrawer(row); }, + disabled: (row: Alert.AlertConfigInfo) => + !isProductPro.value && ['weCom', 'dingTalk', 'feiShu', 'sms'].includes(row.type), }, { label: i18n.global.t('commons.button.delete'),