Skip to content

Commit 43d0192

Browse files
committed
fix(feishu): use correct message.get endpoint for merge_forward parsing
1 parent 0783955 commit 43d0192

2 files changed

Lines changed: 269 additions & 73 deletions

File tree

packages/cli/src/services/feishu/gateway.test.ts

Lines changed: 163 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -332,16 +332,32 @@ describe('FeishuGateway - Message Parsing', () => {
332332
});
333333

334334
const fetchMock = vi.fn().mockImplementation(async (url: string) => {
335-
if (url.includes('/merged_forward')) {
335+
if (url.includes('/tenant_access_token')) {
336+
return mockFetchOk({
337+
tenant_access_token: 't-mock-token',
338+
expire: 7200,
339+
});
340+
}
341+
if (url.includes('/im/v1/messages/')) {
342+
// 飞书「获取指定消息内容」对 merge_forward 返回扁平化消息树:
343+
// 第一条是父消息(无 upper_message_id),其后子消息带 upper_message_id。
336344
return mockFetchOk({
337345
code: 0,
338346
msg: 'success',
339347
data: {
340348
items: [
349+
{
350+
message_id: 'om_merge_123',
351+
msg_type: 'merge_forward',
352+
body: { content: '{}' },
353+
create_time: '1615367850000',
354+
sender: { id: 'ou_root', id_type: 'open_id', sender_type: 'user' },
355+
},
341356
{
342357
message_id: 'om_sub_1',
343-
message_type: 'text',
344-
content: JSON.stringify({ text: 'hello from sub1' }),
358+
upper_message_id: 'om_merge_123',
359+
msg_type: 'text',
360+
body: { content: JSON.stringify({ text: 'hello from sub1' }) },
345361
create_time: '1615367851000',
346362
sender: {
347363
id: 'ou_user_1',
@@ -351,8 +367,9 @@ describe('FeishuGateway - Message Parsing', () => {
351367
},
352368
{
353369
message_id: 'om_sub_2',
354-
message_type: 'file',
355-
content: JSON.stringify({ file_key: 'file_sub_2', file_name: 'nested.zip' }),
370+
upper_message_id: 'om_merge_123',
371+
msg_type: 'file',
372+
body: { content: JSON.stringify({ file_key: 'file_sub_2', file_name: 'nested.zip' }) },
356373
create_time: '1615367852000',
357374
sender: {
358375
id: 'ou_user_2',
@@ -364,12 +381,6 @@ describe('FeishuGateway - Message Parsing', () => {
364381
},
365382
});
366383
}
367-
if (url.includes('/tenant_access_token')) {
368-
return mockFetchOk({
369-
tenant_access_token: 't-mock-token',
370-
expire: 7200,
371-
});
372-
}
373384
return mockFetchOk({ code: 0 });
374385
});
375386

@@ -422,26 +433,42 @@ describe('FeishuGateway - Message Parsing', () => {
422433
});
423434

424435
const fetchMock = vi.fn().mockImplementation(async (url: string) => {
425-
if (url.includes('/merged_forward')) {
436+
if (url.includes('/tenant_access_token')) {
437+
return mockFetchOk({
438+
tenant_access_token: 't-mock-token',
439+
expire: 7200,
440+
});
441+
}
442+
if (url.includes('/im/v1/messages/')) {
426443
return mockFetchOk({
427444
code: 0,
428445
msg: 'success',
429446
data: {
430447
items: [
448+
{
449+
message_id: 'om_merge_125',
450+
msg_type: 'merge_forward',
451+
body: { content: '{}' },
452+
create_time: '1615367850000',
453+
sender: { id: 'ou_root', id_type: 'open_id', sender_type: 'user' },
454+
},
431455
{
432456
message_id: 'om_sub_post_1',
433-
message_type: 'post',
434-
content: JSON.stringify({
435-
zh_cn: {
436-
title: 'First post',
437-
content: [
438-
[
439-
{ tag: 'text', text: 'Check first: ' },
440-
{ tag: 'img', image_key: 'img_key_1' },
457+
upper_message_id: 'om_merge_125',
458+
msg_type: 'post',
459+
body: {
460+
content: JSON.stringify({
461+
zh_cn: {
462+
title: 'First post',
463+
content: [
464+
[
465+
{ tag: 'text', text: 'Check first: ' },
466+
{ tag: 'img', image_key: 'img_key_1' },
467+
],
441468
],
442-
],
443-
},
444-
}),
469+
},
470+
}),
471+
},
445472
create_time: '1615367851000',
446473
sender: {
447474
id: 'ou_user_1',
@@ -451,18 +478,21 @@ describe('FeishuGateway - Message Parsing', () => {
451478
},
452479
{
453480
message_id: 'om_sub_post_2',
454-
message_type: 'post',
455-
content: JSON.stringify({
456-
zh_cn: {
457-
title: 'Second post',
458-
content: [
459-
[
460-
{ tag: 'text', text: 'Check second: ' },
461-
{ tag: 'img', image_key: 'img_key_2' },
481+
upper_message_id: 'om_merge_125',
482+
msg_type: 'post',
483+
body: {
484+
content: JSON.stringify({
485+
zh_cn: {
486+
title: 'Second post',
487+
content: [
488+
[
489+
{ tag: 'text', text: 'Check second: ' },
490+
{ tag: 'img', image_key: 'img_key_2' },
491+
],
462492
],
463-
],
464-
},
465-
}),
493+
},
494+
}),
495+
},
466496
create_time: '1615367852000',
467497
sender: {
468498
id: 'ou_user_2',
@@ -474,12 +504,6 @@ describe('FeishuGateway - Message Parsing', () => {
474504
},
475505
});
476506
}
477-
if (url.includes('/tenant_access_token')) {
478-
return mockFetchOk({
479-
tenant_access_token: 't-mock-token',
480-
expire: 7200,
481-
});
482-
}
483507
return mockFetchOk({ code: 0 });
484508
});
485509

@@ -524,7 +548,7 @@ describe('FeishuGateway - Message Parsing', () => {
524548
expect(receivedMsg.text).toContain('[图片_2]');
525549
});
526550

527-
it('correctly reports error when fetching merged_forward fails', async () => {
551+
it('correctly reports error when fetching merge_forward sub-messages fails', async () => {
528552
await gateway.connect();
529553

530554
const mockFetchError = (body: any) => ({
@@ -533,18 +557,18 @@ describe('FeishuGateway - Message Parsing', () => {
533557
});
534558

535559
const fetchMock = vi.fn().mockImplementation(async (url: string) => {
536-
if (url.includes('/merged_forward')) {
537-
return mockFetchError({
538-
code: 99991403,
539-
msg: 'No permission for merged_forward API',
540-
});
541-
}
542560
if (url.includes('/tenant_access_token')) {
543561
return mockFetchError({
544562
tenant_access_token: 't-mock-token',
545563
expire: 7200,
546564
});
547565
}
566+
if (url.includes('/im/v1/messages/')) {
567+
return mockFetchError({
568+
code: 230002,
569+
msg: 'Bot has no permission to access the message',
570+
});
571+
}
548572
return mockFetchError({ code: 0 });
549573
});
550574

@@ -577,7 +601,98 @@ describe('FeishuGateway - Message Parsing', () => {
577601

578602
expect(receivedMsg).not.toBeNull();
579603
expect(receivedMsg.messageType).toBe('merge_forward');
580-
expect(receivedMsg.text).toContain('原因: 飞书接口返回错误 (code: 99991403): No permission for merged_forward API');
604+
expect(receivedMsg.text).toContain('原因: 飞书接口返回错误 (code: 230002): Bot has no permission to access the message');
605+
});
606+
607+
it('recursively expands nested merge_forward sub-messages using upper_message_id tree', async () => {
608+
await gateway.connect();
609+
610+
const mockFetchOk = (body: any) => ({
611+
ok: true,
612+
json: async () => body,
613+
});
614+
615+
// 扁平树:root -> (text A) 和 (nested merge_forward) -> (text B 挂在 nested 下)
616+
const fetchMock = vi.fn().mockImplementation(async (url: string) => {
617+
if (url.includes('/tenant_access_token')) {
618+
return mockFetchOk({ tenant_access_token: 't-mock-token', expire: 7200 });
619+
}
620+
if (url.includes('/im/v1/messages/')) {
621+
return mockFetchOk({
622+
code: 0,
623+
msg: 'success',
624+
data: {
625+
items: [
626+
{
627+
message_id: 'om_root',
628+
msg_type: 'merge_forward',
629+
body: { content: '{}' },
630+
create_time: '1615367850000',
631+
sender: { id: 'ou_root', id_type: 'open_id', sender_type: 'user' },
632+
},
633+
{
634+
message_id: 'om_textA',
635+
upper_message_id: 'om_root',
636+
msg_type: 'text',
637+
body: { content: JSON.stringify({ text: 'top level A' }) },
638+
create_time: '1615367851000',
639+
sender: { id: 'ou_a', id_type: 'open_id', sender_type: 'user' },
640+
},
641+
{
642+
message_id: 'om_nested',
643+
upper_message_id: 'om_root',
644+
msg_type: 'merge_forward',
645+
body: { content: '{}' },
646+
create_time: '1615367852000',
647+
sender: { id: 'ou_nest', id_type: 'open_id', sender_type: 'user' },
648+
},
649+
{
650+
message_id: 'om_textB',
651+
upper_message_id: 'om_nested',
652+
msg_type: 'text',
653+
body: { content: JSON.stringify({ text: 'deep nested B' }) },
654+
create_time: '1615367853000',
655+
sender: { id: 'ou_b', id_type: 'open_id', sender_type: 'user' },
656+
},
657+
],
658+
},
659+
});
660+
}
661+
return mockFetchOk({ code: 0 });
662+
});
663+
664+
vi.stubGlobal('fetch', fetchMock);
665+
666+
const mockEvent = {
667+
event: {
668+
message: {
669+
message_id: 'om_root',
670+
message_type: 'merge_forward',
671+
content: 'Nested merged',
672+
chat_id: 'oc_456',
673+
chat_type: 'p2p',
674+
},
675+
sender: { sender_id: { open_id: 'ou_789' } },
676+
},
677+
};
678+
679+
let receivedMsg: any = null;
680+
gateway.onMessage = async (msg) => {
681+
receivedMsg = msg;
682+
return null;
683+
};
684+
685+
await messageCallback(mockEvent);
686+
687+
expect(receivedMsg).not.toBeNull();
688+
expect(receivedMsg.messageType).toBe('merge_forward');
689+
// 顶层文本与深层嵌套文本都应被展开
690+
expect(receivedMsg.text).toContain('top level A');
691+
expect(receivedMsg.text).toContain('deep nested B');
692+
// 深层文本应有更深的缩进(嵌套渲染)
693+
const lines = receivedMsg.text.split('\n');
694+
const deepLine = lines.find((l: string) => l.includes('deep nested B'));
695+
expect(deepLine.startsWith(' ')).toBe(true);
581696
});
582697
});
583698

0 commit comments

Comments
 (0)