@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.
@@ -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 isLegacyWecomPath(path: string): boolean {
224
- return path === WEBHOOK_PATHS.BOT || path === WEBHOOK_PATHS.BOT_ALT || path === WEBHOOK_PATHS.AGENT;
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: "webchat",
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. 路由分发:按 Matrix/Legacy 路径分流 Bot 与 Agent 回调。
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() && isLegacyWecomPath(path)) {
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 isAgentPath = path === WEBHOOK_PATHS.AGENT || path.startsWith(`${WEBHOOK_PATHS.AGENT}/`);
2154
- if (isAgentPath) {
2155
- const targets = agentTargets.get(path) ?? [];
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, /wecom/bot
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 matrix bot callback by explicit account path", async () => {
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-matrix";
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 matrix" },
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)}&timestamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}`,
431
+ url: `/plugins/wecom/bot/acct-b?msg_signature=${encodeURIComponent(msg_signature)}&timestamp=${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)}&timestamp=${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 bot path when matrix explicit routes are registered", async () => {
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)}&timestamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}&echostr=${encodeURIComponent(echostr)}`,
675
+ url: `/plugins/wecom/agent/default?msg_signature=${encodeURIComponent(signature)}&timestamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}&echostr=${encodeURIComponent(echostr)}`,
583
676
  });
584
677
  const res = createMockResponse();
585
678
  const handled = await handleWecomWebhookRequest(req, res);