ForcePilot/backend/package/yuxi/channels/plugins/wecom/card_builder.py
Kris eda29502ed feat(wecom): 完善客服消息适配逻辑
1. 新增客服open_kfid缓存常量与key生成工具
2. 重构入站适配器缓存客服映射关系
3. 重写出站适配器使用缓存判断客服场景
4. 修复多选卡片构建逻辑与日志方法调用
5. 调整wechat_woc常量导入顺序
6. 优化命令适配器配置异常处理逻辑
2026-07-09 04:20:26 +08:00

190 lines
6.3 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嵌套结构
企业微信 multiple_interaction 卡片要求每个元素含 ``question_name`` 与
``option_list``(嵌套数组),而非扁平列表。每个 ``RichMessageCheckbox``
对应一个问题组,``cb.label`` 为问题名,``cb.options`` 为选项列表。
"""
result: list[dict[str, Any]] = []
for cb in checkboxes:
option_list = [{"text": opt.label, "key": opt.value} for opt in cb.options]
result.append(
{
"question_name": cb.label,
"option_list": option_list,
}
)
return result