You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

169 lines
6.1 KiB

"""从微信聊天面板提取消息"""
from __future__ import annotations
import hashlib
import logging
import time
from typing import Any
import uiautomation as uia
from ..models.message import Message, MessageType
logger = logging.getLogger(__name__)
class MessageList:
"""从聊天面板中读取消息列表"""
def __init__(self, window: uia.WindowControl) -> None:
self._window = window
self._chat_list: uia.ListControl | None = None
# ------------------------------------------------------------------
# 内部
# ------------------------------------------------------------------
def _ensure_chat_list(self) -> uia.ListControl | None:
if self._chat_list is not None and self._chat_list.Exists(maxSearchSeconds=0.5):
return self._chat_list
try:
# 聊天消息列表 — 搜索较深层级
lst = self._window.ListControl(searchDepth=10)
if lst.Exists(maxSearchSeconds=2):
self._chat_list = lst
return lst
except Exception:
logger.debug("定位聊天消息列表失败")
return None
@staticmethod
def _make_msg_id(conversation: str, sender: str, content: str, ts: float) -> str:
raw = f"{conversation}|{sender}|{content}|{ts}"
return hashlib.md5(raw.encode()).hexdigest()
# ------------------------------------------------------------------
# 公开方法
# ------------------------------------------------------------------
def extract_messages(self, conversation: str = "") -> list[Message]:
"""提取当前聊天面板中的所有可见消息"""
lst = self._ensure_chat_list()
if lst is None:
return []
messages: list[Message] = []
try:
items = lst.GetChildren()
except Exception:
logger.exception("获取消息列表子项失败")
return []
for item in items:
msg = self._parse_item(item, conversation)
if msg is not None:
messages.append(msg)
return messages
def get_latest_messages(self, conversation: str = "", since: float = 0) -> list[Message]:
"""获取指定时间戳之后的新消息"""
all_msgs = self.extract_messages(conversation)
return [m for m in all_msgs if m.timestamp > since]
# ------------------------------------------------------------------
# 解析单条消息控件
# ------------------------------------------------------------------
def _parse_item(self, item: uia.Control, conversation: str) -> Message | None:
"""尝试将一个 ListItem 解析为 Message"""
try:
name = item.Name or ""
if not name:
return None
# 判断是否自己发送 — 通过控件的某些属性
is_self = self._guess_is_self(item)
sender = self._extract_sender(item, is_self)
content = name # Name 通常就是消息内容
msg_type = self._guess_type(item, content)
ts = time.time()
msg_id = self._make_msg_id(conversation, sender, content, ts)
return Message(
msg_id=msg_id,
conversation=conversation,
sender=sender,
content=content,
msg_type=msg_type,
timestamp=ts,
is_self=is_self,
raw_data={"automation_id": item.AutomationId, "class_name": item.ClassName},
)
except Exception:
logger.debug("解析消息控件失败", exc_info=True)
return None
def _guess_is_self(self, item: uia.Control) -> bool:
"""启发式判断消息是否为自己发送"""
try:
# 微信中,自己的消息通常在右侧,ClassName 可能包含 "Self" 或类似标识
cls_name = (item.ClassName or "").lower()
if "self" in cls_name or "right" in cls_name:
return True
# 也可以通过位置判断(控件在父容器右侧)
rect = item.BoundingRectangle
parent_rect = item.GetParentControl().BoundingRectangle if item.GetParentControl() else None
if parent_rect and rect:
mid = (parent_rect.left + parent_rect.right) / 2
return rect.left > mid
except Exception:
pass
return False
def _extract_sender(self, item: uia.Control, is_self: bool) -> str:
"""提取消息发送者名称"""
try:
# 尝试从子控件中获取发送者名称
for child in item.GetChildren():
name = child.Name or ""
# 发送者名称通常较短且不是消息内容本身
if name and len(name) < 30 and child.ControlTypeName in (
"TextControl", "ButtonControl",
):
return name
except Exception:
pass
return "self" if is_self else "unknown"
@staticmethod
def _guess_type(item: uia.Control, content: str) -> MessageType:
"""启发式判断消息类型"""
try:
cls_name = (item.ClassName or "").lower()
if "image" in cls_name or "pic" in cls_name:
return MessageType.IMAGE
if "file" in cls_name:
return MessageType.FILE
if "voice" in cls_name or "audio" in cls_name:
return MessageType.VOICE
if "video" in cls_name:
return MessageType.VIDEO
except Exception:
pass
# 内容判断
if content.startswith("[") and content.endswith("]"):
tag = content.strip("[]")
if "图片" in tag or "image" in tag.lower():
return MessageType.IMAGE
if "文件" in tag or "file" in tag.lower():
return MessageType.FILE
if "语音" in tag or "voice" in tag.lower():
return MessageType.VOICE
if "视频" in tag or "video" in tag.lower():
return MessageType.VIDEO
return MessageType.TEXT