Skip to content

Commit ae7b04e

Browse files
committed
feat: 支持企业微信智能机器人API模式
1 parent dceeaf3 commit ae7b04e

8 files changed

Lines changed: 353 additions & 28 deletions

File tree

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# 企业微信智能机器人 API 模式支持 Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** 在不影响旧 access-token 接口的前提下,支持新版智能机器人 API 模式回调解密和 response_url 加密回复。
6+
7+
**Architecture:** 用独立密码工具承载机器人 API 模式的 Token、AESKey、机器人 ID,避免误用企业应用配置。服务接口复用该工具解析回调,并以原始临时 URL 执行 HTTP POST,文档只展示这条链路。
8+
9+
**Tech Stack:** Java 8、Gson、现有 `WxCryptUtil`、Apache HttpClient 5、JUnit 5。
10+
11+
## Global Constraints
12+
13+
- 不删除或改变现有 `WxCpIntelligentRobotService` 的 access-token 方法。
14+
- 不在 `response_url` 上拼接 `access_token`
15+
- 所有生产行为先由失败的单测定义。
16+
17+
---
18+
19+
### Task 1: API 模式密码工具
20+
21+
**Files:**
22+
- Create: `weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/crypto/WxCpIntelligentRobotCryptUtil.java`
23+
- Test: `weixin-java-cp/src/test/java/me/chanjar/weixin/cp/util/crypto/WxCpIntelligentRobotCryptUtilTest.java`
24+
25+
**Interfaces:**
26+
- Produces: `WxCpIntelligentRobotCryptUtil(String token, String encodingAesKey, String aiBotId)` with `decrypt`, `encrypt`, and `verifyUrl` methods.
27+
28+
- [ ] **Step 1: Write failing encryption round-trip tests**
29+
30+
```java
31+
assertEquals(plainJson, cryptUtil.decrypt(signature, timestamp, nonce,
32+
cryptUtil.encrypt(plainJson, timestamp, nonce)));
33+
```
34+
35+
- [ ] **Step 2: Run the test to verify it fails**
36+
37+
Run: `mvn -pl weixin-java-cp -Dtest=WxCpIntelligentRobotCryptUtilTest test`
38+
Expected: compilation failure because the utility does not exist.
39+
40+
- [ ] **Step 3: Implement the minimal utility**
41+
42+
Extend `WxCryptUtil`, initialize `token`, decoded `aesKey`, and `appidOrCorpid` from robot settings; delegate its encrypt/decrypt mechanics and convert encrypted JSON to the API-mode envelope.
43+
44+
- [ ] **Step 4: Run the test to verify it passes**
45+
46+
Run: `mvn -pl weixin-java-cp -Dtest=WxCpIntelligentRobotCryptUtilTest test`
47+
Expected: PASS.
48+
49+
### Task 2: Service parsing and response posting
50+
51+
**Files:**
52+
- Modify: `weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpIntelligentRobotService.java`
53+
- Modify: `weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpIntelligentRobotServiceImpl.java`
54+
- Test: `weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpIntelligentRobotServiceImplTest.java`
55+
56+
**Interfaces:**
57+
- Consumes: `WxCpIntelligentRobotCryptUtil` from Task 1.
58+
- Produces: encrypted callback parser and `response_url` reply method.
59+
60+
- [ ] **Step 1: Write failing tests for encrypted callback parsing and reply request**
61+
62+
```java
63+
assertEquals("text", service.parseEncryptedCallbackMessage(...).getMsgType());
64+
assertEquals(plainJson, decryptPostedBody(localResponseUrl));
65+
```
66+
67+
- [ ] **Step 2: Run the targeted tests to verify they fail**
68+
69+
Run: `mvn -pl weixin-java-cp -Dtest=WxCpIntelligentRobotServiceImplTest test`
70+
Expected: compilation failure because new service methods do not exist.
71+
72+
- [ ] **Step 3: Implement minimal service methods**
73+
74+
Construct the dedicated crypt utility, parse its plaintext with `WxCpIntelligentRobotMessage.fromJson`, encrypt outgoing JSON, and invoke the raw URL through the existing HTTP client abstraction without token refresh.
75+
76+
- [ ] **Step 4: Run targeted tests to verify they pass**
77+
78+
Run: `mvn -pl weixin-java-cp -Dtest=WxCpIntelligentRobotServiceImplTest test`
79+
Expected: PASS.
80+
81+
### Task 3: Correct public documentation
82+
83+
**Files:**
84+
- Modify: `weixin-java-cp/INTELLIGENT_ROBOT.md`
85+
86+
**Interfaces:**
87+
- Consumes: final API names from Tasks 1 and 2.
88+
89+
- [ ] **Step 1: Replace XML and access-token examples for API mode**
90+
91+
Document robot-console configuration, encrypted JSON callback parsing, and `response_url` replies. Mark the pre-existing create/chat/send methods as legacy access-token endpoints.
92+
93+
- [ ] **Step 2: Verify all documented symbols exist**
94+
95+
Run: `rg -n 'parseEncryptedCallbackMessage|replyMessage' weixin-java-cp/src/main/java`
96+
Expected: both APIs are found.
97+
98+
### Task 4: Full verification and publication
99+
100+
**Files:**
101+
- Modify: all files from Tasks 1–3.
102+
103+
- [ ] **Step 1: Run module test suite**
104+
105+
Run: `mvn -pl weixin-java-cp test`
106+
Expected: PASS.
107+
108+
- [ ] **Step 2: Inspect scope and commit only intended files**
109+
110+
Run: `git status --short && git diff --check`
111+
Expected: only API-mode implementation, tests, and docs are changed; no whitespace errors.
112+
113+
- [ ] **Step 3: Publish a draft PR**
114+
115+
Run: `git add -- <intended paths> && git commit -m 'feat: 支持企业微信智能机器人 API 模式' && git pull --rebase && git push`
116+
Expected: branch is pushed and a draft PR targets `develop`.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# 企业微信智能机器人 API 模式支持设计
2+
3+
## 目标
4+
5+
`weixin-java-cp` 能接入企业微信当前的智能机器人 API 模式:解密回调 JSON、解析消息,并将加密回复 POST 到回调给出的 `response_url`
6+
7+
## 范围与边界
8+
9+
- 保留已有基于企业应用 `access_token``WxCpIntelligentRobotService` 方法,避免破坏兼容性;这些方法不用于新版 API 模式。
10+
- 新增独立的 API 模式密码工具,使用机器人后台配置的 Token、EncodingAESKey 和机器人 ID(作为接收方标识),不依赖 `WxCpConfigStorage``secret`
11+
- 回调入口将加密 JSON 信封解密成明文 JSON,再交给已有 `WxCpIntelligentRobotMessage` 解析。
12+
- 回复 API 接收 `response_url` 与明文业务 JSON,完成加密、签名和 POST;它不追加 `access_token`
13+
- 文档只展示新版 API 模式的正确链路,并明确旧服务的方法边界。
14+
15+
## 接口设计
16+
17+
新增 `WxCpIntelligentRobotCryptUtil(token, encodingAesKey, aiBotId)`
18+
19+
- `decrypt(msgSignature, timestamp, nonce, encryptedJson)` 返回回调明文 JSON;
20+
- `encrypt(plainJson, timestamp, nonce)` 返回可直接 POST 的加密 JSON 信封;
21+
- `verifyUrl(msgSignature, timestamp, nonce, echoStr)` 验证 URL 时解密 echo 字段。
22+
23+
`WxCpIntelligentRobotService` 新增:
24+
25+
- `parseEncryptedCallbackMessage(...)`,解密后返回 `WxCpIntelligentRobotMessage`
26+
- `replyMessage(responseUrl, plainJson, ...)`,将加密响应 POST 到临时 URL。
27+
28+
## 验证
29+
30+
单测覆盖已知密文回调可解密并解析,及加密后的回复可被同一配置解密回原文;HTTP 层以本地 mock 服务验证不携带 access token、正文为加密 JSON。模块测试与格式检查通过。

