@yanhaidao/wecom 2.2.7 → 2.3.2

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.
Files changed (54) hide show
  1. package/.github/workflows/release.yml +56 -0
  2. package/CLAUDE.md +1 -1
  3. package/GOVERNANCE.md +26 -0
  4. package/LICENSE +7 -0
  5. package/README.md +275 -91
  6. package/assets/01.bot-add.png +0 -0
  7. package/assets/01.bot-setp2.png +0 -0
  8. package/assets/02.agent.add.png +0 -0
  9. package/assets/02.agent.api-set.png +0 -0
  10. package/assets/register.png +0 -0
  11. package/changelog/v2.2.28.md +70 -0
  12. package/changelog/v2.3.2.md +70 -0
  13. package/compat-single-account.md +118 -0
  14. package/package.json +10 -2
  15. package/src/accounts.ts +17 -55
  16. package/src/agent/api-client.ts +84 -37
  17. package/src/agent/api-client.upload.test.ts +110 -0
  18. package/src/agent/handler.event-filter.test.ts +50 -0
  19. package/src/agent/handler.ts +147 -145
  20. package/src/channel.config.test.ts +147 -0
  21. package/src/channel.lifecycle.test.ts +234 -0
  22. package/src/channel.ts +90 -140
  23. package/src/config/accounts.resolve.test.ts +38 -0
  24. package/src/config/accounts.ts +257 -22
  25. package/src/config/index.ts +6 -0
  26. package/src/config/network.ts +9 -5
  27. package/src/config/routing.test.ts +88 -0
  28. package/src/config/routing.ts +26 -0
  29. package/src/config/schema.ts +35 -4
  30. package/src/config-schema.ts +5 -41
  31. package/src/dynamic-agent.account-scope.test.ts +17 -0
  32. package/src/dynamic-agent.ts +13 -13
  33. package/src/gateway-monitor.ts +200 -0
  34. package/src/http.ts +16 -2
  35. package/src/media.test.ts +28 -1
  36. package/src/media.ts +59 -1
  37. package/src/monitor/state.queue.test.ts +1 -1
  38. package/src/monitor/state.ts +1 -1
  39. package/src/monitor/types.ts +1 -1
  40. package/src/monitor.active.test.ts +13 -7
  41. package/src/monitor.inbound-filter.test.ts +63 -0
  42. package/src/monitor.ts +948 -128
  43. package/src/monitor.webhook.test.ts +288 -3
  44. package/src/outbound.test.ts +130 -0
  45. package/src/outbound.ts +44 -9
  46. package/src/shared/command-auth.ts +4 -2
  47. package/src/shared/xml-parser.test.ts +21 -1
  48. package/src/shared/xml-parser.ts +18 -0
  49. package/src/types/account.ts +43 -14
  50. package/src/types/config.ts +37 -2
  51. package/src/types/index.ts +3 -0
  52. package/src/types.ts +29 -147
  53. package/GEMINI.md +0 -76
  54. package//345/212/250/346/200/201Agent/350/267/257/347/224/261.md +0 -360
@@ -5,21 +5,26 @@ import { describe, expect, it } from "vitest";
5
5
 
6
6
  import type { OpenClawConfig } from "openclaw/plugin-sdk";
7
7
 
8
- import type { ResolvedWecomAccount } from "./types.js";
8
+ import type { ResolvedWecomAccount } from "./types/index.js";
9
9
  import { computeWecomMsgSignature, decryptWecomEncrypted, encryptWecomPlaintext } from "./crypto.js";
10
- import { handleWecomWebhookRequest, registerWecomWebhookTarget } from "./monitor.js";
10
+ import { handleWecomWebhookRequest, registerAgentWebhookTarget, registerWecomWebhookTarget } from "./monitor.js";
11
11
 
