pyxllib 0.3.197__py3-none-any.whl → 0.3.200__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. pyxllib/__init__.py +21 -21
  2. pyxllib/algo/__init__.py +8 -8
  3. pyxllib/algo/disjoint.py +54 -54
  4. pyxllib/algo/geo.py +541 -541
  5. pyxllib/algo/intervals.py +964 -964
  6. pyxllib/algo/matcher.py +389 -389
  7. pyxllib/algo/newbie.py +166 -166
  8. pyxllib/algo/pupil.py +629 -629
  9. pyxllib/algo/shapelylib.py +67 -67
  10. pyxllib/algo/specialist.py +241 -241
  11. pyxllib/algo/stat.py +494 -494
  12. pyxllib/algo/treelib.py +149 -149
  13. pyxllib/algo/unitlib.py +66 -66
  14. pyxllib/autogui/__init__.py +5 -5
  15. pyxllib/autogui/activewin.py +246 -246
  16. pyxllib/autogui/all.py +9 -9
  17. pyxllib/autogui/autogui.py +852 -852
  18. pyxllib/autogui/uiautolib.py +362 -362
  19. pyxllib/autogui/virtualkey.py +102 -102
  20. pyxllib/autogui/wechat.py +827 -827
  21. pyxllib/autogui/wechat_msg.py +421 -421
  22. pyxllib/autogui/wxautolib.py +84 -84
  23. pyxllib/cv/__init__.py +5 -5
  24. pyxllib/cv/expert.py +267 -267
  25. pyxllib/cv/imfile.py +159 -159
  26. pyxllib/cv/imhash.py +39 -39
  27. pyxllib/cv/pupil.py +9 -9
  28. pyxllib/cv/rgbfmt.py +1525 -1525
  29. pyxllib/cv/slidercaptcha.py +137 -137
  30. pyxllib/cv/trackbartools.py +251 -251
  31. pyxllib/cv/xlcvlib.py +1040 -1040
  32. pyxllib/cv/xlpillib.py +423 -423
  33. pyxllib/data/echarts.py +240 -240
  34. pyxllib/data/jsonlib.py +89 -89
  35. pyxllib/data/oss.py +72 -72
  36. pyxllib/data/pglib.py +1127 -1127
  37. pyxllib/data/sqlite.py +568 -568
  38. pyxllib/data/sqllib.py +297 -297
  39. pyxllib/ext/JLineViewer.py +505 -505
  40. pyxllib/ext/__init__.py +6 -6
  41. pyxllib/ext/demolib.py +246 -246
  42. pyxllib/ext/drissionlib.py +277 -277
  43. pyxllib/ext/kq5034lib.py +12 -12
  44. pyxllib/ext/old.py +663 -663
  45. pyxllib/ext/qt.py +449 -449
  46. pyxllib/ext/robustprocfile.py +497 -497
  47. pyxllib/ext/seleniumlib.py +76 -76
  48. pyxllib/ext/tk.py +173 -173
  49. pyxllib/ext/unixlib.py +827 -827
  50. pyxllib/ext/utools.py +351 -351
  51. pyxllib/ext/webhook.py +124 -119
  52. pyxllib/ext/win32lib.py +40 -40
  53. pyxllib/ext/wjxlib.py +88 -88
  54. pyxllib/ext/wpsapi.py +124 -124
  55. pyxllib/ext/xlwork.py +9 -9
  56. pyxllib/ext/yuquelib.py +1105 -1105
  57. pyxllib/file/__init__.py +17 -17
  58. pyxllib/file/docxlib.py +761 -761
  59. pyxllib/file/gitlib.py +309 -309
  60. pyxllib/file/libreoffice.py +165 -165
  61. pyxllib/file/movielib.py +148 -148
  62. pyxllib/file/newbie.py +10 -10
  63. pyxllib/file/onenotelib.py +1469 -1469
  64. pyxllib/file/packlib/__init__.py +330 -330
  65. pyxllib/file/packlib/zipfile.py +2441 -2441
  66. pyxllib/file/pdflib.py +426 -426
  67. pyxllib/file/pupil.py +185 -185
  68. pyxllib/file/specialist/__init__.py +685 -685
  69. pyxllib/file/specialist/dirlib.py +799 -799
  70. pyxllib/file/specialist/download.py +193 -193
  71. pyxllib/file/specialist/filelib.py +2829 -2829
  72. pyxllib/file/xlsxlib.py +3131 -3131
  73. pyxllib/file/xlsyncfile.py +341 -341
  74. pyxllib/prog/__init__.py +5 -5
  75. pyxllib/prog/cachetools.py +64 -64
  76. pyxllib/prog/deprecatedlib.py +233 -233
  77. pyxllib/prog/filelock.py +42 -42
  78. pyxllib/prog/ipyexec.py +253 -253
  79. pyxllib/prog/multiprogs.py +940 -940
  80. pyxllib/prog/newbie.py +451 -451
  81. pyxllib/prog/pupil.py +1197 -1197
  82. pyxllib/prog/sitepackages.py +33 -33
  83. pyxllib/prog/specialist/__init__.py +391 -391
  84. pyxllib/prog/specialist/bc.py +203 -203
  85. pyxllib/prog/specialist/browser.py +497 -497
  86. pyxllib/prog/specialist/common.py +347 -347
  87. pyxllib/prog/specialist/datetime.py +198 -198
  88. pyxllib/prog/specialist/tictoc.py +240 -240
  89. pyxllib/prog/specialist/xllog.py +180 -180
  90. pyxllib/prog/xlosenv.py +108 -108
  91. pyxllib/stdlib/__init__.py +17 -17
  92. pyxllib/stdlib/tablepyxl/__init__.py +10 -10
  93. pyxllib/stdlib/tablepyxl/style.py +303 -303
  94. pyxllib/stdlib/tablepyxl/tablepyxl.py +130 -130
  95. pyxllib/text/__init__.py +8 -8
  96. pyxllib/text/ahocorasick.py +39 -39
  97. pyxllib/text/airscript.js +744 -744
  98. pyxllib/text/charclasslib.py +121 -121
  99. pyxllib/text/jiebalib.py +267 -267
  100. pyxllib/text/jinjalib.py +32 -32
  101. pyxllib/text/jsa_ai_prompt.md +271 -271
  102. pyxllib/text/jscode.py +922 -922
  103. pyxllib/text/latex/__init__.py +158 -158
  104. pyxllib/text/levenshtein.py +303 -303
  105. pyxllib/text/nestenv.py +1215 -1215
  106. pyxllib/text/newbie.py +300 -300
  107. pyxllib/text/pupil/__init__.py +8 -8
  108. pyxllib/text/pupil/common.py +1121 -1121
  109. pyxllib/text/pupil/xlalign.py +326 -326
  110. pyxllib/text/pycode.py +47 -47
  111. pyxllib/text/specialist/__init__.py +8 -8
  112. pyxllib/text/specialist/common.py +112 -112
  113. pyxllib/text/specialist/ptag.py +186 -186
  114. pyxllib/text/spellchecker.py +172 -172
  115. pyxllib/text/templates/echart_base.html +10 -10
  116. pyxllib/text/templates/highlight_code.html +16 -16
  117. pyxllib/text/templates/latex_editor.html +102 -102
  118. pyxllib/text/vbacode.py +17 -17
  119. pyxllib/text/xmllib.py +747 -747
  120. pyxllib/xl.py +42 -39
  121. pyxllib/xlcv.py +17 -17
  122. {pyxllib-0.3.197.dist-info → pyxllib-0.3.200.dist-info}/METADATA +1 -1
  123. pyxllib-0.3.200.dist-info/RECORD +126 -0
  124. {pyxllib-0.3.197.dist-info → pyxllib-0.3.200.dist-info}/licenses/LICENSE +190 -190
  125. pyxllib-0.3.197.dist-info/RECORD +0 -126
  126. {pyxllib-0.3.197.dist-info → pyxllib-0.3.200.dist-info}/WHEEL +0 -0
