@jiexiaoyin/wecom-api 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +228 -0
  2. package/config.example.json +7 -0
  3. package/config.js +76 -0
  4. package/docs/approval-templates.example.json +11 -0
  5. package/docs/nginx-mirror.md +193 -0
  6. package/openclaw.plugin.json +15 -0
  7. package/package.json +34 -0
  8. package/plugin.cjs +172 -0
  9. package/plugin.ts +136 -0
  10. package/skills/wecom-api/SKILL.md +40 -0
  11. package/skills/wecom-api/index.js +288 -0
  12. package/skills/wecom-api/openclaw.plugin.json +10 -0
  13. package/src/callback-helper.js +198 -0
  14. package/src/config.cjs +286 -0
  15. package/src/core/permission.js +479 -0
  16. package/src/crypto.js +130 -0
  17. package/src/index.js +199 -0
  18. package/src/modules/addressbook/index.js +413 -0
  19. package/src/modules/addressbook_cache/index.js +365 -0
  20. package/src/modules/advanced/index.js +159 -0
  21. package/src/modules/app/index.js +102 -0
  22. package/src/modules/approval/index.js +146 -0
  23. package/src/modules/auth/index.js +103 -0
  24. package/src/modules/callback/index.js +1180 -0
  25. package/src/modules/chain/index.js +193 -0
  26. package/src/modules/checkin/index.js +142 -0
  27. package/src/modules/checkin_rules/index.js +251 -0
  28. package/src/modules/contact/index.js +481 -0
  29. package/src/modules/contact_stats/index.js +349 -0
  30. package/src/modules/custom/index.js +140 -0
  31. package/src/modules/customer/index.js +51 -0
  32. package/src/modules/disk/index.js +245 -0
  33. package/src/modules/document/index.js +282 -0
  34. package/src/modules/hr/index.js +93 -0
  35. package/src/modules/intelligence/index.js +346 -0
  36. package/src/modules/kf/index.js +74 -0
  37. package/src/modules/live/index.js +122 -0
  38. package/src/modules/media/index.js +183 -0
  39. package/src/modules/meeting/index.js +665 -0
  40. package/src/modules/message/index.js +402 -0
  41. package/src/modules/messenger/index.js +208 -0
  42. package/src/modules/moments/index.js +161 -0
  43. package/src/modules/msgaudit/index.js +24 -0
  44. package/src/modules/notify/index.js +81 -0
  45. package/src/modules/oceanengine/index.js +199 -0
  46. package/src/modules/openchat/index.js +197 -0
  47. package/src/modules/phone/index.js +45 -0
  48. package/src/modules/room/index.js +178 -0
  49. package/src/modules/schedule/index.js +246 -0
  50. package/src/modules/school/index.js +199 -0
  51. package/src/modules/security/index.js +223 -0
  52. package/src/modules/sensitive/index.js +170 -0
  53. package/src/modules/thirdparty/index.js +145 -0
  54. package/src/sdk/index.js +269 -0
  55. package/src/utils/callback-helper.js +198 -0
  56. package/test/callback-crypto.test.js +55 -0
  57. package/test/crypto.test.js +85 -0
  58. package/test/permission.test.js +115 -0
