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:  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 (|---|---|)
);
}