@yanhaidao/wecom 2.3.2 → 2.3.4
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/.github/workflows/release.yml +69 -1
- package/README.md +35 -28
- package/changelog/v2.3.2.md +28 -70
- package/changelog/v2.3.4.md +20 -0
- package/compat-single-account.md +1 -1
- package/index.test.ts +34 -0
- package/index.ts +11 -3
- package/package.json +1 -1
- package/src/channel.lifecycle.test.ts +24 -6
- package/src/channel.ts +11 -6
- package/src/gateway-monitor.ts +51 -20
- package/src/monitor.active.test.ts +2 -2
- package/src/monitor.integration.test.ts +4 -2
- package/src/monitor.ts +27 -10
- package/src/monitor.webhook.test.ts +104 -11
- package/src/onboarding.ts +219 -43
- package/src/types/constants.ts +7 -3
|
@@ -28,7 +28,7 @@ function createMockRequest(bodyObj: any): IncomingMessage {
|
|
|
28
28
|
const socket = new Socket();
|
|
29
29
|
const req = new IncomingMessage(socket);
|
|
30
30
|
req.method = "POST";
|
|
31
|
-
req.url = "/wecom?timestamp=123&nonce=456&signature=789";
|
|
31
|
+
req.url = "/plugins/wecom/bot/default?timestamp=123&nonce=456&signature=789";
|
|
32
32
|
req.push(JSON.stringify(bodyObj));
|
|
33
33
|
req.push(null);
|
|
34
34
|
return req;
|
|
@@ -140,7 +140,7 @@ describe("Monitor Active Features", () => {
|
|
|
140
140
|
} as any,
|
|
141
141
|
runtime: { log: () => { } },
|
|
142
142
|
core: mockCore,
|
|
143
|
-
path: "/wecom"
|
|
143
|
+
path: "/plugins/wecom/bot/default"
|
|
144
144
|
});
|
|
145
145
|
});
|
|
146
146
|
|
|
@@ -21,7 +21,7 @@ function createMockRequest(bodyObj: any, query: URLSearchParams): IncomingMessag
|
|
|
21
21
|
const socket = new Socket();
|
|
22
22
|
const req = new IncomingMessage(socket);
|
|
23
23
|
req.method = "POST";
|
|
24
|
-
req.url = `/wecom?${query.toString()}`;
|
|
24
|
+
req.url = `/plugins/wecom/bot/default?${query.toString()}`;
|
|
25
25
|
req.push(JSON.stringify(bodyObj));
|
|
26
26
|
req.push(null);
|
|
27
27
|
return req;
|
|
@@ -108,7 +108,7 @@ describe("Monitor Integration: Inbound Image", () => {
|
|
|
108
108
|
config: {} as any,
|
|
109
109
|
runtime: { log: console.log, error: console.error },
|
|
110
110
|
core: mockCore as any,
|
|
111
|
-
path: "/wecom"
|
|
111
|
+
path: "/plugins/wecom/bot/default"
|
|
112
112
|
});
|
|
113
113
|
});
|
|
114
114
|
|
|
@@ -198,6 +198,8 @@ describe("Monitor Integration: Inbound Image", () => {
|
|
|
198
198
|
// Expect Context Injection
|
|
199
199
|
expect(ctx.MediaPath).toBe("/tmp/saved-image.jpg");
|
|
200
200
|
expect(ctx.MediaType).toBe("image/jpeg");
|
|
201
|
+
expect(ctx.Surface).toBe("wecom");
|
|
202
|
+
expect(ctx.OriginatingChannel).toBe("wecom");
|
|
201
203
|
|
|
202
204
|
expect(undiciFetch).toHaveBeenCalledWith(
|
|
203
205
|
imageUrl,
|
package/src/monitor.ts
CHANGED
|
@@ -220,16 +220,24 @@ type RouteFailureReason =
|
|
|
220
220
|
| "wecom_identity_mismatch"
|
|
221
221
|
| "wecom_matrix_path_required";
|
|
222
222
|
|
|
223
|
-
function
|
|
224
|
-
return
|
|
223
|
+
function isNonMatrixWecomBasePath(path: string): boolean {
|
|
224
|
+
return (
|
|
225
|
+
path === WEBHOOK_PATHS.BOT ||
|
|
226
|
+
path === WEBHOOK_PATHS.BOT_ALT ||
|
|
227
|
+
path === WEBHOOK_PATHS.AGENT ||
|
|
228
|
+
path === WEBHOOK_PATHS.BOT_PLUGIN ||
|
|
229
|
+
path === WEBHOOK_PATHS.AGENT_PLUGIN
|
|
230
|
+
);
|
|
225
231
|
}
|
|
226
232
|
|
|
227
233
|
function hasMatrixExplicitRoutesRegistered(): boolean {
|
|
228
234
|
for (const key of webhookTargets.keys()) {
|
|
229
235
|
if (key.startsWith(`${WEBHOOK_PATHS.BOT_ALT}/`)) return true;
|
|
236
|
+
if (key.startsWith(`${WEBHOOK_PATHS.BOT_PLUGIN}/`)) return true;
|
|
230
237
|
}
|
|
231
238
|
for (const key of agentTargets.keys()) {
|
|
232
239
|
if (key.startsWith(`${WEBHOOK_PATHS.AGENT}/`)) return true;
|
|
240
|
+
if (key.startsWith(`${WEBHOOK_PATHS.AGENT_PLUGIN}/`)) return true;
|
|
233
241
|
}
|
|
234
242
|
return false;
|
|
235
243
|
}
|
|
@@ -1549,7 +1557,11 @@ async function startAgentForStream(params: {
|
|
|
1549
1557
|
SenderName: userid,
|
|
1550
1558
|
SenderId: userid,
|
|
1551
1559
|
Provider: "wecom",
|
|
1552
|
-
Surface
|
|
1560
|
+
// Keep Surface aligned with OriginatingChannel for Bot-mode delivery.
|
|
1561
|
+
// If Surface is "webchat", core dispatch treats this as cross-channel
|
|
1562
|
+
// and routes replies via routeReply -> wecom outbound (Agent API),
|
|
1563
|
+
// bypassing the Bot stream deliver path.
|
|
1564
|
+
Surface: "wecom",
|
|
1553
1565
|
MessageSid: msg.msgid,
|
|
1554
1566
|
CommandAuthorized: commandAuthorized,
|
|
1555
1567
|
OriginatingChannel: "wecom",
|
|
@@ -2110,7 +2122,7 @@ export function registerAgentWebhookTarget(target: AgentWebhookTarget): () => vo
|
|
|
2110
2122
|
*
|
|
2111
2123
|
* 处理来自企业微信的所有 Webhook 请求。
|
|
2112
2124
|
* 职责:
|
|
2113
|
-
* 1.
|
|
2125
|
+
* 1. 路由分发:优先按 `/plugins/wecom/{bot|agent}/{accountId}` 分流,并兼容历史 `/wecom/*` 路径。
|
|
2114
2126
|
* 2. 安全校验:验证企业微信签名 (Signature)。
|
|
2115
2127
|
* 3. 消息解密:处理企业微信的加密包。
|
|
2116
2128
|
* 4. 响应处理:
|
|
@@ -2134,7 +2146,7 @@ export async function handleWecomWebhookRequest(req: IncomingMessage, res: Serve
|
|
|
2134
2146
|
`[wecom] inbound(http): reqId=${reqId} path=${path} method=${req.method ?? "UNKNOWN"} remote=${remote} ua=${ua ? `"${ua}"` : "N/A"} contentLength=${cl || "N/A"} query={timestamp:${hasTimestamp},nonce:${hasNonce},echostr:${hasEchostr},msg_signature:${hasMsgSig},signature:${hasSignature}}`,
|
|
2135
2147
|
);
|
|
2136
2148
|
|
|
2137
|
-
if (hasMatrixExplicitRoutesRegistered() &&
|
|
2149
|
+
if (hasMatrixExplicitRoutesRegistered() && isNonMatrixWecomBasePath(path)) {
|
|
2138
2150
|
logRouteFailure({
|
|
2139
2151
|
reqId,
|
|
2140
2152
|
path,
|
|
@@ -2145,14 +2157,19 @@ export async function handleWecomWebhookRequest(req: IncomingMessage, res: Serve
|
|
|
2145
2157
|
writeRouteFailure(
|
|
2146
2158
|
res,
|
|
2147
2159
|
"wecom_matrix_path_required",
|
|
2148
|
-
"Matrix mode requires explicit account path. Use /wecom/bot/{accountId} or /wecom/agent/{accountId}.",
|
|
2160
|
+
"Matrix mode requires explicit account path. Use /plugins/wecom/bot/{accountId} or /plugins/wecom/agent/{accountId}.",
|
|
2149
2161
|
);
|
|
2150
2162
|
return true;
|
|
2151
2163
|
}
|
|
2152
2164
|
|
|
2153
|
-
const
|
|
2154
|
-
|
|
2155
|
-
|
|
2165
|
+
const isAgentPathCandidate =
|
|
2166
|
+
path === WEBHOOK_PATHS.AGENT ||
|
|
2167
|
+
path === WEBHOOK_PATHS.AGENT_PLUGIN ||
|
|
2168
|
+
path.startsWith(`${WEBHOOK_PATHS.AGENT}/`) ||
|
|
2169
|
+
path.startsWith(`${WEBHOOK_PATHS.AGENT_PLUGIN}/`);
|
|
2170
|
+
const matchedAgentTargets = agentTargets.get(path) ?? [];
|
|
2171
|
+
if (matchedAgentTargets.length > 0 || isAgentPathCandidate) {
|
|
2172
|
+
const targets = matchedAgentTargets;
|
|
2156
2173
|
if (targets.length > 0) {
|
|
2157
2174
|
const query = resolveQueryParams(req);
|
|
2158
2175
|
const timestamp = query.get("timestamp") ?? "";
|
|
@@ -2328,7 +2345,7 @@ export async function handleWecomWebhookRequest(req: IncomingMessage, res: Serve
|
|
|
2328
2345
|
return true;
|
|
2329
2346
|
}
|
|
2330
2347
|
|
|
2331
|
-
// Bot 模式路由: /wecom
|
|
2348
|
+
// Bot 模式路由: /plugins/wecom/bot(推荐)以及 /wecom、/wecom/bot(兼容)
|
|
2332
2349
|
const targets = webhookTargets.get(path);
|
|
2333
2350
|
if (!targets || targets.length === 0) return false;
|
|
2334
2351
|
|
|
@@ -369,7 +369,7 @@ describe("handleWecomWebhookRequest", () => {
|
|
|
369
369
|
}
|
|
370
370
|
});
|
|
371
371
|
|
|
372
|
-
it("routes
|
|
372
|
+
it("routes bot callback by explicit plugin account path", async () => {
|
|
373
373
|
const token = "MATRIX-TOKEN";
|
|
374
374
|
const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
|
|
375
375
|
|
|
@@ -390,7 +390,7 @@ describe("handleWecomWebhookRequest", () => {
|
|
|
390
390
|
config: { channels: { wecom: { accounts: {} } } } as OpenClawConfig,
|
|
391
391
|
runtime: {},
|
|
392
392
|
core: {} as any,
|
|
393
|
-
path: "/wecom/bot/acct-a",
|
|
393
|
+
path: "/plugins/wecom/bot/acct-a",
|
|
394
394
|
});
|
|
395
395
|
const unregisterB = registerWecomWebhookTarget({
|
|
396
396
|
account: {
|
|
@@ -409,12 +409,12 @@ describe("handleWecomWebhookRequest", () => {
|
|
|
409
409
|
config: { channels: { wecom: { accounts: {} } } } as OpenClawConfig,
|
|
410
410
|
runtime: {},
|
|
411
411
|
core: {} as any,
|
|
412
|
-
path: "/wecom/bot/acct-b",
|
|
412
|
+
path: "/plugins/wecom/bot/acct-b",
|
|
413
413
|
});
|
|
414
414
|
|
|
415
415
|
try {
|
|
416
416
|
const timestamp = "1700000999";
|
|
417
|
-
const nonce = "nonce-
|
|
417
|
+
const nonce = "nonce-plugin-account";
|
|
418
418
|
const plain = JSON.stringify({
|
|
419
419
|
msgid: "MATRIX-MSG-1",
|
|
420
420
|
aibotid: "BOT_B",
|
|
@@ -422,13 +422,94 @@ describe("handleWecomWebhookRequest", () => {
|
|
|
422
422
|
from: { userid: "USERID_B" },
|
|
423
423
|
response_url: "RESPONSEURL",
|
|
424
424
|
msgtype: "text",
|
|
425
|
-
text: { content: "hello
|
|
425
|
+
text: { content: "hello plugin account path" },
|
|
426
426
|
});
|
|
427
427
|
const encrypt = encryptWecomPlaintext({ encodingAESKey, receiveId: "", plaintext: plain });
|
|
428
428
|
const msg_signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt });
|
|
429
429
|
const req = createMockRequest({
|
|
430
430
|
method: "POST",
|
|
431
|
-
url: `/wecom/bot/acct-b?msg_signature=${encodeURIComponent(msg_signature)}×tamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}`,
|
|
431
|
+
url: `/plugins/wecom/bot/acct-b?msg_signature=${encodeURIComponent(msg_signature)}×tamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}`,
|
|
432
|
+
body: { encrypt },
|
|
433
|
+
});
|
|
434
|
+
const res = createMockResponse();
|
|
435
|
+
const handled = await handleWecomWebhookRequest(req, res);
|
|
436
|
+
expect(handled).toBe(true);
|
|
437
|
+
expect(res._getStatusCode()).toBe(200);
|
|
438
|
+
|
|
439
|
+
const json = JSON.parse(res._getData()) as any;
|
|
440
|
+
const replyPlain = decryptWecomEncrypted({
|
|
441
|
+
encodingAESKey,
|
|
442
|
+
receiveId: "",
|
|
443
|
+
encrypt: json.encrypt,
|
|
444
|
+
});
|
|
445
|
+
const reply = JSON.parse(replyPlain) as any;
|
|
446
|
+
expect(reply.stream?.content).toBe("B处理中");
|
|
447
|
+
} finally {
|
|
448
|
+
unregisterA();
|
|
449
|
+
unregisterB();
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("routes bot callback by explicit plugin namespace path", async () => {
|
|
454
|
+
const token = "MATRIX-TOKEN-PLUGIN";
|
|
455
|
+
const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
|
|
456
|
+
|
|
457
|
+
const unregisterA = registerWecomWebhookTarget({
|
|
458
|
+
account: {
|
|
459
|
+
accountId: "acct-a",
|
|
460
|
+
enabled: true,
|
|
461
|
+
configured: true,
|
|
462
|
+
token,
|
|
463
|
+
encodingAESKey,
|
|
464
|
+
receiveId: "",
|
|
465
|
+
config: {
|
|
466
|
+
token,
|
|
467
|
+
encodingAESKey,
|
|
468
|
+
streamPlaceholderContent: "A处理中",
|
|
469
|
+
} as any,
|
|
470
|
+
} as any,
|
|
471
|
+
config: { channels: { wecom: { accounts: {} } } } as OpenClawConfig,
|
|
472
|
+
runtime: {},
|
|
473
|
+
core: {} as any,
|
|
474
|
+
path: "/plugins/wecom/bot/acct-a",
|
|
475
|
+
});
|
|
476
|
+
const unregisterB = registerWecomWebhookTarget({
|
|
477
|
+
account: {
|
|
478
|
+
accountId: "acct-b",
|
|
479
|
+
enabled: true,
|
|
480
|
+
configured: true,
|
|
481
|
+
token,
|
|
482
|
+
encodingAESKey,
|
|
483
|
+
receiveId: "",
|
|
484
|
+
config: {
|
|
485
|
+
token,
|
|
486
|
+
encodingAESKey,
|
|
487
|
+
streamPlaceholderContent: "B处理中",
|
|
488
|
+
} as any,
|
|
489
|
+
} as any,
|
|
490
|
+
config: { channels: { wecom: { accounts: {} } } } as OpenClawConfig,
|
|
491
|
+
runtime: {},
|
|
492
|
+
core: {} as any,
|
|
493
|
+
path: "/plugins/wecom/bot/acct-b",
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
try {
|
|
497
|
+
const timestamp = "1700001000";
|
|
498
|
+
const nonce = "nonce-matrix-plugin";
|
|
499
|
+
const plain = JSON.stringify({
|
|
500
|
+
msgid: "MATRIX-MSG-PLUGIN-1",
|
|
501
|
+
aibotid: "BOT_B",
|
|
502
|
+
chattype: "single",
|
|
503
|
+
from: { userid: "USERID_B_PLUGIN" },
|
|
504
|
+
response_url: "RESPONSEURL",
|
|
505
|
+
msgtype: "text",
|
|
506
|
+
text: { content: "hello matrix plugin path" },
|
|
507
|
+
});
|
|
508
|
+
const encrypt = encryptWecomPlaintext({ encodingAESKey, receiveId: "", plaintext: plain });
|
|
509
|
+
const msg_signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt });
|
|
510
|
+
const req = createMockRequest({
|
|
511
|
+
method: "POST",
|
|
512
|
+
url: `/plugins/wecom/bot/acct-b?msg_signature=${encodeURIComponent(msg_signature)}×tamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}`,
|
|
432
513
|
body: { encrypt },
|
|
433
514
|
});
|
|
434
515
|
const res = createMockResponse();
|
|
@@ -501,7 +582,7 @@ describe("handleWecomWebhookRequest", () => {
|
|
|
501
582
|
}
|
|
502
583
|
});
|
|
503
584
|
|
|
504
|
-
it("rejects legacy
|
|
585
|
+
it("rejects legacy paths and accountless plugin paths", async () => {
|
|
505
586
|
const token = "MATRIX-TOKEN-3";
|
|
506
587
|
const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
|
|
507
588
|
const unregister = registerWecomWebhookTarget({
|
|
@@ -517,7 +598,7 @@ describe("handleWecomWebhookRequest", () => {
|
|
|
517
598
|
config: { channels: { wecom: { accounts: { "acct-a": { bot: {} } } } } } as OpenClawConfig,
|
|
518
599
|
runtime: {},
|
|
519
600
|
core: {} as any,
|
|
520
|
-
path: "/wecom/bot/acct-a",
|
|
601
|
+
path: "/plugins/wecom/bot/acct-a",
|
|
521
602
|
});
|
|
522
603
|
try {
|
|
523
604
|
const req = createMockRequest({
|
|
@@ -531,6 +612,18 @@ describe("handleWecomWebhookRequest", () => {
|
|
|
531
612
|
expect(JSON.parse(res._getData())).toMatchObject({
|
|
532
613
|
error: "wecom_matrix_path_required",
|
|
533
614
|
});
|
|
615
|
+
|
|
616
|
+
const pluginReq = createMockRequest({
|
|
617
|
+
method: "GET",
|
|
618
|
+
url: "/plugins/wecom/bot?timestamp=t&nonce=n&msg_signature=s&echostr=e",
|
|
619
|
+
});
|
|
620
|
+
const pluginRes = createMockResponse();
|
|
621
|
+
const pluginHandled = await handleWecomWebhookRequest(pluginReq, pluginRes);
|
|
622
|
+
expect(pluginHandled).toBe(true);
|
|
623
|
+
expect(pluginRes._getStatusCode()).toBe(401);
|
|
624
|
+
expect(JSON.parse(pluginRes._getData())).toMatchObject({
|
|
625
|
+
error: "wecom_matrix_path_required",
|
|
626
|
+
});
|
|
534
627
|
} finally {
|
|
535
628
|
unregister();
|
|
536
629
|
}
|
|
@@ -557,7 +650,7 @@ describe("handleWecomWebhookRequest", () => {
|
|
|
557
650
|
},
|
|
558
651
|
config: { channels: { wecom: { accounts: {} } } } as OpenClawConfig,
|
|
559
652
|
runtime: {},
|
|
560
|
-
path: "/wecom/agent",
|
|
653
|
+
path: "/plugins/wecom/agent/default",
|
|
561
654
|
} as any);
|
|
562
655
|
const unregisterB = registerAgentWebhookTarget({
|
|
563
656
|
agent: {
|
|
@@ -573,13 +666,13 @@ describe("handleWecomWebhookRequest", () => {
|
|
|
573
666
|
},
|
|
574
667
|
config: { channels: { wecom: { accounts: {} } } } as OpenClawConfig,
|
|
575
668
|
runtime: {},
|
|
576
|
-
path: "/wecom/agent",
|
|
669
|
+
path: "/plugins/wecom/agent/default",
|
|
577
670
|
} as any);
|
|
578
671
|
|
|
579
672
|
try {
|
|
580
673
|
const req = createMockRequest({
|
|
581
674
|
method: "GET",
|
|
582
|
-
url: `/wecom/agent?msg_signature=${encodeURIComponent(signature)}×tamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}&echostr=${encodeURIComponent(echostr)}`,
|
|
675
|
+
url: `/plugins/wecom/agent/default?msg_signature=${encodeURIComponent(signature)}×tamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}&echostr=${encodeURIComponent(echostr)}`,
|
|
583
676
|
});
|
|
584
677
|
const res = createMockResponse();
|
|
585
678
|
const handled = await handleWecomWebhookRequest(req, res);
|