weixin-java-cp/INTELLIGENT_ROBOT.md

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# 企业微信智能机器人接口
22

3-
本模块提供企业微信智能机器人相关的API接口实现。
3+
本模块提供企业微信智能机器人相关的 API 接口实现。
4+
5+
> `createRobot``chat``sendMessage` 等既有方法走企业应用 `access_token` 接口,
6+
> 需要在 `WxCpConfigStorage` 中配置应用 `agentId``secret`。它们不适用于机器人后台创建的新版 API 模式。
47
58
## 官方文档
69

@@ -73,7 +76,7 @@ String sessionId = "session123";
7376
robotService.resetSession(robotId, userid, sessionId);
7477
```
7578

76-
### 主动发送消息
79+
### 旧版 access_token 主动发送消息
7780

7881
智能机器人可以主动向用户发送消息,用于推送通知或提醒。
7982

@@ -89,34 +92,29 @@ String msgId = response.getMsgId();
8992
String sessionId = response.getSessionId();
9093
```
9194

92-
### 接收用户消息
95+
### 新版 API 模式:接收回调与回复消息
9396

94-
当用户向智能机器人发送消息时,企业微信会通过回调接口推送消息。可以使用 `WxCpXmlMessage` 接收和解析这些消息:
97+
在机器人后台开启 API 模式后,配置 URL、Token、EncodingAESKey。企业微信会推送加密 JSON 回调;
98+
它不是 XML,也不需要企业应用 `secret`。从请求参数取得 `msg_signature``timestamp``nonce`
99+
从请求体取得 `encrypt` 字段后,可以直接解密和解析:
95100

