175 lines
5.7 KiB
Python
175 lines
5.7 KiB
Python
|
|
import xml.etree.ElementTree as ET
|
||
|
|
from datetime import datetime, UTC
|
||
|
|
|
||
|
|
from yuxi.channel.extensions.wechat_mp.types import InboundWeChatMessage
|
||
|
|
from yuxi.channel.message.models import MessageType, PeerInfo, UnifiedMessage
|
||
|
|
from yuxi.channel.routing.models import PeerKind
|
||
|
|
|
||
|
|
|
||
|
|
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 build_unified_message(raw_msg: InboundWeChatMessage, content: str, account_id: str = "default") -> UnifiedMessage:
|
||
|
|
msg_type = MessageType.TEXT
|
||
|
|
if raw_msg.msg_type == "image":
|
||
|
|
msg_type = MessageType.IMAGE
|
||
|
|
elif raw_msg.msg_type == "voice":
|
||
|
|
msg_type = MessageType.VOICE
|
||
|
|
|
||
|
|
return UnifiedMessage(
|
||
|
|
msg_id=raw_msg.msg_id,
|
||
|
|
channel_type="wechat-miniprogram",
|
||
|
|
account_id=account_id,
|
||
|
|
content=content,
|
||
|
|
message_type=msg_type,
|
||
|
|
sender=PeerInfo(
|
||
|
|
id=raw_msg.from_user,
|
||
|
|
kind=PeerKind.DIRECT,
|
||
|
|
display_name=raw_msg.from_user,
|
||
|
|
),
|
||
|
|
timestamp=datetime.fromtimestamp(raw_msg.create_time, tz=UTC) if raw_msg.create_time else None,
|
||
|
|
raw_payload=raw_msg.raw_xml,
|
||
|
|
body_for_agent=content,
|
||
|
|
metadata={
|
||
|
|
"FromUserName": raw_msg.from_user,
|
||
|
|
"ToUserName": raw_msg.to_user,
|
||
|
|
"MsgType": raw_msg.msg_type,
|
||
|
|
"Event": raw_msg.event,
|
||
|
|
"EventKey": raw_msg.event_key,
|
||
|
|
"MediaId": raw_msg.media_id,
|
||
|
|
"PicUrl": raw_msg.pic_url,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
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
|