-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathconfig_editor.py
More file actions
1553 lines (1292 loc) · 62.9 KB
/
config_editor.py
File metadata and controls
1553 lines (1292 loc) · 62.9 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 os
import sys
import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext
import copy
import ruamel.yaml
import yaml # 添加PyYAML导入
import urllib.request
import json
import webbrowser
import threading
class ConfigEditor:
# 定义版本号常量
VERSION = "0.1.4"
VERSION_CHECK_URL = "https://api.github.com/repos/your-username/xiaozhi-esp32-config-editor/releases/latest"
DOWNLOAD_URL = "https://github.com/your-username/xiaozhi-esp32-config-editor/releases/latest"
def __init__(self, root):
self.root = root
self.root.title(f"小智ESP32服务端配置编辑器v{self.VERSION} 作者:曾能混 关注我的B站 https://space.bilibili.com/298384872")
self.root.geometry("1000x700")
# 配置文件路径
self.config_path = os.path.join('data', '.config.yaml')
self.config = None
self.original_config = None
self.yaml = ruamel.yaml.YAML()
self.yaml.preserve_quotes = True
self.yaml.indent(mapping=2, sequence=4, offset=2)
# 检查配置文件和目录
self.check_config_file()
# 初始化翻译字典
self.init_translations()
# 加载配置
self.load_config()
# 创建UI
self.create_ui()
# 启动后台线程检查更新
threading.Thread(target=self.check_for_updates_silent, daemon=True).start()
def check_config_file(self):
"""检查配置文件和目录是否存在,如果不存在则提示创建"""
# 检查data目录是否存在
data_dir = os.path.dirname(self.config_path)
if not os.path.exists(data_dir):
if messagebox.askyesno("目录不存在", f"目录 {data_dir} 不存在,是否创建?", parent=self.root):
try:
os.makedirs(data_dir)
print(f"已创建目录: {data_dir}")
except Exception as e:
messagebox.showerror("错误", f"创建目录失败: {str(e)}", parent=self.root)
self.root.destroy()
sys.exit(0)
else:
messagebox.showinfo("退出", "应用程序将退出", parent=self.root)
self.root.destroy()
sys.exit(0)
# 检查配置文件是否存在
if not os.path.exists(self.config_path):
# 检查是否存在config.yaml文件
source_config = "config.yaml"
if os.path.exists(source_config):
if messagebox.askyesno("配置文件不存在",
f"配置文件 {self.config_path} 不存在,是否从 {source_config} 复制创建?",
parent=self.root):
try:
import shutil
shutil.copy2(source_config, self.config_path)
messagebox.showinfo("成功", f"已从 {source_config} 创建 {self.config_path}", parent=self.root)
except Exception as e:
messagebox.showerror("错误", f"复制配置文件失败: {str(e)}", parent=self.root)
if messagebox.askyesno("创建空文件", "是否创建空的配置文件?", parent=self.root):
try:
with open(self.config_path, 'w', encoding='utf-8') as f:
self.yaml.dump({}, f)
messagebox.showinfo("成功", f"已创建空的配置文件 {self.config_path}", parent=self.root)
except Exception as e:
messagebox.showerror("错误", f"创建配置文件失败: {str(e)}", parent=self.root)
self.root.destroy()
sys.exit(0)
else:
messagebox.showinfo("退出", "应用程序将退出", parent=self.root)
self.root.destroy()
sys.exit(0)
else:
if messagebox.askyesno("创建空文件", "是否创建空的配置文件?", parent=self.root):
try:
with open(self.config_path, 'w', encoding='utf-8') as f:
self.yaml.dump({}, f)
messagebox.showinfo("成功", f"已创建空的配置文件 {self.config_path}", parent=self.root)
except Exception as e:
messagebox.showerror("错误", f"创建配置文件失败: {str(e)}", parent=self.root)
self.root.destroy()
sys.exit(0)
else:
messagebox.showinfo("退出", "应用程序将退出", parent=self.root)
self.root.destroy()
sys.exit(0)
else:
# 如果没有源配置文件,询问是否创建空的配置文件
if messagebox.askyesno("配置文件不存在",
f"配置文件 {self.config_path} 不存在,且未找到源文件 {source_config},是否创建空的配置文件?",
parent=self.root):
try:
with open(self.config_path, 'w', encoding='utf-8') as f:
self.yaml.dump({}, f)
messagebox.showinfo("成功", f"已创建空的配置文件 {self.config_path}", parent=self.root)
except Exception as e:
messagebox.showerror("错误", f"创建配置文件失败: {str(e)}", parent=self.root)
self.root.destroy()
sys.exit(0)
else:
messagebox.showinfo("退出", "应用程序将退出", parent=self.root)
self.root.destroy()
sys.exit(0)
def init_translations(self):
"""初始化翻译字典"""
self.translations = {
# 主菜单项
"server": "服务器设置",
"log": "日志设置",
"iot": "物联网设备",
"xiaozhi": "小智设置",
"selected_module": "模块选择",
"prompt": "提示词",
"ASR": "语音识别",
"VAD": "语音活动检测",
"LLM": "大语言模型",
"TTS": "语音合成",
"Memory": "记忆模块",
"Intent": "意图识别",
"music": "音乐设置",
"module_test": "模块测试",
"delete_audio": "删除音频",
"close_connection_no_voice_time": "无语音断开时间",
"CMD_exit": "退出命令",
"manager": "管理器",
"use_private_config": "使用私有配置",
# 服务器设置
"ip": "IP地址",
"port": "端口",
"auth": "认证设置",
"enabled": "启用",
"tokens": "令牌列表",
"token": "令牌",
"name": "名称",
"allowed_devices": "允许设备",
# 日志设置
"log_format": "日志格式",
"log_format_simple": "简单日志格式",
"log_level": "日志级别",
"log_dir": "日志目录",
"log_file": "日志文件",
"data_dir": "数据目录",
# 物联网设备
"Speaker": "扬声器",
"volume": "音量",
# 小智设置
"type": "类型",
"version": "版本",
"transport": "传输方式",
"audio_params": "音频参数",
"format": "格式",
"sample_rate": "采样率",
"channels": "通道数",
"frame_duration": "帧持续时间",
# 音乐设置
"music_dir": "音乐目录",
"music_ext": "音乐文件扩展名",
"refresh_time": "刷新时间",
# 模块测试
"test_sentences": "测试句子",
# 按钮和通用文本
"Save": "保存",
"Reset": "重置",
"Add": "添加",
"Edit": "编辑",
"Delete": "删除",
"Update": "更新",
"Cancel": "取消",
"Confirm": "确认",
"Value": "值",
"Enable": "启用",
"Disable": "禁用",
"Success": "成功",
"Error": "错误",
"Warning": "警告",
# 模型名称
"SileroVAD": "Silero语音活动检测",
"FunASR": "Fun语音识别",
"DoubaoASR": "豆包语音识别",
"OllamaLLM": "Ollama大语言模型",
"GPT_SOVITS_V2": "GPT语音合成V2",
"GPT_SOVITS_V3": "GPT语音合成V3",
"mem_local_short": "本地短期记忆",
"intent_llm": "LLM意图识别",
"function_call": "函数调用意图识别",
"nointent": "无意图识别",
"nomem": "无记忆",
"mem0ai": "Mem0AI记忆",
}
def translate(self, key):
"""翻译键名"""
return self.translations.get(key, key)
def load_config(self):
"""加载配置文件,保留原始格式和注释"""
try:
# 初始化ruamel.yaml
self.yaml = ruamel.yaml.YAML()
self.yaml.preserve_quotes = True
self.yaml.indent(mapping=2, sequence=4, offset=2)
# 使用ruamel.yaml加载配置文件
with open(self.config_path, 'r', encoding='utf-8') as f:
self.config = self.yaml.load(f)
# 保存原始配置的副本
self.original_config = copy.deepcopy(self.config)
if self.config is None:
self.config = {}
self.original_config = {}
except Exception as e:
messagebox.showerror("错误", f"加载配置文件失败: {str(e)}", parent=self.root)
if hasattr(self, 'config') and self.config is not None:
# 已有配置,不做任何操作
return
else:
# 首次加载失败,退出程序
self.root.destroy()
sys.exit(0)
def save_config(self):
"""保存配置文件,保留原始格式和注释"""
try:
# 创建备份
import shutil
backup_path = f"{self.config_path}.bak"
try:
shutil.copy2(self.config_path, backup_path)
except Exception as e:
print(f"创建备份失败: {str(e)}")
# 使用ruamel.yaml保存配置,保留原始格式和注释
with open(self.config_path, 'r', encoding='utf-8') as f:
yaml_content = self.yaml.load(f)
# 更新修改的值
self.update_yaml_values(yaml_content, self.config)
# 保存回文件
with open(self.config_path, 'w', encoding='utf-8') as f:
self.yaml.dump(yaml_content, f)
messagebox.showinfo("成功", "配置已保存", parent=self.root)
self.original_config = copy.deepcopy(self.config)
except Exception as e:
messagebox.showerror("错误", f"保存配置文件失败: {str(e)}", parent=self.root)
# 如果保存失败且存在备份,询问是否恢复
if os.path.exists(backup_path):
if messagebox.askyesno("恢复备份", "保存失败,是否从备份恢复?", parent=self.root):
try:
shutil.copy2(backup_path, self.config_path)
messagebox.showinfo("成功", "已从备份恢复", parent=self.root)
except Exception as restore_err:
messagebox.showerror("错误", f"恢复备份失败: {str(restore_err)}", parent=self.root)
def update_yaml_values(self, target, source):
"""递归更新YAML值,保留注释和格式"""
if isinstance(source, dict) and isinstance(target, dict):
for key, value in source.items():
if key in target:
if isinstance(value, (dict, list)) and isinstance(target[key], (dict, list)):
self.update_yaml_values(target[key], value)
else:
target[key] = value
else:
target[key] = value
elif isinstance(source, list) and isinstance(target, list):
# 对于列表,我们需要完全替换,因为索引可能已经改变
target.clear()
target.extend(source)
def create_ui(self):
"""创建用户界面 - 添加检查更新按钮"""
# 创建主框架
main_frame = ttk.Frame(self.root)
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 创建工具栏
toolbar = ttk.Frame(main_frame)
toolbar.pack(fill=tk.X, pady=(0, 10))
# 添加加载配置文件按钮
ttk.Button(toolbar, text="加载配置", command=self.load_config_file_dialog).pack(side=tk.LEFT, padx=5)
ttk.Button(toolbar, text="保存到文件", command=self.save_config).pack(side=tk.LEFT, padx=5)
ttk.Button(toolbar, text="重置", command=self.reset_config).pack(side=tk.LEFT, padx=5)
# 添加检查更新按钮
ttk.Button(toolbar, text="检查更新", command=self.check_for_updates).pack(side=tk.LEFT, padx=5)
# 添加关于按钮
ttk.Button(toolbar, text="关于", command=self.show_about).pack(side=tk.RIGHT, padx=5)
# 显示当前配置文件路径
self.file_path_var = tk.StringVar(value=f"当前配置: {self.config_path}")
ttk.Label(toolbar, textvariable=self.file_path_var).pack(side=tk.RIGHT, padx=20)
# 创建分割窗口
paned = ttk.PanedWindow(main_frame, orient=tk.HORIZONTAL)
paned.pack(fill=tk.BOTH, expand=True)
# 左侧菜单区域
left_frame = ttk.Frame(paned, width=120)
paned.add(left_frame, weight=1)
# 创建树形菜单
self.menu_tree = ttk.Treeview(left_frame, show="tree")
self.menu_tree.pack(fill=tk.BOTH, expand=True, padx=0, pady=0)
self.menu_tree.bind("<<TreeviewSelect>>", self.on_menu_select)
# 设置菜单样式,使文字完全靠左对齐
style = ttk.Style()
style.configure("Treeview", indent=0)
# 填充菜单
self.populate_menu()
# 右侧内容区域 - 使用Frame包含内容和按钮区域
right_container = ttk.Frame(paned)
paned.add(right_container, weight=4)
# 创建内容区域(带滚动条)
content_container = ttk.Frame(right_container)
content_container.pack(fill=tk.BOTH, expand=True, padx=0, pady=0)
# 创建Canvas和滚动条
self.canvas = tk.Canvas(content_container)
scrollbar = ttk.Scrollbar(content_container, orient="vertical", command=self.canvas.yview)
# 创建内容框架
self.content_frame = ttk.Frame(self.canvas)
# 配置Canvas
self.canvas.configure(yscrollcommand=scrollbar.set)
self.canvas_window = self.canvas.create_window((0, 0), window=self.content_frame, anchor="nw")
# 绑定事件,确保内容框架宽度与Canvas宽度一致
def configure_canvas(event):
self.canvas.itemconfig(self.canvas_window, width=event.width)
self.canvas.bind('<Configure>', configure_canvas)
# 绑定事件,确保Canvas滚动区域与内容框架大小一致
def configure_scroll_region(event):
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
self.content_frame.bind('<Configure>', configure_scroll_region)
# 放置Canvas和滚动条
self.canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# 添加底部按钮区域(固定在底部)
self.bottom_frame = ttk.Frame(right_container)
self.bottom_frame.pack(fill=tk.X, padx=10, pady=10, side=tk.BOTTOM)
# 添加全局应用更改按钮(始终可用)
self.apply_button = ttk.Button(
self.bottom_frame,
text="应用更改",
command=self.apply_changes
)
self.apply_button.pack(side=tk.RIGHT, padx=5)
# 初始化变更跟踪字典
self.changes = {}
# 绑定鼠标滚轮事件
self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)
self.canvas.bind_all("<Button-4>", self._on_mousewheel)
self.canvas.bind_all("<Button-5>", self._on_mousewheel)
def populate_menu(self):
"""填充左侧菜单 - 动态从配置文件读取"""
self.menu_tree.delete(*self.menu_tree.get_children())
# 按照特定顺序显示主要配置项
priority_keys = [
"server", "log", "iot", "xiaozhi", "selected_module", "prompt",
"delete_audio", "close_connection_no_voice_time", "CMD_exit",
"music", "module_test", "manager", "use_private_config"
]
# 首先添加优先级高的配置项
for key in priority_keys:
if key in self.config:
display_name = self.translate(key)
self.menu_tree.insert("", "end", text=display_name, values=(key,))
# 然后添加其他配置项
for key in self.config:
if key not in priority_keys:
display_name = self.translate(key)
self.menu_tree.insert("", "end", text=display_name, values=(key,))
def filter_menu(self, search_text):
"""根据搜索文本过滤菜单"""
self.menu_tree.delete(*self.menu_tree.get_children())
search_text = search_text.lower()
for key in self.config:
if search_text in key.lower():
self.menu_tree.insert("", "end", text=key, values=(key,))
def on_menu_select(self, event):
"""处理菜单选择事件 - 使用翻译,去掉标题框"""
selected = self.menu_tree.selection()
if not selected:
return
item = self.menu_tree.item(selected[0])
key = item['values'][0]
# 添加调试信息
print(f"选择菜单项: {key}")
print(f"配置类型: {type(self.config[key])}")
if isinstance(self.config[key], dict):
print(f"子项: {list(self.config[key].keys())}")
# 清空内容区
for widget in self.content_frame.winfo_children():
widget.destroy()
# 创建编辑界面
try:
self.create_editor(key, self.config[key])
except Exception as e:
messagebox.showerror("错误", f"创建编辑界面失败: {str(e)}", parent=self.root)
import traceback
traceback.print_exc()
def create_editor(self, key, value):
"""创建编辑界面,在内容框架中创建编辑器"""
# 清空内容区
for widget in self.content_frame.winfo_children():
widget.destroy()
# 根据不同类型创建不同的编辑器
if key == "prompt":
self.create_prompt_editor(self.content_frame, key, value)
elif key == "selected_module":
self.create_module_selector(self.content_frame, key, value)
elif isinstance(value, dict):
self.create_dict_editor(self.content_frame, key, value)
elif isinstance(value, list):
self.create_list_editor(self.content_frame, key, value)
elif isinstance(value, bool):
self.create_bool_editor(self.content_frame, key, value)
else:
self.create_simple_editor(self.content_frame, key, value)
# 重置滚动位置到顶部
self.canvas.yview_moveto(0)
def create_prompt_editor(self, parent, key, value):
"""创建提示词编辑器 - 使用全局保存按钮"""
frame = ttk.LabelFrame(parent, text="编辑提示词")
frame.pack(fill=tk.X, expand=True, padx=10, pady=5)
# 添加说明
ttk.Label(
frame,
text="提示词决定AI的行为和回复风格,请谨慎编辑",
wraplength=700
).pack(anchor=tk.W, padx=10, pady=(5, 10))
# 创建文本编辑器
text = scrolledtext.ScrolledText(frame, wrap=tk.WORD, height=15)
text.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
text.insert(tk.END, value)
# 添加文本变更事件
def on_text_change(event=None):
self.track_change(key, text.get(1.0, tk.END).strip())
text.bind("<KeyRelease>", on_text_change)
# 创建按钮框架
btn_frame = ttk.Frame(frame)
btn_frame.pack(fill=tk.X, padx=10, pady=10)
ttk.Button(
btn_frame,
text="重置",
command=lambda: text.delete(1.0, tk.END) or text.insert(tk.END, value)
).pack(side=tk.LEFT, padx=5)
def create_module_selector(self, parent, key, value):
"""创建模块选择器界面 - 动态适应配置文件变化"""
ttk.Label(
parent,
text="选择要使用的各类模块",
font=("Arial", 11, "bold")
).pack(anchor=tk.W, padx=10, pady=(5, 10))
# 模块说明 - 从配置文件中提取注释
module_descriptions = self.extract_module_descriptions()
# 为每个模块创建选择器
for module_type, current_value in value.items():
# 创建模块框架
module_frame = ttk.LabelFrame(parent, text=f"{self.translate(module_type)}")
module_frame.pack(fill=tk.X, expand=True, padx=10, pady=5)
# 添加模块描述
if module_type in module_descriptions:
ttk.Label(
module_frame,
text=module_descriptions[module_type],
wraplength=700
).pack(anchor=tk.W, padx=10, pady=(5, 10))
# 获取可用的选项
options = self.get_available_options(module_type)
# 创建选择框架
select_frame = ttk.Frame(module_frame)
select_frame.pack(fill=tk.X, padx=10, pady=10)
ttk.Label(select_frame, text="当前选择:").pack(side=tk.LEFT, padx=(0, 10))
# 创建下拉菜单
combo_var = tk.StringVar(value=current_value)
# 如果当前值不在选项中,添加它
if current_value not in options and current_value:
options.append(current_value)
# 如果没有选项,添加一个空选项
if not options:
options = [""]
combo = ttk.Combobox(
select_frame,
textvariable=combo_var,
values=options,
width=30,
state="readonly"
)
combo.pack(side=tk.LEFT, padx=(0, 10))
# 添加变更跟踪
def on_combo_change(module=module_type, var=combo_var):
self.track_nested_change(key, module, None, var.get())
# 绑定下拉菜单变更事件
combo.bind("<<ComboboxSelected>>", lambda event, m=module_type, v=combo_var:
self.track_nested_change(key, m, None, v.get()))
def create_dict_editor(self, parent, key, value):
"""创建字典编辑器 - 使用翻译"""
ttk.Label(
parent,
text=f"编辑 {self.translate(key)} 配置",
font=("Arial", 11, "bold")
).pack(anchor=tk.W, padx=10, pady=(5, 10))
# 为每个子项创建编辑框架
for sub_key, sub_value in value.items():
# 创建子项框架 - 使用翻译
sub_frame = ttk.LabelFrame(parent, text=self.translate(sub_key))
sub_frame.pack(fill=tk.X, expand=True, padx=10, pady=5)
if isinstance(sub_value, dict):
self.create_nested_dict_editor(sub_frame, key, sub_key, sub_value)
elif isinstance(sub_value, list):
self.create_nested_list_editor(sub_frame, key, sub_key, sub_value)
elif isinstance(sub_value, bool):
self.create_nested_bool_editor(sub_frame, key, sub_key, sub_value)
else:
self.create_nested_simple_editor(sub_frame, key, sub_key, sub_value)
def create_nested_list_editor(self, parent, parent_key, key, value):
"""创建嵌套列表编辑器 - 为tokens列表提供文本编辑方式"""
# 检查是否是tokens列表
is_tokens_list = (key == "tokens")
# 如果是tokens列表,使用文本编辑方式
if is_tokens_list:
# 创建框架
text_frame = ttk.Frame(parent)
text_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# 提取真实数据(去除ruamel.yaml内部结构)
clean_tokens = []
for item in value:
clean_token = {}
if 'token' in item:
clean_token['token'] = str(item['token'])
if 'name' in item:
clean_token['name'] = str(item['name'])
if 'allowed_devices' in item:
clean_token['allowed_devices'] = [str(dev) for dev in item.get('allowed_devices', [])]
clean_tokens.append(clean_token)
# 转换为普通YAML文本
import yaml
yaml_text = yaml.dump(clean_tokens, default_flow_style=False)
# 创建文本编辑器
text = scrolledtext.ScrolledText(text_frame, wrap=tk.WORD, height=10)
text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
text.insert(tk.END, yaml_text)
# 添加说明
ttk.Label(
text_frame,
text="请按YAML格式编辑令牌列表,每个令牌项包含token和name字段",
wraplength=700,
foreground="gray"
).pack(anchor=tk.W, padx=5, pady=(5, 0))
# 添加文本变更事件
def on_text_change(event=None):
try:
# 尝试解析YAML文本
new_value = yaml.safe_load(text.get(1.0, tk.END))
if new_value is None:
new_value = []
# 验证格式
valid = True
for item in new_value:
if not isinstance(item, dict) or 'token' not in item or 'name' not in item:
valid = False
break
if valid:
# 跟踪变更
self.track_nested_change(parent_key, key, None, new_value)
# 移除错误提示(如果有)
for widget in text_frame.winfo_children():
if hasattr(widget, 'error_label') and widget.error_label:
widget.error_label.destroy()
widget.error_label = None
else:
# 显示错误提示
if not hasattr(text, 'error_label') or not text.error_label:
text.error_label = ttk.Label(
text_frame,
text="格式错误:每个项必须包含token和name字段",
foreground="red"
)
text.error_label.pack(anchor=tk.W, padx=5, pady=(0, 5))
except Exception as e:
# 显示错误提示
if not hasattr(text, 'error_label') or not text.error_label:
text.error_label = ttk.Label(
text_frame,
text=f"YAML解析错误: {str(e)}",
foreground="red"
)
text.error_label.pack(anchor=tk.W, padx=5, pady=(0, 5))
text.bind("<KeyRelease>", on_text_change)
# 添加重置按钮
btn_frame = ttk.Frame(parent)
btn_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Button(
btn_frame,
text="重置",
command=lambda: text.delete(1.0, tk.END) or text.insert(tk.END, yaml.dump(clean_tokens, default_flow_style=False))
).pack(side=tk.LEFT, padx=5)
else:
# 创建列表框
list_frame = ttk.Frame(parent)
list_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# 列表显示
listbox = tk.Listbox(list_frame, height=6)
listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 滚动条
scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=listbox.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
listbox.config(yscrollcommand=scrollbar.set)
# 填充列表
for item in value:
listbox.insert(tk.END, str(item))
# 编辑按钮
btn_frame = ttk.Frame(parent)
btn_frame.pack(fill=tk.X, padx=5, pady=5)
# 添加按钮 - 使用变更跟踪
def add_item():
dialog = tk.Toplevel(self.root)
dialog.title("添加项")
dialog.geometry("300x100")
dialog.transient(self.root)
dialog.grab_set()
ttk.Label(dialog, text="输入新项:").pack(pady=(10, 5))
var = tk.StringVar()
entry = ttk.Entry(dialog, textvariable=var, width=40)
entry.pack(pady=5)
def add():
value = var.get()
if value:
# 获取当前列表的副本
current_list = []
parts = parent_key.split(".")
current = self.config
for part in parts:
current = current[part]
current_list = current[key].copy()
current_list.append(value)
# 跟踪变更
self.track_nested_change(parent_key, key, None, current_list)
# 更新UI
listbox.insert(tk.END, value)
dialog.destroy()
ttk.Button(btn_frame, text="添加", command=add_item).pack(side=tk.LEFT, padx=5)
# 编辑按钮 - 使用变更跟踪
def edit_item():
selected = listbox.curselection()
if not selected:
messagebox.showwarning("警告", "请先选择一项", parent=self.root)
return
index = selected[0]
# 获取当前值
parts = parent_key.split(".")
current = self.config
for part in parts:
current = current[part]
old_value = current[key][index]
dialog = tk.Toplevel(self.root)
dialog.title("编辑项")
dialog.geometry("300x100")
dialog.transient(self.root)
dialog.grab_set()
ttk.Label(dialog, text="编辑项:").pack(pady=(10, 5))
var = tk.StringVar(value=str(old_value))
entry = ttk.Entry(dialog, textvariable=var, width=40)
entry.pack(pady=5)
def update():
value = var.get()
if value:
# 获取当前列表的副本
current_list = []
parts = parent_key.split(".")
current = self.config
for part in parts:
current = current[part]
current_list = current[key].copy()
current_list[index] = value
# 跟踪变更
self.track_nested_change(parent_key, key, None, current_list)
# 更新UI
listbox.delete(index)
listbox.insert(index, value)
dialog.destroy()
ttk.Button(dialog, text="更新", command=update).pack(pady=5)
ttk.Button(btn_frame, text="编辑", command=edit_item).pack(side=tk.LEFT, padx=5)
# 删除按钮 - 使用变更跟踪
def delete_item():
selected = listbox.curselection()
if not selected:
messagebox.showwarning("警告", "请先选择一项", parent=self.root)
return
index = selected[0]
if messagebox.askyesno("确认", "确定要删除所选项吗?", parent=self.root):
# 获取当前列表的副本
current_list = []
parts = parent_key.split(".")
current = self.config
for part in parts:
current = current[part]
current_list = current[key].copy()
del current_list[index]
# 跟踪变更
self.track_nested_change(parent_key, key, None, current_list)
# 更新UI
listbox.delete(index)
def create_bool_editor(self, parent, key, value):
"""创建布尔值编辑器 - 移除保存按钮,添加变更跟踪"""
frame = ttk.LabelFrame(parent, text=f"编辑 {key}")
frame.pack(fill=tk.X, expand=True, padx=10, pady=5)
var = tk.BooleanVar(value=value)
# 添加变更跟踪
var.trace_add("write", lambda *args: self.track_change(key, var.get()))
# 创建选择框架
select_frame = ttk.Frame(frame)
select_frame.pack(fill=tk.X, padx=10, pady=10)
ttk.Checkbutton(
select_frame,
text="启用",
variable=var
).pack(side=tk.LEFT, padx=(0, 10))
def create_simple_editor(self, parent, key, value):
"""创建简单值编辑器 - 移除保存按钮,添加变更跟踪"""
frame = ttk.LabelFrame(parent, text=f"编辑 {key}")
frame.pack(fill=tk.X, expand=True, padx=10, pady=5)
# 创建编辑框架
edit_frame = ttk.Frame(frame)
edit_frame.pack(fill=tk.X, padx=10, pady=10)
var = tk.StringVar(value=str(value))
# 添加变更跟踪
var.trace_add("write", lambda *args: self.track_change(key, var.get(), type(value)))
ttk.Label(edit_frame, text="值:").pack(side=tk.LEFT, padx=(0, 5))
entry = ttk.Entry(edit_frame, textvariable=var, width=50)
entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
def create_nested_dict_editor(self, parent, parent_key, key, value):
"""创建嵌套字典编辑器 - 去掉多余的容器框架"""
# 直接在父框架中创建子项,不使用额外的容器框架
for sub_key, sub_value in value.items():
# 创建子项框架
sub_frame = ttk.LabelFrame(parent, text=sub_key)
sub_frame.pack(fill=tk.X, expand=True, padx=5, pady=5)
if isinstance(sub_value, dict):
# 递归处理嵌套字典
self.create_nested_dict_editor(sub_frame, f"{parent_key}.{key}", sub_key, sub_value)
elif isinstance(sub_value, list):
# 处理嵌套列表 - 修复这里,添加缺少的key参数
self.create_nested_list_editor(sub_frame, f"{parent_key}.{key}", sub_key, sub_value)
elif isinstance(sub_value, bool):
# 处理嵌套布尔值
var = tk.BooleanVar(value=sub_value)
check_frame = ttk.Frame(sub_frame)
check_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Checkbutton(
check_frame,
text="启用",
variable=var
).pack(side=tk.LEFT, padx=(0, 10))
ttk.Button(
check_frame,
text="保存",
command=lambda p=f"{parent_key}.{key}", k=sub_key, v=var:
self.update_nested_value(p, k, None, v.get())
).pack(side=tk.LEFT)
else:
# 处理嵌套简单值
self.create_nested_simple_editor(sub_frame, f"{parent_key}.{key}", sub_key, sub_value)
def create_nested_bool_editor(self, parent, parent_key, key, value):
"""创建嵌套布尔值编辑器"""
edit_frame = ttk.Frame(parent)
edit_frame.pack(fill=tk.X, padx=5, pady=5)
var = tk.BooleanVar(value=value)
ttk.Checkbutton(
edit_frame,
text="启用",
variable=var
).pack(side=tk.LEFT, padx=(0, 10))
ttk.Button(
edit_frame,
text="保存",
command=lambda p=parent_key, k=key, v=var:
self.update_nested_value(p, k, None, v.get())
).pack(side=tk.LEFT)
def create_nested_simple_editor(self, parent, parent_key, key, value):
"""创建嵌套简单值编辑器 - 移除保存按钮,添加变更跟踪"""
edit_frame = ttk.Frame(parent)
edit_frame.pack(fill=tk.X, padx=5, pady=5)
# 添加说明标签
if isinstance(value, str):
if value.startswith("你的") or value.startswith("your"):
ttk.Label(
edit_frame,
text="请输入您的配置值",
foreground="gray"
).pack(anchor=tk.W, padx=5, pady=(0, 5))
elif "#" in value: # 如果值中包含注释
comment = value.split("#", 1)[1].strip()
ttk.Label(
edit_frame,
text=comment,
foreground="gray"
).pack(anchor=tk.W, padx=5, pady=(0, 5))
var = tk.StringVar(value=str(value))
# 添加变更跟踪
var.trace_add("write", lambda *args: self.track_nested_change(parent_key, key, None, var.get(), type(value)))
ttk.Label(edit_frame, text=f"{key}:").pack(side=tk.LEFT, padx=(0, 5))
entry = ttk.Entry(edit_frame, textvariable=var, width=50)
entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
def update_value(self, key, value):
"""更新配置值"""
self.config[key] = value
messagebox.showinfo("成功", f"{key} 已更新", parent=self.root)
def update_nested_value(self, parent_key, key, sub_key, value):
"""更新嵌套配置值"""
try:
if "." in parent_key: # 处理多层嵌套
parts = parent_key.split(".")
current = self.config
for part in parts:
current = current[part]
if sub_key:
current[key][sub_key] = value
else:
current[key] = value
messagebox.showinfo("成功", f"{sub_key if sub_key else key} 已更新", parent=self.root)
except Exception as e:
messagebox.showerror("错误", f"更新配置失败: {str(e)}\n路径: {parent_key}.{key}" +
(f".{sub_key}" if sub_key else ""), parent=self.root)
def add_list_item(self, key, listbox):
"""添加列表项"""
dialog = tk.Toplevel(self.root)
dialog.title("添加项")
dialog.geometry("300x100")
dialog.transient(self.root)
dialog.grab_set()
ttk.Label(dialog, text="输入新项:").pack(pady=(10, 5))
var = tk.StringVar()
entry = ttk.Entry(dialog, textvariable=var, width=40)
entry.pack(pady=5)
def add():
value = var.get()
if value:
self.config[key].append(value)
listbox.insert(tk.END, value)
dialog.destroy()
ttk.Button(dialog, text="添加", command=add).pack(pady=5)
def edit_list_item(self, key, listbox):
"""编辑列表项"""
selected = listbox.curselection()