问答小助手
This commit is contained in:
parent
48e111afa9
commit
ff8f30bee1
@ -1,7 +1,10 @@
|
||||
# 客资日报企业微信回调
|
||||
|
||||
客资日报助手复用现有企业微信通用配置,不新增第二套 CorpId、AgentId、Secret、Token 或 EncodingAESKey。
|
||||
每个助手只配置自己的回调地址,服务端使用独立的回调方法接收消息:
|
||||
同一企业下的助手共用 CorpId;每个自建应用分别配置 AgentId、Secret 和回调地址。运动馆智能助手使用:
|
||||
|
||||
`GET/POST /v1/integrations/wecom/sports-venue/callback`
|
||||
|
||||
原客资日报地址继续作为兼容入口:
|
||||
|
||||
`GET/POST /v1/integrations/wecom/customer-report/callback`
|
||||
|
||||
@ -9,8 +12,9 @@ GET 用于企业微信保存回调配置时验签,POST 用于接收用户消
|
||||
|
||||
部署前配置:
|
||||
|
||||
- 沿用 `foundation.wecom` 下现有的 CorpId、AgentId、Secret、Token、EncodingAESKey。
|
||||
- 沿用 `foundation.wecom` 下现有的 CorpId、Token、EncodingAESKey。
|
||||
- 问答小助手和运动馆智能助手分别配置在 `foundation.wecom.apps.general`、`foundation.wecom.apps.sports-venue`。
|
||||
- `DEEPSEEK_API_KEY`(沿用项目现有 DeepSeek 配置)。
|
||||
- `CRM_REPORT_BASE_URL`,指向学邦同步服务,例如 `http://127.0.0.1:9999`。
|
||||
|
||||
`/v1/integrations/wecom/callback` 和 `/v1/integrations/wecom/customer-report/callback` 分别对应独立的 GET/POST 回调方法,但复用签名验证、解密和语义路由逻辑。后续增加助手时只新增回调路径和入口方法,不重复企业微信公共配置。
|
||||
`/v1/integrations/wecom/callback` 保持原有扣子默认工作流;`/v1/integrations/wecom/sports-venue/callback` 使用 DeepSeek 语义识别并调用客资日报接口。回复消息时,服务端会根据入口选择对应应用的 AgentId 和 Secret。
|
||||
|
||||
@ -100,29 +100,42 @@ public class FoundationProperties {
|
||||
public static class Wecom {
|
||||
private boolean enabled;
|
||||
private String corpId;
|
||||
private Integer agentId;
|
||||
private String secret;
|
||||
private String defaultApp = "general";
|
||||
private String studentContactSecret;
|
||||
private String token;
|
||||
private String encodingAesKey;
|
||||
private String callbackPath = "/v1/integrations/wecom/callback";
|
||||
private Map<String, App> apps = new HashMap<>();
|
||||
|
||||
public boolean isEnabled() { return enabled; }
|
||||
public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
public String getCorpId() { return corpId; }
|
||||
public void setCorpId(String corpId) { this.corpId = corpId; }
|
||||
public Integer getAgentId() { return agentId; }
|
||||
public void setAgentId(Integer agentId) { this.agentId = agentId; }
|
||||
public String getSecret() { return secret; }
|
||||
public void setSecret(String secret) { this.secret = secret; }
|
||||
public String getDefaultApp() { return defaultApp; }
|
||||
public void setDefaultApp(String defaultApp) { this.defaultApp = defaultApp; }
|
||||
public String getStudentContactSecret() { return studentContactSecret; }
|
||||
public void setStudentContactSecret(String studentContactSecret) { this.studentContactSecret = studentContactSecret; }
|
||||
public String getToken() { return token; }
|
||||
public void setToken(String token) { this.token = token; }
|
||||
public String getEncodingAesKey() { return encodingAesKey; }
|
||||
public void setEncodingAesKey(String encodingAesKey) { this.encodingAesKey = encodingAesKey; }
|
||||
public String getCallbackPath() { return callbackPath; }
|
||||
public void setCallbackPath(String callbackPath) { this.callbackPath = callbackPath; }
|
||||
public Map<String, App> getApps() { return apps; }
|
||||
public void setApps(Map<String, App> apps) { this.apps = apps; }
|
||||
|
||||
public static class App {
|
||||
private String name;
|
||||
private Integer agentId;
|
||||
private String secret;
|
||||
private String callbackPath;
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public Integer getAgentId() { return agentId; }
|
||||
public void setAgentId(Integer agentId) { this.agentId = agentId; }
|
||||
public String getSecret() { return secret; }
|
||||
public void setSecret(String secret) { this.secret = secret; }
|
||||
public String getCallbackPath() { return callbackPath; }
|
||||
public void setCallbackPath(String callbackPath) { this.callbackPath = callbackPath; }
|
||||
}
|
||||
}
|
||||
|
||||
public static class Semantic {
|
||||
|
||||
@ -27,8 +27,6 @@ public class WecomClient {
|
||||
private final FoundationProperties properties;
|
||||
private final WebClient webClient;
|
||||
|
||||
private volatile String cachedToken;
|
||||
private volatile Instant tokenExpireAt = Instant.EPOCH;
|
||||
private final Map<String, TokenCache> tokenCaches = new ConcurrentHashMap<>();
|
||||
|
||||
public WecomClient(FoundationProperties properties, WebClient.Builder builder) {
|
||||
@ -37,11 +35,17 @@ public class WecomClient {
|
||||
}
|
||||
|
||||
public void sendText(String toUser, String content) {
|
||||
log.info("wecom sendText start toUser={} agentId={} contentPreview={}", toUser, properties.getWecom().getAgentId(), preview(content));
|
||||
String token = getAccessToken();
|
||||
sendText(properties.getWecom().getDefaultApp(), toUser, content);
|
||||
}
|
||||
|
||||
public void sendText(String appKey, String toUser, String content) {
|
||||
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(properties.getWecom().getAgentId());
|
||||
req.setAgentId(app.getAgentId());
|
||||
WecomSendMessageRequest.Text text = new WecomSendMessageRequest.Text();
|
||||
text.setContent(content);
|
||||
req.setText(text);
|
||||
@ -118,35 +122,8 @@ public class WecomClient {
|
||||
return response;
|
||||
}
|
||||
|
||||
private String getAccessToken() {
|
||||
if (cachedToken != null && Instant.now().isBefore(tokenExpireAt)) {
|
||||
log.info("wecom gettoken use cached token expireAt={}", tokenExpireAt);
|
||||
return cachedToken;
|
||||
}
|
||||
|
||||
WecomTokenResponse response = webClient.get()
|
||||
.uri(uriBuilder -> uriBuilder.path("/cgi-bin/gettoken")
|
||||
.queryParam("corpid", properties.getWecom().getCorpId())
|
||||
.queryParam("corpsecret", properties.getWecom().getSecret())
|
||||
.build())
|
||||
.retrieve()
|
||||
.bodyToMono(WecomTokenResponse.class)
|
||||
.block();
|
||||
|
||||
if (response == null || !response.success() || response.accessToken() == null) {
|
||||
log.error("wecom gettoken failed err={}", response == null ? "null" : response.errmsg());
|
||||
throw new IllegalStateException("WeCom gettoken failed: " + (response == null ? "null" : response.errmsg()));
|
||||
}
|
||||
|
||||
cachedToken = response.accessToken();
|
||||
int expires = response.expiresIn() == null ? 7200 : response.expiresIn();
|
||||
tokenExpireAt = Instant.now().plusSeconds(Math.max(300, expires - 120));
|
||||
log.info("wecom gettoken success expireAt={}", tokenExpireAt);
|
||||
return cachedToken;
|
||||
}
|
||||
|
||||
private String getAccessToken(String secret) {
|
||||
String safeSecret = secret == null || secret.isBlank() ? properties.getWecom().getSecret() : secret.trim();
|
||||
String safeSecret = requireText(secret, "WeCom app secret is missing");
|
||||
TokenCache cache = tokenCaches.computeIfAbsent(safeSecret, ignored -> new TokenCache());
|
||||
if (cache.token != null && Instant.now().isBefore(cache.expireAt)) {
|
||||
log.info("wecom gettoken use cached token expireAt={}", cache.expireAt);
|
||||
@ -176,7 +153,27 @@ public class WecomClient {
|
||||
|
||||
private String studentContactSecret() {
|
||||
String secret = properties.getWecom().getStudentContactSecret();
|
||||
return secret == null || secret.isBlank() ? properties.getWecom().getSecret() : secret;
|
||||
return secret == null || secret.isBlank() ? app(properties.getWecom().getDefaultApp()).getSecret() : secret;
|
||||
}
|
||||
|
||||
private FoundationProperties.Wecom.App app(String appKey) {
|
||||
String safeKey = requireText(appKey, "WeCom app key is missing");
|
||||
FoundationProperties.Wecom.App app = properties.getWecom().getApps().get(safeKey);
|
||||
if (app == null) {
|
||||
throw new IllegalStateException("WeCom app is not configured: " + safeKey);
|
||||
}
|
||||
if (app.getAgentId() == null || app.getAgentId() <= 0) {
|
||||
throw new IllegalStateException("WeCom agent-id is missing: " + safeKey);
|
||||
}
|
||||
requireText(app.getSecret(), "WeCom secret is missing: " + safeKey);
|
||||
return app;
|
||||
}
|
||||
|
||||
private String requireText(String value, String message) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalStateException(message);
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private String preview(String text) {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.company.agentfoundation.integration.wecom.controller;
|
||||
|
||||
import com.company.agentfoundation.integration.coze.service.CozeWorkflowService;
|
||||
import com.company.agentfoundation.integration.wecom.client.WecomClient;
|
||||
import com.company.agentfoundation.integration.wecom.model.WecomEncryptedEnvelope;
|
||||
import com.company.agentfoundation.integration.wecom.model.WecomIncomingMessage;
|
||||
@ -27,7 +28,10 @@ public class WecomCallbackController {
|
||||
private static final Logger log = LoggerFactory.getLogger(WecomCallbackController.class);
|
||||
private static final long MESSAGE_DEDUP_TTL_SECONDS = 600;
|
||||
private static final String ERROR_REPLY = "语义识别或业务查询失败,请稍后重试";
|
||||
private static final String GENERAL_APP = "general";
|
||||
private static final String SPORTS_VENUE_APP = "sports-venue";
|
||||
private final SemanticDispatchService semanticDispatchService;
|
||||
private final CozeWorkflowService cozeWorkflowService;
|
||||
private final WecomClient wecomClient;
|
||||
private final WecomCryptoService wecomCryptoService;
|
||||
private final XmlMapper xmlMapper = new XmlMapper();
|
||||
@ -35,10 +39,12 @@ public class WecomCallbackController {
|
||||
|
||||
public WecomCallbackController(
|
||||
SemanticDispatchService semanticDispatchService,
|
||||
CozeWorkflowService cozeWorkflowService,
|
||||
WecomClient wecomClient,
|
||||
WecomCryptoService wecomCryptoService
|
||||
) {
|
||||
this.semanticDispatchService = semanticDispatchService;
|
||||
this.cozeWorkflowService = cozeWorkflowService;
|
||||
this.wecomClient = wecomClient;
|
||||
this.wecomCryptoService = wecomCryptoService;
|
||||
}
|
||||
@ -82,7 +88,27 @@ public class WecomCallbackController {
|
||||
@RequestParam("nonce") String nonce,
|
||||
@RequestBody String body
|
||||
) throws Exception {
|
||||
return receiveCallback(signature, timestamp, nonce, body, "default");
|
||||
return receiveCallback(signature, timestamp, nonce, body, "default", GENERAL_APP, CallbackMode.GENERAL_CHAT);
|
||||
}
|
||||
|
||||
@GetMapping("/sports-venue/callback")
|
||||
public String verifySportsVenueCallback(
|
||||
@RequestParam("msg_signature") String signature,
|
||||
@RequestParam("timestamp") String timestamp,
|
||||
@RequestParam("nonce") String nonce,
|
||||
@RequestParam("echostr") String echostr
|
||||
) {
|
||||
return verifyCallback(signature, timestamp, nonce, echostr, "sports-venue");
|
||||
}
|
||||
|
||||
@PostMapping(value = "/sports-venue/callback", consumes = {MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_XML_VALUE})
|
||||
public String receiveSportsVenueCallback(
|
||||
@RequestParam("msg_signature") String signature,
|
||||
@RequestParam("timestamp") String timestamp,
|
||||
@RequestParam("nonce") String nonce,
|
||||
@RequestBody String body
|
||||
) throws Exception {
|
||||
return receiveCallback(signature, timestamp, nonce, body, "sports-venue", SPORTS_VENUE_APP, CallbackMode.SEMANTIC_ROUTING);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/customer-report/callback", consumes = {MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_XML_VALUE})
|
||||
@ -92,10 +118,18 @@ public class WecomCallbackController {
|
||||
@RequestParam("nonce") String nonce,
|
||||
@RequestBody String body
|
||||
) throws Exception {
|
||||
return receiveCallback(signature, timestamp, nonce, body, "customer-report");
|
||||
return receiveCallback(signature, timestamp, nonce, body, "customer-report", SPORTS_VENUE_APP, CallbackMode.SEMANTIC_ROUTING);
|
||||
}
|
||||
|
||||
private String receiveCallback(String signature, String timestamp, String nonce, String body, String callbackName) throws Exception {
|
||||
private String receiveCallback(
|
||||
String signature,
|
||||
String timestamp,
|
||||
String nonce,
|
||||
String body,
|
||||
String callbackName,
|
||||
String appKey,
|
||||
CallbackMode callbackMode
|
||||
) throws Exception {
|
||||
log.info("wecom callback route={} message received", callbackName);
|
||||
log.info("wecom callback message received safeMode={} timestamp={} nonce={} bodyLength={}",
|
||||
wecomCryptoService.isSafeModeEnabled(), timestamp, nonce, body == null ? 0 : body.length());
|
||||
@ -124,7 +158,9 @@ public class WecomCallbackController {
|
||||
|
||||
String reply;
|
||||
try {
|
||||
reply = semanticDispatchService.dispatch(msg.getFromUserName(), userText);
|
||||
reply = callbackMode == CallbackMode.GENERAL_CHAT
|
||||
? cozeWorkflowService.chatDefault(userText)
|
||||
: semanticDispatchService.dispatch(msg.getFromUserName(), userText);
|
||||
} catch (Exception e) {
|
||||
log.error("wecom callback semantic dispatch failed fromUser={} err={}", msg.getFromUserName(), e.getMessage(), e);
|
||||
reply = ERROR_REPLY;
|
||||
@ -134,7 +170,7 @@ public class WecomCallbackController {
|
||||
reply = ERROR_REPLY;
|
||||
}
|
||||
log.info("wecom callback sending semantic reply toUser={} replyPreview={}", msg.getFromUserName(), preview(reply));
|
||||
wecomClient.sendText(msg.getFromUserName(), reply);
|
||||
wecomClient.sendText(appKey, msg.getFromUserName(), reply);
|
||||
log.info("wecom callback reply sent toUser={}", msg.getFromUserName());
|
||||
return "success";
|
||||
}
|
||||
@ -192,4 +228,9 @@ public class WecomCallbackController {
|
||||
}
|
||||
return text.length() <= 80 ? text : text.substring(0, 80) + "...";
|
||||
}
|
||||
|
||||
private enum CallbackMode {
|
||||
GENERAL_CHAT,
|
||||
SEMANTIC_ROUTING
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,11 +50,20 @@ foundation:
|
||||
wecom:
|
||||
enabled: true
|
||||
corp-id: wwaec174f56094f2b5
|
||||
agent-id: 1000003
|
||||
secret: Sq76WOBNf3S85RoSzrJ_3u5iQrl8DRTvjTv9X0-z7HE
|
||||
default-app: general
|
||||
token: crmAgentToken2026
|
||||
encoding-aes-key: k3F8mN2vQ7xLp9sTd4YwH6cRb1JuEz5AaZq7BnVcK2D
|
||||
callback-path: /v1/integrations/wecom/callback
|
||||
apps:
|
||||
general:
|
||||
name: 问答小助手
|
||||
agent-id: 1000003
|
||||
secret: Sq76WOBNf3S85RoSzrJ_3u5iQrl8DRTvjTv9X0-z7HE
|
||||
callback-path: /v1/integrations/wecom/callback
|
||||
sports-venue:
|
||||
name: 运动馆智能助手
|
||||
agent-id: 1000006
|
||||
secret: V-fh1ROzjb06Hi9XB8VPb4HhE2ApI4j99NAjAXKYOb8
|
||||
callback-path: /v1/integrations/wecom/sports-venue/callback
|
||||
semantic:
|
||||
agent-id: crm-assistant
|
||||
minimum-confidence: 0.60
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user