96101
```java
97-
// 在接收回调消息的接口中
98-
WxCpXmlMessage message = WxCpXmlMessage.fromEncryptedXml(
99-
requestBody, wxCpConfigStorage, timestamp, nonce, msgSignature
100-
);
101-
102-
// 获取智能机器人相关字段
103-
String robotId = message.getRobotId(); // 机器人ID
104-
String sessionId = message.getSessionId(); // 会话ID
105-
String content = message.getContent(); // 消息内容
106-
String fromUser = message.getFromUserName(); // 发送用户
107-
108-
// 处理消息并回复
109-
// ...
102+
WxCpIntelligentRobotMessage callbackMessage =
103+
robotService.parseEncryptedCallbackMessage(
104+
msgSignature, timestamp, nonce, encryptedJson,
105+
token, encodingAesKey, aiBotId);
106+
107+
String responseUrl = callbackMessage.getResponseUrl();
108+
String content = callbackMessage.getText().getContent();
110109
```
111110

112-
对于智能机器人 API 模式的 JSON 回调消息,可使用 `WxCpIntelligentRobotMessage` 解析
111+
回复时使用回调中的短期 `response_url`,不调用基于 `access_token``sendMessage`
113112

114113
```java
115-
WxCpIntelligentRobotMessage callbackMessage =
116-
robotService.parseCallbackMessage(jsonBody);
117-
String botId = callbackMessage.getAiBotId();
118-
String userId = callbackMessage.getFrom().getUserid();
119-
String msgType = callbackMessage.getMsgType();
114+
String replyJson = "{\"msgtype\":\"text\",\"text\":{\"content\":\"您好\"}}";
115+
robotService.replyMessage(
116+
responseUrl, replyJson, token, encodingAesKey, aiBotId,
117+
String.valueOf(System.currentTimeMillis() / 1000), java.util.UUID.randomUUID().toString());
120118
```
121119

122120
### 删除智能机器人
@@ -144,7 +142,8 @@ robotService.deleteRobot(robotId);
144142

145143
### 消息接收
146144

147-
- `WxCpXmlMessage`: 支持接收智能机器人回调消息,包含 `robotId``sessionId` 字段
145+
- `WxCpIntelligentRobotMessage`: 智能机器人 API 模式的已解密 JSON 回调消息
146+
- `WxCpIntelligentRobotCryptUtil`: 智能机器人 API 模式的消息加解密工具
148147

149148
### 服务接口
150149

@@ -153,7 +152,6 @@ robotService.deleteRobot(robotId);
153152

154153
## 注意事项
155154

156-
1. 需要确保企业微信应用具有智能机器人相关权限
157-
2. 智能机器人功能可能需要特定的企业微信版本支持
158-
3. 会话ID可以用于保持对话的连续性,提升用户体验
159-
4. 机器人状态: 0表示停用,1表示启用
155+
1. 新版 API 模式的 Token、EncodingAESKey 和机器人 ID 由机器人后台配置,不要填写企业应用 secret。
156+
2. `response_url` 是回调附带的临时地址,应及时使用,且不应持久化。
157+
3. `parseCallbackMessage` 仅用于已解密的 JSON;HTTP 回调入口应使用 `parseEncryptedCallbackMessage`

weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpIntelligentRobotService.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,36 @@ public interface WxCpIntelligentRobotService {
8282
*/
8383
WxCpIntelligentRobotMessage parseCallbackMessage(String callbackMessageJson);
8484

85+
/**
86+
* 解密并解析智能机器人 API 模式回调消息.
87+
*
88+
* @param msgSignature 回调 URL 参数中的签名
89+
* @param timestamp 回调 URL 参数中的时间戳
90+
* @param nonce 回调 URL 参数中的随机串
91+
* @param encryptedJson 回调 JSON 信封中的 encrypt 字段
92+
* @param token 机器人后台配置的 Token
93+
* @param encodingAesKey 机器人后台配置的 EncodingAESKey
94+
* @param aiBotId 机器人 ID
95+
* @return 解密并解析后的回调消息
96+
*/
97+
WxCpIntelligentRobotMessage parseEncryptedCallbackMessage(String msgSignature, String timestamp, String nonce,
98+
String encryptedJson, String token, String encodingAesKey,
99+
String aiBotId);
100+
101+
/**
102+
* 加密并向智能机器人 API 模式的临时 response_url 回复消息.
103+
*
104+
* @param responseUrl 回调消息中的 response_url
105+
* @param plainJson 回复的明文 JSON
106+
* @param token 机器人后台配置的 Token
107+
* @param encodingAesKey 机器人后台配置的 EncodingAESKey
108+
* @param aiBotId 机器人 ID
109+
* @param timestamp 回复时间戳
110+
* @param nonce 回复随机串
111+
* @return 企业微信响应内容
112+
* @throws WxErrorException 微信接口异常
113+
*/
114+
String replyMessage(String responseUrl, String plainJson, String token, String encodingAesKey, String aiBotId,
115+
String timestamp, String nonce) throws WxErrorException;
116+
85117
}

weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpIntelligentRobotServiceImpl.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import me.chanjar.weixin.cp.api.WxCpIntelligentRobotService;
77
import me.chanjar.weixin.cp.api.WxCpService;
88
import me.chanjar.weixin.cp.bean.intelligentrobot.*;
9+
import me.chanjar.weixin.cp.util.crypto.WxCpIntelligentRobotCryptUtil;
910
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
1011

1112
import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.IntelligentRobot.*;
@@ -72,4 +73,19 @@ public WxCpIntelligentRobotMessage parseCallbackMessage(String callbackMessageJs
7273
return WxCpIntelligentRobotMessage.fromJson(callbackMessageJson);
7374
}
7475

76+
@Override
77+
public WxCpIntelligentRobotMessage parseEncryptedCallbackMessage(String msgSignature, String timestamp, String nonce,
78+
String encryptedJson, String token,
79+
String encodingAesKey, String aiBotId) {
80+
WxCpIntelligentRobotCryptUtil cryptUtil = new WxCpIntelligentRobotCryptUtil(token, encodingAesKey, aiBotId);
81+
return parseCallbackMessage(cryptUtil.decrypt(msgSignature, timestamp, nonce, encryptedJson));
82+
}
83+
84+
@Override
85+
public String replyMessage(String responseUrl, String plainJson, String token, String encodingAesKey,
86+
String aiBotId, String timestamp, String nonce) throws WxErrorException {
87+
WxCpIntelligentRobotCryptUtil cryptUtil = new WxCpIntelligentRobotCryptUtil(token, encodingAesKey, aiBotId);
88+
return this.cpService.postWithoutToken(responseUrl, cryptUtil.encrypt(plainJson, timestamp, nonce));
89+
}
90+
7591
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package me.chanjar.weixin.cp.util.crypto;
2+
3+
import com.google.gson.JsonObject;
4+
import me.chanjar.weixin.common.util.crypto.SHA1;
5+
import me.chanjar.weixin.common.util.crypto.WxCryptUtil;
6+
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
7+
8+
import java.util.UUID;
9+
10+
/**
11+
* 企业微信智能机器人 API 模式消息加解密工具.
12+
*
13+
* <p>机器人 API 模式使用机器人后台配置的 Token、EncodingAESKey 和机器人 ID,
14+
* 与企业应用 access_token 无关。</p>
15+
*/
16+
public class WxCpIntelligentRobotCryptUtil extends WxCryptUtil {
17+
18+
public WxCpIntelligentRobotCryptUtil(String token, String encodingAesKey, String aiBotId) {
19+
super(token, encodingAesKey, aiBotId);
20+
}
21+
22+
/**
23+
* 解密机器人 API 模式的 JSON 回调消息.
24+
*/
25+
public String decrypt(String msgSignature, String timestamp, String nonce, String encryptedContent) {
26+
return decryptContent(msgSignature, timestamp, nonce, encryptedContent);
27+
}
28+
29+
/**
30+
* 加密机器人 API 模式的 JSON 回复消息.
31+
*/
32+
public String encrypt(String plainJson, String timestamp, String nonce) {
33+
String encryptedContent = encrypt(UUID.randomUUID().toString().replace("-", "").substring(0, 16), plainJson);
34+
JsonObject result = new JsonObject();
35+
result.addProperty("encrypt", encryptedContent);
36+
result.addProperty("msg_signature", SHA1.gen(this.token, timestamp, nonce, encryptedContent));
37+
result.addProperty("timestamp", timestamp);
38+
result.addProperty("nonce", nonce);
39+
return WxCpGsonBuilder.create().toJson(result);
40+
}
41+
42+
/**
43+
* 解密 URL 校验请求中的 echostr.
44+
*/
45+
public String verifyUrl(String msgSignature, String timestamp, String nonce, String echoStr) {
46+
return decrypt(msgSignature, timestamp, nonce, echoStr);
47+
}
48+
}

0 commit comments

Comments
 (0)