-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathgui.py
More file actions
5734 lines (4696 loc) · 281 KB
/
gui.py
File metadata and controls
5734 lines (4696 loc) · 281 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import subprocess
import time
import re
from typing import Optional, Tuple
def _adb_shell(cmd: str, adb: str = "adb", timeout: int = 5) -> str:
try:
creationflags = subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
p = subprocess.run([adb, 'shell', cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=timeout, creationflags=creationflags)
return p.stdout.decode(errors='ignore')
except Exception:
return ""
def is_screen_on(adb: str = "adb") -> bool:
"""检查设备屏幕是否点亮。返回 True 表示亮屏。
使用 `dumpsys power` 的输出进行多种模式解析,提高兼容性。
"""
out = _adb_shell('dumpsys power', adb)
if not out:
return False
m = re.search(r'mWakefulness=(\w+)', out)
if m:
return m.group(1).lower() == 'awake'
m = re.search(r'mScreenOn=(true|false)', out, re.I)
if m:
return m.group(1).lower() == 'true'
m = re.search(r'Display Power: state=(\w+)', out, re.I)
if m:
return m.group(1).lower() != 'off'
# 兜底:如果包含 Awake 关键字则认为是亮屏
if 'awake' in out.lower():
return True
return False
def wake_and_unlock(adb: str = "adb", max_attempts: int = 3, swipe: Optional[Tuple[int, int, int, int]] = None, password: Optional[str] = None) -> bool:
"""唤醒并尝试解锁屏幕。
顺序:发送 WAKEUP -> 发送 MENU (或解锁键) -> 可选滑动解锁。
返回 True 表示检测到屏幕已点亮。
"""
creationflags = subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
for _ in range(max_attempts):
subprocess.run([adb, 'shell', 'input', 'keyevent', '224'], creationflags=creationflags) # KEYCODE_WAKEUP
time.sleep(0.4)
subprocess.run([adb, 'shell', 'input', 'keyevent', '82'], creationflags=creationflags) # KEYCODE_MENU (通常可解锁)
time.sleep(0.4)
if swipe:
x1, y1, x2, y2 = swipe
subprocess.run([adb, 'shell', 'input', 'swipe', str(x1), str(y1), str(x2), str(y2)], creationflags=creationflags)
time.sleep(0.5)
# 如果提供了密码,尝试通过输入密码解锁(在滑动或按键后)
if password:
try:
# input text 对空格的处理需要替换为 %s
esc = str(password).replace(' ', '%s')
subprocess.run([adb, 'shell', 'input', 'text', esc], creationflags=creationflags)
time.sleep(0.3)
# 按回车或确认键
subprocess.run([adb, 'shell', 'input', 'keyevent', '66'], creationflags=creationflags)
time.sleep(0.6)
except Exception:
pass
if is_screen_on(adb):
return True
# 备用:短按电源键(某些机型需要)
subprocess.run([adb, 'shell', 'input', 'keyevent', '26'], creationflags=creationflags)
time.sleep(0.6)
return is_screen_on(adb)
def ensure_awake_and_unlocked(adb: str = "adb", swipe: Optional[Tuple[int, int, int, int]] = None, password: Optional[str] = None) -> bool:
"""在继续执行前确保屏幕已唤醒并尽量解锁。
返回 True 表示屏幕已唤醒(或已成功解锁)。
"""
try:
if is_screen_on(adb):
return True
return wake_and_unlock(adb, swipe=swipe, password=password)
except Exception:
return False
#!/usr/bin/env python3
"""
GUI for Phone Agent - AI-powered phone automation.
"""
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox, filedialog
import subprocess
import threading
import os
import sys
# 隐藏控制台窗口(仅在Windows上有效)
if sys.platform == 'win32' and 'python.exe' in sys.executable:
import ctypes
try:
# 尝试隐藏控制台窗口
ctypes.windll.user32.ShowWindow(ctypes.windll.kernel32.GetConsoleWindow(), 0)
except:
pass
import sys
import json
from datetime import datetime
import re
# 导入任务精简器
from task_simplifier import TaskSimplifierManager
class PhoneAgentGUI:
def __init__(self, root):
self.root = root
self.root.title("鸡哥手机助手 v1.8 - 更多好玩的工具请关注微信公众号:菜芽创作小助手")
self.root.geometry("1200x750")
self.root.minsize(1100, 650)
# 显示快速启动提示
self.show_startup_message()
# 设置样式
self.setup_styles()
# 变量存储
self.base_url = tk.StringVar(value="https://open.bigmodel.cn/api/paas/v4")
self.model = tk.StringVar(value="autoglm-phone")
self.apikey = tk.StringVar(value="your-bigmodel-api-key")
self.task = tk.StringVar(value="输入你想要执行的任务,例如:打开美团搜索附近的火锅店")
self.max_steps = tk.StringVar(value="200")
self.temperature = tk.StringVar(value="0.0") # 新增temperature参数
self.device_type = tk.StringVar(value="安卓") # 默认为安卓
self.process = None
self.running = False
self.config_file = "gui_config.json"
# 设备相关变量
self.connected_devices = []
self.selected_device_id = tk.StringVar(value="")
# 支持环境变量 PHONE_AGENT_DEVICE_ID
self.env_device_id = os.getenv("PHONE_AGENT_DEVICE_ID", "")
# iOS设备IP地址
self.ios_device_ip = tk.StringVar(value="localhost")
# 窗口控制变量
self.qrcode_window = None
self.adb_connection_window = None
self.device_details_window = None
self.remote_desktop_window = None
# 设备类型防重复变量
self._last_device_type = None
# iOS IP对话框状态标志
self._ios_ip_dialog_open = False
# 初始化任务精简器
self.task_simplifier = TaskSimplifierManager()
# 任务历史记录
self.task_history_file = "task_history.json"
self.task_history = []
self.load_task_history()
# 快速创建基础界面
self.create_basic_widgets()
# 更新界面显示完成
self.root.update_idletasks()
# 异步加载剩余组件和配置
threading.Thread(target=self.async_initialization, daemon=True).start()
# 设置程序关闭时的自动保存
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
def show_startup_message(self):
"""显示启动提示"""
startup_label = tk.Label(self.root, text="🚀 正在启动...",
font=('Microsoft YaHei', 12),
fg='#2E86AB', bg='white')
startup_label.place(relx=0.5, rely=0.5, anchor='center')
self.startup_label = startup_label
self.root.update_idletasks()
def async_initialization(self):
"""异步初始化剩余组件"""
try:
# 延迟创建完整界面
self.root.after(50, self.create_full_widgets)
# 延迟加载配置
self.root.after(150, self.load_config_async)
except Exception as e:
print(f"异步初始化错误: {e}")
def _prepare_device_on_startup(self, adb: str = 'adb', swipe: Optional[Tuple[int, int, int, int]] = (300, 1000, 300, 300)):
"""在后台检查设备屏幕并尝试唤醒/解锁,避免阻塞 GUI 启动。
使用已有的 `ensure_awake_and_unlocked` 函数。
"""
try:
try:
self.root.after(0, lambda: self.startup_label.config(text='🔌 检查并唤醒设备...'))
except Exception:
pass
try:
import os
pwd = os.getenv('PHONE_AGENT_LOCK_PASSWORD', '')
except Exception:
pwd = ''
ok = ensure_awake_and_unlocked(adb=adb, swipe=swipe, password=pwd if pwd else None)
if ok:
msg = '✅ 设备已唤醒并尽量解锁'
else:
msg = '⚠️ 无法唤醒设备,请手动检查'
try:
# 如果 status_var 可用则更新,否则更新 startup_label
if hasattr(self, 'status_var'):
self.root.after(0, lambda: self.status_var.set(msg))
else:
self.root.after(0, lambda: self.startup_label.config(text=msg))
except Exception:
pass
except Exception as e:
print(f"设备准备失败: {e}")
def load_config_async(self):
"""异步加载配置,避免阻塞启动"""
threading.Thread(target=self._background_load_config, daemon=True).start()
def _background_load_config(self):
"""后台线程中加载配置"""
try:
config_data = None
config_file_path = self.config_file
# 检查配置文件是否存在
if os.path.exists(config_file_path):
with open(config_file_path, 'r', encoding='utf-8') as f:
config_data = json.load(f)
# 在主线程中应用配置
if config_data:
self.root.after(0, lambda: self._apply_config(config_data))
else:
self.root.after(0, self._create_default_config)
except Exception as e:
print(f"后台加载配置失败: {str(e)}")
if hasattr(self, 'status_var'):
self.root.after(0, lambda: self.status_var.set("⚠️ 配置加载失败"))
def _apply_config(self, config):
"""在主线程中应用配置"""
try:
self.base_url.set(config.get('base_url', 'https://open.bigmodel.cn/api/paas/v4'))
self.model.set(config.get('model', 'autoglm-phone'))
self.apikey.set(config.get('apikey', 'your-bigmodel-api-key'))
task_text = config.get('task', '输入你想要执行的任务,例如:打开美团搜索附近的火锅店')
self.task.set(task_text)
self.max_steps.set(str(config.get('max_steps', '200')))
self.temperature.set(str(config.get('temperature', '0.0'))) # 添加temperature加载
device_type_value = config.get('device_type', 'adb')
# 将保存的英文值转换为中文显示
if device_type_value == 'adb':
self.device_type.set('安卓')
elif device_type_value == 'ios':
self.device_type.set('iOS')
else:
self.device_type.set('鸿蒙')
# 加载iOS设备IP配置
ios_ip = config.get('ios_device_ip', 'localhost')
if hasattr(self, 'ios_device_ip'):
self.ios_device_ip.set(ios_ip)
# 如果界面已创建,更新任务文本框
if hasattr(self, 'task_text'):
self.task_text.delete("1.0", tk.END)
self.task_text.insert("1.0", task_text)
# 恢复选中的设备,优先使用环境变量
selected_device = self.env_device_id or config.get('selected_device', '')
if selected_device and hasattr(self, 'selected_device_id'):
self.selected_device_id.set(selected_device)
print(f"🔍 配置加载: 设置selected_device_id为 '{selected_device}'")
# 如果界面已创建,只更新界面显示,不自动扫描设备
if hasattr(self, 'adb_frame'):
current_device_type = self.device_type.get()
self._last_device_type = current_device_type # 更新防重复标志
# 只更新界面显示,不执行设备扫描
if hasattr(self, 'adb_control_frame'):
# 将中文选项转换为英文值用于内部处理
if current_device_type == "安卓":
device_type_en = "adb"
elif current_device_type == "鸿蒙":
device_type_en = "hdc"
elif current_device_type == "iOS":
device_type_en = "ios"
else:
device_type_en = "adb" # 默认
# 更新标题和按钮文本,并更新按钮可见性
if device_type_en == "hdc":
self.adb_frame.config(text="📱 HDC设备管理")
elif device_type_en == "ios":
self.adb_frame.config(text="🍎 iOS设备管理")
if hasattr(self, 'device_status_label'):
current_ip = self.ios_device_ip.get()
if current_ip and current_ip != "localhost":
self.device_status_label.config(text=f"iOS设备IP: {current_ip}")
else:
self.device_status_label.config(text="iOS设备未配置IP")
else:
self.adb_frame.config(text="📱 ADB设备管理")
if hasattr(self, 'device_status_label'):
if selected_device:
self.device_status_label.config(text=f"已连接: {selected_device}")
else:
self.device_status_label.config(text=f"未连接ADB设备")
# 重要:更新按钮的可见性和连接按钮文本
self.update_device_buttons_visibility()
# 加载远程连接配置
self.last_remote_connection = config.get('remote_connection', {
'ip': '192.168.1.100',
'port': '5555'
})
# 加载无线调试配对配置
self.last_wireless_pair = config.get('wireless_pair', {
'pair_address': '10.10.10.100:41717',
'connect_address': '10.10.10.100:5555'
})
# 加载Android 10及以下无线调试配置
self.last_legacy_wireless = config.get('legacy_wireless', {
'ip': '192.168.1.100',
'port': '5555'
})
# 加载锁屏密码配置
lock_password = config.get('lock_password', '')
if lock_password:
import os
os.environ['PHONE_AGENT_LOCK_PASSWORD'] = lock_password
if hasattr(self, 'status_var'):
self.status_var.set("✅ 配置已加载")
except Exception as e:
print(f"应用配置失败: {str(e)}")
if hasattr(self, 'status_var'):
self.status_var.set("⚠️ 配置应用失败")
def _calculate_center_position(self, child_width, child_height):
"""计算相对于主窗口的居中位置"""
# 确保主窗口完全更新
self.root.update_idletasks()
# 获取主窗口的位置和大小
main_x = self.root.winfo_x()
main_y = self.root.winfo_y()
main_width = self.root.winfo_width()
main_height = self.root.winfo_height()
# 计算居中位置
center_x = main_x + (main_width // 2) - (child_width // 2)
center_y = main_y + (main_height // 2) - (child_height // 2)
# 确保窗口不会超出屏幕边界
import tkinter as tk
screen_width = self.root.winfo_screenwidth()
screen_height = self.root.winfo_screenheight()
if center_x < 0:
center_x = 0
if center_y < 0:
center_y = 0
if center_x + child_width > screen_width:
center_x = screen_width - child_width
if center_y + child_height > screen_height:
center_y = screen_height - child_height
return center_x, center_y
def center_window(self, window, width=None, height=None):
"""将窗口居中显示在主窗口中间,避免闪现"""
try:
# 先隐藏窗口,避免闪现
window.withdraw()
window.update_idletasks()
# 使用计算方法获取位置
if width and height:
center_x, center_y = self._calculate_center_position(width, height)
window.geometry(f"{width}x{height}+{center_x}+{center_y}")
else:
child_width = window.winfo_width()
child_height = window.winfo_height()
# 如果窗口还没有实际大小,使用默认值
if child_width <= 1:
child_width = 500
if child_height <= 1:
child_height = 400
center_x, center_y = self._calculate_center_position(child_width, child_height)
window.geometry(f"+{center_x}+{center_y}")
# 最后显示窗口
window.deiconify()
window.update_idletasks()
except Exception as e:
print(f"居中窗口失败: {e}")
# 如果失败,确保窗口可见
try:
window.deiconify()
except:
pass
def create_centered_toplevel(self, parent, title, width, height, resizable=True):
"""创建居中显示的Toplevel窗口,避免闪现
Args:
parent: 父窗口
title: 窗口标题
width: 窗口宽度
height: 窗口高度
resizable: 是否可调整大小
Returns:
创建的Toplevel窗口
"""
try:
# 先计算居中位置
center_x, center_y = self._calculate_center_position(width, height)
# 创建窗口时直接设置位置
window = tk.Toplevel(parent)
window.title(title)
window.geometry(f"{width}x{height}+{center_x}+{center_y}")
# 设置是否可调整大小
if resizable:
window.resizable(True, True)
else:
window.resizable(False, False)
# 确保窗口正确显示
window.update_idletasks()
return window
except Exception as e:
print(f"创建居中窗口失败: {e}")
# 降级方案:使用普通的Toplevel
window = tk.Toplevel(parent)
window.title(title)
window.geometry(f"{width}x{height}")
if resizable:
window.resizable(True, True)
else:
window.resizable(False, False)
self.center_window(window, width, height)
return window
def _create_default_config(self):
"""创建默认配置"""
if hasattr(self, 'status_var'):
self.status_var.set("📝 使用默认配置")
# 首次启动时不自动扫描设备,避免弹出CMD窗口
# 用户手动操作时会自动触发设备扫描
pass
def setup_styles(self):
"""设置界面样式"""
style = ttk.Style()
# 设置主题
style.theme_use('clam')
# 配置颜色
style.configure('Title.TLabel', font=('Microsoft YaHei', 18, 'bold'), foreground='#2E86AB')
style.configure('Header.TLabel', font=('Microsoft YaHei', 12, 'bold'), foreground='#333333')
style.configure('Success.TButton', font=('Microsoft YaHei', 10, 'bold'),
foreground='white', background='#28a745')
style.map('Success.TButton',
background=[('active', '#218838'), ('pressed', '#1e7e34')])
style.configure('Danger.TButton', font=('Microsoft YaHei', 10, 'bold'),
foreground='white', background='#dc3545')
style.map('Danger.TButton',
background=[('active', '#c82333'), ('pressed', '#bd2130')])
style.configure('Secondary.TButton', font=('Microsoft YaHei', 10, 'bold'),
foreground='#333333', background='#6c757d')
style.map('Secondary.TButton',
background=[('active', '#5a6268'), ('pressed', '#545b62')])
# 配置框架
style.configure('Card.TFrame', relief='raised', borderwidth=1)
style.configure('Output.TFrame', relief='sunken', borderwidth=2)
def create_basic_widgets(self):
"""创建基础界面组件(快速显示)"""
# 主框架
self.main_frame = ttk.Frame(self.root, padding="15")
self.main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# 配置权重
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
self.main_frame.columnconfigure(1, weight=1)
def create_full_widgets(self):
"""创建完整界面组件(异步加载)"""
try:
# 快速移除启动提示,避免界面闪烁
if hasattr(self, 'startup_label'):
self.startup_label.destroy()
# 标题区域
title_frame = ttk.Frame(self.main_frame)
title_frame.grid(row=0, column=0, columnspan=3, pady=(0, 25))
title_label = ttk.Label(title_frame, text="🤖 鸡哥手机助手", style='Title.TLabel')
title_label.pack()
subtitle_label = ttk.Label(title_frame, text="AI驱动的手机自动化工具", font=('Microsoft YaHei', 10))
subtitle_label.pack()
# 配置区域
config_frame = ttk.LabelFrame(self.main_frame, text="⚙️ 配置参数", style='Card.TFrame', padding="8")
config_frame.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 8))
config_frame.columnconfigure(1, weight=1)
# Base URL
ttk.Label(config_frame, text="🌐 Base URL:", font=('Microsoft YaHei', 9, 'bold')).grid(row=0, column=0, sticky=tk.W, pady=3)
url_entry = ttk.Entry(config_frame, textvariable=self.base_url, width=50, font=('Microsoft YaHei', 9))
url_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(10, 0), pady=3)
# Model
ttk.Label(config_frame, text="🧠 Model:", font=('Microsoft YaHei', 9, 'bold')).grid(row=1, column=0, sticky=tk.W, pady=3)
model_entry = ttk.Entry(config_frame, textvariable=self.model, width=50, font=('Microsoft YaHei', 9))
model_entry.grid(row=1, column=1, sticky=(tk.W, tk.E), padx=(10, 0), pady=3)
# API Key
ttk.Label(config_frame, text="🔑 API Key:", font=('Microsoft YaHei', 9, 'bold')).grid(row=2, column=0, sticky=tk.W, pady=3)
apikey_frame = ttk.Frame(config_frame)
apikey_frame.grid(row=2, column=1, sticky=(tk.W, tk.E), padx=(10, 0), pady=3)
apikey_frame.columnconfigure(0, weight=1)
self.apikey_entry = ttk.Entry(apikey_frame, textvariable=self.apikey, width=40, show="*", font=('Microsoft YaHei', 9))
self.apikey_entry.grid(row=0, column=0, sticky=(tk.W, tk.E))
self.show_apikey_btn = ttk.Button(apikey_frame, text="👁️", width=2, command=self.toggle_apikey_visibility)
self.show_apikey_btn.grid(row=0, column=1, padx=(3, 0))
# Task
ttk.Label(config_frame, text="📝 Task:", font=('Microsoft YaHei', 9, 'bold')).grid(row=3, column=0, sticky=(tk.NW, tk.W), pady=3)
# 任务输入框和按钮的组合框架
task_frame = ttk.Frame(config_frame)
task_frame.grid(row=3, column=1, sticky=(tk.W, tk.E), padx=(10, 0), pady=3)
task_frame.columnconfigure(0, weight=1)
self.task_text = tk.Text(task_frame, width=50, height=2, font=('Microsoft YaHei', 9), wrap=tk.WORD)
self.task_text.grid(row=0, column=0, sticky=(tk.W, tk.E))
# 任务操作按钮框架
task_buttons_frame = ttk.Frame(task_frame)
task_buttons_frame.grid(row=0, column=1, padx=(5, 0))
# 任务精简按钮
self.simplify_task_button = ttk.Button(task_buttons_frame, text="🤖 AI润色",
command=self.show_task_simplifier,
style='Success.TButton')
self.simplify_task_button.grid(row=0, column=1, padx=(5, 0))
# 任务历史按钮(放在AI润色按钮左边)
self.task_history_button = ttk.Button(task_buttons_frame, text="📚",
command=self.show_task_history,
width=2)
self.task_history_button.grid(row=0, column=0)
# 设置初始任务文本
self.task_text.insert("1.0", self.task.get())
self.task_text.bind("<KeyRelease>", lambda e: self.on_task_change())
# Max Steps 和 Device Type 在同一排
settings_row_frame = ttk.Frame(config_frame)
settings_row_frame.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=3)
settings_row_frame.columnconfigure(1, weight=1)
settings_row_frame.columnconfigure(3, weight=1)
settings_row_frame.columnconfigure(5, weight=1)
# Device Type (左半部分) - 精确对齐Task输入框
device_type_frame = ttk.Frame(settings_row_frame)
device_type_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), padx=(0, 10))
device_type_frame.columnconfigure(0, weight=0) # 标签列固定宽度
device_type_frame.columnconfigure(1, weight=1) # 输入框列拉伸
# 标签,与配置区域的标签对齐
ttk.Label(device_type_frame, text="🔗设备类型:", font=('Microsoft YaHei', 9, 'bold')).grid(row=0, column=0, sticky=tk.W, padx=(0, 10))
# 输入框和说明文字的组合 - 使用10px的padding与Task输入框对齐
device_type_combo_frame = ttk.Frame(device_type_frame)
device_type_combo_frame.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(10, 0))
device_type_combo_frame.columnconfigure(0, weight=0)
self.device_type_combo = ttk.Combobox(device_type_combo_frame, textvariable=self.device_type, width=15, font=('Microsoft YaHei', 9), state="readonly")
self.device_type_combo['values'] = ('安卓', '鸿蒙', 'iOS')
self.device_type_combo.grid(row=0, column=0, sticky=tk.W)
self.device_type_combo.bind('<<ComboboxSelected>>', lambda e: self.on_device_type_change())
ttk.Label(device_type_combo_frame, text="(选择设备系统类型)", font=('Microsoft YaHei', 8), foreground='gray').grid(row=0, column=1, padx=(3, 0))
# Temperature (右半部分) - 在最大步数右边
temperature_frame = ttk.Frame(settings_row_frame)
temperature_frame.grid(row=0, column=5, columnspan=1, sticky=(tk.W, tk.E), padx=(10, 0))
temperature_frame.columnconfigure(0, weight=0)
temperature_frame.columnconfigure(1, weight=1)
# 标签,与设备类型和最大步数保持完全一致的间距
ttk.Label(temperature_frame, text="🌡️温度值:", font=('Microsoft YaHei', 9, 'bold')).grid(row=0, column=0, sticky=tk.W, padx=(0, 10))
# 输入框和说明文字的组合
temperature_entry_frame = ttk.Frame(temperature_frame)
temperature_entry_frame.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(10, 0))
temperature_entry_frame.columnconfigure(0, weight=0)
self.temperature_entry = ttk.Entry(temperature_entry_frame, textvariable=self.temperature, width=8, font=('Microsoft YaHei', 9))
self.temperature_entry.grid(row=0, column=0, sticky=tk.W)
self.temperature_entry.bind("<FocusOut>", lambda e: self.validate_temperature())
ttk.Label(temperature_entry_frame, text="(控制随机性,0.0-1.0)", font=('Microsoft YaHei', 8), foreground='gray').grid(row=0, column=1, padx=(3, 0))
# Max Steps (右半部分)
max_steps_frame = ttk.Frame(settings_row_frame)
max_steps_frame.grid(row=0, column=3, columnspan=2, sticky=(tk.W, tk.E), padx=(10, 0))
max_steps_frame.columnconfigure(0, weight=0)
max_steps_frame.columnconfigure(1, weight=1)
# 标签,与配置区域的标签对齐
ttk.Label(max_steps_frame, text="🔢最大步数:", font=('Microsoft YaHei', 9, 'bold')).grid(row=0, column=0, sticky=tk.W, padx=(0, 10))
# 输入框和说明文字的组合 - 使用10px的padding与Task输入框对齐
max_steps_entry_frame = ttk.Frame(max_steps_frame)
max_steps_entry_frame.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(10, 0))
max_steps_entry_frame.columnconfigure(0, weight=0)
self.max_steps_entry = ttk.Entry(max_steps_entry_frame, textvariable=self.max_steps, width=10, font=('Microsoft YaHei', 9))
self.max_steps_entry.grid(row=0, column=0, sticky=tk.W)
ttk.Label(max_steps_entry_frame, text="(每个任务最大执行步数)", font=('Microsoft YaHei', 8), foreground='gray').grid(row=0, column=1, padx=(3, 0))
# Base URL变化时自动保存
url_entry.bind("<KeyRelease>", lambda e: self.on_config_change())
# Model变化时自动保存
model_entry.bind("<KeyRelease>", lambda e: self.on_config_change())
# API Key变化时自动保存
self.apikey_entry.bind("<KeyRelease>", lambda e: self.on_config_change())
# Max Steps变化时自动保存
self.max_steps_entry.bind("<KeyRelease>", lambda e: self.on_config_change())
# Temperature变化时自动保存
self.temperature_entry.bind("<KeyRelease>", lambda e: self.on_config_change())
# ADB设备区域
self.adb_frame = ttk.LabelFrame(self.main_frame, text="📱 ADB设备管理", style='Card.TFrame', padding="8")
self.adb_frame.grid(row=2, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(8, 8))
self.adb_frame.columnconfigure(1, weight=1)
# ADB控制按钮
self.adb_control_frame = ttk.Frame(self.adb_frame)
self.adb_control_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
# 通用按钮(所有设备类型都显示)
ttk.Button(self.adb_control_frame, text="🔄 刷新设备", command=self.refresh_devices).pack(side=tk.LEFT, padx=(0, 8))
self.connect_button = ttk.Button(self.adb_control_frame, text="🔗 连接设备", command=self.connect_adb_device)
self.connect_button.pack(side=tk.LEFT, padx=(0, 8))
ttk.Button(self.adb_control_frame, text="📋 设备详情", command=self.show_device_details).pack(side=tk.LEFT, padx=(0, 8))
# 仅安卓设备的按钮
self.remote_desktop_button = ttk.Button(self.adb_control_frame, text="🖥️远程桌面", command=self.open_remote_desktop)
self.adb_keyboard_button = ttk.Button(self.adb_control_frame, text="📲 安装ADB键盘", command=self.install_adb_keyboard)
# 通用按钮 - 关注公众号按钮始终在最右边
ttk.Button(self.adb_control_frame, text="📱 关注公众号", command=self.open_wechat_qrcode).pack(side=tk.LEFT, padx=(0, 8))
# 初始设置按钮显示状态
self.update_device_buttons_visibility()
# 设备选择
ttk.Label(self.adb_frame, text="📱 选择设备:", font=('Microsoft YaHei', 9, 'bold')).grid(row=1, column=0, sticky=tk.W, pady=5)
device_select_frame = ttk.Frame(self.adb_frame)
device_select_frame.grid(row=1, column=1, sticky=(tk.W, tk.E), padx=(15, 0))
device_select_frame.columnconfigure(0, weight=1)
self.device_combo = ttk.Combobox(device_select_frame, textvariable=self.selected_device_id,
state="readonly", font=('Microsoft YaHei', 9))
self.device_combo.grid(row=0, column=0, sticky=(tk.W, tk.E))
# 设备选择变化时自动保存配置
self.device_combo.bind("<<ComboboxSelected>>", lambda e: self.on_device_change())
self.device_status_label = ttk.Label(device_select_frame, text="未检测到设备",
font=('Microsoft YaHei', 9), foreground='red')
self.device_status_label.grid(row=0, column=1, padx=(10, 0))
# 初始化设备类型但不自动扫描设备(避免启动时弹出CMD窗口)
# 用户手动操作时会自动触发设备扫描
current_device_type = self.device_type.get()
self._last_device_type = current_device_type # 设置初始值防止重复扫描
# 只更新界面显示,不扫描设备
if hasattr(self, 'adb_frame'):
if hasattr(self, 'adb_control_frame'):
# 将中文选项转换为英文值用于内部处理
if current_device_type == "安卓":
device_type_en = "adb"
elif current_device_type == "鸿蒙":
device_type_en = "hdc"
elif current_device_type == "iOS":
device_type_en = "ios"
else:
device_type_en = "adb" # 默认
# 只更新标题和按钮文本,不执行设备扫描
if device_type_en == "hdc":
self.adb_frame.config(text="📱 HDC设备管理")
elif device_type_en == "ios":
self.adb_frame.config(text="🍎 iOS设备管理")
if hasattr(self, 'device_status_label'):
current_ip = self.ios_device_ip.get()
if current_ip and current_ip != "localhost":
self.device_status_label.config(text=f"iOS设备IP: {current_ip}")
else:
self.device_status_label.config(text="iOS设备未配置IP")
else:
self.adb_frame.config(text="📱 ADB设备管理")
if hasattr(self, 'device_status_label'):
self.device_status_label.config(text=f"未连接ADB设备")
# 按钮区域
button_frame = ttk.Frame(self.main_frame)
button_frame.grid(row=3, column=0, columnspan=3, pady=5)
# 主要操作按钮
main_buttons = ttk.Frame(button_frame)
main_buttons.pack(side=tk.LEFT, padx=(0, 20))
self.run_button = ttk.Button(main_buttons, text="🚀 运行", command=self.run_agent, style='Success.TButton')
self.run_button.grid(row=0, column=0, padx=5)
self.stop_button = ttk.Button(main_buttons, text="⏹️ 停止", command=self.stop_agent, state=tk.DISABLED, style='Danger.TButton')
self.stop_button.grid(row=0, column=1, padx=5)
# 锁屏密码设置按钮(用于手动设置测试密码)
self.pwd_button = ttk.Button(main_buttons, text="🔒 自动唤醒/解锁", command=self.open_lock_password_dialog)
self.pwd_button.grid(row=0, column=2, padx=5)
# 辅助功能按钮
aux_buttons = ttk.Frame(button_frame)
aux_buttons.pack(side=tk.LEFT)
ttk.Button(aux_buttons, text="🗑️ 清空", command=self.clear_output).grid(row=0, column=0, padx=5)
ttk.Button(aux_buttons, text="💾 保存配置", command=self.save_config).grid(row=0, column=1, padx=5)
ttk.Button(aux_buttons, text="📁 加载配置", command=self.load_config_dialog).grid(row=0, column=2, padx=5)
# 输出区域
output_frame = ttk.LabelFrame(self.main_frame, text="📋 输出控制台", style='Output.TFrame', padding="5")
output_frame.grid(row=4, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(5, 0))
output_frame.columnconfigure(0, weight=1)
output_frame.rowconfigure(0, weight=1)
self.main_frame.rowconfigure(4, weight=1)
# 主输出文本框(移除行号)
self.output_text = scrolledtext.ScrolledText(output_frame, wrap=tk.WORD, width=80, height=20,
font=('Microsoft YaHei', 9), bg='#1e1e1e', fg='#ffffff',
insertbackground='#ffffff')
self.output_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# 状态栏
status_frame = ttk.Frame(self.main_frame)
status_frame.grid(row=5, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(5, 0))
status_frame.columnconfigure(1, weight=1)
self.status_var = tk.StringVar(value="✅ 就绪")
status_label = ttk.Label(status_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
status_label.grid(row=0, column=0, sticky=(tk.W, tk.E))
# 微信公众号推广文字
wechat_label = ttk.Label(status_frame, text="更多好玩的工具请关注微信公众号:菜芽创作小助手",
font=('Microsoft YaHei', 8), foreground='#666666')
wechat_label.grid(row=0, column=1, sticky=tk.N)
# 时间显示
self.time_var = tk.StringVar(value="")
time_label = ttk.Label(status_frame, textvariable=self.time_var, relief=tk.SUNKEN, anchor=tk.E, width=25)
time_label.grid(row=0, column=2, sticky=(tk.E))
# 更新时间
self.update_time()
# 设备扫描将在配置加载完成后进行,避免重复扫描
# self.root.after(500, self.async_refresh_devices) # 注释掉避免重复
except Exception as e:
print(f"创建完整界面时出错: {e}")
# 如果失败,至少显示基本界面
try:
if hasattr(self, 'startup_label') and self.startup_label.winfo_exists():
self.startup_label.config(text="❌ 界面加载失败")
except tk.TclError:
# startup_label 可能已经被销毁
pass
def update_time(self):
"""更新时间显示"""
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if hasattr(self, 'time_var'):
self.time_var.set(current_time)
self.root.after(1000, self.update_time)
def update_time(self):
"""更新时间显示"""
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.time_var.set(current_time)
self.root.after(1000, self.update_time)
def run_agent(self):
if self.running:
return
# 获取参数
base_url = self.base_url.get().strip()
model = self.model.get().strip()
apikey = self.apikey.get().strip()
task = self.task_text.get("1.0", tk.END).strip()
# 验证必要参数
if not base_url:
messagebox.showerror("错误", "请输入基础URL")
return
if not model:
messagebox.showerror("错误", "请输入模型名称")
return
if not apikey:
messagebox.showerror("错误", "请输入API密钥")
return
if not task:
messagebox.showerror("错误", "请输入任务描述")
return
# 设置运行状态和UI
self.running = True
self.run_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL)
self.status_var.set("🔄 正在执行任务...")
self.clear_output()
# 添加任务到历史记录
self.add_task_to_history(task)
# 获取设备ID,优先使用环境变量,其次是用户选择
selected_device = self.env_device_id or self.selected_device_id.get()
# 如果环境变量存在,输出提示信息
if self.env_device_id:
self._append_output(f"🔧 使用环境变量设备ID: {self.env_device_id}\n")
elif selected_device:
self._append_output(f"📱 使用用户选择设备ID: {selected_device}\n")
else:
self._append_output("⚠️ 未指定设备ID,将使用默认设备\n")
# 对于iOS设备,直接运行,不需要唤醒检测
if self.device_type.get() == 'iOS':
self._append_output(f"🍎 准备运行iOS设备任务...\n")
self.status_var.set("🍎 准备运行iOS任务...")
self._run_ios_agent(base_url, model, apikey, task)
else:
# 异步执行系统检查,避免阻塞界面
self._run_agent_async(base_url, model, apikey, task, selected_device)
def _run_adb_silent(self, cmd, timeout=10):
"""静默执行ADB命令,避免弹窗"""
import os
creation_flags = subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout,
creationflags=creation_flags)
def _run_ios_agent(self, base_url, model, apikey, task):
"""运行iOS设备代理"""
import sys
import os
import threading
import traceback
try:
# 获取iOS设备IP地址
ios_ip = self.ios_device_ip.get()
if not ios_ip:
messagebox.showerror("错误", "请先设置iOS设备IP地址")
return
# 构建iOS脚本路径
ios_script_path = os.path.join(os.path.dirname(__file__), "ios.py")
if not os.path.exists(ios_script_path):
self._append_output("❌ 未找到ios.py脚本文件\n")
messagebox.showerror("错误", "未找到ios.py脚本文件")
return
# 使用模块导入的方式,避免创建新进程和新窗口
def run_ios_in_thread():
try:
self._append_output(f"🍎 开始执行iOS任务...\n")
# 模拟命令行参数
old_argv = sys.argv[:]
sys.argv = [
ios_script_path,
"--base-url", base_url,
"--model", model,
"--apikey", apikey, # 修正参数名
"--wda-url", f"http://{ios_ip}:8100",
task
]
# 重定向stdout和stderr到GUI
import io
from contextlib import redirect_stdout, redirect_stderr
# 创建输出捕获器
class OutputCapture:
def __init__(self, append_func):
self.append_func = append_func
self.buffer = ""
def write(self, text):
# 立即写入所有文本,包括换行符
if text:
self.append_func(text)
self.buffer += text
def flush(self):
# 刷新缓冲区(这里不需要,因为我们立即写入)
pass
output_capture = OutputCapture(self._append_output)
# 在新线程中执行ios.py并捕获输出
with redirect_stdout(output_capture), redirect_stderr(output_capture):
# 执行ios.py的main逻辑
import ios
# 显式调用main函数,因为import不会自动执行
ios.main()
# 恢复原始命令行参数
sys.argv = old_argv
self._append_output(f"🍎 iOS任务执行完成\n")
success = True
except Exception as e:
self._append_output(f"❌ iOS任务执行失败: {str(e)}\n")
self._append_output(f"详细错误: {traceback.format_exc()}\n")
success = False
finally:
# 在主线程中更新UI状态
return_code = 0 if success else -1
self.root.after(0, lambda: self._on_process_finished(return_code))
# 在新线程中运行iOS任务,避免阻塞GUI
thread = threading.Thread(target=run_ios_in_thread, daemon=True)