crm 接口编写

This commit is contained in:
tanglong 2026-06-08 09:37:35 +08:00
parent 503c534982
commit ba0c3460ec
7 changed files with 402 additions and 25 deletions

View File

@ -0,0 +1,29 @@
package com.company.agentfoundation.agent;
import java.time.Instant;
import java.util.List;
import java.util.Map;
public record AgentCapabilityResponse(
String intentCode,
String title,
String mcpGroup,
String status,
String message,
List<String> dataSources,
Map<String, Object> data,
Instant timestamp
) {
public static AgentCapabilityResponse placeholder(AgentIntent intent, String message, List<String> dataSources) {
return new AgentCapabilityResponse(
intent.code(),
intent.title(),
intent.mcpGroup(),
"building",
message,
dataSources,
Map.of(),
Instant.now()
);
}
}

View File

@ -0,0 +1,46 @@
package com.company.agentfoundation.agent;
public enum AgentIntent {
DATA_SYNC_STATUS("data_sync_status", "integration", "\u4e09\u7cfb\u7edf\u6570\u636e\u540c\u6b65\u72b6\u6001"),
CUSTOMER_DASHBOARD_DAILY("customer_dashboard_daily", "customer", "\u5ba2\u8d44\u7ba1\u7406\u65e5\u62a5"),
STUDENT_ATTENDANCE_DAILY("student_attendance_daily", "education", "\u5b66\u5458\u6559\u5b66\u51fa\u52e4\u65e5\u62a5"),
WECOM_OPERATION_DAILY("wecom_operation_daily", "wecom", "\u4f01\u4e1a\u5fae\u4fe1\u5ba2\u6237\u8fd0\u8425\u65e5\u62a5"),
EC_CALL_COMPLIANCE_DAILY("ec_call_compliance_daily", "ec", "EC \u7535\u8bdd\u5408\u89c4\u76d1\u63a7\u65e5\u62a5"),
PERIOD_SUMMARY_REPORT("period_summary_report", "management", "\u5468\u671f\u5316\u7ecf\u8425\u6c47\u603b\u62a5\u8868"),
CONTENT_GENERATION("content_generation", "content", "\u7ecf\u8425\u5185\u5bb9\u667a\u80fd\u751f\u6210"),
GENERAL_CHAT("general_chat", "general", "\u901a\u7528\u5bf9\u8bdd");
private final String code;
private final String mcpGroup;
private final String title;
AgentIntent(String code, String mcpGroup, String title) {
this.code = code;
this.mcpGroup = mcpGroup;
this.title = title;
}
public String code() {
return code;
}
public String mcpGroup() {
return mcpGroup;
}
public String title() {
return title;
}
public static AgentIntent fromCode(String code) {
if (code == null || code.isBlank()) {
return GENERAL_CHAT;
}
for (AgentIntent intent : values()) {
if (intent.code.equalsIgnoreCase(code.trim())) {
return intent;
}
}
return GENERAL_CHAT;
}
}

View File

@ -0,0 +1,68 @@
package com.company.agentfoundation.agent;
import com.company.agentfoundation.domain.ChatRequest;
import com.company.agentfoundation.domain.ChatResponse;
import com.company.agentfoundation.security.AgentContext;
import com.company.agentfoundation.service.ChatService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class AgentOrchestratorService {
private static final Logger log = LoggerFactory.getLogger(AgentOrchestratorService.class);
private final IntentRecognitionService intentRecognitionService;
private final OperationReportService operationReportService;
private final ChatService chatService;
public AgentOrchestratorService(
IntentRecognitionService intentRecognitionService,
OperationReportService operationReportService,
ChatService chatService
) {
this.intentRecognitionService = intentRecognitionService;
this.operationReportService = operationReportService;
this.chatService = chatService;
}
public String handleMessage(String agentId, String fromUser, String userText) {
AgentIntent intent = intentRecognitionService.recognize(agentId, userText);
log.info("agent orchestrator route fromUser={} intent={} group={}", fromUser, intent.code(), intent.mcpGroup());
if (intent != AgentIntent.GENERAL_CHAT) {
return toWecomText(operationReportService.handle(intent));
}
ChatRequest request = new ChatRequest(
null,
List.of(
new ChatRequest.Message("system", "You are an enterprise CRM assistant. Reply concisely in Chinese."),
new ChatRequest.Message("user", userText)
),
0.3
);
ChatResponse response = chatService.chat(new AgentContext(agentId), request);
return response.content();
}
private String toWecomText(AgentCapabilityResponse response) {
return """
\u3010%s\u3011
%s
\u80fd\u529b\u5206\u7ec4\uff1a%s
\u80fd\u529b\u7f16\u7801\uff1a%s
\u6570\u636e\u6e90\uff1a%s
\u72b6\u6001\uff1a%s
""".formatted(
response.title(),
response.message(),
response.mcpGroup(),
response.intentCode(),
String.join("\u3001", response.dataSources()),
response.status()
).trim();
}
}

