@mocrane/wecom 2026.2.5

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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +0 -0
  3. package/clawdbot.plugin.json +10 -0
  4. package/index.ts +28 -0
  5. package/openclaw.plugin.json +10 -0
  6. package/package.json +81 -0
  7. package/src/accounts.ts +72 -0
  8. package/src/agent/api-client.ts +336 -0
  9. package/src/agent/handler.ts +566 -0
  10. package/src/agent/index.ts +12 -0
  11. package/src/channel.ts +259 -0
  12. package/src/config/accounts.ts +99 -0
  13. package/src/config/index.ts +12 -0
  14. package/src/config/media.ts +14 -0
  15. package/src/config/network.ts +16 -0
  16. package/src/config/schema.ts +104 -0
  17. package/src/config-schema.ts +41 -0
  18. package/src/crypto/aes.ts +108 -0
  19. package/src/crypto/index.ts +24 -0
  20. package/src/crypto/signature.ts +43 -0
  21. package/src/crypto/xml.ts +49 -0
  22. package/src/crypto.test.ts +32 -0
  23. package/src/crypto.ts +176 -0
  24. package/src/http.ts +102 -0
  25. package/src/media.test.ts +55 -0
  26. package/src/media.ts +55 -0
  27. package/src/monitor/state.queue.test.ts +185 -0
  28. package/src/monitor/state.ts +514 -0
  29. package/src/monitor/types.ts +136 -0
  30. package/src/monitor.active.test.ts +239 -0
  31. package/src/monitor.integration.test.ts +207 -0
  32. package/src/monitor.ts +1802 -0
  33. package/src/monitor.webhook.test.ts +311 -0
  34. package/src/onboarding.ts +472 -0
  35. package/src/outbound.test.ts +143 -0
  36. package/src/outbound.ts +200 -0
  37. package/src/runtime.ts +14 -0
  38. package/src/shared/command-auth.ts +101 -0
  39. package/src/shared/index.ts +5 -0
  40. package/src/shared/xml-parser.test.ts +30 -0
  41. package/src/shared/xml-parser.ts +183 -0
  42. package/src/target.ts +80 -0
  43. package/src/types/account.ts +76 -0
  44. package/src/types/config.ts +88 -0
  45. package/src/types/constants.ts +42 -0
  46. package/src/types/global.d.ts +9 -0
  47. package/src/types/index.ts +38 -0
  48. package/src/types/message.ts +185 -0
  49. package/src/types.ts +159 -0
