Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String, NotifierService> notifierMap;

public NotifierFactory(List<NotifierService> 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;
}
}

Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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 + "&timestamp=" + 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);
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}

Loading
Loading