feat: 微信自动化客服(wechatauto-replica) 干净历史导入 - AI 自动回复/语音收发/朋友圈发布
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
from __future__ import annotations
|
||||
from wechatauto import uia
|
||||
from wechatauto.param import (
|
||||
WxParam,
|
||||
WxResponse,
|
||||
)
|
||||
from wechatauto.languages import MENU_OPTIONS, get_lang
|
||||
from wechatauto.ui.component import Menu
|
||||
from wechatauto.utils.win32 import SetClipboardText
|
||||
from wechatauto.logger import wxlog
|
||||
import time
|
||||
from typing import (
|
||||
Union,
|
||||
List
|
||||
)
|
||||
import re
|
||||
|
||||
|
||||
class SessionBox:
|
||||
"""会话列表区域"""
|
||||
|
||||
def __init__(self, control, parent):
|
||||
self.control: uia.Control = control
|
||||
self.root = parent.root
|
||||
self.parent = parent
|
||||
self.init()
|
||||
|
||||
def init(self):
|
||||
self.searchbox = self.control.GroupControl(ClassName="mmui::XSearchField").EditControl()
|
||||
self.session_list = self.control.GroupControl(ClassName="mmui::ChatSessionList").\
|
||||
ListControl(ClassName="mmui::XTableView", Name="会话")
|
||||
self.search_content = self.parent.control.WindowControl(ClassName="mmui::SearchContentPopover")
|
||||
|
||||
def roll_up(self, n: int = 5):
|
||||
self.control.MiddleClick()
|
||||
self.control.WheelUp(wheelTimes=n)
|
||||
|
||||
def roll_down(self, n: int = 5):
|
||||
self.control.MiddleClick()
|
||||
self.control.WheelDown(wheelTimes=n)
|
||||
|
||||
def get_session(self) -> List['SessionElement']:
|
||||
if self.session_list.Exists(0):
|
||||
return [SessionElement(i, self) for i in self.session_list.GetChildren()]
|
||||
else:
|
||||
return []
|
||||
|
||||
def search(
|
||||
self,
|
||||
keywords: str,
|
||||
force: bool = False,
|
||||
force_wait: Union[float, int] = 0.5
|
||||
):
|
||||
self.searchbox.RightClick()
|
||||
SetClipboardText(keywords)
|
||||
menu = Menu(self)
|
||||
menu.select('粘贴')
|
||||
self.searchbox.MiddleClick()
|
||||
|
||||
search_result = self.search_content.ListControl()
|
||||
|
||||
if force:
|
||||
time.sleep(force_wait)
|
||||
|
||||
return [SearchResultElement(i) for i in search_result.GetChildren()]
|
||||
|
||||
def switch_chat(
|
||||
self,
|
||||
keywords: str,
|
||||
exact: bool = True,
|
||||
force: bool = False,
|
||||
force_wait: Union[float, int] = 0.5
|
||||
):
|
||||
wxlog.debug(f"切换聊天窗口: {keywords}, {exact}, {force}, {force_wait}")
|
||||
search_box = self.search_content.ListControl()
|
||||
search_result = self.search(keywords, force, force_wait)
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < WxParam.SEARCH_CHAT_TIMEOUT:
|
||||
results = []
|
||||
search_result_items = search_box.GetChildren()
|
||||
for search_result_item in search_result_items:
|
||||
text: str = search_result_item.Name
|
||||
if exact:
|
||||
if text == keywords:
|
||||
search_result_item.Click()
|
||||
return keywords
|
||||
elif (
|
||||
' 微信号: ' in text
|
||||
and (split := text.split(' 微信号: '))[-1].lower() == keywords.lower()
|
||||
):
|
||||
search_result_item.Click()
|
||||
return split[0]
|
||||
elif (
|
||||
' 昵称: ' in text
|
||||
and (split := text.split(' 昵称: '))[-1].lower() == keywords.lower()
|
||||
):
|
||||
search_result_item.Click()
|
||||
return split[0]
|
||||
else:
|
||||
if keywords in text:
|
||||
search_result_item.Click()
|
||||
return text
|
||||
|
||||
if self.search_content.Exists(0):
|
||||
self.control.MiddleClick()
|
||||
|
||||
def open_separate_window(self, name: str):
|
||||
wxlog.debug(f"打开独立窗口: {name}")
|
||||
realname = self.switch_chat(name)
|
||||
if not realname:
|
||||
return WxResponse.failure('未找到会话')
|
||||
time.sleep(0.3)
|
||||
while True:
|
||||
session = [i for i in self.get_session() if uia.IsElementInWindow(self.session_list, i.control)][0]
|
||||
if session.content.startswith(realname):
|
||||
break
|
||||
session.double_click()
|
||||
return WxResponse.success(data={'nickname': realname})
|
||||
|
||||
def go_top(self):
|
||||
wxlog.debug("回到会话列表顶部")
|
||||
self.control.MiddleClick()
|
||||
self.control.SendKeys('{Home}')
|
||||
|
||||
def go_bottom(self):
|
||||
wxlog.debug("回到会话列表底部")
|
||||
self.control.MiddleClick()
|
||||
self.control.SendKeys('{End}')
|
||||
|
||||
|
||||
class SessionElement:
|
||||
"""会话列表中的单条会话"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
control: uia.Control,
|
||||
parent: SessionBox,
|
||||
):
|
||||
self.root = parent.root
|
||||
self.parent = parent
|
||||
self.control = control
|
||||
self.content = control.Name
|
||||
|
||||
@property
|
||||
def texts(self) -> List[str]:
|
||||
"""拆分当前会话控件中的文本行"""
|
||||
|
||||
return [
|
||||
line for line in str(self.content).split('\n')
|
||||
if line and line.strip()
|
||||
]
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""会话名称"""
|
||||
|
||||
if self.texts:
|
||||
return self.texts[0]
|
||||
return ''
|
||||
|
||||
@property
|
||||
def unread_count(self) -> int:
|
||||
"""未读消息数量"""
|
||||
|
||||
unread_pattern = re.compile(r'\[(\d+)条\]')
|
||||
for text in self.texts:
|
||||
if match := unread_pattern.search(text):
|
||||
return int(match.group(1))
|
||||
return 0
|
||||
|
||||
def _menu_option_text(self, option_key: str) -> str:
|
||||
return get_lang(MENU_OPTIONS, option_key)
|
||||
|
||||
def select_menu_option(self, option_key: str, wait=0.3):
|
||||
"""根据配置语言选择菜单项"""
|
||||
|
||||
option_text = self._menu_option_text(option_key)
|
||||
return self.select_option(option_text, wait)
|
||||
|
||||
def __repr__(self):
|
||||
content = str(self.content).replace('\n', ' ')
|
||||
if len(content) > 5:
|
||||
content = content[:5] + '...'
|
||||
return f"<wechatauto Session Element({content})>"
|
||||
|
||||
def roll_into_view(self):
|
||||
uia.RollIntoView(self.control.GetParentControl(), self.control)
|
||||
|
||||
def _click(self, right: bool = False, double: bool = False):
|
||||
self.roll_into_view()
|
||||
if right:
|
||||
self.control.RightClick()
|
||||
elif double:
|
||||
self.control.DoubleClick()
|
||||
else:
|
||||
self.control.Click()
|
||||
|
||||
def click(self):
|
||||
self._click()
|
||||
|
||||
def right_click(self):
|
||||
self._click(right=True)
|
||||
|
||||
def double_click(self):
|
||||
self._click()
|
||||
self._click(double=True)
|
||||
|
||||
def select_option(self, option: str, wait=0.3):
|
||||
self.roll_into_view()
|
||||
self.control.RightClick()
|
||||
time.sleep(wait)
|
||||
menu = Menu(self.parent)
|
||||
return menu.select(option)
|
||||
|
||||
def pin(self):
|
||||
"""置顶聊天"""
|
||||
|
||||
return self.select_menu_option('置顶')
|
||||
|
||||
def unpin(self):
|
||||
"""取消置顶聊天"""
|
||||
|
||||
return self.select_menu_option('取消置顶')
|
||||
|
||||
def mark_unread(self):
|
||||
"""标记为未读"""
|
||||
|
||||
return self.select_menu_option('标为未读')
|
||||
|
||||
def toggle_mute(self):
|
||||
"""切换消息免打扰状态"""
|
||||
|
||||
return self.select_menu_option('消息免打扰')
|
||||
|
||||
def open_in_separate_window(self):
|
||||
"""在独立窗口中打开会话"""
|
||||
|
||||
return self.select_menu_option('在独立窗口打开')
|
||||
|
||||
def hide(self):
|
||||
"""不显示聊天"""
|
||||
|
||||
return self.select_menu_option('不显示聊天')
|
||||
|
||||
def delete(self):
|
||||
"""删除聊天"""
|
||||
|
||||
return self.select_menu_option('删除聊天')
|
||||
|
||||
|
||||
class SearchResultElement:
|
||||
"""搜索结果中的单条结果"""
|
||||
|
||||
def __init__(self, control):
|
||||
self.control = control
|
||||
self.content = control.Name
|
||||
self.type = control.ClassName
|
||||
|
||||
def __repr__(self):
|
||||
content = str(self.content).replace('\n', ' ')
|
||||
if len(content) > 5:
|
||||
content = content[:5] + '...'
|
||||
return f"<wechatauto Search Element({content})>"
|
||||
|
||||
def get_all_text(self):
|
||||
return [
|
||||
line for line in str(self.content).split('\n')
|
||||
if line and line.strip()
|
||||
]
|
||||
|
||||
def click(self):
|
||||
uia.RollIntoView(self.control.GetParentControl(), self.control)
|
||||
self.control.Click()
|
||||
|
||||
def close(self):
|
||||
self.control.SendKeys('{Esc}')
|
||||
Reference in New Issue
Block a user