日报格式
This commit is contained in:
parent
a1645981fd
commit
6940c5b6c5
@ -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() {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -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" -> "全部组织";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user