From 4752e95a24ca52edfcd09e07433d26db81e5645f Mon Sep 17 00:00:00 2001 From: Re-bin Date: Sun, 1 Mar 2026 06:36:29 +0000 Subject: [PATCH] merge origin/main into pr-1361 --- nanobot/channels/feishu.py | 104 ++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 58 deletions(-) diff --git a/nanobot/channels/feishu.py b/nanobot/channels/feishu.py index 6bc0ebd..0a0a5e4 100644 --- a/nanobot/channels/feishu.py +++ b/nanobot/channels/feishu.py @@ -181,71 +181,59 @@ def _extract_element_content(element: dict) -> list[str]: def _extract_post_content(content_json: dict) -> tuple[str, list[str]]: - """Extract text and image keys from Feishu post (rich text) message content. + """Extract text and image keys from Feishu post (rich text) message. - Supports two formats: - 1. Direct format: {"title": "...", "content": [...]} - 2. Localized format: {"zh_cn": {"title": "...", "content": [...]}} - - Returns: - (text, image_keys) - extracted text and list of image keys + Handles three payload shapes: + - Direct: {"title": "...", "content": [[...]]} + - Localized: {"zh_cn": {"title": "...", "content": [...]}} + - Wrapped: {"post": {"zh_cn": {"title": "...", "content": [...]}}} """ - def extract_from_lang(lang_content: dict) -> tuple[str | None, list[str]]: - if not isinstance(lang_content, dict): + + def _parse_block(block: dict) -> tuple[str | None, list[str]]: + if not isinstance(block, dict) or not isinstance(block.get("content"), list): return None, [] - title = lang_content.get("title", "") - content_blocks = lang_content.get("content", []) - if not isinstance(content_blocks, list): - return None, [] - text_parts = [] - image_keys = [] - if title: - text_parts.append(title) - for block in content_blocks: - if not isinstance(block, list): + texts, images = [], [] + if title := block.get("title"): + texts.append(title) + for row in block["content"]: + if not isinstance(row, list): continue - for element in block: - if isinstance(element, dict): - tag = element.get("tag") - if tag == "text": - text_parts.append(element.get("text", "")) - elif tag == "a": - text_parts.append(element.get("text", "")) - elif tag == "at": - text_parts.append(f"@{element.get('user_name', 'user')}") - elif tag == "img": - img_key = element.get("image_key") - if img_key: - image_keys.append(img_key) - text = " ".join(text_parts).strip() if text_parts else None - return text, image_keys + for el in row: + if not isinstance(el, dict): + continue + tag = el.get("tag") + if tag in ("text", "a"): + texts.append(el.get("text", "")) + elif tag == "at": + texts.append(f"@{el.get('user_name', 'user')}") + elif tag == "img" and (key := el.get("image_key")): + images.append(key) + return (" ".join(texts).strip() or None), images - # Compatible with both shapes: - # 1) {"post": {"zh_cn": {...}}} - # 2) {"zh_cn": {...}} or {"title": "...", "content": [...]} - post_root = content_json.get("post") if isinstance(content_json, dict) else None - if not isinstance(post_root, dict): - post_root = content_json if isinstance(content_json, dict) else {} + # Unwrap optional {"post": ...} envelope + root = content_json + if isinstance(root, dict) and isinstance(root.get("post"), dict): + root = root["post"] + if not isinstance(root, dict): + return "", [] - # Try direct format first - if "content" in post_root: - text, images = extract_from_lang(post_root) - if text or images: - return text or "", images + # Direct format + if "content" in root: + text, imgs = _parse_block(root) + if text or imgs: + return text or "", imgs - # Try localized format - for lang_key in ("zh_cn", "en_us", "ja_jp"): - lang_content = post_root.get(lang_key) - text, images = extract_from_lang(lang_content) - if text or images: - return text or "", images - - # Fallback: first dict-shaped child - for value in post_root.values(): - if isinstance(value, dict): - text, images = extract_from_lang(value) - if text or images: - return text or "", images + # Localized: prefer known locales, then fall back to any dict child + for key in ("zh_cn", "en_us", "ja_jp"): + if key in root: + text, imgs = _parse_block(root[key]) + if text or imgs: + return text or "", imgs + for val in root.values(): + if isinstance(val, dict): + text, imgs = _parse_block(val) + if text or imgs: + return text or "", imgs return "", []