MuMu.py 60 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354
  1. import random
  2. import subprocess
  3. import json
  4. import time
  5. import os
  6. import tkinter as tk
  7. from tkinter import ttk, scrolledtext, messagebox, filedialog
  8. import threading
  9. import configparser
  10. from datetime import datetime
  11. # 用于存储线程的列表
  12. threads = []
  13. class MuMuEmulatorManager:
  14. # 类级别的剪贴板锁,所有实例共享
  15. _clipboard_lock = threading.Lock()
  16. def __init__(self, manager_path=r"D:\MuMuPlayer\nx_main\MuMuManager.exe"):
  17. self.manager_path = manager_path
  18. if not os.path.exists(manager_path):
  19. raise FileNotFoundError(f"找不到 MuMuManager.exe: {manager_path}")
  20. def get_adb_port(self, index, log_callback=None):
  21. """实时获取指定模拟器的 ADB 端口,获取不到就一直等待直到成功"""
  22. while True:
  23. cmd = [self.manager_path, "info", "-v", str(index)]
  24. result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8')
  25. if result.returncode == 0:
  26. try:
  27. data = json.loads(result.stdout)
  28. adb_port = data.get('adb_port')
  29. if adb_port is not None:
  30. if log_callback:
  31. log_callback(f"✅ 模拟器 {index} ADB端口: {adb_port}")
  32. return adb_port
  33. except:
  34. pass
  35. if log_callback:
  36. log_callback(f"⏳ 模拟器 {index} 等待ADB端口...")
  37. time.sleep(3) # 等待3秒后重试
  38. def get_emulator_list(self):
  39. """获取所有模拟器列表"""
  40. cmd = [self.manager_path, "info", "-v", "all"]
  41. result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8')
  42. if result.returncode != 0:
  43. return []
  44. try:
  45. data = json.loads(result.stdout)
  46. emulators = []
  47. for key, value in data.items():
  48. if isinstance(value, dict):
  49. value['index'] = key
  50. emulators.append(value)
  51. return emulators
  52. except json.JSONDecodeError:
  53. return []
  54. def start_emulator(self, index):
  55. """启动指定索引的模拟器"""
  56. cmd = [self.manager_path, "control", "-v", str(index), "launch"]
  57. result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8')
  58. return result.returncode == 0
  59. def wait_for_emulator_ready(self, index, timeout=120, check_interval=3, log_callback=None):
  60. """等待模拟器启动完成"""
  61. start_time = time.time()
  62. while time.time() - start_time < timeout:
  63. cmd = [self.manager_path, "info", "-v", str(index)]
  64. result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8')
  65. if result.returncode == 0:
  66. try:
  67. data = json.loads(result.stdout)
  68. if data.get('is_android_started') == True:
  69. return True
  70. except:
  71. pass
  72. if log_callback:
  73. log_callback(f"等待模拟器 {index} 启动... ({int(time.time() - start_time)}秒)")
  74. time.sleep(check_interval)
  75. return False
  76. def stop_emulator(self, index):
  77. """关闭模拟器"""
  78. cmd = [self.manager_path, "control", "-v", str(index), "shutdown"]
  79. result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8')
  80. return result.returncode == 0
  81. def clear_app_data(self, index, manager, log_callback):
  82. """清除指定应用的数据"""
  83. manager.tap(index, 59, 90, log_callback)
  84. time.sleep(2)
  85. manager.tap(index, 59, 90, log_callback)
  86. time.sleep(2)
  87. manager.tap(index, 59, 90, log_callback)
  88. time.sleep(2)
  89. manager.tap(index, 59, 90, log_callback)
  90. time.sleep(2)
  91. color5990 = manager.get_pixel_color(index, 59, 90, log_callback=log_callback)
  92. log_callback(color5990)
  93. if color5990 == "#F3F3F3":
  94. manager.tap(index, 645, 1240, log_callback)
  95. time.sleep(2)
  96. manager.tap(index, 663, 91, log_callback)
  97. time.sleep(2)
  98. manager.tap(index, 663, 91, log_callback)
  99. time.sleep(2)
  100. manager.tap(index, 144, 239, log_callback)
  101. time.sleep(2)
  102. manager.tap(index, 592, 618, log_callback)
  103. time.sleep(2)
  104. def install_apk(self, index, apk_path, log_callback=None):
  105. """安装APK"""
  106. if not os.path.exists(apk_path):
  107. if log_callback:
  108. log_callback(f"❌ APK文件不存在: {apk_path}")
  109. return False
  110. adb_port = self.get_adb_port(index)
  111. target_device = f"127.0.0.1:{adb_port}"
  112. # 连接ADB
  113. subprocess.run(f"adb connect {target_device}", shell=True, capture_output=True)
  114. time.sleep(2)
  115. # 检查是否已安装
  116. check_cmd = f"adb -s {target_device} shell pm list packages | findstr \"com.dragon.read\""
  117. check_result = subprocess.run(check_cmd, shell=True, capture_output=True, text=True)
  118. if "com.dragon.read" in check_result.stdout:
  119. if log_callback:
  120. log_callback(f"✅ com.dragon.read 已安装,跳过安装步骤")
  121. return True
  122. # 安装APK
  123. if log_callback:
  124. log_callback(f"正在安装APK: {os.path.basename(apk_path)} 端口: {adb_port}...")
  125. install_cmd = f"adb -s {target_device} install -r \"{apk_path}\""
  126. result = subprocess.run(install_cmd, shell=True, capture_output=True, text=True)
  127. return "Success" in result.stdout
  128. def open_app(self, index, package_name, log_callback=None):
  129. """打开应用"""
  130. adb_port = self.get_adb_port(index)
  131. target_device = f"127.0.0.1:{adb_port}"
  132. subprocess.run(f"adb connect {target_device}", shell=True, capture_output=True)
  133. time.sleep(1)
  134. cmd = f"adb -s {target_device} shell monkey -p {package_name} -c android.intent.category.LAUNCHER 1"
  135. result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
  136. if "Events injected" in result.stdout or result.returncode == 0:
  137. if log_callback:
  138. log_callback(f"✅ 已打开应用: {package_name}")
  139. return True
  140. return False
  141. # 替代方案:直接发送文本字符,不用剪贴板
  142. def paste_text(self, index, text, log_callback=None):
  143. adb_port = self.get_adb_port(index)
  144. target_device = f"127.0.0.1:{adb_port}"
  145. subprocess.run(f"adb connect {target_device}", shell=True, capture_output=True)
  146. time.sleep(0.3)
  147. # 直接通过 ADB 输入文本(逐字符)
  148. # 先确保输入框获得焦点(点击一下)
  149. subprocess.run(f"adb -s {target_device} shell input tap 390 90", shell=True)
  150. time.sleep(0.5)
  151. # 使用 adb shell input text 输入(会自动处理空格和特殊字符)
  152. # 注意:需要用 %s 转义空格
  153. safe_text = text.replace(' ', '%s').replace('&', '\\&')
  154. subprocess.run(f"adb -s {target_device} shell input text '{safe_text}'", shell=True)
  155. if log_callback:
  156. log_callback(f"✅ 已输入: {text[:50]}{'...' if len(text) > 50 else ''}")
  157. return True
  158. def tap(self, index, x, y, log_callback=None):
  159. """点击坐标"""
  160. adb_port = self.get_adb_port(index)
  161. target_device = f"127.0.0.1:{adb_port}"
  162. subprocess.run(f"adb connect {target_device}", shell=True, capture_output=True)
  163. cmd = f"adb -s {target_device} shell input tap {x} {y}"
  164. result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
  165. if result.returncode == 0:
  166. if log_callback:
  167. log_callback(f"✅ 点击坐标 ({x}, {y})")
  168. return True
  169. return False
  170. def swipe(self, index, x1, y1, x2, y2, duration_ms=300, log_callback=None):
  171. """从坐标 (x1, y1) 滑动到 (x2, y2)"""
  172. adb_port = self.get_adb_port(index)
  173. target_device = f"127.0.0.1:{adb_port}"
  174. subprocess.run(f"adb connect {target_device}", shell=True, capture_output=True)
  175. cmd = f"adb -s {target_device} shell input swipe {x1} {y1} {x2} {y2} {duration_ms}"
  176. result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
  177. if result.returncode == 0:
  178. if log_callback:
  179. log_callback(f"✅ 滑动从 ({x1}, {y1}) 到 ({x2}, {y2}),耗时 {duration_ms}ms")
  180. return True
  181. else:
  182. if log_callback:
  183. log_callback(f"❌ 滑动失败: {result.stderr}")
  184. return False
  185. def get_screen_size(self, index, log_callback=None):
  186. """获取模拟器屏幕分辨率,返回 (width, height)"""
  187. adb_port = self.get_adb_port(index)
  188. target_device = f"127.0.0.1:{adb_port}"
  189. subprocess.run(f"adb connect {target_device}", shell=True, capture_output=True)
  190. time.sleep(0.5)
  191. get_resolution_cmd = f"adb -s {target_device} shell wm size"
  192. result = subprocess.run(get_resolution_cmd, shell=True, capture_output=True, text=True)
  193. if result.stdout:
  194. import re
  195. match = re.search(r'(\d+)x(\d+)', result.stdout)
  196. if match:
  197. width = int(match.group(1))
  198. height = int(match.group(2))
  199. if log_callback:
  200. log_callback(f"📱 模拟器 {index} 分辨率: {width}x{height}")
  201. return width, height
  202. return None, None
  203. def check_resolution(self, index, expected_width=720, log_callback=None):
  204. """检查分辨率宽度是否符合要求,返回 True/False"""
  205. width, height = self.get_screen_size(index, log_callback)
  206. if width is None:
  207. if log_callback:
  208. log_callback(f"⚠️ 模拟器 {index} 无法获取分辨率")
  209. return False
  210. return width == expected_width
  211. def get_pixel_color(self, index, x, y, log_callback=None):
  212. """获取模拟器内指定坐标点的颜色"""
  213. adb_port = self.get_adb_port(index)
  214. target_device = f"127.0.0.1:{adb_port}"
  215. # 连接ADB并验证连接
  216. subprocess.run(f"adb connect {target_device}", shell=True, capture_output=True)
  217. time.sleep(0.5)
  218. # 验证设备是否在线
  219. verify_cmd = f"adb -s {target_device} shell echo 1"
  220. verify_result = subprocess.run(verify_cmd, shell=True, capture_output=True, text=True)
  221. if verify_result.returncode != 0:
  222. if log_callback:
  223. log_callback(f"⚠️ 模拟器 {index} ADB 连接失败,重新连接...")
  224. subprocess.run(f"adb disconnect {target_device}", shell=True, capture_output=True)
  225. time.sleep(1)
  226. subprocess.run(f"adb connect {target_device}", shell=True, capture_output=True)
  227. time.sleep(1)
  228. try:
  229. # 获取屏幕分辨率
  230. get_resolution_cmd = f"adb -s {target_device} shell wm size"
  231. resolution_result = subprocess.run(get_resolution_cmd, shell=True, capture_output=True, text=True)
  232. if resolution_result.stdout:
  233. import re
  234. match = re.search(r'(\d+)x(\d+)', resolution_result.stdout)
  235. if match:
  236. screen_width = int(match.group(1))
  237. screen_height = int(match.group(2))
  238. else:
  239. screen_width = 720
  240. screen_height = 1280
  241. else:
  242. screen_width = 720
  243. screen_height = 1280
  244. if log_callback:
  245. log_callback(f"📱 屏幕分辨率: {screen_width}x{screen_height}")
  246. # 使用唯一的临时文件名(包含线程ID和时间戳)
  247. import threading
  248. thread_id = threading.current_thread().ident
  249. temp_local_file = f"temp_screenshot_{thread_id}_{int(time.time()*1000)}.png"
  250. # 截图并保存到本地
  251. screenshot_cmd = f"adb -s {target_device} exec-out screencap -p > {temp_local_file}"
  252. subprocess.run(screenshot_cmd, shell=True, capture_output=True, text=True)
  253. time.sleep(0.3)
  254. # 检查文件是否存在且不为空
  255. if os.path.exists(temp_local_file) and os.path.getsize(temp_local_file) > 0:
  256. try:
  257. from PIL import Image
  258. # 打开图片
  259. img = Image.open(temp_local_file)
  260. # 确保坐标在范围内
  261. width, height = img.size
  262. if x < 0 or x >= width or y < 0 or y >= height:
  263. if log_callback:
  264. log_callback(f"❌ 坐标({x},{y})超出屏幕范围 {width}x{height}")
  265. os.remove(temp_local_file)
  266. return None
  267. # 获取像素颜色
  268. pixel = img.getpixel((x, y))
  269. # 转换为十六进制颜色值
  270. if isinstance(pixel, tuple):
  271. if len(pixel) >= 3:
  272. r, g, b = pixel[0], pixel[1], pixel[2]
  273. else:
  274. r, g, b = pixel, pixel, pixel
  275. else:
  276. r = g = b = pixel
  277. color = f"#{r:02X}{g:02X}{b:02X}"
  278. # 关闭图片并清理临时文件
  279. img.close()
  280. if os.path.exists(temp_local_file):
  281. os.remove(temp_local_file)
  282. if log_callback:
  283. log_callback(f"🎨 坐标({x},{y}) 颜色: {color}")
  284. return color
  285. except ImportError:
  286. if log_callback:
  287. log_callback("❌ 请先安装PIL库: pip install Pillow")
  288. if os.path.exists(temp_local_file):
  289. os.remove(temp_local_file)
  290. return None
  291. except Exception as e:
  292. if log_callback:
  293. log_callback(f"❌ 解析图片失败: {e}")
  294. if os.path.exists(temp_local_file):
  295. try:
  296. os.remove(temp_local_file)
  297. except:
  298. pass
  299. return None
  300. else:
  301. if log_callback:
  302. log_callback("❌ 截图失败")
  303. if os.path.exists(temp_local_file):
  304. try:
  305. os.remove(temp_local_file)
  306. except:
  307. pass
  308. return None
  309. except Exception as e:
  310. if log_callback:
  311. log_callback(f"❌ 获取颜色失败: {e}")
  312. return None
  313. class MuMuAutoGUI:
  314. def __init__(self):
  315. self.root = tk.Tk()
  316. self.root.title("MuMu模拟器自动化工具")
  317. self.root.geometry("700x360")
  318. # 配置文件
  319. self.config_file = "mumu_config.ini"
  320. self.config = configparser.ConfigParser()
  321. self.load_config()
  322. # 运行状态
  323. self.is_running = False
  324. self.is_paused = False
  325. self.should_stop = False
  326. self.current_thread = None
  327. self.selected_emulators = []
  328. self.selected_emulators = []
  329. self.thread_semaphore = None # 添加信号量控制并发数
  330. self.active_threads = 0 # 记录当前活跃线程数
  331. self.threads_lock = threading.Lock() # 线程锁
  332. self.load_btn = None
  333. self.start_read_btn = None
  334. self.start_comment_btn = None
  335. self.results = {}
  336. # 创建界面
  337. self.create_widgets()
  338. # 加载保存的配置
  339. self.load_settings()
  340. def load_config(self):
  341. """加载配置文件"""
  342. if os.path.exists(self.config_file):
  343. self.config.read(self.config_file, encoding='utf-8')
  344. else:
  345. self.config['Settings'] = {
  346. 'mumu_path': r'D:\MuMuPlayer\nx_main\MuMuManager.exe',
  347. 'apk_path': 'fanqie.apk',
  348. 'package_name': 'com.dragon.read',
  349. 'search_content': '玄幻战神:开局就得到大佬的守护'
  350. }
  351. def save_config(self):
  352. """保存配置文件"""
  353. with open(self.config_file, 'w', encoding='utf-8') as f:
  354. self.config.write(f)
  355. def load_settings(self):
  356. """加载设置到界面"""
  357. self.mumu_path_var.set(self.config['Settings']['mumu_path'] if 'mumu_path' in self.config['Settings'] else '')
  358. self.apk_path_var.set(self.config['Settings']['apk_path'] if 'apk_path' in self.config['Settings'] else '')
  359. self.package_name_var.set(self.config['Settings']['package_name'] if 'package_name' in self.config['Settings'] else '')
  360. self.search_content_var.set(self.config['Settings']['search_content'] if 'search_content' in self.config['Settings'] else '盗墓笔记')
  361. self.page_count_var.set(self.config['Settings']['page_count_var'] if 'page_count_var' in self.config['Settings'] else '10-30')
  362. self.page_interval_var.set(self.config['Settings']['page_interval_var'] if 'page_interval_var' in self.config['Settings'] else '5')
  363. self.max_threads_var.set(self.config['Settings']['max_threads_var'] if 'max_threads_var' in self.config['Settings'] else '1')
  364. def save_settings(self):
  365. """保存界面设置到文件"""
  366. self.config['Settings']['mumu_path'] = self.mumu_path_var.get()
  367. self.config['Settings']['apk_path'] = self.apk_path_var.get()
  368. self.config['Settings']['package_name'] = self.package_name_var.get()
  369. self.config['Settings']['search_content'] = self.search_content_var.get()
  370. self.config['Settings']['page_count_var'] = self.page_count_var.get()
  371. self.config['Settings']['page_interval_var'] = self.page_interval_var.get()
  372. self.config['Settings']['max_threads_var'] = self.max_threads_var.get()
  373. self.save_config()
  374. self.log_message("✅ 配置已保存")
  375. def create_widgets(self):
  376. """创建界面组件"""
  377. # 创建选项卡
  378. self.notebook = ttk.Notebook(self.root)
  379. self.notebook.pack(fill='both', expand=True, padx=5, pady=5)
  380. # 配置选项卡
  381. self.create_config_tab()
  382. # 任务选项卡
  383. self.create_task_tab()
  384. # 日志选项卡
  385. self.create_log_tab()
  386. def create_config_tab(self):
  387. """创建配置选项卡"""
  388. config_frame = ttk.Frame(self.notebook)
  389. self.notebook.add(config_frame, text="配置")
  390. # 创建滚动框架
  391. canvas = tk.Canvas(config_frame)
  392. scrollbar = ttk.Scrollbar(config_frame, orient="vertical", command=canvas.yview)
  393. scrollable_frame = ttk.Frame(canvas)
  394. scrollable_frame.bind(
  395. "<Configure>",
  396. lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
  397. )
  398. canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
  399. canvas.configure(yscrollcommand=scrollbar.set)
  400. # 配置项
  401. row = 0
  402. # MuMu路径
  403. ttk.Label(scrollable_frame, text="MuMuManager路径:").grid(row=row, column=0, sticky='w', padx=10, pady=5)
  404. self.mumu_path_var = tk.StringVar()
  405. mumu_entry = ttk.Entry(scrollable_frame, textvariable=self.mumu_path_var, width=60)
  406. mumu_entry.grid(row=row, column=1, padx=10, pady=5)
  407. ttk.Button(scrollable_frame, text="浏览", command=self.browse_mumu_path).grid(row=row, column=2, padx=5, pady=5)
  408. row += 1
  409. # APK路径
  410. ttk.Label(scrollable_frame, text="APK文件路径:").grid(row=row, column=0, sticky='w', padx=10, pady=5)
  411. self.apk_path_var = tk.StringVar()
  412. apk_entry = ttk.Entry(scrollable_frame, textvariable=self.apk_path_var, width=60)
  413. apk_entry.grid(row=row, column=1, padx=10, pady=5)
  414. ttk.Button(scrollable_frame, text="浏览", command=self.browse_apk_path).grid(row=row, column=2, padx=5, pady=5)
  415. row += 1
  416. # 包名
  417. ttk.Label(scrollable_frame, text="应用包名:").grid(row=row, column=0, sticky='w', padx=10, pady=5)
  418. self.package_name_var = tk.StringVar()
  419. ttk.Entry(scrollable_frame, textvariable=self.package_name_var, width=40).grid(row=row, column=1, sticky='w', padx=10, pady=5)
  420. row += 1
  421. # 搜索内容
  422. ttk.Label(scrollable_frame, text="搜索内容:").grid(row=row, column=0, sticky='w', padx=10, pady=5)
  423. self.search_content_var = tk.StringVar()
  424. ttk.Entry(scrollable_frame, textvariable=self.search_content_var, width=60).grid(row=row, column=1, padx=10, pady=5)
  425. row += 1
  426. # 翻页间隔
  427. ttk.Label(scrollable_frame, text="翻页间隔(秒):").grid(row=row, column=0, sticky='w', padx=10, pady=5)
  428. self.page_interval_var = tk.StringVar()
  429. ttk.Entry(scrollable_frame, textvariable=self.page_interval_var, width=60).grid(row=row, column=1, padx=10, pady=5)
  430. row += 1
  431. # 阅读页数
  432. ttk.Label(scrollable_frame, text="阅读页数:").grid(row=row, column=0, sticky='w', padx=10, pady=5)
  433. self.page_count_var = tk.StringVar()
  434. ttk.Entry(scrollable_frame, textvariable=self.page_count_var, width=60).grid(row=row, column=1, padx=10, pady=5)
  435. row += 1
  436. # 最大线程
  437. ttk.Label(scrollable_frame, text="最大线程:").grid(row=row, column=0, sticky='w', padx=10, pady=5)
  438. self.max_threads_var = tk.StringVar()
  439. ttk.Entry(scrollable_frame, textvariable=self.max_threads_var, width=60).grid(row=row, column=1, padx=10, pady=5)
  440. row += 1
  441. # 保存按钮
  442. ttk.Button(scrollable_frame, text="保存配置", command=self.save_settings).grid(row=row, column=0, columnspan=3, pady=20)
  443. canvas.pack(side="left", fill="both", expand=True)
  444. scrollbar.pack(side="right", fill="y")
  445. def select_failed_emulators(self):
  446. """选择所有执行失败的模拟器"""
  447. if not self.results:
  448. self.log_message("⚠️ 没有任务执行记录,请先运行任务")
  449. return
  450. failed_indices = [idx for idx, success in self.results.items() if not success]
  451. if not failed_indices:
  452. self.log_message("✅ 没有失败的模拟器")
  453. return
  454. # 清空当前选择
  455. for item in self.emulator_tree.get_children():
  456. values = self.emulator_tree.item(item, 'values')
  457. if values[0] == "☑":
  458. self.emulator_tree.item(item, values=("□", values[1], values[2], values[3]))
  459. # 选中失败的模拟器
  460. selected_count = 0
  461. for item in self.emulator_tree.get_children():
  462. values = self.emulator_tree.item(item, 'values')
  463. if values[1] in failed_indices:
  464. self.emulator_tree.item(item, values=("☑", values[1], values[2], values[3]))
  465. selected_count += 1
  466. self.log_message(f"✅ 已选中 {selected_count} 个失败模拟器: {', '.join(failed_indices)}")
  467. def create_task_tab(self):
  468. """创建任务选项卡"""
  469. task_frame = ttk.Frame(self.notebook)
  470. self.notebook.add(task_frame, text="任务")
  471. # 上部:模拟器列表
  472. list_frame = ttk.LabelFrame(task_frame, text="任务控制")
  473. list_frame.pack(fill='both', expand=True, padx=5, pady=5)
  474. # 按钮栏
  475. button_frame = ttk.Frame(list_frame)
  476. button_frame.pack(fill='x', padx=5, pady=5)
  477. self.load_btn = ttk.Button(button_frame, text="读取模拟器", command=self.load_emulators)
  478. self.load_btn.pack(side='left', padx=5)
  479. # 添加全选按钮
  480. self.select_all_btn = ttk.Button(button_frame, text="全选", command=self.select_all_emulators, width=6)
  481. self.select_all_btn.pack(side='left', padx=5)
  482. # 添加选择失败项按钮
  483. self.select_failed_btn = ttk.Button(button_frame, text="选择失败项", command=self.select_failed_emulators, width=10)
  484. self.select_failed_btn.pack(side='left', padx=5)
  485. self.start_read_btn = ttk.Button(button_frame, text="开始阅读", command=self.start_task, width=10)
  486. self.start_read_btn.pack(side='left', padx=10)
  487. self.start_comment_btn = ttk.Button(button_frame, text="开始评价", command=self.start_task2, width=10)
  488. self.start_comment_btn.pack(side='left', padx=10)
  489. self.pause_btn = ttk.Button(button_frame, text="暂停", command=self.pause_task, width=10, state='disabled')
  490. self.pause_btn.pack(side='left', padx=10)
  491. self.stop_btn = ttk.Button(button_frame, text="停止", command=self.stop_task, width=10, state='disabled')
  492. self.stop_btn.pack(side='left', padx=10)
  493. # 模拟器列表(带复选框)
  494. tree_frame = ttk.Frame(list_frame)
  495. tree_frame.pack(fill='both', expand=True, padx=5, pady=5)
  496. # 创建Treeview
  497. columns = ("选择", "索引", "名称", "状态")
  498. self.emulator_tree = ttk.Treeview(tree_frame, columns=columns, show='headings', height=8)
  499. # 设置列标题
  500. self.emulator_tree.heading("选择", text="选择")
  501. self.emulator_tree.heading("索引", text="索引")
  502. self.emulator_tree.heading("名称", text="名称")
  503. self.emulator_tree.heading("状态", text="状态")
  504. # 设置列宽
  505. self.emulator_tree.column("选择", width=50)
  506. self.emulator_tree.column("索引", width=50)
  507. self.emulator_tree.column("名称", width=150)
  508. self.emulator_tree.column("状态", width=100)
  509. # 添加滚动条
  510. vsb = ttk.Scrollbar(tree_frame, orient="vertical", command=self.emulator_tree.yview)
  511. self.emulator_tree.configure(yscrollcommand=vsb.set)
  512. self.emulator_tree.pack(side='left', fill='both', expand=True)
  513. vsb.pack(side='right', fill='y')
  514. # 绑定双击选择
  515. self.emulator_tree.bind('<Button-1>', self.on_tree_click)
  516. def create_log_tab(self):
  517. """创建日志选项卡"""
  518. log_frame = ttk.Frame(self.notebook)
  519. self.notebook.add(log_frame, text="日志")
  520. # 日志文本框
  521. self.log_text = scrolledtext.ScrolledText(log_frame, wrap=tk.WORD, height=20)
  522. self.log_text.pack(fill='both', expand=True, padx=5, pady=5)
  523. # 清空按钮
  524. btn_frame = ttk.Frame(log_frame)
  525. btn_frame.pack(fill='x', padx=5, pady=5)
  526. ttk.Button(btn_frame, text="清空日志", command=self.clear_log).pack(side='right')
  527. def browse_mumu_path(self):
  528. """浏览MuMuManager路径"""
  529. path = filedialog.askopenfilename(title="选择MuMuManager.exe", filetypes=[("Executable", "*.exe")])
  530. if path:
  531. self.mumu_path_var.set(path)
  532. def browse_apk_path(self):
  533. """浏览APK文件"""
  534. path = filedialog.askopenfilename(title="选择APK文件", filetypes=[("APK", "*.apk")])
  535. if path:
  536. self.apk_path_var.set(path)
  537. def select_all_emulators(self):
  538. """全选所有模拟器"""
  539. for item in self.emulator_tree.get_children():
  540. values = self.emulator_tree.item(item, 'values')
  541. if values[0] == "□":
  542. self.emulator_tree.item(item, values=("☑", values[1], values[2], values[3]))
  543. self.log_message("已全选所有模拟器")
  544. def update_emulator_status(self, index, status):
  545. """更新指定索引的模拟器状态显示
  546. Args:
  547. index: 模拟器索引(字符串)
  548. status: 状态文本,如 "运行中" 或 "未运行"
  549. """
  550. for item in self.emulator_tree.get_children():
  551. values = self.emulator_tree.item(item, 'values')
  552. if values[1] == str(index):
  553. self.emulator_tree.item(item, values=(values[0], values[1], values[2], status))
  554. break
  555. def load_emulators(self):
  556. """加载模拟器列表"""
  557. try:
  558. manager = MuMuEmulatorManager(self.mumu_path_var.get())
  559. emulators = manager.get_emulator_list()
  560. # 清空现有列表
  561. for item in self.emulator_tree.get_children():
  562. self.emulator_tree.delete(item)
  563. # 添加模拟器
  564. for emu in emulators:
  565. status = "运行中" if emu.get('is_process_started') else "未运行"
  566. self.emulator_tree.insert('', 'end', values=("□", emu.get('index'), emu.get('name'), status))
  567. self.log_message(f"已加载 {len(emulators)} 个模拟器")
  568. except Exception as e:
  569. self.log_message(f"加载模拟器失败: {e}")
  570. def on_tree_click(self, event):
  571. """处理列表点击选择"""
  572. region = self.emulator_tree.identify_region(event.x, event.y)
  573. if region == "cell":
  574. column = self.emulator_tree.identify_column(event.x)
  575. if column == "#1": # 选择列
  576. item = self.emulator_tree.identify_row(event.y)
  577. if item:
  578. values = self.emulator_tree.item(item, 'values')
  579. current = values[0]
  580. new_value = "☑" if current == "□" else "□"
  581. self.emulator_tree.item(item, values=(new_value, values[1], values[2], values[3]))
  582. def get_selected_emulators(self):
  583. """获取选中的模拟器"""
  584. selected = []
  585. for item in self.emulator_tree.get_children():
  586. values = self.emulator_tree.item(item, 'values')
  587. if values[0] == "☑":
  588. selected.append({
  589. 'index': values[1],
  590. 'name': values[2]
  591. })
  592. return selected
  593. def log_message(self, message):
  594. """添加日志"""
  595. timestamp = datetime.now().strftime("%H:%M:%S")
  596. self.log_text.insert(tk.END, f"[{timestamp}] {message}\n")
  597. self.log_text.see(tk.END)
  598. self.root.update()
  599. def start_task(self):
  600. """开始阅读任务"""
  601. selected = self.get_selected_emulators()
  602. if not selected:
  603. messagebox.showwarning("警告", "请至少选择一个模拟器")
  604. return
  605. self.selected_emulators = selected
  606. self.is_running = True
  607. self.is_paused = False
  608. self.should_stop = False
  609. # 禁用相关按钮
  610. self.load_btn.config(state='disabled')
  611. self.start_read_btn.config(state='disabled')
  612. self.start_comment_btn.config(state='disabled')
  613. self.pause_btn.config(state='normal')
  614. self.stop_btn.config(state='normal')
  615. # 在新线程中运行任务
  616. self.current_thread = threading.Thread(target=self.run_task, daemon=True)
  617. self.current_thread.start()
  618. def start_task2(self):
  619. """开始评价任务"""
  620. selected = self.get_selected_emulators()
  621. if not selected:
  622. messagebox.showwarning("警告", "请至少选择一个模拟器")
  623. return
  624. self.selected_emulators = selected
  625. self.is_running = True
  626. self.is_paused = False
  627. self.should_stop = False
  628. # 禁用相关按钮
  629. self.load_btn.config(state='disabled')
  630. self.start_read_btn.config(state='disabled')
  631. self.start_comment_btn.config(state='disabled')
  632. self.pause_btn.config(state='normal')
  633. self.stop_btn.config(state='normal')
  634. # 在新线程中运行评价任务
  635. self.current_thread = threading.Thread(target=self.run_task2, daemon=True)
  636. self.current_thread.start()
  637. def pause_task(self):
  638. """暂停任务"""
  639. if self.is_running and not self.is_paused:
  640. self.is_paused = True
  641. self.pause_btn.config(text="继续")
  642. self.log_message("⏸ 任务已暂停")
  643. elif self.is_running and self.is_paused:
  644. self.is_paused = False
  645. self.pause_btn.config(text="暂停")
  646. self.log_message("▶️ 任务已继续")
  647. def stop_task(self):
  648. """停止任务"""
  649. if self.is_running:
  650. self.should_stop = True
  651. self.is_running = False
  652. self.is_paused = False
  653. self.log_message("⏹ 正在停止任务...")
  654. def openBook(self, manager, index):
  655. """打开书籍,遇到错误返回False"""
  656. # 执行操作
  657. manager.tap(index, 390, 90, self.log_message)
  658. errNumber = 0
  659. while True:
  660. color56065 = manager.get_pixel_color(index, 560, 65, log_callback=self.log_message)
  661. if color56065 == "#F7F7F7":
  662. break
  663. elif color56065 == "#EBF8EC":
  664. manager.tap(index, 390, 90, self.log_message)
  665. self.log_message(f"模拟器 {index} 尝试重新点击搜索...")
  666. elif color56065 == "#5E635E":
  667. self.log_message(f"模拟器 {index} 需要关闭红包弹窗...")
  668. # manager.tap(index, 640, 260, self.log_message)
  669. manager.tap(index, 360, 1035, self.log_message)
  670. time.sleep(20)
  671. manager.tap(index, 57, 193, self.log_message)
  672. elif color56065 in ["#EBE8E4", "#CDD0D1"]:
  673. self.log_message(f"模拟器 {index} 进入错误页面,返回...")
  674. manager.tap(index, 44, 92, self.log_message)
  675. else:
  676. errNumber = errNumber + 1
  677. if errNumber > 30:
  678. self.log_message(f"模拟器 {index} 连续多次未检测到搜索页面,设置错误并退出...")
  679. manager.stop_emulator(index)
  680. return False
  681. self.log_message(f"模拟器 {index} 等待搜索页面准备就绪...")
  682. time.sleep(3)
  683. time.sleep(3)
  684. # 点输入框
  685. manager.tap(index, 340, 90, self.log_message)
  686. time.sleep(3)
  687. # 黏贴
  688. manager.paste_text(index, self.search_content_var.get(), self.log_message)
  689. time.sleep(3)
  690. errNumber = 0
  691. while True:
  692. if manager.get_pixel_color(index, 435, 880, log_callback=self.log_message) == "#FFFFFF":
  693. break
  694. errNumber = errNumber + 1
  695. if errNumber > 30:
  696. self.log_message(f"模拟器 {index} 连续多次未检测到搜索页面,设置错误并退出...")
  697. manager.stop_emulator(index)
  698. return False
  699. self.log_message(f"模拟器 {index} 没有输入搜索内容...")
  700. # 点输入框
  701. manager.tap(index, 556, 87, self.log_message)
  702. time.sleep(1)
  703. manager.tap(index, 340, 90, self.log_message)
  704. time.sleep(2)
  705. # 黏贴
  706. manager.paste_text(index, self.search_content_var.get(), self.log_message)
  707. time.sleep(3)
  708. manager.tap(index, 655, 92, self.log_message)
  709. time.sleep(6)
  710. errNumber = 0
  711. while True:
  712. color630235 = manager.get_pixel_color(index, 630, 235, log_callback=self.log_message)
  713. if color630235 in ["#FFFFFF"]:
  714. self.log_message(f"模拟器 {index} 已经在搜索结果页面,继续...")
  715. break
  716. elif color630235 in ["#E8E3CE", "#E0DBC6", "#CCCBCB", "#DFDAC5", "#141000"]:
  717. self.log_message(f"模拟器 {index} 已经在看书目录界面,继续...")
  718. break
  719. elif color630235 in ["#F9F9FC"]:
  720. self.log_message(f"模拟器 {index} 关闭广告弹窗,继续...")
  721. manager.tap(index, 634, 123, self.log_message)
  722. else:
  723. errNumber = errNumber + 1
  724. if errNumber > 30:
  725. self.log_message(f"模拟器 {index} 连续多次未检测到搜索页面,设置错误并退出...")
  726. manager.stop_emulator(index)
  727. return False
  728. self.log_message(f"模拟器 {index} 等待搜索结果...")
  729. time.sleep(3)
  730. # 进入书目
  731. time.sleep(6)
  732. manager.tap(index, 355, 333, self.log_message)
  733. time.sleep(5)
  734. errNumber = 0
  735. while True:
  736. color630235 = manager.get_pixel_color(index, 630, 235, log_callback=self.log_message)
  737. if color630235 in ["#E8E3CE", "#E0DBC6", "#CCCBCB", "#DFDAC5", "#141000", "#E3DEC9"]:
  738. self.log_message(f"模拟器 {index} 已经在看书目录界面,继续...")
  739. return True
  740. else:
  741. errNumber = errNumber + 1
  742. if errNumber > 20:
  743. self.log_message(f"模拟器 {index} 连续多次未检测到搜索页面,设置错误并退出...")
  744. manager.stop_emulator(index)
  745. return False
  746. self.log_message(f"模拟器 {index} 还在搜索结果页面,重新点击...")
  747. manager.tap(index, 355, 333, self.log_message)
  748. time.sleep(5)
  749. def run_task(self):
  750. """执行任务(支持并发,出错直接退出不重试)"""
  751. global threads
  752. try:
  753. # 获取最大线程数
  754. max_threads = int(self.max_threads_var.get()) if self.max_threads_var.get().isdigit() else 1
  755. self.thread_semaphore = threading.Semaphore(max_threads)
  756. self.active_threads = 0
  757. self.log_message(f"📌 最大并发数: {max_threads}")
  758. threads = []
  759. def process_emulator(emu):
  760. """处理单个模拟器的函数,出错直接退出"""
  761. # 获取信号量,控制并发数
  762. self.thread_semaphore.acquire()
  763. # 增加活跃线程计数
  764. with self.threads_lock:
  765. self.active_threads += 1
  766. current_active = self.active_threads
  767. self.log_message(f"📊 当前活跃线程数: {current_active}/{max_threads}")
  768. index = emu['index']
  769. try:
  770. if self.should_stop:
  771. return
  772. self.log_message(f"\n{'='*50}")
  773. self.log_message(f"开始处理模拟器 {index} ({emu['name']})")
  774. self.log_message(f"{'='*50}")
  775. # 更新状态为运行中
  776. self.root.after(0, lambda: self.update_emulator_status(index, "运行中"))
  777. manager = MuMuEmulatorManager(self.mumu_path_var.get())
  778. # 启动模拟器
  779. self.log_message(f"正在启动模拟器 {index}...")
  780. if not manager.start_emulator(index):
  781. self.log_message(f"❌ 模拟器 {index} 启动失败")
  782. self.results[index] = False
  783. self.root.after(0, lambda: self.update_emulator_status(index, "启动失败"))
  784. return
  785. # 等待就绪
  786. if not manager.wait_for_emulator_ready(index, timeout=180, log_callback=self.log_message):
  787. self.log_message(f"❌ 模拟器 {index} 启动超时")
  788. self.results[index] = False
  789. self.root.after(0, lambda: self.update_emulator_status(index, "启动超时"))
  790. return
  791. time.sleep(5)
  792. if not manager.check_resolution(index, 720, self.log_message):
  793. self.log_message(f"❌ 模拟器 {index} 分辨率不是720,停止任务并关闭模拟器")
  794. manager.stop_emulator(index)
  795. self.results[index] = False
  796. self.root.after(0, lambda: self.update_emulator_status(index, "分辨率错误"))
  797. return
  798. # 安装APK
  799. if not manager.install_apk(index, self.apk_path_var.get(), self.log_message):
  800. self.log_message(f"⚠️ APK安装失败,但继续执行...")
  801. # 打开应用
  802. manager.open_app(index, self.package_name_var.get(), self.log_message)
  803. # 判断是否需要同意
  804. time.sleep(20)
  805. button_color = manager.get_pixel_color(index, 394, 846, log_callback=self.log_message)
  806. # 是否是第一次进入
  807. if button_color and button_color.upper() == "#FC7838":
  808. manager.tap(index, 394, 846, self.log_message)
  809. time.sleep(120)
  810. manager.tap(index, 640, 260, self.log_message)
  811. time.sleep(20)
  812. manager.tap(index, 57, 193, self.log_message)
  813. time.sleep(5)
  814. else:
  815. time.sleep(10)
  816. # 等待进入主界面
  817. errNumber = 0
  818. while True:
  819. if self.should_stop or self.is_paused:
  820. # 处理暂停
  821. while self.is_paused and not self.should_stop:
  822. time.sleep(1)
  823. if self.should_stop:
  824. return
  825. color560130 = manager.get_pixel_color(index, 560, 130, log_callback=self.log_message)
  826. if color560130 in ["#EEF8EE", "#F7F7F7"]:
  827. self.log_message(f"模拟器 {index} 已经进入主界面,继续...")
  828. break
  829. elif color560130 in ["#5F635F"]:
  830. self.log_message(f"模拟器 {index} 需要关闭红包弹窗...")
  831. # manager.tap(index, 640, 260, self.log_message)
  832. manager.tap(index, 360, 1035, self.log_message)
  833. time.sleep(20)
  834. manager.tap(index, 57, 193, self.log_message)
  835. elif color560130 in ["#CED0D2"]:
  836. self.log_message(f"模拟器 {index} 需要关闭广告弹窗...")
  837. manager.tap(index, 200, 80, self.log_message)
  838. elif color560130 in ["#F3CEA9"]:
  839. self.log_message(f"模拟器 {index} 进入错误页面,返回...")
  840. manager.tap(index, 44, 92, self.log_message)
  841. time.sleep(20)
  842. manager.tap(index, 57, 193, self.log_message)
  843. else:
  844. errNumber = errNumber + 1
  845. if errNumber > 30:
  846. self.log_message(f"模拟器 {index} 连续多次未进入主界面,退出...")
  847. manager.stop_emulator(index)
  848. self.results[index] = False
  849. self.root.after(0, lambda: self.update_emulator_status(index, "进入主界面失败"))
  850. return
  851. self.log_message(f"模拟器 {index} 等待进入主界面中...")
  852. time.sleep(5)
  853. # 执行打开书籍
  854. if not self.openBook(manager, index):
  855. self.results[index] = False
  856. self.root.after(0, lambda: self.update_emulator_status(index, "打开书籍失败"))
  857. return
  858. time.sleep(8)
  859. # 翻页
  860. page_range = self.page_count_var.get()
  861. if '-' in page_range:
  862. parts = page_range.split('-')
  863. if len(parts) == 2 and parts[0].isdigit() and parts[1].isdigit():
  864. page_count = random.randint(int(parts[0]), int(parts[1]))
  865. else:
  866. page_count = 10
  867. else:
  868. page_count = int(page_range) if page_range.isdigit() else 10
  869. for page in range(page_count):
  870. if self.should_stop or self.is_paused:
  871. # 处理暂停
  872. while self.is_paused and not self.should_stop:
  873. time.sleep(1)
  874. if self.should_stop:
  875. return
  876. # 判断是否在看书目录界面
  877. errNumber = 0
  878. while True:
  879. color7001000 = manager.get_pixel_color(index, 700, 1000, log_callback=self.log_message)
  880. if color7001000 in ["#E8E3CE", "#E0DBC6", "#CCCBCB"]:
  881. self.log_message(f"模拟器 {index} 在看书目录界面,继续翻页...")
  882. break
  883. elif color7001000 in ["#F9F9FC"]:
  884. self.log_message(f"模拟器 {index} 遇到广告,点击跳过...")
  885. manager.tap(index, 700, 300, self.log_message)
  886. else:
  887. errNumber = errNumber + 1
  888. if errNumber > 5:
  889. self.log_message(f"尝试滑动是否能跳过!")
  890. manager.swipe(index, 700, 700, 200, 700, 500, self.log_message)
  891. if errNumber > 10:
  892. self.log_message(f"模拟器 {index} 连续多次未检测到目录界面,退出...")
  893. manager.stop_emulator(index)
  894. self.results[index] = False
  895. self.root.after(0, lambda: self.update_emulator_status(index, "翻页失败"))
  896. return
  897. # 判断是否有广告
  898. if manager.get_pixel_color(index, 700, 1200, log_callback=self.log_message) in ["#E8E3CE"]:
  899. self.log_message(f"模拟器 {index} 点击跳过广告!")
  900. manager.tap(index, 700, 1200, self.log_message)
  901. time.sleep(2)
  902. # 随机翻页间隔
  903. interval_str = self.page_interval_var.get()
  904. if '-' in interval_str:
  905. parts = interval_str.split('-')
  906. if len(parts) == 2 and parts[0].isdigit() and parts[1].isdigit():
  907. intervalNum = random.randint(int(parts[0]), int(parts[1]))
  908. else:
  909. intervalNum = 30
  910. else:
  911. intervalNum = int(interval_str) if interval_str.isdigit() else 30
  912. time.sleep(intervalNum)
  913. manager.swipe(index, 700, 700, 200, 700, 500, self.log_message)
  914. time.sleep(2)
  915. # 加入书签
  916. self.log_message(f"正在加入书架 {index}...")
  917. manager.tap(index, 360, 600, self.log_message)
  918. time.sleep(2)
  919. manager.tap(index, 255, 84, self.log_message)
  920. time.sleep(3)
  921. # 清除应用数据
  922. self.log_message(f"正在清除模拟器 {index} 应用数据...")
  923. manager.clear_app_data(index, manager, self.log_message)
  924. time.sleep(2)
  925. # 关闭模拟器
  926. self.log_message(f"正在关闭模拟器 {index}...")
  927. manager.stop_emulator(index)
  928. self.log_message(f"✅ 模拟器 {index} 任务完成")
  929. self.results[index] = True
  930. self.root.after(0, lambda: self.update_emulator_status(index, "任务完成"))
  931. except Exception as e:
  932. self.log_message(f"❌ 模拟器 {index} 执行出错: {e}")
  933. self.results[index] = False
  934. self.root.after(0, lambda: self.update_emulator_status(index, "执行出错"))
  935. finally:
  936. # 减少活跃线程计数
  937. with self.threads_lock:
  938. self.active_threads -= 1
  939. current_active = self.active_threads
  940. self.log_message(f"📊 当前活跃线程数: {current_active}/{max_threads}")
  941. # 释放信号量,让下一个等待的线程开始
  942. self.thread_semaphore.release()
  943. # 启动所有模拟器任务(信号量会自动控制并发数)
  944. for emu in self.selected_emulators:
  945. if self.should_stop:
  946. break
  947. thread = threading.Thread(target=process_emulator, args=(emu,))
  948. thread.start()
  949. threads.append(thread)
  950. # 稍微延迟一下,避免同时启动太多
  951. time.sleep(2)
  952. # 等待所有线程完成
  953. for thread in threads:
  954. thread.join()
  955. success_count = sum(1 for v in self.results.values() if v)
  956. self.log_message(f"\n🎉 任务执行完毕!成功: {success_count}/{len(self.selected_emulators)}")
  957. self.select_failed_emulators()
  958. self.start_task()
  959. except Exception as e:
  960. self.log_message(f"❌ 任务执行出错: {e}")
  961. finally:
  962. self.is_running = False
  963. self.is_paused = False
  964. self.pause_btn.config(text="暂停")
  965. # 恢复按钮状态
  966. self.root.after(0, self.reset_buttons)
  967. def run_task2(self):
  968. """执行评价任务(支持并发,出错直接退出不重试)"""
  969. try:
  970. # 获取最大线程数
  971. max_threads = int(self.max_threads_var.get()) if self.max_threads_var.get().isdigit() else 1
  972. self.thread_semaphore = threading.Semaphore(max_threads)
  973. self.active_threads = 0
  974. self.log_message(f"📌 最大并发数: {max_threads}")
  975. threads = []
  976. def process_emulator(emu):
  977. """处理单个模拟器的函数,出错直接退出"""
  978. # 获取信号量,控制并发数
  979. self.thread_semaphore.acquire()
  980. # 增加活跃线程计数
  981. with self.threads_lock:
  982. self.active_threads += 1
  983. current_active = self.active_threads
  984. self.log_message(f"📊 当前活跃线程数: {current_active}/{max_threads}")
  985. index = emu['index']
  986. try:
  987. if self.should_stop:
  988. return
  989. self.log_message(f"\n{'='*50}")
  990. self.log_message(f"开始处理模拟器 {index} ({emu['name']})")
  991. self.log_message(f"{'='*50}")
  992. # 更新状态为运行中
  993. self.root.after(0, lambda: self.update_emulator_status(index, "运行中"))
  994. manager = MuMuEmulatorManager(self.mumu_path_var.get())
  995. # 启动模拟器
  996. self.log_message(f"正在启动模拟器 {index}...")
  997. if not manager.start_emulator(index):
  998. self.log_message(f"❌ 模拟器 {index} 启动失败")
  999. self.results[index] = False
  1000. self.root.after(0, lambda: self.update_emulator_status(index, "启动失败"))
  1001. return
  1002. # 等待就绪
  1003. if not manager.wait_for_emulator_ready(index, timeout=180, log_callback=self.log_message):
  1004. self.log_message(f"❌ 模拟器 {index} 启动超时")
  1005. self.results[index] = False
  1006. self.root.after(0, lambda: self.update_emulator_status(index, "启动超时"))
  1007. return
  1008. time.sleep(5)
  1009. if not manager.check_resolution(index, 720, self.log_message):
  1010. self.log_message(f"❌ 模拟器 {index} 分辨率不是720,停止任务并关闭模拟器")
  1011. manager.stop_emulator(index)
  1012. self.results[index] = False
  1013. self.root.after(0, lambda: self.update_emulator_status(index, "分辨率错误"))
  1014. return
  1015. # 安装APK
  1016. if not manager.install_apk(index, self.apk_path_var.get(), self.log_message):
  1017. self.log_message(f"⚠️ APK安装失败,但继续执行...")
  1018. # 打开应用
  1019. manager.open_app(index, self.package_name_var.get(), self.log_message)
  1020. # 等待进入主界面
  1021. errNumber = 0
  1022. while True:
  1023. if self.should_stop or self.is_paused:
  1024. while self.is_paused and not self.should_stop:
  1025. time.sleep(1)
  1026. if self.should_stop:
  1027. return
  1028. color560130 = manager.get_pixel_color(index, 560, 130, log_callback=self.log_message)
  1029. if color560130 in ["#EEF8EE", "#F7F7F7"]:
  1030. self.log_message(f"模拟器 {index} 已经进入主界面,继续...")
  1031. break
  1032. elif color560130 in ["#5F635F"]:
  1033. self.log_message(f"模拟器 {index} 需要关闭红包弹窗...")
  1034. # manager.tap(index, 640, 260, self.log_message)
  1035. manager.tap(index, 360, 1035, self.log_message)
  1036. time.sleep(20)
  1037. manager.tap(index, 57, 193, self.log_message)
  1038. elif color560130 in ["#CED0D2"]:
  1039. self.log_message(f"模拟器 {index} 需要关闭广告弹窗...")
  1040. manager.tap(index, 200, 80, self.log_message)
  1041. elif color560130 in ["#F3CEA9"]:
  1042. self.log_message(f"模拟器 {index} 进入错误页面,返回...")
  1043. manager.tap(index, 44, 92, self.log_message)
  1044. time.sleep(20)
  1045. manager.tap(index, 57, 193, self.log_message)
  1046. else:
  1047. errNumber = errNumber + 1
  1048. if errNumber > 30:
  1049. self.log_message(f"模拟器 {index} 连续多次未进入主界面,退出...")
  1050. manager.stop_emulator(index)
  1051. self.results[index] = False
  1052. self.root.after(0, lambda: self.update_emulator_status(index, "进入主界面失败"))
  1053. return
  1054. self.log_message(f"模拟器 {index} 等待进入主界面中...")
  1055. time.sleep(5)
  1056. # 执行打开书籍
  1057. if not self.openBook(manager, index):
  1058. self.results[index] = False
  1059. self.root.after(0, lambda: self.update_emulator_status(index, "打开书籍失败"))
  1060. return
  1061. # 评价
  1062. time.sleep(8)
  1063. manager.tap(index, 680, 95, self.log_message)
  1064. time.sleep(2)
  1065. manager.tap(index, 633, 930, self.log_message)
  1066. # 发表
  1067. time.sleep(5)
  1068. manager.tap(index, 640, 95, self.log_message)
  1069. time.sleep(4)
  1070. # 清除应用数据
  1071. self.log_message(f"正在清除模拟器 {index} 应用数据...")
  1072. manager.clear_app_data(index, manager, self.log_message)
  1073. time.sleep(2)
  1074. # 关闭模拟器
  1075. self.log_message(f"正在关闭模拟器 {index}...")
  1076. manager.stop_emulator(index)
  1077. self.log_message(f"✅ 模拟器 {index} 任务完成")
  1078. self.results[index] = True
  1079. self.root.after(0, lambda: self.update_emulator_status(index, "任务完成"))
  1080. except Exception as e:
  1081. self.log_message(f"❌ 模拟器 {index} 执行出错: {e}")
  1082. self.results[index] = False
  1083. self.root.after(0, lambda: self.update_emulator_status(index, "执行出错"))
  1084. finally:
  1085. # 减少活跃线程计数
  1086. with self.threads_lock:
  1087. self.active_threads -= 1
  1088. current_active = self.active_threads
  1089. self.log_message(f"📊 当前活跃线程数: {current_active}/{max_threads}")
  1090. # 释放信号量
  1091. self.thread_semaphore.release()
  1092. # 启动所有模拟器任务
  1093. for emu in self.selected_emulators:
  1094. if self.should_stop:
  1095. break
  1096. thread = threading.Thread(target=process_emulator, args=(emu,))
  1097. thread.start()
  1098. threads.append(thread)
  1099. time.sleep(2)
  1100. # 等待所有线程完成
  1101. for thread in threads:
  1102. thread.join()
  1103. success_count = sum(1 for v in self.results.values() if v)
  1104. self.log_message(f"\n🎉 任务执行完毕!成功: {success_count}/{len(self.selected_emulators)}")
  1105. except Exception as e:
  1106. self.log_message(f"❌ 任务执行出错: {e}")
  1107. finally:
  1108. self.is_running = False
  1109. self.is_paused = False
  1110. self.pause_btn.config(text="暂停")
  1111. # 恢复按钮状态
  1112. self.root.after(0, self.reset_buttons)
  1113. def reset_buttons(self):
  1114. """重置按钮状态"""
  1115. self.load_btn.config(state='normal')
  1116. self.start_read_btn.config(state='normal')
  1117. self.start_comment_btn.config(state='normal')
  1118. self.pause_btn.config(state='disabled', text="暂停")
  1119. self.stop_btn.config(state='disabled')
  1120. def clear_log(self):
  1121. """清空日志"""
  1122. self.log_text.delete(1.0, tk.END)
  1123. def run(self):
  1124. """运行GUI"""
  1125. self.root.mainloop()
  1126. if __name__ == "__main__":
  1127. app = MuMuAutoGUI()
  1128. app.run()