@yanhaidao/wecom 1.0.0 → 2.0.0

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.
@@ -1,29 +1,48 @@
1
- import { createServer } from "node:http";
2
- import type { AddressInfo } from "node:net";
1
+ import { IncomingMessage, ServerResponse } from "node:http";
2
+ import { Socket } from "node:net";
3
3
 
4
4
  import { describe, expect, it } from "vitest";
5
5
 
6
- import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
6
+ import type { OpenClawConfig } from "openclaw/plugin-sdk";
7
7
 
8
8
  import type { ResolvedWecomAccount } from "./types.js";
9
9
  import { computeWecomMsgSignature, decryptWecomEncrypted, encryptWecomPlaintext } from "./crypto.js";
10
10
  import { handleWecomWebhookRequest, registerWecomWebhookTarget } from "./monitor.js";
11
11
 
12
- async function withServer(
13
- handler: Parameters<typeof createServer>[0],
14
- fn: (baseUrl: string) => Promise<void>,
15
- ) {
16
- const server = createServer(handler);
17
- await new Promise<void>((resolve) => {
18
- server.listen(0, "127.0.0.1", () => resolve());
19
- });
20
- const address = server.address() as AddressInfo | null;
21
- if (!address) throw new Error("missing server address");
22
- try {
23
- await fn(`http://127.0.0.1:${address.port}`);
24
- } finally {
25
- await new Promise<void>((resolve) => server.close(() => resolve()));
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 ?? {}));
26
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;
27
46
  }
28
47
 
