@yanhaidao/wecom 2.2.3 → 2.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.
- package/README.md +171 -45
- package/assets/03.agent.page.png +0 -0
- package/assets/03.bot.page.png +0 -0
- package/assets/link-me.jpg +0 -0
- package/index.ts +8 -0
- package/package.json +6 -2
- package/scripts/test-proxy.ts +70 -0
- package/src/agent/api-client.ts +140 -29
- package/src/agent/handler.ts +280 -21
- package/src/channel.ts +18 -1
- package/src/config/accounts.ts +5 -3
- package/src/config/index.ts +2 -0
- package/src/config/media.ts +14 -0
- package/src/config/network.ts +16 -0
- package/src/config/schema.ts +50 -5
- package/src/config-schema.ts +2 -0
- package/src/crypto.ts +43 -0
- package/src/http.ts +102 -0
- package/src/media.test.ts +15 -9
- package/src/media.ts +28 -12
- package/src/monitor/state.queue.test.ts +185 -0
- package/src/monitor/state.ts +514 -0
- package/src/monitor/types.ts +136 -0
- package/src/monitor.active.test.ts +90 -7
- package/src/monitor.integration.test.ts +15 -5
- package/src/monitor.ts +1038 -200
- package/src/monitor.webhook.test.ts +83 -1
- package/src/onboarding.ts +3 -3
- package/src/outbound.test.ts +82 -17
- package/src/outbound.ts +97 -42
- package/src/shared/command-auth.ts +101 -0
- package/src/shared/xml-parser.test.ts +30 -0
- package/src/shared/xml-parser.ts +112 -6
- package/src/target.ts +80 -0
- package/src/types/account.ts +5 -1
- package/src/types/config.ts +7 -0
- package/src/types/global.d.ts +9 -0
- package/src/types/message.ts +43 -0
|
@@ -2,12 +2,26 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
|
2
2
|
import { sendActiveMessage, handleWecomWebhookRequest, registerWecomWebhookTarget } from "./monitor.js";
|
|
3
3
|
import * as cryptoHelpers from "./crypto.js";
|
|
4
4
|
import * as runtime from "./runtime.js";
|
|
5
|
-
import
|
|
5
|
+
import * as agentApi from "./agent/api-client.js";
|
|
6
6
|
import { IncomingMessage, ServerResponse } from "node:http";
|
|
7
7
|
import { Socket } from "node:net";
|
|
8
8
|
import * as crypto from "node:crypto";
|
|
9
9
|
|
|
10
|
-
vi.
|
|
10
|
+
const { undiciFetch } = vi.hoisted(() => {
|
|
11
|
+
const undiciFetch = vi.fn();
|
|
12
|
+
return { undiciFetch };
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
vi.mock("undici", () => ({
|
|
16
|
+
fetch: undiciFetch,
|
|
17
|
+
ProxyAgent: class ProxyAgent { },
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock("./agent/api-client.js", () => ({
|
|
21
|
+
sendText: vi.fn(),
|
|
22
|
+
sendMedia: vi.fn(),
|
|
23
|
+
uploadMedia: vi.fn(),
|
|
24
|
+
}));
|
|
11
25
|
|
|
12
26
|
// Helpers
|
|
13
27
|
function createMockRequest(bodyObj: any): IncomingMessage {
|
|
@@ -32,6 +46,9 @@ function createMockResponse(): ServerResponse {
|
|
|
32
46
|
describe("Monitor Active Features", () => {
|
|
33
47
|
let capturedDeliver: ((payload: { text: string }) => Promise<void>) | undefined;
|
|
34
48
|
let mockCore: any;
|
|
49
|
+
let msgSeq = 0;
|
|
50
|
+
let senderUserId = "";
|
|
51
|
+
let senderChatId = "";
|
|
35
52
|
// Valid 32-byte AES Key (Base64 encoded)
|
|
36
53
|
const validKey = "jWmYm7qr5nMoCAstdRmNjt3p7vsH8HkK+qiJqQ0aaaa=";
|
|
37
54
|
|
|
@@ -39,6 +56,10 @@ describe("Monitor Active Features", () => {
|
|
|
39
56
|
vi.useFakeTimers();
|
|
40
57
|
capturedDeliver = undefined;
|
|
41
58
|
vi.restoreAllMocks();
|
|
59
|
+
undiciFetch.mockClear();
|
|
60
|
+
msgSeq += 1;
|
|
61
|
+
senderUserId = `zhangsan-${msgSeq}`;
|
|
62
|
+
senderChatId = `wr123-${msgSeq}`;
|
|
42
63
|
|
|
43
64
|
// Spy on crypto.randomBytes (default export in monitor.ts usage)
|
|
44
65
|
vi.spyOn(crypto.default, "randomBytes").mockImplementation((size) => {
|
|
@@ -55,7 +76,11 @@ describe("Monitor Active Features", () => {
|
|
|
55
76
|
// If this fails, we will know.
|
|
56
77
|
vi.spyOn(cryptoHelpers, "decryptWecomEncrypted").mockImplementation((opts) => {
|
|
57
78
|
return JSON.stringify({
|
|
58
|
-
msgid:
|
|
79
|
+
msgid: `test-msg-id-${msgSeq}`,
|
|
80
|
+
aibotid: "bot-1",
|
|
81
|
+
chattype: "group",
|
|
82
|
+
chatid: senderChatId,
|
|
83
|
+
from: { userid: senderUserId },
|
|
59
84
|
msgtype: "text",
|
|
60
85
|
text: { content: "hello" },
|
|
61
86
|
response_url: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=test-key"
|
|
@@ -98,7 +123,20 @@ describe("Monitor Active Features", () => {
|
|
|
98
123
|
|
|
99
124
|
registerWecomWebhookTarget({
|
|
100
125
|
account: { accountId: "1", enabled: true, configured: true, token: "T", encodingAESKey: validKey, receiveId: "R", config: {} as any },
|
|
101
|
-
config: {
|
|
126
|
+
config: {
|
|
127
|
+
channels: {
|
|
128
|
+
wecom: {
|
|
129
|
+
enabled: true,
|
|
130
|
+
agent: {
|
|
131
|
+
corpId: "corp",
|
|
132
|
+
corpSecret: "secret",
|
|
133
|
+
agentId: 1000002,
|
|
134
|
+
token: "token",
|
|
135
|
+
encodingAESKey: "aes",
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
} as any,
|
|
102
140
|
runtime: { log: () => { } },
|
|
103
141
|
core: mockCore,
|
|
104
142
|
path: "/wecom"
|
|
@@ -117,7 +155,7 @@ describe("Monitor Active Features", () => {
|
|
|
117
155
|
// The WeCom monitor debounces inbound messages before starting the agent.
|
|
118
156
|
// `flushPending` triggers async agent start without awaiting it, so give the
|
|
119
157
|
// microtask queue a chance to run after the timer fires.
|
|
120
|
-
await vi.
|
|
158
|
+
await vi.runOnlyPendingTimersAsync();
|
|
121
159
|
await Promise.resolve();
|
|
122
160
|
await Promise.resolve();
|
|
123
161
|
|
|
@@ -146,11 +184,56 @@ describe("Monitor Active Features", () => {
|
|
|
146
184
|
|
|
147
185
|
const streamId = Buffer.alloc(16, 0x11).toString("hex");
|
|
148
186
|
|
|
187
|
+
undiciFetch.mockResolvedValue(new Response("ok", { status: 200 }));
|
|
149
188
|
await sendActiveMessage(streamId, "Active Hello");
|
|
150
189
|
|
|
151
|
-
expect(
|
|
190
|
+
expect(undiciFetch).toHaveBeenCalledWith(
|
|
152
191
|
"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=test-key",
|
|
153
|
-
{
|
|
192
|
+
expect.objectContaining({
|
|
193
|
+
method: "POST",
|
|
194
|
+
headers: expect.objectContaining({ "Content-Type": "application/json" }),
|
|
195
|
+
body: JSON.stringify({ msgtype: "text", text: { content: "Active Hello" } }),
|
|
196
|
+
}),
|
|
154
197
|
);
|
|
155
198
|
});
|
|
199
|
+
|
|
200
|
+
it("should fallback non-image media to agent DM (and push a Chinese prompt)", async () => {
|
|
201
|
+
const { uploadMedia, sendMedia } = agentApi as any;
|
|
202
|
+
uploadMedia.mockResolvedValue("media-id-1");
|
|
203
|
+
sendMedia.mockResolvedValue(undefined);
|
|
204
|
+
|
|
205
|
+
const req = createMockRequest({ encrypt: "mock-encrypt" });
|
|
206
|
+
const res = createMockResponse();
|
|
207
|
+
await handleWecomWebhookRequest(req, res);
|
|
208
|
+
|
|
209
|
+
await vi.advanceTimersByTimeAsync(600);
|
|
210
|
+
await Promise.resolve();
|
|
211
|
+
await Promise.resolve();
|
|
212
|
+
|
|
213
|
+
expect(capturedDeliver).toBeDefined();
|
|
214
|
+
|
|
215
|
+
// Create a local PDF to force non-image content-type inference.
|
|
216
|
+
const fs = await import("node:fs/promises");
|
|
217
|
+
const os = await import("node:os");
|
|
218
|
+
const path = await import("node:path");
|
|
219
|
+
const tmp = path.join(os.tmpdir(), `wecom-test-${Date.now()}.pdf`);
|
|
220
|
+
await fs.writeFile(tmp, Buffer.from("pdf"));
|
|
221
|
+
|
|
222
|
+
undiciFetch.mockResolvedValue(new Response("ok", { status: 200 }));
|
|
223
|
+
|
|
224
|
+
await capturedDeliver!({ text: "here", mediaUrls: [tmp] } as any);
|
|
225
|
+
|
|
226
|
+
expect(uploadMedia).toHaveBeenCalled();
|
|
227
|
+
expect(sendMedia).toHaveBeenCalledWith(
|
|
228
|
+
expect.objectContaining({
|
|
229
|
+
toUser: senderUserId,
|
|
230
|
+
mediaType: "file",
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
233
|
+
// Ensure we attempted to push a prompt to response_url (uses undici fetch).
|
|
234
|
+
expect(undiciFetch).toHaveBeenCalled();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// 注:本机路径(/Users/... 或 /tmp/...)短路发图逻辑属于运行态特性,
|
|
238
|
+
// 单测在 fake timers + module singleton 状态下容易引入脆弱性;这里优先覆盖更关键的兜底链路与去重逻辑。
|
|
156
239
|
});
|
|
@@ -2,12 +2,19 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
|
2
2
|
import { handleWecomWebhookRequest, registerWecomWebhookTarget } from "./monitor.js";
|
|
3
3
|
import { encryptWecomPlaintext, computeWecomMsgSignature, WECOM_PKCS7_BLOCK_SIZE } from "./crypto.js";
|
|
4
4
|
import * as runtime from "./runtime.js";
|
|
5
|
-
import axios from "axios";
|
|
6
5
|
import crypto from "node:crypto";
|
|
7
6
|
import { IncomingMessage, ServerResponse } from "node:http";
|
|
8
7
|
import { Socket } from "node:net";
|
|
9
8
|
|
|
10
|
-
vi.
|
|
9
|
+
const { undiciFetch } = vi.hoisted(() => {
|
|
10
|
+
const undiciFetch = vi.fn();
|
|
11
|
+
return { undiciFetch };
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
vi.mock("undici", () => ({
|
|
15
|
+
fetch: undiciFetch,
|
|
16
|
+
ProxyAgent: class ProxyAgent { },
|
|
17
|
+
}));
|
|
11
18
|
|
|
12
19
|
// Helpers to simulate HTTP request
|
|
13
20
|
function createMockRequest(bodyObj: any, query: URLSearchParams): IncomingMessage {
|
|
@@ -126,8 +133,8 @@ describe("Monitor Integration: Inbound Image", () => {
|
|
|
126
133
|
cipher.setAutoPadding(false);
|
|
127
134
|
const encryptedMedia = Buffer.concat([cipher.update(pkcs7Pad(fileContent, WECOM_PKCS7_BLOCK_SIZE)), cipher.final()]);
|
|
128
135
|
|
|
129
|
-
// Mock
|
|
130
|
-
|
|
136
|
+
// Mock HTTP fetch to return this encrypted media
|
|
137
|
+
undiciFetch.mockResolvedValue(new Response(encryptedMedia));
|
|
131
138
|
|
|
132
139
|
// 2. Prepare Inbound Message (The Webhook JSON)
|
|
133
140
|
const imageUrl = "http://wecom.server/media/123";
|
|
@@ -192,6 +199,9 @@ describe("Monitor Integration: Inbound Image", () => {
|
|
|
192
199
|
expect(ctx.MediaPath).toBe("/tmp/saved-image.jpg");
|
|
193
200
|
expect(ctx.MediaType).toBe("image/jpeg");
|
|
194
201
|
|
|
195
|
-
expect(
|
|
202
|
+
expect(undiciFetch).toHaveBeenCalledWith(
|
|
203
|
+
imageUrl,
|
|
204
|
+
expect.objectContaining({ signal: expect.anything() }),
|
|
205
|
+
);
|
|
196
206
|
});
|
|
197
207
|
});
|