日报格式

This commit is contained in:
tanglong 2026-07-21 15:16:24 +08:00
parent a1645981fd
commit 6940c5b6c5
5 changed files with 120 additions and 33 deletions

View File

@ -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() {

View File

@ -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;
}
}

View File

@ -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<String, Long> 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 {

View File

@ -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<String> getMentionedList() { return mentionedList; }
public void setMentionedList(List<String> mentionedList) { this.mentionedList = mentionedList; }
}
public static class Markdown {
private String content;
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
}
}

View File

@ -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("新增:<font color=\"info\">").append(core.path("added").asLong()).append("</font>")
.append(" | 分配:<font color=\"info\">").append(core.path("assigned").asLong()).append("</font>")
.append(" | 跟进:<font color=\"info\">").append(core.path("followed").asLong()).append("</font>")
.append(" | 转化:<font color=\"info\">").append(core.path("converted").asLong()).append("</font>")
.append(" | 流失(无效):").append(core.path("invalid").asLong());
reply.append("\n\n全链路漏斗");
reply.append("\n\n**全链路漏斗** ");
List<String> 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("<font color=\"").append(color).append("\">")
.append(percent(rate)).append("</font>");
}
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<font color=\"warning\">🔴 低于35%:")
.append(String.join("、", lowConversions)).append("</font>");
}
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<font color=\"warning\">⚠️ ")
.append(alerts.get(i).path("message").asText()).append("</font>");
}
JsonNode specification = report.path("specificationCheck");
reply.append("\n\n资料完整度:")
boolean passed = specification.path("passed").asBoolean(false);
reply.append("\n\n**规范检查** ")
.append("资料完整度:<font color=\"").append(passed ? "info" : "warning").append("\">")
.append(percent(specification.path("completenessRate").asDouble()))
.append(specification.path("passed").asBoolean(false) ? "(达标)" : "(未达标)");
.append(passed ? "(达标)" : "(未达标)").append("</font>");
return reply.toString();
}
private String alertValue(long value) {
return value == 0 ? "<font color=\"info\">0</font>" : "<font color=\"warning\">" + value + "</font>";
}
private String scopeLabel(String scope) {
return switch (scope) {
case "ALL_ORGANIZATION" -> "全部组织";