新增微信客服、微信公众号、微信支付通知三个渠道扩展。 微信客服渠道扩展功能模块: - 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: 回调补偿
139 lines
4.4 KiB
Python
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 |