View File

@ -0,0 +1,110 @@
package com.company.agentfoundation.agent;
import com.company.agentfoundation.domain.ChatRequest;
import com.company.agentfoundation.domain.ChatResponse;
import com.company.agentfoundation.security.AgentContext;
import com.company.agentfoundation.service.ChatService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Locale;
@Service
public class IntentRecognitionService {
private static final Logger log = LoggerFactory.getLogger(IntentRecognitionService.class);
private final ChatService chatService;
public IntentRecognitionService(ChatService chatService) {
this.chatService = chatService;
}
public AgentIntent recognize(String agentId, String userText) {
AgentIntent keywordIntent = fallback(userText);
if (keywordIntent != AgentIntent.GENERAL_CHAT) {
log.info("agent intent recognized by keywords intent={} textPreview={}", keywordIntent.code(), preview(userText));
return keywordIntent;
}
try {
ChatRequest request = new ChatRequest(
null,
List.of(
new ChatRequest.Message("system", """
You classify user intent for a sports venue CRM agent.
Return exactly one code:
data_sync_status
customer_dashboard_daily
student_attendance_daily
wecom_operation_daily
ec_call_compliance_daily
period_summary_report
content_generation
general_chat
Do not explain.
"""),
new ChatRequest.Message("user", userText)
),
0.0
);
ChatResponse response = chatService.chat(new AgentContext(agentId), request);
AgentIntent intent = AgentIntent.fromCode(clean(response.content()));
log.info("agent intent recognized by llm intent={} textPreview={}", intent.code(), preview(userText));
return intent;
} catch (Exception ex) {
log.warn("agent intent llm recognition failed fallback=general_chat err={}", ex.getMessage());
return AgentIntent.GENERAL_CHAT;
}
}
private AgentIntent fallback(String userText) {
String text = userText == null ? "" : userText.toLowerCase(Locale.ROOT);
if (containsAny(text, "\u5ba2\u8d44", "\u5ba2\u6237\u8d44\u6599", "\u7ebf\u7d22", "\u65b0\u589e\u5ba2\u6237", "\u4eca\u65e5\u5ba2\u8d44")) {
return AgentIntent.CUSTOMER_DASHBOARD_DAILY;
}
if (containsAny(text, "\u51fa\u52e4", "\u7b7e\u5230", "\u6392\u8bfe", "\u8bfe\u540e\u8bc4\u4ef7", "\u6559\u5b66\u65e5\u62a5", "\u5b66\u5458")) {
return AgentIntent.STUDENT_ATTENDANCE_DAILY;
}
if (containsAny(text, "\u4f01\u4e1a\u5fae\u4fe1", "\u4f01\u5fae", "\u5ba2\u6237\u8fd0\u8425", "\u7fa4\u53d1", "\u89e6\u8fbe")) {
return AgentIntent.WECOM_OPERATION_DAILY;
}
if (containsAny(text, "ec", "\u7535\u8bdd", "\u5916\u547c", "\u63a5\u901a\u7387", "\u5f55\u97f3", "\u7535\u9500")) {
return AgentIntent.EC_CALL_COMPLIANCE_DAILY;
}
if (containsAny(text, "\u5468\u62a5", "\u6708\u62a5", "\u5468\u671f", "\u590d\u76d8", "\u7ecf\u8425\u5206\u6790")) {
return AgentIntent.PERIOD_SUMMARY_REPORT;
}
if (containsAny(text, "\u8bdd\u672f", "sop", "\u6587\u6848", "\u6d3b\u52a8\u65b9\u6848", "\u5ba3\u4f20", "\u5236\u5ea6", "\u8003\u6838")) {
return AgentIntent.CONTENT_GENERATION;
}
if (containsAny(text, "\u540c\u6b65", "\u63a5\u53e3", "\u4e09\u7cfb\u7edf", "\u6570\u636e\u6253\u901a")) {
return AgentIntent.DATA_SYNC_STATUS;
}
return AgentIntent.GENERAL_CHAT;
}
private boolean containsAny(String text, String... keywords) {
for (String keyword : keywords) {
if (text.contains(keyword.toLowerCase(Locale.ROOT))) {
return true;
}
}
return false;
}
private String clean(String content) {
if (content == null) {
return "";
}
return content.trim().replace("`", "").replace("\"", "");
}
private String preview(String text) {
if (text == null || text.isBlank()) {
return "";
}
return text.length() <= 80 ? text : text.substring(0, 80) + "...";
}
}

