From 2ca15f2a9d0d9e5fb858bd68def980697f9f3137 Mon Sep 17 00:00:00 2001 From: Re-bin Date: Sat, 7 Feb 2026 09:46:53 +0000 Subject: [PATCH] feat: enhance Feishu markdown rendering --- nanobot/channels/feishu.py | 50 +++++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/nanobot/channels/feishu.py b/nanobot/channels/feishu.py index 01b808e..1c176a2 100644 --- a/nanobot/channels/feishu.py +++ b/nanobot/channels/feishu.py @@ -2,6 +2,7 @@ import asyncio import json +import re import threading from collections import OrderedDict from typing import Any @@ -156,6 +157,44 @@ class FeishuChannel(BaseChannel): loop = asyncio.get_running_loop() await loop.run_in_executor(None, self._add_reaction_sync, message_id, emoji_type) + # Regex to match markdown tables (header + separator + data rows) + _TABLE_RE = re.compile( + r"((?:^[ \t]*\|.+\|[ \t]*\n)(?:^[ \t]*\|[-:\s|]+\|[ \t]*\n)(?:^[ \t]*\|.+\|[ \t]*\n?)+)", + re.MULTILINE, + ) + + @staticmethod + def _parse_md_table(table_text: str) -> dict | None: + """Parse a markdown table into a Feishu table element.""" + lines = [l.strip() for l in table_text.strip().split("\n") if l.strip()] + if len(lines) < 3: + return None + split = lambda l: [c.strip() for c in l.strip("|").split("|")] + headers = split(lines[0]) + rows = [split(l) for l in lines[2:]] + columns = [{"tag": "column", "name": f"c{i}", "display_name": h, "width": "auto"} + for i, h in enumerate(headers)] + return { + "tag": "table", + "page_size": len(rows) + 1, + "columns": columns, + "rows": [{f"c{i}": r[i] if i < len(r) else "" for i in range(len(headers))} for r in rows], + } + + def _build_card_elements(self, content: str) -> list[dict]: + """Split content into markdown + table elements for Feishu card.""" + elements, last_end = [], 0 + for m in self._TABLE_RE.finditer(content): + before = content[last_end:m.start()].strip() + if before: + elements.append({"tag": "markdown", "content": before}) + elements.append(self._parse_md_table(m.group(1)) or {"tag": "markdown", "content": m.group(1)}) + last_end = m.end() + remaining = content[last_end:].strip() + if remaining: + elements.append({"tag": "markdown", "content": remaining}) + return elements or [{"tag": "markdown", "content": content}] + async def send(self, msg: OutboundMessage) -> None: """Send a message through Feishu.""" if not self._client: @@ -170,15 +209,20 @@ class FeishuChannel(BaseChannel): else: receive_id_type = "open_id" - # Build text message content - content = json.dumps({"text": msg.content}) + # Build card with markdown + table support + elements = self._build_card_elements(msg.content) + card = { + "config": {"wide_screen_mode": True}, + "elements": elements, + } + content = json.dumps(card, ensure_ascii=False) request = CreateMessageRequest.builder() \ .receive_id_type(receive_id_type) \ .request_body( CreateMessageRequestBody.builder() .receive_id(msg.chat_id) - .msg_type("text") + .msg_type("interactive") .content(content) .build() ).build()