feat: 微信自动化客服(wechatauto-replica) 干净历史导入 - AI 自动回复/语音收发/朋友圈发布
This commit is contained in:
@@ -0,0 +1,305 @@
|
||||
from wechatauto import uia
|
||||
from wechatauto.utils.win32 import (
|
||||
FindWindow,
|
||||
GetAllWindows,
|
||||
SetClipboardText,
|
||||
ReadClipboardData
|
||||
)
|
||||
from wechatauto.utils.tools import (
|
||||
find_window_from_root,
|
||||
find_all_windows_from_root,
|
||||
is_valid_image,
|
||||
get_file_dir,
|
||||
now_time,
|
||||
)
|
||||
from .base import BaseUISubWnd
|
||||
from wechatauto.param import WxParam, WxResponse
|
||||
from wechatauto.logger import wxlog
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
List,
|
||||
Literal
|
||||
)
|
||||
import traceback
|
||||
import shutil
|
||||
import time
|
||||
import os
|
||||
|
||||
|
||||
def _find_popup(ui_cls_name: str, win_cls_name: str, win_name: str = None, pid: int = None, timeout: float = 2):
|
||||
"""查找微信弹窗/浮层窗口。
|
||||
|
||||
优先按 UIA 类名匹配(不受 Qt 版本影响),失败时回退到 win32 窗口类名匹配。
|
||||
"""
|
||||
t0 = time.time()
|
||||
while True:
|
||||
if time.time() - t0 > timeout:
|
||||
break
|
||||
wins = find_all_windows_from_root(pid=pid, uiaclsname=ui_cls_name)
|
||||
if len(wins) > 0:
|
||||
return wins[0]
|
||||
wins = find_all_windows_from_root(classname=win_cls_name, name=win_name, pid=pid)
|
||||
if len(wins) > 0:
|
||||
return wins[0]
|
||||
time.sleep(0.05)
|
||||
return None
|
||||
|
||||
|
||||
class UpdateWindow(BaseUISubWnd):
|
||||
"""微信更新提示窗口"""
|
||||
|
||||
_ui_cls_name: str = "mmui::XView"
|
||||
_win_cls_name: str = "Qt51514QWindowIcon"
|
||||
_win_name: str = "微信"
|
||||
|
||||
def __init__(self):
|
||||
wins = GetAllWindows(name=self._win_name, classname=self._win_cls_name)
|
||||
for win in wins:
|
||||
self.control = uia.ControlFromHandle(win[0])
|
||||
if (
|
||||
(text := self.control.TextControl()).Exists(0)
|
||||
and text.Name == '新版本'
|
||||
):
|
||||
break
|
||||
|
||||
def ignore(self):
|
||||
ignore_btn = self.control.ButtonControl(
|
||||
ClassName="mmui::XOutlineButton",
|
||||
Name="忽略本次更新"
|
||||
)
|
||||
if ignore_btn.Exists(0):
|
||||
ignore_btn.Click()
|
||||
|
||||
|
||||
class Menu(BaseUISubWnd):
|
||||
"""右键菜单窗口"""
|
||||
|
||||
_ui_cls_name: str = "mmui::XMenu"
|
||||
_win_cls_name: str = "Qt51514QWindowToolSaveBits"
|
||||
_win_name: str = "Weixin"
|
||||
|
||||
def __init__(self, parent, timeout=2):
|
||||
self.parent = parent
|
||||
self.root = parent.root
|
||||
self.control = _find_popup(
|
||||
ui_cls_name=self._ui_cls_name,
|
||||
win_cls_name=self._win_cls_name,
|
||||
win_name=self._win_name,
|
||||
pid=self.root.pid,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
@property
|
||||
def option_controls(self):
|
||||
if not self.control:
|
||||
return []
|
||||
return [
|
||||
i for i in
|
||||
self.control.GetChildren()
|
||||
if i.ControlTypeName == 'MenuItemControl'
|
||||
]
|
||||
|
||||
@property
|
||||
def option_names(self):
|
||||
return [c.Name for c in self.option_controls]
|
||||
|
||||
def select(self, item):
|
||||
if not self.control or not self.exists(0):
|
||||
return WxResponse.failure('菜单窗口不存在')
|
||||
if isinstance(item, int):
|
||||
options = self.option_controls
|
||||
if item >= len(options):
|
||||
return WxResponse.failure(f'菜单选项越界:{item}')
|
||||
options[item].Click()
|
||||
return WxResponse.success()
|
||||
|
||||
for c in self.option_controls:
|
||||
if c.Name == item:
|
||||
c.Click()
|
||||
return WxResponse.success()
|
||||
if self.exists(0):
|
||||
self.close()
|
||||
return WxResponse.failure(f'未找到选项:{item}')
|
||||
|
||||
|
||||
class SelectContactWnd(BaseUISubWnd):
|
||||
"""转发消息时的联系人选择窗口"""
|
||||
|
||||
_ui_cls_name: str = "mmui::SessionPickerWindow"
|
||||
_ui_name: str = "微信发送给"
|
||||
_win_cls_name: str = "Qt51514QWindowIcon"
|
||||
_win_name: str = "微信发送给"
|
||||
|
||||
def __init__(self, parent, timeout=2):
|
||||
self.parent = parent
|
||||
self.root = parent.root
|
||||
self.control = _find_popup(
|
||||
ui_cls_name=self._ui_cls_name,
|
||||
win_cls_name=self._win_cls_name,
|
||||
win_name=self._win_name,
|
||||
pid=self.root.pid,
|
||||
timeout=timeout,
|
||||
)
|
||||
if self.control:
|
||||
self.confirm_btn = self.control.ButtonControl(AutomationId="confirm_btn")
|
||||
|
||||
def search(self, keyword, interval=0.1):
|
||||
"""搜索并选择,需完全匹配"""
|
||||
search_control = self.control.EditControl(ClassName="mmui::XValidatorTextEdit")
|
||||
SetClipboardText(keyword)
|
||||
search_control.Click()
|
||||
search_control.SendKeys('{Ctrl}a')
|
||||
search_control.RightClick()
|
||||
menu = Menu(self)
|
||||
menu.select('粘贴')
|
||||
time.sleep(interval)
|
||||
all_controls = []
|
||||
for c, d in uia.WalkControl(self.control):
|
||||
all_controls.append(c)
|
||||
for target in all_controls:
|
||||
if (
|
||||
target.ControlTypeName == 'CheckBoxControl'
|
||||
and target.Name == keyword
|
||||
):
|
||||
target.Click()
|
||||
return True
|
||||
|
||||
def confirm(self):
|
||||
self.confirm_btn.Click()
|
||||
|
||||
def send(self, target, interval=0.1):
|
||||
if isinstance(target, str):
|
||||
target = [target]
|
||||
|
||||
for i in target:
|
||||
self.search(i, interval)
|
||||
|
||||
self.confirm()
|
||||
|
||||
|
||||
class SearchNewFriendWnd(BaseUISubWnd):
|
||||
"""添加朋友窗口"""
|
||||
|
||||
_win_cls_name: str = 'Qt51514QWindowIcon'
|
||||
_win_name: str = "添加朋友"
|
||||
|
||||
def __init__(self):
|
||||
self.control = find_window_from_root(classname=self._win_cls_name, name=self._win_name)
|
||||
if self.control:
|
||||
self.init()
|
||||
|
||||
def init(self):
|
||||
self.apply_btn = self.control.ButtonControl(
|
||||
AutomationId="fixed_height_v_view.content_v_view.ContactProfileBottomUi.add_friend_button",
|
||||
Name="添加到通讯录",
|
||||
ClassName="mmui::XOutlineButton",
|
||||
searchDepth=9
|
||||
)
|
||||
self.search_edit = self.control.EditControl(ClassName="mmui::XValidatorTextEdit", Name="搜索")
|
||||
self.search_btn = self.control.ButtonControl(ClassName="mmui::XOutlineButton", Name="搜索")
|
||||
|
||||
def search(self, keyword):
|
||||
self.search_edit.SendKeys('{Ctrl}a')
|
||||
SetClipboardText(keyword)
|
||||
self.search_edit.SendKeys('{Ctrl}v')
|
||||
self.search_btn.Click()
|
||||
|
||||
def apply(self):
|
||||
if self.apply_btn.Exists(0):
|
||||
self.apply_btn.Click()
|
||||
return WxResponse.success()
|
||||
else:
|
||||
return WxResponse.failure('未找到添加按钮')
|
||||
|
||||
|
||||
class WeChatImage(BaseUISubWnd):
|
||||
"""图片/视频预览窗口"""
|
||||
|
||||
_win_cls_name: str = 'Qt51514QWindowIcon'
|
||||
_win_name: str = "预览"
|
||||
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
self.root = self.parent.root
|
||||
self.control = find_window_from_root(classname=self._win_cls_name, name=self._win_name)
|
||||
if self.control:
|
||||
self.init()
|
||||
|
||||
def init(self):
|
||||
toolbar_control = self.control.GroupControl(ClassName="mmui::PreviewToolbarView")
|
||||
self.tools = {
|
||||
btn.Name: btn for ele in toolbar_control.GetChildren()
|
||||
if (btn := ele.ButtonControl()).Exists(0)
|
||||
}
|
||||
if self.control.WindowControl(ClassName="mmui::XPlayerControlView").Exists(0):
|
||||
self.type = 'video'
|
||||
else:
|
||||
self.type = 'image'
|
||||
|
||||
def save(self, dir_path=None, timeout=10) -> Path:
|
||||
"""保存图片/视频
|
||||
|
||||
Args:
|
||||
dir_path (str): 保存文件夹路径
|
||||
timeout (int, optional): 保存超时时间,默认10秒
|
||||
|
||||
Returns:
|
||||
Path: 文件保存路径,即savepath
|
||||
"""
|
||||
image_sufix = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'pic']
|
||||
if dir_path is None:
|
||||
dir_path = WxParam.DEFAULT_SAVE_PATH
|
||||
t0 = time.time()
|
||||
|
||||
n = 1
|
||||
SetClipboardText('')
|
||||
while True:
|
||||
if time.time() - t0 > timeout:
|
||||
if self.control.Exists(0):
|
||||
self.control.SendKeys('{Esc}')
|
||||
return WxResponse.failure('下载超时')
|
||||
if self.control.TextControl(Name="图片过期或已被清理").Exists(0):
|
||||
return WxResponse.failure('图片过期或已被清理')
|
||||
try:
|
||||
self.tools['更多'].Click()
|
||||
menu = Menu(self.root)
|
||||
menu.select('复制')
|
||||
if self.type == 'video':
|
||||
for _ in range(30):
|
||||
clipboard_data = ReadClipboardData()
|
||||
wxlog.debug(f"读取到剪贴板数据:{clipboard_data.keys()}")
|
||||
if '15' not in clipboard_data:
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
path = clipboard_data['15'][0]
|
||||
break
|
||||
else:
|
||||
clipboard_data = ReadClipboardData()
|
||||
path = clipboard_data['15'][0]
|
||||
if not os.path.exists(path):
|
||||
return WxResponse.failure('微信BUG无法获取该图片,请重新获取')
|
||||
suffix = os.path.splitext(path)[1]
|
||||
if (
|
||||
suffix in image_sufix
|
||||
and not is_valid_image(path)
|
||||
) or not os.path.getsize(path):
|
||||
wxlog.debug("图片格式不正确,删除文件")
|
||||
os.remove(path)
|
||||
continue
|
||||
wxlog.debug(f"读取到图片/视频路径[{os.path.exists(path)}, {os.path.getsize(path)}]:{path}")
|
||||
break
|
||||
except Exception:
|
||||
if n > 3:
|
||||
return WxResponse.failure('微信BUG无法获取该图片,请重新获取')
|
||||
n += 1
|
||||
wxlog.debug(traceback.format_exc())
|
||||
time.sleep(0.1)
|
||||
filename = f"wechatauto_{self.type}_{now_time()}{suffix}"
|
||||
filepath = get_file_dir(dir_path) / filename
|
||||
wxlog.debug(f"保存到文件:{filepath}")
|
||||
shutil.copyfile(path, filepath)
|
||||
SetClipboardText('')
|
||||
if self.control.Exists(0):
|
||||
wxlog.debug("关闭图片窗口")
|
||||
self.control.SendKeys('{Esc}')
|
||||
return filepath
|
||||
Reference in New Issue
Block a user