View File

@ -0,0 +1,62 @@
package com.company.agentfoundation.agent;
import com.company.agentfoundation.security.AgentAuthService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/v1/agent/capabilities")
public class OperationReportController {
private final AgentAuthService authService;
private final OperationReportService operationReportService;
public OperationReportController(AgentAuthService authService, OperationReportService operationReportService) {
this.authService = authService;
this.operationReportService = operationReportService;
}
@GetMapping("/data-sync/status")
public ResponseEntity<AgentCapabilityResponse> dataSyncStatus(@RequestHeader("X-Agent-Id") String agentId, @RequestHeader("X-Internal-Key") String internalKey) {
authService.authenticate(agentId, internalKey);
return ResponseEntity.ok(operationReportService.dataSyncStatus());
}
@GetMapping("/customer/dashboard/daily")
public ResponseEntity<AgentCapabilityResponse> customerDashboardDaily(@RequestHeader("X-Agent-Id") String agentId, @RequestHeader("X-Internal-Key") String internalKey) {
authService.authenticate(agentId, internalKey);
return ResponseEntity.ok(operationReportService.customerDashboardDaily());
}
@GetMapping("/student/attendance/daily")
public ResponseEntity<AgentCapabilityResponse> studentAttendanceDaily(@RequestHeader("X-Agent-Id") String agentId, @RequestHeader("X-Internal-Key") String internalKey) {
authService.authenticate(agentId, internalKey);
return ResponseEntity.ok(operationReportService.studentAttendanceDaily());
}
@GetMapping("/wecom/operation/daily")
public ResponseEntity<AgentCapabilityResponse> wecomOperationDaily(@RequestHeader("X-Agent-Id") String agentId, @RequestHeader("X-Internal-Key") String internalKey) {
authService.authenticate(agentId, internalKey);
return ResponseEntity.ok(operationReportService.wecomOperationDaily());
}
@GetMapping("/ec/call-compliance/daily")
public ResponseEntity<AgentCapabilityResponse> ecCallComplianceDaily(@RequestHeader("X-Agent-Id") String agentId, @RequestHeader("X-Internal-Key") String internalKey) {
authService.authenticate(agentId, internalKey);
return ResponseEntity.ok(operationReportService.ecCallComplianceDaily());
}
@GetMapping("/reports/period-summary")
public ResponseEntity<AgentCapabilityResponse> periodSummaryReport(@RequestHeader("X-Agent-Id") String agentId, @RequestHeader("X-Internal-Key") String internalKey) {
authService.authenticate(agentId, internalKey);
return ResponseEntity.ok(operationReportService.periodSummaryReport());
}
@GetMapping("/content/generation")
public ResponseEntity<AgentCapabilityResponse> contentGeneration(@RequestHeader("X-Agent-Id") String agentId, @RequestHeader("X-Internal-Key") String internalKey) {
authService.authenticate(agentId, internalKey);
return ResponseEntity.ok(operationReportService.contentGeneration());
}
}

View File