12
12
  function createMockRequest(params: {
13
13
  method: "GET" | "POST";
14
14
  url: string;
15
15
  body?: unknown;
16
+ rawBody?: string;
16
17
  }): IncomingMessage {
17
18
  const socket = new Socket();
18
19
  const req = new IncomingMessage(socket);
19
20
  req.method = params.method;
20
21
  req.url = params.url;
21
22
  if (params.method === "POST") {
22
- req.push(JSON.stringify(params.body ?? {}));
23
+ if (typeof params.rawBody === "string") {
24
+ req.push(params.rawBody);
25
+ } else {
26
+ req.push(JSON.stringify(params.body ?? {}));
27
+ }
23
28
  }
24
29
  req.push(null);
25
30
  return req;
@@ -227,6 +232,61 @@ describe("handleWecomWebhookRequest", () => {
227
232
  }
228
233
  });
229
234
 
235
+ it("skips bot callbacks with missing sender and returns empty ack", async () => {
236
+ const account: ResolvedWecomAccount = {
237
+ accountId: "default",
238
+ name: "Test",
239
+ enabled: true,
240
+ configured: true,
241
+ token,
242
+ encodingAESKey,
243
+ receiveId: "",
244
+ config: { webhookPath: "/hook", token, encodingAESKey },
245
+ };
246
+
247
+ const unregister = registerWecomWebhookTarget({
248
+ account,
249
+ config: {} as OpenClawConfig,
250
+ runtime: {},
251
+ core: {} as any,
252
+ path: "/hook",
253
+ });
254
+
255
+ try {
256
+ const timestamp = "1700000001";
257
+ const nonce = "nonce-missing-sender";
258
+ const plain = JSON.stringify({
259
+ msgid: "MSGID-MISSING-SENDER",
260
+ aibotid: "AIBOTID",
261
+ chattype: "single",
262
+ msgtype: "text",
263
+ text: { content: "hello" },
264
+ });
265
+ const encrypt = encryptWecomPlaintext({ encodingAESKey, receiveId: "", plaintext: plain });
266
+ const msg_signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt });
267
+
268
+ const req = createMockRequest({
269
+ method: "POST",
270
+ url: `/hook?msg_signature=${encodeURIComponent(msg_signature)}&timestamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}`,
271
+ body: { encrypt },
272
+ });
273
+ const res = createMockResponse();
274
+ const handled = await handleWecomWebhookRequest(req, res);
275
+ expect(handled).toBe(true);
276
+ expect(res._getStatusCode()).toBe(200);
277
+
278
+ const json = JSON.parse(res._getData()) as any;
279
+ const replyPlain = decryptWecomEncrypted({
280
+ encodingAESKey,
281
+ receiveId: "",
282
+ encrypt: json.encrypt,
283
+ });
284
+ expect(JSON.parse(replyPlain)).toEqual({});
285
+ } finally {
286
+ unregister();
287
+ }
288
+ });
289
+
230
290
  it("returns a queued stream for 2, and an ack stream for merged follow-ups", async () => {
231
291
  const token = "TOKEN";
232
292
  const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
@@ -308,4 +368,229 @@ describe("handleWecomWebhookRequest", () => {
308
368
  unregister();
309
369
  }
310
370
  });
371
+
372
+ it("routes matrix bot callback by explicit account path", async () => {
373
+ const token = "MATRIX-TOKEN";
374
+ const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
375
+
376
+ const unregisterA = registerWecomWebhookTarget({
377
+ account: {
378
+ accountId: "acct-a",
379
+ enabled: true,
380
+ configured: true,
381
+ token,
382
+ encodingAESKey,
383
+ receiveId: "",
384
+ config: {
385
+ token,
386
+ encodingAESKey,
387
+ streamPlaceholderContent: "A处理中",
388
+ } as any,
389
+ } as any,
390
+ config: { channels: { wecom: { accounts: {} } } } as OpenClawConfig,
391
+ runtime: {},
392
+ core: {} as any,
393
+ path: "/wecom/bot/acct-a",
394
+ });
395
+ const unregisterB = registerWecomWebhookTarget({
396
+ account: {
397
+ accountId: "acct-b",
398
+ enabled: true,
399
+ configured: true,
400
+ token,
401
+ encodingAESKey,
402
+ receiveId: "",
403
+ config: {
404
+ token,
405
+ encodingAESKey,
406
+ streamPlaceholderContent: "B处理中",
407
+ } as any,
408
+ } as any,
409
+ config: { channels: { wecom: { accounts: {} } } } as OpenClawConfig,
410
+ runtime: {},
411
+ core: {} as any,
412
+ path: "/wecom/bot/acct-b",
413
+ });
414
+
415
+ try {
416
+ const timestamp = "1700000999";
417
+ const nonce = "nonce-matrix";
418
+ const plain = JSON.stringify({
419
+ msgid: "MATRIX-MSG-1",
420
+ aibotid: "BOT_B",
421
+ chattype: "single",
422
+ from: { userid: "USERID_B" },
423
+ response_url: "RESPONSEURL",
424
+ msgtype: "text",
425
+ text: { content: "hello matrix" },
426
+ });
427
+ const encrypt = encryptWecomPlaintext({ encodingAESKey, receiveId: "", plaintext: plain });
428
+ const msg_signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt });
429
+ const req = createMockRequest({
430
+ method: "POST",
431
+ url: `/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("does not reject when aibotid mismatches configured value", async () => {
454
+ const token = "MATRIX-TOKEN-2";
455
+ const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
456
+ const unregister = registerWecomWebhookTarget({
457
+ account: {
458
+ accountId: "acct-a",
459
+ enabled: true,
460
+ configured: true,
461
+ token,
462
+ encodingAESKey,
463
+ receiveId: "",
464
+ config: {
465
+ token,
466
+ encodingAESKey,
467
+ aibotid: "BOT_ONLY",
468
+ } as any,
469
+ } as any,
470
+ config: { channels: { wecom: { accounts: {} } } } as OpenClawConfig,
471
+ runtime: {},
472
+ core: {} as any,
473
+ path: "/hook-matrix-mismatch",
474
+ });
475
+
476
+ try {
477
+ const timestamp = "1700001001";
478
+ const nonce = "nonce-matrix-mismatch";
479
+ const plain = JSON.stringify({
480
+ msgid: "MATRIX-MSG-2",
481
+ aibotid: "BOT_OTHER",
482
+ chattype: "single",
483
+ from: { userid: "USERID_X" },
484
+ response_url: "RESPONSEURL",
485
+ msgtype: "text",
486
+ text: { content: "hello mismatch" },
487
+ });
488
+ const encrypt = encryptWecomPlaintext({ encodingAESKey, receiveId: "", plaintext: plain });
489
+ const msg_signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt });
490
+ const req = createMockRequest({
491
+ method: "POST",
492
+ url: `/hook-matrix-mismatch?msg_signature=${encodeURIComponent(msg_signature)}&timestamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}`,
493
+ body: { encrypt },
494
+ });
495
+ const res = createMockResponse();
496
+ const handled = await handleWecomWebhookRequest(req, res);
497
+ expect(handled).toBe(true);
498
+ expect(res._getStatusCode()).toBe(200);
499
+ } finally {
500
+ unregister();
501
+ }
502
+ });
503
+
504
+ it("rejects legacy bot path when matrix explicit routes are registered", async () => {
505
+ const token = "MATRIX-TOKEN-3";
506
+ const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
507
+ const unregister = registerWecomWebhookTarget({
508
+ account: {
509
+ accountId: "acct-a",
510
+ enabled: true,
511
+ configured: true,
512
+ token,
513
+ encodingAESKey,
514
+ receiveId: "",
515
+ config: { token, encodingAESKey } as any,
516
+ } as any,
517
+ config: { channels: { wecom: { accounts: { "acct-a": { bot: {} } } } } } as OpenClawConfig,
518
+ runtime: {},
519
+ core: {} as any,
520
+ path: "/wecom/bot/acct-a",
521
+ });
522
+ try {
523
+ const req = createMockRequest({
524
+ method: "GET",
525
+ url: "/wecom/bot?timestamp=t&nonce=n&msg_signature=s&echostr=e",
526
+ });
527
+ const res = createMockResponse();
528
+ const handled = await handleWecomWebhookRequest(req, res);
529
+ expect(handled).toBe(true);
530
+ expect(res._getStatusCode()).toBe(401);
531
+ expect(JSON.parse(res._getData())).toMatchObject({
532
+ error: "wecom_matrix_path_required",
533
+ });
534
+ } finally {
535
+ unregister();
536
+ }
537
+ });
538
+
539
+ it("returns account conflict for agent GET verification when multiple accounts share token", async () => {
540
+ const token = "AGENT-TOKEN";
541
+ const timestamp = "1700002001";
542
+ const nonce = "nonce-agent";
543
+ const echostr = "ECHOSTR";
544
+ const signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt: echostr });
545
+
546
+ const unregisterA = registerAgentWebhookTarget({
547
+ agent: {
548
+ accountId: "agent-a",
549
+ enabled: true,
550
+ configured: true,
551
+ corpId: "corp-a",
552
+ corpSecret: "secret-a",
553
+ agentId: 1001,
554
+ token,
555
+ encodingAESKey: "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG",
556
+ config: {} as any,
557
+ },
558
+ config: { channels: { wecom: { accounts: {} } } } as OpenClawConfig,
559
+ runtime: {},
560
+ path: "/wecom/agent",
561
+ } as any);
562
+ const unregisterB = registerAgentWebhookTarget({
563
+ agent: {
564
+ accountId: "agent-b",
565
+ enabled: true,
566
+ configured: true,
567
+ corpId: "corp-b",
568
+ corpSecret: "secret-b",
569
+ agentId: 1002,
570
+ token,
571
+ encodingAESKey: "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG",
572
+ config: {} as any,
573
+ },
574
+ config: { channels: { wecom: { accounts: {} } } } as OpenClawConfig,
575
+ runtime: {},
576
+ path: "/wecom/agent",
577
+ } as any);
578
+
579
+ try {
580
+ const req = createMockRequest({
581
+ method: "GET",
582
+ url: `/wecom/agent?msg_signature=${encodeURIComponent(signature)}&timestamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}&echostr=${encodeURIComponent(echostr)}`,
583
+ });
584
+ const res = createMockResponse();
585
+ const handled = await handleWecomWebhookRequest(req, res);
586
+ expect(handled).toBe(true);
587
+ expect(res._getStatusCode()).toBe(401);
588
+ expect(JSON.parse(res._getData())).toMatchObject({
589
+ error: "wecom_account_conflict",
590
+ });
591
+ } finally {
592
+ unregisterA();
593
+ unregisterB();
594
+ }
595
+ });
311
596
  });
@@ -19,6 +19,38 @@ describe("wecomOutbound", () => {
19
19
  ).rejects.toThrow(/Agent mode/i);
20
20
  });
21
21
 
22
+ it("throws explicit error when outbound accountId does not exist", async () => {
23
+ const { wecomOutbound } = await import("./outbound.js");
24
+ const cfg = {
25
+ channels: {
26
+ wecom: {
27
+ enabled: true,
28
+ defaultAccount: "acct-a",
29
+ accounts: {
30
+ "acct-a": {
31
+ enabled: true,
32
+ agent: {
33
+ corpId: "corp-a",
34
+ corpSecret: "secret-a",
35
+ agentId: 10001,
36
+ token: "token-a",
37
+ encodingAESKey: "aes-a",
38
+ },
39
+ },
40
+ },
41
+ },
42
+ },
43
+ };
44
+ await expect(
45
+ wecomOutbound.sendText({
46
+ cfg,
47
+ accountId: "acct-missing",
48
+ to: "user:zhangsan",
49
+ text: "hello",
50
+ } as any),
51
+ ).rejects.toThrow(/account "acct-missing" not found/i);
52
+ });
53
+
22
54
  it("routes sendText to agent chatId/userid", async () => {
23
55
  const { wecomOutbound } = await import("./outbound.js");
24
56
  const api = await import("./agent/api-client.js");
@@ -140,4 +172,102 @@ describe("wecomOutbound", () => {
140
172
 
141
173
  now.mockRestore();
142
174
  });
175
+
176
+ it("uses account-scoped agent config in matrix mode", async () => {
177
+ const { wecomOutbound } = await import("./outbound.js");
178
+ const api = await import("./agent/api-client.js");
179
+ (api.sendText as any).mockResolvedValue(undefined);
180
+ (api.sendText as any).mockClear();
181
+
182
+ const cfg = {
183
+ channels: {
184
+ wecom: {
185
+ enabled: true,
186
+ defaultAccount: "acct-a",
187
+ accounts: {
188
+ "acct-a": {
189
+ enabled: true,
190
+ agent: {
191
+ corpId: "corp-a",
192
+ corpSecret: "secret-a",
193
+ agentId: 10001,
194
+ token: "token-a",
195
+ encodingAESKey: "aes-a",
196
+ },
197
+ },
198
+ "acct-b": {
199
+ enabled: true,
200
+ agent: {
201
+ corpId: "corp-b",
202
+ corpSecret: "secret-b",
203
+ agentId: 10002,
204
+ token: "token-b",
205
+ encodingAESKey: "aes-b",
206
+ },
207
+ },
208
+ },
209
+ },
210
+ },
211
+ };
212
+
213
+ await wecomOutbound.sendText({
214
+ cfg,
215
+ accountId: "acct-b",
216
+ to: "user:lisi",
217
+ text: "hello b",
218
+ } as any);
219
+ expect(api.sendText).toHaveBeenCalledWith(
220
+ expect.objectContaining({
221
+ toUser: "lisi",
222
+ agent: expect.objectContaining({
223
+ accountId: "acct-b",
224
+ agentId: 10002,
225
+ corpId: "corp-b",
226
+ }),
227
+ }),
228
+ );
229
+ });
230
+
231
+ it("rejects outbound when target account has matrix conflict", async () => {
232
+ const { wecomOutbound } = await import("./outbound.js");
233
+ const cfg = {
234
+ channels: {
235
+ wecom: {
236
+ enabled: true,
237
+ defaultAccount: "acct-a",
238
+ accounts: {
239
+ "acct-a": {
240
+ enabled: true,
241
+ agent: {
242
+ corpId: "corp-shared",
243
+ corpSecret: "secret-a",
244
+ agentId: 10001,
245
+ token: "token-a",
246
+ encodingAESKey: "aes-a",
247
+ },
248
+ },
249
+ "acct-b": {
250
+ enabled: true,
251
+ agent: {
252
+ corpId: "corp-shared",
253
+ corpSecret: "secret-b",
254
+ agentId: 10001,
255
+ token: "token-b",
256
+ encodingAESKey: "aes-b",
257
+ },
258
+ },
259
+ },
260
+ },
261
+ },
262
+ };
263
+
264
+ await expect(
265
+ wecomOutbound.sendText({
266
+ cfg,
267
+ accountId: "acct-b",
268
+ to: "user:lisi",
269
+ text: "hello",
270
+ } as any),
271
+ ).rejects.toThrow(/duplicate wecom agent identity/i);
272
+ });
143
273
  });
package/src/outbound.ts CHANGED
@@ -1,20 +1,49 @@
1
1
  import type { ChannelOutboundAdapter, ChannelOutboundContext } from "openclaw/plugin-sdk";
2
2
 
3
3
  import { sendText as sendAgentText, sendMedia as sendAgentMedia, uploadMedia } from "./agent/api-client.js";
4
- import { resolveWecomAccounts } from "./config/index.js";
4
+ import { resolveWecomAccount, resolveWecomAccountConflict, resolveWecomAccounts } from "./config/index.js";
5
5
  import { getWecomRuntime } from "./runtime.js";
6
6
 
7
7
  import { resolveWecomTarget } from "./target.js";
8
8
 
9
- function resolveAgentConfigOrThrow(cfg: ChannelOutboundContext["cfg"]) {
10
- const account = resolveWecomAccounts(cfg).agent;
9
+ function resolveAgentConfigOrThrow(params: {
10
+ cfg: ChannelOutboundContext["cfg"];
11
+ accountId?: string | null;
12
+ }) {
13
+ const resolvedAccounts = resolveWecomAccounts(params.cfg);
14
+ const conflictAccountId = params.accountId?.trim() || resolvedAccounts.defaultAccountId;
15
+ const conflict = resolveWecomAccountConflict({
16
+ cfg: params.cfg,
17
+ accountId: conflictAccountId,
18
+ });
19
+ if (conflict) {
20
+ throw new Error(conflict.message);
21
+ }
22
+
23
+ const requestedAccountId = params.accountId?.trim();
24
+ if (requestedAccountId) {
25
+ if (!resolvedAccounts.accounts[requestedAccountId]) {
26
+ throw new Error(
27
+ `WeCom outbound account "${requestedAccountId}" not found. Configure channels.wecom.accounts.${requestedAccountId} or use an existing accountId.`,
28
+ );
29
+ }
30
+ }
31
+ const account = resolveWecomAccount({
32
+ cfg: params.cfg,
33
+ accountId: params.accountId,
34
+ }).agent;
11
35
  if (!account?.configured) {
12
36
  throw new Error(
13
- "WeCom outbound requires Agent mode. Configure channels.wecom.agent (corpId/corpSecret/agentId/token/encodingAESKey).",
37
+ `WeCom outbound requires Agent mode for account=${params.accountId ?? "default"}. Configure channels.wecom.accounts.<accountId>.agent (or legacy channels.wecom.agent).`,
38
+ );
39
+ }
40
+ if (typeof account.agentId !== "number" || !Number.isFinite(account.agentId)) {
41
+ throw new Error(
42
+ `WeCom outbound requires channels.wecom.accounts.<accountId>.agent.agentId (or legacy channels.wecom.agent.agentId) for account=${params.accountId ?? account.accountId}.`,
14
43
  );
15
44
  }
16
45
  // 注意:不要在日志里输出 corpSecret 等敏感信息
17
- console.log(`[wecom-outbound] Using agent config: corpId=${account.corpId}, agentId=${account.agentId}`);
46
+ console.log(`[wecom-outbound] Using agent config: accountId=${account.accountId}, corpId=${account.corpId}, agentId=${account.agentId}`);
18
47
  return account;
19
48
  }
20
49
 
@@ -29,10 +58,10 @@ export const wecomOutbound: ChannelOutboundAdapter = {
29
58
  return [text];
30
59
  }
31
60
  },
32
- sendText: async ({ cfg, to, text }: ChannelOutboundContext) => {
61
+ sendText: async ({ cfg, to, text, accountId }: ChannelOutboundContext) => {
33
62
  // signal removed - not supported in current SDK
34
63
 
35
- const agent = resolveAgentConfigOrThrow(cfg);
64
+ const agent = resolveAgentConfigOrThrow({ cfg, accountId });
36
65
  const target = resolveWecomTarget(to);
37
66
  if (!target) {
38
67
  throw new Error("WeCom outbound requires a target (userid, partyid, tagid or chatid).");
@@ -98,10 +127,10 @@ export const wecomOutbound: ChannelOutboundAdapter = {
98
127
  timestamp: Date.now(),
99
128
  };
100
129
  },
101
- sendMedia: async ({ cfg, to, text, mediaUrl }: ChannelOutboundContext) => {
130
+ sendMedia: async ({ cfg, to, text, mediaUrl, accountId }: ChannelOutboundContext) => {
102
131
  // signal removed - not supported in current SDK
103
132
 
104
- const agent = resolveAgentConfigOrThrow(cfg);
133
+ const agent = resolveAgentConfigOrThrow({ cfg, accountId });
105
134
  const target = resolveWecomTarget(to);
106
135
  if (!target) {
107
136
  throw new Error("WeCom outbound requires a target (userid, partyid, tagid or chatid).");
@@ -149,6 +178,12 @@ export const wecomOutbound: ChannelOutboundAdapter = {
149
178
  amr: "audio/amr", mp4: "video/mp4", pdf: "application/pdf", doc: "application/msword",
150
179
  docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
151
180
  xls: "application/vnd.ms-excel", xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
181
+ ppt: "application/vnd.ms-powerpoint", pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
182
+ txt: "text/plain", csv: "text/csv", tsv: "text/tab-separated-values", md: "text/markdown", json: "application/json",
183
+ xml: "application/xml", yaml: "application/yaml", yml: "application/yaml",
184
+ zip: "application/zip", rar: "application/vnd.rar", "7z": "application/x-7z-compressed",
185
+ tar: "application/x-tar", gz: "application/gzip", tgz: "application/gzip",
186
+ rtf: "application/rtf", odt: "application/vnd.oasis.opendocument.text",
152
187
  };
153
188
  contentType = mimeTypes[ext] || "application/octet-stream";
154
189
  console.log(`[wecom-outbound] Reading local file: ${mediaUrl}, ext=${ext}, contentType=${contentType}`);
@@ -1,6 +1,8 @@
1
1
  import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
2
2
 
3
- import type { WecomAccountConfig } from "../types.js";
3
+ import type { WecomAgentConfig, WecomBotConfig } from "../types/index.js";
4
+
5
+ type WecomCommandAuthAccountConfig = Pick<WecomBotConfig, "dm"> | Pick<WecomAgentConfig, "dm">;
4
6
 
5
7
  function normalizeWecomAllowFromEntry(raw: string): string {
6
8
  return raw
@@ -22,7 +24,7 @@ function isWecomSenderAllowed(senderUserId: string, allowFrom: string[]): boolea
22
24
  export async function resolveWecomCommandAuthorization(params: {
23
25
  core: PluginRuntime;
24
26
  cfg: OpenClawConfig;
25
- accountConfig: WecomAccountConfig;
27
+ accountConfig: WecomCommandAuthAccountConfig;
26
28
  rawBody: string;
27
29
  senderUserId: string;
28
30
  }): Promise<{
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, test } from "vitest";
2
2
 
3
- import { extractContent, extractMediaId, extractMsgId } from "./xml-parser.js";
3
+ import { extractContent, extractFromUser, extractMediaId, extractMsgId, parseXml } from "./xml-parser.js";
4
4
 
5
5
  describe("wecom xml-parser", () => {
6
6
  test("extractContent is robust to non-string Content", () => {
@@ -27,4 +27,24 @@ describe("wecom xml-parser", () => {
27
27
  const msg: any = { MsgId: 123456789 };
28
28
  expect(extractMsgId(msg)).toBe("123456789");
29
29
  });
30
+
31
+ test("parseXml preserves leading zero userid in FromUserName", () => {
32
+ const xml = `
33
+ <xml>
34
+ <FromUserName><![CDATA[0254571]]></FromUserName>
35
+ </xml>
36
+ `;
37
+ const msg = parseXml(xml);
38
+ expect(extractFromUser(msg)).toBe("0254571");
39
+ });
40
+
41
+ test("parseXml preserves 64-bit MsgId as string", () => {
42
+ const xml = `
43
+ <xml>
44
+ <MsgId>1234567890123456</MsgId>
45
+ </xml>
46
+ `;
47
+ const msg = parseXml(xml);
48
+ expect(extractMsgId(msg)).toBe("1234567890123456");
49
+ });
30
50
  });
@@ -10,6 +10,8 @@ const xmlParser = new XMLParser({
10
10
  ignoreAttributes: false,
11
11
  trimValues: true,
12
12
  processEntities: false,
13
+ parseTagValue: false,
14
+ parseAttributeValue: false,
13
15
  });
14
16
 
15
17
  /**
@@ -72,6 +74,22 @@ export function extractChatId(msg: WecomAgentInboundMessage): string | undefined
72
74
  return msg.ChatId ? String(msg.ChatId) : undefined;
73
75
  }
74
76
 
77
+ /**
78
+ * 从 XML 中提取 AgentID(兼容 AgentID/agentid 等大小写)
79
+ */
80
+ export function extractAgentId(msg: WecomAgentInboundMessage): string | number | undefined {
81
+ const raw =
82
+ (msg as any).AgentID ??
83
+ (msg as any).AgentId ??
84
+ (msg as any).agentid ??
85
+ (msg as any).agentId;
86
+ if (raw == null) return undefined;
87
+ if (typeof raw === "string") return raw.trim() || undefined;
88
+ if (typeof raw === "number") return raw;
89
+ const asString = String(raw).trim();
90
+ return asString || undefined;
91
+ }
92
+
75
93
  /**
76
94
  * 从 XML 中提取消息内容
77
95
  */