feat: 微信自动化客服(wechatauto-replica) 干净历史导入 - AI 自动回复/语音收发/朋友圈发布
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
from .base import BaseUISubWnd, BaseUIWnd
|
||||
from .navigationbox import NavigationBox
|
||||
from .sessionbox import SessionBox
|
||||
from .chatbox import ChatBox
|
||||
from wechatauto.utils.win32 import (
|
||||
FindWindow,
|
||||
GetAllWindows,
|
||||
GetPathByHwnd,
|
||||
get_windows_by_pid
|
||||
)
|
||||
from wechatauto.param import WxParam, WxResponse, PROJECT_NAME
|
||||
from wechatauto.logger import wxlog
|
||||
from wechatauto import uia
|
||||
from typing import (
|
||||
Union,
|
||||
List,
|
||||
Literal
|
||||
)
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def find_wechat_windows(ui_cls_name: str) -> List[uia.Control]:
|
||||
"""从所有顶层窗口中查找指定 UIA 类名的微信窗口控件。
|
||||
|
||||
Args:
|
||||
ui_cls_name: 目标 UIA 类名,如 ``mmui::MainWindow``
|
||||
|
||||
Returns:
|
||||
匹配到的窗口控件列表
|
||||
"""
|
||||
targets = []
|
||||
for hwnd, clsname, winname in GetAllWindows():
|
||||
# 跳过 Qt 内部消息泵等隐藏窗口,其 UIA 服务可能阻塞调用
|
||||
if clsname.startswith(uia.QT_INTERNAL_WIN_CLASS_PREFIX):
|
||||
continue
|
||||
if uia.GetUiClassNameWithTimeout(hwnd) != ui_cls_name:
|
||||
continue
|
||||
try:
|
||||
control = uia.ControlFromHandle(hwnd)
|
||||
except Exception:
|
||||
continue
|
||||
if control is not None:
|
||||
targets.append(control)
|
||||
return targets
|
||||
|
||||
|
||||
class WeChatSubWnd(BaseUISubWnd):
|
||||
"""微信独立聊天子窗口"""
|
||||
|
||||
_ui_cls_name: str = 'mmui::FramelessMainWindow'
|
||||
_win_cls_name: str = 'Qt51514QWindowIcon'
|
||||
_chat_api: ChatBox = None
|
||||
nickname: str = ''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
key: Union[str, int],
|
||||
parent: 'WeChatMainWnd',
|
||||
timeout: int = 3
|
||||
):
|
||||
self.root = self
|
||||
self.parent = parent
|
||||
if isinstance(key, str):
|
||||
hwnd = FindWindow(classname=self._win_cls_name, name=key, timeout=timeout)
|
||||
else:
|
||||
hwnd = key
|
||||
self.control = uia.ControlFromHandle(hwnd)
|
||||
if self.control is not None:
|
||||
chatbox_control = self.control.\
|
||||
GroupControl(ClassName="mmui::ChatMessagePage").\
|
||||
CustomControl(ClassName="mmui::XSplitterView")
|
||||
self._chat_api = ChatBox(chatbox_control, self)
|
||||
self.nickname = self.control.Name
|
||||
|
||||
def __repr__(self):
|
||||
return f'<{PROJECT_NAME} - {self.__class__.__name__} object("{self.nickname}")>'
|
||||
|
||||
@property
|
||||
def pid(self):
|
||||
if not hasattr(self, '_pid'):
|
||||
self._pid = self.control.ProcessId
|
||||
return self._pid
|
||||
|
||||
def _get_chatbox(
|
||||
self,
|
||||
nickname: str = None,
|
||||
exact: bool = False
|
||||
) -> ChatBox:
|
||||
return self._chat_api
|
||||
|
||||
def _get_windows(self):
|
||||
wins = []
|
||||
for hwnd in get_windows_by_pid(self.pid):
|
||||
try:
|
||||
wins.append(uia.ControlFromHandle(hwnd))
|
||||
except Exception:
|
||||
pass
|
||||
ignore_cls = ['basepopupshadow', 'popupshadow']
|
||||
return [win for win in wins if win.ClassName not in ignore_cls]
|
||||
|
||||
def chat_info(self):
|
||||
return self._chat_api.get_info()
|
||||
|
||||
def send_msg(
|
||||
self,
|
||||
msg: str,
|
||||
who: str = None,
|
||||
clear: bool = True,
|
||||
at: Union[str, List[str]] = None,
|
||||
exact: bool = False,
|
||||
) -> WxResponse:
|
||||
chatbox = self._get_chatbox(who, exact)
|
||||
if chatbox is None:
|
||||
return WxResponse.failure(f"未找到聊天窗口:{who}")
|
||||
return chatbox.send_msg(msg, clear, at)
|
||||
|
||||
def send_files(
|
||||
self,
|
||||
filepath,
|
||||
who=None,
|
||||
exact=False
|
||||
) -> WxResponse:
|
||||
chatbox = self._get_chatbox(who, exact)
|
||||
if chatbox is None:
|
||||
return WxResponse.failure(f"未找到聊天窗口:{who}")
|
||||
return chatbox.send_file(filepath)
|
||||
|
||||
def get_msgs(self):
|
||||
chatbox = self._get_chatbox()
|
||||
if chatbox:
|
||||
return chatbox.get_msgs()
|
||||
return []
|
||||
|
||||
def get_new_msgs(self):
|
||||
return self._get_chatbox().get_new_msgs()
|
||||
|
||||
def get_msg_by_id(self, msg_id):
|
||||
chatbox = self._get_chatbox()
|
||||
if chatbox:
|
||||
return chatbox.get_msg_by_id(msg_id)
|
||||
|
||||
def get_msg_by_hash(self, msg_hash: str):
|
||||
chatbox = self._get_chatbox()
|
||||
if chatbox:
|
||||
return chatbox.get_msg_by_hash(msg_hash)
|
||||
|
||||
def get_last_msg(self):
|
||||
chatbox = self._get_chatbox()
|
||||
if chatbox:
|
||||
return chatbox.get_last_msg()
|
||||
|
||||
|
||||
class WeChatMainWnd(WeChatSubWnd):
|
||||
"""微信主窗口"""
|
||||
|
||||
_ui_cls_name: str = 'mmui::MainWindow'
|
||||
_win_cls_name: str = 'Qt51514QWindowIcon'
|
||||
_ui_name: str = '微信'
|
||||
|
||||
def __init__(self, nickname: str = None, hwnd: int = None):
|
||||
self.root = self
|
||||
self.parent = self
|
||||
if hwnd:
|
||||
self._setup_ui(hwnd)
|
||||
else:
|
||||
# 优先按 UIA 类名定位主窗口,兼容不同 Qt 版本的客户端
|
||||
controls = find_wechat_windows(self._ui_cls_name)
|
||||
if not controls:
|
||||
controls = find_wechat_windows(self._ui_cls_name.replace('MainWindow', 'FramelessMainWindow'))
|
||||
if not controls:
|
||||
raise Exception('未找到已登录的微信主窗口')
|
||||
target = None
|
||||
for control in controls:
|
||||
if nickname and control.Name != nickname:
|
||||
continue
|
||||
target = control
|
||||
break
|
||||
if target is None:
|
||||
target = controls[0]
|
||||
self._setup_ui(target.NativeWindowHandle)
|
||||
|
||||
print(f'初始化成功,获取到已登录窗口:{self.nickname}')
|
||||
|
||||
def _setup_ui(self, hwnd: int):
|
||||
self.HWND = hwnd
|
||||
self.control = uia.ControlFromHandle(hwnd)
|
||||
if self.control is not None:
|
||||
navigation_control = self.control.\
|
||||
ToolBarControl(ClassName="mmui::MainTabBar", AutomationId='main_tabbar')
|
||||
sessionbox_control = self.control.\
|
||||
GroupControl(ClassName="mmui::ChatMasterView")
|
||||
chatbox_control = self.control.\
|
||||
GroupControl(ClassName="mmui::ChatMessagePage").\
|
||||
CustomControl(ClassName="mmui::XSplitterView")
|
||||
self._navigation_api = NavigationBox(navigation_control, self)
|
||||
self._session_api = SessionBox(sessionbox_control, self)
|
||||
self._chat_api = ChatBox(chatbox_control, self)
|
||||
self.nickname = self.control.Name
|
||||
|
||||
def __repr__(self):
|
||||
return f'<{PROJECT_NAME} - {self.__class__.__name__} object("{self.nickname}")>'
|
||||
|
||||
def _get_wx_path(self):
|
||||
return GetPathByHwnd(self.HWND)
|
||||
|
||||
def _get_wx_dir(self):
|
||||
wxdir = os.path.dirname(self._get_wx_path())
|
||||
for d in os.listdir(wxdir):
|
||||
if re.match(r'\d+\.\d+\.\d+\.\d+', d):
|
||||
return os.path.join(wxdir, d)
|
||||
|
||||
def _get_chatbox(
|
||||
self,
|
||||
nickname: str = None,
|
||||
exact: bool = False
|
||||
) -> ChatBox:
|
||||
if nickname and (chatbox := WeChatSubWnd(nickname, self, timeout=0)).control:
|
||||
return chatbox._chat_api
|
||||
else:
|
||||
if nickname:
|
||||
switch_result = self._session_api.switch_chat(keywords=nickname, exact=exact)
|
||||
if not switch_result:
|
||||
return None
|
||||
if self._chat_api.msgbox.Exists(0.5):
|
||||
return self._chat_api
|
||||
|
||||
def switch_chat(
|
||||
self,
|
||||
keywords: str,
|
||||
exact: bool = True,
|
||||
force: bool = False,
|
||||
force_wait: Union[float, int] = 0.5
|
||||
):
|
||||
return self._session_api.switch_chat(keywords, exact, force, force_wait)
|
||||
|
||||
def get_all_sub_wnds(self):
|
||||
sub_wxs = GetAllWindows(classname=WeChatSubWnd._win_cls_name)
|
||||
return [
|
||||
sub_win
|
||||
for i in sub_wxs
|
||||
if (
|
||||
uia.ControlFromHandle(i[0]).ClassName == WeChatSubWnd._ui_cls_name
|
||||
and (sub_win := WeChatSubWnd(i[0], self)).pid == self.pid
|
||||
)
|
||||
]
|
||||
|
||||
def get_sub_wnd(self, who: str):
|
||||
subwins = self.get_all_sub_wnds()
|
||||
for subwin in subwins:
|
||||
if subwin.nickname == who:
|
||||
return subwin
|
||||
|
||||
def open_separate_window(self, keywords: str) -> WeChatSubWnd:
|
||||
if subwin := self.get_sub_wnd(keywords):
|
||||
wxlog.debug(f"{keywords} 获取到已存在的子窗口: {subwin}")
|
||||
return subwin
|
||||
if nickname := self._session_api.switch_chat(keywords):
|
||||
wxlog.debug(f"{keywords} 切换到聊天窗口: {nickname}")
|
||||
if subwin := self.get_sub_wnd(nickname):
|
||||
wxlog.debug(f"{nickname} 获取到已存在的子窗口: {subwin}")
|
||||
return subwin
|
||||
else:
|
||||
keywords = nickname
|
||||
if result := self._session_api.open_separate_window(keywords):
|
||||
find_nickname = result['data'].get('nickname', keywords)
|
||||
return WeChatSubWnd(find_nickname, self)
|
||||
Reference in New Issue
Block a user