From 16ebfaf77779924cada723c991364078f2a9e3d5 Mon Sep 17 00:00:00 2001 From: alex xiang Date: Thu, 29 Jan 2026 16:56:26 +0800 Subject: [PATCH] update for feishu --- src/feishu/send.ts | 103 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 97 insertions(+), 6 deletions(-) diff --git a/src/feishu/send.ts b/src/feishu/send.ts index a05e8bbef..9c5541bd3 100644 --- a/src/feishu/send.ts +++ b/src/feishu/send.ts @@ -17,15 +17,18 @@ import { /** * Feishu Interactive Card structure for rich markdown messages * - * Feishu supports full markdown in interactive cards (msg_type: "interactive"): + * Feishu supports markdown in interactive cards (msg_type: "interactive"): * - Basic: **bold**, *italic*, ~~strikethrough~~, [link](url) - * - Headings: # H1, ## H2 (only H1/H2 supported) - * - Lists: - item (unordered), 1. item (ordered) + * - Headings: # H1, ## H2 (H3-H6 auto-converted to bold text) + * - Lists: - item (unordered), 1. item (ordered) - no nesting * - Code: ```lang code``` (requires Feishu 7.6+) * - Images: ![alt](url) or img_key * - Horizontal rule: --- - * - Colors: text + * - Colors: text (green, red, grey) * - @mention: , + * - Tables: (NOT standard markdown |col|col|) + * + * Note: Standard markdown tables (|col|col|) are auto-converted to Feishu format. * * See: https://www.feishu.cn/content/7gprunv5 */ @@ -50,9 +53,90 @@ export type FeishuInteractiveCard = { }>; }; +/** + * Convert H3-H6 headings to bold text (Feishu only supports H1/H2) + * + * Input: ### Title + * Output: **Title** + * + * Input: #### Subtitle + * Output: **Subtitle** + */ +function convertUnsupportedHeadings(markdown: string): string { + // Match H3-H6 headings (### to ######) at start of line + // Convert to bold text since Feishu doesn't support these levels + return markdown.replace(/^(#{3,6})\s+(.+)$/gm, (_match, _hashes, title: string) => { + return `**${title.trim()}**`; + }); +} + +/** + * Convert standard markdown table to Feishu table component syntax + * + * Input: + * | Name | Age | + * |------|-----| + * | John | 30 | + * | Jane | 25 | + * + * Output: + *
+ */ +function convertMarkdownTableToFeishu(markdown: string): string { + // Match markdown table pattern + const tableRegex = /^\|(.+)\|\s*\n\|[-|:\s]+\|\s*\n((?:\|.+\|\s*\n?)+)/gm; + + return markdown.replace(tableRegex, (match, headerRow: string, bodyRows: string) => { + // Parse header columns + const headers = headerRow + .split("|") + .map((h: string) => h.trim()) + .filter((h: string) => h.length > 0); + + if (headers.length === 0) return match; + + // Build columns definition + const columns = headers.map((title: string, index: number) => ({ + dataIndex: `col${index}`, + title: title, + })); + + // Parse body rows + const rows = bodyRows + .trim() + .split("\n") + .map((row: string) => { + const cells = row + .split("|") + .map((c: string) => c.trim()) + .filter( + (c: string, i: number, arr: string[]) => (i > 0 && i < arr.length - 1) || c.length > 0, + ); + + // Build row object + const rowObj: Record = {}; + cells.forEach((cell: string, index: number) => { + if (index < headers.length) { + rowObj[`col${index}`] = cell; + } + }); + return rowObj; + }) + .filter((row: Record) => Object.keys(row).length > 0); + + // Build Feishu table component + return `
`; + }); +} + /** * Build a Feishu interactive card with markdown content * + * Automatically converts standard markdown features to Feishu format: + * - Tables: |col|col| converted to
component + * - Headings: # and ## native, ### to ###### converted to **bold** + * - Lists, code blocks, links, bold/italic: native support + * * @param markdown - Markdown content to display * @param options - Optional card configuration */ @@ -64,6 +148,11 @@ export function buildFeishuMarkdownCard( wideScreen?: boolean; }, ): FeishuInteractiveCard { + // Convert unsupported markdown features to Feishu-compatible format + let processedMarkdown = markdown; + processedMarkdown = convertUnsupportedHeadings(processedMarkdown); // H3-H6 → bold + processedMarkdown = convertMarkdownTableToFeishu(processedMarkdown); // |table| →
+ const card: FeishuInteractiveCard = { config: { wide_screen_mode: options?.wideScreen ?? true, @@ -71,7 +160,7 @@ export function buildFeishuMarkdownCard( elements: [ { tag: "markdown", - content: markdown, + content: processedMarkdown, }, ], }; @@ -250,7 +339,9 @@ export function hasMarkdown(text: string): boolean { /^#{1,6}\s+/m.test(text) || // headings (# to ######) /^[-*]\s+/m.test(text) || // unordered list /^\d+\.\s+/m.test(text) || // ordered list - /```[\s\S]*?```/.test(text) // fenced code blocks + /```[\s\S]*?```/.test(text) || // fenced code blocks + /^\|.+\|$/m.test(text) || // table rows (|col|col|) + /^[-|:]+$/m.test(text) // table separator (|---|---|) ); }