@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.
- package/README.md +37 -17
- package/index.ts +4 -4
- package/{clawdbot.plugin.json → openclaw.plugin.json} +1 -0
- package/package.json +6 -6
- package/src/accounts.ts +9 -10
- package/src/channel.ts +12 -12
- package/src/config-schema.ts +3 -0
- package/src/crypto.ts +3 -3
- package/src/media.test.ts +49 -0
- package/src/media.ts +37 -0
- package/src/monitor.active.test.ts +137 -0
- package/src/monitor.integration.test.ts +171 -0
- package/src/monitor.ts +153 -32
- package/src/monitor.webhook.test.ts +162 -94
- package/src/runtime.ts +1 -2
- package/src/types.ts +19 -3
- package/tsconfig.json +1 -1
- package/vitest.config.ts +15 -0
|
@@ -1,29 +1,48 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
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 {
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (
|
|
22
|
-
|
|
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
|
|
66
|
+
config: {} as OpenClawConfig,
|
|
48
67
|
runtime: {},
|
|
49
68
|
core: {} as any,
|
|
50
69
|
path: "/hook",
|
|
51
70
|
});
|
|
52
71
|
|
|
53
72
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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)}×tamp=${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)}×tamp=${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
|
|
109
|
+
config: {} as OpenClawConfig,
|
|
95
110
|
runtime: {},
|
|
96
111
|
core: {} as any,
|
|
97
112
|
path: "/hook",
|
|
98
113
|
});
|
|
99
114
|
|
|
100
115
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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)}×tamp=${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)}×tamp=${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 "
|
|
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
package/vitest.config.ts
ADDED
|
@@ -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
|
+
});
|