29
48
  describe("handleWecomWebhookRequest", () => {
@@ -44,34 +63,30 @@ describe("handleWecomWebhookRequest", () => {
44
63
 
45
64
  const unregister = registerWecomWebhookTarget({
46
65
  account,
47
- config: {} as ClawdbotConfig,
66
+ config: {} as OpenClawConfig,
48
67
  runtime: {},
49
68
  core: {} as any,
50
69
  path: "/hook",
51
70
  });
52
71
 
53
72
  try {
54
- await withServer(async (req, res) => {
55
- const handled = await handleWecomWebhookRequest(req, res);
56
- if (!handled) {
57
- res.statusCode = 404;
58
- res.end("not found");
59
- }
60
- }, async (baseUrl) => {
61
- const timestamp = "13500001234";
62
- const nonce = "123412323";
63
- const echostr = encryptWecomPlaintext({
64
- encodingAESKey,
65
- receiveId: "",
66
- plaintext: "ping",
67
- });
68
- const msg_signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt: echostr });
69
- const response = await fetch(
70
- `${baseUrl}/hook?msg_signature=${encodeURIComponent(msg_signature)}&timestamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}&echostr=${encodeURIComponent(echostr)}`,
71
- );
72
- expect(response.status).toBe(200);
73
- expect(await response.text()).toBe("ping");
73
+ const timestamp = "13500001234";
74
+ const nonce = "123412323";
75
+ const echostr = encryptWecomPlaintext({
76
+ encodingAESKey,
77
+ receiveId: "",
78
+ plaintext: "ping",
74
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");
75
90
  } finally {
76
91
  unregister();
77
92
  }
@@ -91,69 +106,122 @@ describe("handleWecomWebhookRequest", () => {
91
106
 
92
107
  const unregister = registerWecomWebhookTarget({
93
108
  account,
94
- config: {} as ClawdbotConfig,
109
+ config: {} as OpenClawConfig,
95
110
  runtime: {},
96
111
  core: {} as any,
97
112
  path: "/hook",
98
113
  });
99
114
 
100
115
  try {
101
- await withServer(async (req, res) => {
102
- const handled = await handleWecomWebhookRequest(req, res);
103
- if (!handled) {
104
- res.statusCode = 404;
105
- res.end("not found");
106
- }
107
- }, async (baseUrl) => {
108
- const timestamp = "1700000000";
109
- const nonce = "nonce";
110
- const plain = JSON.stringify({
111
- msgid: "MSGID",
112
- aibotid: "AIBOTID",
113
- chattype: "single",
114
- from: { userid: "USERID" },
115
- response_url: "RESPONSEURL",
116
- msgtype: "text",
117
- text: { content: "hello" },
118
- });
119
- const encrypt = encryptWecomPlaintext({ encodingAESKey, receiveId: "", plaintext: plain });
120
- const msg_signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt });
121
-
122
- const response = await fetch(
123
- `${baseUrl}/hook?msg_signature=${encodeURIComponent(msg_signature)}&timestamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}`,
124
- {
125
- method: "POST",
126
- headers: { "content-type": "application/json" },
127
- body: JSON.stringify({ encrypt }),
128
- },
129
- );
130
- expect(response.status).toBe(200);
131
- const json = JSON.parse(await response.text()) as any;
132
- expect(typeof json.encrypt).toBe("string");
133
- expect(typeof json.msgsignature).toBe("string");
134
- expect(typeof json.timestamp).toBe("string");
135
- expect(typeof json.nonce).toBe("string");
136
-
137
- const replyPlain = decryptWecomEncrypted({
138
- encodingAESKey,
139
- receiveId: "",
140
- encrypt: json.encrypt,
141
- });
142
- const reply = JSON.parse(replyPlain) as any;
143
- expect(reply.msgtype).toBe("stream");
144
- expect(reply.stream?.content).toBe("1");
145
- expect(reply.stream?.finish).toBe(false);
146
- expect(typeof reply.stream?.id).toBe("string");
147
- expect(reply.stream?.id.length).toBeGreaterThan(0);
148
-
149
- const expectedSig = computeWecomMsgSignature({
150
- token,
151
- timestamp: String(json.timestamp),
152
- nonce: String(json.nonce),
153
- encrypt: String(json.encrypt),
154
- });
155
- expect(json.msgsignature).toBe(expectedSig);
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: "USERID" },
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,
156
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);
157
225
  } finally {
158
226
  unregister();
159
227
  }
package/src/runtime.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { PluginRuntime } from "clawdbot/plugin-sdk";
1
+ import type { PluginRuntime } from "openclaw/plugin-sdk";
2
2
 
3
3
  let runtime: PluginRuntime | null = null;
4
4
 
@@ -12,4 +12,3 @@ export function getWecomRuntime(): PluginRuntime {
12
12
  }
13
13
  return runtime;
14
14
  }
15
-
package/src/types.ts CHANGED
@@ -12,6 +12,8 @@ export type WecomAccountConfig = {
12
12
  encodingAESKey?: string;
13
13
  receiveId?: string;
14
14
 
15
+ streamPlaceholderContent?: string;
16
+
15
17
  dm?: WecomDmConfig;
16
18
  welcomeText?: string;
17
19
  };
@@ -68,10 +70,24 @@ export type WecomInboundEvent = WecomInboundBase & {
68
70
  };
69
71
  };
70
72
 
73
+ export type WecomInboundQuote = {
74
+ msgtype?: "text" | "image" | "mixed" | "voice" | "file";
75
+ text?: { content?: string };
76
+ image?: { url?: string };
77
+ mixed?: {
78
+ msg_item?: Array<{
79
+ msgtype: "text" | "image";
80
+ text?: { content?: string };
81
+ image?: { url?: string };
82
+ }>;
83
+ };
84
+ voice?: { content?: string };
85
+ file?: { url?: string };
86
+ };
87
+
71
88
  export type WecomInboundMessage =
72
- | WecomInboundText
89
+ | (WecomInboundText & { quote?: WecomInboundQuote })
73
90
  | WecomInboundVoice
74
91
  | WecomInboundStreamRefresh
75
92
  | WecomInboundEvent
76
- | (WecomInboundBase & Record<string, unknown>);
77
-
93
+ | (WecomInboundBase & { quote?: WecomInboundQuote } & Record<string, unknown>);
package/tsconfig.json CHANGED
@@ -19,4 +19,4 @@
19
19
  "node_modules",
20
20
  "**/*.test.ts"
21
21
  ]
22
- }
22
+ }
@@ -0,0 +1,15 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ import baseConfig from "../../vitest.config.ts";
4
+
5
+ const baseTest = (baseConfig as { test?: { exclude?: string[] } }).test ?? {};
6
+ const exclude = baseTest.exclude ?? [];
7
+
8
+ export default defineConfig({
9
+ ...baseConfig,
10
+ test: {
11
+ ...baseTest,
12
+ include: ["extensions/wecom/src/**/*.test.ts"],
13
+ exclude,
14
+ },
15
+ });