[feat] add ntfy as a new alert notification channel (#4132)

Co-authored-by: lynx009 <2030509072@qq.com>
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
This commit is contained in:
Zmjjeff7
2026-07-03 00:30:57 +08:00
committed by GitHub
co-authored by lynx009 Tomsun28
parent 2389db7b3b
commit 2fd363dfdd
11 changed files with 548 additions and 2 deletions
@@ -74,6 +74,11 @@ public class AlerterProperties {
*/
private String gotifyWebhookUrl = "https://push.example.de/message?token=";
/**
* Ntfy default server url
*/
private String ntfyDefaultServerUrl = "https://ntfy.sh";
/**
* Data entry configuration properties
*/
@@ -0,0 +1,182 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.notice.impl;
import java.util.Map;
import java.util.StringJoiner;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.notice.AlertNoticeException;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
/**
* Send alert notification through ntfy push notification service.
* Supports priority mapping, tags/emoji, click action URL,
* and optional Bearer token authentication for self-hosted ntfy servers.
*
* @see <a href="https://docs.ntfy.sh/publish/">ntfy publish API</a>
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class NtfyAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl {
private static final String STATUS_FIRING = "firing";
private static final String SEVERITY_CRITICAL = "critical";
private static final String SEVERITY_WARNING = "warning";
@Override
public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) throws AlertNoticeException {
try {
String content = renderContent(noticeTemplate, alert);
String url = buildNtfyUrl(receiver);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_PLAIN);
headers.set("Title", bundle.getString("alerter.notify.title"));
headers.set("Markdown", "yes");
headers.set("Priority", String.valueOf(mapPriority(alert)));
headers.set("Tags", buildTags(alert));
// Set click action to console URL if available
if (alerterProperties != null && alerterProperties.getConsoleUrl() != null
&& !alerterProperties.getConsoleUrl().isEmpty()) {
headers.set("Click", alerterProperties.getConsoleUrl());
}
// Set Bearer token authentication for self-hosted ntfy servers
String token = receiver.getNtfyToken();
if (token != null && !token.isEmpty()) {
headers.set("Authorization", "Bearer " + token);
}
HttpEntity<String> httpEntity = new HttpEntity<>(content, headers);
ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, httpEntity, String.class);
if (responseEntity.getStatusCode() == HttpStatus.OK) {
log.debug("Send ntfy notification to {} Success", url);
} else {
log.warn("Send ntfy notification to {} Failed: {}", url, responseEntity.getBody());
throw new AlertNoticeException("Http StatusCode " + responseEntity.getStatusCode());
}
} catch (Exception e) {
throw new AlertNoticeException("[Ntfy Notify Error] " + e.getMessage());
}
}
@Override
public byte type() {
return 15;
}
/**
* Build the ntfy publish URL from receiver configuration.
*/
private String buildNtfyUrl(NoticeReceiver receiver) {
String serverUrl = receiver.getNtfyServerUrl();
if (serverUrl == null || serverUrl.isEmpty()) {
serverUrl = alerterProperties.getNtfyDefaultServerUrl();
}
if (serverUrl.endsWith("/")) {
serverUrl = serverUrl.substring(0, serverUrl.length() - 1);
}
return serverUrl + "/" + receiver.getNtfyTopic();
}
/**
* Map alert severity to ntfy priority level (1-5).
* <ul>
* <li>5 (max) - critical severity while firing</li>
* <li>4 (high) - warning severity while firing</li>
* <li>3 (default) - info or unknown severity while firing</li>
* <li>2 (low) - resolved alerts</li>
* </ul>
*
* @see <a href="https://docs.ntfy.sh/publish/#message-priority">ntfy message priority</a>
*/
protected int mapPriority(GroupAlert alert) {
String status = alert.getStatus();
if (!STATUS_FIRING.equalsIgnoreCase(status)) {
return 2;
}
String severity = extractSeverity(alert);
if (SEVERITY_CRITICAL.equalsIgnoreCase(severity)) {
return 5;
} else if (SEVERITY_WARNING.equalsIgnoreCase(severity)) {
return 4;
}
return 3;
}
/**
* Build ntfy tags string with emoji based on alert status and severity.
* Tags appear as emoji icons in the notification.
*
* @see <a href="https://docs.ntfy.sh/publish/#tags-emojis">ntfy tags &amp; emojis</a>
*/
protected String buildTags(GroupAlert alert) {
StringJoiner joiner = new StringJoiner(",");
String status = alert.getStatus();
if (STATUS_FIRING.equalsIgnoreCase(status)) {
String severity = extractSeverity(alert);
if (SEVERITY_CRITICAL.equalsIgnoreCase(severity)) {
joiner.add("rotating_light");
joiner.add("skull");
} else if (SEVERITY_WARNING.equalsIgnoreCase(severity)) {
joiner.add("warning");
} else {
joiner.add("information_source");
}
} else {
joiner.add("white_check_mark");
}
// Append alert name as a plain-text tag if available
Map<String, String> commonLabels = alert.getCommonLabels();
if (commonLabels != null && commonLabels.containsKey("alertname")) {
joiner.add(commonLabels.get("alertname"));
}
return joiner.toString();
}
/**
* Extract severity from alert's common labels.
* Checks "severity" key first, then falls back to "priority".
*/
private String extractSeverity(GroupAlert alert) {
Map<String, String> commonLabels = alert.getCommonLabels();
if (commonLabels == null) {
return null;
}
String severity = commonLabels.get("severity");
if (severity == null) {
severity = commonLabels.get("priority");
}
return severity;
}
}
@@ -0,0 +1,288 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.notice.impl;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import org.apache.hertzbeat.alert.AlerterProperties;
import org.apache.hertzbeat.alert.notice.AlertNoticeException;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
/**
* Test case for {@link NtfyAlertNotifyHandlerImpl}
*/
@ExtendWith(MockitoExtension.class)
class NtfyAlertNotifyHandlerImplTest {
@Mock
private RestTemplate restTemplate;
@Mock
private AlerterProperties alerterProperties;
@Mock
private ResourceBundle bundle;
@InjectMocks
private NtfyAlertNotifyHandlerImpl ntfyHandler;
private NoticeReceiver receiver;
private NoticeTemplate template;
@BeforeEach
public void setUp() {
receiver = new NoticeReceiver();
receiver.setId(1L);
receiver.setName("ntfy-test");
receiver.setNtfyServerUrl("https://ntfy.example.com");
receiver.setNtfyTopic("hertzbeat-alerts");
template = new NoticeTemplate();
template.setId(1L);
template.setName("test-template");
template.setContent("test alert content");
lenient().when(alerterProperties.getNtfyDefaultServerUrl()).thenReturn("https://ntfy.sh");
lenient().when(alerterProperties.getConsoleUrl()).thenReturn("https://console.hertzbeat.com");
lenient().when(bundle.getString("alerter.notify.title")).thenReturn("HertzBeat Alert");
}
@Test
void testType() {
assertEquals(15, ntfyHandler.type());
}
@Test
void testSendSuccess() {
GroupAlert alert = buildGroupAlert("firing", "critical");
ResponseEntity<String> responseEntity = new ResponseEntity<>("{\"id\":\"abc123\"}", HttpStatus.OK);
when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(String.class)))
.thenReturn(responseEntity);
ntfyHandler.send(receiver, template, alert);
ArgumentCaptor<String> urlCaptor = ArgumentCaptor.forClass(String.class);
verify(restTemplate).postForEntity(urlCaptor.capture(), any(HttpEntity.class), eq(String.class));
assertEquals("https://ntfy.example.com/hertzbeat-alerts", urlCaptor.getValue());
}
@Test
void testSendWithAuthToken() {
receiver.setNtfyToken("tk_testtoken123");
GroupAlert alert = buildGroupAlert("firing", "warning");
ResponseEntity<String> responseEntity = new ResponseEntity<>("{}", HttpStatus.OK);
when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(String.class)))
.thenReturn(responseEntity);
ntfyHandler.send(receiver, template, alert);
@SuppressWarnings("unchecked")
ArgumentCaptor<HttpEntity<String>> entityCaptor = ArgumentCaptor.forClass(HttpEntity.class);
verify(restTemplate).postForEntity(anyString(), entityCaptor.capture(), eq(String.class));
assertEquals("Bearer tk_testtoken123", entityCaptor.getValue().getHeaders().getFirst("Authorization"));
}
@Test
void testSendUsesDefaultServerWhenEmpty() {
receiver.setNtfyServerUrl("");
GroupAlert alert = buildGroupAlert("firing", "info");
ResponseEntity<String> responseEntity = new ResponseEntity<>("{}", HttpStatus.OK);
when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(String.class)))
.thenReturn(responseEntity);
ntfyHandler.send(receiver, template, alert);
ArgumentCaptor<String> urlCaptor = ArgumentCaptor.forClass(String.class);
verify(restTemplate).postForEntity(urlCaptor.capture(), any(HttpEntity.class), eq(String.class));
assertEquals("https://ntfy.sh/hertzbeat-alerts", urlCaptor.getValue());
}
@Test
void testSendFailure() {
GroupAlert alert = buildGroupAlert("firing", "critical");
ResponseEntity<String> responseEntity = new ResponseEntity<>("error", HttpStatus.INTERNAL_SERVER_ERROR);
when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(String.class)))
.thenReturn(responseEntity);
assertThrows(AlertNoticeException.class, () -> ntfyHandler.send(receiver, template, alert));
}
@Test
void testSendNetworkError() {
GroupAlert alert = buildGroupAlert("firing", "warning");
when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(String.class)))
.thenThrow(new org.springframework.web.client.ResourceAccessException("Connection refused"));
assertThrows(AlertNoticeException.class, () -> ntfyHandler.send(receiver, template, alert));
}
@Test
void testMapPriorityCriticalFiring() {
GroupAlert alert = buildGroupAlert("firing", "critical");
assertEquals(5, ntfyHandler.mapPriority(alert));
}
@Test
void testMapPriorityWarningFiring() {
GroupAlert alert = buildGroupAlert("firing", "warning");
assertEquals(4, ntfyHandler.mapPriority(alert));
}
@Test
void testMapPriorityInfoFiring() {
GroupAlert alert = buildGroupAlert("firing", "info");
assertEquals(3, ntfyHandler.mapPriority(alert));
}
@Test
void testMapPriorityResolved() {
GroupAlert alert = buildGroupAlert("resolved", "critical");
assertEquals(2, ntfyHandler.mapPriority(alert));
}
@Test
void testMapPriorityUnknownSeverity() {
GroupAlert alert = buildGroupAlert("firing", null);
assertEquals(3, ntfyHandler.mapPriority(alert));
}
@Test
void testBuildTagsCriticalFiring() {
GroupAlert alert = buildGroupAlert("firing", "critical");
String tags = ntfyHandler.buildTags(alert);
assertTrue(tags.contains("rotating_light"));
assertTrue(tags.contains("skull"));
}
@Test
void testBuildTagsWarningFiring() {
GroupAlert alert = buildGroupAlert("firing", "warning");
String tags = ntfyHandler.buildTags(alert);
assertTrue(tags.contains("warning"));
}
@Test
void testBuildTagsResolved() {
GroupAlert alert = buildGroupAlert("resolved", "critical");
String tags = ntfyHandler.buildTags(alert);
assertTrue(tags.contains("white_check_mark"));
}
@Test
void testBuildTagsIncludesAlertName() {
GroupAlert alert = buildGroupAlert("firing", "warning");
alert.getCommonLabels().put("alertname", "HighCPU");
String tags = ntfyHandler.buildTags(alert);
assertTrue(tags.contains("HighCPU"));
}
@Test
void testSendHeadersContainPriorityAndTags() {
GroupAlert alert = buildGroupAlert("firing", "critical");
ResponseEntity<String> responseEntity = new ResponseEntity<>("{}", HttpStatus.OK);
when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(String.class)))
.thenReturn(responseEntity);
ntfyHandler.send(receiver, template, alert);
@SuppressWarnings("unchecked")
ArgumentCaptor<HttpEntity<String>> entityCaptor = ArgumentCaptor.forClass(HttpEntity.class);
verify(restTemplate).postForEntity(anyString(), entityCaptor.capture(), eq(String.class));
HttpEntity<String> captured = entityCaptor.getValue();
assertEquals("5", captured.getHeaders().getFirst("Priority"));
assertEquals("yes", captured.getHeaders().getFirst("Markdown"));
assertEquals("https://console.hertzbeat.com", captured.getHeaders().getFirst("Click"));
assertTrue(captured.getHeaders().getFirst("Tags").contains("rotating_light"));
}
@Test
void testServerUrlTrailingSlashRemoved() {
receiver.setNtfyServerUrl("https://ntfy.example.com/");
GroupAlert alert = buildGroupAlert("firing", "info");
ResponseEntity<String> responseEntity = new ResponseEntity<>("{}", HttpStatus.OK);
when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(String.class)))
.thenReturn(responseEntity);
ntfyHandler.send(receiver, template, alert);
ArgumentCaptor<String> urlCaptor = ArgumentCaptor.forClass(String.class);
verify(restTemplate).postForEntity(urlCaptor.capture(), any(HttpEntity.class), eq(String.class));
assertEquals("https://ntfy.example.com/hertzbeat-alerts", urlCaptor.getValue());
}
private GroupAlert buildGroupAlert(String status, String severity) {
GroupAlert groupAlert = new GroupAlert();
groupAlert.setStatus(status);
Map<String, String> commonLabels = new HashMap<>();
if (severity != null) {
commonLabels.put("severity", severity);
}
groupAlert.setCommonLabels(commonLabels);
groupAlert.setCommonAnnotations(new HashMap<>());
groupAlert.setGroupLabels(new HashMap<>());
SingleAlert singleAlert = new SingleAlert();
singleAlert.setLabels(new HashMap<>());
singleAlert.setAnnotations(new HashMap<>());
if (severity != null) {
singleAlert.getLabels().put("severity", severity);
}
List<SingleAlert> alerts = new ArrayList<>();
alerts.add(singleAlert);
groupAlert.setAlerts(alerts);
return groupAlert;
}
}
@@ -69,12 +69,12 @@ public class NoticeReceiver {
@Schema(title = "Notification information method: 0-SMS 1-Email 2-webhook 3-WeChat Official Account 4-Enterprise WeChat Robot "
+ "5-DingTalk Robot 6-FeiShu Robot 7-Telegram Bot 8-SlackWebHook 9-Discord Bot 10-Enterprise WeChat app message "
+ "11-Slack 12-Discord 13-Gotify 14-FeiShu app message",
+ "11-Huawei Cloud SMN 12-ServerChan 13-Gotify 14-FeiShu app message 15-Ntfy",
description = "Notification information method: "
+ "0-SMS 1-Email 2-webhook 3-WeChat Official Account "
+ "4-Enterprise WeChat Robot 5-DingTalk Robot 6-FeiShu Robot "
+ "7-Telegram Bot 8-SlackWebHook 9-Discord Bot 10-Enterprise "
+ "WeChat app message 11-Slack 12-Discord 13-Gotify 14-FeiShu app message",
+ "WeChat app message 11-Huawei Cloud SMN 12-ServerChan 13-Gotify 14-FeiShu app message 15-Ntfy",
accessMode = READ_WRITE)
@Min(0)
@NotNull(message = "type can not null")
@@ -258,6 +258,27 @@ public class NoticeReceiver {
@Column(length = 300)
private String gotifyToken;
@Schema(title = "Ntfy server URL : The notification method is valid for Ntfy",
description = "Ntfy server URL : The notification method is valid for Ntfy, default is https://ntfy.sh",
example = "https://ntfy.sh", accessMode = READ_WRITE)
@Size(max = 300)
@Column(length = 300)
private String ntfyServerUrl;
@Schema(title = "Ntfy topic : The notification method is valid for Ntfy",
description = "Ntfy topic : The notification method is valid for Ntfy",
example = "hertzbeat-alerts", accessMode = READ_WRITE)
@Size(max = 300)
@Column(length = 300)
private String ntfyTopic;
@Schema(title = "Ntfy access token : Bearer token for self-hosted ntfy servers with authentication",
description = "Ntfy access token : Bearer token for self-hosted ntfy servers with authentication",
example = "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", accessMode = READ_WRITE)
@Size(max = 300)
@Column(length = 300)
private String ntfyToken;
@Schema(title = "The creator of this record", example = "tom",
accessMode = READ_ONLY)
@CreatedBy
+4
View File
@@ -22,6 +22,7 @@ export class NoticeReceiver {
name!: string;
// notification mode: 0-sms 1-email 2-webhook 3-wechat public account 4-work wechat robot 5-Dingding robot 6-Feishu robot
// 7-Telegram robot 8-SlackWebHook 9-Discord robot 10-work wechat app message 11-Huawei cloud SMN 12-ServerChan 13-Gotify
// 14-FeiShu app message 15-Ntfy
type: number = 1;
phone!: string;
email!: string;
@@ -49,6 +50,9 @@ export class NoticeReceiver {
smnTopicUrn!: string;
serverChanToken!: string;
gotifyToken!: string;
ntfyServerUrl!: string;
ntfyTopic!: string;
ntfyToken!: string;
creator!: string;
modifier!: string;
gmtCreate!: number;
@@ -131,6 +131,10 @@
<i nz-icon nzTheme="outline" nzType="notification"></i>
<span>{{ 'alert.notice.type.lark-app' | i18n }}</span>
</nz-tag>
<nz-tag *ngIf="data.type == 15" nzColor="orange">
<i nz-icon nzTheme="outline" nzType="notification"></i>
<span>{{ 'alert.notice.type.ntfy' | i18n }}</span>
</nz-tag>
</td>
<td nzAlign="center">
<span *ngIf="data.type == 0">{{ data.phone }}</span>
@@ -148,6 +152,7 @@
<span *ngIf="data.type == 12">{{ data.serverChanToken }}</span>
<span *ngIf="data.type == 13">{{ data.gotifyToken }}</span>
<span *ngIf="data.type == 14">{{ data.appId }}</span>
<span *ngIf="data.type == 15">{{ data.ntfyServerUrl }}/{{ data.ntfyTopic }}</span>
</td>
<td nzAlign="center">{{ (data.gmtUpdate ? data.gmtUpdate : data.gmtCreate) | date : 'YYYY-MM-dd HH:mm:ss' }}</td>
<td nzAlign="center" nzRight>
@@ -230,6 +235,7 @@
<nz-option [nzLabel]="'alert.notice.type.serverchan' | i18n" [nzValue]="12"></nz-option>
<nz-option [nzLabel]="'alert.notice.type.gotify' | i18n" [nzValue]="13"></nz-option>
<nz-option [nzLabel]="'alert.notice.type.lark-app' | i18n" [nzValue]="14"></nz-option>
<nz-option [nzLabel]="'alert.notice.type.ntfy' | i18n" [nzValue]="15"></nz-option>
</nz-select>
</nz-form-control>
</nz-form-item>
@@ -528,6 +534,26 @@
<input [(ngModel)]="receiver.gotifyToken" [required]="receiver.type === 13" name="gotifyToken" nz-input type="text" />
</nz-form-control>
</nz-form-item>
<nz-form-item *ngIf="receiver.type === 15">
<nz-form-label [nzSpan]="7" nzFor="ntfyServerUrl">{{ 'alert.notice.type.ntfy-server-url' | i18n }}</nz-form-label>
<nz-form-control [nzSpan]="12">
<input [(ngModel)]="receiver.ntfyServerUrl" name="ntfyServerUrl" nz-input type="url" placeholder="https://ntfy.sh" />
</nz-form-control>
</nz-form-item>
<nz-form-item *ngIf="receiver.type === 15">
<nz-form-label [nzRequired]="receiver.type === 15" [nzSpan]="7" nzFor="ntfyTopic"
>{{ 'alert.notice.type.ntfy-topic' | i18n }}
</nz-form-label>
<nz-form-control [nzErrorTip]="'validation.required' | i18n" [nzSpan]="12">
<input [(ngModel)]="receiver.ntfyTopic" [required]="receiver.type === 15" name="ntfyTopic" nz-input type="text" />
</nz-form-control>
</nz-form-item>
<nz-form-item *ngIf="receiver.type === 15">
<nz-form-label [nzSpan]="7" nzFor="ntfyToken">{{ 'alert.notice.type.ntfy-token' | i18n }}</nz-form-label>
<nz-form-control [nzSpan]="12">
<input [(ngModel)]="receiver.ntfyToken" name="ntfyToken" nz-input type="password" placeholder="" />
</nz-form-control>
</nz-form-item>
<nz-form-item *ngIf="receiver.type === 14">
<nz-form-label [nzRequired]="receiver.type === 14" [nzSpan]="7" nzFor="appId">
{{ 'alert.notice.type.lark-app-appId' | i18n }}
+4
View File
@@ -218,6 +218,10 @@
"alert.notice.type.lark-app-chatId": "Chat Id(separated by , symbol)",
"alert.notice.type.gotify": "Gotify",
"alert.notice.type.gotify-token": "Gotify Token",
"alert.notice.type.ntfy": "Ntfy",
"alert.notice.type.ntfy-server-url": "Ntfy Server URL",
"alert.notice.type.ntfy-topic": "Ntfy Topic",
"alert.notice.type.ntfy-token": "Ntfy Access Token",
"alert.notice.type.phone": "Phone",
"alert.notice.type.serverchan": "ServerChan",
"alert.notice.type.serverchan-token": "ServerChanToken",
+4
View File
@@ -205,6 +205,10 @@
"alert.notice.type.fei-shu-key": "FeiShu ロボットキー",
"alert.notice.type.gotify": "Gotify",
"alert.notice.type.gotify-token": "Gotify トークン",
"alert.notice.type.ntfy": "Ntfy",
"alert.notice.type.ntfy-server-url": "Ntfy サーバーURL",
"alert.notice.type.ntfy-topic": "Ntfy トピック",
"alert.notice.type.ntfy-token": "Ntfy アクセストークン",
"alert.notice.type.phone": "電話",
"alert.notice.type.serverchan": "ServerChan",
"alert.notice.type.serverchan-token": "ServerChanトークン",
+4
View File
@@ -200,6 +200,10 @@
"alert.notice.type.serverchan-token": "Token do ServerChan",
"alert.notice.type.gotify": "Gotify",
"alert.notice.type.gotify-token": "Token do Gotify",
"alert.notice.type.ntfy": "Ntfy",
"alert.notice.type.ntfy-server-url": "URL do Servidor Ntfy",
"alert.notice.type.ntfy-topic": "Tópico Ntfy",
"alert.notice.type.ntfy-token": "Token de Acesso Ntfy",
"alert.notice.rule": "Política de Notificação",
"alert.notice.rule.new": "Nova Política de Notificação",
"alert.notice.rule.edit": "Editar Política de Notificação",
+4
View File
@@ -218,6 +218,10 @@
"alert.notice.type.lark-app-chatId": "群聊id(多个使用,符号分隔)",
"alert.notice.type.gotify": "Gotify",
"alert.notice.type.gotify-token": "Gotify Token",
"alert.notice.type.ntfy": "Ntfy",
"alert.notice.type.ntfy-server-url": "Ntfy 服务器地址",
"alert.notice.type.ntfy-topic": "Ntfy 主题",
"alert.notice.type.ntfy-token": "Ntfy 访问令牌",
"alert.notice.type.phone": "手机号",
"alert.notice.type.serverchan": "Server酱(ServerChan)",
"alert.notice.type.serverchan-token": "Server酱Token",
+4
View File
@@ -204,6 +204,10 @@
"alert.notice.type.fei-shu-key": "飛書機器人KEY",
"alert.notice.type.gotify": "Gotify",
"alert.notice.type.gotify-token": "Gotify Token",
"alert.notice.type.ntfy": "Ntfy",
"alert.notice.type.ntfy-server-url": "Ntfy 伺服器地址",
"alert.notice.type.ntfy-topic": "Ntfy 主題",
"alert.notice.type.ntfy-token": "Ntfy 存取權杖",
"alert.notice.type.phone": "手機號",
"alert.notice.type.serverchan": "ServerChan",
"alert.notice.type.serverchan-token": "ServerChanToken",