"""企业微信模板卡片构建模块。 将契约层 ``RichMessage`` DTO 映射为企业微信模板卡片 JSON,支持 5 种子类型: - ``text_notice``: 文本通知型 - ``news_notice``: 图文展示型 - ``button_interaction``: 按钮交互型 - ``vote_interaction``: 投票选择型 - ``multiple_interaction``: 多项选择型 并提供降级 Markdown 文本的构建能力。仅依赖契约层 DTO 与标准库。 """ from __future__ import annotations from typing import Any from yuxi.channels.contract.dtos.outbound import RichMessage def build_template_card_from_rich_message(rich_message: RichMessage) -> dict[str, Any]: """将 ``RichMessage`` 映射为企业微信模板卡片 JSON。 映射规则: - 有 ``buttons`` → ``button_interaction`` 类型 - 有 ``checkboxes`` → ``multiple_interaction`` 类型 - 有 ``selects`` 且无 buttons → ``vote_interaction`` 类型 - 有 ``image_url`` 且无交互元素 → ``news_notice`` 类型 - 默认 → ``text_notice`` 类型 Returns: ``{"msgtype": "template_card", "template_card": {...}}`` 格式 dict。 """ card_type = _infer_card_type(rich_message) card: dict[str, Any] = { "card_type": card_type, } if rich_message.title: card["main_title"] = {"title": rich_message.title} if rich_message.text: card.setdefault("main_title", {})["desc"] = rich_message.text if rich_message.description: card["subtitle"] = rich_message.description if rich_message.image_url and card_type == "news_notice": card["image_url"] = rich_message.image_url if card_type == "button_interaction": card["button_list"] = _build_button_list(rich_message.buttons) if card_type == "vote_interaction": # 企业微信 vote_interaction 卡片使用 option_list 字段(非 checkboxes, # 与 multiple_interaction 区分),否则 API 拒绝 card["option_list"] = _build_vote_options(rich_message.selects) if card_type == "multiple_interaction": card["checkboxes"] = _build_multiple_options(rich_message.checkboxes) return {"msgtype": "template_card", "template_card": card} def build_text_notice_card( title: str, description: str, *, url: str | None = None, ) -> dict[str, Any]: """构建文本通知型模板卡片。 用于流式输出首块占位卡片(决策 4)。 """ card: dict[str, Any] = { "card_type": "text_notice", "main_title": {"title": title, "desc": description}, } if url: card["url"] = url return {"msgtype": "template_card", "template_card": card} def build_markdown_message(content: str) -> dict[str, Any]: """构建 Markdown 消息。 企业微信 Markdown 消息格式:``{"msgtype": "markdown", "markdown": {"content": "..."}}``。 """ return {"msgtype": "markdown", "markdown": {"content": content}} def build_text_message(content: str) -> dict[str, Any]: """构建文本消息。 企业微信文本消息格式:``{"msgtype": "text", "text": {"content": "..."}}``。 """ return {"msgtype": "text", "text": {"content": content}} def build_image_message(media_id: str) -> dict[str, Any]: """构建图片消息。""" return {"msgtype": "image", "image": {"media_id": media_id}} def build_video_message(media_id: str, thumb_media_id: str = "") -> dict[str, Any]: """构建视频消息。""" return {"msgtype": "video", "video": {"media_id": media_id, "thumb_media_id": thumb_media_id}} def build_voice_message(media_id: str) -> dict[str, Any]: """构建语音消息。""" return {"msgtype": "voice", "voice": {"media_id": media_id}} def build_file_message(media_id: str) -> dict[str, Any]: """构建文件消息。""" return {"msgtype": "file", "file": {"media_id": media_id}} def degrade_to_markdown(rich_message: RichMessage) -> str: """将 ``RichMessage`` 降级为 Markdown 文本,空字段跳过。 标题用 ``# `` 前缀,按钮用 ``- [label](value)`` 列表。 """ lines: list[str] = [] if rich_message.title: lines.append(f"# {rich_message.title}") if rich_message.text: lines.append(rich_message.text) if rich_message.description: lines.append(rich_message.description) if rich_message.buttons: for btn in rich_message.buttons: target = btn.value or btn.action lines.append(f"- [{btn.label}]({target})") return "\n".join(lines) def _infer_card_type(rich_message: RichMessage) -> str: """根据 RichMessage 字段推断模板卡片子类型。""" if rich_message.buttons: return "button_interaction" if rich_message.checkboxes: return "multiple_interaction" if rich_message.selects: return "vote_interaction" if rich_message.image_url: return "news_notice" return "text_notice" def _build_button_list(buttons: tuple[Any, ...]) -> list[dict[str, Any]]: """构建按钮交互型 button_list。""" result: list[dict[str, Any]] = [] for btn in buttons: item: dict[str, Any] = {"text": btn.label, "style": 1} if btn.value is not None: item["key"] = btn.value elif btn.action: item["key"] = btn.action result.append(item) return result def _build_vote_options(selects: tuple[Any, ...]) -> list[dict[str, Any]]: """构建投票选择型 option。""" result: list[dict[str, Any]] = [] for sel in selects: for opt in sel.options: result.append({"text": opt.label, "key": opt.value}) return result def _build_multiple_options(checkboxes: tuple[Any, ...]) -> list[dict[str, Any]]: """构建多项选择型 checkboxes。""" result: list[dict[str, Any]] = [] for cb in checkboxes: for opt in cb.options: result.append({"text": opt.label, "key": opt.value}) return result