@yanhaidao/wecom 2.3.180 → 2.3.260
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 +23 -4
- package/README.md +87 -2
- package/SKILLS_DOC.md +272 -120
- package/changelog/v2.3.19.md +73 -0
- package/changelog/v2.3.26.md +21 -0
- package/package.json +2 -2
- package/src/agent/handler.ts +5 -3
- package/src/app/account-runtime.ts +5 -1
- package/src/app/index.ts +117 -0
- package/src/capability/bot/stream-orchestrator.ts +1 -1
- package/src/capability/doc/client.ts +228 -9
- package/src/capability/doc/tool.ts +14 -7
- package/src/channel.ts +1 -1
- package/src/config/index.ts +7 -1
- package/src/config/media.test.ts +113 -0
- package/src/config/media.ts +130 -5
- package/src/config/schema.ts +3 -0
- package/src/context-store.ts +264 -0
- package/src/onboarding.ts +1 -1
- package/src/outbound.test.ts +565 -5
- package/src/outbound.ts +94 -7
- package/src/runtime/dispatcher.ts +24 -5
- package/src/runtime/routing-bridge.test.ts +115 -0
- package/src/runtime/routing-bridge.ts +26 -1
- package/src/runtime/session-manager.test.ts +135 -0
- package/src/runtime/session-manager.ts +40 -8
- package/src/runtime/source-registry.ts +79 -0
- package/src/runtime.ts +3 -0
- package/src/target.ts +20 -8
- package/src/transport/bot-webhook/inbound-normalizer.ts +4 -4
- package/src/transport/bot-ws/media.test.ts +44 -0
- package/src/transport/bot-ws/media.ts +7 -4
- package/src/transport/bot-ws/reply.test.ts +131 -1
- package/src/transport/bot-ws/reply.ts +15 -3
- package/src/transport/bot-ws/sdk-adapter.ts +2 -1
- package/src/transport/http/registry.ts +1 -1
- package/src/types/config.ts +3 -0
- package/src/types/runtime.ts +1 -0
- package/src/wecom_msg_adapter/markdown_adapter.ts +331 -0
package/src/outbound.test.ts
CHANGED
|
@@ -29,8 +29,23 @@ describe("wecomOutbound", () => {
|
|
|
29
29
|
|
|
30
30
|
afterEach(async () => {
|
|
31
31
|
const runtime = await import("./runtime.js");
|
|
32
|
+
const sourceRegistry = await import("./runtime/source-registry.js");
|
|
32
33
|
runtime.unregisterBotWsPushHandle("default");
|
|
33
34
|
runtime.unregisterBotWsPushHandle("acct-ws");
|
|
35
|
+
runtime.unregisterActiveBotWsReplyHandle({
|
|
36
|
+
accountId: "default",
|
|
37
|
+
sessionKey: "agent:ops_bot:wecom:default:dm:zhangsan",
|
|
38
|
+
peerKind: "direct",
|
|
39
|
+
peerId: "zhangsan",
|
|
40
|
+
});
|
|
41
|
+
runtime.unregisterActiveBotWsReplyHandle({
|
|
42
|
+
accountId: "acct-ws",
|
|
43
|
+
sessionKey: "agent:ops_bot:wecom:acct-ws:dm:lisi",
|
|
44
|
+
peerKind: "direct",
|
|
45
|
+
peerId: "lisi",
|
|
46
|
+
});
|
|
47
|
+
sourceRegistry.clearWecomSourceAccount("default");
|
|
48
|
+
sourceRegistry.clearWecomSourceAccount("acct-ws");
|
|
34
49
|
vi.unstubAllGlobals();
|
|
35
50
|
});
|
|
36
51
|
|
|
@@ -132,10 +147,10 @@ describe("wecomOutbound", () => {
|
|
|
132
147
|
|
|
133
148
|
(api.sendText as any).mockClear();
|
|
134
149
|
|
|
135
|
-
// Test:
|
|
150
|
+
// Test: Numeric targets default to User ID
|
|
136
151
|
await wecomOutbound.sendText({ cfg, to: "1001", text: "hi party" } as any);
|
|
137
152
|
expect(api.sendText).toHaveBeenCalledWith(
|
|
138
|
-
expect.objectContaining({ toUser:
|
|
153
|
+
expect.objectContaining({ toUser: "1001", toParty: undefined }),
|
|
139
154
|
);
|
|
140
155
|
|
|
141
156
|
(api.sendText as any).mockClear();
|
|
@@ -256,6 +271,136 @@ describe("wecomOutbound", () => {
|
|
|
256
271
|
now.mockRestore();
|
|
257
272
|
});
|
|
258
273
|
|
|
274
|
+
it("keeps agent-source sessions on the Agent text path even when ws is primary", async () => {
|
|
275
|
+
const { wecomOutbound } = await import("./outbound.js");
|
|
276
|
+
const runtime = await import("./runtime.js");
|
|
277
|
+
const api = await import("./transport/agent-api/core.js");
|
|
278
|
+
const sourceRegistry = await import("./runtime/source-registry.js");
|
|
279
|
+
const sendMarkdown = vi.fn().mockResolvedValue(undefined);
|
|
280
|
+
runtime.registerBotWsPushHandle(
|
|
281
|
+
"acct-ws",
|
|
282
|
+
createBotWsHandle({
|
|
283
|
+
sendMarkdown,
|
|
284
|
+
}),
|
|
285
|
+
);
|
|
286
|
+
sourceRegistry.registerWecomSourceSnapshot({
|
|
287
|
+
accountId: "acct-ws",
|
|
288
|
+
source: "agent-callback",
|
|
289
|
+
sessionKey: "agent:ops_bot:wecom:acct-ws:dm:lisi",
|
|
290
|
+
});
|
|
291
|
+
(api.sendText as any).mockResolvedValue(undefined);
|
|
292
|
+
(api.sendText as any).mockClear();
|
|
293
|
+
|
|
294
|
+
const cfg = {
|
|
295
|
+
channels: {
|
|
296
|
+
wecom: {
|
|
297
|
+
enabled: true,
|
|
298
|
+
defaultAccount: "acct-ws",
|
|
299
|
+
accounts: {
|
|
300
|
+
"acct-ws": {
|
|
301
|
+
enabled: true,
|
|
302
|
+
bot: {
|
|
303
|
+
primaryTransport: "ws",
|
|
304
|
+
ws: {
|
|
305
|
+
botId: "bot-1",
|
|
306
|
+
secret: "secret-1",
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
agent: {
|
|
310
|
+
corpId: "corp-ws",
|
|
311
|
+
corpSecret: "agent-secret",
|
|
312
|
+
agentId: 10001,
|
|
313
|
+
token: "token-ws",
|
|
314
|
+
encodingAESKey: "aes-ws",
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
await wecomOutbound.sendText({
|
|
323
|
+
cfg,
|
|
324
|
+
accountId: "acct-ws",
|
|
325
|
+
sessionKey: "agent:ops_bot:wecom:acct-ws:dm:lisi",
|
|
326
|
+
to: "user:lisi",
|
|
327
|
+
text: "hello agent",
|
|
328
|
+
} as any);
|
|
329
|
+
|
|
330
|
+
expect(sendMarkdown).not.toHaveBeenCalled();
|
|
331
|
+
expect(api.sendText).toHaveBeenCalledWith(
|
|
332
|
+
expect.objectContaining({
|
|
333
|
+
toUser: "lisi",
|
|
334
|
+
text: "hello agent",
|
|
335
|
+
}),
|
|
336
|
+
);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("keeps agent-source peer targets on the Agent text path without sessionKey", async () => {
|
|
340
|
+
const { wecomOutbound } = await import("./outbound.js");
|
|
341
|
+
const runtime = await import("./runtime.js");
|
|
342
|
+
const api = await import("./transport/agent-api/core.js");
|
|
343
|
+
const sourceRegistry = await import("./runtime/source-registry.js");
|
|
344
|
+
const sendMarkdown = vi.fn().mockResolvedValue(undefined);
|
|
345
|
+
runtime.registerBotWsPushHandle(
|
|
346
|
+
"acct-ws",
|
|
347
|
+
createBotWsHandle({
|
|
348
|
+
sendMarkdown,
|
|
349
|
+
}),
|
|
350
|
+
);
|
|
351
|
+
sourceRegistry.registerWecomSourceSnapshot({
|
|
352
|
+
accountId: "acct-ws",
|
|
353
|
+
source: "agent-callback",
|
|
354
|
+
peerKind: "direct",
|
|
355
|
+
peerId: "lisi",
|
|
356
|
+
});
|
|
357
|
+
(api.sendText as any).mockResolvedValue(undefined);
|
|
358
|
+
(api.sendText as any).mockClear();
|
|
359
|
+
|
|
360
|
+
const cfg = {
|
|
361
|
+
channels: {
|
|
362
|
+
wecom: {
|
|
363
|
+
enabled: true,
|
|
364
|
+
defaultAccount: "acct-ws",
|
|
365
|
+
accounts: {
|
|
366
|
+
"acct-ws": {
|
|
367
|
+
enabled: true,
|
|
368
|
+
bot: {
|
|
369
|
+
primaryTransport: "ws",
|
|
370
|
+
ws: {
|
|
371
|
+
botId: "bot-1",
|
|
372
|
+
secret: "secret-1",
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
agent: {
|
|
376
|
+
corpId: "corp-ws",
|
|
377
|
+
corpSecret: "agent-secret",
|
|
378
|
+
agentId: 10001,
|
|
379
|
+
token: "token-ws",
|
|
380
|
+
encodingAESKey: "aes-ws",
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
await wecomOutbound.sendText({
|
|
389
|
+
cfg,
|
|
390
|
+
accountId: "acct-ws",
|
|
391
|
+
to: "user:lisi",
|
|
392
|
+
text: "hello peer",
|
|
393
|
+
} as any);
|
|
394
|
+
|
|
395
|
+
expect(sendMarkdown).not.toHaveBeenCalled();
|
|
396
|
+
expect(api.sendText).toHaveBeenCalledWith(
|
|
397
|
+
expect.objectContaining({
|
|
398
|
+
toUser: "lisi",
|
|
399
|
+
text: "hello peer",
|
|
400
|
+
}),
|
|
401
|
+
);
|
|
402
|
+
});
|
|
403
|
+
|
|
259
404
|
it("does not silently fall back to Agent when Bot WS active push is configured but unavailable", async () => {
|
|
260
405
|
const { wecomOutbound } = await import("./outbound.js");
|
|
261
406
|
const api = await import("./transport/agent-api/core.js");
|
|
@@ -339,24 +484,101 @@ describe("wecomOutbound", () => {
|
|
|
339
484
|
|
|
340
485
|
expect(sendMedia).toHaveBeenCalledWith({
|
|
341
486
|
chatId: "zhangsan",
|
|
487
|
+
maxBytes: 80 * 1024 * 1024,
|
|
342
488
|
mediaUrl: "https://example.com/media.png",
|
|
343
|
-
mediaLocalRoots:
|
|
489
|
+
mediaLocalRoots: expect.any(Array),
|
|
344
490
|
text: "caption",
|
|
345
491
|
});
|
|
346
492
|
expect(api.sendMedia).not.toHaveBeenCalled();
|
|
347
493
|
});
|
|
348
494
|
|
|
349
|
-
it("
|
|
495
|
+
it("marks the active bot-ws reply handle when same-session text is sent via active push", async () => {
|
|
350
496
|
const { wecomOutbound } = await import("./outbound.js");
|
|
351
497
|
const runtime = await import("./runtime.js");
|
|
352
498
|
const api = await import("./transport/agent-api/core.js");
|
|
353
|
-
const
|
|
499
|
+
const sendMarkdown = vi.fn().mockResolvedValue(undefined);
|
|
500
|
+
const markExternalActivity = vi.fn();
|
|
501
|
+
runtime.registerBotWsPushHandle(
|
|
502
|
+
"acct-ws",
|
|
503
|
+
createBotWsHandle({
|
|
504
|
+
sendMarkdown,
|
|
505
|
+
}),
|
|
506
|
+
);
|
|
507
|
+
runtime.registerActiveBotWsReplyHandle({
|
|
508
|
+
accountId: "acct-ws",
|
|
509
|
+
sessionKey: "agent:ops_bot:wecom:acct-ws:dm:lisi",
|
|
510
|
+
peerKind: "direct",
|
|
511
|
+
peerId: "lisi",
|
|
512
|
+
handle: {
|
|
513
|
+
context: {
|
|
514
|
+
transport: "bot-ws",
|
|
515
|
+
accountId: "acct-ws",
|
|
516
|
+
raw: { transport: "bot-ws", envelopeType: "ws", body: {} },
|
|
517
|
+
},
|
|
518
|
+
deliver: vi.fn(),
|
|
519
|
+
markExternalActivity,
|
|
520
|
+
} as any,
|
|
521
|
+
});
|
|
522
|
+
(api.sendText as any).mockClear();
|
|
523
|
+
|
|
524
|
+
const cfg = {
|
|
525
|
+
channels: {
|
|
526
|
+
wecom: {
|
|
527
|
+
enabled: true,
|
|
528
|
+
defaultAccount: "acct-ws",
|
|
529
|
+
accounts: {
|
|
530
|
+
"acct-ws": {
|
|
531
|
+
enabled: true,
|
|
532
|
+
bot: {
|
|
533
|
+
primaryTransport: "ws",
|
|
534
|
+
ws: {
|
|
535
|
+
botId: "bot-1",
|
|
536
|
+
secret: "secret-1",
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
agent: {
|
|
540
|
+
corpId: "corp-ws",
|
|
541
|
+
corpSecret: "agent-secret",
|
|
542
|
+
agentId: 10001,
|
|
543
|
+
token: "token-ws",
|
|
544
|
+
encodingAESKey: "aes-ws",
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
await wecomOutbound.sendText({
|
|
553
|
+
cfg,
|
|
554
|
+
accountId: "acct-ws",
|
|
555
|
+
sessionKey: "agent:ops_bot:wecom:acct-ws:dm:lisi",
|
|
556
|
+
to: "user:lisi",
|
|
557
|
+
text: "hello ws",
|
|
558
|
+
} as any);
|
|
559
|
+
|
|
560
|
+
expect(sendMarkdown).toHaveBeenCalledWith("lisi", "hello ws");
|
|
561
|
+
expect(markExternalActivity).toHaveBeenCalledTimes(1);
|
|
562
|
+
expect(api.sendText).not.toHaveBeenCalled();
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it("keeps agent-source sessions on the Agent media path even when ws is primary", async () => {
|
|
566
|
+
const { wecomOutbound } = await import("./outbound.js");
|
|
567
|
+
const runtime = await import("./runtime.js");
|
|
568
|
+
const api = await import("./transport/agent-api/core.js");
|
|
569
|
+
const sourceRegistry = await import("./runtime/source-registry.js");
|
|
570
|
+
const sendMedia = vi.fn().mockResolvedValue({ ok: true, messageId: "ws-media-1" });
|
|
354
571
|
runtime.registerBotWsPushHandle(
|
|
355
572
|
"default",
|
|
356
573
|
createBotWsHandle({
|
|
357
574
|
sendMedia,
|
|
358
575
|
}),
|
|
359
576
|
);
|
|
577
|
+
sourceRegistry.registerWecomSourceSnapshot({
|
|
578
|
+
accountId: "default",
|
|
579
|
+
source: "agent-callback",
|
|
580
|
+
sessionKey: "agent:ops_bot:wecom:default:dm:zhangsan",
|
|
581
|
+
});
|
|
360
582
|
(api.uploadMedia as any).mockResolvedValue("media-1");
|
|
361
583
|
(api.sendMedia as any).mockResolvedValue(undefined);
|
|
362
584
|
(api.sendMedia as any).mockClear();
|
|
@@ -393,12 +615,350 @@ describe("wecomOutbound", () => {
|
|
|
393
615
|
|
|
394
616
|
await wecomOutbound.sendMedia({
|
|
395
617
|
cfg,
|
|
618
|
+
sessionKey: "agent:ops_bot:wecom:default:dm:zhangsan",
|
|
619
|
+
to: "user:zhangsan",
|
|
620
|
+
text: "caption",
|
|
621
|
+
mediaUrl: "https://example.com/media.png",
|
|
622
|
+
} as any);
|
|
623
|
+
|
|
624
|
+
expect(sendMedia).not.toHaveBeenCalled();
|
|
625
|
+
expect(api.sendMedia).toHaveBeenCalledTimes(1);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it("marks the active bot-ws reply handle when same-session media is sent via active push", async () => {
|
|
629
|
+
const { wecomOutbound } = await import("./outbound.js");
|
|
630
|
+
const runtime = await import("./runtime.js");
|
|
631
|
+
const sendMedia = vi.fn().mockResolvedValue({ ok: true, messageId: "ws-media-1" });
|
|
632
|
+
const markExternalActivity = vi.fn();
|
|
633
|
+
runtime.registerBotWsPushHandle(
|
|
634
|
+
"default",
|
|
635
|
+
createBotWsHandle({
|
|
636
|
+
sendMedia,
|
|
637
|
+
}),
|
|
638
|
+
);
|
|
639
|
+
runtime.registerActiveBotWsReplyHandle({
|
|
640
|
+
accountId: "default",
|
|
641
|
+
sessionKey: "agent:ops_bot:wecom:default:dm:zhangsan",
|
|
642
|
+
peerKind: "direct",
|
|
643
|
+
peerId: "zhangsan",
|
|
644
|
+
handle: {
|
|
645
|
+
context: {
|
|
646
|
+
transport: "bot-ws",
|
|
647
|
+
accountId: "default",
|
|
648
|
+
raw: { transport: "bot-ws", envelopeType: "ws", body: {} },
|
|
649
|
+
},
|
|
650
|
+
deliver: vi.fn(),
|
|
651
|
+
markExternalActivity,
|
|
652
|
+
} as any,
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
const cfg = {
|
|
656
|
+
channels: {
|
|
657
|
+
wecom: {
|
|
658
|
+
enabled: true,
|
|
659
|
+
bot: {
|
|
660
|
+
primaryTransport: "ws",
|
|
661
|
+
ws: {
|
|
662
|
+
botId: "bot-1",
|
|
663
|
+
secret: "secret-1",
|
|
664
|
+
},
|
|
665
|
+
},
|
|
666
|
+
agent: {
|
|
667
|
+
corpId: "corp",
|
|
668
|
+
corpSecret: "secret",
|
|
669
|
+
agentId: 1000002,
|
|
670
|
+
token: "token",
|
|
671
|
+
encodingAESKey: "aes",
|
|
672
|
+
},
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
await wecomOutbound.sendMedia({
|
|
678
|
+
cfg,
|
|
679
|
+
sessionKey: "agent:ops_bot:wecom:default:dm:zhangsan",
|
|
396
680
|
to: "user:zhangsan",
|
|
397
681
|
text: "caption",
|
|
398
682
|
mediaUrl: "https://example.com/media.png",
|
|
399
683
|
} as any);
|
|
400
684
|
|
|
401
685
|
expect(sendMedia).toHaveBeenCalledTimes(1);
|
|
686
|
+
expect(markExternalActivity).toHaveBeenCalledTimes(1);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it("keeps agent-source peer targets on the Agent media path without sessionKey", async () => {
|
|
690
|
+
const { wecomOutbound } = await import("./outbound.js");
|
|
691
|
+
const runtime = await import("./runtime.js");
|
|
692
|
+
const api = await import("./transport/agent-api/core.js");
|
|
693
|
+
const sourceRegistry = await import("./runtime/source-registry.js");
|
|
694
|
+
const sendMedia = vi.fn().mockResolvedValue({ ok: true, messageId: "ws-media-1" });
|
|
695
|
+
runtime.registerBotWsPushHandle(
|
|
696
|
+
"default",
|
|
697
|
+
createBotWsHandle({
|
|
698
|
+
sendMedia,
|
|
699
|
+
}),
|
|
700
|
+
);
|
|
701
|
+
sourceRegistry.registerWecomSourceSnapshot({
|
|
702
|
+
accountId: "default",
|
|
703
|
+
source: "agent-callback",
|
|
704
|
+
peerKind: "direct",
|
|
705
|
+
peerId: "zhangsan",
|
|
706
|
+
});
|
|
707
|
+
(api.uploadMedia as any).mockResolvedValue("media-1");
|
|
708
|
+
(api.sendMedia as any).mockResolvedValue(undefined);
|
|
709
|
+
(api.sendMedia as any).mockClear();
|
|
710
|
+
vi.stubGlobal(
|
|
711
|
+
"fetch",
|
|
712
|
+
vi.fn().mockResolvedValue({
|
|
713
|
+
ok: true,
|
|
714
|
+
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
|
715
|
+
headers: new Headers({ "content-type": "image/png" }),
|
|
716
|
+
}),
|
|
717
|
+
);
|
|
718
|
+
|
|
719
|
+
const cfg = {
|
|
720
|
+
channels: {
|
|
721
|
+
wecom: {
|
|
722
|
+
enabled: true,
|
|
723
|
+
bot: {
|
|
724
|
+
primaryTransport: "ws",
|
|
725
|
+
ws: {
|
|
726
|
+
botId: "bot-1",
|
|
727
|
+
secret: "secret-1",
|
|
728
|
+
},
|
|
729
|
+
},
|
|
730
|
+
agent: {
|
|
731
|
+
corpId: "corp",
|
|
732
|
+
corpSecret: "secret",
|
|
733
|
+
agentId: 1000002,
|
|
734
|
+
token: "token",
|
|
735
|
+
encodingAESKey: "aes",
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
},
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
await wecomOutbound.sendMedia({
|
|
742
|
+
cfg,
|
|
743
|
+
to: "user:zhangsan",
|
|
744
|
+
text: "caption",
|
|
745
|
+
mediaUrl: "https://example.com/media.png",
|
|
746
|
+
} as any);
|
|
747
|
+
|
|
748
|
+
expect(sendMedia).not.toHaveBeenCalled();
|
|
749
|
+
expect(api.sendMedia).toHaveBeenCalledTimes(1);
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
it("merges configured media local roots into Bot WS sends", async () => {
|
|
753
|
+
const { wecomOutbound } = await import("./outbound.js");
|
|
754
|
+
const runtime = await import("./runtime.js");
|
|
755
|
+
const sendMedia = vi.fn().mockResolvedValue({ ok: true, messageId: "ws-media-merged" });
|
|
756
|
+
runtime.registerBotWsPushHandle(
|
|
757
|
+
"default",
|
|
758
|
+
createBotWsHandle({
|
|
759
|
+
sendMedia,
|
|
760
|
+
}),
|
|
761
|
+
);
|
|
762
|
+
|
|
763
|
+
const cfg = {
|
|
764
|
+
channels: {
|
|
765
|
+
wecom: {
|
|
766
|
+
enabled: true,
|
|
767
|
+
bot: {
|
|
768
|
+
primaryTransport: "ws",
|
|
769
|
+
ws: {
|
|
770
|
+
botId: "bot-1",
|
|
771
|
+
secret: "secret-1",
|
|
772
|
+
},
|
|
773
|
+
},
|
|
774
|
+
media: {
|
|
775
|
+
localRoots: ["/tmp/downloads"],
|
|
776
|
+
},
|
|
777
|
+
},
|
|
778
|
+
},
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
await wecomOutbound.sendMedia({
|
|
782
|
+
cfg,
|
|
783
|
+
to: "user:zhangsan",
|
|
784
|
+
mediaUrl: "/tmp/workspace-agent/01.png",
|
|
785
|
+
mediaLocalRoots: ["/tmp/workspace-agent"],
|
|
786
|
+
} as any);
|
|
787
|
+
|
|
788
|
+
expect(sendMedia).toHaveBeenCalledWith(
|
|
789
|
+
expect.objectContaining({
|
|
790
|
+
chatId: "zhangsan",
|
|
791
|
+
mediaUrl: "/tmp/workspace-agent/01.png",
|
|
792
|
+
mediaLocalRoots: expect.arrayContaining(["/tmp/workspace-agent", "/tmp/downloads"]),
|
|
793
|
+
text: undefined,
|
|
794
|
+
}),
|
|
795
|
+
);
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
it("passes account-aware mediaMaxMb to Bot WS media sends", async () => {
|
|
799
|
+
const { wecomOutbound } = await import("./outbound.js");
|
|
800
|
+
const runtime = await import("./runtime.js");
|
|
801
|
+
const sendMedia = vi.fn().mockResolvedValue({ ok: true, messageId: "ws-media-limit" });
|
|
802
|
+
runtime.registerBotWsPushHandle(
|
|
803
|
+
"acct-ws",
|
|
804
|
+
createBotWsHandle({
|
|
805
|
+
sendMedia,
|
|
806
|
+
}),
|
|
807
|
+
);
|
|
808
|
+
|
|
809
|
+
const cfg = {
|
|
810
|
+
agents: {
|
|
811
|
+
defaults: {
|
|
812
|
+
mediaMaxMb: 12,
|
|
813
|
+
},
|
|
814
|
+
},
|
|
815
|
+
channels: {
|
|
816
|
+
wecom: {
|
|
817
|
+
enabled: true,
|
|
818
|
+
mediaMaxMb: 24,
|
|
819
|
+
accounts: {
|
|
820
|
+
"acct-ws": {
|
|
821
|
+
enabled: true,
|
|
822
|
+
mediaMaxMb: 36,
|
|
823
|
+
bot: {
|
|
824
|
+
primaryTransport: "ws",
|
|
825
|
+
ws: {
|
|
826
|
+
botId: "bot-1",
|
|
827
|
+
secret: "secret-1",
|
|
828
|
+
},
|
|
829
|
+
},
|
|
830
|
+
},
|
|
831
|
+
},
|
|
832
|
+
},
|
|
833
|
+
},
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
await wecomOutbound.sendMedia({
|
|
837
|
+
cfg,
|
|
838
|
+
accountId: "acct-ws",
|
|
839
|
+
to: "user:zhangsan",
|
|
840
|
+
mediaUrl: "https://example.com/media.png",
|
|
841
|
+
} as any);
|
|
842
|
+
|
|
843
|
+
expect(sendMedia).toHaveBeenCalledWith(
|
|
844
|
+
expect.objectContaining({
|
|
845
|
+
chatId: "zhangsan",
|
|
846
|
+
maxBytes: 36 * 1024 * 1024,
|
|
847
|
+
}),
|
|
848
|
+
);
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
it("does not fall back to Agent media when Bot WS conversation media delivery fails", async () => {
|
|
852
|
+
const { wecomOutbound } = await import("./outbound.js");
|
|
853
|
+
const runtime = await import("./runtime.js");
|
|
854
|
+
const api = await import("./transport/agent-api/core.js");
|
|
855
|
+
const sendMedia = vi.fn().mockResolvedValue({ ok: false, error: "upload failed" });
|
|
856
|
+
runtime.registerBotWsPushHandle(
|
|
857
|
+
"default",
|
|
858
|
+
createBotWsHandle({
|
|
859
|
+
sendMedia,
|
|
860
|
+
}),
|
|
861
|
+
);
|
|
862
|
+
(api.uploadMedia as any).mockResolvedValue("media-1");
|
|
863
|
+
(api.sendMedia as any).mockResolvedValue(undefined);
|
|
864
|
+
(api.sendMedia as any).mockClear();
|
|
865
|
+
vi.stubGlobal(
|
|
866
|
+
"fetch",
|
|
867
|
+
vi.fn().mockResolvedValue({
|
|
868
|
+
ok: true,
|
|
869
|
+
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
|
870
|
+
headers: new Headers({ "content-type": "image/png" }),
|
|
871
|
+
}),
|
|
872
|
+
);
|
|
873
|
+
|
|
874
|
+
const cfg = {
|
|
875
|
+
channels: {
|
|
876
|
+
wecom: {
|
|
877
|
+
enabled: true,
|
|
878
|
+
bot: {
|
|
879
|
+
primaryTransport: "ws",
|
|
880
|
+
ws: {
|
|
881
|
+
botId: "bot-1",
|
|
882
|
+
secret: "secret-1",
|
|
883
|
+
},
|
|
884
|
+
},
|
|
885
|
+
agent: {
|
|
886
|
+
corpId: "corp",
|
|
887
|
+
corpSecret: "secret",
|
|
888
|
+
agentId: 1000002,
|
|
889
|
+
token: "token",
|
|
890
|
+
encodingAESKey: "aes",
|
|
891
|
+
},
|
|
892
|
+
},
|
|
893
|
+
},
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
await expect(
|
|
897
|
+
wecomOutbound.sendMedia({
|
|
898
|
+
cfg,
|
|
899
|
+
to: "user:zhangsan",
|
|
900
|
+
text: "caption",
|
|
901
|
+
mediaUrl: "https://example.com/media.png",
|
|
902
|
+
} as any),
|
|
903
|
+
).rejects.toThrow(/Bot WS media delivery failed/i);
|
|
904
|
+
|
|
905
|
+
expect(sendMedia).toHaveBeenCalledTimes(1);
|
|
906
|
+
expect(api.sendMedia).not.toHaveBeenCalled();
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
it("keeps explicit agent targets on the Agent media path", async () => {
|
|
910
|
+
const { wecomOutbound } = await import("./outbound.js");
|
|
911
|
+
const runtime = await import("./runtime.js");
|
|
912
|
+
const api = await import("./transport/agent-api/core.js");
|
|
913
|
+
const sendMedia = vi.fn().mockResolvedValue({ ok: true, messageId: "ws-media-1" });
|
|
914
|
+
runtime.registerBotWsPushHandle(
|
|
915
|
+
"default",
|
|
916
|
+
createBotWsHandle({
|
|
917
|
+
sendMedia,
|
|
918
|
+
}),
|
|
919
|
+
);
|
|
920
|
+
(api.uploadMedia as any).mockResolvedValue("media-1");
|
|
921
|
+
(api.sendMedia as any).mockResolvedValue(undefined);
|
|
922
|
+
(api.sendMedia as any).mockClear();
|
|
923
|
+
vi.stubGlobal(
|
|
924
|
+
"fetch",
|
|
925
|
+
vi.fn().mockResolvedValue({
|
|
926
|
+
ok: true,
|
|
927
|
+
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
|
928
|
+
headers: new Headers({ "content-type": "image/png" }),
|
|
929
|
+
}),
|
|
930
|
+
);
|
|
931
|
+
|
|
932
|
+
const cfg = {
|
|
933
|
+
channels: {
|
|
934
|
+
wecom: {
|
|
935
|
+
enabled: true,
|
|
936
|
+
bot: {
|
|
937
|
+
primaryTransport: "ws",
|
|
938
|
+
ws: {
|
|
939
|
+
botId: "bot-1",
|
|
940
|
+
secret: "secret-1",
|
|
941
|
+
},
|
|
942
|
+
},
|
|
943
|
+
agent: {
|
|
944
|
+
corpId: "corp",
|
|
945
|
+
corpSecret: "secret",
|
|
946
|
+
agentId: 1000002,
|
|
947
|
+
token: "token",
|
|
948
|
+
encodingAESKey: "aes",
|
|
949
|
+
},
|
|
950
|
+
},
|
|
951
|
+
},
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
await wecomOutbound.sendMedia({
|
|
955
|
+
cfg,
|
|
956
|
+
to: "wecom-agent:default:user:zhangsan",
|
|
957
|
+
text: "caption",
|
|
958
|
+
mediaUrl: "https://example.com/media.png",
|
|
959
|
+
} as any);
|
|
960
|
+
|
|
961
|
+
expect(sendMedia).not.toHaveBeenCalled();
|
|
402
962
|
expect(api.sendMedia).toHaveBeenCalledTimes(1);
|
|
403
963
|
});
|
|
404
964
|
|