@openclaw/feishu 2026.3.2 → 2026.3.8-beta.1
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/index.ts +2 -2
- package/package.json +1 -1
- package/src/accounts.test.ts +199 -13
- package/src/accounts.ts +45 -17
- package/src/bitable.ts +40 -28
- package/src/bot.checkBotMentioned.test.ts +8 -0
- package/src/bot.stripBotMention.test.ts +118 -22
- package/src/bot.test.ts +516 -9
- package/src/bot.ts +366 -109
- package/src/card-action.ts +1 -1
- package/src/channel.test.ts +1 -1
- package/src/channel.ts +52 -64
- package/src/chat.test.ts +2 -2
- package/src/chat.ts +1 -1
- package/src/client.test.ts +207 -4
- package/src/client.ts +70 -5
- package/src/config-schema.test.ts +14 -6
- package/src/config-schema.ts +5 -1
- package/src/dedup.ts +1 -1
- package/src/directory.test.ts +40 -0
- package/src/directory.ts +29 -50
- package/src/docx-batch-insert.test.ts +90 -0
- package/src/docx-batch-insert.ts +8 -11
- package/src/docx.account-selection.test.ts +3 -3
- package/src/docx.ts +1 -1
- package/src/drive.ts +13 -17
- package/src/dynamic-agent.ts +1 -1
- package/src/feishu-command-handler.ts +59 -0
- package/src/media.test.ts +60 -13
- package/src/media.ts +23 -9
- package/src/monitor.account.ts +19 -8
- package/src/monitor.reaction.test.ts +111 -105
- package/src/monitor.startup.test.ts +11 -10
- package/src/monitor.startup.ts +20 -7
- package/src/monitor.state.ts +4 -1
- package/src/monitor.test-mocks.ts +42 -9
- package/src/monitor.transport.ts +4 -1
- package/src/monitor.ts +4 -4
- package/src/monitor.webhook-security.test.ts +8 -23
- package/src/onboarding.status.test.ts +1 -1
- package/src/onboarding.test.ts +143 -0
- package/src/onboarding.ts +86 -71
- package/src/outbound.test.ts +178 -0
- package/src/outbound.ts +39 -6
- package/src/perm.ts +11 -15
- package/src/policy.test.ts +40 -0
- package/src/policy.ts +9 -10
- package/src/probe.test.ts +18 -18
- package/src/reactions.ts +1 -1
- package/src/reply-dispatcher.test.ts +175 -0
- package/src/reply-dispatcher.ts +69 -21
- package/src/runtime.ts +5 -13
- package/src/secret-input.ts +8 -14
- package/src/send-message.ts +71 -0
- package/src/send-target.test.ts +1 -1
- package/src/send-target.ts +1 -1
- package/src/send.reply-fallback.test.ts +74 -0
- package/src/send.test.ts +1 -1
- package/src/send.ts +88 -49
- package/src/streaming-card.test.ts +54 -0
- package/src/streaming-card.ts +96 -28
- package/src/targets.ts +5 -1
- package/src/tool-account-routing.test.ts +3 -3
- package/src/tool-account.ts +1 -1
- package/src/tool-factory-test-harness.ts +1 -1
- package/src/tool-result.test.ts +32 -0
- package/src/tool-result.ts +14 -0
- package/src/types.ts +2 -3
- package/src/typing.ts +1 -1
- package/src/wiki.ts +15 -19
package/src/bot.test.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/feishu";
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { createPluginRuntimeMock } from "../../test-utils/plugin-runtime-mock.js";
|
|
4
4
|
import type { FeishuMessageEvent } from "./bot.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
buildBroadcastSessionKey,
|
|
7
|
+
buildFeishuAgentBody,
|
|
8
|
+
handleFeishuMessage,
|
|
9
|
+
resolveBroadcastAgents,
|
|
10
|
+
toMessageResourceType,
|
|
11
|
+
} from "./bot.js";
|
|
6
12
|
import { setFeishuRuntime } from "./runtime.js";
|
|
7
13
|
|
|
8
14
|
const {
|
|
@@ -453,14 +459,17 @@ describe("handleFeishuMessage command authorization", () => {
|
|
|
453
459
|
id: "ou-unapproved",
|
|
454
460
|
meta: { name: undefined },
|
|
455
461
|
});
|
|
456
|
-
expect(mockBuildPairingReply).toHaveBeenCalledWith({
|
|
457
|
-
channel: "feishu",
|
|
458
|
-
idLine: "Your Feishu user id: ou-unapproved",
|
|
459
|
-
code: "ABCDEFGH",
|
|
460
|
-
});
|
|
461
462
|
expect(mockSendMessageFeishu).toHaveBeenCalledWith(
|
|
462
463
|
expect.objectContaining({
|
|
463
464
|
to: "chat:oc-dm",
|
|
465
|
+
text: expect.stringContaining("Your Feishu user id: ou-unapproved"),
|
|
466
|
+
accountId: "default",
|
|
467
|
+
}),
|
|
468
|
+
);
|
|
469
|
+
expect(mockSendMessageFeishu).toHaveBeenCalledWith(
|
|
470
|
+
expect.objectContaining({
|
|
471
|
+
to: "chat:oc-dm",
|
|
472
|
+
text: expect.stringContaining("Pairing code: ABCDEFGH"),
|
|
464
473
|
accountId: "default",
|
|
465
474
|
}),
|
|
466
475
|
);
|
|
@@ -515,6 +524,42 @@ describe("handleFeishuMessage command authorization", () => {
|
|
|
515
524
|
);
|
|
516
525
|
});
|
|
517
526
|
|
|
527
|
+
it("normalizes group mention-prefixed slash commands before command-auth probing", async () => {
|
|
528
|
+
mockShouldComputeCommandAuthorized.mockReturnValue(true);
|
|
529
|
+
|
|
530
|
+
const cfg: ClawdbotConfig = {
|
|
531
|
+
channels: {
|
|
532
|
+
feishu: {
|
|
533
|
+
groups: {
|
|
534
|
+
"oc-group": {
|
|
535
|
+
requireMention: false,
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
} as ClawdbotConfig;
|
|
541
|
+
|
|
542
|
+
const event: FeishuMessageEvent = {
|
|
543
|
+
sender: {
|
|
544
|
+
sender_id: {
|
|
545
|
+
open_id: "ou-attacker",
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
message: {
|
|
549
|
+
message_id: "msg-group-mention-command-probe",
|
|
550
|
+
chat_id: "oc-group",
|
|
551
|
+
chat_type: "group",
|
|
552
|
+
message_type: "text",
|
|
553
|
+
content: JSON.stringify({ text: "@_user_1/model" }),
|
|
554
|
+
mentions: [{ key: "@_user_1", id: { open_id: "ou-bot" }, name: "Bot", tenant_key: "" }],
|
|
555
|
+
},
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
await dispatchMessage({ cfg, event });
|
|
559
|
+
|
|
560
|
+
expect(mockShouldComputeCommandAuthorized).toHaveBeenCalledWith("/model", cfg);
|
|
561
|
+
});
|
|
562
|
+
|
|
518
563
|
it("falls back to top-level allowFrom for group command authorization", async () => {
|
|
519
564
|
mockShouldComputeCommandAuthorized.mockReturnValue(true);
|
|
520
565
|
mockResolveCommandAuthorizedFromAuthorizers.mockReturnValue(true);
|
|
@@ -1046,7 +1091,7 @@ describe("handleFeishuMessage command authorization", () => {
|
|
|
1046
1091
|
channels: {
|
|
1047
1092
|
feishu: {
|
|
1048
1093
|
appId: "cli_test",
|
|
1049
|
-
appSecret: "sec_test",
|
|
1094
|
+
appSecret: "sec_test", // pragma: allowlist secret
|
|
1050
1095
|
groups: {
|
|
1051
1096
|
"oc-group": {
|
|
1052
1097
|
requireMention: false,
|
|
@@ -1109,7 +1154,7 @@ describe("handleFeishuMessage command authorization", () => {
|
|
|
1109
1154
|
channels: {
|
|
1110
1155
|
feishu: {
|
|
1111
1156
|
appId: "cli_scope_bug",
|
|
1112
|
-
appSecret: "sec_scope_bug",
|
|
1157
|
+
appSecret: "sec_scope_bug", // pragma: allowlist secret
|
|
1113
1158
|
groups: {
|
|
1114
1159
|
"oc-group": {
|
|
1115
1160
|
requireMention: false,
|
|
@@ -1511,6 +1556,120 @@ describe("handleFeishuMessage command authorization", () => {
|
|
|
1511
1556
|
);
|
|
1512
1557
|
});
|
|
1513
1558
|
|
|
1559
|
+
it("replies to triggering message in normal group even when root_id is present (#32980)", async () => {
|
|
1560
|
+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
1561
|
+
|
|
1562
|
+
const cfg: ClawdbotConfig = {
|
|
1563
|
+
channels: {
|
|
1564
|
+
feishu: {
|
|
1565
|
+
groups: {
|
|
1566
|
+
"oc-group": {
|
|
1567
|
+
requireMention: false,
|
|
1568
|
+
groupSessionScope: "group",
|
|
1569
|
+
},
|
|
1570
|
+
},
|
|
1571
|
+
},
|
|
1572
|
+
},
|
|
1573
|
+
} as ClawdbotConfig;
|
|
1574
|
+
|
|
1575
|
+
const event: FeishuMessageEvent = {
|
|
1576
|
+
sender: { sender_id: { open_id: "ou-normal-user" } },
|
|
1577
|
+
message: {
|
|
1578
|
+
message_id: "om_quote_reply",
|
|
1579
|
+
root_id: "om_original_msg",
|
|
1580
|
+
chat_id: "oc-group",
|
|
1581
|
+
chat_type: "group",
|
|
1582
|
+
message_type: "text",
|
|
1583
|
+
content: JSON.stringify({ text: "hello in normal group" }),
|
|
1584
|
+
},
|
|
1585
|
+
};
|
|
1586
|
+
|
|
1587
|
+
await dispatchMessage({ cfg, event });
|
|
1588
|
+
|
|
1589
|
+
expect(mockCreateFeishuReplyDispatcher).toHaveBeenCalledWith(
|
|
1590
|
+
expect.objectContaining({
|
|
1591
|
+
replyToMessageId: "om_quote_reply",
|
|
1592
|
+
rootId: "om_original_msg",
|
|
1593
|
+
}),
|
|
1594
|
+
);
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
it("replies to topic root in topic-mode group with root_id", async () => {
|
|
1598
|
+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
1599
|
+
|
|
1600
|
+
const cfg: ClawdbotConfig = {
|
|
1601
|
+
channels: {
|
|
1602
|
+
feishu: {
|
|
1603
|
+
groups: {
|
|
1604
|
+
"oc-group": {
|
|
1605
|
+
requireMention: false,
|
|
1606
|
+
groupSessionScope: "group_topic",
|
|
1607
|
+
},
|
|
1608
|
+
},
|
|
1609
|
+
},
|
|
1610
|
+
},
|
|
1611
|
+
} as ClawdbotConfig;
|
|
1612
|
+
|
|
1613
|
+
const event: FeishuMessageEvent = {
|
|
1614
|
+
sender: { sender_id: { open_id: "ou-topic-user" } },
|
|
1615
|
+
message: {
|
|
1616
|
+
message_id: "om_topic_reply",
|
|
1617
|
+
root_id: "om_topic_root",
|
|
1618
|
+
chat_id: "oc-group",
|
|
1619
|
+
chat_type: "group",
|
|
1620
|
+
message_type: "text",
|
|
1621
|
+
content: JSON.stringify({ text: "hello in topic group" }),
|
|
1622
|
+
},
|
|
1623
|
+
};
|
|
1624
|
+
|
|
1625
|
+
await dispatchMessage({ cfg, event });
|
|
1626
|
+
|
|
1627
|
+
expect(mockCreateFeishuReplyDispatcher).toHaveBeenCalledWith(
|
|
1628
|
+
expect.objectContaining({
|
|
1629
|
+
replyToMessageId: "om_topic_root",
|
|
1630
|
+
rootId: "om_topic_root",
|
|
1631
|
+
}),
|
|
1632
|
+
);
|
|
1633
|
+
});
|
|
1634
|
+
|
|
1635
|
+
it("replies to topic root in topic-sender group with root_id", async () => {
|
|
1636
|
+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
1637
|
+
|
|
1638
|
+
const cfg: ClawdbotConfig = {
|
|
1639
|
+
channels: {
|
|
1640
|
+
feishu: {
|
|
1641
|
+
groups: {
|
|
1642
|
+
"oc-group": {
|
|
1643
|
+
requireMention: false,
|
|
1644
|
+
groupSessionScope: "group_topic_sender",
|
|
1645
|
+
},
|
|
1646
|
+
},
|
|
1647
|
+
},
|
|
1648
|
+
},
|
|
1649
|
+
} as ClawdbotConfig;
|
|
1650
|
+
|
|
1651
|
+
const event: FeishuMessageEvent = {
|
|
1652
|
+
sender: { sender_id: { open_id: "ou-topic-sender-user" } },
|
|
1653
|
+
message: {
|
|
1654
|
+
message_id: "om_topic_sender_reply",
|
|
1655
|
+
root_id: "om_topic_sender_root",
|
|
1656
|
+
chat_id: "oc-group",
|
|
1657
|
+
chat_type: "group",
|
|
1658
|
+
message_type: "text",
|
|
1659
|
+
content: JSON.stringify({ text: "hello in topic sender group" }),
|
|
1660
|
+
},
|
|
1661
|
+
};
|
|
1662
|
+
|
|
1663
|
+
await dispatchMessage({ cfg, event });
|
|
1664
|
+
|
|
1665
|
+
expect(mockCreateFeishuReplyDispatcher).toHaveBeenCalledWith(
|
|
1666
|
+
expect.objectContaining({
|
|
1667
|
+
replyToMessageId: "om_topic_sender_root",
|
|
1668
|
+
rootId: "om_topic_sender_root",
|
|
1669
|
+
}),
|
|
1670
|
+
);
|
|
1671
|
+
});
|
|
1672
|
+
|
|
1514
1673
|
it("forces thread replies when inbound message contains thread_id", async () => {
|
|
1515
1674
|
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
1516
1675
|
|
|
@@ -1598,3 +1757,351 @@ describe("toMessageResourceType", () => {
|
|
|
1598
1757
|
expect(toMessageResourceType("sticker")).toBe("file");
|
|
1599
1758
|
});
|
|
1600
1759
|
});
|
|
1760
|
+
|
|
1761
|
+
describe("resolveBroadcastAgents", () => {
|
|
1762
|
+
it("returns agent list when broadcast config has the peerId", () => {
|
|
1763
|
+
const cfg = { broadcast: { oc_group123: ["susan", "main"] } } as unknown as ClawdbotConfig;
|
|
1764
|
+
expect(resolveBroadcastAgents(cfg, "oc_group123")).toEqual(["susan", "main"]);
|
|
1765
|
+
});
|
|
1766
|
+
|
|
1767
|
+
it("returns null when no broadcast config", () => {
|
|
1768
|
+
const cfg = {} as ClawdbotConfig;
|
|
1769
|
+
expect(resolveBroadcastAgents(cfg, "oc_group123")).toBeNull();
|
|
1770
|
+
});
|
|
1771
|
+
|
|
1772
|
+
it("returns null when peerId not in broadcast", () => {
|
|
1773
|
+
const cfg = { broadcast: { oc_other: ["susan"] } } as unknown as ClawdbotConfig;
|
|
1774
|
+
expect(resolveBroadcastAgents(cfg, "oc_group123")).toBeNull();
|
|
1775
|
+
});
|
|
1776
|
+
|
|
1777
|
+
it("returns null when agent list is empty", () => {
|
|
1778
|
+
const cfg = { broadcast: { oc_group123: [] } } as unknown as ClawdbotConfig;
|
|
1779
|
+
expect(resolveBroadcastAgents(cfg, "oc_group123")).toBeNull();
|
|
1780
|
+
});
|
|
1781
|
+
});
|
|
1782
|
+
|
|
1783
|
+
describe("buildBroadcastSessionKey", () => {
|
|
1784
|
+
it("replaces agent ID prefix in session key", () => {
|
|
1785
|
+
expect(buildBroadcastSessionKey("agent:main:feishu:group:oc_group123", "main", "susan")).toBe(
|
|
1786
|
+
"agent:susan:feishu:group:oc_group123",
|
|
1787
|
+
);
|
|
1788
|
+
});
|
|
1789
|
+
|
|
1790
|
+
it("handles compound peer IDs", () => {
|
|
1791
|
+
expect(
|
|
1792
|
+
buildBroadcastSessionKey(
|
|
1793
|
+
"agent:main:feishu:group:oc_group123:sender:ou_user1",
|
|
1794
|
+
"main",
|
|
1795
|
+
"susan",
|
|
1796
|
+
),
|
|
1797
|
+
).toBe("agent:susan:feishu:group:oc_group123:sender:ou_user1");
|
|
1798
|
+
});
|
|
1799
|
+
|
|
1800
|
+
it("returns base key unchanged when prefix does not match", () => {
|
|
1801
|
+
expect(buildBroadcastSessionKey("custom:key:format", "main", "susan")).toBe(
|
|
1802
|
+
"custom:key:format",
|
|
1803
|
+
);
|
|
1804
|
+
});
|
|
1805
|
+
});
|
|
1806
|
+
|
|
1807
|
+
describe("broadcast dispatch", () => {
|
|
1808
|
+
const mockFinalizeInboundContext = vi.fn((ctx: unknown) => ctx);
|
|
1809
|
+
const mockDispatchReplyFromConfig = vi
|
|
1810
|
+
.fn()
|
|
1811
|
+
.mockResolvedValue({ queuedFinal: false, counts: { final: 1 } });
|
|
1812
|
+
const mockWithReplyDispatcher = vi.fn(
|
|
1813
|
+
async ({
|
|
1814
|
+
dispatcher,
|
|
1815
|
+
run,
|
|
1816
|
+
onSettled,
|
|
1817
|
+
}: Parameters<PluginRuntime["channel"]["reply"]["withReplyDispatcher"]>[0]) => {
|
|
1818
|
+
try {
|
|
1819
|
+
return await run();
|
|
1820
|
+
} finally {
|
|
1821
|
+
dispatcher.markComplete();
|
|
1822
|
+
try {
|
|
1823
|
+
await dispatcher.waitForIdle();
|
|
1824
|
+
} finally {
|
|
1825
|
+
await onSettled?.();
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
},
|
|
1829
|
+
);
|
|
1830
|
+
const mockShouldComputeCommandAuthorized = vi.fn(() => false);
|
|
1831
|
+
const mockSaveMediaBuffer = vi.fn().mockResolvedValue({
|
|
1832
|
+
path: "/tmp/inbound-clip.mp4",
|
|
1833
|
+
contentType: "video/mp4",
|
|
1834
|
+
});
|
|
1835
|
+
|
|
1836
|
+
beforeEach(() => {
|
|
1837
|
+
vi.clearAllMocks();
|
|
1838
|
+
mockResolveAgentRoute.mockReturnValue({
|
|
1839
|
+
agentId: "main",
|
|
1840
|
+
channel: "feishu",
|
|
1841
|
+
accountId: "default",
|
|
1842
|
+
sessionKey: "agent:main:feishu:group:oc-broadcast-group",
|
|
1843
|
+
mainSessionKey: "agent:main:main",
|
|
1844
|
+
matchedBy: "default",
|
|
1845
|
+
});
|
|
1846
|
+
mockCreateFeishuClient.mockReturnValue({
|
|
1847
|
+
contact: {
|
|
1848
|
+
user: {
|
|
1849
|
+
get: vi.fn().mockResolvedValue({ data: { user: { name: "Sender" } } }),
|
|
1850
|
+
},
|
|
1851
|
+
},
|
|
1852
|
+
});
|
|
1853
|
+
setFeishuRuntime({
|
|
1854
|
+
system: {
|
|
1855
|
+
enqueueSystemEvent: vi.fn(),
|
|
1856
|
+
},
|
|
1857
|
+
channel: {
|
|
1858
|
+
routing: {
|
|
1859
|
+
resolveAgentRoute: mockResolveAgentRoute,
|
|
1860
|
+
},
|
|
1861
|
+
reply: {
|
|
1862
|
+
resolveEnvelopeFormatOptions: vi.fn(() => ({ template: "channel+name+time" })),
|
|
1863
|
+
formatAgentEnvelope: vi.fn((params: { body: string }) => params.body),
|
|
1864
|
+
finalizeInboundContext: mockFinalizeInboundContext,
|
|
1865
|
+
dispatchReplyFromConfig: mockDispatchReplyFromConfig,
|
|
1866
|
+
withReplyDispatcher: mockWithReplyDispatcher,
|
|
1867
|
+
},
|
|
1868
|
+
commands: {
|
|
1869
|
+
shouldComputeCommandAuthorized: mockShouldComputeCommandAuthorized,
|
|
1870
|
+
resolveCommandAuthorizedFromAuthorizers: vi.fn(() => false),
|
|
1871
|
+
},
|
|
1872
|
+
media: {
|
|
1873
|
+
saveMediaBuffer: mockSaveMediaBuffer,
|
|
1874
|
+
},
|
|
1875
|
+
pairing: {
|
|
1876
|
+
readAllowFromStore: vi.fn().mockResolvedValue([]),
|
|
1877
|
+
upsertPairingRequest: vi.fn().mockResolvedValue({ code: "ABCDEFGH", created: false }),
|
|
1878
|
+
buildPairingReply: vi.fn(() => "Pairing response"),
|
|
1879
|
+
},
|
|
1880
|
+
},
|
|
1881
|
+
media: {
|
|
1882
|
+
detectMime: vi.fn(async () => "application/octet-stream"),
|
|
1883
|
+
},
|
|
1884
|
+
} as unknown as PluginRuntime);
|
|
1885
|
+
});
|
|
1886
|
+
|
|
1887
|
+
it("dispatches to all broadcast agents when bot is mentioned", async () => {
|
|
1888
|
+
const cfg: ClawdbotConfig = {
|
|
1889
|
+
broadcast: { "oc-broadcast-group": ["susan", "main"] },
|
|
1890
|
+
agents: { list: [{ id: "main" }, { id: "susan" }] },
|
|
1891
|
+
channels: {
|
|
1892
|
+
feishu: {
|
|
1893
|
+
groups: {
|
|
1894
|
+
"oc-broadcast-group": {
|
|
1895
|
+
requireMention: true,
|
|
1896
|
+
},
|
|
1897
|
+
},
|
|
1898
|
+
},
|
|
1899
|
+
},
|
|
1900
|
+
} as unknown as ClawdbotConfig;
|
|
1901
|
+
|
|
1902
|
+
const event: FeishuMessageEvent = {
|
|
1903
|
+
sender: { sender_id: { open_id: "ou-sender" } },
|
|
1904
|
+
message: {
|
|
1905
|
+
message_id: "msg-broadcast-mentioned",
|
|
1906
|
+
chat_id: "oc-broadcast-group",
|
|
1907
|
+
chat_type: "group",
|
|
1908
|
+
message_type: "text",
|
|
1909
|
+
content: JSON.stringify({ text: "hello @bot" }),
|
|
1910
|
+
mentions: [
|
|
1911
|
+
{ key: "@_user_1", id: { open_id: "bot-open-id" }, name: "Bot", tenant_key: "" },
|
|
1912
|
+
],
|
|
1913
|
+
},
|
|
1914
|
+
};
|
|
1915
|
+
|
|
1916
|
+
await handleFeishuMessage({
|
|
1917
|
+
cfg,
|
|
1918
|
+
event,
|
|
1919
|
+
botOpenId: "bot-open-id",
|
|
1920
|
+
runtime: createRuntimeEnv(),
|
|
1921
|
+
});
|
|
1922
|
+
|
|
1923
|
+
// Both agents should get dispatched
|
|
1924
|
+
expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(2);
|
|
1925
|
+
|
|
1926
|
+
// Verify session keys for both agents
|
|
1927
|
+
const sessionKeys = mockFinalizeInboundContext.mock.calls.map(
|
|
1928
|
+
(call: unknown[]) => (call[0] as { SessionKey: string }).SessionKey,
|
|
1929
|
+
);
|
|
1930
|
+
expect(sessionKeys).toContain("agent:susan:feishu:group:oc-broadcast-group");
|
|
1931
|
+
expect(sessionKeys).toContain("agent:main:feishu:group:oc-broadcast-group");
|
|
1932
|
+
|
|
1933
|
+
// Active agent (mentioned) gets the real Feishu reply dispatcher
|
|
1934
|
+
expect(mockCreateFeishuReplyDispatcher).toHaveBeenCalledTimes(1);
|
|
1935
|
+
expect(mockCreateFeishuReplyDispatcher).toHaveBeenCalledWith(
|
|
1936
|
+
expect.objectContaining({ agentId: "main" }),
|
|
1937
|
+
);
|
|
1938
|
+
});
|
|
1939
|
+
|
|
1940
|
+
it("skips broadcast dispatch when bot is NOT mentioned (requireMention=true)", async () => {
|
|
1941
|
+
const cfg: ClawdbotConfig = {
|
|
1942
|
+
broadcast: { "oc-broadcast-group": ["susan", "main"] },
|
|
1943
|
+
agents: { list: [{ id: "main" }, { id: "susan" }] },
|
|
1944
|
+
channels: {
|
|
1945
|
+
feishu: {
|
|
1946
|
+
groups: {
|
|
1947
|
+
"oc-broadcast-group": {
|
|
1948
|
+
requireMention: true,
|
|
1949
|
+
},
|
|
1950
|
+
},
|
|
1951
|
+
},
|
|
1952
|
+
},
|
|
1953
|
+
} as unknown as ClawdbotConfig;
|
|
1954
|
+
|
|
1955
|
+
const event: FeishuMessageEvent = {
|
|
1956
|
+
sender: { sender_id: { open_id: "ou-sender" } },
|
|
1957
|
+
message: {
|
|
1958
|
+
message_id: "msg-broadcast-not-mentioned",
|
|
1959
|
+
chat_id: "oc-broadcast-group",
|
|
1960
|
+
chat_type: "group",
|
|
1961
|
+
message_type: "text",
|
|
1962
|
+
content: JSON.stringify({ text: "hello everyone" }),
|
|
1963
|
+
},
|
|
1964
|
+
};
|
|
1965
|
+
|
|
1966
|
+
await handleFeishuMessage({
|
|
1967
|
+
cfg,
|
|
1968
|
+
event,
|
|
1969
|
+
runtime: createRuntimeEnv(),
|
|
1970
|
+
});
|
|
1971
|
+
|
|
1972
|
+
// No dispatch: requireMention=true and bot not mentioned → returns early.
|
|
1973
|
+
// The mentioned bot's handler (on another account or same account with
|
|
1974
|
+
// matching botOpenId) will handle broadcast dispatch for all agents.
|
|
1975
|
+
expect(mockDispatchReplyFromConfig).not.toHaveBeenCalled();
|
|
1976
|
+
expect(mockCreateFeishuReplyDispatcher).not.toHaveBeenCalled();
|
|
1977
|
+
});
|
|
1978
|
+
|
|
1979
|
+
it("preserves single-agent dispatch when no broadcast config", async () => {
|
|
1980
|
+
const cfg: ClawdbotConfig = {
|
|
1981
|
+
channels: {
|
|
1982
|
+
feishu: {
|
|
1983
|
+
groups: {
|
|
1984
|
+
"oc-broadcast-group": {
|
|
1985
|
+
requireMention: false,
|
|
1986
|
+
},
|
|
1987
|
+
},
|
|
1988
|
+
},
|
|
1989
|
+
},
|
|
1990
|
+
} as ClawdbotConfig;
|
|
1991
|
+
|
|
1992
|
+
const event: FeishuMessageEvent = {
|
|
1993
|
+
sender: { sender_id: { open_id: "ou-sender" } },
|
|
1994
|
+
message: {
|
|
1995
|
+
message_id: "msg-no-broadcast",
|
|
1996
|
+
chat_id: "oc-broadcast-group",
|
|
1997
|
+
chat_type: "group",
|
|
1998
|
+
message_type: "text",
|
|
1999
|
+
content: JSON.stringify({ text: "hello" }),
|
|
2000
|
+
},
|
|
2001
|
+
};
|
|
2002
|
+
|
|
2003
|
+
await handleFeishuMessage({
|
|
2004
|
+
cfg,
|
|
2005
|
+
event,
|
|
2006
|
+
runtime: createRuntimeEnv(),
|
|
2007
|
+
});
|
|
2008
|
+
|
|
2009
|
+
// Single dispatch (no broadcast)
|
|
2010
|
+
expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1);
|
|
2011
|
+
expect(mockCreateFeishuReplyDispatcher).toHaveBeenCalledTimes(1);
|
|
2012
|
+
expect(mockFinalizeInboundContext).toHaveBeenCalledWith(
|
|
2013
|
+
expect.objectContaining({
|
|
2014
|
+
SessionKey: "agent:main:feishu:group:oc-broadcast-group",
|
|
2015
|
+
}),
|
|
2016
|
+
);
|
|
2017
|
+
});
|
|
2018
|
+
|
|
2019
|
+
it("cross-account broadcast dedup: second account skips dispatch", async () => {
|
|
2020
|
+
const cfg: ClawdbotConfig = {
|
|
2021
|
+
broadcast: { "oc-broadcast-group": ["susan", "main"] },
|
|
2022
|
+
agents: { list: [{ id: "main" }, { id: "susan" }] },
|
|
2023
|
+
channels: {
|
|
2024
|
+
feishu: {
|
|
2025
|
+
groups: {
|
|
2026
|
+
"oc-broadcast-group": {
|
|
2027
|
+
requireMention: false,
|
|
2028
|
+
},
|
|
2029
|
+
},
|
|
2030
|
+
},
|
|
2031
|
+
},
|
|
2032
|
+
} as unknown as ClawdbotConfig;
|
|
2033
|
+
|
|
2034
|
+
const event: FeishuMessageEvent = {
|
|
2035
|
+
sender: { sender_id: { open_id: "ou-sender" } },
|
|
2036
|
+
message: {
|
|
2037
|
+
message_id: "msg-multi-account-dedup",
|
|
2038
|
+
chat_id: "oc-broadcast-group",
|
|
2039
|
+
chat_type: "group",
|
|
2040
|
+
message_type: "text",
|
|
2041
|
+
content: JSON.stringify({ text: "hello" }),
|
|
2042
|
+
},
|
|
2043
|
+
};
|
|
2044
|
+
|
|
2045
|
+
// First account handles broadcast normally
|
|
2046
|
+
await handleFeishuMessage({
|
|
2047
|
+
cfg,
|
|
2048
|
+
event,
|
|
2049
|
+
runtime: createRuntimeEnv(),
|
|
2050
|
+
accountId: "account-A",
|
|
2051
|
+
});
|
|
2052
|
+
expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(2);
|
|
2053
|
+
|
|
2054
|
+
mockDispatchReplyFromConfig.mockClear();
|
|
2055
|
+
mockFinalizeInboundContext.mockClear();
|
|
2056
|
+
|
|
2057
|
+
// Second account: same message ID, different account.
|
|
2058
|
+
// Per-account dedup passes (different namespace), but cross-account
|
|
2059
|
+
// broadcast dedup blocks dispatch.
|
|
2060
|
+
await handleFeishuMessage({
|
|
2061
|
+
cfg,
|
|
2062
|
+
event,
|
|
2063
|
+
runtime: createRuntimeEnv(),
|
|
2064
|
+
accountId: "account-B",
|
|
2065
|
+
});
|
|
2066
|
+
expect(mockDispatchReplyFromConfig).not.toHaveBeenCalled();
|
|
2067
|
+
});
|
|
2068
|
+
|
|
2069
|
+
it("skips unknown agents not in agents.list", async () => {
|
|
2070
|
+
const cfg: ClawdbotConfig = {
|
|
2071
|
+
broadcast: { "oc-broadcast-group": ["susan", "unknown-agent"] },
|
|
2072
|
+
agents: { list: [{ id: "main" }, { id: "susan" }] },
|
|
2073
|
+
channels: {
|
|
2074
|
+
feishu: {
|
|
2075
|
+
groups: {
|
|
2076
|
+
"oc-broadcast-group": {
|
|
2077
|
+
requireMention: false,
|
|
2078
|
+
},
|
|
2079
|
+
},
|
|
2080
|
+
},
|
|
2081
|
+
},
|
|
2082
|
+
} as unknown as ClawdbotConfig;
|
|
2083
|
+
|
|
2084
|
+
const event: FeishuMessageEvent = {
|
|
2085
|
+
sender: { sender_id: { open_id: "ou-sender" } },
|
|
2086
|
+
message: {
|
|
2087
|
+
message_id: "msg-broadcast-unknown-agent",
|
|
2088
|
+
chat_id: "oc-broadcast-group",
|
|
2089
|
+
chat_type: "group",
|
|
2090
|
+
message_type: "text",
|
|
2091
|
+
content: JSON.stringify({ text: "hello" }),
|
|
2092
|
+
},
|
|
2093
|
+
};
|
|
2094
|
+
|
|
2095
|
+
await handleFeishuMessage({
|
|
2096
|
+
cfg,
|
|
2097
|
+
event,
|
|
2098
|
+
runtime: createRuntimeEnv(),
|
|
2099
|
+
});
|
|
2100
|
+
|
|
2101
|
+
// Only susan should get dispatched (unknown-agent skipped)
|
|
2102
|
+
expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1);
|
|
2103
|
+
const sessionKey = (mockFinalizeInboundContext.mock.calls[0]?.[0] as { SessionKey: string })
|
|
2104
|
+
.SessionKey;
|
|
2105
|
+
expect(sessionKey).toBe("agent:susan:feishu:group:oc-broadcast-group");
|
|
2106
|
+
});
|
|
2107
|
+
});
|