diff --git a/src/feishu/__tests__/merge-forward.test.ts b/src/feishu/__tests__/merge-forward.test.ts index f74a407f..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', () => { @@ -95,4 +95,164 @@ 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'); + }); + + // --- 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('[系统消息]'); + }); }); diff --git a/src/feishu/client.ts b/src/feishu/client.ts index 69e0e759..02f75e39 100644 --- a/src/feishu/client.ts +++ b/src/feishu/client.ts @@ -564,6 +564,28 @@ 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); + } + 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 9df455b4..315f539b 100644 --- a/src/feishu/event-handler.ts +++ b/src/feishu/event-handler.ts @@ -2230,11 +2230,27 @@ 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); + } + 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('---'); } } return textParts.join(' ').trim(); @@ -42,8 +64,13 @@ export function formatMergeForwardSubMessage( if (msgType === 'file') return `[文件: ${body.file_name ?? ''}]`; if (msgType === 'audio') return '[语音消息]'; if (msgType === 'video') return '[视频]'; + if (msgType === 'media') return '[视频]'; if (msgType === 'sticker') return '[表情]'; + if (msgType === 'interactive') return '[卡片消息]'; + if (msgType === 'share_chat') return '[群名片]'; + if (msgType === 'share_user') return '[个人名片]'; if (msgType === 'merge_forward') return '[嵌套的合并转发消息]'; + if (msgType === 'system') return '[系统消息]'; return `[${msgType}消息]`; } catch {