实现企业微信全功能渠道插件,包含适配器、客户端、签名校验、模板卡片构建等模块,完成从事件接收、会话解析到消息发送的完整流程支持,包含配置校验、错误翻译、探测能力与生命周期管理。
180 lines
5.9 KiB
Python
180 lines
5.9 KiB
Python
"""企业微信模板卡片构建模块。
|
||
|
||
将契约层 ``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
|