ForcePilot/backend/package/yuxi/channel/extensions/wechat-mp/message.py
Kris 87a8931db3 feat(channel): 添加微信客服、微信公众号和微信支付通知渠道扩展
新增微信客服、微信公众号、微信支付通知三个渠道扩展。

微信客服渠道扩展功能模块:
- account: 账户管理
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- webhook: Webhook 事件处理
- outbound: 外发消息管理
- streaming: 流式消息处理
- pairing: 用户配对与绑定
- security: 安全校验
- crypto: 加解密处理
- dedupe: 消息去重
- customer: 客户管理
- servicer: 客服管理
- session: 会话管理
- status: 会话状态管理
- media: 媒体资源处理
- statistics: 统计功能
- sync: 数据同步
- upgrade: 升级处理

微信公众号渠道扩展功能模块:
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- webhook: Webhook 事件处理
- outbound: 外发消息管理
- streaming: 流式消息处理
- pairing: 用户配对与绑定
- security: 安全校验
- crypto: 加解密处理
- dedupe: 消息去重
- passive_reply: 被动回复
- message: 消息处理
- broadcast: 群发消息
- template: 模板消息
- menu: 菜单管理
- qrcode: 二维码管理
- user: 用户管理
- media: 媒体资源处理
- status: 会话状态管理

微信支付通知渠道扩展功能模块:
- config: 渠道配置管理
- webhook: Webhook 事件处理
- crypto: 加解密与签名校验
- cert_manager: 证书管理
- event_router: 事件路由
- dedupe: 消息去重
- pay_repo: 支付数据仓库
- query_client: 查询客户端
- arq_tasks: 异步任务
- callback_compensator: 回调补偿
2026-05-21 12:00:30 +08:00

139 lines
4.4 KiB
Python

import xml.etree.ElementTree as ET
from yuxi.channel.extensions.wechat_mp.types import InboundWeChatMessage
def parse_xml_to_message(xml_text: str) -> InboundWeChatMessage:
root = ET.fromstring(xml_text)
def _text(tag: str) -> str:
el = root.find(tag)
return el.text or "" if el is not None else ""
def _int(tag: str) -> int:
try:
return int(_text(tag))
except (ValueError, TypeError):
return 0
def _float(tag: str) -> float:
try:
return float(_text(tag))
except (ValueError, TypeError):
return 0.0
msg_type = _text("MsgType")
event = _text("Event") if msg_type == "event" else ""
msg_id = _text("MsgId")
if not msg_id:
msg_id = f"{_text('FromUserName')}_{_text('CreateTime')}_{msg_type}_{event}"
return InboundWeChatMessage(
msg_id=msg_id,
msg_type=msg_type,
from_user=_text("FromUserName"),
to_user=_text("ToUserName"),
create_time=_int("CreateTime"),
content=_text("Content"),
pic_url=_text("PicUrl"),
media_id=_text("MediaId"),
media_id_16k=_text("MediaId16K"),
thumb_media_id=_text("ThumbMediaId"),
media_format=_text("Format"),
recognition=_text("Recognition"),
location_x=_float("Location_X"),
location_y=_float("Location_Y"),
label=_text("Label"),
title=_text("Title"),
description=_text("Description"),
url=_text("Url"),
event=event,
event_key=_text("EventKey"),
app_id=_text("AppId"),
page_path=_text("PagePath"),
thumb_url=_text("ThumbUrl"),
raw_xml=_dict_from_element(root),
)
def resolve_voice(msg: InboundWeChatMessage) -> dict:
if msg.recognition and msg.recognition.strip():
return {"mode": "recognition", "text": msg.recognition, "ctype": "text"}
if msg.media_id:
return {"mode": "download", "media_id": msg.media_id, "format": msg.media_format, "ctype": "voice"}
return {"mode": "unknown", "ctype": "text"}
def extract_content(msg: InboundWeChatMessage) -> str:
match msg.msg_type:
case "text":
return msg.content
case "image":
if msg.pic_url:
return f"[图片: {msg.pic_url}]"
if msg.media_id:
return f"[图片: {msg.media_id}]"
return "[图片]"
case "voice":
voice_info = resolve_voice(msg)
if voice_info["mode"] == "recognition":
return voice_info["text"]
if voice_info["mode"] == "download":
return f"[语音: {voice_info['media_id']}]"
return "[语音]"
case "video" | "shortvideo":
if msg.media_id:
return f"[视频: {msg.media_id}]"
return "[视频]"
case "location":
return f"{msg.label}\n({msg.location_y}, {msg.location_x})"
case "link":
return f"{msg.title}\n{msg.description}\n{msg.url}"
case "miniprogrampage":
parts = []
if msg.title:
parts.append(f"小程序卡片: {msg.title}")
if msg.app_id:
parts.append(f"AppId: {msg.app_id}")
if msg.page_path:
parts.append(f"页面路径: {msg.page_path}")
return "\n".join(parts) if parts else "[小程序卡片]"
case _:
return msg.content or ""
async def download_media(media_id: str, gateway) -> bytes | None:
from yuxi.channel.extensions.wechat_mp.media import WeChatMedia
token = gateway.access_token if gateway else None
if not token:
return None
wm = WeChatMedia(lambda: token)
result = await wm.download(media_id)
if result.get("success"):
return result["data"]
return None
async def download_image_content(msg: InboundWeChatMessage, gateway) -> str:
if not msg.media_id and not msg.pic_url:
return ""
if msg.pic_url:
return msg.pic_url
data = await download_media(msg.media_id, gateway)
if data:
import base64
return f"data:image/jpeg;base64,{base64.b64encode(data).decode()}"
return ""
def _dict_from_element(element: ET.Element) -> dict:
result = {}
for child in element:
if len(child) > 0:
result[child.tag] = _dict_from_element(child)
else:
result[child.tag] = child.text or ""
return result