From 1aa30240a3a976c3838f2bdc8acff0a97b4ec1da Mon Sep 17 00:00:00 2001 From: lishuceo Date: Thu, 12 Mar 2026 12:16:23 +0800 Subject: [PATCH 1/4] fix: parse link (a tag) in post messages to prevent empty_content skip Post messages containing only links were parsed as empty because the parser only extracted 'text' tag elements, ignoring 'a' (link) tags. This caused rootId injection and fetchRecentMessages to skip link-only posts, making the bot unable to see links shared in thread topics. Co-Authored-By: Claude Opus 4.6 --- src/feishu/client.ts | 5 +++++ src/feishu/event-handler.ts | 6 +++++- src/feishu/message-parser.ts | 5 +++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/feishu/client.ts b/src/feishu/client.ts index 69e0e759..7f364ae8 100644 --- a/src/feishu/client.ts +++ b/src/feishu/client.ts @@ -564,6 +564,11 @@ export class FeishuClient { for (const paragraph of (postBody?.content as Array>>) ?? []) { for (const element of paragraph ?? []) { if (element.tag === 'text') textParts.push((element.text as string) ?? ''); + else if (element.tag === 'a') { + const linkText = (element.text as string) ?? ''; + const href = (element.href as string) ?? ''; + textParts.push(linkText && href ? `[${linkText}](${href})` : href || linkText); + } } } content = textParts.join(' '); diff --git a/src/feishu/event-handler.ts b/src/feishu/event-handler.ts index 9df455b4..72476169 100644 --- a/src/feishu/event-handler.ts +++ b/src/feishu/event-handler.ts @@ -2230,11 +2230,15 @@ async function parseMessage(data: MessageEventData): Promise>>) ?? []) { for (const element of paragraph ?? []) { if (element.tag === 'text') textParts.push((element.text as string) ?? ''); + else if (element.tag === 'a') { + const linkText = (element.text as string) ?? ''; + const href = (element.href as string) ?? ''; + textParts.push(linkText && href ? `[${linkText}](${href})` : href || linkText); + } } } return textParts.join(' ').trim(); From 395bb4b46e14d3c63c099444f85a629053e83cee Mon Sep 17 00:00:00 2001 From: lishuceo Date: Thu, 12 Mar 2026 12:16:45 +0800 Subject: [PATCH 2/4] test: add tests for post messages with link (a tag) elements Co-Authored-By: Claude Opus 4.6 --- src/feishu/__tests__/merge-forward.test.ts | 37 ++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/feishu/__tests__/merge-forward.test.ts b/src/feishu/__tests__/merge-forward.test.ts index f74a407f..9754e567 100644 --- a/src/feishu/__tests__/merge-forward.test.ts +++ b/src/feishu/__tests__/merge-forward.test.ts @@ -95,4 +95,41 @@ describe('formatMergeForwardSubMessage', () => { it('should handle empty content string', () => { expect(formatMergeForwardSubMessage('', 'text')).toBe(''); }); + + it('should parse post message with link (a tag)', () => { + const content = JSON.stringify({ + content: [[{ tag: 'a', text: 'Google', href: 'https://google.com' }]], + }); + expect(formatMergeForwardSubMessage(content, 'post')).toBe('[Google](https://google.com)'); + }); + + it('should parse post message with mixed text and links', () => { + const content = JSON.stringify({ + title: '分享', + content: [ + [ + { tag: 'text', text: '看看这个链接 ' }, + { tag: 'a', text: '点击查看', href: 'https://example.com/article' }, + { tag: 'text', text: ' 很有意思' }, + ], + ], + }); + expect(formatMergeForwardSubMessage(content, 'post')).toBe( + '分享 看看这个链接 [点击查看](https://example.com/article) 很有意思', + ); + }); + + it('should parse post with link-only content (no text elements)', () => { + const content = JSON.stringify({ + content: [[{ tag: 'a', text: '', href: 'https://example.com/page' }]], + }); + expect(formatMergeForwardSubMessage(content, 'post')).toBe('https://example.com/page'); + }); + + it('should parse post with link that has text but no href', () => { + const content = JSON.stringify({ + content: [[{ tag: 'a', text: 'some text', href: '' }]], + }); + expect(formatMergeForwardSubMessage(content, 'post')).toBe('some text'); + }); }); From aa9c2ef154f5a0e4d5993d255136205896800fcb Mon Sep 17 00:00:00 2001 From: lishuceo Date: Thu, 12 Mar 2026 12:38:14 +0800 Subject: [PATCH 3/4] feat: handle all 9 post element tags and additional message types Add support for all Feishu post rich text element tags (referencing OpenClaw's implementation): at, img, media, emotion, code_block, md, hr. Previously only text and a were handled, causing content loss. Also add explicit message type placeholders for media, interactive, share_chat, share_user, and system message types. Co-Authored-By: Claude Opus 4.6 --- src/feishu/client.ts | 17 +++++++++++++++++ src/feishu/event-handler.ts | 14 +++++++++++++- src/feishu/message-parser.ts | 22 ++++++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/feishu/client.ts b/src/feishu/client.ts index 7f364ae8..02f75e39 100644 --- a/src/feishu/client.ts +++ b/src/feishu/client.ts @@ -569,6 +569,23 @@ export class FeishuClient { const href = (element.href as string) ?? ''; textParts.push(linkText && href ? `[${linkText}](${href})` : href || linkText); } + else if (element.tag === 'at') { + const atName = (element.user_name as string) ?? ''; + if (atName) textParts.push(`@${atName}`); + } + else if (element.tag === 'img') textParts.push('[图片]'); + else if (element.tag === 'media') textParts.push('[视频]'); + else if (element.tag === 'emotion') { + const emojiType = (element.emoji_type as string) ?? ''; + textParts.push(emojiType ? `[${emojiType}]` : '[表情]'); + } + else if (element.tag === 'code_block') { + const lang = (element.language as string) ?? ''; + const code = (element.text as string) ?? ''; + textParts.push(lang ? `\`\`\`${lang}\n${code}\`\`\`` : `\`\`\`\n${code}\`\`\``); + } + else if (element.tag === 'md') textParts.push((element.text as string) ?? ''); + else if (element.tag === 'hr') textParts.push('---'); } } content = textParts.join(' '); diff --git a/src/feishu/event-handler.ts b/src/feishu/event-handler.ts index 72476169..315f539b 100644 --- a/src/feishu/event-handler.ts +++ b/src/feishu/event-handler.ts @@ -2237,8 +2237,20 @@ async function parseMessage(data: MessageEventData): Promise Date: Thu, 12 Mar 2026 12:38:22 +0800 Subject: [PATCH 4/4] test: add tests for all post element tags and message type placeholders Cover at, img, media, emotion, code_block, md, hr post tags and media, interactive, share_chat, share_user, system message types. 35 tests total, all passing. Co-Authored-By: Claude Opus 4.6 --- src/feishu/__tests__/merge-forward.test.ts | 125 ++++++++++++++++++++- 1 file changed, 124 insertions(+), 1 deletion(-) diff --git a/src/feishu/__tests__/merge-forward.test.ts b/src/feishu/__tests__/merge-forward.test.ts index 9754e567..c0f2bf09 100644 --- a/src/feishu/__tests__/merge-forward.test.ts +++ b/src/feishu/__tests__/merge-forward.test.ts @@ -85,7 +85,7 @@ describe('formatMergeForwardSubMessage', () => { }); it('should return generic placeholder for unknown message types', () => { - expect(formatMergeForwardSubMessage('{}', 'share_chat')).toBe('[share_chat消息]'); + expect(formatMergeForwardSubMessage('{}', 'share_calendar_event')).toBe('[share_calendar_event消息]'); }); it('should handle malformed JSON gracefully', () => { @@ -132,4 +132,127 @@ describe('formatMergeForwardSubMessage', () => { }); expect(formatMergeForwardSubMessage(content, 'post')).toBe('some text'); }); + + // --- Post element tags: at, img, media, emotion, code_block, md, hr --- + + it('should parse post with @mention (at tag)', () => { + const content = JSON.stringify({ + content: [[ + { tag: 'text', text: '请看 ' }, + { tag: 'at', user_id: 'ou_123', user_name: '张三' }, + { tag: 'text', text: ' 的方案' }, + ]], + }); + expect(formatMergeForwardSubMessage(content, 'post')).toBe('请看 @张三 的方案'); + }); + + it('should parse post with inline image (img tag)', () => { + const content = JSON.stringify({ + content: [[ + { tag: 'text', text: '截图如下 ' }, + { tag: 'img', image_key: 'img_xxx' }, + ]], + }); + expect(formatMergeForwardSubMessage(content, 'post')).toBe('截图如下 [图片]'); + }); + + it('should parse post with media (video) tag', () => { + const content = JSON.stringify({ + content: [[{ tag: 'media', file_key: 'file_xxx', image_key: 'img_xxx' }]], + }); + expect(formatMergeForwardSubMessage(content, 'post')).toBe('[视频]'); + }); + + it('should parse post with emotion tag', () => { + const content = JSON.stringify({ + content: [[ + { tag: 'text', text: '好的 ' }, + { tag: 'emotion', emoji_type: 'THUMBSUP' }, + ]], + }); + expect(formatMergeForwardSubMessage(content, 'post')).toBe('好的 [THUMBSUP]'); + }); + + it('should parse post with emotion tag without emoji_type', () => { + const content = JSON.stringify({ + content: [[{ tag: 'emotion' }]], + }); + expect(formatMergeForwardSubMessage(content, 'post')).toBe('[表情]'); + }); + + it('should parse post with code_block tag', () => { + const content = JSON.stringify({ + content: [[{ tag: 'code_block', language: 'typescript', text: 'const x = 1;' }]], + }); + expect(formatMergeForwardSubMessage(content, 'post')).toBe('```typescript\nconst x = 1;```'); + }); + + it('should parse post with code_block tag without language', () => { + const content = JSON.stringify({ + content: [[{ tag: 'code_block', text: 'echo hello' }]], + }); + expect(formatMergeForwardSubMessage(content, 'post')).toBe('```\necho hello```'); + }); + + it('should parse post with md tag', () => { + const content = JSON.stringify({ + content: [[{ tag: 'md', text: '**bold** and _italic_' }]], + }); + expect(formatMergeForwardSubMessage(content, 'post')).toBe('**bold** and _italic_'); + }); + + it('should parse post with hr tag', () => { + const content = JSON.stringify({ + content: [ + [{ tag: 'text', text: '上面的内容' }], + [{ tag: 'hr' }], + [{ tag: 'text', text: '下面的内容' }], + ], + }); + expect(formatMergeForwardSubMessage(content, 'post')).toBe('上面的内容 --- 下面的内容'); + }); + + it('should parse post with all element types mixed', () => { + const content = JSON.stringify({ + title: '技术分享', + content: [ + [ + { tag: 'text', text: '请 ' }, + { tag: 'at', user_id: 'ou_123', user_name: '李四' }, + { tag: 'text', text: ' 看看这个 ' }, + { tag: 'a', text: '链接', href: 'https://example.com' }, + ], + [{ tag: 'img', image_key: 'img_xxx' }], + [{ tag: 'emotion', emoji_type: 'SMILE' }], + ], + }); + const result = formatMergeForwardSubMessage(content, 'post'); + expect(result).toContain('技术分享'); + expect(result).toContain('@李四'); + expect(result).toContain('[链接](https://example.com)'); + expect(result).toContain('[图片]'); + expect(result).toContain('[SMILE]'); + }); + + // --- Message type placeholders --- + + it('should return placeholder for media message type', () => { + expect(formatMergeForwardSubMessage('{}', 'media')).toBe('[视频]'); + }); + + it('should return placeholder for interactive (card) message type', () => { + expect(formatMergeForwardSubMessage('{}', 'interactive')).toBe('[卡片消息]'); + }); + + it('should return placeholder for share_chat message type', () => { + expect(formatMergeForwardSubMessage('{}', 'share_chat')).toBe('[群名片]'); + }); + + it('should return placeholder for share_user message type', () => { + expect(formatMergeForwardSubMessage('{}', 'share_user')).toBe('[个人名片]'); + }); + + it('should return placeholder for system message type', () => { + expect(formatMergeForwardSubMessage('{}', 'system')).toBe('[系统消息]'); + }); });