@ -0,0 +1,77 @@
package com.company.agentfoundation.agent;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class OperationReportService {
public AgentCapabilityResponse handle(AgentIntent intent) {
return switch (intent) {
case DATA_SYNC_STATUS -> dataSyncStatus();
case CUSTOMER_DASHBOARD_DAILY -> customerDashboardDaily();
case STUDENT_ATTENDANCE_DAILY -> studentAttendanceDaily();
case WECOM_OPERATION_DAILY -> wecomOperationDaily();
case EC_CALL_COMPLIANCE_DAILY -> ecCallComplianceDaily();
case PERIOD_SUMMARY_REPORT -> periodSummaryReport();
case CONTENT_GENERATION -> contentGeneration();
case GENERAL_CHAT -> AgentCapabilityResponse.placeholder(intent, "\u901a\u7528\u5bf9\u8bdd\u80fd\u529b\u6b63\u5728\u5b8c\u5584\u4e2d\u3002", List.of("DeepSeek"));
};
}
public AgentCapabilityResponse dataSyncStatus() {
return AgentCapabilityResponse.placeholder(
AgentIntent.DATA_SYNC_STATUS,
"\u4e09\u7cfb\u7edf\u63a5\u53e3\u6253\u901a\u4e0e\u6570\u636e\u540c\u6b65\u80fd\u529b\u6b63\u5728\u5b8c\u5584\u4e2d\uff0c\u540e\u7eed\u5c06\u63a5\u5165\u5b66\u90a6\u6559\u52a1\u7cfb\u7edf\u3001\u4f01\u4e1a\u5fae\u4fe1\u3001EC \u7535\u8bdd\u76d1\u63a7\u5e73\u53f0\u3002",
List.of("\u5b66\u90a6\u6559\u52a1\u7cfb\u7edf", "\u4f01\u4e1a\u5fae\u4fe1", "EC \u7535\u8bdd\u76d1\u63a7\u5e73\u53f0")
);
}
public AgentCapabilityResponse customerDashboardDaily() {
return AgentCapabilityResponse.placeholder(
AgentIntent.CUSTOMER_DASHBOARD_DAILY,
"\u5ba2\u8d44\u5927\u76d8\u6570\u636e\u6b63\u5728\u5b8c\u5584\u4e2d\uff0c\u540e\u7eed\u5c06\u8986\u76d6\u5ba2\u8d44\u65b0\u589e\u3001\u5206\u914d\u3001\u8ddf\u8fdb\u3001\u8f6c\u5316\u3001\u6d41\u5931\u4e0e\u5f02\u5e38\u98ce\u9669\u9884\u8b66\u3002",
List.of("\u5b66\u90a6\u6559\u52a1\u7cfb\u7edf", "\u4f01\u4e1a\u5fae\u4fe1", "CRM \u6570\u636e\u5e93")
);
}
public AgentCapabilityResponse studentAttendanceDaily() {
return AgentCapabilityResponse.placeholder(
AgentIntent.STUDENT_ATTENDANCE_DAILY,
"\u5b66\u5458\u6559\u5b66\u51fa\u52e4\u65e5\u62a5\u6b63\u5728\u5b8c\u5584\u4e2d\uff0c\u540e\u7eed\u5c06\u6c47\u603b\u6392\u8bfe\u3001\u7b7e\u5230\u51fa\u52e4\u3001\u8bfe\u540e\u8bc4\u4ef7\u548c\u8bfe\u7a0b\u6210\u679c\u7d20\u6750\u3002",
List.of("\u5b66\u90a6\u6559\u52a1\u7cfb\u7edf")
);
}
public AgentCapabilityResponse wecomOperationDaily() {
return AgentCapabilityResponse.placeholder(
AgentIntent.WECOM_OPERATION_DAILY,
"\u4f01\u4e1a\u5fae\u4fe1\u5ba2\u6237\u8fd0\u8425\u65e5\u62a5\u6b63\u5728\u5b8c\u5584\u4e2d\uff0c\u540e\u7eed\u5c06\u7edf\u8ba1\u5ba2\u6237\u65b0\u589e\u3001\u6c9f\u901a\u6d3b\u8dc3\u5ea6\u3001\u89e6\u8fbe\u6548\u679c\u4e0e\u5ba2\u6237\u6d41\u5931\u3002",
List.of("\u4f01\u4e1a\u5fae\u4fe1")
);
}
public AgentCapabilityResponse ecCallComplianceDaily() {
return AgentCapabilityResponse.placeholder(
AgentIntent.EC_CALL_COMPLIANCE_DAILY,
"EC \u7535\u8bdd\u5408\u89c4\u76d1\u63a7\u65e5\u62a5\u6b63\u5728\u5b8c\u5584\u4e2d\uff0c\u540e\u7eed\u5c06\u7edf\u8ba1\u5916\u547c\u603b\u91cf\u3001\u63a5\u901a\u7387\u3001\u6709\u6548\u901a\u8bdd\u65f6\u957f\u3001\u5f55\u97f3\u5408\u89c4\u4e0e\u8d26\u53f7\u98ce\u63a7\u3002",
List.of("EC \u7535\u8bdd\u76d1\u63a7\u5e73\u53f0")
);
}
public AgentCapabilityResponse periodSummaryReport() {
return AgentCapabilityResponse.placeholder(
AgentIntent.PERIOD_SUMMARY_REPORT,
"\u5468\u671f\u5316\u7ecf\u8425\u5468\u62a5\u3001\u6708\u62a5\u6b63\u5728\u5b8c\u5584\u4e2d\uff0c\u540e\u7eed\u5c06\u6574\u5408\u4e09\u5e73\u53f0\u6bcf\u65e5\u6570\u636e\u7528\u4e8e\u7ba1\u7406\u590d\u76d8\u548c\u7ecf\u8425\u5206\u6790\u3002",
List.of("\u5b66\u90a6\u6559\u52a1\u7cfb\u7edf", "\u4f01\u4e1a\u5fae\u4fe1", "EC \u7535\u8bdd\u76d1\u63a7\u5e73\u53f0")
);
}
public AgentCapabilityResponse contentGeneration() {
return AgentCapabilityResponse.placeholder(
AgentIntent.CONTENT_GENERATION,
"\u7ecf\u8425\u5185\u5bb9\u667a\u80fd\u751f\u6210\u80fd\u529b\u6b63\u5728\u5b8c\u5584\u4e2d\uff0c\u540e\u7eed\u5c06\u652f\u6301\u5ba2\u670d\u8bdd\u672f\u3001\u670d\u52a1 SOP\u3001\u8425\u9500\u6d3b\u52a8\u65b9\u6848\u3001\u83b7\u5ba2\u6587\u6848\u548c\u7ba1\u7406\u5236\u5ea6\u751f\u6210\u3002",
List.of("DeepSeek", "\u8fd0\u8425\u77e5\u8bc6\u5e93")
);
}
}

