diff --git a/nanobot/channels/feishu.py b/nanobot/channels/feishu.py index d1d0e5627..d3a4f8ee7 100644 --- a/nanobot/channels/feishu.py +++ b/nanobot/channels/feishu.py @@ -166,6 +166,28 @@ def _extract_interactive_content(content: dict) -> list[str]: return parts +def _stringify_table_cell(value: Any) -> str: + if value is None: + return "" + if isinstance(value, str): + return value.strip() + if isinstance(value, int | float | bool): + return str(value) + if isinstance(value, list): + return " ".join(filter(None, (_stringify_table_cell(item) for item in value))) + if isinstance(value, dict): + nested = _extract_element_content(value) + if nested: + return " ".join(nested) + for key in ("content", "text", "value", "name"): + text = value.get(key) + if isinstance(text, str): + return text.strip() + if isinstance(text, int | float | bool): + return str(text) + return "" + + def _extract_element_content(element: dict) -> list[str]: """Extract content from a single card element.""" parts = [] @@ -237,6 +259,28 @@ def _extract_element_content(element: dict) -> list[str]: if content: parts.append(content) + elif tag == "table": + columns = element.get("columns", []) + rows = element.get("rows", []) + if isinstance(columns, list): + column_names = [] + headers = [] + for column in columns: + if not isinstance(column, dict) or not column.get("name"): + continue + column_names.append(column["name"]) + headers.append(str(column.get("display_name") or column["name"])) + if headers: + parts.append(" | ".join(headers)) + if isinstance(rows, list): + for row in rows: + if not isinstance(row, dict): + continue + values = [_stringify_table_cell(row.get(name)) for name in column_names] + row_text = " | ".join(values).strip() + if row_text: + parts.append(row_text) + else: for ne in element.get("elements", []): parts.extend(_extract_element_content(ne)) diff --git a/tests/channels/test_feishu_card_extraction.py b/tests/channels/test_feishu_card_extraction.py new file mode 100644 index 000000000..05bfd7eb1 --- /dev/null +++ b/tests/channels/test_feishu_card_extraction.py @@ -0,0 +1,39 @@ +import json + +from nanobot.channels.feishu import _extract_share_card_content + + +def test_extract_interactive_card_reads_user_dsl_body_elements() -> None: + content = { + "user_dsl": json.dumps( + { + "schema": "2.0", + "body": {"elements": [{"tag": "markdown", "content": "**hello**"}]}, + } + ) + } + + assert _extract_share_card_content(content, "interactive") == "**hello**" + + +def test_extract_interactive_card_reads_nested_text_elements() -> None: + content = {"elements": [[{"tag": "text", "text": "hello"}]]} + + assert _extract_share_card_content(content, "interactive") == "hello" + + +def test_extract_interactive_card_reads_table_rows() -> None: + content = { + "elements": [ + { + "tag": "table", + "columns": [ + {"name": "c0", "display_name": "Name"}, + {"name": "c1", "display_name": "Score"}, + ], + "rows": [{"c0": "Alice", "c1": 98}], + } + ] + } + + assert _extract_share_card_content(content, "interactive") == "Name | Score\nAlice | 98"