package com.company.agentfoundation.integration.wecom; 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; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; 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 XmlMapper xmlMapper = new XmlMapper(); public WecomCallbackController( FoundationProperties properties, ChatService chatService, WecomClient wecomClient, WecomCryptoService wecomCryptoService ) { this.properties = properties; this.chatService = chatService; this.wecomClient = wecomClient; this.wecomCryptoService = wecomCryptoService; } @GetMapping("/callback") public String verify( @RequestParam("msg_signature") String signature, @RequestParam("timestamp") String timestamp, @RequestParam("nonce") String nonce, @RequestParam("echostr") String echostr ) { log.info("wecom callback verify received safeMode={} timestamp={} nonce={}", wecomCryptoService.isSafeModeEnabled(), timestamp, nonce); String payload = wecomCryptoService.isSafeModeEnabled() ? echostr : null; if (!wecomCryptoService.validateSignature(signature, timestamp, nonce, payload)) { log.warn("wecom callback verify signature invalid timestamp={} nonce={}", timestamp, nonce); throw new IllegalArgumentException("Invalid WeCom signature"); } log.info("wecom callback verify success safeMode={}", wecomCryptoService.isSafeModeEnabled()); return wecomCryptoService.isSafeModeEnabled() ? wecomCryptoService.decrypt(echostr) : echostr; } @PostMapping(value = "/callback", consumes = {MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_XML_VALUE}) public String receive( @RequestParam("msg_signature") String signature, @RequestParam("timestamp") String timestamp, @RequestParam("nonce") String nonce, @RequestBody String body ) throws Exception { log.info("wecom callback message received safeMode={} timestamp={} nonce={} bodyLength={}", wecomCryptoService.isSafeModeEnabled(), timestamp, nonce, body == null ? 0 : body.length()); WecomIncomingMessage msg = parseIncomingMessage(signature, timestamp, nonce, body); log.info("wecom callback parsed fromUser={} toUser={} msgType={} contentPreview={}", msg == null ? null : msg.getFromUserName(), msg == null ? null : msg.getToUserName(), msg == null ? null : msg.getMsgType(), preview(msg == null ? null : msg.getContent())); log.info("wecom callback current user FromUserName={}", msg == null ? null : msg.getFromUserName()); if (msg == null || msg.getMsgType() == null || !"text".equalsIgnoreCase(msg.getMsgType())) { log.info("wecom callback ignored non-text message"); return "success"; } String userText = msg.getContent() == null ? "" : msg.getContent().trim(); if (userText.isBlank()) { log.info("wecom callback ignored blank text"); 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()) ? "Request received, but I could not generate advice right now." : chat.content(); log.info("wecom callback agent response traceId={} replyPreview={}", chat.traceId(), preview(reply)); wecomClient.sendText(msg.getFromUserName(), reply); log.info("wecom callback reply sent toUser={}", msg.getFromUserName()); return "success"; } private WecomIncomingMessage parseIncomingMessage(String signature, String timestamp, String nonce, String body) throws Exception { if (wecomCryptoService.isSafeModeEnabled()) { WecomEncryptedEnvelope envelope = xmlMapper.readValue(body, WecomEncryptedEnvelope.class); if (envelope == null || envelope.getEncrypt() == null || envelope.getEncrypt().isBlank()) { log.warn("wecom callback encrypted envelope missing encrypt field"); throw new IllegalArgumentException("Missing WeCom encrypted payload"); } if (!wecomCryptoService.validateSignature(signature, timestamp, nonce, envelope.getEncrypt())) { log.warn("wecom callback encrypted signature invalid timestamp={} nonce={}", timestamp, nonce); throw new IllegalArgumentException("Invalid WeCom signature"); } 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); } if (!wecomCryptoService.validateSignature(signature, timestamp, nonce, null)) { log.warn("wecom callback plain signature invalid timestamp={} nonce={}", timestamp, nonce); throw new IllegalArgumentException("Invalid WeCom signature"); } return xmlMapper.readValue(body, WecomIncomingMessage.class); } private String preview(String text) { if (text == null || text.isBlank()) { return ""; } return text.length() <= 80 ? text : text.substring(0, 80) + "..."; } }