@openclaw/feishu 2026.3.11 → 2026.3.13
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 +40 -16
- package/src/accounts.ts +5 -1
- package/src/bot.ts +20 -11
- package/src/channel.ts +2 -2
- package/src/config-schema.test.ts +67 -16
- package/src/config-schema.ts +30 -9
- package/src/dedup.ts +103 -0
- package/src/media.test.ts +38 -61
- package/src/media.ts +64 -76
- package/src/monitor.account.ts +39 -22
- package/src/monitor.reaction.test.ts +134 -65
- package/src/monitor.startup.test.ts +16 -30
- package/src/monitor.transport.ts +104 -6
- package/src/monitor.webhook-e2e.test.ts +214 -0
- package/src/monitor.webhook-security.test.ts +23 -92
- package/src/monitor.webhook.test-helpers.ts +98 -0
- package/src/onboarding.ts +31 -0
- package/src/outbound.test.ts +11 -16
- package/src/probe.test.ts +112 -113
- package/src/reactions.ts +20 -27
- package/src/reply-dispatcher.test.ts +65 -143
- package/src/reply-dispatcher.ts +37 -40
- package/src/send.reply-fallback.test.ts +50 -40
- package/src/send.ts +95 -91
- package/src/types.ts +14 -0
|
@@ -25,44 +25,33 @@ vi.mock("./typing.js", () => ({
|
|
|
25
25
|
addTypingIndicator: addTypingIndicatorMock,
|
|
26
26
|
removeTypingIndicator: removeTypingIndicatorMock,
|
|
27
27
|
}));
|
|
28
|
-
vi.mock("./streaming-card.js", () =>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
this.active = true;
|
|
50
|
-
});
|
|
51
|
-
update = vi.fn(async () => {});
|
|
52
|
-
close = vi.fn(async () => {
|
|
53
|
-
this.active = false;
|
|
54
|
-
});
|
|
55
|
-
isActive = vi.fn(() => this.active);
|
|
56
|
-
|
|
57
|
-
constructor() {
|
|
58
|
-
streamingInstances.push(this);
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
}));
|
|
28
|
+
vi.mock("./streaming-card.js", async () => {
|
|
29
|
+
const actual = await vi.importActual<typeof import("./streaming-card.js")>("./streaming-card.js");
|
|
30
|
+
return {
|
|
31
|
+
mergeStreamingText: actual.mergeStreamingText,
|
|
32
|
+
FeishuStreamingSession: class {
|
|
33
|
+
active = false;
|
|
34
|
+
start = vi.fn(async () => {
|
|
35
|
+
this.active = true;
|
|
36
|
+
});
|
|
37
|
+
update = vi.fn(async () => {});
|
|
38
|
+
close = vi.fn(async () => {
|
|
39
|
+
this.active = false;
|
|
40
|
+
});
|
|
41
|
+
isActive = vi.fn(() => this.active);
|
|
42
|
+
|
|
43
|
+
constructor() {
|
|
44
|
+
streamingInstances.push(this);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
});
|
|
62
49
|
|
|
63
50
|
import { createFeishuReplyDispatcher } from "./reply-dispatcher.js";
|
|
64
51
|
|
|
65
52
|
describe("createFeishuReplyDispatcher streaming behavior", () => {
|
|
53
|
+
type ReplyDispatcherArgs = Parameters<typeof createFeishuReplyDispatcher>[0];
|
|
54
|
+
|
|
66
55
|
beforeEach(() => {
|
|
67
56
|
vi.clearAllMocks();
|
|
68
57
|
streamingInstances.length = 0;
|
|
@@ -128,6 +117,25 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
|
|
|
128
117
|
return createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
|
|
129
118
|
}
|
|
130
119
|
|
|
120
|
+
function createRuntimeLogger() {
|
|
121
|
+
return { log: vi.fn(), error: vi.fn() } as never;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function createDispatcherHarness(overrides: Partial<ReplyDispatcherArgs> = {}) {
|
|
125
|
+
const result = createFeishuReplyDispatcher({
|
|
126
|
+
cfg: {} as never,
|
|
127
|
+
agentId: "agent",
|
|
128
|
+
runtime: {} as never,
|
|
129
|
+
chatId: "oc_chat",
|
|
130
|
+
...overrides,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
result,
|
|
135
|
+
options: createReplyDispatcherWithTypingMock.mock.calls.at(-1)?.[0],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
131
139
|
it("skips typing indicator when account typingIndicator is disabled", async () => {
|
|
132
140
|
resolveFeishuAccountMock.mockReturnValue({
|
|
133
141
|
accountId: "main",
|
|
@@ -209,14 +217,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
|
|
|
209
217
|
});
|
|
210
218
|
|
|
211
219
|
it("keeps auto mode plain text on non-streaming send path", async () => {
|
|
212
|
-
|
|
213
|
-
cfg: {} as never,
|
|
214
|
-
agentId: "agent",
|
|
215
|
-
runtime: {} as never,
|
|
216
|
-
chatId: "oc_chat",
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
|
|
220
|
+
const { options } = createDispatcherHarness();
|
|
220
221
|
await options.deliver({ text: "plain text" }, { kind: "final" });
|
|
221
222
|
|
|
222
223
|
expect(streamingInstances).toHaveLength(0);
|
|
@@ -225,14 +226,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
|
|
|
225
226
|
});
|
|
226
227
|
|
|
227
228
|
it("suppresses internal block payload delivery", async () => {
|
|
228
|
-
|
|
229
|
-
cfg: {} as never,
|
|
230
|
-
agentId: "agent",
|
|
231
|
-
runtime: {} as never,
|
|
232
|
-
chatId: "oc_chat",
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
|
|
229
|
+
const { options } = createDispatcherHarness();
|
|
236
230
|
await options.deliver({ text: "internal reasoning chunk" }, { kind: "block" });
|
|
237
231
|
|
|
238
232
|
expect(streamingInstances).toHaveLength(0);
|
|
@@ -253,15 +247,10 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
|
|
|
253
247
|
});
|
|
254
248
|
|
|
255
249
|
it("uses streaming session for auto mode markdown payloads", async () => {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
agentId: "agent",
|
|
259
|
-
runtime: { log: vi.fn(), error: vi.fn() } as never,
|
|
260
|
-
chatId: "oc_chat",
|
|
250
|
+
const { options } = createDispatcherHarness({
|
|
251
|
+
runtime: createRuntimeLogger(),
|
|
261
252
|
rootId: "om_root_topic",
|
|
262
253
|
});
|
|
263
|
-
|
|
264
|
-
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
|
|
265
254
|
await options.deliver({ text: "```ts\nconst x = 1\n```" }, { kind: "final" });
|
|
266
255
|
|
|
267
256
|
expect(streamingInstances).toHaveLength(1);
|
|
@@ -277,14 +266,9 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
|
|
|
277
266
|
});
|
|
278
267
|
|
|
279
268
|
it("closes streaming with block text when final reply is missing", async () => {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
agentId: "agent",
|
|
283
|
-
runtime: { log: vi.fn(), error: vi.fn() } as never,
|
|
284
|
-
chatId: "oc_chat",
|
|
269
|
+
const { options } = createDispatcherHarness({
|
|
270
|
+
runtime: createRuntimeLogger(),
|
|
285
271
|
});
|
|
286
|
-
|
|
287
|
-
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
|
|
288
272
|
await options.deliver({ text: "```md\npartial answer\n```" }, { kind: "block" });
|
|
289
273
|
await options.onIdle?.();
|
|
290
274
|
|
|
@@ -295,14 +279,9 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
|
|
|
295
279
|
});
|
|
296
280
|
|
|
297
281
|
it("delivers distinct final payloads after streaming close", async () => {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
agentId: "agent",
|
|
301
|
-
runtime: { log: vi.fn(), error: vi.fn() } as never,
|
|
302
|
-
chatId: "oc_chat",
|
|
282
|
+
const { options } = createDispatcherHarness({
|
|
283
|
+
runtime: createRuntimeLogger(),
|
|
303
284
|
});
|
|
304
|
-
|
|
305
|
-
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
|
|
306
285
|
await options.deliver({ text: "```md\n完整回复第一段\n```" }, { kind: "final" });
|
|
307
286
|
await options.deliver({ text: "```md\n完整回复第一段 + 第二段\n```" }, { kind: "final" });
|
|
308
287
|
|
|
@@ -316,14 +295,9 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
|
|
|
316
295
|
});
|
|
317
296
|
|
|
318
297
|
it("skips exact duplicate final text after streaming close", async () => {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
agentId: "agent",
|
|
322
|
-
runtime: { log: vi.fn(), error: vi.fn() } as never,
|
|
323
|
-
chatId: "oc_chat",
|
|
298
|
+
const { options } = createDispatcherHarness({
|
|
299
|
+
runtime: createRuntimeLogger(),
|
|
324
300
|
});
|
|
325
|
-
|
|
326
|
-
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
|
|
327
301
|
await options.deliver({ text: "```md\n同一条回复\n```" }, { kind: "final" });
|
|
328
302
|
await options.deliver({ text: "```md\n同一条回复\n```" }, { kind: "final" });
|
|
329
303
|
|
|
@@ -383,14 +357,9 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
|
|
|
383
357
|
},
|
|
384
358
|
});
|
|
385
359
|
|
|
386
|
-
const result =
|
|
387
|
-
|
|
388
|
-
agentId: "agent",
|
|
389
|
-
runtime: { log: vi.fn(), error: vi.fn() } as never,
|
|
390
|
-
chatId: "oc_chat",
|
|
360
|
+
const { result, options } = createDispatcherHarness({
|
|
361
|
+
runtime: createRuntimeLogger(),
|
|
391
362
|
});
|
|
392
|
-
|
|
393
|
-
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
|
|
394
363
|
await options.onReplyStart?.();
|
|
395
364
|
await result.replyOptions.onPartialReply?.({ text: "hello" });
|
|
396
365
|
await options.deliver({ text: "lo world" }, { kind: "block" });
|
|
@@ -402,14 +371,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
|
|
|
402
371
|
});
|
|
403
372
|
|
|
404
373
|
it("sends media-only payloads as attachments", async () => {
|
|
405
|
-
|
|
406
|
-
cfg: {} as never,
|
|
407
|
-
agentId: "agent",
|
|
408
|
-
runtime: {} as never,
|
|
409
|
-
chatId: "oc_chat",
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
|
|
374
|
+
const { options } = createDispatcherHarness();
|
|
413
375
|
await options.deliver({ mediaUrl: "https://example.com/a.png" }, { kind: "final" });
|
|
414
376
|
|
|
415
377
|
expect(sendMediaFeishuMock).toHaveBeenCalledTimes(1);
|
|
@@ -424,14 +386,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
|
|
|
424
386
|
});
|
|
425
387
|
|
|
426
388
|
it("falls back to legacy mediaUrl when mediaUrls is an empty array", async () => {
|
|
427
|
-
|
|
428
|
-
cfg: {} as never,
|
|
429
|
-
agentId: "agent",
|
|
430
|
-
runtime: {} as never,
|
|
431
|
-
chatId: "oc_chat",
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
|
|
389
|
+
const { options } = createDispatcherHarness();
|
|
435
390
|
await options.deliver(
|
|
436
391
|
{ text: "caption", mediaUrl: "https://example.com/a.png", mediaUrls: [] },
|
|
437
392
|
{ kind: "final" },
|
|
@@ -447,14 +402,9 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
|
|
|
447
402
|
});
|
|
448
403
|
|
|
449
404
|
it("sends attachments after streaming final markdown replies", async () => {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
agentId: "agent",
|
|
453
|
-
runtime: { log: vi.fn(), error: vi.fn() } as never,
|
|
454
|
-
chatId: "oc_chat",
|
|
405
|
+
const { options } = createDispatcherHarness({
|
|
406
|
+
runtime: createRuntimeLogger(),
|
|
455
407
|
});
|
|
456
|
-
|
|
457
|
-
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
|
|
458
408
|
await options.deliver(
|
|
459
409
|
{ text: "```ts\nconst x = 1\n```", mediaUrls: ["https://example.com/a.png"] },
|
|
460
410
|
{ kind: "final" },
|
|
@@ -472,16 +422,10 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
|
|
|
472
422
|
});
|
|
473
423
|
|
|
474
424
|
it("passes replyInThread to sendMessageFeishu for plain text", async () => {
|
|
475
|
-
|
|
476
|
-
cfg: {} as never,
|
|
477
|
-
agentId: "agent",
|
|
478
|
-
runtime: {} as never,
|
|
479
|
-
chatId: "oc_chat",
|
|
425
|
+
const { options } = createDispatcherHarness({
|
|
480
426
|
replyToMessageId: "om_msg",
|
|
481
427
|
replyInThread: true,
|
|
482
428
|
});
|
|
483
|
-
|
|
484
|
-
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
|
|
485
429
|
await options.deliver({ text: "plain text" }, { kind: "final" });
|
|
486
430
|
|
|
487
431
|
expect(sendMessageFeishuMock).toHaveBeenCalledWith(
|
|
@@ -504,16 +448,10 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
|
|
|
504
448
|
},
|
|
505
449
|
});
|
|
506
450
|
|
|
507
|
-
|
|
508
|
-
cfg: {} as never,
|
|
509
|
-
agentId: "agent",
|
|
510
|
-
runtime: {} as never,
|
|
511
|
-
chatId: "oc_chat",
|
|
451
|
+
const { options } = createDispatcherHarness({
|
|
512
452
|
replyToMessageId: "om_msg",
|
|
513
453
|
replyInThread: true,
|
|
514
454
|
});
|
|
515
|
-
|
|
516
|
-
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
|
|
517
455
|
await options.deliver({ text: "card text" }, { kind: "final" });
|
|
518
456
|
|
|
519
457
|
expect(sendMarkdownCardFeishuMock).toHaveBeenCalledWith(
|
|
@@ -525,16 +463,11 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
|
|
|
525
463
|
});
|
|
526
464
|
|
|
527
465
|
it("passes replyToMessageId and replyInThread to streaming.start()", async () => {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
agentId: "agent",
|
|
531
|
-
runtime: { log: vi.fn(), error: vi.fn() } as never,
|
|
532
|
-
chatId: "oc_chat",
|
|
466
|
+
const { options } = createDispatcherHarness({
|
|
467
|
+
runtime: createRuntimeLogger(),
|
|
533
468
|
replyToMessageId: "om_msg",
|
|
534
469
|
replyInThread: true,
|
|
535
470
|
});
|
|
536
|
-
|
|
537
|
-
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
|
|
538
471
|
await options.deliver({ text: "```ts\nconst x = 1\n```" }, { kind: "final" });
|
|
539
472
|
|
|
540
473
|
expect(streamingInstances).toHaveLength(1);
|
|
@@ -545,18 +478,13 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
|
|
|
545
478
|
});
|
|
546
479
|
|
|
547
480
|
it("disables streaming for thread replies and keeps reply metadata", async () => {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
agentId: "agent",
|
|
551
|
-
runtime: { log: vi.fn(), error: vi.fn() } as never,
|
|
552
|
-
chatId: "oc_chat",
|
|
481
|
+
const { options } = createDispatcherHarness({
|
|
482
|
+
runtime: createRuntimeLogger(),
|
|
553
483
|
replyToMessageId: "om_msg",
|
|
554
484
|
replyInThread: false,
|
|
555
485
|
threadReply: true,
|
|
556
486
|
rootId: "om_root_topic",
|
|
557
487
|
});
|
|
558
|
-
|
|
559
|
-
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
|
|
560
488
|
await options.deliver({ text: "```ts\nconst x = 1\n```" }, { kind: "final" });
|
|
561
489
|
|
|
562
490
|
expect(streamingInstances).toHaveLength(0);
|
|
@@ -569,16 +497,10 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
|
|
|
569
497
|
});
|
|
570
498
|
|
|
571
499
|
it("passes replyInThread to media attachments", async () => {
|
|
572
|
-
|
|
573
|
-
cfg: {} as never,
|
|
574
|
-
agentId: "agent",
|
|
575
|
-
runtime: {} as never,
|
|
576
|
-
chatId: "oc_chat",
|
|
500
|
+
const { options } = createDispatcherHarness({
|
|
577
501
|
replyToMessageId: "om_msg",
|
|
578
502
|
replyInThread: true,
|
|
579
503
|
});
|
|
580
|
-
|
|
581
|
-
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
|
|
582
504
|
await options.deliver({ mediaUrl: "https://example.com/a.png" }, { kind: "final" });
|
|
583
505
|
|
|
584
506
|
expect(sendMediaFeishuMock).toHaveBeenCalledWith(
|
package/src/reply-dispatcher.ts
CHANGED
|
@@ -224,6 +224,41 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|
|
224
224
|
lastPartial = "";
|
|
225
225
|
};
|
|
226
226
|
|
|
227
|
+
const sendChunkedTextReply = async (params: {
|
|
228
|
+
text: string;
|
|
229
|
+
useCard: boolean;
|
|
230
|
+
infoKind?: string;
|
|
231
|
+
}) => {
|
|
232
|
+
let first = true;
|
|
233
|
+
const chunkSource = params.useCard
|
|
234
|
+
? params.text
|
|
235
|
+
: core.channel.text.convertMarkdownTables(params.text, tableMode);
|
|
236
|
+
for (const chunk of core.channel.text.chunkTextWithMode(
|
|
237
|
+
chunkSource,
|
|
238
|
+
textChunkLimit,
|
|
239
|
+
chunkMode,
|
|
240
|
+
)) {
|
|
241
|
+
const message = {
|
|
242
|
+
cfg,
|
|
243
|
+
to: chatId,
|
|
244
|
+
text: chunk,
|
|
245
|
+
replyToMessageId: sendReplyToMessageId,
|
|
246
|
+
replyInThread: effectiveReplyInThread,
|
|
247
|
+
mentions: first ? mentionTargets : undefined,
|
|
248
|
+
accountId,
|
|
249
|
+
};
|
|
250
|
+
if (params.useCard) {
|
|
251
|
+
await sendMarkdownCardFeishu(message);
|
|
252
|
+
} else {
|
|
253
|
+
await sendMessageFeishu(message);
|
|
254
|
+
}
|
|
255
|
+
first = false;
|
|
256
|
+
}
|
|
257
|
+
if (params.infoKind === "final") {
|
|
258
|
+
deliveredFinalTexts.add(params.text);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
227
262
|
const { dispatcher, replyOptions, markDispatchIdle } =
|
|
228
263
|
core.channel.reply.createReplyDispatcherWithTyping({
|
|
229
264
|
responsePrefix: prefixContext.responsePrefix,
|
|
@@ -303,48 +338,10 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|
|
303
338
|
return;
|
|
304
339
|
}
|
|
305
340
|
|
|
306
|
-
let first = true;
|
|
307
341
|
if (useCard) {
|
|
308
|
-
|
|
309
|
-
text,
|
|
310
|
-
textChunkLimit,
|
|
311
|
-
chunkMode,
|
|
312
|
-
)) {
|
|
313
|
-
await sendMarkdownCardFeishu({
|
|
314
|
-
cfg,
|
|
315
|
-
to: chatId,
|
|
316
|
-
text: chunk,
|
|
317
|
-
replyToMessageId: sendReplyToMessageId,
|
|
318
|
-
replyInThread: effectiveReplyInThread,
|
|
319
|
-
mentions: first ? mentionTargets : undefined,
|
|
320
|
-
accountId,
|
|
321
|
-
});
|
|
322
|
-
first = false;
|
|
323
|
-
}
|
|
324
|
-
if (info?.kind === "final") {
|
|
325
|
-
deliveredFinalTexts.add(text);
|
|
326
|
-
}
|
|
342
|
+
await sendChunkedTextReply({ text, useCard: true, infoKind: info?.kind });
|
|
327
343
|
} else {
|
|
328
|
-
|
|
329
|
-
for (const chunk of core.channel.text.chunkTextWithMode(
|
|
330
|
-
converted,
|
|
331
|
-
textChunkLimit,
|
|
332
|
-
chunkMode,
|
|
333
|
-
)) {
|
|
334
|
-
await sendMessageFeishu({
|
|
335
|
-
cfg,
|
|
336
|
-
to: chatId,
|
|
337
|
-
text: chunk,
|
|
338
|
-
replyToMessageId: sendReplyToMessageId,
|
|
339
|
-
replyInThread: effectiveReplyInThread,
|
|
340
|
-
mentions: first ? mentionTargets : undefined,
|
|
341
|
-
accountId,
|
|
342
|
-
});
|
|
343
|
-
first = false;
|
|
344
|
-
}
|
|
345
|
-
if (info?.kind === "final") {
|
|
346
|
-
deliveredFinalTexts.add(text);
|
|
347
|
-
}
|
|
344
|
+
await sendChunkedTextReply({ text, useCard: false, infoKind: info?.kind });
|
|
348
345
|
}
|
|
349
346
|
}
|
|
350
347
|
|
|
@@ -25,6 +25,16 @@ describe("Feishu reply fallback for withdrawn/deleted targets", () => {
|
|
|
25
25
|
const replyMock = vi.fn();
|
|
26
26
|
const createMock = vi.fn();
|
|
27
27
|
|
|
28
|
+
async function expectFallbackResult(
|
|
29
|
+
send: () => Promise<{ messageId?: string }>,
|
|
30
|
+
expectedMessageId: string,
|
|
31
|
+
) {
|
|
32
|
+
const result = await send();
|
|
33
|
+
expect(replyMock).toHaveBeenCalledTimes(1);
|
|
34
|
+
expect(createMock).toHaveBeenCalledTimes(1);
|
|
35
|
+
expect(result.messageId).toBe(expectedMessageId);
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
beforeEach(() => {
|
|
29
39
|
vi.clearAllMocks();
|
|
30
40
|
resolveFeishuSendTargetMock.mockReturnValue({
|
|
@@ -51,16 +61,16 @@ describe("Feishu reply fallback for withdrawn/deleted targets", () => {
|
|
|
51
61
|
data: { message_id: "om_new" },
|
|
52
62
|
});
|
|
53
63
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
await expectFallbackResult(
|
|
65
|
+
() =>
|
|
66
|
+
sendMessageFeishu({
|
|
67
|
+
cfg: {} as never,
|
|
68
|
+
to: "user:ou_target",
|
|
69
|
+
text: "hello",
|
|
70
|
+
replyToMessageId: "om_parent",
|
|
71
|
+
}),
|
|
72
|
+
"om_new",
|
|
73
|
+
);
|
|
64
74
|
});
|
|
65
75
|
|
|
66
76
|
it("falls back to create for withdrawn card replies", async () => {
|
|
@@ -73,16 +83,16 @@ describe("Feishu reply fallback for withdrawn/deleted targets", () => {
|
|
|
73
83
|
data: { message_id: "om_card_new" },
|
|
74
84
|
});
|
|
75
85
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
await expectFallbackResult(
|
|
87
|
+
() =>
|
|
88
|
+
sendCardFeishu({
|
|
89
|
+
cfg: {} as never,
|
|
90
|
+
to: "user:ou_target",
|
|
91
|
+
card: { schema: "2.0" },
|
|
92
|
+
replyToMessageId: "om_parent",
|
|
93
|
+
}),
|
|
94
|
+
"om_card_new",
|
|
95
|
+
);
|
|
86
96
|
});
|
|
87
97
|
|
|
88
98
|
it("still throws for non-withdrawn reply failures", async () => {
|
|
@@ -111,16 +121,16 @@ describe("Feishu reply fallback for withdrawn/deleted targets", () => {
|
|
|
111
121
|
data: { message_id: "om_thrown_fallback" },
|
|
112
122
|
});
|
|
113
123
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
await expectFallbackResult(
|
|
125
|
+
() =>
|
|
126
|
+
sendMessageFeishu({
|
|
127
|
+
cfg: {} as never,
|
|
128
|
+
to: "user:ou_target",
|
|
129
|
+
text: "hello",
|
|
130
|
+
replyToMessageId: "om_parent",
|
|
131
|
+
}),
|
|
132
|
+
"om_thrown_fallback",
|
|
133
|
+
);
|
|
124
134
|
});
|
|
125
135
|
|
|
126
136
|
it("falls back to create when card reply throws a not-found AxiosError", async () => {
|
|
@@ -133,16 +143,16 @@ describe("Feishu reply fallback for withdrawn/deleted targets", () => {
|
|
|
133
143
|
data: { message_id: "om_axios_fallback" },
|
|
134
144
|
});
|
|
135
145
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
+
await expectFallbackResult(
|
|
147
|
+
() =>
|
|
148
|
+
sendCardFeishu({
|
|
149
|
+
cfg: {} as never,
|
|
150
|
+
to: "user:ou_target",
|
|
151
|
+
card: { schema: "2.0" },
|
|
152
|
+
replyToMessageId: "om_parent",
|
|
153
|
+
}),
|
|
154
|
+
"om_axios_fallback",
|
|
155
|
+
);
|
|
146
156
|
});
|
|
147
157
|
|
|
148
158
|
it("re-throws non-withdrawn thrown errors for text messages", async () => {
|