🎨 #4100 【企业微信】支持智能机器人 API 模式
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
# 企业微信智能机器人接口
|
||||
|
||||
本模块提供企业微信智能机器人相关的API接口实现。
|
||||
本模块提供企业微信智能机器人相关的 API 接口实现。
|
||||
|
||||
> `createRobot`、`chat`、`sendMessage` 等既有方法走企业应用 `access_token` 接口,
|
||||
> 需要在 `WxCpConfigStorage` 中配置应用 `agentId` 和 `secret`。它们不适用于机器人后台创建的新版 API 模式。
|
||||
|
||||
## 官方文档
|
||||
|
||||
@@ -73,7 +76,7 @@ String sessionId = "session123";
|
||||
robotService.resetSession(robotId, userid, sessionId);
|
||||
```
|
||||
|
||||
### 主动发送消息
|
||||
### 旧版 access_token 主动发送消息
|
||||
|
||||
智能机器人可以主动向用户发送消息,用于推送通知或提醒。
|
||||
|
||||
@@ -89,34 +92,29 @@ String msgId = response.getMsgId();
|
||||
String sessionId = response.getSessionId();
|
||||
```
|
||||
|
||||
### 接收用户消息
|
||||
### 新版 API 模式:接收回调与回复消息
|
||||
|
||||
当用户向智能机器人发送消息时,企业微信会通过回调接口推送消息。可以使用 `WxCpXmlMessage` 接收和解析这些消息:
|
||||
|
||||
```java
|
||||
// 在接收回调消息的接口中
|
||||
WxCpXmlMessage message = WxCpXmlMessage.fromEncryptedXml(
|
||||
requestBody, wxCpConfigStorage, timestamp, nonce, msgSignature
|
||||
);
|
||||
|
||||
// 获取智能机器人相关字段
|
||||
String robotId = message.getRobotId(); // 机器人ID
|
||||
String sessionId = message.getSessionId(); // 会话ID
|
||||
String content = message.getContent(); // 消息内容
|
||||
String fromUser = message.getFromUserName(); // 发送用户
|
||||
|
||||
// 处理消息并回复
|
||||
// ...
|
||||
```
|
||||
|
||||
对于智能机器人 API 模式的 JSON 回调消息,可使用 `WxCpIntelligentRobotMessage` 解析:
|
||||
在机器人后台开启 API 模式后,配置 URL、Token、EncodingAESKey。企业微信会推送加密 JSON 回调;
|
||||
它不是 XML,也不需要企业应用 `secret`。从请求参数取得 `msg_signature`、`timestamp`、`nonce`,
|
||||
从请求体取得 `encrypt` 字段后,可以直接解密和解析:
|
||||
|
||||
```java
|
||||
WxCpIntelligentRobotMessage callbackMessage =
|
||||
robotService.parseCallbackMessage(jsonBody);
|
||||
String botId = callbackMessage.getAiBotId();
|
||||
String userId = callbackMessage.getFrom().getUserid();
|
||||
String msgType = callbackMessage.getMsgType();
|
||||
robotService.parseEncryptedCallbackMessage(
|
||||
msgSignature, timestamp, nonce, encryptedJson,
|
||||
token, encodingAesKey, aiBotId);
|
||||
|
||||
String responseUrl = callbackMessage.getResponseUrl();
|
||||
String content = callbackMessage.getText().getContent();
|
||||
```
|
||||
|
||||
回复时使用回调中的短期 `response_url`,不调用基于 `access_token` 的 `sendMessage`:
|
||||
|
||||
```java
|
||||
String replyJson = "{\"msgtype\":\"text\",\"text\":{\"content\":\"您好\"}}";
|
||||
robotService.replyMessage(
|
||||
responseUrl, replyJson, token, encodingAesKey, aiBotId,
|
||||
String.valueOf(System.currentTimeMillis() / 1000), java.util.UUID.randomUUID().toString());
|
||||
```
|
||||
|
||||
### 删除智能机器人
|
||||
@@ -144,7 +142,8 @@ robotService.deleteRobot(robotId);
|
||||
|
||||
### 消息接收
|
||||
|
||||
- `WxCpXmlMessage`: 支持接收智能机器人回调消息,包含 `robotId` 和 `sessionId` 字段
|
||||
- `WxCpIntelligentRobotMessage`: 智能机器人 API 模式的已解密 JSON 回调消息
|
||||
- `WxCpIntelligentRobotCryptUtil`: 智能机器人 API 模式的消息加解密工具
|
||||
|
||||
### 服务接口
|
||||
|
||||
@@ -153,7 +152,6 @@ robotService.deleteRobot(robotId);
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 需要确保企业微信应用具有智能机器人相关权限
|
||||
2. 智能机器人功能可能需要特定的企业微信版本支持
|
||||
3. 会话ID可以用于保持对话的连续性,提升用户体验
|
||||
4. 机器人状态: 0表示停用,1表示启用
|
||||
1. 新版 API 模式的 Token、EncodingAESKey 和机器人 ID 由机器人后台配置,不要填写企业应用 secret。
|
||||
2. `response_url` 是回调附带的临时地址,应及时使用,且不应持久化。
|
||||
3. `parseCallbackMessage` 仅用于已解密的 JSON;HTTP 回调入口应使用 `parseEncryptedCallbackMessage`。
|
||||
|
||||
@@ -82,4 +82,40 @@ public interface WxCpIntelligentRobotService {
|
||||
*/
|
||||
WxCpIntelligentRobotMessage parseCallbackMessage(String callbackMessageJson);
|
||||
|
||||
/**
|
||||
* 解密并解析智能机器人 API 模式回调消息.
|
||||
*
|
||||
* @param msgSignature 回调 URL 参数中的签名
|
||||
* @param timestamp 回调 URL 参数中的时间戳
|
||||
* @param nonce 回调 URL 参数中的随机串
|
||||
* @param encryptedJson 回调 JSON 信封中的 encrypt 字段
|
||||
* @param token 机器人后台配置的 Token
|
||||
* @param encodingAesKey 机器人后台配置的 EncodingAESKey
|
||||
* @param aiBotId 机器人 ID
|
||||
* @return 解密并解析后的回调消息
|
||||
*/
|
||||
default WxCpIntelligentRobotMessage parseEncryptedCallbackMessage(String msgSignature, String timestamp, String nonce,
|
||||
String encryptedJson, String token, String encodingAesKey,
|
||||
String aiBotId) {
|
||||
throw new UnsupportedOperationException("当前智能机器人服务不支持 API 模式回调解析");
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密并向智能机器人 API 模式的临时 response_url 回复消息.
|
||||
*
|
||||
* @param responseUrl 回调消息中的 response_url
|
||||
* @param plainJson 回复的明文 JSON
|
||||
* @param token 机器人后台配置的 Token
|
||||
* @param encodingAesKey 机器人后台配置的 EncodingAESKey
|
||||
* @param aiBotId 机器人 ID
|
||||
* @param timestamp 回复时间戳
|
||||
* @param nonce 回复随机串
|
||||
* @return 企业微信响应内容
|
||||
* @throws WxErrorException 微信接口异常
|
||||
*/
|
||||
default String replyMessage(String responseUrl, String plainJson, String token, String encodingAesKey, String aiBotId,
|
||||
String timestamp, String nonce) throws WxErrorException {
|
||||
throw new UnsupportedOperationException("当前智能机器人服务不支持 API 模式消息回复");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -430,23 +430,29 @@ public abstract class BaseWxCpServiceImpl<H, P> implements WxCpService, RequestH
|
||||
* 普通请求,不自动带accessToken
|
||||
*/
|
||||
private <T, E> T executeNormal(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException {
|
||||
String uriForLog = redactQueryString(uri);
|
||||
try {
|
||||
T result = executor.execute(uri, data, WxType.CP);
|
||||
log.debug("\n【请求地址】: {}\n【请求参数】:{}\n【响应数据】:{}", uri, data, result);
|
||||
log.debug("\n【请求地址】: {}\n【请求参数】:{}\n【响应数据】:{}", uriForLog, data, result);
|
||||
return result;
|
||||
} catch (WxErrorException e) {
|
||||
WxError error = e.getError();
|
||||
if (error.getErrorCode() != 0) {
|
||||
log.error("\n【请求地址】: {}\n【请求参数】:{}\n【错误信息】:{}", uri, data, error);
|
||||
log.error("\n【请求地址】: {}\n【请求参数】:{}\n【错误信息】:{}", uriForLog, data, error);
|
||||
throw new WxErrorException(error, e);
|
||||
}
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
log.error("\n【请求地址】: {}\n【请求参数】:{}\n【异常信息】:{}", uri, data, e.getMessage());
|
||||
log.error("\n【请求地址】: {}\n【请求参数】:{}\n【异常信息】:{}", uriForLog, data, e.getMessage());
|
||||
throw new WxErrorException(e);
|
||||
}
|
||||
}
|
||||
|
||||
static String redactQueryString(String uri) {
|
||||
int queryStart = uri.indexOf('?');
|
||||
return queryStart < 0 ? uri : uri.substring(0, queryStart) + "?******";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setWxCpConfigStorage(WxCpConfigStorage wxConfigProvider) {
|
||||
this.configStorage = wxConfigProvider;
|
||||
|
||||
@@ -6,6 +6,7 @@ import me.chanjar.weixin.common.error.WxErrorException;
|
||||
import me.chanjar.weixin.cp.api.WxCpIntelligentRobotService;
|
||||
import me.chanjar.weixin.cp.api.WxCpService;
|
||||
import me.chanjar.weixin.cp.bean.intelligentrobot.*;
|
||||
import me.chanjar.weixin.cp.util.crypto.WxCpIntelligentRobotCryptUtil;
|
||||
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
|
||||
|
||||
import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.IntelligentRobot.*;
|
||||
@@ -72,4 +73,19 @@ public class WxCpIntelligentRobotServiceImpl implements WxCpIntelligentRobotServ
|
||||
return WxCpIntelligentRobotMessage.fromJson(callbackMessageJson);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WxCpIntelligentRobotMessage parseEncryptedCallbackMessage(String msgSignature, String timestamp, String nonce,
|
||||
String encryptedJson, String token,
|
||||
String encodingAesKey, String aiBotId) {
|
||||
WxCpIntelligentRobotCryptUtil cryptUtil = new WxCpIntelligentRobotCryptUtil(token, encodingAesKey, aiBotId);
|
||||
return parseCallbackMessage(cryptUtil.decrypt(msgSignature, timestamp, nonce, encryptedJson));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String replyMessage(String responseUrl, String plainJson, String token, String encodingAesKey,
|
||||
String aiBotId, String timestamp, String nonce) throws WxErrorException {
|
||||
WxCpIntelligentRobotCryptUtil cryptUtil = new WxCpIntelligentRobotCryptUtil(token, encodingAesKey, aiBotId);
|
||||
return this.cpService.postWithoutToken(responseUrl, cryptUtil.encrypt(plainJson, timestamp, nonce));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package me.chanjar.weixin.cp.util.crypto;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import me.chanjar.weixin.common.util.crypto.SHA1;
|
||||
import me.chanjar.weixin.common.util.crypto.WxCryptUtil;
|
||||
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
|
||||
import me.chanjar.weixin.common.error.WxRuntimeException;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 企业微信智能机器人 API 模式消息加解密工具.
|
||||
*
|
||||
* <p>机器人 API 模式使用机器人后台配置的 Token、EncodingAESKey 和机器人 ID,
|
||||
* 与企业应用 access_token 无关。</p>
|
||||
*/
|
||||
public class WxCpIntelligentRobotCryptUtil extends WxCryptUtil {
|
||||
|
||||
public WxCpIntelligentRobotCryptUtil(String token, String encodingAesKey, String aiBotId) {
|
||||
super(token, encodingAesKey, aiBotId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密机器人 API 模式的 JSON 回调消息.
|
||||
*/
|
||||
public String decrypt(String msgSignature, String timestamp, String nonce, String encryptedContent) {
|
||||
String signature = SHA1.gen(this.token, timestamp, nonce, encryptedContent);
|
||||
if (!signature.equals(msgSignature)) {
|
||||
throw new WxRuntimeException("加密消息签名校验失败");
|
||||
}
|
||||
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(this.aesKey, "AES"),
|
||||
new IvParameterSpec(Arrays.copyOfRange(this.aesKey, 0, 16)));
|
||||
byte[] bytes = me.chanjar.weixin.common.util.crypto.PKCS7Encoder.decode(
|
||||
cipher.doFinal(Base64.decodeBase64(encryptedContent)));
|
||||
if (bytes.length < 20) {
|
||||
throw new WxRuntimeException("解密后数据长度异常,可能为错误的密文或EncodingAESKey");
|
||||
}
|
||||
|
||||
int plainTextLength = 0;
|
||||
for (int index = 16; index < 20; index++) {
|
||||
plainTextLength = (plainTextLength << 8) | (bytes[index] & 0xff);
|
||||
}
|
||||
int plainTextEnd = 20 + plainTextLength;
|
||||
if (plainTextLength < 0 || plainTextEnd > bytes.length) {
|
||||
throw new WxRuntimeException("解密后数据格式非法:消息长度不正确,可能为错误的密文或EncodingAESKey");
|
||||
}
|
||||
|
||||
String receiverId = new String(Arrays.copyOfRange(bytes, plainTextEnd, bytes.length), StandardCharsets.UTF_8);
|
||||
if (!this.appidOrCorpid.equals(receiverId)) {
|
||||
throw new WxRuntimeException("智能机器人ID不正确,请核实!");
|
||||
}
|
||||
return new String(Arrays.copyOfRange(bytes, 20, plainTextEnd), StandardCharsets.UTF_8);
|
||||
} catch (WxRuntimeException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new WxRuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密机器人 API 模式的 JSON 回复消息.
|
||||
*/
|
||||
public String encrypt(String plainJson, String timestamp, String nonce) {
|
||||
String encryptedContent = encrypt(UUID.randomUUID().toString().replace("-", "").substring(0, 16), plainJson);
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty("encrypt", encryptedContent);
|
||||
result.addProperty("msg_signature", SHA1.gen(this.token, timestamp, nonce, encryptedContent));
|
||||
result.addProperty("timestamp", timestamp);
|
||||
result.addProperty("nonce", nonce);
|
||||
return WxCpGsonBuilder.create().toJson(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密 URL 校验请求中的 echostr.
|
||||
*/
|
||||
public String verifyUrl(String msgSignature, String timestamp, String nonce, String echoStr) {
|
||||
return decrypt(msgSignature, timestamp, nonce, echoStr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package me.chanjar.weixin.cp.api.impl;
|
||||
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
import static org.testng.Assert.assertEquals;
|
||||
|
||||
public class BaseWxCpServiceImplLogTest {
|
||||
|
||||
@Test
|
||||
public void redactQueryStringShouldHideTemporaryResponseUrlCredentials() {
|
||||
assertEquals(BaseWxCpServiceImpl.redactQueryString("https://example.com/reply?token=temporary-secret&nonce=123"),
|
||||
"https://example.com/reply?******");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void redactQueryStringShouldKeepUrlWithoutQueryString() {
|
||||
assertEquals(BaseWxCpServiceImpl.redactQueryString("https://example.com/reply"), "https://example.com/reply");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package me.chanjar.weixin.cp.api.impl;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import me.chanjar.weixin.common.util.json.GsonParser;
|
||||
import me.chanjar.weixin.cp.api.WxCpService;
|
||||
import me.chanjar.weixin.cp.bean.intelligentrobot.WxCpIntelligentRobotMessage;
|
||||
import me.chanjar.weixin.cp.util.crypto.WxCpIntelligentRobotCryptUtil;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.testng.Assert.assertEquals;
|
||||
|
||||
public class WxCpIntelligentRobotApiModeServiceTest {
|
||||
private static final String TOKEN = "test-token";
|
||||
private static final String AES_KEY = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFA";
|
||||
private static final String AI_BOT_ID = "bot_1";
|
||||
private static final String TIMESTAMP = "1710000000";
|
||||
private static final String NONCE = "test-nonce";
|
||||
|
||||
@Test
|
||||
public void shouldParseEncryptedCallbackMessage() {
|
||||
String callbackJson = "{\"msgid\":\"msg_1\",\"aibotid\":\"bot_1\",\"msgtype\":\"text\","
|
||||
+ "\"from\":{\"userid\":\"user_1\"},\"text\":{\"content\":\"hello\"}}";
|
||||
WxCpIntelligentRobotCryptUtil cryptUtil = new WxCpIntelligentRobotCryptUtil(TOKEN, AES_KEY, AI_BOT_ID);
|
||||
JsonObject encrypted = GsonParser.parse(cryptUtil.encrypt(callbackJson, TIMESTAMP, NONCE));
|
||||
WxCpIntelligentRobotServiceImpl service = new WxCpIntelligentRobotServiceImpl(mock(WxCpService.class));
|
||||
|
||||
WxCpIntelligentRobotMessage message = service.parseEncryptedCallbackMessage(
|
||||
encrypted.get("msg_signature").getAsString(), TIMESTAMP, NONCE, encrypted.get("encrypt").getAsString(),
|
||||
TOKEN, AES_KEY, AI_BOT_ID);
|
||||
|
||||
assertEquals(message.getMsgId(), "msg_1");
|
||||
assertEquals(message.getText().getContent(), "hello");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReplyThroughResponseUrlWithoutAccessToken() throws Exception {
|
||||
WxCpService cpService = mock(WxCpService.class);
|
||||
when(cpService.postWithoutToken(anyString(), anyString())).thenReturn("ok");
|
||||
WxCpIntelligentRobotServiceImpl service = new WxCpIntelligentRobotServiceImpl(cpService);
|
||||
String responseUrl = "https://example.com/response";
|
||||
String plainJson = "{\"msgtype\":\"text\"}";
|
||||
|
||||
assertEquals(service.replyMessage(responseUrl, plainJson, TOKEN, AES_KEY, AI_BOT_ID, TIMESTAMP, NONCE), "ok");
|
||||
|
||||
ArgumentCaptor<String> bodyCaptor = ArgumentCaptor.forClass(String.class);
|
||||
verify(cpService).postWithoutToken(org.mockito.ArgumentMatchers.eq(responseUrl), bodyCaptor.capture());
|
||||
JsonObject encrypted = GsonParser.parse(bodyCaptor.getValue());
|
||||
WxCpIntelligentRobotCryptUtil cryptUtil = new WxCpIntelligentRobotCryptUtil(TOKEN, AES_KEY, AI_BOT_ID);
|
||||
assertEquals(cryptUtil.decrypt(encrypted.get("msg_signature").getAsString(), TIMESTAMP, NONCE,
|
||||
encrypted.get("encrypt").getAsString()), plainJson);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package me.chanjar.weixin.cp.util.crypto;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import me.chanjar.weixin.common.util.json.GsonParser;
|
||||
import me.chanjar.weixin.common.error.WxRuntimeException;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
import static org.testng.Assert.assertEquals;
|
||||
|
||||
public class WxCpIntelligentRobotCryptUtilTest {
|
||||
private static final String TOKEN = "test-token";
|
||||
private static final String AES_KEY = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFA";
|
||||
private static final String AI_BOT_ID = "aibot-123";
|
||||
private static final String TIMESTAMP = "1710000000";
|
||||
private static final String NONCE = "test-nonce";
|
||||
|
||||
@Test
|
||||
public void encryptShouldProduceDecryptableJsonEnvelope() {
|
||||
WxCpIntelligentRobotCryptUtil cryptUtil = new WxCpIntelligentRobotCryptUtil(TOKEN, AES_KEY, AI_BOT_ID);
|
||||
String plainJson = "{\"msgtype\":\"text\",\"text\":{\"content\":\"hello\"}}";
|
||||
|
||||
JsonObject encrypted = GsonParser.parse(cryptUtil.encrypt(plainJson, TIMESTAMP, NONCE));
|
||||
|
||||
assertEquals(encrypted.get("timestamp").getAsString(), TIMESTAMP);
|
||||
assertEquals(encrypted.get("nonce").getAsString(), NONCE);
|
||||
assertEquals(cryptUtil.decrypt(encrypted.get("msg_signature").getAsString(), TIMESTAMP, NONCE,
|
||||
encrypted.get("encrypt").getAsString()), plainJson);
|
||||
}
|
||||
|
||||
@Test(expectedExceptions = WxRuntimeException.class)
|
||||
public void decryptShouldRejectMessageForAnotherRobot() {
|
||||
String plainJson = "{\"msgtype\":\"text\"}";
|
||||
WxCpIntelligentRobotCryptUtil source = new WxCpIntelligentRobotCryptUtil(TOKEN, AES_KEY, AI_BOT_ID);
|
||||
JsonObject encrypted = GsonParser.parse(source.encrypt(plainJson, TIMESTAMP, NONCE));
|
||||
WxCpIntelligentRobotCryptUtil otherRobot = new WxCpIntelligentRobotCryptUtil(TOKEN, AES_KEY, "aibot-456");
|
||||
|
||||
otherRobot.decrypt(encrypted.get("msg_signature").getAsString(), TIMESTAMP, NONCE,
|
||||
encrypted.get("encrypt").getAsString());
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@
|
||||
<class name="me.chanjar.weixin.cp.tp.service.impl.WxCpTpTagServiceImplTest"/>
|
||||
<class name="me.chanjar.weixin.cp.tp.service.impl.WxCpTpMessageServiceImplTest"/>
|
||||
<class name="me.chanjar.weixin.cp.api.impl.WxCpHrServiceImplTest"/>
|
||||
<class name="me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImplLogTest"/>
|
||||
<class name="me.chanjar.weixin.cp.api.impl.WxCpIntelligentRobotApiModeServiceTest"/>
|
||||
</classes>
|
||||
</test>
|
||||
|
||||
@@ -25,6 +27,7 @@
|
||||
<class name="me.chanjar.weixin.cp.bean.message.WxCpXmlOutVoiceMessageTest"/>
|
||||
<class name="me.chanjar.weixin.cp.bean.message.WxCpXmlOutTextMessageTest"/>
|
||||
<class name="me.chanjar.weixin.cp.bean.todo.WxCpTodoServiceMockTest"/>
|
||||
<class name="me.chanjar.weixin.cp.util.crypto.WxCpIntelligentRobotCryptUtilTest"/>
|
||||
</classes>
|
||||
</test>
|
||||
</suite>
|
||||
|
||||
Reference in New Issue
Block a user