ForcePilot/backend/package/yuxi/channels/plugins/wecom/card_builder.py

180 lines
5.9 KiB
Python
Raw Normal View History

"""企业微信模板卡片构建模块。
将契约层 ``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