feat: 微信自动化客服(wechatauto-replica) 干净历史导入 - AI 自动回复/语音收发/朋友圈发布

This commit is contained in:
2026-09-16 10:12:49 +08:00
commit c3b98f4c4e
69 changed files with 19828 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
from .base import Message, BaseMessage, HumanMessage
from .mattr import SystemMessage, FriendMessage, SelfMessage
from .mtype import (
TextMessage,
QuoteMessage,
VoiceMessage,
ImageMessage,
VideoMessage,
FileMessage,
LinkMessage,
LocationMessage,
PersonalCardMessage,
OtherMessage,
)
from .friend import (
FriendTextMessage,
FriendQuoteMessage,
FriendImageMessage,
FriendFileMessage,
FriendVideoMessage,
FriendVoiceMessage,
FriendLinkMessage,
FriendLocationMessage,
FriendPersonalCardMessage,
FriendOtherMessage,
)
from .self import (
SelfTextMessage,
SelfQuoteMessage,
SelfImageMessage,
SelfFileMessage,
SelfVideoMessage,
SelfVoiceMessage,
SelfLinkMessage,
SelfLocationMessage,
SelfPersonalCardMessage,
SelfOtherMessage,
)
from .msg import parse_msg, parse_msg_attr, parse_msg_type
__all__ = [
"Message",
"BaseMessage",
"HumanMessage",
"SystemMessage",
"FriendMessage",
"SelfMessage",
"TextMessage",
"QuoteMessage",
"VoiceMessage",
"ImageMessage",
"VideoMessage",
"FileMessage",
"LinkMessage",
"LocationMessage",
"PersonalCardMessage",
"OtherMessage",
"FriendTextMessage",
"FriendQuoteMessage",
"FriendImageMessage",
"FriendFileMessage",
"FriendVideoMessage",
"FriendVoiceMessage",
"FriendLinkMessage",
"FriendLocationMessage",
"FriendPersonalCardMessage",
"FriendOtherMessage",
"SelfTextMessage",
"SelfQuoteMessage",
"SelfImageMessage",
"SelfFileMessage",
"SelfVideoMessage",
"SelfVoiceMessage",
"SelfLinkMessage",
"SelfLocationMessage",
"SelfPersonalCardMessage",
"SelfOtherMessage",
"parse_msg",
"parse_msg_attr",
"parse_msg_type",
]
+285
View File
@@ -0,0 +1,285 @@
from wechatauto import uia
from wechatauto.ui.component import (
Menu,
SelectContactWnd
)
from wechatauto.utils import uilock
from wechatauto.param import WxParam, WxResponse, PROJECT_NAME
from abc import ABC, abstractmethod
from typing import (
Dict,
List,
Union,
Any,
TYPE_CHECKING,
Iterator,
Tuple
)
from hashlib import md5
if TYPE_CHECKING:
from wechatauto.ui.chatbox import ChatBox
def truncate_string(s: str, n: int = 8) -> str:
s = s.replace('\n', '').strip()
return s if len(s) <= n else s[:n] + '...'
class Message:
"""消息对象基类
该类不会直接实例化,而是作为所有消息类型的基类提供
常用的工具方法。实际的属性均由子类在 ``__init__`` 中
动态注入。
"""
_EXCLUDE_FIELDS = {"control", "parent", "root"}
# region --- 迭代/映射相关 -------------------------------------------------
def _iter_public_items(self) -> Iterator[Tuple[str, Any]]:
"""遍历当前消息可公开的字段"""
if not hasattr(self, "__dict__"):
return
seen = set()
for key, value in self.__dict__.items():
if key.startswith("_") or key in self._EXCLUDE_FIELDS:
continue
if key == "hash" and not WxParam.MESSAGE_HASH:
continue
seen.add(key)
yield key, value
# 补充类级字段(如 type/attr),保证 to_dict/match 可用
for key in ('type', 'attr'):
if key in seen:
continue
if any(key in cls.__dict__ for cls in type(self).__mro__):
yield key, getattr(self, key)
def __iter__(self) -> Iterator[str]:
for key, _ in self._iter_public_items():
yield key
def __len__(self) -> int:
return sum(1 for _ in self._iter_public_items())
def __getitem__(self, item: str) -> Any:
for key, value in self._iter_public_items():
if key == item:
return value
raise KeyError(item)
def __contains__(self, key: object) -> bool:
if not isinstance(key, str):
return False
return any(field == key for field, _ in self._iter_public_items())
# endregion ----------------------------------------------------------------
# region --- 字段访问 -------------------------------------------------------
def keys(self) -> Tuple[str, ...]:
return tuple(key for key, _ in self._iter_public_items())
def values(self) -> Tuple[Any, ...]:
return tuple(value for _, value in self._iter_public_items())
def items(self) -> Tuple[Tuple[str, Any], ...]:
return tuple(self._iter_public_items())
def get(self, key: str, default: Any = None) -> Any:
for field, value in self._iter_public_items():
if field == key:
return value
return default
def to_dict(self) -> Dict[str, Any]:
return dict(self._iter_public_items())
def copy(self) -> Dict[str, Any]:
return self.to_dict().copy()
# endregion ----------------------------------------------------------------
# region --- 状态判断 -------------------------------------------------------
def match(self, **conditions: Any) -> bool:
"""判断当前消息是否同时满足给定的字段条件"""
data = self.to_dict()
return all(data.get(key) == value for key, value in conditions.items())
@property
def is_self(self) -> bool:
return getattr(self, "attr", None) == "self"
@property
def is_friend(self) -> bool:
return getattr(self, "attr", None) == "friend"
@property
def is_system(self) -> bool:
return getattr(self, "attr", None) == "system"
# endregion ----------------------------------------------------------------
# region --- 魔术方法 -------------------------------------------------------
def __str__(self) -> str:
content = getattr(self, "content", None)
if content is None:
return super().__str__()
return str(content)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Message):
return NotImplemented
self_id = getattr(self, "id", None)
other_id = getattr(other, "id", None)
if self_id is not None and other_id is not None:
return self_id == other_id
if WxParam.MESSAGE_HASH:
return getattr(self, "hash", None) == getattr(other, "hash", None)
return self is other
def __hash__(self) -> int:
msg_id = getattr(self, "id", None)
if msg_id is not None:
return hash(msg_id)
if WxParam.MESSAGE_HASH:
return hash(getattr(self, "hash", None))
return super().__hash__()
# endregion ----------------------------------------------------------------
class BaseMessage(Message, ABC):
type: str = 'base'
attr: str = 'base'
control: uia.Control
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
self.parent = parent
self.control = control
self.direction = additonal_attr.get('direction', None)
self.distince = additonal_attr.get('direction_distence', None)
self.root = parent.root
self.id = self.control.runtimeid
self.content = self.control.Name
rect = self.control.BoundingRectangle
self.hash_text = f'({rect.height()},{rect.width()}){self.content}'
self.hash = md5(self.hash_text.encode()).hexdigest()
def __repr__(self):
cls_name = self.__class__.__name__
content = truncate_string(self.content)
return f"<{PROJECT_NAME} - {cls_name}({content}) at {hex(id(self))}>"
def roll_into_view(self):
if not self.exists():
return WxResponse.failure('消息目标控件不存在,无法滚动至显示窗口')
if uia.RollIntoView(
self.parent.msgbox,
self.control
) == 'not exist':
return WxResponse.failure('消息目标控件不存在,无法滚动至显示窗口')
return WxResponse.success('成功')
def exists(self):
if self.control.Exists(0) and self.control.BoundingRectangle.height() > 0:
return True
return False
class HumanMessage(BaseMessage, ABC):
attr = 'human'
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)
@abstractmethod
def _click(self, x, y, right=False): ...
@abstractmethod
def _bias(self): ...
def click(self):
self._click(right=False, x=self._bias * 2, y=WxParam.DEFAULT_MESSAGE_YBIAS)
def right_click(self):
self._click(right=True, x=self._bias, y=WxParam.DEFAULT_MESSAGE_YBIAS)
@uilock
def select_option(self, option: str, timeout=2) -> WxResponse:
if not self.exists():
return WxResponse.failure('消息对象已失效')
self._click(right=True, x=self._bias * 2, y=WxParam.DEFAULT_MESSAGE_YBIAS)
if menu := Menu(self, timeout):
return menu.select(option)
else:
return WxResponse.failure('操作失败')
@uilock
def forward(
self,
targets: Union[List[str], str],
timeout: int = 3,
interval: float = 0.1
) -> WxResponse:
"""转发消息
Args:
targets (Union[List[str], str]): 目标用户列表
timeout (int, optional): 超时时间,单位为秒,若为None则不启用超时设置
interval (float): 选择联系人时间间隔
Returns:
WxResponse: 调用结果
"""
if not self.exists():
return WxResponse.failure('消息对象已失效')
if not self.select_option('转发...', timeout=timeout):
return WxResponse.failure('当前消息无法转发')
select_wnd = SelectContactWnd(self)
return select_wnd.send(targets, interval=interval)
@uilock
def quote(
self, text: str,
at: Union[List[str], str] = None,
timeout: int = 3
) -> WxResponse:
"""引用消息
Args:
text (str): 引用内容
at (List[str], optional): @用户列表
timeout (int, optional): 超时时间,单位为秒,若为None则不启用超时设置
Returns:
WxResponse: 调用结果
"""
if not self.exists():
return WxResponse.failure('消息对象已失效')
if not self.select_option('引用', timeout=timeout):
return WxResponse.failure('当前消息无法引用')
if at:
self.parent.input_at(at)
return self.parent.send_text(text)
+112
View File
@@ -0,0 +1,112 @@
from .mattr import *
from .mtype import *
class FriendTextMessage(FriendMessage, TextMessage):
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)
class FriendQuoteMessage(FriendMessage, QuoteMessage):
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)
class FriendImageMessage(FriendMessage, ImageMessage):
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)
class FriendEmojiMessage(FriendMessage, EmojiMessage):
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)
class FriendFileMessage(FriendMessage, FileMessage):
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)
class FriendVideoMessage(FriendMessage, VideoMessage):
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)
class FriendVoiceMessage(FriendMessage, VoiceMessage):
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)
class FriendLinkMessage(FriendMessage, LinkMessage):
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)
class FriendLocationMessage(FriendMessage, LocationMessage):
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)
class FriendPersonalCardMessage(FriendMessage, PersonalCardMessage):
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)
class FriendOtherMessage(FriendMessage, OtherMessage):
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)
+79
View File
@@ -0,0 +1,79 @@
from .base import (
BaseMessage,
HumanMessage
)
from wechatauto import uia
from wechatauto.param import (
WxParam,
WxResponse,
PROJECT_NAME
)
from typing import (
Dict,
List,
Any,
TYPE_CHECKING
)
if TYPE_CHECKING:
from wechatauto.ui.chatbox import ChatBox
class SystemMessage(BaseMessage):
attr = 'system'
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)
self.sender = 'system'
self.sender_remark = 'system'
class FriendMessage(HumanMessage):
attr = 'friend'
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)
def _click(self, x, y, right=False):
self.roll_into_view()
if right:
self.control.RightClick(x=x, y=y, ratioX=0, ratioY=0)
else:
self.control.Click(ratioX=0, ratioY=0)
@property
def _bias(self):
return WxParam.DEFAULT_MESSAGE_XBIAS
class SelfMessage(HumanMessage):
attr = 'self'
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)
def _click(self, x, y, right=False):
self.roll_into_view()
if right:
self.control.RightClick(x=x, y=y, ratioX=1, ratioY=0)
else:
self.control.Click(x=x, y=y, ratioX=1, ratioY=0)
@property
def _bias(self):
return -WxParam.DEFAULT_MESSAGE_XBIAS
+140
View File
@@ -0,0 +1,140 @@
from wechatauto.utils.tools import (
detect_message_direction
)
from wechatauto import uia
from .mattr import (
SystemMessage,
FriendMessage,
SelfMessage
)
from .mtype import *
from . import self as selfmsg
from . import friend as friendmsg
from typing import (
TYPE_CHECKING,
Literal,
Dict,
Any
)
import os
import re
if TYPE_CHECKING:
from wechatauto.ui.chatbox import ChatBox
def parse_msg_attr(
control: uia.Control,
parent: 'ChatBox'
):
msg_direction_hash = {
'left': 'friend',
'right': 'self'
}
if control.AutomationId:
msg_screenshot = control.ScreenShot()
msg_direction, msg_direction_distence = detect_message_direction(msg_screenshot)
msg_attr = msg_direction_hash.get(msg_direction)
os.remove(msg_screenshot)
additonal_attr = {
'direction': msg_direction,
'direction_distence': msg_direction_distence
}
else:
msg_attr = 'system'
if msg_attr == 'system':
return SystemMessage(control, parent)
elif msg_attr == 'friend':
return parse_msg_type(control, parent, 'Friend', additonal_attr)
elif msg_attr == 'self':
return parse_msg_type(control, parent, 'Self', additonal_attr)
def parse_msg_type(
control: uia.Control,
parent,
attr: Literal['Self', 'Friend'],
additonal_attr: Dict[str, Any]
):
"""
多层次消息类型识别算法
基于ClassName、Name等多重验证确保识别准确性
"""
if attr == 'Friend':
msgtype = friendmsg
else:
msgtype = selfmsg
msg_text = control.Name
msg_classname = control.ClassName
msg_automation_id = control.AutomationId
# 第一层:ClassName强特征识别(最可靠)
classname_result = _classify_by_classname(msg_classname)
if classname_result:
return getattr(msgtype, f'{attr}{classname_result}')(control, parent, additonal_attr)
# 第二层:基于ClassName分类后的详细识别
if msg_classname == "mmui::ChatBubbleItemView":
# Name前缀特征识别
prefix_result = _classify_by_name_prefix(msg_text)
if prefix_result:
return getattr(msgtype, f'{attr}{prefix_result}')(control, parent, additonal_attr)
# 图片消息处理
if msg_text == '图片':
return getattr(msgtype, f'{attr}ImageMessage')(control, parent, additonal_attr)
# 如果都不匹配,归类为其他消息
return getattr(msgtype, f'{attr}OtherMessage')(control, parent, additonal_attr)
elif msg_classname == "mmui::ChatTextItemView":
# 第三层:引用消息处理
if _is_quote_message(msg_text):
return getattr(msgtype, f'{attr}QuoteMessage')(control, parent, additonal_attr)
else:
return getattr(msgtype, f'{attr}TextMessage')(control, parent, additonal_attr)
return getattr(msgtype, f'{attr}OtherMessage')(control, parent, additonal_attr)
def _classify_by_classname(classname: str) -> str:
classname_mapping = {
"mmui::ChatVoiceItemView": "VoiceMessage",
"mmui::ChatPersonalCardItemView": "PersonalCardMessage",
}
return classname_mapping.get(classname, "")
def _classify_by_name_prefix(name: str) -> str:
if name.startswith("[动画表情]") or name.startswith("[动画表情"):
return "EmojiMessage"
if name.startswith("[链接]"):
return "LinkMessage"
elif name.startswith("位置"):
return "LocationMessage"
elif name.startswith("文件\n"):
return "FileMessage"
elif name.startswith("视频"):
return "VideoMessage"
return ""
def _is_quote_message(name: str) -> bool:
quote_pattern = r'^(.*?)\s*\n引用\s+(.+?)\s+的消息\s*:\s*(.*)$'
return bool(re.search(quote_pattern, name, re.DOTALL))
def parse_msg(
control: uia.Control,
parent
):
return parse_msg_attr(control, parent)
File diff suppressed because it is too large Load Diff
+112
View File
@@ -0,0 +1,112 @@
from .mattr import *
from .mtype import *
class SelfTextMessage(SelfMessage, TextMessage):
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)
class SelfQuoteMessage(SelfMessage, QuoteMessage):
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)
class SelfImageMessage(SelfMessage, ImageMessage):
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)
class SelfEmojiMessage(SelfMessage, EmojiMessage):
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)
class SelfFileMessage(SelfMessage, FileMessage):
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)
class SelfVideoMessage(SelfMessage, VideoMessage):
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)
class SelfVoiceMessage(SelfMessage, VoiceMessage):
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)
class SelfLinkMessage(SelfMessage, LinkMessage):
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)
class SelfLocationMessage(SelfMessage, LocationMessage):
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)
class SelfPersonalCardMessage(SelfMessage, PersonalCardMessage):
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)
class SelfOtherMessage(SelfMessage, OtherMessage):
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)