@@ -0,0 +1,311 @@
1
+ import { IncomingMessage, ServerResponse } from "node:http";
2
+ import { Socket } from "node:net";
3
+
4
+ import { describe, expect, it } from "vitest";
5
+
6
+ import type { OpenClawConfig } from "openclaw/plugin-sdk";
7
+
8
+ import type { ResolvedWecomAccount } from "./types.js";
9
+ import { computeWecomMsgSignature, decryptWecomEncrypted, encryptWecomPlaintext } from "./crypto.js";
10
+ import { handleWecomWebhookRequest, registerWecomWebhookTarget } from "./monitor.js";
11
+
12
+ function createMockRequest(params: {
13
+ method: "GET" | "POST";
14
+ url: string;
15
+ body?: unknown;
16
+ }): IncomingMessage {
17
+ const socket = new Socket();
18
+ const req = new IncomingMessage(socket);
19
+ req.method = params.method;
20
+ req.url = params.url;
21
+ if (params.method === "POST") {
22
+ req.push(JSON.stringify(params.body ?? {}));
23
+ }
24
+ req.push(null);
25
+ return req;
26
+ }
27
+
28
+ function createMockResponse(): ServerResponse & {
29
+ _getData: () => string;
30
+ _getStatusCode: () => number;
31
+ } {
32
+ const req = new IncomingMessage(new Socket());
33
+ const res = new ServerResponse(req);
34
+ let data = "";
35
+ res.write = (chunk: any) => {
36
+ data += String(chunk);
37
+ return true;
38
+ };
39
+ res.end = (chunk: any) => {
40
+ if (chunk) data += String(chunk);
41
+ return res;
42
+ };
43
+ (res as any)._getData = () => data;
44
+ (res as any)._getStatusCode = () => res.statusCode;
45
+ return res as any;
46
+ }
47
+
48
+ describe("handleWecomWebhookRequest", () => {
49
+ const token = "test-token";
50
+ const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
51
+
52
+ it("handles GET url verification", async () => {
53
+ const account: ResolvedWecomAccount = {
54
+ accountId: "default",
55
+ name: "Test",
56
+ enabled: true,
57
+ configured: true,
58
+ token,
59
+ encodingAESKey,
60
+ receiveId: "",
61
+ config: { webhookPath: "/hook", token, encodingAESKey },
62
+ };
63
+
64
+ const unregister = registerWecomWebhookTarget({
65
+ account,
66
+ config: {} as OpenClawConfig,
67
+ runtime: {},
68
+ core: {} as any,
69
+ path: "/hook",
70
+ });
71
+
72
+ try {
73
+ const timestamp = "13500001234";
74
+ const nonce = "123412323";
75
+ const echostr = encryptWecomPlaintext({
76
+ encodingAESKey,
77
+ receiveId: "",
78
+ plaintext: "ping",
79
+ });
80
+ const msg_signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt: echostr });
81
+ const req = createMockRequest({
82
+ method: "GET",
83
+ url: `/hook?msg_signature=${encodeURIComponent(msg_signature)}&timestamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}&echostr=${encodeURIComponent(echostr)}`,
84
+ });
85
+ const res = createMockResponse();
86
+ const handled = await handleWecomWebhookRequest(req, res);
87
+ expect(handled).toBe(true);
88
+ expect(res._getStatusCode()).toBe(200);
89
+ expect(res._getData()).toBe("ping");
90
+ } finally {
91
+ unregister();
92
+ }
93
+ });
94
+
95
+ it("handles POST callback and returns encrypted stream placeholder", async () => {
96
+ const account: ResolvedWecomAccount = {
97
+ accountId: "default",
98
+ name: "Test",
99
+ enabled: true,
100
+ configured: true,
101
+ token,
102
+ encodingAESKey,
103
+ receiveId: "",
104
+ config: { webhookPath: "/hook", token, encodingAESKey },
105
+ };
106
+
107
+ const unregister = registerWecomWebhookTarget({
108
+ account,
109
+ config: {} as OpenClawConfig,
110
+ runtime: {},
111
+ core: {} as any,
112
+ path: "/hook",
113
+ });
114
+
115
+ try {
116
+ const timestamp = "1700000000";
117
+ const nonce = "nonce";
118
+ const plain = JSON.stringify({
119
+ msgid: "MSGID",
120
+ aibotid: "AIBOTID",
121
+ chattype: "single",
122
+ from: { userid: "USERID" },
123
+ response_url: "RESPONSEURL",
124
+ msgtype: "text",
125
+ text: { content: "hello" },
126
+ });
127
+ const encrypt = encryptWecomPlaintext({ encodingAESKey, receiveId: "", plaintext: plain });
128
+ const msg_signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt });
129
+
130
+ const req = createMockRequest({
131
+ method: "POST",
132
+ url: `/hook?msg_signature=${encodeURIComponent(msg_signature)}&timestamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}`,
133
+ body: { encrypt },
134
+ });
135
+ const res = createMockResponse();
136
+ const handled = await handleWecomWebhookRequest(req, res);
137
+ expect(handled).toBe(true);
138
+ expect(res._getStatusCode()).toBe(200);
139
+
140
+ const json = JSON.parse(res._getData()) as any;
141
+ expect(typeof json.encrypt).toBe("string");
142
+ expect(typeof json.msgsignature).toBe("string");
143
+ expect(typeof json.timestamp).toBe("string");
144
+ expect(typeof json.nonce).toBe("string");
145
+
146
+ const replyPlain = decryptWecomEncrypted({
147
+ encodingAESKey,
148
+ receiveId: "",
149
+ encrypt: json.encrypt,
150
+ });
151
+ const reply = JSON.parse(replyPlain) as any;
152
+ expect(reply.msgtype).toBe("stream");
153
+ expect(reply.stream?.content).toBe("1");
154
+ expect(reply.stream?.finish).toBe(false);
155
+ expect(typeof reply.stream?.id).toBe("string");
156
+ expect(reply.stream?.id.length).toBeGreaterThan(0);
157
+
158
+ const expectedSig = computeWecomMsgSignature({
159
+ token,
160
+ timestamp: String(json.timestamp),
161
+ nonce: String(json.nonce),
162
+ encrypt: String(json.encrypt),
163
+ });
164
+ expect(json.msgsignature).toBe(expectedSig);
165
+ } finally {
166
+ unregister();
167
+ }
168
+ });
169
+
170
+ it("supports custom streamPlaceholderContent", async () => {
171
+ const account: ResolvedWecomAccount = {
172
+ accountId: "default",
173
+ name: "Test",
174
+ enabled: true,
175
+ configured: true,
176
+ token,
177
+ encodingAESKey,
178
+ receiveId: "",
179
+ config: { webhookPath: "/hook", token, encodingAESKey, streamPlaceholderContent: "正在思考..." },
180
+ };
181
+
182
+ const unregister = registerWecomWebhookTarget({
183
+ account,
184
+ config: {} as OpenClawConfig,
185
+ runtime: {},
186
+ core: {} as any,
187
+ path: "/hook",
188
+ });
189
+
190
+ try {
191
+ const timestamp = "1700000001";
192
+ const nonce = "nonce2";
193
+ const plain = JSON.stringify({
194
+ msgid: "MSGID2",
195
+ aibotid: "AIBOTID",
196
+ chattype: "single",
197
+ from: { userid: "USERID2" },
198
+ response_url: "RESPONSEURL",
199
+ msgtype: "text",
200
+ text: { content: "hello" },
201
+ });
202
+ const encrypt = encryptWecomPlaintext({ encodingAESKey, receiveId: "", plaintext: plain });
203
+ const msg_signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt });
204
+
205
+ const req = createMockRequest({
206
+ method: "POST",
207
+ url: `/hook?msg_signature=${encodeURIComponent(msg_signature)}&timestamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}`,
208
+ body: { encrypt },
209
+ });
210
+ const res = createMockResponse();
211
+ const handled = await handleWecomWebhookRequest(req, res);
212
+ expect(handled).toBe(true);
213
+ expect(res._getStatusCode()).toBe(200);
214
+
215
+ const json = JSON.parse(res._getData()) as any;
216
+ const replyPlain = decryptWecomEncrypted({
217
+ encodingAESKey,
218
+ receiveId: "",
219
+ encrypt: json.encrypt,
220
+ });
221
+ const reply = JSON.parse(replyPlain) as any;
222
+ expect(reply.msgtype).toBe("stream");
223
+ expect(reply.stream?.content).toBe("正在思考...");
224
+ expect(reply.stream?.finish).toBe(false);
225
+ } finally {
226
+ unregister();
227
+ }
228
+ });
229
+
230
+ it("returns a queued stream for 2, and an ack stream for merged follow-ups", async () => {
231
+ const token = "TOKEN";
232
+ const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
233
+ const account: any = {
234
+ accountId: "default",
235
+ enabled: true,
236
+ configured: true,
237
+ token,
238
+ encodingAESKey,
239
+ receiveId: "",
240
+ config: {
241
+ streamPlaceholderContent: "正在思考...",
242
+ debounceMs: 10_000,
243
+ },
244
+ };
245
+
246
+ const unregister = registerWecomWebhookTarget({
247
+ account,
248
+ config: {} as OpenClawConfig,
249
+ runtime: {},
250
+ core: {} as any,
251
+ path: "/hook-merge",
252
+ });
253
+
254
+ try {
255
+ const timestamp = "1700000002";
256
+ const nonce = "nonce-merge";
257
+
258
+ const makeReq = (msgid: string) => {
259
+ const plain = JSON.stringify({
260
+ msgid,
261
+ aibotid: "AIBOTID",
262
+ chattype: "single",
263
+ from: { userid: "USERID_QUEUE" },
264
+ response_url: "RESPONSEURL",
265
+ msgtype: "text",
266
+ text: { content: "hello" },
267
+ });
268
+ const encrypt = encryptWecomPlaintext({ encodingAESKey, receiveId: "", plaintext: plain });
269
+ const msg_signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt });
270
+ return createMockRequest({
271
+ method: "POST",
272
+ url: `/hook-merge?msg_signature=${encodeURIComponent(msg_signature)}&timestamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}`,
273
+ body: { encrypt },
274
+ });
275
+ };
276
+
277
+ const res1 = createMockResponse();
278
+ await handleWecomWebhookRequest(makeReq("MSGID-M1"), res1);
279
+ const json1 = JSON.parse(res1._getData()) as any;
280
+ const replyPlain1 = decryptWecomEncrypted({ encodingAESKey, receiveId: "", encrypt: json1.encrypt });
281
+ const reply1 = JSON.parse(replyPlain1) as any;
282
+ expect(reply1.msgtype).toBe("stream");
283
+ expect(reply1.stream?.finish).toBe(false);
284
+
285
+ const res2 = createMockResponse();
286
+ await handleWecomWebhookRequest(makeReq("MSGID-M2"), res2);
287
+ const json2 = JSON.parse(res2._getData()) as any;
288
+ const replyPlain2 = decryptWecomEncrypted({ encodingAESKey, receiveId: "", encrypt: json2.encrypt });
289
+ const reply2 = JSON.parse(replyPlain2) as any;
290
+ expect(reply2.msgtype).toBe("stream");
291
+ expect(reply2.stream?.finish).toBe(false);
292
+ expect(reply2.stream?.id).not.toBe(reply1.stream?.id);
293
+ expect(reply2.stream?.content).toContain("排队");
294
+
295
+ const res3 = createMockResponse();
296
+ await handleWecomWebhookRequest(makeReq("MSGID-M3"), res3);
297
+ const json3 = JSON.parse(res3._getData()) as any;
298
+ const replyPlain3 = decryptWecomEncrypted({ encodingAESKey, receiveId: "", encrypt: json3.encrypt });
299
+ const reply3 = JSON.parse(replyPlain3) as any;
300
+ expect(reply3.msgtype).toBe("stream");
301
+ // merged follow-up should get its own ack stream (not finished yet);
302
+ // it will be updated to a final hint after the merged batch completes.
303
+ expect(reply3.stream?.finish).toBe(false);
304
+ expect(reply3.stream?.id).not.toBe(reply1.stream?.id);
305
+ expect(reply3.stream?.id).not.toBe(reply2.stream?.id);
306
+ expect(reply3.stream?.content).toContain("合并");
307
+ } finally {
308
+ unregister();
309
+ }
310
+ });
311
+ });