From 6940c5b6c5504ed0c6ec0a8f6ee19bd1a6428b42 Mon Sep 17 00:00:00 2001 From: tanglong <842690096@qq.com> Date: Tue, 21 Jul 2026 15:16:24 +0800 Subject: [PATCH] =?UTF-8?q?=E6=97=A5=E6=8A=A5=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration/wecom/client/WecomClient.java | 29 +++++++++- .../wecom/config/WecomAsyncConfig.java | 23 ++++++++ .../controller/WecomCallbackController.java | 34 ++++++++--- .../wecom/dto/WecomSendMessageRequest.java | 10 ++++ .../CustomerDailyReportIntentHandler.java | 57 +++++++++++-------- 5 files changed, 120 insertions(+), 33 deletions(-) create mode 100644 src/main/java/com/company/agentfoundation/integration/wecom/config/WecomAsyncConfig.java diff --git a/src/main/java/com/company/agentfoundation/integration/wecom/client/WecomClient.java b/src/main/java/com/company/agentfoundation/integration/wecom/client/WecomClient.java index 8150a9b..2f23021 100644 --- a/src/main/java/com/company/agentfoundation/integration/wecom/client/WecomClient.java +++ b/src/main/java/com/company/agentfoundation/integration/wecom/client/WecomClient.java @@ -42,13 +42,35 @@ public class WecomClient { FoundationProperties.Wecom.App app = app(appKey); log.info("wecom sendText start appKey={} toUser={} agentId={} contentPreview={}", appKey, toUser, app.getAgentId(), preview(content)); - String token = getAccessToken(app.getSecret()); WecomSendMessageRequest req = new WecomSendMessageRequest(); req.setToUser(toUser); req.setAgentId(app.getAgentId()); WecomSendMessageRequest.Text text = new WecomSendMessageRequest.Text(); text.setContent(content); req.setText(text); + sendMessage(appKey, toUser, req, "text"); + } + + public void sendMarkdown(String appKey, String toUser, String content) { + FoundationProperties.Wecom.App app = app(appKey); + String token = getAccessToken(app.getSecret()); + WecomSendMessageRequest req = new WecomSendMessageRequest(); + req.setToUser(toUser); + req.setAgentId(app.getAgentId()); + req.setMsgType("markdown"); + WecomSendMessageRequest.Markdown markdown = new WecomSendMessageRequest.Markdown(); + markdown.setContent(content); + req.setMarkdown(markdown); + sendMessage(appKey, toUser, req, "markdown", token); + } + + private void sendMessage(String appKey, String toUser, WecomSendMessageRequest req, String messageType) { + FoundationProperties.Wecom.App app = app(appKey); + sendMessage(appKey, toUser, req, messageType, getAccessToken(app.getSecret())); + } + + private void sendMessage(String appKey, String toUser, WecomSendMessageRequest req, String messageType, String token) { + log.info("wecom send message appKey={} type={} toUser={} contentReady=true", appKey, messageType, toUser); WecomApiResponse response = webClient.post() .uri(uriBuilder -> uriBuilder.path("/cgi-bin/message/send").queryParam("access_token", token).build()) @@ -59,10 +81,11 @@ public class WecomClient { .block(); if (response == null || !response.success()) { - log.error("wecom sendText failed toUser={} err={}", toUser, response == null ? "null" : response.errmsg()); + log.error("wecom send message failed appKey={} type={} toUser={} err={}", + appKey, messageType, toUser, response == null ? "null" : response.errmsg()); throw new IllegalStateException("WeCom send failed: " + (response == null ? "null" : response.errmsg())); } - log.info("wecom sendText success toUser={}", toUser); + log.info("wecom send message success appKey={} type={} toUser={}", appKey, messageType, toUser); } public WecomFollowUserListResponse getStudentContactFollowUsers() { diff --git a/src/main/java/com/company/agentfoundation/integration/wecom/config/WecomAsyncConfig.java b/src/main/java/com/company/agentfoundation/integration/wecom/config/WecomAsyncConfig.java new file mode 100644 index 0000000..6887556 --- /dev/null +++ b/src/main/java/com/company/agentfoundation/integration/wecom/config/WecomAsyncConfig.java @@ -0,0 +1,23 @@ +package com.company.agentfoundation.integration.wecom.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; + +@Configuration +public class WecomAsyncConfig { + @Bean("wecomTaskExecutor") + public Executor wecomTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(2); + executor.setMaxPoolSize(8); + executor.setQueueCapacity(200); + executor.setThreadNamePrefix("wecom-reply-"); + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.setAwaitTerminationSeconds(20); + executor.initialize(); + return executor; + } +} diff --git a/src/main/java/com/company/agentfoundation/integration/wecom/controller/WecomCallbackController.java b/src/main/java/com/company/agentfoundation/integration/wecom/controller/WecomCallbackController.java index 556e1ba..5fafebb 100644 --- a/src/main/java/com/company/agentfoundation/integration/wecom/controller/WecomCallbackController.java +++ b/src/main/java/com/company/agentfoundation/integration/wecom/controller/WecomCallbackController.java @@ -10,6 +10,7 @@ import com.company.agentfoundation.semantic.SemanticDispatchService; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -21,6 +22,7 @@ import org.springframework.web.bind.annotation.RestController; import java.time.Instant; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; @RestController @RequestMapping("/v1/integrations/wecom") @@ -35,6 +37,7 @@ public class WecomCallbackController { private final CozeWorkflowService cozeWorkflowService; private final WecomClient wecomClient; private final WecomCryptoService wecomCryptoService; + private final Executor wecomTaskExecutor; private final XmlMapper xmlMapper = new XmlMapper(); private final Map processedMessages = new ConcurrentHashMap<>(); @@ -42,12 +45,14 @@ public class WecomCallbackController { SemanticDispatchService semanticDispatchService, CozeWorkflowService cozeWorkflowService, WecomClient wecomClient, - WecomCryptoService wecomCryptoService + WecomCryptoService wecomCryptoService, + @Qualifier("wecomTaskExecutor") Executor wecomTaskExecutor ) { this.semanticDispatchService = semanticDispatchService; this.cozeWorkflowService = cozeWorkflowService; this.wecomClient = wecomClient; this.wecomCryptoService = wecomCryptoService; + this.wecomTaskExecutor = wecomTaskExecutor; } @GetMapping("/callback") @@ -157,24 +162,39 @@ public class WecomCallbackController { return "success"; } + String fromUser = msg.getFromUserName(); + wecomTaskExecutor.execute(() -> processAndReply(callbackMode, appKey, fromUser, userText)); + log.info("wecom callback accepted for async processing mode={} fromUser={}", callbackMode, fromUser); + return "success"; + } + + private void processAndReply(CallbackMode callbackMode, String appKey, String fromUser, String userText) { String reply; try { reply = callbackMode == CallbackMode.GENERAL_CHAT ? cozeWorkflowService.chatDefault(userText) - : semanticDispatchService.dispatch(msg.getFromUserName(), userText); + : semanticDispatchService.dispatch(fromUser, userText); } catch (Exception e) { log.error("wecom callback processing failed mode={} fromUser={} err={}", - callbackMode, msg.getFromUserName(), e.getMessage(), e); + callbackMode, fromUser, e.getMessage(), e); reply = errorReply(callbackMode); } if (reply == null || reply.isBlank()) { log.warn("wecom callback reply blank mode={}, fallback to error reply", callbackMode); reply = errorReply(callbackMode); } - log.info("wecom callback sending semantic reply toUser={} replyPreview={}", msg.getFromUserName(), preview(reply)); - wecomClient.sendText(appKey, msg.getFromUserName(), reply); - log.info("wecom callback reply sent toUser={}", msg.getFromUserName()); - return "success"; + log.info("wecom callback sending reply mode={} toUser={} replyPreview={}", callbackMode, fromUser, preview(reply)); + try { + if (callbackMode == CallbackMode.SEMANTIC_ROUTING) { + wecomClient.sendMarkdown(appKey, fromUser, reply); + } else { + wecomClient.sendText(appKey, fromUser, reply); + } + log.info("wecom callback reply sent mode={} toUser={}", callbackMode, fromUser); + } catch (Exception e) { + log.error("wecom callback reply send failed mode={} toUser={} err={}", + callbackMode, fromUser, e.getMessage(), e); + } } private WecomIncomingMessage parseIncomingMessage(String signature, String timestamp, String nonce, String body) throws Exception { diff --git a/src/main/java/com/company/agentfoundation/integration/wecom/dto/WecomSendMessageRequest.java b/src/main/java/com/company/agentfoundation/integration/wecom/dto/WecomSendMessageRequest.java index a1e1bdb..42fb9de 100644 --- a/src/main/java/com/company/agentfoundation/integration/wecom/dto/WecomSendMessageRequest.java +++ b/src/main/java/com/company/agentfoundation/integration/wecom/dto/WecomSendMessageRequest.java @@ -12,6 +12,7 @@ public class WecomSendMessageRequest { @JsonProperty("agentid") private Integer agentId; private Text text; + private Markdown markdown; @JsonProperty("safe") private Integer safe = 0; @@ -23,6 +24,8 @@ public class WecomSendMessageRequest { public void setAgentId(Integer agentId) { this.agentId = agentId; } public Text getText() { return text; } public void setText(Text text) { this.text = text; } + public Markdown getMarkdown() { return markdown; } + public void setMarkdown(Markdown markdown) { this.markdown = markdown; } public Integer getSafe() { return safe; } public void setSafe(Integer safe) { this.safe = safe; } @@ -36,4 +39,11 @@ public class WecomSendMessageRequest { public List getMentionedList() { return mentionedList; } public void setMentionedList(List mentionedList) { this.mentionedList = mentionedList; } } + + public static class Markdown { + private String content; + + public String getContent() { return content; } + public void setContent(String content) { this.content = content; } + } } diff --git a/src/main/java/com/company/agentfoundation/semantic/handler/CustomerDailyReportIntentHandler.java b/src/main/java/com/company/agentfoundation/semantic/handler/CustomerDailyReportIntentHandler.java index 3ad7a6b..8c46541 100644 --- a/src/main/java/com/company/agentfoundation/semantic/handler/CustomerDailyReportIntentHandler.java +++ b/src/main/java/com/company/agentfoundation/semantic/handler/CustomerDailyReportIntentHandler.java @@ -73,61 +73,72 @@ public class CustomerDailyReportIntentHandler implements SemanticIntentHandler { private String format(JsonNode report, SemanticDateRangeResolver.DateRange range) { StringBuilder reply = new StringBuilder(); - reply.append("【客资管理日报】\n") - .append(range.startDate()); + reply.append("### 📊 客资管理日报\n") + .append("> 日期:").append(range.startDate()); if (!range.startDate().equals(range.endDate())) { reply.append(" 至 ").append(range.endDate()); } JsonNode scope = report.path("scope"); - reply.append("\n范围:").append(scopeLabel(scope.path("type").asText(""))); + reply.append(" | 范围:**").append(scopeLabel(scope.path("type").asText(""))).append("**"); JsonNode core = report.path("coreMetrics"); - reply.append("\n\n核心指标") - .append("\n新增:").append(core.path("added").asLong()) - .append("|分配:").append(core.path("assigned").asLong()) - .append("|跟进:").append(core.path("followed").asLong()) - .append("\n转化:").append(core.path("converted").asLong()) - .append("|流失(无效):").append(core.path("invalid").asLong()); + reply.append("\n\n**核心指标** ") + .append("新增:").append(core.path("added").asLong()).append("") + .append(" | 分配:").append(core.path("assigned").asLong()).append("") + .append(" | 跟进:").append(core.path("followed").asLong()).append("") + .append(" | 转化:").append(core.path("converted").asLong()).append("") + .append(" | 流失(无效):").append(core.path("invalid").asLong()); - reply.append("\n\n全链路漏斗"); + reply.append("\n\n**全链路漏斗** "); List lowConversions = new ArrayList<>(); JsonNode funnel = report.path("funnel"); for (int i = 0; i < funnel.size(); i++) { JsonNode stage = funnel.get(i); if (i == 0) { - reply.append("\n"); + reply.append(stage.path("label").asText()).append("(").append(stage.path("count").asLong()).append(")"); } else { - reply.append(" → "); - } - reply.append(stage.path("label").asText()).append("(").append(stage.path("count").asLong()).append(")"); - if (i > 0 && !stage.path("conversionRate").isNull()) { - reply.append(" ").append(percent(stage.path("conversionRate").asDouble())); + double rate = stage.path("conversionRate").asDouble(); + String color = stage.path("lowConversion").asBoolean(false) ? "warning" : "info"; + reply.append(" → ").append(stage.path("label").asText()) + .append("(").append(stage.path("count").asLong()).append(") ") + .append("") + .append(percent(rate)).append(""); } if (stage.path("lowConversion").asBoolean(false)) { lowConversions.add(stage.path("label").asText()); } } if (!lowConversions.isEmpty()) { - reply.append("\n🔴 低于35%:").append(String.join("、", lowConversions)); + reply.append("\n🔴 低于35%:") + .append(String.join("、", lowConversions)).append(""); } JsonNode monitor = report.path("exceptionMonitor"); - reply.append("\n\n异常监控") - .append("\n超过24小时无跟进:").append(monitor.path("over24HoursCount").asLong()) - .append("|超过3天:").append(monitor.path("over3DaysCount").asLong()); + long over24Hours = monitor.path("over24HoursCount").asLong(); + long over3Days = monitor.path("over3DaysCount").asLong(); + reply.append("\n\n**异常监控** ") + .append("超过24小时无跟进:").append(alertValue(over24Hours)) + .append(" | 超过3天:").append(alertValue(over3Days)); JsonNode alerts = monitor.path("alerts"); for (int i = 0; i < Math.min(3, alerts.size()); i++) { - reply.append("\n⚠️ ").append(alerts.get(i).path("message").asText()); + reply.append("\n⚠️ ") + .append(alerts.get(i).path("message").asText()).append(""); } JsonNode specification = report.path("specificationCheck"); - reply.append("\n\n资料完整度:") + boolean passed = specification.path("passed").asBoolean(false); + reply.append("\n\n**规范检查** ") + .append("资料完整度:") .append(percent(specification.path("completenessRate").asDouble())) - .append(specification.path("passed").asBoolean(false) ? "(达标)" : "(未达标)"); + .append(passed ? "(达标)" : "(未达标)").append(""); return reply.toString(); } + private String alertValue(long value) { + return value == 0 ? "0" : "" + value + ""; + } + private String scopeLabel(String scope) { return switch (scope) { case "ALL_ORGANIZATION" -> "全部组织";