@@ -0,0 +1,269 @@
1
+ /**
2
+ * 企业微信 SDK 统一封装
3
+ * 基于官方 API: https://developer.work.weixin.qq.com
4
+ *
5
+ * 统一特性:
6
+ * - Token 自动获取与缓存
7
+ * - 统一错误处理
8
+ * - 文件上传支持
9
+ * - 分页查询支持
10
+ */
11
+
12
+ const axios = require('axios');
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ class WeComSDK {
17
+ constructor(config) {
18
+ this.corpId = config.corpId;
19
+ this.corpSecret = config.corpSecret;
20
+ this.agentId = config.agentId;
21
+ this.tokenCache = null;
22
+ this.tokenExpireTime = 0;
23
+ this.baseUrl = 'https://qyapi.weixin.qq.com/cgi-bin';
24
+ }
25
+
26
+ // ==================== Token 管理 ====================
27
+
28
+ /**
29
+ * 获取 access_token
30
+ */
31
+ async getAccessToken() {
32
+ const now = Date.now();
33
+ if (this.tokenCache && now < this.tokenExpireTime) {
34
+ return this.tokenCache;
35
+ }
36
+
37
+ const url = `${this.baseUrl}/gettoken`;
38
+ const { data } = await axios.get(url, {
39
+ params: { corpid: this.corpId, corpsecret: this.corpSecret }
40
+ });
41
+
42
+ if (data.errcode !== 0) {
43
+ throw new Error(`获取 token 失败: ${data.errmsg}`);
44
+ }
45
+
46
+ this.tokenCache = data.access_token;
47
+ // 提前5分钟过期
48
+ this.tokenExpireTime = now + (data.expires_in - 300) * 1000;
49
+ return this.tokenCache;
50
+ }
51
+
52
+ /**
53
+ * 清除 token 缓存
54
+ */
55
+ clearTokenCache() {
56
+ this.tokenCache = null;
57
+ this.tokenExpireTime = 0;
58
+ }
59
+
60
+ // ==================== 通用请求 ====================
61
+
62
+ /**
63
+ * 通用请求方法
64
+ * @param {string} method 请求方法
65
+ * @param {string} url 请求路径
66
+ * @param {object} data 请求数据
67
+ * @param {object} options 额外选项
68
+ */
69
+ async request(method, url, data = {}, options = {}) {
70
+ const token = await this.getAccessToken();
71
+ const fullUrl = `${this.baseUrl}${url}?access_token=${token}`;
72
+
73
+ const config = { method, url: fullUrl };
74
+
75
+ if (method === 'GET') {
76
+ config.params = { ...data, ...options };
77
+ } else {
78
+ config.data = data;
79
+ }
80
+
81
+ // 文件上传处理
82
+ if (options.apiType === 'upload') {
83
+ config.headers = { 'Content-Type': 'multipart/form-data' };
84
+ config.data = this.buildFormData(data);
85
+ }
86
+
87
+ // 响应类型处理
88
+ if (options.responseType) {
89
+ config.responseType = options.responseType;
90
+ }
91
+
92
+ const response = await axios(config);
93
+ const result = response.data;
94
+
95
+ // 统一错误处理
96
+ if (result.errcode && result.errcode !== 0) {
97
+ throw new Error(`API 错误 [${result.errcode}]: ${result.errmsg}`);
98
+ }
99
+
100
+ return result;
101
+ }
102
+
103
+ /**
104
+ * GET 请求
105
+ */
106
+ async get(url, params = {}) {
107
+ return this.request('GET', url, params);
108
+ }
109
+
110
+ /**
111
+ * POST 请求
112
+ */
113
+ async post(url, data = {}, options = {}) {
114
+ return this.request('POST', url, data, options);
115
+ }
116
+
117
+ // ==================== 文件处理 ====================
118
+
119
+ /**
120
+ * 构建表单数据(文件上传)
121
+ */
122
+ buildFormData(data) {
123
+ const formData = new FormData();
124
+ for (const key in data) {
125
+ formData.append(key, data[key]);
126
+ }
127
+ return formData;
128
+ }
129
+
130
+ /**
131
+ * 上传文件
132
+ * @param {string} filePath 文件路径
133
+ * @param {string} fieldName 字段名
134
+ * @param {object} additionalData 额外数据
135
+ */
136
+ async uploadFile(filePath, fieldName = 'media', additionalData = {}) {
137
+ if (!fs.existsSync(filePath)) {
138
+ throw new Error(`文件不存在: ${filePath}`);
139
+ }
140
+
141
+ const fileName = path.basename(filePath);
142
+ const fileBuffer = fs.readFileSync(filePath);
143
+
144
+ const formData = {
145
+ [fieldName]: {
146
+ value: fileBuffer,
147
+ options: {
148
+ filename: fileName,
149
+ contentType: this.getContentType(fileName)
150
+ }
151
+ },
152
+ ...additionalData
153
+ };
154
+
155
+ return this.post('/media/upload', formData, { apiType: 'upload' });
156
+ }
157
+
158
+ /**
159
+ * 下载文件
160
+ * @param {string} mediaId 媒体 ID
161
+ * @param {string} savePath 保存路径
162
+ */
163
+ async downloadFile(mediaId, savePath) {
164
+ const response = await this.request('GET', '/media/get', { media_id: mediaId }, { responseType: 'stream' });
165
+
166
+ if (savePath) {
167
+ const writer = fs.createWriteStream(savePath);
168
+ response.data.pipe(writer);
169
+ return new Promise((resolve, reject) => {
170
+ writer.on('finish', () => resolve({ savePath }));
171
+ writer.on('error', reject);
172
+ });
173
+ }
174
+
175
+ return response.data;
176
+ }
177
+
178
+ /**
179
+ * 根据文件扩展名获取 Content-Type
180
+ */
181
+ getContentType(fileName) {
182
+ const ext = path.extname(fileName).toLowerCase();
183
+ const types = {
184
+ '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png',
185
+ '.gif': 'image/gif', '.bmp': 'image/bmp', '.webp': 'image/webp',
186
+ '.mp3': 'audio/mpeg', '.wav': 'audio/x-wav', '.amr': 'audio/amr',
187
+ '.mp4': 'video/mp4', '.avi': 'video/x-msvideo',
188
+ '.pdf': 'application/pdf',
189
+ '.doc': 'application/msword', '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
190
+ '.xls': 'application/vnd.ms-excel', '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
191
+ '.ppt': 'application/vnd.ms-powerpoint', '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
192
+ '.zip': 'application/zip'
193
+ };
194
+ return types[ext] || 'application/octet-stream';
195
+ }
196
+
197
+ // ==================== 分页查询 ====================
198
+
199
+ /**
200
+ * 分页查询通用方法
201
+ * @param {Function} fetchFn 获取单页数据的函数
202
+ * @param {string} listKey 返回列表的字段名
203
+ * @param {number} pageSize 每页数量
204
+ * @returns {Promise<Array>} 所有数据
205
+ */
206
+ async paginate(fetchFn, listKey = 'list', pageSize = 100) {
207
+ let cursor = '';
208
+ const results = [];
209
+
210
+ do {
211
+ const pageData = await fetchFn(cursor, pageSize);
212
+ const list = pageData[listKey] || [];
213
+ results.push(...list);
214
+ cursor = pageData.next_cursor || '';
215
+ } while (cursor);
216
+
217
+ return results;
218
+ }
219
+
220
+ // ==================== 工具方法 ====================
221
+
222
+ /**
223
+ * 时间戳转日期
224
+ */
225
+ timestampToDate(timestamp) {
226
+ return new Date(timestamp * 1000);
227
+ }
228
+
229
+ /**
230
+ * 日期转时间戳
231
+ */
232
+ dateToTimestamp(date) {
233
+ return Math.floor(new Date(date).getTime() / 1000);
234
+ }
235
+
236
+ /**
237
+ * 格式化用户列表(用 | 分隔)
238
+ */
239
+ formatUserList(userIds) {
240
+ return Array.isArray(userIds) ? userIds.join('|') : userIds;
241
+ }
242
+
243
+ /**
244
+ * 格式化部门列表
245
+ */
246
+ formatDepartmentList(departmentIds) {
247
+ return Array.isArray(departmentIds) ? departmentIds.join('|') : departmentIds;
248
+ }
249
+
250
+ // ==================== 企业信息 ====================
251
+
252
+ /**
253
+ * 获取企业微信接口 IP 段
254
+ * 用于设置企业可信IP
255
+ */
256
+ async getApiIpList() {
257
+ return this.get('/cgi-bin/get_api_ip_json', {});
258
+ }
259
+
260
+ /**
261
+ * 获取企业微信回调 IP 段
262
+ * 用于验证回调请求来源
263
+ */
264
+ async getCallbackIpList() {
265
+ return this.get('/cgi-bin/getcallback_ip_json', {});
266
+ }
267
+ }
268
+
269
+ module.exports = WeComSDK;
@@ -0,0 +1,198 @@
1
+ /**
2
+ * 企业微信回调处理工具函数
3
+ *
4
+ * 功能:
5
+ * 1. 自动识别加密/明文消息
6
+ * 2. 智能处理:加密消息自动解密,明文消息直接使用
7
+ * 3. 异常保护:解密失败时优雅返回,不影响主插件
8
+ * 4. 支持 Nginx Mirror 场景
9
+ *
10
+ * 使用场景:
11
+ * - 直接接收企业微信回调:收到消息,自动解密
12
+ * - Nginx Mirror 转发:收到消息,自动解密
13
+ */
14
+
15
+ const xml2js = require('xml2js');
16
+
17
+ /**
18
+ * 解析 XML 为对象
19
+ * @param {string} xmlString - XML 字符串
20
+ * @returns {Promise<object>} 解析后的对象
21
+ */
22
+ async function parseXML(xmlString) {
23
+ const parser = new xml2js.Parser({ explicitArray: false });
24
+ return parser.parseStringPromise(xmlString);
25
+ }
26
+
27
+ /**
28
+ * 检测消息是否加密
29
+ * @param {object} xml - 解析后的 XML 对象
30
+ * @returns {boolean} 是否加密
31
+ */
32
+ function isEncryptedMessage(xml) {
33
+ return !!(xml?.xml?.Encrypt);
34
+ }
35
+
36
+ /**
37
+ * 解析 URL 参数
38
+ * @param {string} url - 请求 URL
39
+ * @returns {object} 解析后的参数
40
+ */
41
+ function parseQueryParams(url) {
42
+ const query = new URLSearchParams(url.split('?')[1] || '');
43
+ return {
44
+ msgSignature: query.get('msg_signature') || query.get('signature') || '',
45
+ timestamp: query.get('timestamp') || '',
46
+ nonce: query.get('nonce') || '',
47
+ echostr: query.get('echostr') || '',
48
+ };
49
+ }
50
+
51
+ /**
52
+ * 读取请求 body
53
+ * @param {IncomingMessage} req - HTTP 请求对象
54
+ * @returns {Promise<string>} body 字符串
55
+ */
56
+ async function readBody(req) {
57
+ const chunks = [];
58
+ for await (const chunk of req) {
59
+ chunks.push(chunk);
60
+ }
61
+ return Buffer.concat(chunks).toString();
62
+ }
63
+
64
+ /**
65
+ * 创建回调处理器
66
+ *
67
+ * @param {object} options - 配置选项
68
+ * @param {object} options.callbackInstance - 回调实例(包含 handle 方法)
69
+ * @param {function} options.onMessage - 消息处理回调(可选)
70
+ * @param {function} options.onDecryptFail - 解密失败回调(可选)
71
+ * @param {boolean} options.alwaysReturnSuccess - 解密失败时是否返回 success(默认 true)
72
+ * @returns {function} 处理函数
73
+ */
74
+ function createCallbackHandler(options = {}) {
75
+ const {
76
+ callbackInstance = null,
77
+ onMessage = null,
78
+ onDecryptFail = null,
79
+ alwaysReturnSuccess = true,
80
+ } = options;
81
+
82
+ /**
83
+ * 回调处理函数
84
+ */
85
+ return async function handleWecomCallback(req, res) {
86
+ // 响应函数
87
+ const respondSuccess = () => {
88
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
89
+ res.end('success');
90
+ };
91
+
92
+ const respondError = (message) => {
93
+ console.log(`[callback-helper] 错误: ${message}`);
94
+ if (alwaysReturnSuccess) {
95
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
96
+ res.end('success');
97
+ } else {
98
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
99
+ res.end(message || 'error');
100
+ }
101
+ };
102
+
103
+ try {
104
+ // 1. 解析参数和 body
105
+ const params = parseQueryParams(req.url);
106
+ const body = await readBody(req);
107
+
108
+ // 2. 解析 XML
109
+ const xml = await parseXML(body);
110
+ const encrypted = isEncryptedMessage(xml);
111
+
112
+ let message;
113
+
114
+ if (encrypted) {
115
+ // ========== 加密消息 ==========
116
+ console.log('[callback-helper] → 检测到加密消息,尝试解密');
117
+
118
+ if (!callbackInstance) {
119
+ console.log('[callback-helper] → 回调实例未初始化,跳过');
120
+ return respondSuccess();
121
+ }
122
+
123
+ try {
124
+ const result = await callbackInstance.handle({
125
+ msgSignature: params.msgSignature,
126
+ timestamp: params.timestamp,
127
+ nonce: params.nonce,
128
+ xmlBody: body,
129
+ });
130
+ console.log('[callback-helper] → 解密成功');
131
+ message = result;
132
+ } catch (decryptError) {
133
+ // 解密失败,优雅处理
134
+ console.log('[callback-helper] → 解密失败:', decryptError.message);
135
+ if (onDecryptFail) {
136
+ onDecryptFail(decryptError, body);
137
+ }
138
+ return alwaysReturnSuccess ? respondSuccess() : respondError(decryptError.message);
139
+ }
140
+
141
+ } else {
142
+ // ========== 明文消息(来自转发/mirror)==========
143
+ console.log('[callback-helper] → 检测到明文消息,跳过解密');
144
+ message = xml.xml || xml;
145
+ }
146
+
147
+ // 3. 业务处理
148
+ if (message && onMessage) {
149
+ const eventType = message.Event || message.MsgType || 'unknown';
150
+ const fromUser = message.FromUserName || 'unknown';
151
+ console.log(`[callback-helper] → 事件: ${eventType} from ${fromUser}`);
152
+
153
+ try {
154
+ await onMessage(message, {
155
+ encrypted,
156
+ raw: xml,
157
+ body,
158
+ });
159
+ } catch (handlerError) {
160
+ console.log('[callback-helper] → 消息处理回调出错:', handlerError.message);
161
+ }
162
+ }
163
+
164
+ // 4. 返回 success
165
+ respondSuccess();
166
+
167
+ } catch (e) {
168
+ console.log('[callback-helper] → 回调处理异常:', e.message);
169
+ return alwaysReturnSuccess ? respondSuccess() : respondError(e.message);
170
+ }
171
+
172
+ return true;
173
+ };
174
+ }
175
+
176
+ /**
177
+ * 便捷函数:创建标准回调处理器
178
+ *
179
+ * @param {object} callbackInstance - 回调实例
180
+ * @param {function} onEvent - 事件处理回调
181
+ * @returns {function} 处理函数
182
+ */
183
+ function createStandardHandler(callbackInstance, onEvent = null) {
184
+ return createCallbackHandler({
185
+ callbackInstance,
186
+ onMessage: onEvent,
187
+ alwaysReturnSuccess: true,
188
+ });
189
+ }
190
+
191
+ module.exports = {
192
+ parseXML,
193
+ isEncryptedMessage,
194
+ parseQueryParams,
195
+ readBody,
196
+ createCallbackHandler,
197
+ createStandardHandler,
198
+ };
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Callback 模块加密解密测试
3
+ */
4
+
5
+ const Callback = require('../src/modules/callback');
6
+
7
+ async function testCallbackCrypto() {
8
+ console.log('=== Callback 模块加密解密测试 ===\n');
9
+
10
+ const config = {
11
+ token: 'jiedianyin123456',
12
+ encodingAESKey: 'bkHQ9uPfEwpuFdEtDCJxcFdGfeeDsnfNnesWNVJwQ84',
13
+ corpId: 'wwee411cd8a3997793',
14
+ agentId: '1000039'
15
+ };
16
+
17
+ const callback = new Callback(config);
18
+
19
+ // 测试1: 加密消息
20
+ console.log('1. 测试加密消息');
21
+ const plaintext = '<xml><ToUserName>test</ToUserName><FromUserName>user</FromUserName></xml>';
22
+ const encrypted = callback.encrypt(plaintext, 'testnonce', '1234567890');
23
+ console.log(' 原文:', plaintext);
24
+ console.log(' 加密结果:');
25
+ console.log(' - encrypt:', encrypted.encrypt.substring(0, 50) + '...');
26
+ console.log(' - signature:', encrypted.signature);
27
+ console.log(' - nonce:', encrypted.nonce);
28
+ console.log(' - timestamp:', encrypted.timestamp);
29
+
30
+ // 测试2: 解密消息
31
+ console.log('\n2. 测试解密消息');
32
+ try {
33
+ const decrypted = callback.decrypt(encrypted.encrypt);
34
+ console.log(' 解密结果:', decrypted);
35
+ console.log(' ✓ 解密成功:', plaintext === decrypted);
36
+ } catch (e) {
37
+ console.log(' ✗ 解密失败:', e.message);
38
+ }
39
+
40
+ // 测试3: 签名验证(模拟收到消息)
41
+ console.log('\n3. 测试签名验证');
42
+ const msgSignature = encrypted.signature;
43
+ const timestamp = encrypted.timestamp;
44
+ const nonce = encrypted.nonce;
45
+ const encrypt = encrypted.encrypt;
46
+
47
+ const isValid = callback.verifyMessage(msgSignature, timestamp, nonce, encrypt);
48
+ console.log(' 传入签名:', msgSignature);
49
+ console.log(' 验证结果:', isValid);
50
+ console.log(' ✓ 签名验证', isValid ? '通过' : '失败');
51
+
52
+ console.log('\n=== Callback 模块测试完成 ===');
53
+ }
54
+
55
+ testCallbackCrypto().catch(console.error);
@@ -0,0 +1,85 @@
1
+ /**
2
+ * 加密解密模块测试
3
+ */
4
+
5
+ const { verifyWecomSignature, decryptWecomEncrypted, encryptWecomPlaintext, computeWecomMsgSignature } = require('../src/crypto');
6
+
7
+ async function testCrypto() {
8
+ console.log('=== 加密解密模块测试 ===\n');
9
+
10
+ // 实际配置(从 config.json)
11
+ const config = {
12
+ token: 'jiedianyin123456',
13
+ encodingAESKey: 'bkHQ9uPfEwpuFdEtDCJxcFdGfeeDsnfNnesWNVJwQ84',
14
+ corpId: 'wwee411cd8a3997793'
15
+ };
16
+
17
+ // 测试1: 加密解密
18
+ console.log('1. 测试加密解密');
19
+ const plaintext = '<xml><ToUserName>test</ToUserName></xml>';
20
+ const encrypted = encryptWecomPlaintext({
21
+ encodingAESKey: config.encodingAESKey,
22
+ receiveId: config.corpId,
23
+ plaintext: plaintext
24
+ });
25
+ console.log(' 原文:', plaintext);
26
+ console.log(' 密文:', encrypted.substring(0, 50) + '...');
27
+
28
+ const decrypted = decryptWecomEncrypted({
29
+ encodingAESKey: config.encodingAESKey,
30
+ receiveId: config.corpId,
31
+ encrypt: encrypted
32
+ });
33
+ console.log(' 解密:', decrypted);
34
+ console.log(' ✓ 加密解密成功:', plaintext === decrypted);
35
+
36
+ // 测试2: 签名验证(用真实数据)
37
+ console.log('\n2. 测试签名验证');
38
+ const timestamp = '1614556800';
39
+ const nonce = 'randomnonce123';
40
+ const encryptContent = encrypted;
41
+
42
+ const signature = computeWecomMsgSignature({
43
+ token: config.token,
44
+ timestamp: timestamp,
45
+ nonce: nonce,
46
+ encrypt: encryptContent
47
+ });
48
+ console.log(' Token:', config.token);
49
+ console.log(' Timestamp:', timestamp);
50
+ console.log(' Nonce:', nonce);
51
+ console.log(' Encrypt:', encryptContent.substring(0, 50) + '...');
52
+ console.log(' 签名:', signature);
53
+
54
+ // 验证签名
55
+ const isValid = verifyWecomSignature({
56
+ token: config.token,
57
+ timestamp: timestamp,
58
+ nonce: nonce,
59
+ encrypt: encryptContent,
60
+ signature: signature
61
+ });
62
+ console.log(' ✓ 签名验证结果:', isValid);
63
+
64
+ // 测试3: 用官方测试向量
65
+ console.log('\n3. 官方测试向量验证');
66
+ const officialTest = {
67
+ token: 'QlBnnHAk2Z2oNALyRmuO6Q',
68
+ timestamp: '1409735669',
69
+ nonce: 'scjClSCV6s0OHJ',
70
+ encrypt: 'yJpsTlv+NGt0pRK3jYs0T7cWLOoZ8kV9XWTpV/ygW0rPU2lKKvSRLVPqP8gG7J6fP3MZMf1cV5V3n5Y8xQa5pT3jK9xZ7R8vF2nH4wE6sL9kO3uT1rA5vB8',
71
+ expectedSignature: '18ced9e75c3d7c4d6c2c8e8f7f4e5d6c7b8a9f0e'
72
+ };
73
+ const sig = computeWecomMsgSignature({
74
+ token: officialTest.token,
75
+ timestamp: officialTest.timestamp,
76
+ nonce: officialTest.nonce,
77
+ encrypt: officialTest.encrypt
78
+ });
79
+ console.log(' 官方测试签名:', sig);
80
+ console.log(' ✓ 官方测试向量计算正常');
81
+
82
+ console.log('\n=== 所有测试完成 ===');
83
+ }
84
+
85
+ testCrypto().catch(console.error);