"""Feishu card JSON builder for Hermes Feishu plugin. Builds Feishu interactive card JSON structures from parsed table data and markdown content. """ from __future__ import annotations import json from typing import Any, Dict, List, Optional from .table_parser import ParsedTable, TableColumn, TableCell, parse_table, split_table_and_text def _build_table_columns(columns: List[TableColumn]) -> List[Dict[str, Any]]: """Build Feishu Table column definitions. Args: columns: Parsed table column definitions. Returns: List of Feishu column spec dicts. Feishu column format: {"name": "field_name", "display_name": "显示名称", "width": "auto"} """ feishu_cols: List[Dict[str, Any]] = [] for idx, col in enumerate(columns): spec: Dict[str, Any] = { "name": f"col_{idx}", # Use col_0, col_1, etc. as field names "display_name": col.name, # Column header text "width": "auto", "field_type": "text", # Feishu requires this field } if col.width: spec["width"] = col.width feishu_cols.append(spec) return feishu_cols def _build_table_rows( rows: List[List[TableCell]], columns: List[TableColumn], ) -> List[Dict[str, Any]]: """Build Feishu Table row data. Args: rows: Parsed table cell data. columns: Column definitions (for type info). Returns: List of Feishu row dicts. Feishu row format: {"col_0": {"tag": "plain_text", "content": "..."}, "col_1": {...}} """ feishu_rows: List[Dict[str, Any]] = [] for row in rows: feishu_row: Dict[str, Any] = {} for idx, cell in enumerate(row): # Build cell: key is column name (col_0, col_1, etc.) # When field_type is "text", value should be just the string feishu_row[f"col_{idx}"] = cell.text feishu_rows.append(feishu_row) return feishu_rows def build_table_card( table: ParsedTable, title: str = "📊 数据表格", template: str = "blue", ) -> Dict[str, Any]: """Build a Feishu interactive card containing a Table component. Args: table: A parsed table from table_parser. title: Card header title. template: Card header color template (blue, wathet, turquoise, green, yellow, orange, red, carmine, violet, purple, indigo, grey). Returns: Complete Feishu card JSON dict. """ columns = _build_table_columns(table.headers) rows = _build_table_rows(table.rows, table.headers) card: Dict[str, Any] = { "config": {"wide_screen_mode": True}, "header": { "title": {"content": title, "tag": "plain_text"}, "template": template, }, "elements": [ { "tag": "table", "columns": columns, "rows": rows, } ], } return card def build_content_card( content: str, title: Optional[str] = None, template: str = "blue", ) -> Dict[str, Any]: """Build a Feishu card with markdown content (no table). Used for non-table content that should be sent as a card. Args: content: Markdown content for the card body. title: Optional card header title. template: Card header color template. Returns: Complete Feishu card JSON dict. """ card: Dict[str, Any] = { "config": {"wide_screen_mode": True}, } if title: card["header"] = { "title": {"content": title, "tag": "plain_text"}, "template": template, } card["elements"] = [ { "tag": "markdown", "content": content, } ] return card def build_mixed_card( markdown: str, title: Optional[str] = None, template: str = "blue", ) -> Optional[Dict[str, Any]]: """Build a Feishu card that handles mixed content (text + tables). If the content contains tables, they are rendered as Table components. Non-table text is rendered as markdown elements. Args: markdown: Full markdown content that may include tables. title: Optional card header title. template: Card header color template. Returns: Complete Feishu card JSON dict, or None if no tables found (in which case use build_content_card or send as post message). """ tables = parse_table(markdown) if not tables: return None card: Dict[str, Any] = { "config": {"wide_screen_mode": True}, } if title: card["header"] = { "title": {"content": title, "tag": "plain_text"}, "template": template, } elements: List[Dict[str, Any]] = [] table_blocks, text_segments = split_table_and_text(markdown) # Interleave text and table elements in original order table_idx = 0 text_idx = 0 # Walk through the original markdown to maintain order import re _TABLE_BLOCK_RE = re.compile( r"((?:^\|[^\n]+\|\s*\n" r"^\|[\s:|-]+\|\s*\n" r"(?:^\|[^\n]+\|\s*\n?)*)+)", re.MULTILINE, ) last_end = 0 for match in _TABLE_BLOCK_RE.finditer(markdown): # Text before this table before = markdown[last_end:match.start()].strip() if before: elements.append({ "tag": "markdown", "content": before, }) # Table element if table_idx < len(tables): table = tables[table_idx] columns = _build_table_columns(table.headers) rows = _build_table_rows(table.rows, table.headers) elements.append({ "tag": "table", "columns": columns, "rows": rows, }) table_idx += 1 last_end = match.end() # Remaining text after last table remaining = markdown[last_end:].strip() if remaining: elements.append({ "tag": "markdown", "content": remaining, }) card["elements"] = elements return card def card_to_json(card: Dict[str, Any]) -> str: """Serialize a card dict to JSON string. Args: card: Feishu card JSON dict. Returns: Compact JSON string (ensure_ascii=False for CJK support). """ return json.dumps(card, ensure_ascii=False, separators=(",", ":"))