ForcePilot/backend/package/yuxi/channels/plugins/wecom/card_builder.py
Kris 28d2c19f23 feat(wecom): 新增企业微信渠道插件完整实现
实现企业微信全功能渠道插件,包含适配器、客户端、签名校验、模板卡片构建等模块,完成从事件接收、会话解析到消息发送的完整流程支持,包含配置校验、错误翻译、探测能力与生命周期管理。
2026-07-08 22:58:25 +08:00

180 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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