@openclaw/feishu 2026.3.1 → 2026.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.
- package/package.json +1 -1
- package/src/accounts.test.ts +74 -3
- package/src/accounts.ts +69 -10
- package/src/bot.checkBotMentioned.test.ts +1 -1
- package/src/bot.test.ts +390 -29
- package/src/bot.ts +131 -61
- package/src/channel.ts +20 -4
- package/src/client.test.ts +14 -0
- package/src/config-schema.test.ts +19 -0
- package/src/config-schema.ts +13 -9
- package/src/dedup.ts +47 -1
- package/src/doc-schema.ts +16 -22
- package/src/docx.account-selection.test.ts +7 -13
- package/src/docx.test.ts +41 -189
- package/src/media.test.ts +104 -1
- package/src/media.ts +21 -1
- package/src/mention.ts +1 -1
- package/src/monitor.account.ts +266 -18
- package/src/monitor.reaction.test.ts +345 -2
- package/src/monitor.startup.test.ts +17 -1
- package/src/monitor.state.defaults.test.ts +46 -0
- package/src/monitor.state.ts +84 -8
- package/src/monitor.test-mocks.ts +12 -0
- package/src/monitor.webhook-security.test.ts +26 -9
- package/src/onboarding.status.test.ts +25 -0
- package/src/onboarding.ts +144 -52
- package/src/probe.test.ts +38 -20
- package/src/probe.ts +57 -37
- package/src/reply-dispatcher.test.ts +41 -0
- package/src/reply-dispatcher.ts +26 -7
- package/src/secret-input.ts +19 -0
- package/src/send-target.test.ts +74 -0
- package/src/send-target.ts +6 -2
- package/src/targets.test.ts +29 -0
- package/src/targets.ts +21 -1
- package/src/types.ts +9 -1
package/src/bot.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { createPluginRuntimeMock } from "../../test-utils/plugin-runtime-mock.js";
|
|
3
4
|
import type { FeishuMessageEvent } from "./bot.js";
|
|
4
5
|
import { buildFeishuAgentBody, handleFeishuMessage, toMessageResourceType } from "./bot.js";
|
|
5
6
|
import { setFeishuRuntime } from "./runtime.js";
|
|
@@ -27,8 +28,10 @@ const {
|
|
|
27
28
|
mockCreateFeishuClient: vi.fn(),
|
|
28
29
|
mockResolveAgentRoute: vi.fn(() => ({
|
|
29
30
|
agentId: "main",
|
|
31
|
+
channel: "feishu",
|
|
30
32
|
accountId: "default",
|
|
31
33
|
sessionKey: "agent:main:feishu:dm:ou-attacker",
|
|
34
|
+
mainSessionKey: "agent:main:main",
|
|
32
35
|
matchedBy: "default",
|
|
33
36
|
})),
|
|
34
37
|
}));
|
|
@@ -122,7 +125,9 @@ describe("handleFeishuMessage command authorization", () => {
|
|
|
122
125
|
const mockBuildPairingReply = vi.fn(() => "Pairing response");
|
|
123
126
|
const mockEnqueueSystemEvent = vi.fn();
|
|
124
127
|
const mockSaveMediaBuffer = vi.fn().mockResolvedValue({
|
|
128
|
+
id: "inbound-clip.mp4",
|
|
125
129
|
path: "/tmp/inbound-clip.mp4",
|
|
130
|
+
size: Buffer.byteLength("video"),
|
|
126
131
|
contentType: "video/mp4",
|
|
127
132
|
});
|
|
128
133
|
|
|
@@ -131,8 +136,10 @@ describe("handleFeishuMessage command authorization", () => {
|
|
|
131
136
|
mockShouldComputeCommandAuthorized.mockReset().mockReturnValue(true);
|
|
132
137
|
mockResolveAgentRoute.mockReturnValue({
|
|
133
138
|
agentId: "main",
|
|
139
|
+
channel: "feishu",
|
|
134
140
|
accountId: "default",
|
|
135
141
|
sessionKey: "agent:main:feishu:dm:ou-attacker",
|
|
142
|
+
mainSessionKey: "agent:main:main",
|
|
136
143
|
matchedBy: "default",
|
|
137
144
|
});
|
|
138
145
|
mockCreateFeishuClient.mockReturnValue({
|
|
@@ -143,38 +150,46 @@ describe("handleFeishuMessage command authorization", () => {
|
|
|
143
150
|
},
|
|
144
151
|
});
|
|
145
152
|
mockEnqueueSystemEvent.mockReset();
|
|
146
|
-
setFeishuRuntime(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
channel: {
|
|
151
|
-
routing: {
|
|
152
|
-
resolveAgentRoute: mockResolveAgentRoute,
|
|
153
|
-
},
|
|
154
|
-
reply: {
|
|
155
|
-
resolveEnvelopeFormatOptions: vi.fn(() => ({ template: "channel+name+time" })),
|
|
156
|
-
formatAgentEnvelope: vi.fn((params: { body: string }) => params.body),
|
|
157
|
-
finalizeInboundContext: mockFinalizeInboundContext,
|
|
158
|
-
dispatchReplyFromConfig: mockDispatchReplyFromConfig,
|
|
159
|
-
withReplyDispatcher: mockWithReplyDispatcher,
|
|
153
|
+
setFeishuRuntime(
|
|
154
|
+
createPluginRuntimeMock({
|
|
155
|
+
system: {
|
|
156
|
+
enqueueSystemEvent: mockEnqueueSystemEvent,
|
|
160
157
|
},
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
158
|
+
channel: {
|
|
159
|
+
routing: {
|
|
160
|
+
resolveAgentRoute:
|
|
161
|
+
mockResolveAgentRoute as unknown as PluginRuntime["channel"]["routing"]["resolveAgentRoute"],
|
|
162
|
+
},
|
|
163
|
+
reply: {
|
|
164
|
+
resolveEnvelopeFormatOptions: vi.fn(
|
|
165
|
+
() => ({}),
|
|
166
|
+
) as unknown as PluginRuntime["channel"]["reply"]["resolveEnvelopeFormatOptions"],
|
|
167
|
+
formatAgentEnvelope: vi.fn((params: { body: string }) => params.body),
|
|
168
|
+
finalizeInboundContext:
|
|
169
|
+
mockFinalizeInboundContext as unknown as PluginRuntime["channel"]["reply"]["finalizeInboundContext"],
|
|
170
|
+
dispatchReplyFromConfig: mockDispatchReplyFromConfig,
|
|
171
|
+
withReplyDispatcher:
|
|
172
|
+
mockWithReplyDispatcher as unknown as PluginRuntime["channel"]["reply"]["withReplyDispatcher"],
|
|
173
|
+
},
|
|
174
|
+
commands: {
|
|
175
|
+
shouldComputeCommandAuthorized: mockShouldComputeCommandAuthorized,
|
|
176
|
+
resolveCommandAuthorizedFromAuthorizers: mockResolveCommandAuthorizedFromAuthorizers,
|
|
177
|
+
},
|
|
178
|
+
media: {
|
|
179
|
+
saveMediaBuffer:
|
|
180
|
+
mockSaveMediaBuffer as unknown as PluginRuntime["channel"]["media"]["saveMediaBuffer"],
|
|
181
|
+
},
|
|
182
|
+
pairing: {
|
|
183
|
+
readAllowFromStore: mockReadAllowFromStore,
|
|
184
|
+
upsertPairingRequest: mockUpsertPairingRequest,
|
|
185
|
+
buildPairingReply: mockBuildPairingReply,
|
|
186
|
+
},
|
|
164
187
|
},
|
|
165
188
|
media: {
|
|
166
|
-
|
|
189
|
+
detectMime: vi.fn(async () => "application/octet-stream"),
|
|
167
190
|
},
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
upsertPairingRequest: mockUpsertPairingRequest,
|
|
171
|
-
buildPairingReply: mockBuildPairingReply,
|
|
172
|
-
},
|
|
173
|
-
},
|
|
174
|
-
media: {
|
|
175
|
-
detectMime: vi.fn(async () => "application/octet-stream"),
|
|
176
|
-
},
|
|
177
|
-
} as unknown as PluginRuntime);
|
|
191
|
+
}),
|
|
192
|
+
);
|
|
178
193
|
});
|
|
179
194
|
|
|
180
195
|
it("does not enqueue inbound preview text as system events", async () => {
|
|
@@ -366,6 +381,41 @@ describe("handleFeishuMessage command authorization", () => {
|
|
|
366
381
|
);
|
|
367
382
|
});
|
|
368
383
|
|
|
384
|
+
it("replies pairing challenge to DM chat_id instead of user:sender id", async () => {
|
|
385
|
+
const cfg: ClawdbotConfig = {
|
|
386
|
+
channels: {
|
|
387
|
+
feishu: {
|
|
388
|
+
dmPolicy: "pairing",
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
} as ClawdbotConfig;
|
|
392
|
+
|
|
393
|
+
const event: FeishuMessageEvent = {
|
|
394
|
+
sender: {
|
|
395
|
+
sender_id: {
|
|
396
|
+
user_id: "u_mobile_only",
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
message: {
|
|
400
|
+
message_id: "msg-pairing-chat-reply",
|
|
401
|
+
chat_id: "oc_dm_chat_1",
|
|
402
|
+
chat_type: "p2p",
|
|
403
|
+
message_type: "text",
|
|
404
|
+
content: JSON.stringify({ text: "hello" }),
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
mockReadAllowFromStore.mockResolvedValue([]);
|
|
409
|
+
mockUpsertPairingRequest.mockResolvedValue({ code: "ABCDEFGH", created: true });
|
|
410
|
+
|
|
411
|
+
await dispatchMessage({ cfg, event });
|
|
412
|
+
|
|
413
|
+
expect(mockSendMessageFeishu).toHaveBeenCalledWith(
|
|
414
|
+
expect.objectContaining({
|
|
415
|
+
to: "chat:oc_dm_chat_1",
|
|
416
|
+
}),
|
|
417
|
+
);
|
|
418
|
+
});
|
|
369
419
|
it("creates pairing request and drops unauthorized DMs in pairing mode", async () => {
|
|
370
420
|
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
371
421
|
mockReadAllowFromStore.mockResolvedValue([]);
|
|
@@ -410,7 +460,7 @@ describe("handleFeishuMessage command authorization", () => {
|
|
|
410
460
|
});
|
|
411
461
|
expect(mockSendMessageFeishu).toHaveBeenCalledWith(
|
|
412
462
|
expect.objectContaining({
|
|
413
|
-
to: "
|
|
463
|
+
to: "chat:oc-dm",
|
|
414
464
|
accountId: "default",
|
|
415
465
|
}),
|
|
416
466
|
);
|
|
@@ -1038,6 +1088,67 @@ describe("handleFeishuMessage command authorization", () => {
|
|
|
1038
1088
|
);
|
|
1039
1089
|
});
|
|
1040
1090
|
|
|
1091
|
+
it("ignores stale non-existent contact scope permission errors", async () => {
|
|
1092
|
+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
1093
|
+
mockCreateFeishuClient.mockReturnValue({
|
|
1094
|
+
contact: {
|
|
1095
|
+
user: {
|
|
1096
|
+
get: vi.fn().mockRejectedValue({
|
|
1097
|
+
response: {
|
|
1098
|
+
data: {
|
|
1099
|
+
code: 99991672,
|
|
1100
|
+
msg: "permission denied: contact:contact.base:readonly https://open.feishu.cn/app/cli_scope_bug",
|
|
1101
|
+
},
|
|
1102
|
+
},
|
|
1103
|
+
}),
|
|
1104
|
+
},
|
|
1105
|
+
},
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
const cfg: ClawdbotConfig = {
|
|
1109
|
+
channels: {
|
|
1110
|
+
feishu: {
|
|
1111
|
+
appId: "cli_scope_bug",
|
|
1112
|
+
appSecret: "sec_scope_bug",
|
|
1113
|
+
groups: {
|
|
1114
|
+
"oc-group": {
|
|
1115
|
+
requireMention: false,
|
|
1116
|
+
},
|
|
1117
|
+
},
|
|
1118
|
+
},
|
|
1119
|
+
},
|
|
1120
|
+
} as ClawdbotConfig;
|
|
1121
|
+
|
|
1122
|
+
const event: FeishuMessageEvent = {
|
|
1123
|
+
sender: {
|
|
1124
|
+
sender_id: {
|
|
1125
|
+
open_id: "ou-perm-scope",
|
|
1126
|
+
},
|
|
1127
|
+
},
|
|
1128
|
+
message: {
|
|
1129
|
+
message_id: "msg-perm-scope-1",
|
|
1130
|
+
chat_id: "oc-group",
|
|
1131
|
+
chat_type: "group",
|
|
1132
|
+
message_type: "text",
|
|
1133
|
+
content: JSON.stringify({ text: "hello group" }),
|
|
1134
|
+
},
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
await dispatchMessage({ cfg, event });
|
|
1138
|
+
|
|
1139
|
+
expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1);
|
|
1140
|
+
expect(mockFinalizeInboundContext).toHaveBeenCalledWith(
|
|
1141
|
+
expect.objectContaining({
|
|
1142
|
+
BodyForAgent: expect.not.stringContaining("Permission grant URL"),
|
|
1143
|
+
}),
|
|
1144
|
+
);
|
|
1145
|
+
expect(mockFinalizeInboundContext).toHaveBeenCalledWith(
|
|
1146
|
+
expect.objectContaining({
|
|
1147
|
+
BodyForAgent: expect.stringContaining("ou-perm-scope: hello group"),
|
|
1148
|
+
}),
|
|
1149
|
+
);
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1041
1152
|
it("routes group sessions by sender when groupSessionScope=group_sender", async () => {
|
|
1042
1153
|
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
1043
1154
|
|
|
@@ -1113,6 +1224,83 @@ describe("handleFeishuMessage command authorization", () => {
|
|
|
1113
1224
|
);
|
|
1114
1225
|
});
|
|
1115
1226
|
|
|
1227
|
+
it("keeps root_id as topic key when root_id and thread_id both exist", async () => {
|
|
1228
|
+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
1229
|
+
|
|
1230
|
+
const cfg: ClawdbotConfig = {
|
|
1231
|
+
channels: {
|
|
1232
|
+
feishu: {
|
|
1233
|
+
groups: {
|
|
1234
|
+
"oc-group": {
|
|
1235
|
+
requireMention: false,
|
|
1236
|
+
groupSessionScope: "group_topic_sender",
|
|
1237
|
+
},
|
|
1238
|
+
},
|
|
1239
|
+
},
|
|
1240
|
+
},
|
|
1241
|
+
} as ClawdbotConfig;
|
|
1242
|
+
|
|
1243
|
+
const event: FeishuMessageEvent = {
|
|
1244
|
+
sender: { sender_id: { open_id: "ou-topic-user" } },
|
|
1245
|
+
message: {
|
|
1246
|
+
message_id: "msg-scope-topic-thread-id",
|
|
1247
|
+
chat_id: "oc-group",
|
|
1248
|
+
chat_type: "group",
|
|
1249
|
+
root_id: "om_root_topic",
|
|
1250
|
+
thread_id: "omt_topic_1",
|
|
1251
|
+
message_type: "text",
|
|
1252
|
+
content: JSON.stringify({ text: "topic sender scope" }),
|
|
1253
|
+
},
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
await dispatchMessage({ cfg, event });
|
|
1257
|
+
|
|
1258
|
+
expect(mockResolveAgentRoute).toHaveBeenCalledWith(
|
|
1259
|
+
expect.objectContaining({
|
|
1260
|
+
peer: { kind: "group", id: "oc-group:topic:om_root_topic:sender:ou-topic-user" },
|
|
1261
|
+
parentPeer: { kind: "group", id: "oc-group" },
|
|
1262
|
+
}),
|
|
1263
|
+
);
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
it("uses thread_id as topic key when root_id is missing", async () => {
|
|
1267
|
+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
1268
|
+
|
|
1269
|
+
const cfg: ClawdbotConfig = {
|
|
1270
|
+
channels: {
|
|
1271
|
+
feishu: {
|
|
1272
|
+
groups: {
|
|
1273
|
+
"oc-group": {
|
|
1274
|
+
requireMention: false,
|
|
1275
|
+
groupSessionScope: "group_topic_sender",
|
|
1276
|
+
},
|
|
1277
|
+
},
|
|
1278
|
+
},
|
|
1279
|
+
},
|
|
1280
|
+
} as ClawdbotConfig;
|
|
1281
|
+
|
|
1282
|
+
const event: FeishuMessageEvent = {
|
|
1283
|
+
sender: { sender_id: { open_id: "ou-topic-user" } },
|
|
1284
|
+
message: {
|
|
1285
|
+
message_id: "msg-scope-topic-thread-only",
|
|
1286
|
+
chat_id: "oc-group",
|
|
1287
|
+
chat_type: "group",
|
|
1288
|
+
thread_id: "omt_topic_1",
|
|
1289
|
+
message_type: "text",
|
|
1290
|
+
content: JSON.stringify({ text: "topic sender scope" }),
|
|
1291
|
+
},
|
|
1292
|
+
};
|
|
1293
|
+
|
|
1294
|
+
await dispatchMessage({ cfg, event });
|
|
1295
|
+
|
|
1296
|
+
expect(mockResolveAgentRoute).toHaveBeenCalledWith(
|
|
1297
|
+
expect.objectContaining({
|
|
1298
|
+
peer: { kind: "group", id: "oc-group:topic:omt_topic_1:sender:ou-topic-user" },
|
|
1299
|
+
parentPeer: { kind: "group", id: "oc-group" },
|
|
1300
|
+
}),
|
|
1301
|
+
);
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1116
1304
|
it("maps legacy topicSessionMode=enabled to group_topic routing", async () => {
|
|
1117
1305
|
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
1118
1306
|
|
|
@@ -1151,6 +1339,45 @@ describe("handleFeishuMessage command authorization", () => {
|
|
|
1151
1339
|
);
|
|
1152
1340
|
});
|
|
1153
1341
|
|
|
1342
|
+
it("maps legacy topicSessionMode=enabled to root_id when both root_id and thread_id exist", async () => {
|
|
1343
|
+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
1344
|
+
|
|
1345
|
+
const cfg: ClawdbotConfig = {
|
|
1346
|
+
channels: {
|
|
1347
|
+
feishu: {
|
|
1348
|
+
topicSessionMode: "enabled",
|
|
1349
|
+
groups: {
|
|
1350
|
+
"oc-group": {
|
|
1351
|
+
requireMention: false,
|
|
1352
|
+
},
|
|
1353
|
+
},
|
|
1354
|
+
},
|
|
1355
|
+
},
|
|
1356
|
+
} as ClawdbotConfig;
|
|
1357
|
+
|
|
1358
|
+
const event: FeishuMessageEvent = {
|
|
1359
|
+
sender: { sender_id: { open_id: "ou-legacy-thread-id" } },
|
|
1360
|
+
message: {
|
|
1361
|
+
message_id: "msg-legacy-topic-thread-id",
|
|
1362
|
+
chat_id: "oc-group",
|
|
1363
|
+
chat_type: "group",
|
|
1364
|
+
root_id: "om_root_legacy",
|
|
1365
|
+
thread_id: "omt_topic_legacy",
|
|
1366
|
+
message_type: "text",
|
|
1367
|
+
content: JSON.stringify({ text: "legacy topic mode" }),
|
|
1368
|
+
},
|
|
1369
|
+
};
|
|
1370
|
+
|
|
1371
|
+
await dispatchMessage({ cfg, event });
|
|
1372
|
+
|
|
1373
|
+
expect(mockResolveAgentRoute).toHaveBeenCalledWith(
|
|
1374
|
+
expect.objectContaining({
|
|
1375
|
+
peer: { kind: "group", id: "oc-group:topic:om_root_legacy" },
|
|
1376
|
+
parentPeer: { kind: "group", id: "oc-group" },
|
|
1377
|
+
}),
|
|
1378
|
+
);
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1154
1381
|
it("uses message_id as topic root when group_topic + replyInThread and no root_id", async () => {
|
|
1155
1382
|
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
1156
1383
|
|
|
@@ -1189,6 +1416,140 @@ describe("handleFeishuMessage command authorization", () => {
|
|
|
1189
1416
|
);
|
|
1190
1417
|
});
|
|
1191
1418
|
|
|
1419
|
+
it("keeps topic session key stable after first turn creates a thread", async () => {
|
|
1420
|
+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
1421
|
+
|
|
1422
|
+
const cfg: ClawdbotConfig = {
|
|
1423
|
+
channels: {
|
|
1424
|
+
feishu: {
|
|
1425
|
+
groups: {
|
|
1426
|
+
"oc-group": {
|
|
1427
|
+
requireMention: false,
|
|
1428
|
+
groupSessionScope: "group_topic",
|
|
1429
|
+
replyInThread: "enabled",
|
|
1430
|
+
},
|
|
1431
|
+
},
|
|
1432
|
+
},
|
|
1433
|
+
},
|
|
1434
|
+
} as ClawdbotConfig;
|
|
1435
|
+
|
|
1436
|
+
const firstTurn: FeishuMessageEvent = {
|
|
1437
|
+
sender: { sender_id: { open_id: "ou-topic-init" } },
|
|
1438
|
+
message: {
|
|
1439
|
+
message_id: "msg-topic-first",
|
|
1440
|
+
chat_id: "oc-group",
|
|
1441
|
+
chat_type: "group",
|
|
1442
|
+
message_type: "text",
|
|
1443
|
+
content: JSON.stringify({ text: "create topic" }),
|
|
1444
|
+
},
|
|
1445
|
+
};
|
|
1446
|
+
const secondTurn: FeishuMessageEvent = {
|
|
1447
|
+
sender: { sender_id: { open_id: "ou-topic-init" } },
|
|
1448
|
+
message: {
|
|
1449
|
+
message_id: "msg-topic-second",
|
|
1450
|
+
chat_id: "oc-group",
|
|
1451
|
+
chat_type: "group",
|
|
1452
|
+
root_id: "msg-topic-first",
|
|
1453
|
+
thread_id: "omt_topic_created",
|
|
1454
|
+
message_type: "text",
|
|
1455
|
+
content: JSON.stringify({ text: "follow up in same topic" }),
|
|
1456
|
+
},
|
|
1457
|
+
};
|
|
1458
|
+
|
|
1459
|
+
await dispatchMessage({ cfg, event: firstTurn });
|
|
1460
|
+
await dispatchMessage({ cfg, event: secondTurn });
|
|
1461
|
+
|
|
1462
|
+
expect(mockResolveAgentRoute).toHaveBeenNthCalledWith(
|
|
1463
|
+
1,
|
|
1464
|
+
expect.objectContaining({
|
|
1465
|
+
peer: { kind: "group", id: "oc-group:topic:msg-topic-first" },
|
|
1466
|
+
}),
|
|
1467
|
+
);
|
|
1468
|
+
expect(mockResolveAgentRoute).toHaveBeenNthCalledWith(
|
|
1469
|
+
2,
|
|
1470
|
+
expect.objectContaining({
|
|
1471
|
+
peer: { kind: "group", id: "oc-group:topic:msg-topic-first" },
|
|
1472
|
+
}),
|
|
1473
|
+
);
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
it("replies to the topic root when handling a message inside an existing topic", async () => {
|
|
1477
|
+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
1478
|
+
|
|
1479
|
+
const cfg: ClawdbotConfig = {
|
|
1480
|
+
channels: {
|
|
1481
|
+
feishu: {
|
|
1482
|
+
groups: {
|
|
1483
|
+
"oc-group": {
|
|
1484
|
+
requireMention: false,
|
|
1485
|
+
replyInThread: "enabled",
|
|
1486
|
+
},
|
|
1487
|
+
},
|
|
1488
|
+
},
|
|
1489
|
+
},
|
|
1490
|
+
} as ClawdbotConfig;
|
|
1491
|
+
|
|
1492
|
+
const event: FeishuMessageEvent = {
|
|
1493
|
+
sender: { sender_id: { open_id: "ou-topic-user" } },
|
|
1494
|
+
message: {
|
|
1495
|
+
message_id: "om_child_message",
|
|
1496
|
+
root_id: "om_root_topic",
|
|
1497
|
+
chat_id: "oc-group",
|
|
1498
|
+
chat_type: "group",
|
|
1499
|
+
message_type: "text",
|
|
1500
|
+
content: JSON.stringify({ text: "reply inside topic" }),
|
|
1501
|
+
},
|
|
1502
|
+
};
|
|
1503
|
+
|
|
1504
|
+
await dispatchMessage({ cfg, event });
|
|
1505
|
+
|
|
1506
|
+
expect(mockCreateFeishuReplyDispatcher).toHaveBeenCalledWith(
|
|
1507
|
+
expect.objectContaining({
|
|
1508
|
+
replyToMessageId: "om_root_topic",
|
|
1509
|
+
rootId: "om_root_topic",
|
|
1510
|
+
}),
|
|
1511
|
+
);
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
it("forces thread replies when inbound message contains thread_id", async () => {
|
|
1515
|
+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
1516
|
+
|
|
1517
|
+
const cfg: ClawdbotConfig = {
|
|
1518
|
+
channels: {
|
|
1519
|
+
feishu: {
|
|
1520
|
+
groups: {
|
|
1521
|
+
"oc-group": {
|
|
1522
|
+
requireMention: false,
|
|
1523
|
+
groupSessionScope: "group",
|
|
1524
|
+
replyInThread: "disabled",
|
|
1525
|
+
},
|
|
1526
|
+
},
|
|
1527
|
+
},
|
|
1528
|
+
},
|
|
1529
|
+
} as ClawdbotConfig;
|
|
1530
|
+
|
|
1531
|
+
const event: FeishuMessageEvent = {
|
|
1532
|
+
sender: { sender_id: { open_id: "ou-thread-reply" } },
|
|
1533
|
+
message: {
|
|
1534
|
+
message_id: "msg-thread-reply",
|
|
1535
|
+
chat_id: "oc-group",
|
|
1536
|
+
chat_type: "group",
|
|
1537
|
+
thread_id: "omt_topic_thread_reply",
|
|
1538
|
+
message_type: "text",
|
|
1539
|
+
content: JSON.stringify({ text: "thread content" }),
|
|
1540
|
+
},
|
|
1541
|
+
};
|
|
1542
|
+
|
|
1543
|
+
await dispatchMessage({ cfg, event });
|
|
1544
|
+
|
|
1545
|
+
expect(mockCreateFeishuReplyDispatcher).toHaveBeenCalledWith(
|
|
1546
|
+
expect.objectContaining({
|
|
1547
|
+
replyInThread: true,
|
|
1548
|
+
threadReply: true,
|
|
1549
|
+
}),
|
|
1550
|
+
);
|
|
1551
|
+
});
|
|
1552
|
+
|
|
1192
1553
|
it("does not dispatch twice for the same image message_id (concurrent dedupe)", async () => {
|
|
1193
1554
|
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
1194
1555
|
|