View File

@ -1,10 +1,7 @@
package com.company.agentfoundation.integration.wecom;
import com.company.agentfoundation.agent.AgentOrchestratorService;
import com.company.agentfoundation.config.FoundationProperties;
import com.company.agentfoundation.domain.ChatRequest;
import com.company.agentfoundation.domain.ChatResponse;
import com.company.agentfoundation.security.AgentContext;
import com.company.agentfoundation.service.ChatService;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -16,28 +13,26 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/v1/integrations/wecom")
public class WecomCallbackController {
private static final Logger log = LoggerFactory.getLogger(WecomCallbackController.class);
private final FoundationProperties properties;
private final ChatService chatService;
private final WecomClient wecomClient;
private final WecomCryptoService wecomCryptoService;
private final AgentOrchestratorService agentOrchestratorService;
private final XmlMapper xmlMapper = new XmlMapper();
public WecomCallbackController(
FoundationProperties properties,
ChatService chatService,
WecomClient wecomClient,
WecomCryptoService wecomCryptoService
WecomCryptoService wecomCryptoService,
AgentOrchestratorService agentOrchestratorService
) {
this.properties = properties;
this.chatService = chatService;
this.wecomClient = wecomClient;
this.wecomCryptoService = wecomCryptoService;
this.agentOrchestratorService = agentOrchestratorService;
}
@GetMapping("/callback")
@ -84,21 +79,12 @@ public class WecomCallbackController {
return "success";
}
ChatRequest req = new ChatRequest(
null,
List.of(
new ChatRequest.Message("system", "You are an enterprise CRM assistant. Provide concise, actionable follow-up advice."),
new ChatRequest.Message("user", userText)
),
0.3
);
log.info("wecom callback invoking crm agent agentId={} fromUser={}", properties.getWecom().getCrmAgentId(), msg.getFromUserName());
ChatResponse chat = chatService.chat(new AgentContext(properties.getWecom().getCrmAgentId()), req);
String reply = (chat.content() == null || chat.content().isBlank())
log.info("wecom callback invoking agent orchestrator agentId={} fromUser={}", properties.getWecom().getCrmAgentId(), msg.getFromUserName());
String reply = agentOrchestratorService.handleMessage(properties.getWecom().getCrmAgentId(), msg.getFromUserName(), userText);
reply = (reply == null || reply.isBlank())
? "Request received, but I could not generate advice right now."
: chat.content();
log.info("wecom callback agent response traceId={} replyPreview={}", chat.traceId(), preview(reply));
: reply;
log.info("wecom callback agent response replyPreview={}", preview(reply));
wecomClient.sendText(msg.getFromUserName(), reply);
log.info("wecom callback reply sent toUser={}", msg.getFromUserName());
return "success";
@ -117,7 +103,6 @@ public class WecomCallbackController {
}
String plainXml = wecomCryptoService.decrypt(envelope.getEncrypt());
log.info("wecom callback decrypt success plainXmlLength={}", plainXml.length());
log.info("wecom callback decrypted xml={}", plainXml);
return xmlMapper.readValue(plainXml, WecomIncomingMessage.class);
}