diff --git a/data-agent-management/src/main/java/com/alibaba/cloud/ai/dataagent/service/notify/NotificationInfo.java b/data-agent-management/src/main/java/com/alibaba/cloud/ai/dataagent/service/notify/NotificationInfo.java new file mode 100644 index 000000000..d113e22f3 --- /dev/null +++ b/data-agent-management/src/main/java/com/alibaba/cloud/ai/dataagent/service/notify/NotificationInfo.java @@ -0,0 +1,82 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed 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 + * + * https://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 com.alibaba.cloud.ai.dataagent.service.notify; + +import java.time.LocalDateTime; + +public class NotificationInfo { + + private String nodeName; + + private String status; + + private LocalDateTime timestamp; + + private String threadId; + + private String agentId; + + public NotificationInfo() { + } + + public NotificationInfo(String nodeName, String status, LocalDateTime timestamp, String threadId, String agentId) { + this.nodeName = nodeName; + this.status = status; + this.timestamp = timestamp; + this.threadId = threadId; + this.agentId = agentId; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public LocalDateTime getTimestamp() { + return timestamp; + } + + public void setTimestamp(LocalDateTime timestamp) { + this.timestamp = timestamp; + } + + public String getThreadId() { + return threadId; + } + + public void setThreadId(String threadId) { + this.threadId = threadId; + } + + public String getAgentId() { + return agentId; + } + + public void setAgentId(String agentId) { + this.agentId = agentId; + } + + public String getNodeName() { + return nodeName; + } + + public void setNodeName(String nodeName) { + this.nodeName = nodeName; + } +} diff --git a/data-agent-management/src/main/java/com/alibaba/cloud/ai/dataagent/service/notify/NotifierFactory.java b/data-agent-management/src/main/java/com/alibaba/cloud/ai/dataagent/service/notify/NotifierFactory.java new file mode 100644 index 000000000..ffb769127 --- /dev/null +++ b/data-agent-management/src/main/java/com/alibaba/cloud/ai/dataagent/service/notify/NotifierFactory.java @@ -0,0 +1,57 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed 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 + * + * https://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 com.alibaba.cloud.ai.dataagent.service.notify; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class NotifierFactory { + + @Value("${spring.ai.alibaba.data-agent.notify.channel:dingtalk}") + private String defaultChannel; + + private final Map notifierMap; + + public NotifierFactory(List notifierServices) { + this.notifierMap = notifierServices.stream() + .collect(Collectors.toMap(NotifierService::getName, Function.identity(), (a, b) -> a)); + } + + public NotifierService getByName(String name) { + NotifierService notifier = notifierMap.get(name); + if (notifier == null) { + throw new IllegalArgumentException("Notifier not found: " + name); + } + return notifier; + } + + public NotifierService getDefault() { + NotifierService notifier = notifierMap.get(defaultChannel); + if (notifier == null) { + throw new IllegalArgumentException("Default notifier not found for channel: " + defaultChannel); + } + return notifier; + } +} + diff --git a/data-agent-management/src/main/java/com/alibaba/cloud/ai/dataagent/service/notify/NotifierService.java b/data-agent-management/src/main/java/com/alibaba/cloud/ai/dataagent/service/notify/NotifierService.java new file mode 100644 index 000000000..63935bf9f --- /dev/null +++ b/data-agent-management/src/main/java/com/alibaba/cloud/ai/dataagent/service/notify/NotifierService.java @@ -0,0 +1,25 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed 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 + * + * https://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 com.alibaba.cloud.ai.dataagent.service.notify; + +public interface NotifierService { + + String getName(); + + void notify(NotificationInfo info); + + void notify(String message); +} diff --git a/data-agent-management/src/main/java/com/alibaba/cloud/ai/dataagent/service/notify/impl/DingTalkNotifier.java b/data-agent-management/src/main/java/com/alibaba/cloud/ai/dataagent/service/notify/impl/DingTalkNotifier.java new file mode 100644 index 000000000..433dcbac1 --- /dev/null +++ b/data-agent-management/src/main/java/com/alibaba/cloud/ai/dataagent/service/notify/impl/DingTalkNotifier.java @@ -0,0 +1,135 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed 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 + * + * https://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 com.alibaba.cloud.ai.dataagent.service.notify.impl; + +import com.alibaba.cloud.ai.dataagent.service.notify.NotificationInfo; +import com.alibaba.cloud.ai.dataagent.service.notify.NotifierService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Base64; + +@Slf4j +@Component +public class DingTalkNotifier implements NotifierService { + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + @Value("${spring.ai.alibaba.data-agent.notify.dingtalk.webhook-url:}") + private String webhookUrl; + + @Value("${spring.ai.alibaba.data-agent.notify.dingtalk.secret-key:}") + private String secretKey; + + @Override + public String getName() { + return "dingtalk"; + } + + @Override + public void notify(NotificationInfo info) { + if (webhookUrl == null || webhookUrl.isEmpty()) { + log.debug("DingTalk webhook-url is not configured, skipping notification"); + return; + } + + String markdownContent = buildMarkdownContent(info); + sendDingTalkMessage(markdownContent); + } + + public void notify(String message) { + if (webhookUrl == null || webhookUrl.isEmpty()) { + log.debug("DingTalk webhook-url is not configured, skipping notification"); + return; + } + + String markdownContent = """ + ## DataAgent 任务通知 + + """ + message; + sendDingTalkMessage(markdownContent); + } + + private String buildMarkdownContent(NotificationInfo info) { + String statusEmoji = "成功".equals(info.getStatus()) ? "✓" : "✗"; + return String.format(""" + ## DataAgent 任务通知 + + - **触发节点**: %s + - **状态**: %s %s + - **时间**: %s + """, info.getNodeName() != null ? info.getNodeName() : "N/A", + info.getStatus(), statusEmoji, + info.getTimestamp().format(FORMATTER)); + } + + private void sendDingTalkMessage(String markdownContent) { + try { + long timestamp = Instant.now().toEpochMilli(); + String sign = computeSign(timestamp, secretKey); + String urlWithSign = webhookUrl + "×tamp=" + timestamp + "&sign=" + URLEncoder.encode(sign, StandardCharsets.UTF_8); + + String requestBody = String.format(""" + { + "msgtype": "markdown", + "markdown": { + "title": "DataAgent 任务通知", + "text": %s + } + } + """, toJsonString(markdownContent)); + + String response = WebClient.create(urlWithSign) + .post() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(requestBody) + .retrieve() + .bodyToMono(String.class) + .block(); + + log.info("DingTalk response: {}", response); + } + catch (WebClientResponseException e) { + log.error("DingTalk API error: status={}, body={}", e.getStatusCode(), e.getResponseBodyAsString()); + } + catch (Exception e) { + log.error("Failed to send DingTalk notification", e); + } + } + + + private String toJsonString(String text) { + return "\"" + text.replace("\"", "\\\"").replace("\n", "\\n") + "\""; + } + + private String computeSign(long timestamp, String secretKey) throws Exception { + String stringToSign = timestamp + "\n" + secretKey; + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); + byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(signData); + } +} diff --git a/data-agent-management/src/test/java/com/alibaba/cloud/ai/dataagent/service/notify/NotificationInfoTest.java b/data-agent-management/src/test/java/com/alibaba/cloud/ai/dataagent/service/notify/NotificationInfoTest.java new file mode 100644 index 000000000..c0892fcdb --- /dev/null +++ b/data-agent-management/src/test/java/com/alibaba/cloud/ai/dataagent/service/notify/NotificationInfoTest.java @@ -0,0 +1,66 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed 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 + * + * https://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 com.alibaba.cloud.ai.dataagent.service.notify; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +class NotificationInfoTest { + + @Test + void testDefaultConstructor() { + NotificationInfo info = new NotificationInfo(); + assertNull(info.getNodeName()); + assertNull(info.getStatus()); + assertNull(info.getTimestamp()); + assertNull(info.getThreadId()); + assertNull(info.getAgentId()); + } + + @Test + void testAllArgsConstructor() { + LocalDateTime now = LocalDateTime.now(); + NotificationInfo info = new NotificationInfo("ReportGeneratorNode", "成功", now, "thread-123", "agent-456"); + + assertEquals("ReportGeneratorNode", info.getNodeName()); + assertEquals("成功", info.getStatus()); + assertEquals(now, info.getTimestamp()); + assertEquals("thread-123", info.getThreadId()); + assertEquals("agent-456", info.getAgentId()); + } + + @Test + void testSettersAndGetters() { + NotificationInfo info = new NotificationInfo(); + LocalDateTime now = LocalDateTime.now(); + + info.setNodeName("TestNode"); + info.setStatus("失败"); + info.setTimestamp(now); + info.setThreadId("thread-789"); + info.setAgentId("agent-101"); + + assertEquals("TestNode", info.getNodeName()); + assertEquals("失败", info.getStatus()); + assertEquals(now, info.getTimestamp()); + assertEquals("thread-789", info.getThreadId()); + assertEquals("agent-101", info.getAgentId()); + } +} + diff --git a/data-agent-management/src/test/java/com/alibaba/cloud/ai/dataagent/service/notify/impl/DingTalkNotifierTest.java b/data-agent-management/src/test/java/com/alibaba/cloud/ai/dataagent/service/notify/impl/DingTalkNotifierTest.java new file mode 100644 index 000000000..48dfb5c84 --- /dev/null +++ b/data-agent-management/src/test/java/com/alibaba/cloud/ai/dataagent/service/notify/impl/DingTalkNotifierTest.java @@ -0,0 +1,66 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed 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 + * + * https://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 com.alibaba.cloud.ai.dataagent.service.notify.impl; + +import com.alibaba.cloud.ai.dataagent.service.notify.NotificationInfo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +class DingTalkNotifierTest { + + private DingTalkNotifier notifier; + + @BeforeEach + void setUp() throws Exception { + notifier = new DingTalkNotifier(); + + // Use reflection to set @Value fields + setField(notifier, "webhookUrl", ""); + setField(notifier, "secretKey", ""); + } + + private void setField(Object target, String fieldName, Object value) throws Exception { + Field field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + } + + @Test + void testNotifyWhenNoWebhookUrl() throws Exception { + DingTalkNotifier notifierNoWebhook = new DingTalkNotifier(); + setField(notifierNoWebhook, "webhookUrl", ""); + + NotificationInfo info = new NotificationInfo("ReportGeneratorNode", "成功", LocalDateTime.now(), "t1", "a1"); + notifierNoWebhook.notify(info); + } + + @Test + void testNotifyBuildsCorrectContent() { + NotificationInfo info = new NotificationInfo("ReportGeneratorNode", "成功", LocalDateTime.now(), "thread-1", "agent-1"); + notifier.notify(info); + } + + @Test + void testNotifyStringMessage() { + notifier.notify("测试消息内容"); + } +} +