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
+305
View File
@@ -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