This is an automated email from the ASF dual-hosted git repository.
gongchao pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
The following commit(s) were added to refs/heads/master by this push:
new 035d63c880 [feature] Support FeiShu/Lark App Alert Notification (#3856)
035d63c880 is described below
commit 035d63c880ee8e3bda29795fc2b0f3e7b8af9568
Author: P_Peaceful <[email protected]>
AuthorDate: Thu Nov 20 00:14:57 2025 +0800
[feature] Support FeiShu/Lark App Alert Notification (#3856)
Co-authored-by: Tomsun28 <[email protected]>
Co-authored-by: Calvin <[email protected]>
---
.../impl/FeiShuAppAlertNotifyHandlerImpl.java | 487 +++++++++++++++++++++
.../impl/FeiShuAppAlertNotifyHandlerImplTest.java | 333 ++++++++++++++
.../common/entity/alerter/NoticeReceiver.java | 21 +-
.../resources/templates/14-FeiShuAppTemplate.txt | 47 ++
home/docs/help/alert_feishu_app.md | 75 ++++
.../current/help/alert_feishu_app.md | 75 ++++
home/sidebars.json | 1 +
web-app/src/app/pojo/NoticeReceiver.ts | 3 +
.../alert-notice-receiver.component.html | 74 ++++
.../alert-notice-rule.component.ts | 3 +
.../alert-notice-template.component.html | 4 +
web-app/src/assets/i18n/en-US.json | 12 +
web-app/src/assets/i18n/zh-CN.json | 12 +
13 files changed, 1145 insertions(+), 2 deletions(-)
diff --git
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImpl.java
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImpl.java
new file mode 100644
index 0000000000..1c49c08af8
--- /dev/null
+++
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImpl.java
@@ -0,0 +1,487 @@
+/*
+ * 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 com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+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.util.JsonUtil;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+/**
+ * FeiShu app alert notify impl
+ */
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class FeiShuAppAlertNotifyHandlerImpl extends
AbstractAlertNotifyHandlerImpl {
+
+ /**
+ * get tenant access_token url
+ */
+ private static final String TENANT_ACCESS_TOKEN_URL =
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal";
+
+ /**
+ * get FeiShu app employee url
+ */
+ private static final String EMPLOYEE_URL =
"https://open.feishu.cn/open-apis/ehr/v1/employees?status=2&status=4&user_id_type=user_id&page_size=100";
+
+ /**
+ * send FeiShu app message url
+ */
+ private static final String APP_MESSAGE_URL =
"https://open.feishu.cn/open-apis/im/v1/messages";
+
+ /**
+ * send FeiShu app batch message url
+ */
+ private static final String APP_BATCH_MESSAGE_URL =
"https://open.feishu.cn/open-apis/message/v4/batch_send/";
+
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+ private static final byte USER_RECEIVE_TYPE = 0;
+ private static final byte CHAT_RECEIVE_TYPE = 1;
+ private static final byte PART_RECEIVE_TYPE = 2;
+ private static final byte ALL_RECEIVE_TYPE = 3;
+
+ private final RestTemplate restTemplate;
+ private final AlerterProperties alerterProperties;
+
+ @Override
+ public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate,
GroupAlert alert) throws AlertNoticeException {
+ var appId = receiver.getAppId();
+ var appSecret = receiver.getAppSecret();
+ var larkReceiveIdType = receiver.getLarkReceiveType();
+ try {
+ var accessToken = getAccessToken(appId, appSecret);
+ var notificationContent =
JsonUtil.toJson(renderContent(noticeTemplate, alert));
+ JsonNode messageContent = createLarkMessage(receiver,
notificationContent);
+ switch (larkReceiveIdType) {
+ case USER_RECEIVE_TYPE -> {
+ String[] userIds = receiver.getUserId().split(",");
+ if (userIds.length == 1) {
+ sendLarkMessage(accessToken, "user_id", userIds[0],
messageContent);
+ } else {
+ sendLarkUserBatchMessage(accessToken, userIds,
messageContent);
+ }
+ }
+ case CHAT_RECEIVE_TYPE -> sendLarkMessage(accessToken,
"chat_id", receiver.getChatId(), messageContent);
+ case PART_RECEIVE_TYPE ->
+ sendLarkDepartmentBatchMessage(accessToken,
receiver.getPartyId().split(","), messageContent);
+ case ALL_RECEIVE_TYPE -> {
+ List<String> userIds = new ArrayList<>();
+ getLarkEmployeeUserIds(accessToken, null, userIds);
+ sendLarkUserBatchMessage(accessToken, userIds.toArray(new
String[0]), messageContent);
+ }
+ default -> throw new AlertNoticeException("Invalid
larkReceiveIdType: " + larkReceiveIdType);
+ }
+ } catch (Exception e) {
+ throw new AlertNoticeException("[FeiShu App Notify Error] " +
e.getMessage());
+ }
+ }
+
+ @Override
+ public byte type() {
+ return 14;
+ }
+
+ /**
+ * Send FeiShu app message to chat or designated personnel
+ *
+ * @param accessToken Tenant access token
+ * @param receiverIdType FeiShu app send message receiver id type:
user_id, chat_id
+ * @param receiverId FeiShu app user id or chat id
+ * @param messageContent Message content
+ * @see <a
href="https://open.feishu.cn/document/server-docs/im-v1/batch_message/send-messages-in-batches">send
message</a>
+ */
+ private void sendLarkMessage(String accessToken, String receiverIdType,
String receiverId, JsonNode messageContent) throws JsonProcessingException {
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ headers.setBearerAuth(accessToken);
+ FeiShuAppMessageDto messageDto = FeiShuAppMessageDto.builder()
+ .receiveId(receiverId)
+ .content(escapedCompactJson(messageContent))
+ .build();
+ HttpEntity<FeiShuAppMessageDto> request = new HttpEntity<>(messageDto,
headers);
+ call(APP_MESSAGE_URL + "?receive_id_type=" + receiverIdType, request,
HttpMethod.POST, FeiShuAppResponse.class);
+ }
+
+ /**
+ * Send FeiShu app department batch message
+ *
+ * @param accessToken Tenant access token
+ * @param partyIds FeiShu app department ids
+ * @param messageContent Message content
+ * @see <a
href="https://open.feishu.cn/document/server-docs/im-v1/batch_message/send-messages-in-batches">send
batch message</a>
+ */
+ private void sendLarkDepartmentBatchMessage(String accessToken, String[]
partyIds, JsonNode messageContent) {
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ headers.setBearerAuth(accessToken);
+ FeiShuAppBatchMessageDto batchMessageDto =
FeiShuAppBatchMessageDto.builder()
+ .departmentIds(partyIds)
+ .card(messageContent)
+ .build();
+ HttpEntity<FeiShuAppBatchMessageDto> request = new
HttpEntity<>(batchMessageDto, headers);
+ call(APP_BATCH_MESSAGE_URL, request, HttpMethod.POST,
FeiShuAppResponse.class);
+ }
+
+ /**
+ * Send FeiShu app batch message to user
+ *
+ * @param accessToken Tenant access token
+ * @param userIds FeiShu app user ids
+ * @param messageContent Message content
+ * @see <a
href="https://open.feishu.cn/document/server-docs/im-v1/batch_message/send-messages-in-batches">send
batch message</a>
+ */
+ private void sendLarkUserBatchMessage(String accessToken, String[]
userIds, JsonNode messageContent) {
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ headers.setBearerAuth(accessToken);
+ FeiShuAppBatchMessageDto batchMessageDto =
FeiShuAppBatchMessageDto.builder()
+ .userIds(userIds)
+ .card(messageContent)
+ .build();
+ HttpEntity<FeiShuAppBatchMessageDto> request = new
HttpEntity<>(batchMessageDto, headers);
+ FeiShuAppResponse call = call(APP_BATCH_MESSAGE_URL, request,
HttpMethod.POST, FeiShuAppResponse.class);
+ }
+
+ /**
+ * Get FeiShu app tenant access token
+ *
+ * @param appId Unique identifier for the application, obtained after
creating the application
+ * @param appSecret Application key, obtained after creating the
application
+ * @return Tenant access token
+ * @see <a
href="https://open.feishu.cn/document/server-docs/authentication-management/access-token/tenant_access_token_internal">tenant_access_token</a>
+ */
+ private String getAccessToken(String appId, String appSecret) {
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ FeiShuAppAccessTokenDto accessTokenRequest =
FeiShuAppAccessTokenDto.builder()
+ .appId(appId)
+ .appSecret(appSecret)
+ .build();
+ HttpEntity<FeiShuAppAccessTokenDto> request = new
HttpEntity<>(accessTokenRequest, headers);
+ FeiShuAppAccessTokenResponse data = call(TENANT_ACCESS_TOKEN_URL,
request, HttpMethod.POST, FeiShuAppAccessTokenResponse.class);
+ return data.getTenantAccessToken();
+ }
+
+
+ /**
+ * Get FeiShu app employee user ids
+ *
+ * @param accessToken FeiShu app tenant access token
+ * @param pageToken Paging marker, left blank for the first request,
indicating traversal from scratch;
+ * When there are more items in the pagination query
result, a new page_token will be returned at the same time.
+ * The next iteration can use this page_token to obtain
the query result
+ * @param userIds Collection for recursive padding
+ * @see <a
href="https://open.feishu.cn/document/server-docs/ehr-v1/list?appId=cli_a999532b1f52900b">https://open.feishu.cn/open-apis/ehr/v1/employees</a>
+ */
+ private void getLarkEmployeeUserIds(String accessToken, String pageToken,
List<String> userIds) {
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ headers.setBearerAuth(accessToken);
+ HttpEntity<String> request = new HttpEntity<>(headers);
+ var url = StringUtils.isNotBlank(pageToken) ? EMPLOYEE_URL +
"&page_token=" + pageToken : EMPLOYEE_URL;
+ FeiShuAppEmployeeResponse employeeResponse = call(url,
+ request,
+ HttpMethod.GET,
+ FeiShuAppEmployeeResponse.class);
+ if (Objects.equals(employeeResponse.getCode(), 0)) {
+
userIds.addAll(employeeResponse.getData().getItems().stream().map(FeiShuAppEmployeeResponse.Employee::getUserId).toList());
+ var hasMore = employeeResponse.getData().getHasMore();
+ if (Boolean.TRUE.equals(hasMore)) {
+ getLarkEmployeeUserIds(accessToken,
employeeResponse.getData().getPageToken(), userIds);
+ }
+ }
+
+ }
+
+ private <R extends FeiShuAppResponse, E> R call(String url, HttpEntity<E>
request, HttpMethod httpMethod, Class<R> responseType) {
+ ResponseEntity<R> response = restTemplate.exchange(url, httpMethod,
request, responseType);
+ if (Objects.nonNull(response.getBody()) &&
!Objects.equals(response.getBody().getCode(), 0)) {
+ log.warn("Send FeiShu App Error: {}", response.getBody().getMsg());
+ throw new AlertNoticeException("Http StatusCode " +
response.getStatusCode() + " Error: " + response.getBody().getMsg());
+ }
+ return response.getBody();
+ }
+
+ private JsonNode createLarkMessage(NoticeReceiver receiver, String
notificationContent) throws JsonProcessingException {
+ String larkCardMessage = """
+ {
+ "schema": "2.0",
+ "config": {
+ "update_multi": true,
+ "locales": [
+ "en_us",
+ "zh_cn"
+ ],
+ "style": {
+ "text_size": {
+ "normal_v2": {
+ "default": "normal",
+ "pc": "normal",
+ "mobile": "heading"
+ }
+ }
+ }
+ },
+ "body": {
+ "direction": "vertical",
+ "padding": "12px 12px 12px 12px",
+ "elements": [
+ {
+ "tag": "markdown",
+ "content": "%s",
+ "i18n_content": {
+ "en_us": ""
+ },
+ "text_align": "left",
+ "text_size": "normal_v2",
+ "margin": "0px 0px 0px 0px"
+ },
+ {
+ "tag": "hr",
+ "margin": "0px 0px 0px 0px"
+ },
+ {
+ "tag": "column_set",
+ "horizontal_align": "left",
+ "columns": [
+ {
+ "tag": "column",
+ "width": "weighted",
+ "elements": [
+ {
+ "tag": "button",
+ "text": {
+ "tag": "plain_text",
+ "content": "登入控制台",
+ "i18n_content": {
+ "en_us": "Login In"
+ }
+ },
+ "type": "default",
+ "width": "default",
+ "size": "medium",
+ "behaviors": [
+ {
+ "type": "open_url",
+ "default_url": "%s",
+ "pc_url": "",
+ "ios_url": "",
+ "android_url": ""
+ }
+ ]
+ }
+ ],
+ "direction": "horizontal",
+ "vertical_spacing": "8px",
+ "horizontal_align": "left",
+ "vertical_align": "top",
+ "weight": 1
+ }
+ ],
+ "margin": "0px 0px 0px 0px"
+ }
+ ]
+ },
+ "header": {
+ "title": {
+ "tag": "plain_text",
+ "content": "HertzBeat 告警",
+ "i18n_content": {
+ "en_us": "HertzBeat Alarm"
+ }
+ },
+ "subtitle": {
+ "tag": "plain_text",
+ "content": ""
+ },
+ "template": "red",
+ "padding": "12px 12px 12px 12px"
+ }
+ }
+ """;
+ Byte larkReceiveIdType = receiver.getLarkReceiveType();
+ String userId = receiver.getUserId();
+ String atUserElement = "";
+ if (Objects.equals(larkReceiveIdType, CHAT_RECEIVE_TYPE) &&
StringUtils.isNotBlank(userId)) {
+ atUserElement = "\\n" + Arrays.stream(userId.split(","))
+ .map(id -> "<at id=" + id + "></at>")
+ .collect(Collectors.joining(" "));
+ }
+
+ if (notificationContent.startsWith("\"") &&
notificationContent.endsWith("\"")) {
+ notificationContent = StringUtils.removeStart(notificationContent,
"\"");
+ notificationContent = StringUtils.removeEnd(notificationContent,
"\"");
+ }
+ String jsonStr = String.format(larkCardMessage,
+ notificationContent.replace("\"", "\\\"") + atUserElement,
+ alerterProperties.getConsoleUrl());
+ return OBJECT_MAPPER.readTree(jsonStr);
+ }
+
+
+ private String escapedCompactJson(JsonNode json) throws
JsonProcessingException {
+ return OBJECT_MAPPER.writeValueAsString(json);
+ }
+
+ /**
+ * feiShu app response
+ */
+ @Data
+ protected static class FeiShuAppResponse {
+
+ private Integer code;
+
+ private String msg;
+
+ }
+
+ /**
+ * FeiShu app message get tenant access token request
+ */
+ @Data
+ @Builder
+ @AllArgsConstructor
+ @NoArgsConstructor
+ protected static class FeiShuAppAccessTokenDto {
+
+ @JsonProperty("app_id")
+ private String appId;
+
+ @JsonProperty("app_secret")
+ private String appSecret;
+ }
+
+
+ /**
+ * FeiShu app message get tenant access token response
+ */
+ @EqualsAndHashCode(callSuper = true)
+ @Data
+ protected static class FeiShuAppAccessTokenResponse extends
FeiShuAppResponse {
+
+ @JsonProperty("tenant_access_token")
+ private String tenantAccessToken;
+ }
+
+ /**
+ * FeiShu app employee response
+ */
+ @EqualsAndHashCode(callSuper = true)
+ @Data
+ protected static class FeiShuAppEmployeeResponse extends FeiShuAppResponse
{
+
+ private EmployeeResponseData data;
+
+ @Data
+ private static class EmployeeResponseData {
+ @JsonProperty("page_token")
+ private String pageToken;
+
+ @JsonProperty("has_more")
+ private Boolean hasMore;
+
+ private List<Employee> items;
+ }
+
+ @Data
+ private static class Employee {
+
+ @JsonProperty("user_id")
+ private String userId;
+ }
+ }
+
+
+ /**
+ * FeiShu app message request
+ */
+ @Data
+ @Builder
+ @AllArgsConstructor
+ @NoArgsConstructor
+ protected static class FeiShuAppMessageDto {
+
+ @JsonProperty("receive_id")
+ private String receiveId;
+
+ @Builder.Default
+ @JsonProperty("msg_type")
+ private String msgType = "interactive";
+
+ @JsonProperty("content")
+ private String content;
+
+ @Builder.Default
+ private final String uuid = UUID.randomUUID().toString();
+
+ }
+
+ /**
+ * FeiShu app batch message request
+ */
+ @Data
+ @Builder
+ @AllArgsConstructor
+ @NoArgsConstructor
+ protected static class FeiShuAppBatchMessageDto {
+
+ @Builder.Default
+ @JsonProperty("msg_type")
+ private final String msgType = "interactive";
+
+ private JsonNode card;
+
+ @JsonProperty("department_ids")
+ private String[] departmentIds;
+
+ @JsonProperty("user_ids")
+ private String[] userIds;
+
+ }
+
+
+}
diff --git
a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImplTest.java
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImplTest.java
new file mode 100644
index 0000000000..8ce2607188
--- /dev/null
+++
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImplTest.java
@@ -0,0 +1,333 @@
+/*
+ * 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 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.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ResourceBundle;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+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.when;
+
+/**
+ * Test case for FeiShu App Alert Notify
+ */
+@ExtendWith(MockitoExtension.class)
+class FeiShuAppAlertNotifyHandlerImplTest {
+
+ @Mock
+ private RestTemplate restTemplate;
+
+ @Mock
+ private ResourceBundle bundle;
+
+ @Mock
+ private AlerterProperties alerterProperties;
+
+ @InjectMocks
+ private FeiShuAppAlertNotifyHandlerImpl feiShuAppAlertNotifyHandler;
+
+ private NoticeReceiver receiver;
+ private GroupAlert groupAlert;
+ private NoticeTemplate template;
+
+ @BeforeEach
+ public void setUp() {
+ receiver = new NoticeReceiver();
+ receiver.setId(1L);
+ receiver.setName("test-receiver");
+ receiver.setType((byte) 14);
+ receiver.setAppId("cli-test-app-id");
+ receiver.setAppSecret("test-app-secret");
+
+ groupAlert = new GroupAlert();
+ SingleAlert singleAlert = new SingleAlert();
+ singleAlert.setLabels(new HashMap<>());
+ singleAlert.getLabels().put("severity", "critical");
+ singleAlert.getLabels().put("alertname", "Test Alert");
+
+ List<SingleAlert> alerts = new ArrayList<>();
+ alerts.add(singleAlert);
+ groupAlert.setAlerts(alerts);
+
+ template = new NoticeTemplate();
+ template.setId(1L);
+ template.setName("test-template");
+ template.setContent("test content");
+
+
lenient().when(bundle.getString("alerter.notify.title")).thenReturn("Alert
Notification");
+
lenient().when(alerterProperties.getConsoleUrl()).thenReturn("https://console.hertzbeat.com");
+ }
+
+ /**
+ * Test successful notification to single user
+ */
+ @Test
+ public void testNotifyAlertSuccessSingleUser() {
+ // Setup receiver for single user
+ receiver.setLarkReceiveType((byte) 0);
+ receiver.setUserId("user-001");
+
+ // Mock access token response
+ FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse
accessTokenResp =
+ new
FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse();
+ accessTokenResp.setCode(0);
+ accessTokenResp.setMsg("success");
+ accessTokenResp.setTenantAccessToken("test-access-token");
+
+ // Mock message send response
+ FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse messageResp =
+ new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse();
+ messageResp.setCode(0);
+ messageResp.setMsg("success");
+
+ // Mock restTemplate calls
+ when(restTemplate.exchange(
+ anyString(),
+ eq(org.springframework.http.HttpMethod.POST),
+ any(),
+
eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse.class)))
+ .thenReturn(new ResponseEntity<>(accessTokenResp,
HttpStatus.OK));
+
+ when(restTemplate.exchange(
+ anyString(),
+ eq(org.springframework.http.HttpMethod.POST),
+ any(),
+ eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse.class)))
+ .thenReturn(new ResponseEntity<>(messageResp, HttpStatus.OK));
+ feiShuAppAlertNotifyHandler.send(receiver, template, groupAlert);
+ }
+
+ /**
+ * Test successful notification to multiple users
+ */
+ @Test
+ public void testNotifyAlertSuccessMultipleUsers() {
+ receiver.setLarkReceiveType((byte) 0);
+ receiver.setUserId("user-001,user-002,user-003");
+
+ FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse
accessTokenResp =
+ new
FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse();
+ accessTokenResp.setCode(0);
+ accessTokenResp.setMsg("success");
+ accessTokenResp.setTenantAccessToken("test-access-token");
+
+ FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse messageResp =
+ new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse();
+ messageResp.setCode(0);
+ messageResp.setMsg("success");
+
+ when(restTemplate.exchange(
+ anyString(),
+ eq(org.springframework.http.HttpMethod.POST),
+ any(),
+
eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse.class)))
+ .thenReturn(new ResponseEntity<>(accessTokenResp,
HttpStatus.OK));
+
+ when(restTemplate.exchange(
+ anyString(),
+ eq(org.springframework.http.HttpMethod.POST),
+ any(),
+ eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse.class)))
+ .thenReturn(new ResponseEntity<>(messageResp, HttpStatus.OK));
+
+ feiShuAppAlertNotifyHandler.send(receiver, template, groupAlert);
+ }
+
+ /**
+ * Test successful notification to chat
+ */
+ @Test
+ public void testNotifyAlertSuccessChat() {
+ receiver.setLarkReceiveType((byte) 1);
+ receiver.setChatId("chat-001");
+
+ FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse
accessTokenResp =
+ new
FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse();
+ accessTokenResp.setCode(0);
+ accessTokenResp.setMsg("success");
+ accessTokenResp.setTenantAccessToken("test-access-token");
+
+ FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse messageResp =
+ new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse();
+ messageResp.setCode(0);
+ messageResp.setMsg("success");
+
+ when(restTemplate.exchange(
+ anyString(),
+ eq(org.springframework.http.HttpMethod.POST),
+ any(),
+
eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse.class)))
+ .thenReturn(new ResponseEntity<>(accessTokenResp,
HttpStatus.OK));
+
+ when(restTemplate.exchange(
+ anyString(),
+ eq(org.springframework.http.HttpMethod.POST),
+ any(),
+ eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse.class)))
+ .thenReturn(new ResponseEntity<>(messageResp, HttpStatus.OK));
+
+ feiShuAppAlertNotifyHandler.send(receiver, template, groupAlert);
+ }
+
+ /**
+ * Test successful notification to departments
+ */
+ @Test
+ public void testNotifyAlertSuccessDepartments() {
+ receiver.setLarkReceiveType((byte) 2);
+ receiver.setPartyId("dept-001,dept-002");
+
+ FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse
accessTokenResp =
+ new
FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse();
+ accessTokenResp.setCode(0);
+ accessTokenResp.setMsg("success");
+ accessTokenResp.setTenantAccessToken("test-access-token");
+
+ FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse messageResp =
+ new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse();
+ messageResp.setCode(0);
+ messageResp.setMsg("success");
+
+ when(restTemplate.exchange(
+ anyString(),
+ eq(org.springframework.http.HttpMethod.POST),
+ any(),
+
eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse.class)))
+ .thenReturn(new ResponseEntity<>(accessTokenResp,
HttpStatus.OK));
+
+ when(restTemplate.exchange(
+ anyString(),
+ eq(org.springframework.http.HttpMethod.POST),
+ any(),
+ eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse.class)))
+ .thenReturn(new ResponseEntity<>(messageResp, HttpStatus.OK));
+
+ feiShuAppAlertNotifyHandler.send(receiver, template, groupAlert);
+ }
+
+ /**
+ * Test notification failure due to access token error
+ */
+ @Test
+ public void testNotifyAlertFailureAccessToken() {
+ receiver.setLarkReceiveType((byte) 0);
+ receiver.setUserId("user-001");
+
+ FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse
accessTokenResp =
+ new
FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse();
+ accessTokenResp.setCode(999);
+ accessTokenResp.setMsg("Invalid app credentials");
+
+ when(restTemplate.exchange(
+ anyString(),
+ eq(org.springframework.http.HttpMethod.POST),
+ any(),
+
eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse.class)))
+ .thenReturn(new ResponseEntity<>(accessTokenResp,
HttpStatus.OK));
+
+ assertThrows(AlertNoticeException.class, () -> {
+ feiShuAppAlertNotifyHandler.send(receiver, template, groupAlert);
+ });
+ }
+
+ /**
+ * Test notification failure due to message send error
+ */
+ @Test
+ public void testNotifyAlertFailureMessageSend() {
+ receiver.setLarkReceiveType((byte) 0);
+ receiver.setUserId("user-001");
+
+ FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse
accessTokenResp =
+ new
FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse();
+ accessTokenResp.setCode(0);
+ accessTokenResp.setMsg("success");
+ accessTokenResp.setTenantAccessToken("test-access-token");
+
+ FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse messageResp =
+ new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse();
+ messageResp.setCode(999);
+ messageResp.setMsg("User not found");
+
+ when(restTemplate.exchange(
+ anyString(),
+ eq(org.springframework.http.HttpMethod.POST),
+ any(),
+
eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse.class)))
+ .thenReturn(new ResponseEntity<>(accessTokenResp,
HttpStatus.OK));
+
+ when(restTemplate.exchange(
+ anyString(),
+ eq(org.springframework.http.HttpMethod.POST),
+ any(),
+ eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse.class)))
+ .thenReturn(new ResponseEntity<>(messageResp, HttpStatus.OK));
+
+ assertThrows(AlertNoticeException.class, () -> {
+ feiShuAppAlertNotifyHandler.send(receiver, template, groupAlert);
+ });
+ }
+
+ /**
+ * Test invalid larkReceiveType
+ */
+ @Test
+ public void testInvalidLarkReceiveType() {
+ receiver.setLarkReceiveType((byte) 99);
+ receiver.setUserId("user-001");
+
+ FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse
accessTokenResp =
+ new
FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse();
+ accessTokenResp.setCode(0);
+ accessTokenResp.setMsg("success");
+ accessTokenResp.setTenantAccessToken("test-access-token");
+
+ when(restTemplate.exchange(
+ anyString(),
+ eq(org.springframework.http.HttpMethod.POST),
+ any(),
+
eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse.class)))
+ .thenReturn(new ResponseEntity<>(accessTokenResp,
HttpStatus.OK));
+
+ assertThrows(AlertNoticeException.class, () -> {
+ feiShuAppAlertNotifyHandler.send(receiver, template, groupAlert);
+ });
+ }
+}
diff --git
a/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/alerter/NoticeReceiver.java
b/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/alerter/NoticeReceiver.java
index a33c6b9d1b..262ac733f5 100644
---
a/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/alerter/NoticeReceiver.java
+++
b/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/alerter/NoticeReceiver.java
@@ -68,12 +68,13 @@ public class NoticeReceiver {
private String name;
@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",
+ + "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",
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",
+ + "WeChat app message 11-Slack 12-Discord 13-Gotify
14-FeiShu app message",
accessMode = READ_WRITE)
@Min(0)
@NotNull(message = "type can not null")
@@ -119,6 +120,12 @@ public class NoticeReceiver {
@Column(length = 300)
private String wechatId;
+ @Schema(title = "FeiShu app id : The notification method is valid for
FeiShu app message",
+ description = "FeiShu app id : The notification method is valid
for FeiShu app message",
+ example = "34823984635647", accessMode = READ_WRITE)
+ @Size(max = 255)
+ private String appId;
+
@Schema(title = "Access token : The notification method is valid for
DingTalk robot",
description = "Access token : The notification method is valid for
DingTalk robot",
example = "34823984635647", accessMode = READ_WRITE)
@@ -141,11 +148,21 @@ public class NoticeReceiver {
example = "779294123", accessMode = READ_WRITE)
private String tgMessageThreadId;
+ @Schema(title = "FeiShu app message receiveType: 0-user 1-chat 2-party
3-all",
+ description = "FeiShu app message receiveType: 0-user 1-chat
2-party 3-all",
+ example = "1", accessMode = READ_WRITE)
+ private Byte larkReceiveType;
+
@Schema(title = "DingTalk,FeiShu,WeWork user id: The notification method
is valid for DingTalk,FeiShu,WeWork Bot",
description = "DingTalk,FeiShu,WeWork user id: The notification
method is valid for DingTalk,FeiShu,WeWork Bot",
example = "779294123", accessMode = READ_WRITE)
private String userId;
+ @Schema(title = "FeiShu app message chatId: The notification method is
valid for FeiShu app message",
+ description = "FeiShu app message chatId: The notification method
is valid for FeiShu app message",
+ example = "779294123", accessMode = READ_WRITE)
+ private String chatId;
+
@Schema(title = "URL address: The notification method is valid for Slack",
description = "URL address: The notification method is valid for
Slack",
example = "https://hooks.slack.com/services/XXXX/XXXX/XXXX",
accessMode = READ_WRITE)
diff --git
a/hertzbeat-manager/src/main/resources/templates/14-FeiShuAppTemplate.txt
b/hertzbeat-manager/src/main/resources/templates/14-FeiShuAppTemplate.txt
new file mode 100644
index 0000000000..4f9e8deed1
--- /dev/null
+++ b/hertzbeat-manager/src/main/resources/templates/14-FeiShuAppTemplate.txt
@@ -0,0 +1,47 @@
+## 🔔 HertzBeat Alert Notification
+
+### Alert Summary
+> - Status: ${status}
+<#if commonLabels??>
+<#if commonLabels.severity??>
+> - Severity: ${commonLabels.severity?switch("critical", "❤️ Critical",
"warning", "💛 Warning", "info", "💚 Info", "Unknown")}
+</#if>
+<#if commonLabels.alertname??>
+> - Alert Name: ${commonLabels.alertname}
+</#if>
+</#if>
+
+### Alert Details
+<#list alerts as alert>
+#### Alert ${alert?index + 1}
+<#if alert.labels??>
+<#list alert.labels?keys as key>
+> - ${key}: ${alert.labels[key]}
+</#list>
+</#if>
+<#if alert.content?? && alert.content != "">
+> - Content: ${alert.content}
+</#if>
+> - Trigger Count: ${alert.triggerTimes}
+> - Start Time: ${(alert.startAt?number_to_datetime)?string('yyyy-MM-dd
HH:mm:ss')}
+<#if alert.activeAt??>
+> - Active Time: ${(alert.activeAt?number_to_datetime)?string('yyyy-MM-dd
HH:mm:ss')}
+</#if>
+<#if alert.endAt??>
+> - End Time: ${(alert.endAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')}
+</#if>
+
+<#if alert.annotations?? && alert.annotations?size gt 0>
+##### Additional Information
+<#list alert.annotations?keys as key>
+> - ${key}: ${alert.annotations[key]}
+</#list>
+</#if>
+</#list>
+
+<#if commonAnnotations?? && commonAnnotations?size gt 0>
+### Common Information
+<#list commonAnnotations?keys as key>
+> - ${key}: ${commonAnnotations[key]}
+</#list>
+</#if>
diff --git a/home/docs/help/alert_feishu_app.md
b/home/docs/help/alert_feishu_app.md
new file mode 100644
index 0000000000..fbba885011
--- /dev/null
+++ b/home/docs/help/alert_feishu_app.md
@@ -0,0 +1,75 @@
+---
+id: alert_feishu_app
+title: Alert FeiShu app notification
+sidebar_label: Alert FeiShu app notification
+keywords: [Alert FeiShu app notification , open source alerter, open source
feishu app notification]
+---
+
+> After the threshold is triggered send alarm information and notify the
recipient by FeiShu app.
+
+### Operation steps
+
+1. **【[FeiShu Open Platform](https://open.feishu.cn/)】->【Create Custom
App】->【Create】->【Add Features:Bot】**
+
+2. **【Development Configuration】->【Permissions & Scopes】:Different types of
notification objects require different API permissions, which can be opened as
needed**
+
+ | Permission Name |
Permission code | Designated User | Designated Group Chat |
Designated Department | All User |
+
|-----------------------------------------------------------------|-----------------------------------|:---------------:|:---------------------:|:---------------------:|:--------:|
+ | Obtain user ID |
contact:user.employee_id:readonly | ✓ | |
| ✓ |
+ | Send messages as an app |
im:message:send_as_bot | ✓ | ✓ |
✓ | ✓ |
+ | Obtain employee information in FeiShu CoreHR (Standard version) |
ehr:employee:readonly | | |
| ✓ |
+ | Send batch messages to members from one or more departments |
im:message:send_multi_depts | | |
✓ | |
+ | Send batch messages to multiple users |
im:message:send_multi_users | | |
| ✓ |
+
+ > Attention⚠️:In the designated notification type of group chat,the
application needs to be added as a robot to the group chat, to @someone, you
need to enable the permission to 'obtain user ID'
+ >
+ > Batch import scopes
+ >
+ > ```json
+ > {
+ > "scopes": {
+ > "tenant": [
+ > "contact:user.employee_id:readonly",
+ > "im:message:send_as_bot",
+ > "ehr:employee:readonly",
+ > "im:message:send_multi_depts",
+ > "im:message:send_multi_users"
+ > ],
+ > "user": []
+ > }
+ > }
+ > ```
+
+3. **【App Versions】->【Version Management & Release】->【Create a
version】->【Save】->【Publish】**
+
+4. **【Basic Info】->【Credentials & Basic Info】->【copy and save AppID and
AppSecret】**
+
+5. **【Notice Receiver】->【New Receiver】 ->【Choose FeiShu App method】->【Set
AppID and AppSecret】-> 【Select Notice Object Type】-> 【Set the corresponding
ID】**
+
+6. **Configure the associated alarm notification strategy⚠️ 【Add new
notification strategy】-> 【Associate the recipient just set】-> 【Confirm】**
+
+ > **Note⚠️ Adding a new recipient does not mean that it is effective to
receive alarm information. It is also necessary to configure the associated
alarm notification strategy, that is, to specify which messages are sent to
which recipients.**
+
+ 
+
+### FeiShu app notification common issues
+
+1. FeiShu app did not receive the robot alarm notification.
+
+ > Please check whether there is any triggered alarm information in the
alarm center.
+ > Please check whether the AppID and AppSecret is configured correctly and
whether the alarm strategy association is configured.
+ > Please check if the user was within the available range when the
application was published.
+
+2. How to @someone in a designated group chat
+
+ > In the form for adding recipients, fill in the `User ID`. If you need to
@everyone, you can enter `all` in the `User ID` field. Multiple user IDs are
also supported, separated by commas `,`. For detailed instructions on how to
get the FeiShu user ID, please refer to: [Get FeiShu user
id](https://open.feishu.cn/document/faq/trouble-shooting/how-to-obtain-user-id#529e21a9)
+
+3. How to obtain a chat ID
+
+ > Please refer to: [Chat ID
description](https://open.feishu.cn/document/server-docs/group/chat/chat-id-description#394516c9)
+
+4. How to obtain party ID
+
+ > Please refer to: [Department resource
introduction](https://open.feishu.cn/document/server-docs/contact-v3/department/field-overview#9c02ed7a)
+
+Other issues can be fed back through the communication group ISSUE!
diff --git
a/home/i18n/zh-cn/docusaurus-plugin-content-docs/current/help/alert_feishu_app.md
b/home/i18n/zh-cn/docusaurus-plugin-content-docs/current/help/alert_feishu_app.md
new file mode 100644
index 0000000000..3c3c249232
--- /dev/null
+++
b/home/i18n/zh-cn/docusaurus-plugin-content-docs/current/help/alert_feishu_app.md
@@ -0,0 +1,75 @@
+---
+id: alert_feishu_app
+title: 告警飞书自建应用通知
+sidebar_label: 告警飞书自建应用通知
+keywords: [告警飞书自建应用通知 , 开源告警系统, 开源监控告警系统]
+---
+
+> 阈值触发后发送告警信息,通过飞书自建应用通知到接收人。
+
+### 操作步骤
+
+1. **【[飞书开放平台](https://open.feishu.cn/)】->【创建企业自建应用】->【创建】->【添加应用能力:机器人】**
+
+2. **【开发配置】->【权限配置】:不同的通知对象类型所需要的API权限不同,可按需开通**
+
+ | 权限名称 | 权限代码 | 指定用户 | 指定群聊
| 指定部门 | 所有用户 |
+
|------------------------|-----------------------------------|:----:|:----:|:----:|:----:|
+ | 获取用户 user ID | contact:user.employee_id:readonly | ✓ |
| | ✓ |
+ | 以应用的身份发消息 | im:message:send_as_bot | ✓ | ✓
| ✓ | ✓ |
+ | 获取飞书人事(标准版)应用中的员工花名册信息 | ehr:employee:readonly | |
| | ✓ |
+ | 给一个或多个部门的成员批量发消息 | im:message:send_multi_depts | |
| ✓ | |
+ | 给多个用户批量发消息 | im:message:send_multi_users | |
| | ✓ |
+
+ > 注意⚠️:在指定群聊的通知类型中,应用需作为机器人添加进群聊里,若要@某人需要开通`获取用户 user ID`权限
+ >
+ > 批量导入权限
+ >
+ > ```json
+ > {
+ > "scopes": {
+ > "tenant": [
+ > "contact:user.employee_id:readonly",
+ > "im:message:send_as_bot",
+ > "ehr:employee:readonly",
+ > "im:message:send_multi_depts",
+ > "im:message:send_multi_users"
+ > ],
+ > "user": []
+ > }
+ > }
+ > ```
+
+3. **【应用发布】->【版本发布与管理】->【创建版本】->【保存】->【发布】**
+
+4. **【基础信息】->【凭证与基础信息】->【复制保存App ID和App Secret】**
+
+5. **【告警通知】->【新增接收人】 ->【选择飞书自建应用通知方式】->【设置应用ID、应用secret】-> 【选择通知对象类型】->
【设置对应的ID】**
+
+6. **配置关联的告警通知策略⚠️ 【新增通知策略】-> 【将刚设置的接收人关联】-> 【确定】**
+
+ > **注意⚠️ 新增了接收人并不代表已经生效可以接收告警信息,还需配置关联的告警通知策略,即指定哪些消息发给哪些接收人**。
+
+ 
+
+### 飞书自建应用通知常见问题
+
+1. 飞书未收到告警通知
+
+ > 请排查在告警中心是否已有触发的告警信息
+ > 请排查是否配置正确App ID和App Secret,是否已配置告警策略关联
+ > 请排查应用发布时该用户是否在可用范围内
+
+2. 如何在指定群聊中@某人
+
+ > 在新增接收人的表单中,填写 `用户ID` 。如果需要 @所有人,可以在 `用户ID` 字段中填入 `all`。同时支持填写多个用户id,用逗号
`,` 分隔。获取飞书用户id的具体方法,请参考:[如何获取用户的 User
ID](https://open.feishu.cn/document/faq/trouble-shooting/how-to-obtain-user-id#529e21a9)
+
+3. 如何获取群聊ID
+
+
请参考:[群ID获取方式](https://open.feishu.cn/document/server-docs/group/chat/chat-id-description#394516c9)
+
+4. 如何获取部门ID
+
+
请参考:[部门资源介绍](https://open.feishu.cn/document/server-docs/contact-v3/department/field-overview#9c02ed7a)
+
+其它问题可以通过交流群ISSUE反馈哦!
diff --git a/home/sidebars.json b/home/sidebars.json
index ed742d35d8..174b58769c 100755
--- a/home/sidebars.json
+++ b/home/sidebars.json
@@ -113,6 +113,7 @@
"help/alert_wework",
"help/alert_dingtalk",
"help/alert_feishu",
+ "help/alert_feishu_app",
"help/alert_console",
"help/alert_enterprise_wechat_app",
"help/alert_smn",
diff --git a/web-app/src/app/pojo/NoticeReceiver.ts
b/web-app/src/app/pojo/NoticeReceiver.ts
index cbd38ab280..d63372b363 100644
--- a/web-app/src/app/pojo/NoticeReceiver.ts
+++ b/web-app/src/app/pojo/NoticeReceiver.ts
@@ -53,4 +53,7 @@ export class NoticeReceiver {
modifier!: string;
gmtCreate!: number;
gmtUpdate!: number;
+ appId!: string;
+ larkReceiveType!: number;
+ chatId!: string;
}
diff --git
a/web-app/src/app/routes/alert/alert-notice/alert-notice-receiver/alert-notice-receiver.component.html
b/web-app/src/app/routes/alert/alert-notice/alert-notice-receiver/alert-notice-receiver.component.html
index 374199b60d..abdb11330e 100644
---
a/web-app/src/app/routes/alert/alert-notice/alert-notice-receiver/alert-notice-receiver.component.html
+++
b/web-app/src/app/routes/alert/alert-notice/alert-notice-receiver/alert-notice-receiver.component.html
@@ -127,6 +127,10 @@
<i nz-icon nzTheme="outline" nzType="notification"></i>
<span>{{ 'alert.notice.type.gotify' | i18n }}</span>
</nz-tag>
+ <nz-tag *ngIf="data.type == 14" nzColor="orange">
+ <i nz-icon nzTheme="outline" nzType="notification"></i>
+ <span>{{ 'alert.notice.type.lark-app' | i18n }}</span>
+ </nz-tag>
</td>
<td nzAlign="center">
<span *ngIf="data.type == 0">{{ data.phone }}</span>
@@ -143,6 +147,7 @@
<span *ngIf="data.type == 11">{{ data.smnAk }}</span>
<span *ngIf="data.type == 12">{{ data.serverChanToken }}</span>
<span *ngIf="data.type == 13">{{ data.gotifyToken }}</span>
+ <span *ngIf="data.type == 14">{{ data.appId }}</span>
</td>
<td nzAlign="center">{{ (data.gmtUpdate ? data.gmtUpdate :
data.gmtCreate) | date : 'YYYY-MM-dd HH:mm:ss' }}</td>
<td nzAlign="center" nzRight>
@@ -224,6 +229,7 @@
<nz-option [nzLabel]="'alert.notice.type.smn' | i18n"
[nzValue]="11"></nz-option>
<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-select>
</nz-form-control>
</nz-form-item>
@@ -522,6 +528,74 @@
<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 === 14">
+ <nz-form-label [nzRequired]="receiver.type === 14" [nzSpan]="7"
nzFor="appId">
+ {{ 'alert.notice.type.lark-app-appId' | i18n }}
+ </nz-form-label>
+ <nz-form-control [nzErrorTip]="'validation.required' | i18n"
[nzSpan]="12">
+ <input [(ngModel)]="receiver.appId" [required]="receiver.type ===
14" name="appId" nz-input type="text" />
+ </nz-form-control>
+ </nz-form-item>
+ <nz-form-item *ngIf="receiver.type === 14">
+ <nz-form-label [nzRequired]="receiver.type === 14" [nzSpan]="7"
nzFor="appSecret">
+ {{ 'alert.notice.type.lark-app-appSecret' | i18n }}
+ </nz-form-label>
+ <nz-form-control [nzErrorTip]="'validation.required' | i18n"
[nzSpan]="12">
+ <input [(ngModel)]="receiver.appSecret" [required]="receiver.type
=== 14" name="appSecret" nz-input type="text" />
+ </nz-form-control>
+ </nz-form-item>
+ <nz-form-item *ngIf="receiver.type === 14">
+ <nz-form-label nzFor="larkReceiveType" nzRequired="true" nzSpan="7"
+ >{{ 'alert.notice.type.lark-app-larkReceiveType' | i18n }}
+ </nz-form-label>
+ <nz-form-control [nzErrorTip]="'validation.required' | i18n"
nzSpan="12">
+ <nz-select
+ [(ngModel)]="receiver.larkReceiveType"
+ [nzOptionOverflowSize]="10"
+ id="larkReceiveType"
+ name="larkReceiveType"
+
[nzPlaceHolder]="'alert.notice.type.lark-app-larkReceiveType.placeholder' |
i18n"
+ required
+ >
+ <nz-option
[nzLabel]="'alert.notice.type.lark-app-larkReceiveType.user' | i18n"
[nzValue]="0"></nz-option>
+ <nz-option
[nzLabel]="'alert.notice.type.lark-app-larkReceiveType.chat' | i18n"
[nzValue]="1"></nz-option>
+ <nz-option
[nzLabel]="'alert.notice.type.lark-app-larkReceiveType.party' | i18n"
[nzValue]="2"></nz-option>
+ <nz-option
[nzLabel]="'alert.notice.type.lark-app-larkReceiveType.all' | i18n"
[nzValue]="3"></nz-option>
+ </nz-select>
+ </nz-form-control>
+ </nz-form-item>
+ <nz-form-item *ngIf="receiver.larkReceiveType === 0">
+ <nz-form-label [nzRequired]="receiver.larkReceiveType === 0"
[nzSpan]="7" nzFor="userId">
+ {{ 'alert.notice.type.lark-app-userId' | i18n }}
+ </nz-form-label>
+ <nz-form-control [nzErrorTip]="'validation.required' | i18n"
[nzSpan]="12">
+ <input [(ngModel)]="receiver.userId"
[required]="receiver.larkReceiveType === 0" name="userId" nz-input type="text"
/>
+ </nz-form-control>
+ </nz-form-item>
+ <nz-form-item *ngIf="receiver.larkReceiveType === 1">
+ <nz-form-label [nzRequired]="receiver.larkReceiveType === 1"
[nzSpan]="7" nzFor="chatId">
+ {{ 'alert.notice.type.lark-app-chatId' | i18n }}
+ </nz-form-label>
+ <nz-form-control [nzErrorTip]="'validation.required' | i18n"
[nzSpan]="12">
+ <input [(ngModel)]="receiver.chatId"
[required]="receiver.larkReceiveType === 1" name="chatId" nz-input type="text"
/>
+ </nz-form-control>
+ </nz-form-item>
+ <nz-form-item *ngIf="receiver.larkReceiveType === 1">
+ <nz-form-label [nzSpan]="7" nzFor="userId">
+ {{ 'alert.notice.type.lark-app-userId' | i18n }}
+ </nz-form-label>
+ <nz-form-control [nzSpan]="12">
+ <input [(ngModel)]="receiver.userId" name="userId" nz-input
type="text" />
+ </nz-form-control>
+ </nz-form-item>
+ <nz-form-item *ngIf="receiver.larkReceiveType === 2">
+ <nz-form-label [nzRequired]="receiver.larkReceiveType === 2"
[nzSpan]="7" nzFor="partyId">
+ {{ 'alert.notice.type.lark-app-partyId' | i18n }}
+ </nz-form-label>
+ <nz-form-control [nzErrorTip]="'validation.required' | i18n"
[nzSpan]="12">
+ <input [(ngModel)]="receiver.partyId"
[required]="receiver.larkReceiveType === 2" name="partyId" nz-input type="text"
/>
+ </nz-form-control>
+ </nz-form-item>
<nz-form-item>
<nz-form-control [nzOffset]="7" [nzSpan]="12">
<button (click)="onSendAlertTestMsg()"
[nzLoading]="isSendTestButtonLoading" nz-button nzDanger>
diff --git
a/web-app/src/app/routes/alert/alert-notice/alert-notice-rule/alert-notice-rule.component.ts
b/web-app/src/app/routes/alert/alert-notice/alert-notice-rule/alert-notice-rule.component.ts
index 49c6a683a1..f1e337af19 100644
---
a/web-app/src/app/routes/alert/alert-notice/alert-notice-rule/alert-notice-rule.component.ts
+++
b/web-app/src/app/routes/alert/alert-notice/alert-notice-rule/alert-notice-rule.component.ts
@@ -275,6 +275,9 @@ export class AlertNoticeRuleComponent implements OnInit {
case 12:
label = `${label}ServerChan`;
break;
+ case 14:
+ label = `${label}FeiShuApp`;
+ break;
}
this.receiversOption.push({
value: item.id,
diff --git
a/web-app/src/app/routes/alert/alert-notice/alert-notice-template/alert-notice-template.component.html
b/web-app/src/app/routes/alert/alert-notice/alert-notice-template/alert-notice-template.component.html
index f20fe0b35b..c5ca96413c 100644
---
a/web-app/src/app/routes/alert/alert-notice/alert-notice-template/alert-notice-template.component.html
+++
b/web-app/src/app/routes/alert/alert-notice/alert-notice-template/alert-notice-template.component.html
@@ -117,6 +117,9 @@
<nz-tag *ngIf="data.type == 13" nzColor="orange">
<span>{{ 'alert.notice.type.gotify' | i18n }}</span>
</nz-tag>
+ <nz-tag *ngIf="data.type == 14" nzColor="orange">
+ <span>{{ 'alert.notice.type.lark-app' | i18n }}</span>
+ </nz-tag>
</span>
</td>
<td nzAlign="center">
@@ -212,6 +215,7 @@
<nz-option [nzLabel]="'alert.notice.type.smn' | i18n"
[nzValue]="11"></nz-option>
<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-select>
</nz-form-control>
</nz-form-item>
diff --git a/web-app/src/assets/i18n/en-US.json
b/web-app/src/assets/i18n/en-US.json
index e82d6992b2..0e4e09a42c 100644
--- a/web-app/src/assets/i18n/en-US.json
+++ b/web-app/src/assets/i18n/en-US.json
@@ -203,6 +203,18 @@
"alert.notice.type.email": "Email",
"alert.notice.type.fei-shu": "FeiShu Robot",
"alert.notice.type.fei-shu-key": "FeiShu Robot KEY",
+ "alert.notice.type.lark-app": "FeiShu App",
+ "alert.notice.type.lark-app-appId": "App Id",
+ "alert.notice.type.lark-app-appSecret": "App Secret",
+ "alert.notice.type.lark-app-larkReceiveType": "Notice Object Type",
+ "alert.notice.type.lark-app-larkReceiveType.placeholder": "Select FeiShu App
Notice Object Type",
+ "alert.notice.type.lark-app-larkReceiveType.user": "Designated User Id",
+ "alert.notice.type.lark-app-larkReceiveType.party": "Designated Party Id",
+ "alert.notice.type.lark-app-larkReceiveType.chat": "Designated Chat Id",
+ "alert.notice.type.lark-app-larkReceiveType.all": "All User",
+ "alert.notice.type.lark-app-userId": "User Id(separated by , symbol)",
+ "alert.notice.type.lark-app-partyId": "Party Id(separated by , symbol)",
+ "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.phone": "Phone",
diff --git a/web-app/src/assets/i18n/zh-CN.json
b/web-app/src/assets/i18n/zh-CN.json
index e1f531b12c..31a7d4bcfc 100644
--- a/web-app/src/assets/i18n/zh-CN.json
+++ b/web-app/src/assets/i18n/zh-CN.json
@@ -203,6 +203,18 @@
"alert.notice.type.email": "邮箱",
"alert.notice.type.fei-shu": "飞书机器人",
"alert.notice.type.fei-shu-key": "飞书机器人KEY",
+ "alert.notice.type.lark-app": "飞书自建应用",
+ "alert.notice.type.lark-app-appId": "应用ID",
+ "alert.notice.type.lark-app-appSecret": "应用secret",
+ "alert.notice.type.lark-app-larkReceiveType": "通知对象类型",
+ "alert.notice.type.lark-app-larkReceiveType.placeholder": "选择通知对象类型",
+ "alert.notice.type.lark-app-larkReceiveType.user": "指定用户",
+ "alert.notice.type.lark-app-larkReceiveType.party": "指定部门",
+ "alert.notice.type.lark-app-larkReceiveType.chat": "指定群聊",
+ "alert.notice.type.lark-app-larkReceiveType.all": "所有用户",
+ "alert.notice.type.lark-app-userId": "用户id(多个使用,符号分隔)",
+ "alert.notice.type.lark-app-partyId": "部门id(多个使用,符号分隔)",
+ "alert.notice.type.lark-app-chatId": "群聊id(多个使用,符号分隔)",
"alert.notice.type.gotify": "Gotify",
"alert.notice.type.gotify-token": "Gotify Token",
"alert.notice.type.phone": "手机号",
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]