@openclaw/feishu 2026.2.24 → 2026.3.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 -0
- package/package.json +2 -1
- package/skills/feishu-doc/SKILL.md +109 -3
- package/src/accounts.test.ts +90 -0
- package/src/accounts.ts +11 -2
- package/src/async.ts +62 -0
- package/src/bitable.ts +189 -215
- package/src/bot.card-action.test.ts +63 -0
- package/src/bot.checkBotMentioned.test.ts +55 -0
- package/src/bot.test.ts +863 -9
- package/src/bot.ts +414 -200
- package/src/card-action.ts +79 -0
- package/src/channel.ts +6 -0
- package/src/chat-schema.ts +24 -0
- package/src/chat.test.ts +89 -0
- package/src/chat.ts +130 -0
- package/src/client.test.ts +107 -0
- package/src/client.ts +13 -0
- package/src/config-schema.test.ts +82 -1
- package/src/config-schema.ts +54 -3
- package/src/doc-schema.ts +141 -0
- package/src/docx-batch-insert.ts +190 -0
- package/src/docx-color-text.ts +149 -0
- package/src/docx-table-ops.ts +298 -0
- package/src/docx.account-selection.test.ts +76 -0
- package/src/docx.test.ts +470 -0
- package/src/docx.ts +996 -72
- package/src/drive.ts +38 -33
- package/src/media.test.ts +123 -6
- package/src/media.ts +31 -10
- package/src/monitor.account.ts +286 -0
- package/src/monitor.reaction.test.ts +235 -0
- package/src/monitor.startup.test.ts +187 -0
- package/src/monitor.startup.ts +51 -0
- package/src/monitor.state.ts +76 -0
- package/src/monitor.transport.ts +163 -0
- package/src/monitor.ts +44 -346
- package/src/monitor.webhook-security.test.ts +27 -1
- package/src/outbound.test.ts +181 -0
- package/src/outbound.ts +94 -7
- package/src/perm.ts +37 -30
- package/src/policy.test.ts +56 -1
- package/src/policy.ts +5 -1
- package/src/post.test.ts +105 -0
- package/src/post.ts +274 -0
- package/src/probe.test.ts +253 -0
- package/src/probe.ts +99 -7
- package/src/reply-dispatcher.test.ts +259 -0
- package/src/reply-dispatcher.ts +139 -45
- package/src/send.reply-fallback.test.ts +105 -0
- package/src/send.test.ts +168 -0
- package/src/send.ts +143 -18
- package/src/streaming-card.ts +131 -43
- package/src/targets.test.ts +26 -1
- package/src/targets.ts +11 -6
- package/src/tool-account-routing.test.ts +129 -0
- package/src/tool-account.ts +70 -0
- package/src/tool-factory-test-harness.ts +76 -0
- package/src/tools-config.test.ts +21 -0
- package/src/tools-config.ts +2 -1
- package/src/types.ts +1 -0
- package/src/typing.test.ts +144 -0
- package/src/typing.ts +140 -10
- package/src/wiki.ts +55 -50
package/src/bot.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import type { FeishuMessageEvent } from "./bot.js";
|
|
4
|
-
import { handleFeishuMessage } from "./bot.js";
|
|
4
|
+
import { buildFeishuAgentBody, handleFeishuMessage, toMessageResourceType } from "./bot.js";
|
|
5
5
|
import { setFeishuRuntime } from "./runtime.js";
|
|
6
6
|
|
|
7
7
|
const {
|
|
@@ -9,6 +9,8 @@ const {
|
|
|
9
9
|
mockSendMessageFeishu,
|
|
10
10
|
mockGetMessageFeishu,
|
|
11
11
|
mockDownloadMessageResourceFeishu,
|
|
12
|
+
mockCreateFeishuClient,
|
|
13
|
+
mockResolveAgentRoute,
|
|
12
14
|
} = vi.hoisted(() => ({
|
|
13
15
|
mockCreateFeishuReplyDispatcher: vi.fn(() => ({
|
|
14
16
|
dispatcher: vi.fn(),
|
|
@@ -22,6 +24,13 @@ const {
|
|
|
22
24
|
contentType: "video/mp4",
|
|
23
25
|
fileName: "clip.mp4",
|
|
24
26
|
}),
|
|
27
|
+
mockCreateFeishuClient: vi.fn(),
|
|
28
|
+
mockResolveAgentRoute: vi.fn(() => ({
|
|
29
|
+
agentId: "main",
|
|
30
|
+
accountId: "default",
|
|
31
|
+
sessionKey: "agent:main:feishu:dm:ou-attacker",
|
|
32
|
+
matchedBy: "default",
|
|
33
|
+
})),
|
|
25
34
|
}));
|
|
26
35
|
|
|
27
36
|
vi.mock("./reply-dispatcher.js", () => ({
|
|
@@ -37,6 +46,10 @@ vi.mock("./media.js", () => ({
|
|
|
37
46
|
downloadMessageResourceFeishu: mockDownloadMessageResourceFeishu,
|
|
38
47
|
}));
|
|
39
48
|
|
|
49
|
+
vi.mock("./client.js", () => ({
|
|
50
|
+
createFeishuClient: mockCreateFeishuClient,
|
|
51
|
+
}));
|
|
52
|
+
|
|
40
53
|
function createRuntimeEnv(): RuntimeEnv {
|
|
41
54
|
return {
|
|
42
55
|
log: vi.fn(),
|
|
@@ -55,16 +68,59 @@ async function dispatchMessage(params: { cfg: ClawdbotConfig; event: FeishuMessa
|
|
|
55
68
|
});
|
|
56
69
|
}
|
|
57
70
|
|
|
71
|
+
describe("buildFeishuAgentBody", () => {
|
|
72
|
+
it("builds message id, speaker, quoted content, mentions, and permission notice in order", () => {
|
|
73
|
+
const body = buildFeishuAgentBody({
|
|
74
|
+
ctx: {
|
|
75
|
+
content: "hello world",
|
|
76
|
+
senderName: "Sender Name",
|
|
77
|
+
senderOpenId: "ou-sender",
|
|
78
|
+
messageId: "msg-42",
|
|
79
|
+
mentionTargets: [{ openId: "ou-target", name: "Target User", key: "@_user_1" }],
|
|
80
|
+
},
|
|
81
|
+
quotedContent: "previous message",
|
|
82
|
+
permissionErrorForAgent: {
|
|
83
|
+
code: 99991672,
|
|
84
|
+
message: "permission denied",
|
|
85
|
+
grantUrl: "https://open.feishu.cn/app/cli_test",
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(body).toBe(
|
|
90
|
+
'[message_id: msg-42]\nSender Name: [Replying to: "previous message"]\n\nhello world\n\n[System: Your reply will automatically @mention: Target User. Do not write @xxx yourself.]\n\n[System: The bot encountered a Feishu API permission error. Please inform the user about this issue and provide the permission grant URL for the admin to authorize. Permission grant URL: https://open.feishu.cn/app/cli_test]',
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
58
95
|
describe("handleFeishuMessage command authorization", () => {
|
|
59
96
|
const mockFinalizeInboundContext = vi.fn((ctx: unknown) => ctx);
|
|
60
97
|
const mockDispatchReplyFromConfig = vi
|
|
61
98
|
.fn()
|
|
62
99
|
.mockResolvedValue({ queuedFinal: false, counts: { final: 1 } });
|
|
100
|
+
const mockWithReplyDispatcher = vi.fn(
|
|
101
|
+
async ({
|
|
102
|
+
dispatcher,
|
|
103
|
+
run,
|
|
104
|
+
onSettled,
|
|
105
|
+
}: Parameters<PluginRuntime["channel"]["reply"]["withReplyDispatcher"]>[0]) => {
|
|
106
|
+
try {
|
|
107
|
+
return await run();
|
|
108
|
+
} finally {
|
|
109
|
+
dispatcher.markComplete();
|
|
110
|
+
try {
|
|
111
|
+
await dispatcher.waitForIdle();
|
|
112
|
+
} finally {
|
|
113
|
+
await onSettled?.();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
);
|
|
63
118
|
const mockResolveCommandAuthorizedFromAuthorizers = vi.fn(() => false);
|
|
64
119
|
const mockShouldComputeCommandAuthorized = vi.fn(() => true);
|
|
65
120
|
const mockReadAllowFromStore = vi.fn().mockResolvedValue([]);
|
|
66
121
|
const mockUpsertPairingRequest = vi.fn().mockResolvedValue({ code: "ABCDEFGH", created: false });
|
|
67
122
|
const mockBuildPairingReply = vi.fn(() => "Pairing response");
|
|
123
|
+
const mockEnqueueSystemEvent = vi.fn();
|
|
68
124
|
const mockSaveMediaBuffer = vi.fn().mockResolvedValue({
|
|
69
125
|
path: "/tmp/inbound-clip.mp4",
|
|
70
126
|
contentType: "video/mp4",
|
|
@@ -72,24 +128,35 @@ describe("handleFeishuMessage command authorization", () => {
|
|
|
72
128
|
|
|
73
129
|
beforeEach(() => {
|
|
74
130
|
vi.clearAllMocks();
|
|
131
|
+
mockShouldComputeCommandAuthorized.mockReset().mockReturnValue(true);
|
|
132
|
+
mockResolveAgentRoute.mockReturnValue({
|
|
133
|
+
agentId: "main",
|
|
134
|
+
accountId: "default",
|
|
135
|
+
sessionKey: "agent:main:feishu:dm:ou-attacker",
|
|
136
|
+
matchedBy: "default",
|
|
137
|
+
});
|
|
138
|
+
mockCreateFeishuClient.mockReturnValue({
|
|
139
|
+
contact: {
|
|
140
|
+
user: {
|
|
141
|
+
get: vi.fn().mockResolvedValue({ data: { user: { name: "Sender" } } }),
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
mockEnqueueSystemEvent.mockReset();
|
|
75
146
|
setFeishuRuntime({
|
|
76
147
|
system: {
|
|
77
|
-
enqueueSystemEvent:
|
|
148
|
+
enqueueSystemEvent: mockEnqueueSystemEvent,
|
|
78
149
|
},
|
|
79
150
|
channel: {
|
|
80
151
|
routing: {
|
|
81
|
-
resolveAgentRoute:
|
|
82
|
-
agentId: "main",
|
|
83
|
-
accountId: "default",
|
|
84
|
-
sessionKey: "agent:main:feishu:dm:ou-attacker",
|
|
85
|
-
matchedBy: "default",
|
|
86
|
-
})),
|
|
152
|
+
resolveAgentRoute: mockResolveAgentRoute,
|
|
87
153
|
},
|
|
88
154
|
reply: {
|
|
89
155
|
resolveEnvelopeFormatOptions: vi.fn(() => ({ template: "channel+name+time" })),
|
|
90
156
|
formatAgentEnvelope: vi.fn((params: { body: string }) => params.body),
|
|
91
157
|
finalizeInboundContext: mockFinalizeInboundContext,
|
|
92
158
|
dispatchReplyFromConfig: mockDispatchReplyFromConfig,
|
|
159
|
+
withReplyDispatcher: mockWithReplyDispatcher,
|
|
93
160
|
},
|
|
94
161
|
commands: {
|
|
95
162
|
shouldComputeCommandAuthorized: mockShouldComputeCommandAuthorized,
|
|
@@ -110,6 +177,37 @@ describe("handleFeishuMessage command authorization", () => {
|
|
|
110
177
|
} as unknown as PluginRuntime);
|
|
111
178
|
});
|
|
112
179
|
|
|
180
|
+
it("does not enqueue inbound preview text as system events", async () => {
|
|
181
|
+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
182
|
+
|
|
183
|
+
const cfg: ClawdbotConfig = {
|
|
184
|
+
channels: {
|
|
185
|
+
feishu: {
|
|
186
|
+
dmPolicy: "open",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
} as ClawdbotConfig;
|
|
190
|
+
|
|
191
|
+
const event: FeishuMessageEvent = {
|
|
192
|
+
sender: {
|
|
193
|
+
sender_id: {
|
|
194
|
+
open_id: "ou-attacker",
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
message: {
|
|
198
|
+
message_id: "msg-no-system-preview",
|
|
199
|
+
chat_id: "oc-dm",
|
|
200
|
+
chat_type: "p2p",
|
|
201
|
+
message_type: "text",
|
|
202
|
+
content: JSON.stringify({ text: "hi there" }),
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
await dispatchMessage({ cfg, event });
|
|
207
|
+
|
|
208
|
+
expect(mockEnqueueSystemEvent).not.toHaveBeenCalled();
|
|
209
|
+
});
|
|
210
|
+
|
|
113
211
|
it("uses authorizer resolution instead of hardcoded CommandAuthorized=true", async () => {
|
|
114
212
|
const cfg: ClawdbotConfig = {
|
|
115
213
|
commands: { useAccessGroups: true },
|
|
@@ -183,12 +281,91 @@ describe("handleFeishuMessage command authorization", () => {
|
|
|
183
281
|
|
|
184
282
|
await dispatchMessage({ cfg, event });
|
|
185
283
|
|
|
186
|
-
expect(mockReadAllowFromStore).toHaveBeenCalledWith(
|
|
284
|
+
expect(mockReadAllowFromStore).toHaveBeenCalledWith({
|
|
285
|
+
channel: "feishu",
|
|
286
|
+
accountId: "default",
|
|
287
|
+
});
|
|
187
288
|
expect(mockResolveCommandAuthorizedFromAuthorizers).not.toHaveBeenCalled();
|
|
188
289
|
expect(mockFinalizeInboundContext).toHaveBeenCalledTimes(1);
|
|
189
290
|
expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1);
|
|
190
291
|
});
|
|
191
292
|
|
|
293
|
+
it("skips sender-name lookup when resolveSenderNames is false", async () => {
|
|
294
|
+
const cfg: ClawdbotConfig = {
|
|
295
|
+
channels: {
|
|
296
|
+
feishu: {
|
|
297
|
+
dmPolicy: "open",
|
|
298
|
+
allowFrom: ["*"],
|
|
299
|
+
resolveSenderNames: false,
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
} as ClawdbotConfig;
|
|
303
|
+
|
|
304
|
+
const event: FeishuMessageEvent = {
|
|
305
|
+
sender: {
|
|
306
|
+
sender_id: {
|
|
307
|
+
open_id: "ou-attacker",
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
message: {
|
|
311
|
+
message_id: "msg-skip-sender-lookup",
|
|
312
|
+
chat_id: "oc-dm",
|
|
313
|
+
chat_type: "p2p",
|
|
314
|
+
message_type: "text",
|
|
315
|
+
content: JSON.stringify({ text: "hello" }),
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
await dispatchMessage({ cfg, event });
|
|
320
|
+
|
|
321
|
+
expect(mockCreateFeishuClient).not.toHaveBeenCalled();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("propagates parent/root message ids into inbound context for reply reconstruction", async () => {
|
|
325
|
+
mockGetMessageFeishu.mockResolvedValueOnce({
|
|
326
|
+
messageId: "om_parent_001",
|
|
327
|
+
chatId: "oc-group",
|
|
328
|
+
content: "quoted content",
|
|
329
|
+
contentType: "text",
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const cfg: ClawdbotConfig = {
|
|
333
|
+
channels: {
|
|
334
|
+
feishu: {
|
|
335
|
+
enabled: true,
|
|
336
|
+
dmPolicy: "open",
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
} as ClawdbotConfig;
|
|
340
|
+
|
|
341
|
+
const event: FeishuMessageEvent = {
|
|
342
|
+
sender: {
|
|
343
|
+
sender_id: {
|
|
344
|
+
open_id: "ou-replier",
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
message: {
|
|
348
|
+
message_id: "om_reply_001",
|
|
349
|
+
root_id: "om_root_001",
|
|
350
|
+
parent_id: "om_parent_001",
|
|
351
|
+
chat_id: "oc-dm",
|
|
352
|
+
chat_type: "p2p",
|
|
353
|
+
message_type: "text",
|
|
354
|
+
content: JSON.stringify({ text: "reply text" }),
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
await dispatchMessage({ cfg, event });
|
|
359
|
+
|
|
360
|
+
expect(mockFinalizeInboundContext).toHaveBeenCalledWith(
|
|
361
|
+
expect.objectContaining({
|
|
362
|
+
ReplyToId: "om_parent_001",
|
|
363
|
+
RootMessageId: "om_root_001",
|
|
364
|
+
ReplyToBody: "quoted content",
|
|
365
|
+
}),
|
|
366
|
+
);
|
|
367
|
+
});
|
|
368
|
+
|
|
192
369
|
it("creates pairing request and drops unauthorized DMs in pairing mode", async () => {
|
|
193
370
|
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
194
371
|
mockReadAllowFromStore.mockResolvedValue([]);
|
|
@@ -222,6 +399,7 @@ describe("handleFeishuMessage command authorization", () => {
|
|
|
222
399
|
|
|
223
400
|
expect(mockUpsertPairingRequest).toHaveBeenCalledWith({
|
|
224
401
|
channel: "feishu",
|
|
402
|
+
accountId: "default",
|
|
225
403
|
id: "ou-unapproved",
|
|
226
404
|
meta: { name: undefined },
|
|
227
405
|
});
|
|
@@ -335,6 +513,158 @@ describe("handleFeishuMessage command authorization", () => {
|
|
|
335
513
|
);
|
|
336
514
|
});
|
|
337
515
|
|
|
516
|
+
it("allows group sender when global groupSenderAllowFrom includes sender", async () => {
|
|
517
|
+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
518
|
+
|
|
519
|
+
const cfg: ClawdbotConfig = {
|
|
520
|
+
channels: {
|
|
521
|
+
feishu: {
|
|
522
|
+
groupPolicy: "open",
|
|
523
|
+
groupSenderAllowFrom: ["ou-allowed"],
|
|
524
|
+
groups: {
|
|
525
|
+
"oc-group": {
|
|
526
|
+
requireMention: false,
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
} as ClawdbotConfig;
|
|
532
|
+
|
|
533
|
+
const event: FeishuMessageEvent = {
|
|
534
|
+
sender: {
|
|
535
|
+
sender_id: {
|
|
536
|
+
open_id: "ou-allowed",
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
message: {
|
|
540
|
+
message_id: "msg-global-group-sender-allow",
|
|
541
|
+
chat_id: "oc-group",
|
|
542
|
+
chat_type: "group",
|
|
543
|
+
message_type: "text",
|
|
544
|
+
content: JSON.stringify({ text: "hello" }),
|
|
545
|
+
},
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
await dispatchMessage({ cfg, event });
|
|
549
|
+
|
|
550
|
+
expect(mockFinalizeInboundContext).toHaveBeenCalledWith(
|
|
551
|
+
expect.objectContaining({
|
|
552
|
+
ChatType: "group",
|
|
553
|
+
SenderId: "ou-allowed",
|
|
554
|
+
}),
|
|
555
|
+
);
|
|
556
|
+
expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it("blocks group sender when global groupSenderAllowFrom excludes sender", async () => {
|
|
560
|
+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
561
|
+
|
|
562
|
+
const cfg: ClawdbotConfig = {
|
|
563
|
+
channels: {
|
|
564
|
+
feishu: {
|
|
565
|
+
groupPolicy: "open",
|
|
566
|
+
groupSenderAllowFrom: ["ou-allowed"],
|
|
567
|
+
groups: {
|
|
568
|
+
"oc-group": {
|
|
569
|
+
requireMention: false,
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
} as ClawdbotConfig;
|
|
575
|
+
|
|
576
|
+
const event: FeishuMessageEvent = {
|
|
577
|
+
sender: {
|
|
578
|
+
sender_id: {
|
|
579
|
+
open_id: "ou-blocked",
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
message: {
|
|
583
|
+
message_id: "msg-global-group-sender-block",
|
|
584
|
+
chat_id: "oc-group",
|
|
585
|
+
chat_type: "group",
|
|
586
|
+
message_type: "text",
|
|
587
|
+
content: JSON.stringify({ text: "hello" }),
|
|
588
|
+
},
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
await dispatchMessage({ cfg, event });
|
|
592
|
+
|
|
593
|
+
expect(mockFinalizeInboundContext).not.toHaveBeenCalled();
|
|
594
|
+
expect(mockDispatchReplyFromConfig).not.toHaveBeenCalled();
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it("prefers per-group allowFrom over global groupSenderAllowFrom", async () => {
|
|
598
|
+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
599
|
+
|
|
600
|
+
const cfg: ClawdbotConfig = {
|
|
601
|
+
channels: {
|
|
602
|
+
feishu: {
|
|
603
|
+
groupPolicy: "open",
|
|
604
|
+
groupSenderAllowFrom: ["ou-global"],
|
|
605
|
+
groups: {
|
|
606
|
+
"oc-group": {
|
|
607
|
+
allowFrom: ["ou-group-only"],
|
|
608
|
+
requireMention: false,
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
} as ClawdbotConfig;
|
|
614
|
+
|
|
615
|
+
const event: FeishuMessageEvent = {
|
|
616
|
+
sender: {
|
|
617
|
+
sender_id: {
|
|
618
|
+
open_id: "ou-global",
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
message: {
|
|
622
|
+
message_id: "msg-per-group-precedence",
|
|
623
|
+
chat_id: "oc-group",
|
|
624
|
+
chat_type: "group",
|
|
625
|
+
message_type: "text",
|
|
626
|
+
content: JSON.stringify({ text: "hello" }),
|
|
627
|
+
},
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
await dispatchMessage({ cfg, event });
|
|
631
|
+
|
|
632
|
+
expect(mockFinalizeInboundContext).not.toHaveBeenCalled();
|
|
633
|
+
expect(mockDispatchReplyFromConfig).not.toHaveBeenCalled();
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it("drops message when groupConfig.enabled is false", async () => {
|
|
637
|
+
const cfg: ClawdbotConfig = {
|
|
638
|
+
channels: {
|
|
639
|
+
feishu: {
|
|
640
|
+
groups: {
|
|
641
|
+
"oc-disabled-group": {
|
|
642
|
+
enabled: false,
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
},
|
|
647
|
+
} as ClawdbotConfig;
|
|
648
|
+
|
|
649
|
+
const event: FeishuMessageEvent = {
|
|
650
|
+
sender: {
|
|
651
|
+
sender_id: { open_id: "ou-sender" },
|
|
652
|
+
},
|
|
653
|
+
message: {
|
|
654
|
+
message_id: "msg-disabled-group",
|
|
655
|
+
chat_id: "oc-disabled-group",
|
|
656
|
+
chat_type: "group",
|
|
657
|
+
message_type: "text",
|
|
658
|
+
content: JSON.stringify({ text: "hello" }),
|
|
659
|
+
},
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
await dispatchMessage({ cfg, event });
|
|
663
|
+
|
|
664
|
+
expect(mockFinalizeInboundContext).not.toHaveBeenCalled();
|
|
665
|
+
expect(mockDispatchReplyFromConfig).not.toHaveBeenCalled();
|
|
666
|
+
});
|
|
667
|
+
|
|
338
668
|
it("uses video file_key (not thumbnail image_key) for inbound video download", async () => {
|
|
339
669
|
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
340
670
|
|
|
@@ -382,4 +712,528 @@ describe("handleFeishuMessage command authorization", () => {
|
|
|
382
712
|
"clip.mp4",
|
|
383
713
|
);
|
|
384
714
|
});
|
|
715
|
+
|
|
716
|
+
it("uses media message_type file_key (not thumbnail image_key) for inbound mobile video download", async () => {
|
|
717
|
+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
718
|
+
|
|
719
|
+
const cfg: ClawdbotConfig = {
|
|
720
|
+
channels: {
|
|
721
|
+
feishu: {
|
|
722
|
+
dmPolicy: "open",
|
|
723
|
+
},
|
|
724
|
+
},
|
|
725
|
+
} as ClawdbotConfig;
|
|
726
|
+
|
|
727
|
+
const event: FeishuMessageEvent = {
|
|
728
|
+
sender: {
|
|
729
|
+
sender_id: {
|
|
730
|
+
open_id: "ou-sender",
|
|
731
|
+
},
|
|
732
|
+
},
|
|
733
|
+
message: {
|
|
734
|
+
message_id: "msg-media-inbound",
|
|
735
|
+
chat_id: "oc-dm",
|
|
736
|
+
chat_type: "p2p",
|
|
737
|
+
message_type: "media",
|
|
738
|
+
content: JSON.stringify({
|
|
739
|
+
file_key: "file_media_payload",
|
|
740
|
+
image_key: "img_media_thumb",
|
|
741
|
+
file_name: "mobile.mp4",
|
|
742
|
+
}),
|
|
743
|
+
},
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
await dispatchMessage({ cfg, event });
|
|
747
|
+
|
|
748
|
+
expect(mockDownloadMessageResourceFeishu).toHaveBeenCalledWith(
|
|
749
|
+
expect.objectContaining({
|
|
750
|
+
messageId: "msg-media-inbound",
|
|
751
|
+
fileKey: "file_media_payload",
|
|
752
|
+
type: "file",
|
|
753
|
+
}),
|
|
754
|
+
);
|
|
755
|
+
expect(mockSaveMediaBuffer).toHaveBeenCalledWith(
|
|
756
|
+
expect.any(Buffer),
|
|
757
|
+
"video/mp4",
|
|
758
|
+
"inbound",
|
|
759
|
+
expect.any(Number),
|
|
760
|
+
"clip.mp4",
|
|
761
|
+
);
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it("downloads embedded media tags from post messages as files", async () => {
|
|
765
|
+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
766
|
+
|
|
767
|
+
const cfg: ClawdbotConfig = {
|
|
768
|
+
channels: {
|
|
769
|
+
feishu: {
|
|
770
|
+
dmPolicy: "open",
|
|
771
|
+
},
|
|
772
|
+
},
|
|
773
|
+
} as ClawdbotConfig;
|
|
774
|
+
|
|
775
|
+
const event: FeishuMessageEvent = {
|
|
776
|
+
sender: {
|
|
777
|
+
sender_id: {
|
|
778
|
+
open_id: "ou-sender",
|
|
779
|
+
},
|
|
780
|
+
},
|
|
781
|
+
message: {
|
|
782
|
+
message_id: "msg-post-media",
|
|
783
|
+
chat_id: "oc-dm",
|
|
784
|
+
chat_type: "p2p",
|
|
785
|
+
message_type: "post",
|
|
786
|
+
content: JSON.stringify({
|
|
787
|
+
title: "Rich text",
|
|
788
|
+
content: [
|
|
789
|
+
[
|
|
790
|
+
{
|
|
791
|
+
tag: "media",
|
|
792
|
+
file_key: "file_post_media_payload",
|
|
793
|
+
file_name: "embedded.mov",
|
|
794
|
+
},
|
|
795
|
+
],
|
|
796
|
+
],
|
|
797
|
+
}),
|
|
798
|
+
},
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
await dispatchMessage({ cfg, event });
|
|
802
|
+
|
|
803
|
+
expect(mockDownloadMessageResourceFeishu).toHaveBeenCalledWith(
|
|
804
|
+
expect.objectContaining({
|
|
805
|
+
messageId: "msg-post-media",
|
|
806
|
+
fileKey: "file_post_media_payload",
|
|
807
|
+
type: "file",
|
|
808
|
+
}),
|
|
809
|
+
);
|
|
810
|
+
expect(mockSaveMediaBuffer).toHaveBeenCalledWith(
|
|
811
|
+
expect.any(Buffer),
|
|
812
|
+
"video/mp4",
|
|
813
|
+
"inbound",
|
|
814
|
+
expect.any(Number),
|
|
815
|
+
);
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
it("includes message_id in BodyForAgent on its own line", async () => {
|
|
819
|
+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
820
|
+
|
|
821
|
+
const cfg: ClawdbotConfig = {
|
|
822
|
+
channels: {
|
|
823
|
+
feishu: {
|
|
824
|
+
dmPolicy: "open",
|
|
825
|
+
},
|
|
826
|
+
},
|
|
827
|
+
} as ClawdbotConfig;
|
|
828
|
+
|
|
829
|
+
const event: FeishuMessageEvent = {
|
|
830
|
+
sender: {
|
|
831
|
+
sender_id: {
|
|
832
|
+
open_id: "ou-msgid",
|
|
833
|
+
},
|
|
834
|
+
},
|
|
835
|
+
message: {
|
|
836
|
+
message_id: "msg-message-id-line",
|
|
837
|
+
chat_id: "oc-dm",
|
|
838
|
+
chat_type: "p2p",
|
|
839
|
+
message_type: "text",
|
|
840
|
+
content: JSON.stringify({ text: "hello" }),
|
|
841
|
+
},
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
await dispatchMessage({ cfg, event });
|
|
845
|
+
|
|
846
|
+
expect(mockFinalizeInboundContext).toHaveBeenCalledWith(
|
|
847
|
+
expect.objectContaining({
|
|
848
|
+
BodyForAgent: "[message_id: msg-message-id-line]\nou-msgid: hello",
|
|
849
|
+
}),
|
|
850
|
+
);
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it("expands merge_forward content from API sub-messages", async () => {
|
|
854
|
+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
855
|
+
const mockGetMerged = vi.fn().mockResolvedValue({
|
|
856
|
+
code: 0,
|
|
857
|
+
data: {
|
|
858
|
+
items: [
|
|
859
|
+
{
|
|
860
|
+
message_id: "container",
|
|
861
|
+
msg_type: "merge_forward",
|
|
862
|
+
body: { content: JSON.stringify({ text: "Merged and Forwarded Message" }) },
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
message_id: "sub-2",
|
|
866
|
+
upper_message_id: "container",
|
|
867
|
+
msg_type: "file",
|
|
868
|
+
body: { content: JSON.stringify({ file_name: "report.pdf" }) },
|
|
869
|
+
create_time: "2000",
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
message_id: "sub-1",
|
|
873
|
+
upper_message_id: "container",
|
|
874
|
+
msg_type: "text",
|
|
875
|
+
body: { content: JSON.stringify({ text: "alpha" }) },
|
|
876
|
+
create_time: "1000",
|
|
877
|
+
},
|
|
878
|
+
],
|
|
879
|
+
},
|
|
880
|
+
});
|
|
881
|
+
mockCreateFeishuClient.mockReturnValue({
|
|
882
|
+
contact: {
|
|
883
|
+
user: {
|
|
884
|
+
get: vi.fn().mockResolvedValue({ data: { user: { name: "Sender" } } }),
|
|
885
|
+
},
|
|
886
|
+
},
|
|
887
|
+
im: {
|
|
888
|
+
message: {
|
|
889
|
+
get: mockGetMerged,
|
|
890
|
+
},
|
|
891
|
+
},
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
const cfg: ClawdbotConfig = {
|
|
895
|
+
channels: {
|
|
896
|
+
feishu: {
|
|
897
|
+
dmPolicy: "open",
|
|
898
|
+
},
|
|
899
|
+
},
|
|
900
|
+
} as ClawdbotConfig;
|
|
901
|
+
|
|
902
|
+
const event: FeishuMessageEvent = {
|
|
903
|
+
sender: {
|
|
904
|
+
sender_id: {
|
|
905
|
+
open_id: "ou-merge",
|
|
906
|
+
},
|
|
907
|
+
},
|
|
908
|
+
message: {
|
|
909
|
+
message_id: "msg-merge-forward",
|
|
910
|
+
chat_id: "oc-dm",
|
|
911
|
+
chat_type: "p2p",
|
|
912
|
+
message_type: "merge_forward",
|
|
913
|
+
content: JSON.stringify({ text: "Merged and Forwarded Message" }),
|
|
914
|
+
},
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
await dispatchMessage({ cfg, event });
|
|
918
|
+
|
|
919
|
+
expect(mockGetMerged).toHaveBeenCalledWith({
|
|
920
|
+
path: { message_id: "msg-merge-forward" },
|
|
921
|
+
});
|
|
922
|
+
expect(mockFinalizeInboundContext).toHaveBeenCalledWith(
|
|
923
|
+
expect.objectContaining({
|
|
924
|
+
BodyForAgent: expect.stringContaining(
|
|
925
|
+
"[Merged and Forwarded Messages]\n- alpha\n- [File: report.pdf]",
|
|
926
|
+
),
|
|
927
|
+
}),
|
|
928
|
+
);
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
it("falls back when merge_forward API returns no sub-messages", async () => {
|
|
932
|
+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
933
|
+
mockCreateFeishuClient.mockReturnValue({
|
|
934
|
+
contact: {
|
|
935
|
+
user: {
|
|
936
|
+
get: vi.fn().mockResolvedValue({ data: { user: { name: "Sender" } } }),
|
|
937
|
+
},
|
|
938
|
+
},
|
|
939
|
+
im: {
|
|
940
|
+
message: {
|
|
941
|
+
get: vi.fn().mockResolvedValue({ code: 0, data: { items: [] } }),
|
|
942
|
+
},
|
|
943
|
+
},
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
const cfg: ClawdbotConfig = {
|
|
947
|
+
channels: {
|
|
948
|
+
feishu: {
|
|
949
|
+
dmPolicy: "open",
|
|
950
|
+
},
|
|
951
|
+
},
|
|
952
|
+
} as ClawdbotConfig;
|
|
953
|
+
|
|
954
|
+
const event: FeishuMessageEvent = {
|
|
955
|
+
sender: {
|
|
956
|
+
sender_id: {
|
|
957
|
+
open_id: "ou-merge-empty",
|
|
958
|
+
},
|
|
959
|
+
},
|
|
960
|
+
message: {
|
|
961
|
+
message_id: "msg-merge-empty",
|
|
962
|
+
chat_id: "oc-dm",
|
|
963
|
+
chat_type: "p2p",
|
|
964
|
+
message_type: "merge_forward",
|
|
965
|
+
content: JSON.stringify({ text: "Merged and Forwarded Message" }),
|
|
966
|
+
},
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
await dispatchMessage({ cfg, event });
|
|
970
|
+
|
|
971
|
+
expect(mockFinalizeInboundContext).toHaveBeenCalledWith(
|
|
972
|
+
expect.objectContaining({
|
|
973
|
+
BodyForAgent: expect.stringContaining("[Merged and Forwarded Message - could not fetch]"),
|
|
974
|
+
}),
|
|
975
|
+
);
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
it("dispatches once and appends permission notice to the main agent body", async () => {
|
|
979
|
+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
980
|
+
mockCreateFeishuClient.mockReturnValue({
|
|
981
|
+
contact: {
|
|
982
|
+
user: {
|
|
983
|
+
get: vi.fn().mockRejectedValue({
|
|
984
|
+
response: {
|
|
985
|
+
data: {
|
|
986
|
+
code: 99991672,
|
|
987
|
+
msg: "permission denied https://open.feishu.cn/app/cli_test",
|
|
988
|
+
},
|
|
989
|
+
},
|
|
990
|
+
}),
|
|
991
|
+
},
|
|
992
|
+
},
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
const cfg: ClawdbotConfig = {
|
|
996
|
+
channels: {
|
|
997
|
+
feishu: {
|
|
998
|
+
appId: "cli_test",
|
|
999
|
+
appSecret: "sec_test",
|
|
1000
|
+
groups: {
|
|
1001
|
+
"oc-group": {
|
|
1002
|
+
requireMention: false,
|
|
1003
|
+
},
|
|
1004
|
+
},
|
|
1005
|
+
},
|
|
1006
|
+
},
|
|
1007
|
+
} as ClawdbotConfig;
|
|
1008
|
+
|
|
1009
|
+
const event: FeishuMessageEvent = {
|
|
1010
|
+
sender: {
|
|
1011
|
+
sender_id: {
|
|
1012
|
+
open_id: "ou-perm",
|
|
1013
|
+
},
|
|
1014
|
+
},
|
|
1015
|
+
message: {
|
|
1016
|
+
message_id: "msg-perm-1",
|
|
1017
|
+
chat_id: "oc-group",
|
|
1018
|
+
chat_type: "group",
|
|
1019
|
+
message_type: "text",
|
|
1020
|
+
content: JSON.stringify({ text: "hello group" }),
|
|
1021
|
+
},
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
await dispatchMessage({ cfg, event });
|
|
1025
|
+
|
|
1026
|
+
expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1);
|
|
1027
|
+
expect(mockFinalizeInboundContext).toHaveBeenCalledWith(
|
|
1028
|
+
expect.objectContaining({
|
|
1029
|
+
BodyForAgent: expect.stringContaining(
|
|
1030
|
+
"Permission grant URL: https://open.feishu.cn/app/cli_test",
|
|
1031
|
+
),
|
|
1032
|
+
}),
|
|
1033
|
+
);
|
|
1034
|
+
expect(mockFinalizeInboundContext).toHaveBeenCalledWith(
|
|
1035
|
+
expect.objectContaining({
|
|
1036
|
+
BodyForAgent: expect.stringContaining("ou-perm: hello group"),
|
|
1037
|
+
}),
|
|
1038
|
+
);
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
it("routes group sessions by sender when groupSessionScope=group_sender", async () => {
|
|
1042
|
+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
1043
|
+
|
|
1044
|
+
const cfg: ClawdbotConfig = {
|
|
1045
|
+
channels: {
|
|
1046
|
+
feishu: {
|
|
1047
|
+
groups: {
|
|
1048
|
+
"oc-group": {
|
|
1049
|
+
requireMention: false,
|
|
1050
|
+
groupSessionScope: "group_sender",
|
|
1051
|
+
},
|
|
1052
|
+
},
|
|
1053
|
+
},
|
|
1054
|
+
},
|
|
1055
|
+
} as ClawdbotConfig;
|
|
1056
|
+
|
|
1057
|
+
const event: FeishuMessageEvent = {
|
|
1058
|
+
sender: { sender_id: { open_id: "ou-scope-user" } },
|
|
1059
|
+
message: {
|
|
1060
|
+
message_id: "msg-scope-group-sender",
|
|
1061
|
+
chat_id: "oc-group",
|
|
1062
|
+
chat_type: "group",
|
|
1063
|
+
message_type: "text",
|
|
1064
|
+
content: JSON.stringify({ text: "group sender scope" }),
|
|
1065
|
+
},
|
|
1066
|
+
};
|
|
1067
|
+
|
|
1068
|
+
await dispatchMessage({ cfg, event });
|
|
1069
|
+
|
|
1070
|
+
expect(mockResolveAgentRoute).toHaveBeenCalledWith(
|
|
1071
|
+
expect.objectContaining({
|
|
1072
|
+
peer: { kind: "group", id: "oc-group:sender:ou-scope-user" },
|
|
1073
|
+
parentPeer: null,
|
|
1074
|
+
}),
|
|
1075
|
+
);
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
it("routes topic sessions and parentPeer when groupSessionScope=group_topic_sender", async () => {
|
|
1079
|
+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
1080
|
+
|
|
1081
|
+
const cfg: ClawdbotConfig = {
|
|
1082
|
+
channels: {
|
|
1083
|
+
feishu: {
|
|
1084
|
+
groups: {
|
|
1085
|
+
"oc-group": {
|
|
1086
|
+
requireMention: false,
|
|
1087
|
+
groupSessionScope: "group_topic_sender",
|
|
1088
|
+
},
|
|
1089
|
+
},
|
|
1090
|
+
},
|
|
1091
|
+
},
|
|
1092
|
+
} as ClawdbotConfig;
|
|
1093
|
+
|
|
1094
|
+
const event: FeishuMessageEvent = {
|
|
1095
|
+
sender: { sender_id: { open_id: "ou-topic-user" } },
|
|
1096
|
+
message: {
|
|
1097
|
+
message_id: "msg-scope-topic-sender",
|
|
1098
|
+
chat_id: "oc-group",
|
|
1099
|
+
chat_type: "group",
|
|
1100
|
+
root_id: "om_root_topic",
|
|
1101
|
+
message_type: "text",
|
|
1102
|
+
content: JSON.stringify({ text: "topic sender scope" }),
|
|
1103
|
+
},
|
|
1104
|
+
};
|
|
1105
|
+
|
|
1106
|
+
await dispatchMessage({ cfg, event });
|
|
1107
|
+
|
|
1108
|
+
expect(mockResolveAgentRoute).toHaveBeenCalledWith(
|
|
1109
|
+
expect.objectContaining({
|
|
1110
|
+
peer: { kind: "group", id: "oc-group:topic:om_root_topic:sender:ou-topic-user" },
|
|
1111
|
+
parentPeer: { kind: "group", id: "oc-group" },
|
|
1112
|
+
}),
|
|
1113
|
+
);
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
it("maps legacy topicSessionMode=enabled to group_topic routing", async () => {
|
|
1117
|
+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
1118
|
+
|
|
1119
|
+
const cfg: ClawdbotConfig = {
|
|
1120
|
+
channels: {
|
|
1121
|
+
feishu: {
|
|
1122
|
+
topicSessionMode: "enabled",
|
|
1123
|
+
groups: {
|
|
1124
|
+
"oc-group": {
|
|
1125
|
+
requireMention: false,
|
|
1126
|
+
},
|
|
1127
|
+
},
|
|
1128
|
+
},
|
|
1129
|
+
},
|
|
1130
|
+
} as ClawdbotConfig;
|
|
1131
|
+
|
|
1132
|
+
const event: FeishuMessageEvent = {
|
|
1133
|
+
sender: { sender_id: { open_id: "ou-legacy" } },
|
|
1134
|
+
message: {
|
|
1135
|
+
message_id: "msg-legacy-topic-mode",
|
|
1136
|
+
chat_id: "oc-group",
|
|
1137
|
+
chat_type: "group",
|
|
1138
|
+
root_id: "om_root_legacy",
|
|
1139
|
+
message_type: "text",
|
|
1140
|
+
content: JSON.stringify({ text: "legacy topic mode" }),
|
|
1141
|
+
},
|
|
1142
|
+
};
|
|
1143
|
+
|
|
1144
|
+
await dispatchMessage({ cfg, event });
|
|
1145
|
+
|
|
1146
|
+
expect(mockResolveAgentRoute).toHaveBeenCalledWith(
|
|
1147
|
+
expect.objectContaining({
|
|
1148
|
+
peer: { kind: "group", id: "oc-group:topic:om_root_legacy" },
|
|
1149
|
+
parentPeer: { kind: "group", id: "oc-group" },
|
|
1150
|
+
}),
|
|
1151
|
+
);
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
it("uses message_id as topic root when group_topic + replyInThread and no root_id", async () => {
|
|
1155
|
+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
1156
|
+
|
|
1157
|
+
const cfg: ClawdbotConfig = {
|
|
1158
|
+
channels: {
|
|
1159
|
+
feishu: {
|
|
1160
|
+
groups: {
|
|
1161
|
+
"oc-group": {
|
|
1162
|
+
requireMention: false,
|
|
1163
|
+
groupSessionScope: "group_topic",
|
|
1164
|
+
replyInThread: "enabled",
|
|
1165
|
+
},
|
|
1166
|
+
},
|
|
1167
|
+
},
|
|
1168
|
+
},
|
|
1169
|
+
} as ClawdbotConfig;
|
|
1170
|
+
|
|
1171
|
+
const event: FeishuMessageEvent = {
|
|
1172
|
+
sender: { sender_id: { open_id: "ou-topic-init" } },
|
|
1173
|
+
message: {
|
|
1174
|
+
message_id: "msg-new-topic-root",
|
|
1175
|
+
chat_id: "oc-group",
|
|
1176
|
+
chat_type: "group",
|
|
1177
|
+
message_type: "text",
|
|
1178
|
+
content: JSON.stringify({ text: "create topic" }),
|
|
1179
|
+
},
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1182
|
+
await dispatchMessage({ cfg, event });
|
|
1183
|
+
|
|
1184
|
+
expect(mockResolveAgentRoute).toHaveBeenCalledWith(
|
|
1185
|
+
expect.objectContaining({
|
|
1186
|
+
peer: { kind: "group", id: "oc-group:topic:msg-new-topic-root" },
|
|
1187
|
+
parentPeer: { kind: "group", id: "oc-group" },
|
|
1188
|
+
}),
|
|
1189
|
+
);
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
it("does not dispatch twice for the same image message_id (concurrent dedupe)", async () => {
|
|
1193
|
+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
1194
|
+
|
|
1195
|
+
const cfg: ClawdbotConfig = {
|
|
1196
|
+
channels: {
|
|
1197
|
+
feishu: {
|
|
1198
|
+
dmPolicy: "open",
|
|
1199
|
+
},
|
|
1200
|
+
},
|
|
1201
|
+
} as ClawdbotConfig;
|
|
1202
|
+
|
|
1203
|
+
const event: FeishuMessageEvent = {
|
|
1204
|
+
sender: {
|
|
1205
|
+
sender_id: {
|
|
1206
|
+
open_id: "ou-image-dedup",
|
|
1207
|
+
},
|
|
1208
|
+
},
|
|
1209
|
+
message: {
|
|
1210
|
+
message_id: "msg-image-dedup",
|
|
1211
|
+
chat_id: "oc-dm",
|
|
1212
|
+
chat_type: "p2p",
|
|
1213
|
+
message_type: "image",
|
|
1214
|
+
content: JSON.stringify({
|
|
1215
|
+
image_key: "img_dedup_payload",
|
|
1216
|
+
}),
|
|
1217
|
+
},
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1220
|
+
await Promise.all([dispatchMessage({ cfg, event }), dispatchMessage({ cfg, event })]);
|
|
1221
|
+
expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1);
|
|
1222
|
+
});
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
describe("toMessageResourceType", () => {
|
|
1226
|
+
it("maps image to image", () => {
|
|
1227
|
+
expect(toMessageResourceType("image")).toBe("image");
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
it("maps audio to file", () => {
|
|
1231
|
+
expect(toMessageResourceType("audio")).toBe("file");
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
it("maps video/file/sticker to file", () => {
|
|
1235
|
+
expect(toMessageResourceType("video")).toBe("file");
|
|
1236
|
+
expect(toMessageResourceType("file")).toBe("file");
|
|
1237
|
+
expect(toMessageResourceType("sticker")).toBe("file");
|
|
1238
|
+
});
|
|
385
1239
|
});
|