Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 161 additions & 1 deletion src/feishu/__tests__/merge-forward.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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('[系统消息]');
});
});
22 changes: 22 additions & 0 deletions src/feishu/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,28 @@ export class FeishuClient {
for (const paragraph of (postBody?.content as Array<Array<Record<string, unknown>>>) ?? []) {
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(' ');
Expand Down
18 changes: 17 additions & 1 deletion src/feishu/event-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2230,11 +2230,27 @@ async function parseMessage(data: MessageEventData): Promise<ParsedMessage | nul
if (!isBot && atName) {
textParts.push(`@${atName}`);
}
} 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 === 'img') {
const imageKey = element.image_key as string | undefined;
if (imageKey) imageKeys.push(imageKey);
} 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('---');
}
// a/emotion 等标签忽略
}
}

Expand Down
27 changes: 27 additions & 0 deletions src/feishu/message-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,28 @@ export function formatMergeForwardSubMessage(
for (const paragraph of (postBody?.content as Array<Array<Record<string, unknown>>>) ?? []) {
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();
Expand All @@ -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 {
Expand Down
Loading