pyxllib/autogui/wechat.py CHANGED
@@ -1,827 +1,827 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- # @Author : 陈坤泽
4
- # @Email : 877362867@qq.com
5
- # @Date : 2024/10/31
6
-
7
- """
8
- 代码参考(抄自):https://github.com/Frica01/WeChatMassTool
9
- """
10
-
11
- import sys
12
- import time
13
- from copy import deepcopy
14
- from typing import (Iterable, Callable, List)
15
- from typing import Optional
16
- from unittest.mock import MagicMock
17
-
18
- from loguru import logger
19
-
20
- if sys.platform == 'win32':
21
- import uiautomation as auto
22
- import win32con
23
- import win32gui
24
-
25
- from pyxllib.prog.filelock import get_autoui_lock
26
-
27
- from pyxllib.autogui.uiautolib import find_ctrl, UiCtrlNode, copy_files_to_clipboard
28
-
29
-
30
- def __1_config():
31
- pass
32
-
33
-
34
- class WeChatConfig:
35
- WeChat_PROCESS_NAME = 'WeChat.exe'
36
- APP_NAME = 'WeChatMassTool'
37
- APP_PROCESS_NAME = 'WeChatMassTool.exe'
38
- APP_LOCK_NAME = 'WeChatMassTool.lock'
39
- WINDOW_NAME = '微信'
40
- WINDOW_CLASSNAME = 'WeChatMainWndForPC'
41
-
42
-
43
- # WeChat = WeChatConfig
44
-
45
-
46
- class IntervalConfig:
47
- BASE_INTERVAL = 0.1 # 基础间隔(秒)
48
- SEND_TEXT_INTERVAL = 0.05 # 发送文本间隔(秒)
49
- SEND_FILE_INTERVAL = 0.25 # 发送文件间隔(秒)
50
- MAX_SEARCH_SECOND = 0.1
51
- MAX_SEARCH_INTERVAL = 0.05
52
-
53
-
54
- Interval = IntervalConfig
55
-
56
-
57
- def __2_window_utils():
58
- pass
59
-
60
-
61
- def minimize_wechat(class_name, name):
62
- """
63
- 关闭Windows窗口
64
-
65
- Args:
66
- name(str): 进程名
67
- class_name(str): 进程class_name
68
-
69
- Returns:
70
-
71
- """
72
- hwnd = win32gui.FindWindow(class_name, name)
73
- if win32gui.IsWindowVisible(hwnd):
74
- win32gui.SendMessage(hwnd, win32con.WM_CLOSE, 0, 0)
75
-
76
-
77
- def wake_up_window(class_name, name):
78
- """
79
- 唤醒Windows窗口
80
-
81
- Args:
82
- name(str): 进程名
83
- class_name(str): 进程class_name
84
-
85
- Returns:
86
-
87
- """
88
- if hwnd := win32gui.FindWindow(class_name, name):
89
- # 恢复窗口
90
- win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
91
- # 检查窗口是否已在前
92
- if win32gui.GetForegroundWindow() != hwnd:
93
- # 尝试将窗口置前
94
- try:
95
- win32gui.SetForegroundWindow(hwnd)
96
- except Exception as e:
97
- pass
98
- # print(f"尝试将窗口置前时出错: {e}")
99
-
100
-
101
- def is_window_visible(class_name, name):
102
- """
103
- 唤醒Windows 窗口的可见性
104
-
105
- Args:
106
- name(str): 进程名
107
- class_name(str): 进程class_name
108
-
109
- Returns:
110
-
111
- """
112
- if hwnd := win32gui.FindWindow(class_name, name):
113
- # 判断窗口可见性
114
- if win32gui.IsWindowVisible(hwnd):
115
- return True
116
- return False
117
-
118
-
119
- def __3_wechat():
120
- pass
121
-
122
-
123
- class MsgNode(UiCtrlNode):
124
- """ 一条消息的节点 """
125
-
126
- def __init__(self, msg_ctrl, parent=None, *, build_depth=-1):
127
- super().__init__(msg_ctrl, parent, build_depth=build_depth)
128
- self.time = None # 每条消息都建立一个最近出现的时间作为时间戳?
129
- self.msg_type = None # 消息类型:system, receive, send
130
- self.content_type = None # 内容类型:text、image、file、time
131
-
132
- self.user = None # 用户名
133
- self.user2 = None # 群聊时用户会多一个群聊昵称
134
- self.cite_text = None # 引用的内容,引用的不一定是文本,但是统一转成文本显示的
135
-
136
- self.render_text = None # 供人预览,和简化给ai查看的格式
137
-
138
- def build_children(self, build_depth, child_node_class=None):
139
- if build_depth == 0:
140
- return
141
- self.children = []
142
- child_node_class = child_node_class or UiCtrlNode
143
- for child_ctrl in self.ctrl.GetChildren():
144
- child_node_class(child_ctrl, parent=self, build_depth=build_depth - 1)
145
-
146
- def init(self):
147
- """ 解析出更精细的结构化信息 """
148
- from pyxllib.autogui.wechat_msg import msg_parsers
149
-
150
- tag = self.get_ctrl_hash_tag()
151
- if tag in msg_parsers:
152
- msg_parsers[tag](self)
153
-
154
- def render_tree(self):
155
- """ 展示以self为根节点的整体内容结构 """
156
- # 1 未解析过的节点,原样输出树形结构供添加解析
157
- if not self.msg_type:
158
- return super().render_tree()
159
-
160
- # 2 已设置好预览文本的直接输出
161
- if self.render_text:
162
- return self.render_text
163
-
164
- # 3 否则用一套通用的机制渲染输出
165
- fmt = self._format_text
166
- # 定义类型到符号的映射
167
- type_to_symbol = {
168
- 'send': '↑',
169
- 'receive': '↓',
170
- 'system': '⚙️' # 假设 'system' 类型对应的符号是 ⚙️
171
- }
172
- # 根据 msg_type 获取相应的符号
173
- msg = [f"{type_to_symbol.get(self.msg_type, self.msg_type)}"]
174
-
175
- if self.user:
176
- msg.append(f"{self.user}: ")
177
- msg.append(fmt(self.text))
178
-
179
- if self.cite_text:
180
- msg.append(f"【引用】{fmt(self.cite_text)}")
181
- return ' '.join(msg)
182
-
183
- def is_match(self, msg_node):
184
- """ 两条msg_node是否对应的上 """
185
- from datetime import datetime
186
-
187
- # 1 比如内容一致
188
- if self.render_tree() == msg_node.render_tree():
189
- return 2 # 强一致
190
-
191
- # 2 或者"撤回"格式对应的上
192
- is_recall = (self.content_type == 'recall' or msg_node.content_type == 'recall')
193
- user_matches = is_recall and (self.user == msg_node.user or self.user2 == msg_node.user2)
194
- if is_recall and user_matches:
195
- return 1 # 弱一致
196
-
197
- # 3 判断是否为 'system'、'time' 类型
198
- is_system_time_type = (self.msg_type == msg_node.msg_type == 'time')
199
- if is_system_time_type:
200
- # 检查时间匹配,确保都是 datetime 类型,并只比较年月日时分部分
201
- same_time = (
202
- isinstance(self.time, datetime) and
203
- isinstance(msg_node.time, datetime) and
204
- self.time.strftime('%Y-%m-%d %H:%M') == msg_node.time.strftime('%Y-%m-%d %H:%M')
205
- )
206
- if same_time:
207
- return 2
208
-
209
- if self.text == '以下是新消息' or msg_node.text == '以下是新消息':
210
- return 1
211
-
212
- # 4 如果有一条.content_type=='button_more',也直接弱匹配
213
- if self.content_type == 'button_more' or msg_node.content_type == 'button_more':
214
- return 1
215
-
216
-
217
- class MsgBoxNode(UiCtrlNode):
218
- """ 当前会话消息窗的节点 """
219
-
220
- def __init__(self, chat_box_ctrl, parent=None, *, build_depth=-1):
221
- super().__init__(chat_box_ctrl, parent, build_depth=build_depth)
222
-
223
- def build_children(self, build_depth=-1, child_node_class=None):
224
- # 1 遍历消息初始化
225
- if build_depth == 0:
226
- return
227
- self.children = []
228
- for child_ctrl in self.ctrl.GetChildren():
229
- node = MsgNode(child_ctrl, parent=self, build_depth=build_depth - 1)
230
- node.init() # 节点扩展的初始化操作
231
-
232
- # 2 设置每条消息的时间
233
- last_time = None
234
- for c in self.children:
235
- if c.time:
236
- last_time = c.time
237
- else:
238
- c.time = last_time
239
-
240
- def findidx_system_time(self):
241
- """ 最后条系统时间标记 """
242
- for idx in range(len(self.children) - 1, -1, -1):
243
- if self.children[idx].content_type == 'time':
244
- return idx
245
- return -1
246
-
247
- def findidx_last_send(self):
248
- """ 最后条发出去的消息 """
249
- for idx in range(len(self.children) - 1, -1, -1):
250
- if self.children[idx].content_type == 'receive':
251
- return idx
252
- return -1
253
-
254
- def findidx_last_match(self, old_childrens):
255
- """
256
- :param old_chat_box: 旧的消息队列
257
- :return: 在当前 self.children 中首次匹配到的 old_chat_box 最后几条消息的起始下标
258
- """
259
- x, y = old_childrens, self.children
260
- n, m = len(x), len(y)
261
-
262
- def is_system(a):
263
- """ 非"撤回"的系统消息 """
264
- if a.msg_type == 'system' and a.content_type != 'recall':
265
- return True
266
-
267
- def check_bais(k):
268
- """ 检查偏移量k是否能匹配 """
269
- # i, j分别指向"最后一条"数据,然后开始匹配
270
- i, j = n - 1, m - k - 1
271
- while min(i, j) > 0: # 不匹配第0条,第0条太特别
272
- if is_system(x[i]):
273
- i -= 1
274
- continue
275
- if is_system(y[j]):
276
- j -= 1
277
- continue
278
- if y[j].is_match(x[i]):
279
- i, j = i - 1, j - 1
280
- continue
281
- return False
282
- return True
283
-
284
- for k in range(m):
285
- if check_bais(k):
286
- return m - k - 1
287
-
288
- return -1 # 如果没有匹配,返回 -1
289
-
290
- def check_update(self):
291
- """ 更新当前消息记录,并返回新收到的消息节点 """
292
- import copy
293
- old_childrens = copy.copy(self.children)
294
- self.build_children()
295
- idx = self.findidx_last_match(old_childrens)
296
- return self.children[idx + 1:]
297
-
298
-
299
- class EditorNode(UiCtrlNode):
300
- """ 编辑器节点 """
301
-
302
-
303
- class WeChatMainWnd(UiCtrlNode):
304
-
305
- def __1_结构建构(self):
306
- pass
307
-
308
- def __init__(self, ctrl=None, parent=None, *, build_depth=12):
309
- """
310
- :param ctrl: 当前节点
311
- :param parent: 父结点
312
- :param build_depth: 自动构建多少层树节点,默认-1表示构建全部节点
313
- 目前微信发现12层一般够大部分情况使用了
314
- """
315
- if ctrl is None:
316
- ctrl = find_ctrl('WeChatMainWndForPC', '微信')
317
- super().__init__(ctrl, parent=parent, build_depth=build_depth)
318
-
319
- def build_children(self, build_depth=12, child_node_class=None):
320
- # 1 构建树结构
321
- if build_depth == 0:
322
- return
323
-
324
- # 跳过两级
325
- self.children = [] # 删除现有的所有子结点
326
- ctrl = self.ctrl.GetChildren()[-1].GetChildren()[0]
327
- for child_ctrl in ctrl.GetChildren():
328
- UiCtrlNode(child_ctrl, parent=self, build_depth=build_depth - 1)
329
-
330
- # 2 预先定义好一些常用的控件
331
- self.nav = self[0] # 导航
332
- self.nav_avatar = self.nav[0] # 导航_头像
333
- self.nav_chat = self.nav[1] # 导航_聊天
334
- self.nav_contacts = self.nav[2] # 导航_通讯录
335
- self.nav_favorites = self.nav[3] # 导航_收藏
336
- self.nav_files = self.nav[4] # 导航_聊天文件
337
- self.nav_moments = self.nav[5] # 导航_朋友圈
338
-
339
- def __2_常用控件(self):
340
- pass
341
-
342
- @property
343
- def column3(self):
344
- # 订阅号、聊天都共通
345
- return self[2][0]
346
-
347
- @property
348
- def chat_window(self):
349
- return self.column3[0][0][0]
350
-
351
- @property
352
- def msg_box(self):
353
- parent = self.chat_window[1][0][0]
354
- if not isinstance(parent[0], MsgBoxNode): # 只初始化一次
355
- parent.children = (MsgBoxNode(parent[0]), *parent.children[1:])
356
- return parent[0]
357
-
358
- @property
359
- def edit_box(self):
360
- parent = self.chat_window[1][1][1]
361
- if not isinstance(parent, EditorNode):
362
- parent.children = (EditorNode(parent[0]), *parent.children[1:])
363
- return parent[0]
364
-
365
- @property
366
- def editor(self):
367
- return self.edit_box[1][0]
368
-
369
- @property
370
- def subscription_window(self):
371
- return self.column3[0][0] # 订阅号窗口
372
-
373
- @property
374
- def subscription_window_title(self):
375
- return self.subscription_window[0][0] # 订阅号窗口标题
376
-
377
- def __3_窗口切换功能(self):
378
- pass
379
-
380
- def get_chat_with(self):
381
- """ 当前在跟谁聊天 """
382
- try:
383
- return self.editor.Name
384
- except IndexError:
385
- return
386
-
387
- def set_chat_with(self, name):
388
- """ 要跟谁聊天,确保聊天框切换过去 """
389
- self.nav_chat.Click()
390
-
391
- if self.get_chat_with() == name:
392
- return
393
-
394
- column2 = self[1] # 订阅号、聊天都共通
395
- search_box = column2[0][0][1][0]
396
-
397
- search_box.Click()
398
- search_box.SendKeys(name)
399
- time.sleep(2)
400
- search_box.SendKeys('{Enter}')
401
-
402
- def __4_编辑器(self):
403
- pass
404
-
405
- def get_editor_content(self):
406
- """ 获得编辑器正在编辑的内容(涉及到换行数据的时候好像不太准确) """
407
- return self.editor.GetValuePattern().Value
408
-
409
- def clear_editor_content(self):
410
- """ 删除编辑区中的所有内容 """
411
- edit_box = self.edit_box
412
- edit_box.SendKeys('{Ctrl}a')
413
- edit_box.SendKeys('{Delete}')
414
-
415
- def write_text(self, text, clear=False, send=False):
416
- # todo 加at人的功能
417
- # 1 是否清空旧数据
418
- if clear:
419
- self.clear_editor_content()
420
-
421
- # 2 写入新数据
422
- def should_use_clipboard(text):
423
- # 简单的策略:如果文本过长或包含特殊字符,则使用剪贴板
424
- return len(text) > 30 or not text.isprintable() or '{' in text
425
-
426
- edit_box = self.edit_box
427
- edit_box.Click()
428
- if should_use_clipboard(text):
429
- auto.SetClipboardText(text)
430
- time.sleep(1)
431
- edit_box.SendKeys('{Ctrl}v')
432
- else:
433
- edit_box.SendKeys(text)
434
-
435
- # 3 发送内容
436
- if send:
437
- time.sleep(1)
438
- edit_box.SendKeys('{Enter}')
439
-
440
- def send_text(self, text, clear=False):
441
- self.write_text(text, clear=clear, send=True)
442
-
443
- def send_files(self, file_paths):
444
- """
445
- 发送多个文件
446
-
447
- :param list[str] file_paths: 必选参数,为文件的路径
448
- """
449
- if copy_files_to_clipboard(file_paths=file_paths):
450
- edit_box = self.edit_box
451
- edit_box.Click()
452
- time.sleep(1)
453
- edit_box.SendKeys('{Ctrl}v')
454
- time.sleep(1)
455
- edit_box.SendKeys('{Enter}')
456
- time.sleep(1) # 等待发送动作完成
457
-
458
-
459
- class WeChatSingletonLock:
460
- """ 基于 get_autoui_lock 的微信全局唯一单例控制器,确保同一时间仅有一个微信自动化程序在操作 """
461
-
462
- def __init__(self, lock_timeout: Optional[float] = -1):
463
- # 初始化全局锁
464
- self.lock = get_autoui_lock(timeout=lock_timeout)
465
- self.wechat = WeChatMainWnd()
466
-
467
- def __enter__(self):
468
- # 获取锁并激活微信窗口
469
- self.lock.acquire()
470
- self.wechat.activate()
471
- return self.wechat
472
-
473
- def __exit__(self, exc_type, exc_value, traceback):
474
- # 释放锁
475
- self.lock.release()
476
-
477
-
478
- def wechat_lock_send(user, text=None, files=None, *, timeout=-1):
479
- """ 使用全局唯一单例锁,确保同一时间仅有一个微信自动化程序在操作 """
480
- with WeChatSingletonLock(timeout) as we:
481
- we.set_chat_with(user)
482
- if text:
483
- we.send_text(text)
484
- if files:
485
- we.send_files(files)
486
-
487
-
488
- def wechat_handler(message):
489
- # 获取群名,如果没有指定,不使用此微信发送功能
490
- user = message.record["extra"].get("wechat_user")
491
- if user:
492
- wechat_lock_send(user, message)
493
-
494
-
495
- if sys.platform == 'win32':
496
- # 创建专用的微信日志记录器,不绑定默认群名
497
- wechat_logger = logger.bind(wechat_user='文件传输助手')
498
-
499
- # 添加专用的微信处理器
500
- wechat_logger.add(wechat_handler,
501
- format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name}:{function}:{line} - {message}")
502
-
503
- """ 往微信发送特殊的日志格式报告
504
- 用法:wechat_logger.bind(wechat_user='文件传输助手').info(message)
505
-
506
- 或者:
507
- # 先做好默认群名绑定
508
- wechat_logger = wechat_logger.bind(wechat_user='考勤管理')
509
- # 然后就能普通logger用法发送了
510
- wechat_logger.info('测试')
511
- """
512
- else:
513
- # 降级为普通logger
514
- wechat_logger = logger
515
-
516
-
517
- def __4_wx():
518
- """ 别人的原版实现 """
519
-
520
-
521
- class WxOperation:
522
- """
523
- 微信群发消息的类,提供了与微信应用交互的方法集,用于发送消息,管理联系人列表等功能。
524
-
525
- Attributes:
526
- ----------
527
- wx_window: auto.WindowControl
528
- 微信控制窗口
529
- input_edit: wx_window.EditControl
530
- 聊天界面输入框编辑控制窗口
531
-
532
- Methods:
533
- -------
534
- goto_chat_box(name):
535
- 跳转到 指定好友窗口
536
- __send_text(*msgs):
537
- 发送文本。
538
- __send_file(*filepath):
539
- 发送文件
540
- get_friend_list(tag, num):
541
- 可指定tag,获取好友num页的好友数量
542
- send_msg(name, msgs, file_paths=None, add_remark_name=False, at_everyone=False,
543
- text_interval=0.05, file_interval=0.5) -> None:
544
- 向指定的好友或群聊发送消息和文件。支持同时发送文本和文件。
545
- """
546
-
547
- def __init__(self):
548
- self.wx_window = None
549
- self.input_edit = None
550
- self.wx_window: auto.WindowControl
551
- self.input_edit: auto.EditControl
552
- auto.SetGlobalSearchTimeout(Interval.BASE_INTERVAL)
553
- self.visible_flag: bool = False
554
-
555
- def locate_wechat_window(self):
556
- if not self.visible_flag:
557
- wake_up_window(class_name=WeChatConfig.WINDOW_CLASSNAME, name=WeChatConfig.WINDOW_NAME)
558
- self.wx_window = auto.WindowControl(Name=WeChatConfig.WINDOW_NAME, ClassName=WeChatConfig.WINDOW_CLASSNAME)
559
- if not self.wx_window.Exists(Interval.MAX_SEARCH_SECOND,
560
- searchIntervalSeconds=Interval.MAX_SEARCH_INTERVAL):
561
- raise Exception('微信似乎并没有登录!')
562
- self.input_edit = self.wx_window.EditControl()
563
- self.visible_flag = bool(self.visible_flag)
564
- # 微信窗口置顶
565
- self.wx_window.SetTopmost(isTopmost=True)
566
-
567
- def match_nickname(self, name):
568
- """获取当前面板的好友昵称"""
569
- self.input_edit = self.wx_window.EditControl(Name=name)
570
- if self.input_edit.Exists(Interval.MAX_SEARCH_SECOND, searchIntervalSeconds=Interval.MAX_SEARCH_INTERVAL):
571
- return self.input_edit
572
- return False
573
-
574
- def goto_chat_box(self, name: str) -> bool:
575
- """
576
- 跳转到指定 name好友的聊天窗口。
577
-
578
- Args:
579
- name(str): 必选参数,好友名称
580
-
581
- Returns:
582
- None
583
- """
584
- if ctrl := self.match_nickname(name):
585
- ctrl.SetFocus()
586
- return ctrl
587
-
588
- assert name, "无法跳转到名字为空的聊天窗口"
589
- self.wx_window.SendKeys(text='{Ctrl}F', waitTime=Interval.BASE_INTERVAL)
590
- self.wx_window.SendKeys(text='{Ctrl}A', waitTime=Interval.BASE_INTERVAL)
591
- self.wx_window.SendKey(key=auto.SpecialKeyNames['DELETE'])
592
- auto.SetClipboardText(text=name)
593
- time.sleep(Interval.BASE_INTERVAL)
594
- self.wx_window.SendKeys(text='{Ctrl}V', waitTime=Interval.BASE_INTERVAL)
595
- # 若有匹配结果,第一个元素的类型为PaneControl
596
- search_nodes = self.wx_window.ListControl(foundIndex=2).GetChildren()
597
- if not isinstance(search_nodes.pop(0), auto.PaneControl):
598
- self.wx_window.SendKeys(text='{Esc}', waitTime=Interval.BASE_INTERVAL)
599
- raise ValueError("昵称不匹配")
600
- # 只考虑全匹配, 不考虑好友昵称重名, 不考虑好友昵称与群聊重名
601
- if search_nodes[0].Name == name:
602
- self.wx_window.SendKey(key=auto.SpecialKeyNames['ENTER'], waitTime=Interval.BASE_INTERVAL)
603
- time.sleep(Interval.BASE_INTERVAL)
604
- return True
605
- # 无匹配用户, 取消搜索框
606
- self.wx_window.SendKeys(text='{Esc}', waitTime=Interval.BASE_INTERVAL)
607
- return False
608
-
609
- def at_at_everyone(self, group_chat_name: str):
610
- """
611
- @全部人的操作
612
- Args:
613
- group_chat_name(str): 群聊名称
614
-
615
- """
616
- # 个人 定位 聊天框,取 foundIndex=2,因为左侧聊天List 也可以匹配到foundIndex=1
617
- # 群聊 定位 聊天框 需要带上群人数,故会匹配失败,所以匹配失败的就是群聊
618
- result = self.wx_window.TextControl(Name=group_chat_name, foundIndex=2)
619
- # 只要匹配不上,说明这是个群聊窗口
620
- if not result.Exists(Interval.MAX_SEARCH_SECOND, searchIntervalSeconds=Interval.MAX_SEARCH_INTERVAL):
621
- # 寻找是否有 @所有人 的选项
622
- self.input_edit.SendKeys(text='{Shift}2', waitTime=Interval.BASE_INTERVAL)
623
- everyone = self.wx_window.ListItemControl(Name='所有人')
624
- if not everyone.Exists(Interval.MAX_SEARCH_SECOND, searchIntervalSeconds=Interval.MAX_SEARCH_INTERVAL):
625
- self.input_edit.SendKeys(text='{Ctrl}A', waitTime=Interval.BASE_INTERVAL)
626
- self.input_edit.SendKeys(text='{Delete}', waitTime=Interval.BASE_INTERVAL)
627
- return
628
- self.input_edit.SendKeys(text='{Up}', waitTime=Interval.BASE_INTERVAL)
629
- self.input_edit.SendKeys(text='{Enter}', waitTime=Interval.BASE_INTERVAL)
630
- self.input_edit.SendKeys(text='{Enter}', waitTime=Interval.BASE_INTERVAL)
631
-
632
- def __send_text(self, *msgs, wait_time, send_shortcut) -> None:
633
- """
634
- 发送文本.
635
-
636
- Args:
637
- input_name(str): 必选参数, 为输入框
638
- *msgs(str): 必选参数,为发送的文本
639
- wait_time(float): 必选参数,为动态等待时间
640
- send_shortcut(str): 必选参数,为发送快捷键
641
-
642
- Returns:
643
- None
644
- """
645
-
646
- def should_use_clipboard(text: str):
647
- # 简单的策略:如果文本过长或包含特殊字符,则使用剪贴板
648
- return len(text) > 30 or not text.isprintable()
649
-
650
- for msg in msgs:
651
- assert msg, "发送的文本内容为空"
652
- self.input_edit.SendKeys(text='{Ctrl}a', waitTime=wait_time)
653
- self.input_edit.SendKey(key=auto.SpecialKeyNames['DELETE'], waitTime=wait_time)
654
- self.input_edit.SendKeys(text='{Ctrl}a', waitTime=wait_time)
655
- self.input_edit.SendKey(key=auto.SpecialKeyNames['DELETE'], waitTime=wait_time)
656
-
657
- if should_use_clipboard(msg):
658
- auto.SetClipboardText(text=msg)
659
- time.sleep(wait_time * 2.5)
660
- self.input_edit.SendKeys(text='{Ctrl}v', waitTime=wait_time * 2)
661
- else:
662
- self.input_edit.SendKeys(text=msg, waitTime=wait_time * 2)
663
-
664
- # 设置到剪切板再黏贴到输入框
665
- self.wx_window.SendKeys(text=f'{send_shortcut}', waitTime=wait_time * 2)
666
-
667
- def __send_file(self, *file_paths, wait_time, send_shortcut) -> None:
668
- """
669
- 发送文件.
670
-
671
- Args:
672
- *file_paths(str): 必选参数,为文件的路径
673
- wait_time(float): 必选参数,为动态等待时间
674
- send_shortcut(str): 必选参数,为发送快捷键
675
-
676
- Returns:
677
- None
678
- """
679
- # 复制文件到剪切板
680
- if copy_files_to_clipboard(file_paths=file_paths):
681
- # 粘贴到输入框
682
- self.input_edit.SendKeys(text='{Ctrl}V', waitTime=wait_time)
683
- # 按下回车键
684
- self.wx_window.SendKeys(text=f'{send_shortcut}', waitTime=wait_time / 2)
685
-
686
- time.sleep(wait_time) # 等待发送动作完成
687
-
688
- def get_friend_list(self, tag: str = None) -> list:
689
- """
690
- 获取微信好友名称.
691
-
692
- Args:
693
- tag(str): 可选参数,如不指定,则获取所有好友
694
-
695
- Returns:
696
- list
697
- """
698
- # 定位到微信窗口
699
- self.locate_wechat_window()
700
- # 取消微信窗口置顶
701
- self.wx_window.SetTopmost(isTopmost=False)
702
- # 点击 通讯录管理
703
- self.wx_window.ButtonControl(Name="通讯录").Click(simulateMove=False)
704
- self.wx_window.ListControl(Name="联系人").ButtonControl(Name="通讯录管理").Click(simulateMove=False)
705
- # 切换到通讯录管理,相当于切换到弹出来的页面
706
- contacts_window = auto.GetForegroundControl()
707
- contacts_window.ButtonControl(Name='最大化').Click(simulateMove=False)
708
-
709
- if tag:
710
- try:
711
- contacts_window.ButtonControl(Name="标签").Click(simulateMove=False)
712
- contacts_window.PaneControl(Name=tag).Click(simulateMove=False)
713
- time.sleep(Interval.BASE_INTERVAL * 2)
714
- except LookupError:
715
- contacts_window.SendKey(auto.SpecialKeyNames['ESC'])
716
- raise LookupError(f'找不到 {tag} 标签')
717
-
718
- name_list = list()
719
- last_names = None
720
- while True:
721
- # TODO 修改成使用 foundIndex 的方式
722
- try:
723
- nodes = contacts_window.ListControl(foundIndex=2).GetChildren()
724
- except LookupError:
725
- nodes = contacts_window.ListControl().GetChildren()
726
- cur_names = [node.TextControl().Name for node in nodes]
727
-
728
- # 如果滚动前后名单未变,认为到达底部
729
- if cur_names == last_names:
730
- break
731
- last_names = cur_names
732
- # 处理当前页的名单
733
- for node in nodes:
734
- # TODO 如果有需要, 可以处理成导出为两列的csv格式
735
- nick_name = node.TextControl().Name # 用户名
736
- remark_name = node.ButtonControl(foundIndex=2).Name # 用户备注名,索引1会错位,索引2是备注名,索引3是标签名
737
- name_list.append(remark_name if remark_name else nick_name)
738
- # 向下滚动页面
739
- contacts_window.WheelDown(wheelTimes=8, waitTime=Interval.BASE_INTERVAL / 2)
740
- # 结束时候关闭 "通讯录管理" 窗口
741
- contacts_window.SendKey(auto.SpecialKeyNames['ESC'])
742
- # 简单去重,但是存在误判(如果存在同名的好友), 保持获取时候的顺序
743
- return list(dict.fromkeys(name_list))
744
-
745
- def get_group_chat_list(self) -> list:
746
- """获取群聊通讯录中的用户名称"""
747
- name_list = list()
748
- auto.ButtonControl(Name='聊天信息').Click()
749
- time.sleep(0.5)
750
- chat_members_win = self.wx_window.ListControl(Name='聊天成员')
751
- if not chat_members_win.Exists():
752
- return list()
753
- self.wx_window.ButtonControl(Name='查看更多').Click()
754
- for item in chat_members_win.GetChildren():
755
- name_list.append(item.ButtonControl().Name)
756
- return name_list
757
-
758
- def send_msg(self, name, msgs=None, file_paths=None, add_remark_name=False, at_everyone=False,
759
- text_interval=Interval.SEND_TEXT_INTERVAL, file_interval=Interval.SEND_FILE_INTERVAL,
760
- send_shortcut='{Enter}') -> None:
761
- """
762
- 发送消息,可同时发送文本和文件(至少选一项
763
-
764
- Args:
765
- name(str):必选参数,接收消息的好友名称, 可以单发
766
- msgs(Iterable[str], Optional): 可选参数,发送的文本消息
767
- file_paths(Iterable[str], Optional):可选参数,发送的文件路径
768
- add_remark_name(bool): 可选参数,是否添加备注名称发送
769
- at_everyone(bool): 可选参数,是否@全部人
770
- text_interval(float): 可选参数,默认为0.05
771
- file_interval(float): 可选参数,默认为0.5
772
- send_shortcut(str): 可选参数,默认为 Enter
773
-
774
- Raises:
775
- ValueError: 如果用户名为空或发送的消息和文件同时为空时抛出异常
776
- TypeError: 如果发送的文本消息或文件路径类型不是列表或元组时抛出异常
777
- """
778
- # 定位到微信窗口
779
- self.locate_wechat_window()
780
-
781
- if not name:
782
- raise ValueError("用户名不能为空")
783
-
784
- if not any([msgs, file_paths]):
785
- raise ValueError("发送的消息和文件不可同时为空")
786
-
787
- if msgs and not isinstance(msgs, Iterable):
788
- raise TypeError("发送的文本消息必须是可迭代的")
789
-
790
- if file_paths and not isinstance(file_paths, Iterable):
791
- raise TypeError("发送的文件路径必须是可迭代的")
792
-
793
- # 如果当前面板已经是需发送好友, 则无需再次搜索跳转
794
- if not self.match_nickname(name=name):
795
- if not self.goto_chat_box(name=name):
796
- raise NameError('昵称不匹配')
797
-
798
- # 设置输入框为当前焦点
799
- self.input_edit = self.wx_window.EditControl(Name=name)
800
- self.input_edit.SetFocus()
801
-
802
- # @所有人
803
- if at_everyone:
804
- auto.SetGlobalSearchTimeout(Interval.BASE_INTERVAL)
805
- self.at_at_everyone(group_chat_name=name)
806
- auto.SetGlobalSearchTimeout(Interval.BASE_INTERVAL * 25)
807
-
808
- # TODO 添加备注可以多做一个选项,添加到每条消息的前面,如xxx,早上好
809
- if msgs and add_remark_name:
810
- new_msgs = deepcopy(list(msgs))
811
- new_msgs.insert(0, name)
812
- self.__send_text(*new_msgs, wait_time=text_interval, send_shortcut=send_shortcut)
813
- elif msgs:
814
- self.__send_text(*msgs, wait_time=text_interval, send_shortcut=send_shortcut)
815
- if file_paths:
816
- self.__send_file(*file_paths, wait_time=file_interval, send_shortcut=send_shortcut)
817
-
818
- # 取消微信窗口置顶
819
- self.wx_window.SetTopmost(isTopmost=False)
820
-
821
-
822
- if __name__ == '__main__':
823
- # wx = WxOperation()
824
- # data = wx.get_friend_list('无标签')
825
- # print(data)
826
- # print(len(data))
827
- pass
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # @Author : 陈坤泽
4
+ # @Email : 877362867@qq.com
5
+ # @Date : 2024/10/31
6
+
7
+ """
8
+ 代码参考(抄自):https://github.com/Frica01/WeChatMassTool
9
+ """
10
+
11
+ import sys
12
+ import time
13
+ from copy import deepcopy
14
+ from typing import (Iterable, Callable, List)
15
+ from typing import Optional
16
+ from unittest.mock import MagicMock
17
+
18
+ from loguru import logger
19
+
20
+ if sys.platform == 'win32':
21
+ import uiautomation as auto
22
+ import win32con
23
+ import win32gui
24
+
25
+ from pyxllib.prog.filelock import get_autoui_lock
26
+
27
+ from pyxllib.autogui.uiautolib import find_ctrl, UiCtrlNode, copy_files_to_clipboard
28
+
29
+
30
+ def __1_config():
31
+ pass
32
+
33
+
34
+ class WeChatConfig:
35
+ WeChat_PROCESS_NAME = 'WeChat.exe'
36
+ APP_NAME = 'WeChatMassTool'
37
+ APP_PROCESS_NAME = 'WeChatMassTool.exe'
38
+ APP_LOCK_NAME = 'WeChatMassTool.lock'
39
+ WINDOW_NAME = '微信'
40
+ WINDOW_CLASSNAME = 'WeChatMainWndForPC'
41
+
42
+
43
+ # WeChat = WeChatConfig
44
+
45
+
46
+ class IntervalConfig:
47
+ BASE_INTERVAL = 0.1 # 基础间隔(秒)
48
+ SEND_TEXT_INTERVAL = 0.05 # 发送文本间隔(秒)
49
+ SEND_FILE_INTERVAL = 0.25 # 发送文件间隔(秒)
50
+ MAX_SEARCH_SECOND = 0.1
51
+ MAX_SEARCH_INTERVAL = 0.05
52
+
53
+
54
+ Interval = IntervalConfig
55
+
56
+
57
+ def __2_window_utils():
58
+ pass
59
+
60
+
61
+ def minimize_wechat(class_name, name):
62
+ """
63
+ 关闭Windows窗口
64
+
65
+ Args:
66
+ name(str): 进程名
67
+ class_name(str): 进程class_name
68
+
69
+ Returns:
70
+
71
+ """
72
+ hwnd = win32gui.FindWindow(class_name, name)
73
+ if win32gui.IsWindowVisible(hwnd):
74
+ win32gui.SendMessage(hwnd, win32con.WM_CLOSE, 0, 0)
75
+
76
+
77
+ def wake_up_window(class_name, name):
78
+ """
79
+ 唤醒Windows窗口
80
+
81
+ Args:
82
+ name(str): 进程名
83
+ class_name(str): 进程class_name
84
+
85
+ Returns:
86
+
87
+ """
88
+ if hwnd := win32gui.FindWindow(class_name, name):
89
+ # 恢复窗口
90
+ win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
91
+ # 检查窗口是否已在前
92
+ if win32gui.GetForegroundWindow() != hwnd:
93
+ # 尝试将窗口置前
94
+ try:
95
+ win32gui.SetForegroundWindow(hwnd)
96
+ except Exception as e:
97
+ pass
98
+ # print(f"尝试将窗口置前时出错: {e}")
99
+
100
+
101
+ def is_window_visible(class_name, name):
102
+ """
103
+ 唤醒Windows 窗口的可见性
104
+
105
+ Args:
106
+ name(str): 进程名
107
+ class_name(str): 进程class_name
108
+
109
+ Returns:
110
+
111
+ """
112
+ if hwnd := win32gui.FindWindow(class_name, name):
113
+ # 判断窗口可见性
114
+ if win32gui.IsWindowVisible(hwnd):
115
+ return True
116
+ return False
117
+
118
+
119
+ def __3_wechat():
120
+ pass
121
+
122
+
123
+ class MsgNode(UiCtrlNode):
124
+ """ 一条消息的节点 """
125
+
126
+ def __init__(self, msg_ctrl, parent=None, *, build_depth=-1):
127
+ super().__init__(msg_ctrl, parent, build_depth=build_depth)
128
+ self.time = None # 每条消息都建立一个最近出现的时间作为时间戳?
129
+ self.msg_type = None # 消息类型:system, receive, send
130
+ self.content_type = None # 内容类型:text、image、file、time
131
+
132
+ self.user = None # 用户名
133
+ self.user2 = None # 群聊时用户会多一个群聊昵称
134
+ self.cite_text = None # 引用的内容,引用的不一定是文本,但是统一转成文本显示的
135
+
136
+ self.render_text = None # 供人预览,和简化给ai查看的格式
137
+
138
+ def build_children(self, build_depth, child_node_class=None):
139
+ if build_depth == 0:
140
+ return
141
+ self.children = []
142
+ child_node_class = child_node_class or UiCtrlNode
143
+ for child_ctrl in self.ctrl.GetChildren():
144
+ child_node_class(child_ctrl, parent=self, build_depth=build_depth - 1)
145
+
146
+ def init(self):
147
+ """ 解析出更精细的结构化信息 """
148
+ from pyxllib.autogui.wechat_msg import msg_parsers
149
+
150
+ tag = self.get_ctrl_hash_tag()
151
+ if tag in msg_parsers:
152
+ msg_parsers[tag](self)
153
+
154
+ def render_tree(self):
155
+ """ 展示以self为根节点的整体内容结构 """
156
+ # 1 未解析过的节点,原样输出树形结构供添加解析
157
+ if not self.msg_type:
158
+ return super().render_tree()
159
+
160
+ # 2 已设置好预览文本的直接输出
161
+ if self.render_text:
162
+ return self.render_text
163
+
164
+ # 3 否则用一套通用的机制渲染输出
165
+ fmt = self._format_text
166
+ # 定义类型到符号的映射
167
+ type_to_symbol = {
168
+ 'send': '↑',
169
+ 'receive': '↓',
170
+ 'system': '⚙️' # 假设 'system' 类型对应的符号是 ⚙️
171
+ }
172
+ # 根据 msg_type 获取相应的符号
173
+ msg = [f"{type_to_symbol.get(self.msg_type, self.msg_type)}"]
174
+
175
+ if self.user:
176
+ msg.append(f"{self.user}: ")
177
+ msg.append(fmt(self.text))
178
+
179
+ if self.cite_text:
180
+ msg.append(f"【引用】{fmt(self.cite_text)}")
181
+ return ' '.join(msg)
182
+
183
+ def is_match(self, msg_node):
184
+ """ 两条msg_node是否对应的上 """
185
+ from datetime import datetime
186
+
187
+ # 1 比如内容一致
188
+ if self.render_tree() == msg_node.render_tree():
189
+ return 2 # 强一致
190
+
191
+ # 2 或者"撤回"格式对应的上
192
+ is_recall = (self.content_type == 'recall' or msg_node.content_type == 'recall')
193
+ user_matches = is_recall and (self.user == msg_node.user or self.user2 == msg_node.user2)
194
+ if is_recall and user_matches:
195
+ return 1 # 弱一致
196
+
197
+ # 3 判断是否为 'system'、'time' 类型
198
+ is_system_time_type = (self.msg_type == msg_node.msg_type == 'time')
199
+ if is_system_time_type:
200
+ # 检查时间匹配,确保都是 datetime 类型,并只比较年月日时分部分
201
+ same_time = (
202
+ isinstance(self.time, datetime) and
203
+ isinstance(msg_node.time, datetime) and
204
+ self.time.strftime('%Y-%m-%d %H:%M') == msg_node.time.strftime('%Y-%m-%d %H:%M')
205
+ )
206
+ if same_time:
207
+ return 2
208
+
209
+ if self.text == '以下是新消息' or msg_node.text == '以下是新消息':
210
+ return 1
211
+
212
+ # 4 如果有一条.content_type=='button_more',也直接弱匹配
213
+ if self.content_type == 'button_more' or msg_node.content_type == 'button_more':
214
+ return 1
215
+
216
+
217
+ class MsgBoxNode(UiCtrlNode):
218
+ """ 当前会话消息窗的节点 """
219
+
220
+ def __init__(self, chat_box_ctrl, parent=None, *, build_depth=-1):
221
+ super().__init__(chat_box_ctrl, parent, build_depth=build_depth)
222
+
223
+ def build_children(self, build_depth=-1, child_node_class=None):
224
+ # 1 遍历消息初始化
225
+ if build_depth == 0:
226
+ return
227
+ self.children = []
228
+ for child_ctrl in self.ctrl.GetChildren():
229
+ node = MsgNode(child_ctrl, parent=self, build_depth=build_depth - 1)
230
+ node.init() # 节点扩展的初始化操作
231
+
232
+ # 2 设置每条消息的时间
233
+ last_time = None
234
+ for c in self.children:
235
+ if c.time:
236
+ last_time = c.time
237
+ else:
238
+ c.time = last_time
239
+
240
+ def findidx_system_time(self):
241
+ """ 最后条系统时间标记 """
242
+ for idx in range(len(self.children) - 1, -1, -1):
243
+ if self.children[idx].content_type == 'time':
244
+ return idx
245
+ return -1
246
+
247
+ def findidx_last_send(self):
248
+ """ 最后条发出去的消息 """
249
+ for idx in range(len(self.children) - 1, -1, -1):
250
+ if self.children[idx].content_type == 'receive':
251
+ return idx
252
+ return -1
253
+
254
+ def findidx_last_match(self, old_childrens):
255
+ """
256
+ :param old_chat_box: 旧的消息队列
257
+ :return: 在当前 self.children 中首次匹配到的 old_chat_box 最后几条消息的起始下标
258
+ """
259
+ x, y = old_childrens, self.children
260
+ n, m = len(x), len(y)
261
+
262
+ def is_system(a):
263
+ """ 非"撤回"的系统消息 """
264
+ if a.msg_type == 'system' and a.content_type != 'recall':
265
+ return True
266
+
267
+ def check_bais(k):
268
+ """ 检查偏移量k是否能匹配 """
269
+ # i, j分别指向"最后一条"数据,然后开始匹配
270
+ i, j = n - 1, m - k - 1
271
+ while min(i, j) > 0: # 不匹配第0条,第0条太特别
272
+ if is_system(x[i]):
273
+ i -= 1
274
+ continue
275
+ if is_system(y[j]):
276
+ j -= 1
277
+ continue
278
+ if y[j].is_match(x[i]):
279
+ i, j = i - 1, j - 1
280
+ continue
281
+ return False
282
+ return True
283
+
284
+ for k in range(m):
285
+ if check_bais(k):
286
+ return m - k - 1
287
+
288
+ return -1 # 如果没有匹配,返回 -1
289
+
290
+ def check_update(self):
291
+ """ 更新当前消息记录,并返回新收到的消息节点 """
292
+ import copy
293
+ old_childrens = copy.copy(self.children)
294
+ self.build_children()
295
+ idx = self.findidx_last_match(old_childrens)
296
+ return self.children[idx + 1:]
297
+
298
+
299
+ class EditorNode(UiCtrlNode):
300
+ """ 编辑器节点 """
301
+
302
+
303
+ class WeChatMainWnd(UiCtrlNode):
304
+
305
+ def __1_结构建构(self):
306
+ pass
307
+
308
+ def __init__(self, ctrl=None, parent=None, *, build_depth=12):
309
+ """
310
+ :param ctrl: 当前节点
311
+ :param parent: 父结点
312
+ :param build_depth: 自动构建多少层树节点,默认-1表示构建全部节点
313
+ 目前微信发现12层一般够大部分情况使用了
314
+ """
315
+ if ctrl is None:
316
+ ctrl = find_ctrl('WeChatMainWndForPC', '微信')
317
+ super().__init__(ctrl, parent=parent, build_depth=build_depth)
318
+
319
+ def build_children(self, build_depth=12, child_node_class=None):
320
+ # 1 构建树结构
321
+ if build_depth == 0:
322
+ return
323
+
324
+ # 跳过两级
325
+ self.children = [] # 删除现有的所有子结点
326
+ ctrl = self.ctrl.GetChildren()[-1].GetChildren()[0]
327
+ for child_ctrl in ctrl.GetChildren():
328
+ UiCtrlNode(child_ctrl, parent=self, build_depth=build_depth - 1)
329
+
330
+ # 2 预先定义好一些常用的控件
331
+ self.nav = self[0] # 导航
332
+ self.nav_avatar = self.nav[0] # 导航_头像
333
+ self.nav_chat = self.nav[1] # 导航_聊天
334
+ self.nav_contacts = self.nav[2] # 导航_通讯录
335
+ self.nav_favorites = self.nav[3] # 导航_收藏
336
+ self.nav_files = self.nav[4] # 导航_聊天文件
337
+ self.nav_moments = self.nav[5] # 导航_朋友圈
338
+
339
+ def __2_常用控件(self):
340
+ pass
341
+
342
+ @property
343
+ def column3(self):
344
+ # 订阅号、聊天都共通
345
+ return self[2][0]
346
+
347
+ @property
348
+ def chat_window(self):
349
+ return self.column3[0][0][0]
350
+
351
+ @property
352
+ def msg_box(self):
353
+ parent = self.chat_window[1][0][0]
354
+ if not isinstance(parent[0], MsgBoxNode): # 只初始化一次
355
+ parent.children = (MsgBoxNode(parent[0]), *parent.children[1:])
356
+ return parent[0]
357
+
358
+ @property
359
+ def edit_box(self):
360
+ parent = self.chat_window[1][1][1]
361
+ if not isinstance(parent, EditorNode):
362
+ parent.children = (EditorNode(parent[0]), *parent.children[1:])
363
+ return parent[0]
364
+
365
+ @property
366
+ def editor(self):
367
+ return self.edit_box[1][0]
368
+
369
+ @property
370
+ def subscription_window(self):
371
+ return self.column3[0][0] # 订阅号窗口
372
+
373
+ @property
374
+ def subscription_window_title(self):
375
+ return self.subscription_window[0][0] # 订阅号窗口标题
376
+
377
+ def __3_窗口切换功能(self):
378
+ pass
379
+
380
+ def get_chat_with(self):
381
+ """ 当前在跟谁聊天 """
382
+ try:
383
+ return self.editor.Name
384
+ except IndexError:
385
+ return
386
+
387
+ def set_chat_with(self, name):
388
+ """ 要跟谁聊天,确保聊天框切换过去 """
389
+ self.nav_chat.Click()
390
+
391
+ if self.get_chat_with() == name:
392
+ return
393
+
394
+ column2 = self[1] # 订阅号、聊天都共通
395
+ search_box = column2[0][0][1][0]
396
+
397
+ search_box.Click()
398
+ search_box.SendKeys(name)
399
+ time.sleep(2)
400
+ search_box.SendKeys('{Enter}')
401
+
402
+ def __4_编辑器(self):
403
+ pass
404
+
405
+ def get_editor_content(self):
406
+ """ 获得编辑器正在编辑的内容(涉及到换行数据的时候好像不太准确) """
407
+ return self.editor.GetValuePattern().Value
408
+
409
+ def clear_editor_content(self):
410
+ """ 删除编辑区中的所有内容 """
411
+ edit_box = self.edit_box
412
+ edit_box.SendKeys('{Ctrl}a')
413
+ edit_box.SendKeys('{Delete}')
414
+
415
+ def write_text(self, text, clear=False, send=False):
416
+ # todo 加at人的功能
417
+ # 1 是否清空旧数据
418
+ if clear:
419
+ self.clear_editor_content()
420
+
421
+ # 2 写入新数据
422
+ def should_use_clipboard(text):
423
+ # 简单的策略:如果文本过长或包含特殊字符,则使用剪贴板
424
+ return len(text) > 30 or not text.isprintable() or '{' in text
425
+
426
+ edit_box = self.edit_box
427
+ edit_box.Click()
428
+ if should_use_clipboard(text):
429
+ auto.SetClipboardText(text)
430
+ time.sleep(1)
431
+ edit_box.SendKeys('{Ctrl}v')
432
+ else:
433
+ edit_box.SendKeys(text)
434
+
435
+ # 3 发送内容
436
+ if send:
437
+ time.sleep(1)
438
+ edit_box.SendKeys('{Enter}')
439
+
440
+ def send_text(self, text, clear=False):
441
+ self.write_text(text, clear=clear, send=True)
442
+
443
+ def send_files(self, file_paths):
444
+ """
445
+ 发送多个文件
446
+
447
+ :param list[str] file_paths: 必选参数,为文件的路径
448
+ """
449
+ if copy_files_to_clipboard(file_paths=file_paths):
450
+ edit_box = self.edit_box
451
+ edit_box.Click()
452
+ time.sleep(1)
453
+ edit_box.SendKeys('{Ctrl}v')
454
+ time.sleep(1)
455
+ edit_box.SendKeys('{Enter}')
456
+ time.sleep(1) # 等待发送动作完成
457
+
458
+
459
+ class WeChatSingletonLock:
460
+ """ 基于 get_autoui_lock 的微信全局唯一单例控制器,确保同一时间仅有一个微信自动化程序在操作 """
461
+
462
+ def __init__(self, lock_timeout: Optional[float] = -1):
463
+ # 初始化全局锁
464
+ self.lock = get_autoui_lock(timeout=lock_timeout)
465
+ self.wechat = WeChatMainWnd()
466
+
467
+ def __enter__(self):
468
+ # 获取锁并激活微信窗口
469
+ self.lock.acquire()
470
+ self.wechat.activate()
471
+ return self.wechat
472
+
473
+ def __exit__(self, exc_type, exc_value, traceback):
474
+ # 释放锁
475
+ self.lock.release()
476
+
477
+
478
+ def wechat_lock_send(user, text=None, files=None, *, timeout=-1):
479
+ """ 使用全局唯一单例锁,确保同一时间仅有一个微信自动化程序在操作 """
480
+ with WeChatSingletonLock(timeout) as we:
481
+ we.set_chat_with(user)
482
+ if text:
483
+ we.send_text(text)
484
+ if files:
485
+ we.send_files(files)
486
+
487
+
488
+ def wechat_handler(message):
489
+ # 获取群名,如果没有指定,不使用此微信发送功能
490
+ user = message.record["extra"].get("wechat_user")
491
+ if user:
492
+ wechat_lock_send(user, message)
493
+
494
+
495
+ if sys.platform == 'win32':
496
+ # 创建专用的微信日志记录器,不绑定默认群名
497
+ wechat_logger = logger.bind(wechat_user='文件传输助手')
498
+
499
+ # 添加专用的微信处理器
500
+ wechat_logger.add(wechat_handler,
501
+ format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name}:{function}:{line} - {message}")
502
+
503
+ """ 往微信发送特殊的日志格式报告
504
+ 用法:wechat_logger.bind(wechat_user='文件传输助手').info(message)
505
+
506
+ 或者:
507
+ # 先做好默认群名绑定
508
+ wechat_logger = wechat_logger.bind(wechat_user='考勤管理')
509
+ # 然后就能普通logger用法发送了
510
+ wechat_logger.info('测试')
511
+ """
512
+ else:
513
+ # 降级为普通logger
514
+ wechat_logger = logger
515
+
516
+
517
+ def __4_wx():
518
+ """ 别人的原版实现 """
519
+
520
+
521
+ class WxOperation:
522
+ """
523
+ 微信群发消息的类,提供了与微信应用交互的方法集,用于发送消息,管理联系人列表等功能。
524
+
525
+ Attributes:
526
+ ----------
527
+ wx_window: auto.WindowControl
528
+ 微信控制窗口
529
+ input_edit: wx_window.EditControl
530
+ 聊天界面输入框编辑控制窗口
531
+
532
+ Methods:
533
+ -------
534
+ goto_chat_box(name):
535
+ 跳转到 指定好友窗口
536
+ __send_text(*msgs):
537
+ 发送文本。
538
+ __send_file(*filepath):
539
+ 发送文件
540
+ get_friend_list(tag, num):
541
+ 可指定tag,获取好友num页的好友数量
542
+ send_msg(name, msgs, file_paths=None, add_remark_name=False, at_everyone=False,
543
+ text_interval=0.05, file_interval=0.5) -> None:
544
+ 向指定的好友或群聊发送消息和文件。支持同时发送文本和文件。
545
+ """
546
+
547
+ def __init__(self):
548
+ self.wx_window = None
549
+ self.input_edit = None
550
+ self.wx_window: auto.WindowControl
551
+ self.input_edit: auto.EditControl
552
+ auto.SetGlobalSearchTimeout(Interval.BASE_INTERVAL)
553
+ self.visible_flag: bool = False
554
+
555
+ def locate_wechat_window(self):
556
+ if not self.visible_flag:
557
+ wake_up_window(class_name=WeChatConfig.WINDOW_CLASSNAME, name=WeChatConfig.WINDOW_NAME)
558
+ self.wx_window = auto.WindowControl(Name=WeChatConfig.WINDOW_NAME, ClassName=WeChatConfig.WINDOW_CLASSNAME)
559
+ if not self.wx_window.Exists(Interval.MAX_SEARCH_SECOND,
560
+ searchIntervalSeconds=Interval.MAX_SEARCH_INTERVAL):
561
+ raise Exception('微信似乎并没有登录!')
562
+ self.input_edit = self.wx_window.EditControl()
563
+ self.visible_flag = bool(self.visible_flag)
564
+ # 微信窗口置顶
565
+ self.wx_window.SetTopmost(isTopmost=True)
566
+
567
+ def match_nickname(self, name):
568
+ """获取当前面板的好友昵称"""
569
+ self.input_edit = self.wx_window.EditControl(Name=name)
570
+ if self.input_edit.Exists(Interval.MAX_SEARCH_SECOND, searchIntervalSeconds=Interval.MAX_SEARCH_INTERVAL):
571
+ return self.input_edit
572
+ return False
573
+
574
+ def goto_chat_box(self, name: str) -> bool:
575
+ """
576
+ 跳转到指定 name好友的聊天窗口。
577
+
578
+ Args:
579
+ name(str): 必选参数,好友名称
580
+
581
+ Returns:
582
+ None
583
+ """
584
+ if ctrl := self.match_nickname(name):
585
+ ctrl.SetFocus()
586
+ return ctrl
587
+
588
+ assert name, "无法跳转到名字为空的聊天窗口"
589
+ self.wx_window.SendKeys(text='{Ctrl}F', waitTime=Interval.BASE_INTERVAL)
590
+ self.wx_window.SendKeys(text='{Ctrl}A', waitTime=Interval.BASE_INTERVAL)
591
+ self.wx_window.SendKey(key=auto.SpecialKeyNames['DELETE'])
592
+ auto.SetClipboardText(text=name)
593
+ time.sleep(Interval.BASE_INTERVAL)
594
+ self.wx_window.SendKeys(text='{Ctrl}V', waitTime=Interval.BASE_INTERVAL)
595
+ # 若有匹配结果,第一个元素的类型为PaneControl
596
+ search_nodes = self.wx_window.ListControl(foundIndex=2).GetChildren()
597
+ if not isinstance(search_nodes.pop(0), auto.PaneControl):
598
+ self.wx_window.SendKeys(text='{Esc}', waitTime=Interval.BASE_INTERVAL)
599
+ raise ValueError("昵称不匹配")
600
+ # 只考虑全匹配, 不考虑好友昵称重名, 不考虑好友昵称与群聊重名
601
+ if search_nodes[0].Name == name:
602
+ self.wx_window.SendKey(key=auto.SpecialKeyNames['ENTER'], waitTime=Interval.BASE_INTERVAL)
603
+ time.sleep(Interval.BASE_INTERVAL)
604
+ return True
605
+ # 无匹配用户, 取消搜索框
606
+ self.wx_window.SendKeys(text='{Esc}', waitTime=Interval.BASE_INTERVAL)
607
+ return False
608
+
609
+ def at_at_everyone(self, group_chat_name: str):
610
+ """
611
+ @全部人的操作
612
+ Args:
613
+ group_chat_name(str): 群聊名称
614
+
615
+ """
616
+ # 个人 定位 聊天框,取 foundIndex=2,因为左侧聊天List 也可以匹配到foundIndex=1
617
+ # 群聊 定位 聊天框 需要带上群人数,故会匹配失败,所以匹配失败的就是群聊
618
+ result = self.wx_window.TextControl(Name=group_chat_name, foundIndex=2)
619
+ # 只要匹配不上,说明这是个群聊窗口
620
+ if not result.Exists(Interval.MAX_SEARCH_SECOND, searchIntervalSeconds=Interval.MAX_SEARCH_INTERVAL):
621
+ # 寻找是否有 @所有人 的选项
622
+ self.input_edit.SendKeys(text='{Shift}2', waitTime=Interval.BASE_INTERVAL)
623
+ everyone = self.wx_window.ListItemControl(Name='所有人')
624
+ if not everyone.Exists(Interval.MAX_SEARCH_SECOND, searchIntervalSeconds=Interval.MAX_SEARCH_INTERVAL):
625
+ self.input_edit.SendKeys(text='{Ctrl}A', waitTime=Interval.BASE_INTERVAL)
626
+ self.input_edit.SendKeys(text='{Delete}', waitTime=Interval.BASE_INTERVAL)
627
+ return
628
+ self.input_edit.SendKeys(text='{Up}', waitTime=Interval.BASE_INTERVAL)
629
+ self.input_edit.SendKeys(text='{Enter}', waitTime=Interval.BASE_INTERVAL)
630
+ self.input_edit.SendKeys(text='{Enter}', waitTime=Interval.BASE_INTERVAL)
631
+
632
+ def __send_text(self, *msgs, wait_time, send_shortcut) -> None:
633
+ """
634
+ 发送文本.
635
+
636
+ Args:
637
+ input_name(str): 必选参数, 为输入框
638
+ *msgs(str): 必选参数,为发送的文本
639
+ wait_time(float): 必选参数,为动态等待时间
640
+ send_shortcut(str): 必选参数,为发送快捷键
641
+
642
+ Returns:
643
+ None
644
+ """
645
+
646
+ def should_use_clipboard(text: str):
647
+ # 简单的策略:如果文本过长或包含特殊字符,则使用剪贴板
648
+ return len(text) > 30 or not text.isprintable()
649
+
650
+ for msg in msgs:
651
+ assert msg, "发送的文本内容为空"
652
+ self.input_edit.SendKeys(text='{Ctrl}a', waitTime=wait_time)
653
+ self.input_edit.SendKey(key=auto.SpecialKeyNames['DELETE'], waitTime=wait_time)
654
+ self.input_edit.SendKeys(text='{Ctrl}a', waitTime=wait_time)
655
+ self.input_edit.SendKey(key=auto.SpecialKeyNames['DELETE'], waitTime=wait_time)
656
+
657
+ if should_use_clipboard(msg):
658
+ auto.SetClipboardText(text=msg)
659
+ time.sleep(wait_time * 2.5)
660
+ self.input_edit.SendKeys(text='{Ctrl}v', waitTime=wait_time * 2)
661
+ else:
662
+ self.input_edit.SendKeys(text=msg, waitTime=wait_time * 2)
663
+
664
+ # 设置到剪切板再黏贴到输入框
665
+ self.wx_window.SendKeys(text=f'{send_shortcut}', waitTime=wait_time * 2)
666
+
667
+ def __send_file(self, *file_paths, wait_time, send_shortcut) -> None:
668
+ """
669
+ 发送文件.
670
+
671
+ Args:
672
+ *file_paths(str): 必选参数,为文件的路径
673
+ wait_time(float): 必选参数,为动态等待时间
674
+ send_shortcut(str): 必选参数,为发送快捷键
675
+
676
+ Returns:
677
+ None
678
+ """
679
+ # 复制文件到剪切板
680
+ if copy_files_to_clipboard(file_paths=file_paths):
681
+ # 粘贴到输入框
682
+ self.input_edit.SendKeys(text='{Ctrl}V', waitTime=wait_time)
683
+ # 按下回车键
684
+ self.wx_window.SendKeys(text=f'{send_shortcut}', waitTime=wait_time / 2)
685
+
686
+ time.sleep(wait_time) # 等待发送动作完成
687
+
688
+ def get_friend_list(self, tag: str = None) -> list:
689
+ """
690
+ 获取微信好友名称.
691
+
692
+ Args:
693
+ tag(str): 可选参数,如不指定,则获取所有好友
694
+
695
+ Returns:
696
+ list
697
+ """
698
+ # 定位到微信窗口
699
+ self.locate_wechat_window()
700
+ # 取消微信窗口置顶
701
+ self.wx_window.SetTopmost(isTopmost=False)
702
+ # 点击 通讯录管理
703
+ self.wx_window.ButtonControl(Name="通讯录").Click(simulateMove=False)
704
+ self.wx_window.ListControl(Name="联系人").ButtonControl(Name="通讯录管理").Click(simulateMove=False)
705
+ # 切换到通讯录管理,相当于切换到弹出来的页面
706
+ contacts_window = auto.GetForegroundControl()
707
+ contacts_window.ButtonControl(Name='最大化').Click(simulateMove=False)
708
+
709
+ if tag:
710
+ try:
711
+ contacts_window.ButtonControl(Name="标签").Click(simulateMove=False)
712
+ contacts_window.PaneControl(Name=tag).Click(simulateMove=False)
713
+ time.sleep(Interval.BASE_INTERVAL * 2)
714
+ except LookupError:
715
+ contacts_window.SendKey(auto.SpecialKeyNames['ESC'])
716
+ raise LookupError(f'找不到 {tag} 标签')
717
+
718
+ name_list = list()
719
+ last_names = None
720
+ while True:
721
+ # TODO 修改成使用 foundIndex 的方式
722
+ try:
723
+ nodes = contacts_window.ListControl(foundIndex=2).GetChildren()
724
+ except LookupError:
725
+ nodes = contacts_window.ListControl().GetChildren()
726
+ cur_names = [node.TextControl().Name for node in nodes]
727
+
728
+ # 如果滚动前后名单未变,认为到达底部
729
+ if cur_names == last_names:
730
+ break
731
+ last_names = cur_names
732
+ # 处理当前页的名单
733
+ for node in nodes:
734
+ # TODO 如果有需要, 可以处理成导出为两列的csv格式
735
+ nick_name = node.TextControl().Name # 用户名
736
+ remark_name = node.ButtonControl(foundIndex=2).Name # 用户备注名,索引1会错位,索引2是备注名,索引3是标签名
737
+ name_list.append(remark_name if remark_name else nick_name)
738
+ # 向下滚动页面
739
+ contacts_window.WheelDown(wheelTimes=8, waitTime=Interval.BASE_INTERVAL / 2)
740
+ # 结束时候关闭 "通讯录管理" 窗口
741
+ contacts_window.SendKey(auto.SpecialKeyNames['ESC'])
742
+ # 简单去重,但是存在误判(如果存在同名的好友), 保持获取时候的顺序
743
+ return list(dict.fromkeys(name_list))
744
+
745
+ def get_group_chat_list(self) -> list:
746
+ """获取群聊通讯录中的用户名称"""
747
+ name_list = list()
748
+ auto.ButtonControl(Name='聊天信息').Click()
749
+ time.sleep(0.5)
750
+ chat_members_win = self.wx_window.ListControl(Name='聊天成员')
751
+ if not chat_members_win.Exists():
752
+ return list()
753
+ self.wx_window.ButtonControl(Name='查看更多').Click()
754
+ for item in chat_members_win.GetChildren():
755
+ name_list.append(item.ButtonControl().Name)
756
+ return name_list
757
+
758
+ def send_msg(self, name, msgs=None, file_paths=None, add_remark_name=False, at_everyone=False,
759
+ text_interval=Interval.SEND_TEXT_INTERVAL, file_interval=Interval.SEND_FILE_INTERVAL,
760
+ send_shortcut='{Enter}') -> None:
761
+ """
762
+ 发送消息,可同时发送文本和文件(至少选一项
763
+
764
+ Args:
765
+ name(str):必选参数,接收消息的好友名称, 可以单发
766
+ msgs(Iterable[str], Optional): 可选参数,发送的文本消息
767
+ file_paths(Iterable[str], Optional):可选参数,发送的文件路径
768
+ add_remark_name(bool): 可选参数,是否添加备注名称发送
769
+ at_everyone(bool): 可选参数,是否@全部人
770
+ text_interval(float): 可选参数,默认为0.05
771
+ file_interval(float): 可选参数,默认为0.5
772
+ send_shortcut(str): 可选参数,默认为 Enter
773
+
774
+ Raises:
775
+ ValueError: 如果用户名为空或发送的消息和文件同时为空时抛出异常
776
+ TypeError: 如果发送的文本消息或文件路径类型不是列表或元组时抛出异常
777
+ """
778
+ # 定位到微信窗口
779
+ self.locate_wechat_window()
780
+
781
+ if not name:
782
+ raise ValueError("用户名不能为空")
783
+
784
+ if not any([msgs, file_paths]):
785
+ raise ValueError("发送的消息和文件不可同时为空")
786
+
787
+ if msgs and not isinstance(msgs, Iterable):
788
+ raise TypeError("发送的文本消息必须是可迭代的")
789
+
790
+ if file_paths and not isinstance(file_paths, Iterable):
791
+ raise TypeError("发送的文件路径必须是可迭代的")
792
+
793
+ # 如果当前面板已经是需发送好友, 则无需再次搜索跳转
794
+ if not self.match_nickname(name=name):
795
+ if not self.goto_chat_box(name=name):
796
+ raise NameError('昵称不匹配')
797
+
798
+ # 设置输入框为当前焦点
799
+ self.input_edit = self.wx_window.EditControl(Name=name)
800
+ self.input_edit.SetFocus()
801
+
802
+ # @所有人
803
+ if at_everyone:
804
+ auto.SetGlobalSearchTimeout(Interval.BASE_INTERVAL)
805
+ self.at_at_everyone(group_chat_name=name)
806
+ auto.SetGlobalSearchTimeout(Interval.BASE_INTERVAL * 25)
807
+
808
+ # TODO 添加备注可以多做一个选项,添加到每条消息的前面,如xxx,早上好
809
+ if msgs and add_remark_name:
810
+ new_msgs = deepcopy(list(msgs))
811
+ new_msgs.insert(0, name)
812
+ self.__send_text(*new_msgs, wait_time=text_interval, send_shortcut=send_shortcut)
813
+ elif msgs:
814
+ self.__send_text(*msgs, wait_time=text_interval, send_shortcut=send_shortcut)
815
+ if file_paths:
816
+ self.__send_file(*file_paths, wait_time=file_interval, send_shortcut=send_shortcut)
817
+
818
+ # 取消微信窗口置顶
819
+ self.wx_window.SetTopmost(isTopmost=False)
820
+
821
+
822
+ if __name__ == '__main__':
823
+ # wx = WxOperation()
824
+ # data = wx.get_friend_list('无标签')
825
+ # print(data)
826
+ # print(len(data))
827
+ pass