1250 lines
48 KiB
Python
1250 lines
48 KiB
Python
import os
|
||
import sys
|
||
import toml
|
||
import customtkinter as ctk
|
||
from tkinter import messagebox, StringVar, filedialog
|
||
import json
|
||
import datetime
|
||
import shutil
|
||
|
||
# 设置主题
|
||
ctk.set_appearance_mode("System") # 系统主题
|
||
ctk.set_default_color_theme("blue") # 蓝色主题
|
||
|
||
# 配置项的中文翻译映射
|
||
SECTION_TRANSLATIONS = {
|
||
"inner": "内部配置",
|
||
"bot": "机器人设置",
|
||
"groups": "群组设置",
|
||
"personality": "人格设置",
|
||
"identity": "身份设置",
|
||
"schedule": "日程设置",
|
||
"platforms": "平台设置",
|
||
"response": "回复设置",
|
||
"heartflow": "心流设置",
|
||
"message": "消息设置",
|
||
"willing": "意愿设置",
|
||
"emoji": "表情设置",
|
||
"memory": "记忆设置",
|
||
"mood": "情绪设置",
|
||
"keywords_reaction": "关键词反应",
|
||
"chinese_typo": "中文错别字",
|
||
"response_splitter": "回复分割器",
|
||
"remote": "远程设置",
|
||
"experimental": "实验功能",
|
||
"model": "模型设置",
|
||
}
|
||
|
||
# 配置项的中文描述
|
||
CONFIG_DESCRIPTIONS = {
|
||
# bot设置
|
||
"bot.qq": "机器人的QQ号码",
|
||
"bot.nickname": "机器人的昵称",
|
||
"bot.alias_names": "机器人的别名列表",
|
||
# 群组设置
|
||
"groups.talk_allowed": "允许机器人回复消息的群号列表",
|
||
"groups.talk_frequency_down": "降低回复频率的群号列表",
|
||
"groups.ban_user_id": "禁止回复和读取消息的QQ号列表",
|
||
# 人格设置
|
||
"personality.personality_core": "人格核心描述,建议20字以内",
|
||
"personality.personality_sides": "人格特点列表",
|
||
# 身份设置
|
||
"identity.identity_detail": "身份细节描述列表",
|
||
"identity.height": "身高(厘米)",
|
||
"identity.weight": "体重(千克)",
|
||
"identity.age": "年龄",
|
||
"identity.gender": "性别",
|
||
"identity.appearance": "外貌特征",
|
||
# 日程设置
|
||
"schedule.enable_schedule_gen": "是否启用日程表生成",
|
||
"schedule.prompt_schedule_gen": "日程表生成提示词",
|
||
"schedule.schedule_doing_update_interval": "日程表更新间隔(秒)",
|
||
"schedule.schedule_temperature": "日程表温度,建议0.3-0.6",
|
||
"schedule.time_zone": "时区设置",
|
||
# 平台设置
|
||
"platforms.nonebot-qq": "QQ平台适配器链接",
|
||
# 回复设置
|
||
"response.response_mode": "回复策略(heart_flow:心流,reasoning:推理)",
|
||
"response.model_r1_probability": "主要回复模型使用概率",
|
||
"response.model_v3_probability": "次要回复模型使用概率",
|
||
# 心流设置
|
||
"heartflow.sub_heart_flow_update_interval": "子心流更新频率(秒)",
|
||
"heartflow.sub_heart_flow_freeze_time": "子心流冻结时间(秒)",
|
||
"heartflow.sub_heart_flow_stop_time": "子心流停止时间(秒)",
|
||
"heartflow.heart_flow_update_interval": "心流更新频率(秒)",
|
||
# 消息设置
|
||
"message.max_context_size": "获取的上下文数量",
|
||
"message.emoji_chance": "使用表情包的概率",
|
||
"message.thinking_timeout": "思考时间(秒)",
|
||
"message.max_response_length": "回答的最大token数",
|
||
"message.message_buffer": "是否启用消息缓冲器",
|
||
"message.ban_words": "禁用词列表",
|
||
"message.ban_msgs_regex": "禁用消息正则表达式列表",
|
||
# 意愿设置
|
||
"willing.willing_mode": "回复意愿模式",
|
||
"willing.response_willing_amplifier": "回复意愿放大系数",
|
||
"willing.response_interested_rate_amplifier": "回复兴趣度放大系数",
|
||
"willing.down_frequency_rate": "降低回复频率的群组回复意愿降低系数",
|
||
"willing.emoji_response_penalty": "表情包回复惩罚系数",
|
||
# 表情设置
|
||
"emoji.max_emoji_num": "表情包最大数量",
|
||
"emoji.max_reach_deletion": "达到最大数量时是否删除表情包",
|
||
"emoji.check_interval": "检查表情包的时间间隔",
|
||
"emoji.auto_save": "是否保存表情包和图片",
|
||
"emoji.enable_check": "是否启用表情包过滤",
|
||
"emoji.check_prompt": "表情包过滤要求",
|
||
# 记忆设置
|
||
"memory.build_memory_interval": "记忆构建间隔(秒)",
|
||
"memory.build_memory_distribution": "记忆构建分布参数",
|
||
"memory.build_memory_sample_num": "采样数量",
|
||
"memory.build_memory_sample_length": "采样长度",
|
||
"memory.memory_compress_rate": "记忆压缩率",
|
||
"memory.forget_memory_interval": "记忆遗忘间隔(秒)",
|
||
"memory.memory_forget_time": "记忆遗忘时间(小时)",
|
||
"memory.memory_forget_percentage": "记忆遗忘比例",
|
||
"memory.memory_ban_words": "记忆禁用词列表",
|
||
# 情绪设置
|
||
"mood.mood_update_interval": "情绪更新间隔(秒)",
|
||
"mood.mood_decay_rate": "情绪衰减率",
|
||
"mood.mood_intensity_factor": "情绪强度因子",
|
||
# 关键词反应
|
||
"keywords_reaction.enable": "是否启用关键词反应功能",
|
||
# 中文错别字
|
||
"chinese_typo.enable": "是否启用中文错别字生成器",
|
||
"chinese_typo.error_rate": "单字替换概率",
|
||
"chinese_typo.min_freq": "最小字频阈值",
|
||
"chinese_typo.tone_error_rate": "声调错误概率",
|
||
"chinese_typo.word_replace_rate": "整词替换概率",
|
||
# 回复分割器
|
||
"response_splitter.enable_response_splitter": "是否启用回复分割器",
|
||
"response_splitter.response_max_length": "回复允许的最大长度",
|
||
"response_splitter.response_max_sentence_num": "回复允许的最大句子数",
|
||
# 远程设置
|
||
"remote.enable": "是否启用远程统计",
|
||
# 实验功能
|
||
"experimental.enable_friend_chat": "是否启用好友聊天",
|
||
"experimental.pfc_chatting": "是否启用PFC聊天",
|
||
# 模型设置
|
||
"model.llm_reasoning.name": "推理模型名称",
|
||
"model.llm_reasoning.provider": "推理模型提供商",
|
||
"model.llm_reasoning.pri_in": "推理模型输入价格",
|
||
"model.llm_reasoning.pri_out": "推理模型输出价格",
|
||
"model.llm_normal.name": "回复模型名称",
|
||
"model.llm_normal.provider": "回复模型提供商",
|
||
"model.llm_normal.pri_in": "回复模型输入价格",
|
||
"model.llm_normal.pri_out": "回复模型输出价格",
|
||
"model.llm_emotion_judge.name": "表情判断模型名称",
|
||
"model.llm_emotion_judge.provider": "表情判断模型提供商",
|
||
"model.llm_emotion_judge.pri_in": "表情判断模型输入价格",
|
||
"model.llm_emotion_judge.pri_out": "表情判断模型输出价格",
|
||
"model.llm_topic_judge.name": "主题判断模型名称",
|
||
"model.llm_topic_judge.provider": "主题判断模型提供商",
|
||
"model.llm_topic_judge.pri_in": "主题判断模型输入价格",
|
||
"model.llm_topic_judge.pri_out": "主题判断模型输出价格",
|
||
"model.llm_summary_by_topic.name": "概括模型名称",
|
||
"model.llm_summary_by_topic.provider": "概括模型提供商",
|
||
"model.llm_summary_by_topic.pri_in": "概括模型输入价格",
|
||
"model.llm_summary_by_topic.pri_out": "概括模型输出价格",
|
||
"model.moderation.name": "内容审核模型名称",
|
||
"model.moderation.provider": "内容审核模型提供商",
|
||
"model.moderation.pri_in": "内容审核模型输入价格",
|
||
"model.moderation.pri_out": "内容审核模型输出价格",
|
||
"model.vlm.name": "图像识别模型名称",
|
||
"model.vlm.provider": "图像识别模型提供商",
|
||
"model.vlm.pri_in": "图像识别模型输入价格",
|
||
"model.vlm.pri_out": "图像识别模型输出价格",
|
||
"model.embedding.name": "嵌入模型名称",
|
||
"model.embedding.provider": "嵌入模型提供商",
|
||
"model.embedding.pri_in": "嵌入模型输入价格",
|
||
"model.embedding.pri_out": "嵌入模型输出价格",
|
||
"model.llm_observation.name": "观察模型名称",
|
||
"model.llm_observation.provider": "观察模型提供商",
|
||
"model.llm_observation.pri_in": "观察模型输入价格",
|
||
"model.llm_observation.pri_out": "观察模型输出价格",
|
||
"model.llm_sub_heartflow.name": "子心流模型名称",
|
||
"model.llm_sub_heartflow.provider": "子心流模型提供商",
|
||
"model.llm_sub_heartflow.pri_in": "子心流模型输入价格",
|
||
"model.llm_sub_heartflow.pri_out": "子心流模型输出价格",
|
||
"model.llm_heartflow.name": "心流模型名称",
|
||
"model.llm_heartflow.provider": "心流模型提供商",
|
||
"model.llm_heartflow.pri_in": "心流模型输入价格",
|
||
"model.llm_heartflow.pri_out": "心流模型输出价格",
|
||
}
|
||
|
||
|
||
# 获取翻译
|
||
def get_translation(key):
|
||
return SECTION_TRANSLATIONS.get(key, key)
|
||
|
||
|
||
# 获取配置项描述
|
||
def get_description(key):
|
||
return CONFIG_DESCRIPTIONS.get(key, "")
|
||
|
||
|
||
# 获取根目录路径
|
||
def get_root_dir():
|
||
try:
|
||
# 获取当前脚本所在目录
|
||
if getattr(sys, "frozen", False):
|
||
# 如果是打包后的应用
|
||
current_dir = os.path.dirname(sys.executable)
|
||
else:
|
||
# 如果是脚本运行
|
||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||
|
||
# 获取根目录(假设当前脚本在temp_utils_ui目录下或者是可执行文件在根目录)
|
||
if os.path.basename(current_dir) == "temp_utils_ui":
|
||
root_dir = os.path.dirname(current_dir)
|
||
else:
|
||
root_dir = current_dir
|
||
|
||
# 检查是否存在config目录
|
||
config_dir = os.path.join(root_dir, "config")
|
||
if not os.path.exists(config_dir):
|
||
os.makedirs(config_dir, exist_ok=True)
|
||
|
||
return root_dir
|
||
except Exception as e:
|
||
print(f"获取根目录路径失败: {e}")
|
||
# 返回当前目录作为备选
|
||
return os.getcwd()
|
||
|
||
|
||
# 配置文件路径
|
||
CONFIG_PATH = os.path.join(get_root_dir(), "config", "bot_config.toml")
|
||
|
||
|
||
# 保存配置
|
||
def save_config(config_data):
|
||
try:
|
||
# 首先备份原始配置文件
|
||
if os.path.exists(CONFIG_PATH):
|
||
# 创建备份目录
|
||
backup_dir = os.path.join(os.path.dirname(CONFIG_PATH), "old")
|
||
if not os.path.exists(backup_dir):
|
||
os.makedirs(backup_dir)
|
||
|
||
# 生成备份文件名(使用时间戳)
|
||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
backup_filename = f"bot_config_{timestamp}.toml.bak"
|
||
backup_path = os.path.join(backup_dir, backup_filename)
|
||
|
||
# 复制文件
|
||
with open(CONFIG_PATH, "r", encoding="utf-8") as src:
|
||
with open(backup_path, "w", encoding="utf-8") as dst:
|
||
dst.write(src.read())
|
||
|
||
# 保存新配置
|
||
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
|
||
toml.dump(config_data, f)
|
||
return True
|
||
except Exception as e:
|
||
print(f"保存配置失败: {e}")
|
||
return False
|
||
|
||
|
||
# 加载配置
|
||
def load_config():
|
||
try:
|
||
if os.path.exists(CONFIG_PATH):
|
||
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
||
return toml.load(f)
|
||
else:
|
||
print(f"配置文件不存在: {CONFIG_PATH}")
|
||
return {}
|
||
except Exception as e:
|
||
print(f"加载配置失败: {e}")
|
||
return {}
|
||
|
||
|
||
# 多行文本输入框
|
||
class ScrollableTextFrame(ctk.CTkFrame):
|
||
def __init__(self, master, initial_text="", height=100, width=400, **kwargs):
|
||
super().__init__(master, **kwargs)
|
||
|
||
self.text_var = StringVar(value=initial_text)
|
||
|
||
# 文本框
|
||
self.text_box = ctk.CTkTextbox(self, height=height, width=width, wrap="word")
|
||
self.text_box.pack(fill="both", expand=True, padx=5, pady=5)
|
||
self.text_box.insert("1.0", initial_text)
|
||
|
||
# 绑定更改事件
|
||
self.text_box.bind("<KeyRelease>", self.update_var)
|
||
|
||
def update_var(self, event=None):
|
||
self.text_var.set(self.text_box.get("1.0", "end-1c"))
|
||
|
||
def get(self):
|
||
return self.text_box.get("1.0", "end-1c")
|
||
|
||
def set(self, text):
|
||
self.text_box.delete("1.0", "end")
|
||
self.text_box.insert("1.0", text)
|
||
self.update_var()
|
||
|
||
|
||
# 配置UI
|
||
class ConfigUI(ctk.CTk):
|
||
def __init__(self):
|
||
super().__init__()
|
||
|
||
# 窗口设置
|
||
self.title("麦麦配置修改器")
|
||
self.geometry("1100x750")
|
||
|
||
# 加载配置
|
||
self.config_data = load_config()
|
||
if not self.config_data:
|
||
messagebox.showerror("错误", "无法加载配置文件!将创建空白配置文件。")
|
||
# 如果配置加载失败,创建一个最小化的空配置
|
||
self.config_data = {"inner": {"version": "1.0.0"}}
|
||
|
||
# 保存原始配置,用于检测变更
|
||
self.original_config = json.dumps(self.config_data, sort_keys=True)
|
||
|
||
# 自动保存状态
|
||
self.auto_save = ctk.BooleanVar(value=False)
|
||
|
||
# 创建主框架
|
||
self.main_frame = ctk.CTkFrame(self)
|
||
self.main_frame.pack(padx=10, pady=10, fill="both", expand=True)
|
||
|
||
# 创建顶部工具栏
|
||
self.create_toolbar()
|
||
|
||
# 创建标签和输入框的字典,用于后续保存配置
|
||
self.config_vars = {}
|
||
|
||
# 创建左侧导航和右侧内容区域
|
||
self.create_split_view()
|
||
|
||
# 创建底部状态栏
|
||
self.status_label = ctk.CTkLabel(self, text="就绪", anchor="w")
|
||
self.status_label.pack(fill="x", padx=10, pady=(0, 5))
|
||
|
||
# 绑定关闭事件
|
||
self.protocol("WM_DELETE_WINDOW", self.on_closing)
|
||
|
||
# 设置最小窗口大小
|
||
self.minsize(800, 600)
|
||
|
||
# 居中显示窗口
|
||
self.center_window()
|
||
|
||
def center_window(self):
|
||
"""将窗口居中显示"""
|
||
try:
|
||
self.update_idletasks()
|
||
width = self.winfo_width()
|
||
height = self.winfo_height()
|
||
x = (self.winfo_screenwidth() // 2) - (width // 2)
|
||
y = (self.winfo_screenheight() // 2) - (height // 2)
|
||
self.geometry(f"{width}x{height}+{x}+{y}")
|
||
except Exception as e:
|
||
print(f"居中窗口时出错: {e}")
|
||
# 使用默认位置
|
||
pass
|
||
|
||
def create_toolbar(self):
|
||
toolbar = ctk.CTkFrame(self.main_frame, height=40)
|
||
toolbar.pack(fill="x", padx=5, pady=5)
|
||
|
||
# 保存按钮
|
||
save_btn = ctk.CTkButton(toolbar, text="保存配置", command=self.save_config, width=100)
|
||
save_btn.pack(side="left", padx=5)
|
||
|
||
# 自动保存选项
|
||
auto_save_cb = ctk.CTkCheckBox(toolbar, text="自动保存", variable=self.auto_save)
|
||
auto_save_cb.pack(side="left", padx=15)
|
||
|
||
# 重新加载按钮
|
||
reload_btn = ctk.CTkButton(toolbar, text="重新加载", command=self.reload_config, width=100)
|
||
reload_btn.pack(side="left", padx=5)
|
||
|
||
# 手动备份按钮
|
||
backup_btn = ctk.CTkButton(toolbar, text="手动备份", command=self.backup_config, width=100)
|
||
backup_btn.pack(side="left", padx=5)
|
||
|
||
# 查看备份按钮
|
||
view_backup_btn = ctk.CTkButton(toolbar, text="查看备份", command=self.view_backups, width=100)
|
||
view_backup_btn.pack(side="left", padx=5)
|
||
|
||
# 导入导出菜单按钮
|
||
import_export_btn = ctk.CTkButton(toolbar, text="导入/导出", command=self.show_import_export_menu, width=100)
|
||
import_export_btn.pack(side="left", padx=5)
|
||
|
||
# 关于按钮
|
||
about_btn = ctk.CTkButton(toolbar, text="关于", command=self.show_about, width=80)
|
||
about_btn.pack(side="right", padx=5)
|
||
|
||
def create_split_view(self):
|
||
# 创建分隔视图框架
|
||
split_frame = ctk.CTkFrame(self.main_frame)
|
||
split_frame.pack(fill="both", expand=True, padx=5, pady=5)
|
||
|
||
# 左侧分类列表
|
||
self.category_frame = ctk.CTkFrame(split_frame, width=220)
|
||
self.category_frame.pack(side="left", fill="y", padx=(0, 5), pady=0)
|
||
self.category_frame.pack_propagate(False) # 固定宽度
|
||
|
||
# 右侧内容区域
|
||
self.content_frame = ctk.CTkScrollableFrame(split_frame)
|
||
self.content_frame.pack(side="right", fill="both", expand=True)
|
||
|
||
# 创建类别列表
|
||
self.create_category_list()
|
||
|
||
def create_category_list(self):
|
||
# 标题和搜索框
|
||
header_frame = ctk.CTkFrame(self.category_frame)
|
||
header_frame.pack(fill="x", padx=5, pady=(10, 5))
|
||
|
||
ctk.CTkLabel(header_frame, text="配置分类", font=("Arial", 14, "bold")).pack(side="left", padx=5, pady=5)
|
||
|
||
# 搜索按钮
|
||
search_btn = ctk.CTkButton(
|
||
header_frame,
|
||
text="🔍",
|
||
width=30,
|
||
command=self.show_search_dialog,
|
||
fg_color="transparent",
|
||
hover_color=("gray80", "gray30"),
|
||
)
|
||
search_btn.pack(side="right", padx=5, pady=5)
|
||
|
||
# 分类按钮
|
||
self.category_buttons = {}
|
||
self.active_category = None
|
||
|
||
# 分类按钮容器
|
||
buttons_frame = ctk.CTkScrollableFrame(self.category_frame, height=600)
|
||
buttons_frame.pack(fill="both", expand=True, padx=5, pady=5)
|
||
|
||
for section in self.config_data:
|
||
# 跳过inner部分,这个不应该被用户修改
|
||
if section == "inner":
|
||
continue
|
||
|
||
# 获取翻译
|
||
section_name = f"{section} ({get_translation(section)})"
|
||
|
||
btn = ctk.CTkButton(
|
||
buttons_frame,
|
||
text=section_name,
|
||
fg_color="transparent",
|
||
text_color=("gray10", "gray90"),
|
||
anchor="w",
|
||
height=35,
|
||
command=lambda s=section: self.show_category(s),
|
||
)
|
||
btn.pack(fill="x", padx=5, pady=2)
|
||
self.category_buttons[section] = btn
|
||
|
||
# 默认显示第一个分类
|
||
first_section = next((s for s in self.config_data.keys() if s != "inner"), None)
|
||
if first_section:
|
||
self.show_category(first_section)
|
||
|
||
def show_category(self, category):
|
||
# 清除当前内容
|
||
for widget in self.content_frame.winfo_children():
|
||
widget.destroy()
|
||
|
||
# 更新按钮状态
|
||
for section, btn in self.category_buttons.items():
|
||
if section == category:
|
||
btn.configure(fg_color=("gray75", "gray25"))
|
||
self.active_category = section
|
||
else:
|
||
btn.configure(fg_color="transparent")
|
||
|
||
# 获取翻译
|
||
category_name = f"{category} ({get_translation(category)})"
|
||
|
||
# 添加标题
|
||
ctk.CTkLabel(self.content_frame, text=f"{category_name} 配置", font=("Arial", 16, "bold")).pack(
|
||
anchor="w", padx=10, pady=(5, 15)
|
||
)
|
||
|
||
# 添加配置项
|
||
self.add_config_section(self.content_frame, category, self.config_data[category])
|
||
|
||
def add_config_section(self, parent, section_path, section_data, indent=0):
|
||
# 递归添加配置项
|
||
for key, value in section_data.items():
|
||
full_path = f"{section_path}.{key}" if indent > 0 else f"{section_path}.{key}"
|
||
|
||
# 获取描述
|
||
description = get_description(full_path)
|
||
|
||
if isinstance(value, dict):
|
||
# 如果是字典,创建一个分组框架并递归添加子项
|
||
group_frame = ctk.CTkFrame(parent)
|
||
group_frame.pack(fill="x", expand=True, padx=10, pady=10)
|
||
|
||
# 添加标题
|
||
header_frame = ctk.CTkFrame(group_frame, fg_color=("gray85", "gray25"))
|
||
header_frame.pack(fill="x", padx=0, pady=0)
|
||
|
||
label = ctk.CTkLabel(header_frame, text=f"{key}", font=("Arial", 13, "bold"), anchor="w")
|
||
label.pack(anchor="w", padx=10, pady=5)
|
||
|
||
# 如果有描述,添加提示图标
|
||
if description:
|
||
# 创建工具提示窗口显示函数
|
||
def show_tooltip(event, text, widget):
|
||
x, y, _, _ = widget.bbox("all")
|
||
x += widget.winfo_rootx() + 25
|
||
y += widget.winfo_rooty() + 25
|
||
|
||
# 创建工具提示窗口
|
||
tipwindow = ctk.CTkToplevel(widget)
|
||
tipwindow.wm_overrideredirect(True)
|
||
tipwindow.wm_geometry(f"+{x}+{y}")
|
||
tipwindow.lift()
|
||
|
||
label = ctk.CTkLabel(tipwindow, text=text, justify="left", wraplength=300)
|
||
label.pack(padx=5, pady=5)
|
||
|
||
# 自动关闭
|
||
def close_tooltip():
|
||
tipwindow.destroy()
|
||
|
||
widget.after(3000, close_tooltip)
|
||
return tipwindow
|
||
|
||
# 在标题后添加提示图标
|
||
tip_label = ctk.CTkLabel(
|
||
header_frame, text="ℹ️", font=("Arial", 12), text_color="light blue", width=20
|
||
)
|
||
tip_label.pack(side="right", padx=5)
|
||
|
||
# 绑定鼠标悬停事件
|
||
tip_label.bind("<Enter>", lambda e, t=description, w=tip_label: show_tooltip(e, t, w))
|
||
|
||
# 添加内容
|
||
content_frame = ctk.CTkFrame(group_frame)
|
||
content_frame.pack(fill="x", expand=True, padx=5, pady=5)
|
||
|
||
self.add_config_section(content_frame, full_path, value, indent + 1)
|
||
|
||
elif isinstance(value, list):
|
||
# 如果是列表,创建一个文本框用于编辑JSON格式的列表
|
||
frame = ctk.CTkFrame(parent)
|
||
frame.pack(fill="x", expand=True, padx=5, pady=5)
|
||
|
||
# 标签和输入框在一行
|
||
label_frame = ctk.CTkFrame(frame)
|
||
label_frame.pack(fill="x", padx=5, pady=(5, 0))
|
||
|
||
# 标签包含描述提示
|
||
label_text = f"{key}:"
|
||
if description:
|
||
label_text = f"{key}: ({description})"
|
||
|
||
label = ctk.CTkLabel(label_frame, text=label_text, font=("Arial", 12), anchor="w")
|
||
label.pack(anchor="w", padx=5 + indent * 10, pady=0)
|
||
|
||
# 添加提示信息
|
||
info_label = ctk.CTkLabel(label_frame, text="(列表格式: JSON)", font=("Arial", 9), text_color="gray50")
|
||
info_label.pack(anchor="w", padx=5 + indent * 10, pady=(0, 5))
|
||
|
||
# 确定文本框高度,根据列表项数量决定
|
||
list_height = max(100, min(len(value) * 20 + 40, 200))
|
||
|
||
# 将列表转换为JSON字符串,美化格式
|
||
json_str = json.dumps(value, ensure_ascii=False, indent=2)
|
||
|
||
# 使用多行文本框
|
||
text_frame = ScrollableTextFrame(frame, initial_text=json_str, height=list_height, width=550)
|
||
text_frame.pack(fill="x", padx=10 + indent * 10, pady=5)
|
||
|
||
self.config_vars[full_path] = (text_frame.text_var, "list")
|
||
|
||
# 绑定变更事件,用于自动保存
|
||
text_frame.text_box.bind("<FocusOut>", lambda e, path=full_path: self.on_field_change(path))
|
||
|
||
elif isinstance(value, bool):
|
||
# 如果是布尔值,创建一个复选框
|
||
frame = ctk.CTkFrame(parent)
|
||
frame.pack(fill="x", expand=True, padx=5, pady=5)
|
||
|
||
var = ctk.BooleanVar(value=value)
|
||
self.config_vars[full_path] = (var, "bool")
|
||
|
||
# 复选框文本包含描述
|
||
checkbox_text = key
|
||
if description:
|
||
checkbox_text = f"{key} ({description})"
|
||
|
||
checkbox = ctk.CTkCheckBox(
|
||
frame, text=checkbox_text, variable=var, command=lambda path=full_path: self.on_field_change(path)
|
||
)
|
||
checkbox.pack(anchor="w", padx=10 + indent * 10, pady=5)
|
||
|
||
elif isinstance(value, (int, float)):
|
||
# 如果是数字,创建一个数字输入框
|
||
frame = ctk.CTkFrame(parent)
|
||
frame.pack(fill="x", expand=True, padx=5, pady=5)
|
||
|
||
# 标签包含描述
|
||
label_text = f"{key}:"
|
||
if description:
|
||
label_text = f"{key}: ({description})"
|
||
|
||
label = ctk.CTkLabel(frame, text=label_text, font=("Arial", 12), anchor="w")
|
||
label.pack(anchor="w", padx=10 + indent * 10, pady=(5, 0))
|
||
|
||
var = StringVar(value=str(value))
|
||
self.config_vars[full_path] = (var, "number", type(value))
|
||
|
||
# 判断数值的长度,决定输入框宽度
|
||
entry_width = max(200, min(len(str(value)) * 15, 300))
|
||
|
||
entry = ctk.CTkEntry(frame, width=entry_width, textvariable=var)
|
||
entry.pack(anchor="w", padx=10 + indent * 10, pady=5)
|
||
|
||
# 绑定变更事件,用于自动保存
|
||
entry.bind("<FocusOut>", lambda e, path=full_path: self.on_field_change(path))
|
||
|
||
else:
|
||
# 对于字符串,创建一个文本输入框
|
||
frame = ctk.CTkFrame(parent)
|
||
frame.pack(fill="x", expand=True, padx=5, pady=5)
|
||
|
||
# 标签包含描述
|
||
label_text = f"{key}:"
|
||
if description:
|
||
label_text = f"{key}: ({description})"
|
||
|
||
label = ctk.CTkLabel(frame, text=label_text, font=("Arial", 12), anchor="w")
|
||
label.pack(anchor="w", padx=10 + indent * 10, pady=(5, 0))
|
||
|
||
var = StringVar(value=str(value))
|
||
self.config_vars[full_path] = (var, "string")
|
||
|
||
# 判断文本长度,决定输入框的类型和大小
|
||
text_len = len(str(value))
|
||
|
||
if text_len > 80 or "\n" in str(value):
|
||
# 对于长文本或多行文本,使用多行文本框
|
||
text_height = max(80, min(str(value).count("\n") * 20 + 40, 150))
|
||
|
||
text_frame = ScrollableTextFrame(frame, initial_text=str(value), height=text_height, width=550)
|
||
text_frame.pack(fill="x", padx=10 + indent * 10, pady=5)
|
||
self.config_vars[full_path] = (text_frame.text_var, "string")
|
||
|
||
# 绑定变更事件,用于自动保存
|
||
text_frame.text_box.bind("<FocusOut>", lambda e, path=full_path: self.on_field_change(path))
|
||
else:
|
||
# 对于短文本,使用单行输入框
|
||
# 根据内容长度动态调整输入框宽度
|
||
entry_width = max(400, min(text_len * 10, 550))
|
||
|
||
entry = ctk.CTkEntry(frame, width=entry_width, textvariable=var)
|
||
entry.pack(anchor="w", padx=10 + indent * 10, pady=5, fill="x")
|
||
|
||
# 绑定变更事件,用于自动保存
|
||
entry.bind("<FocusOut>", lambda e, path=full_path: self.on_field_change(path))
|
||
|
||
def on_field_change(self, path):
|
||
"""当字段值改变时调用,用于自动保存"""
|
||
if self.auto_save.get():
|
||
self.save_config(show_message=False)
|
||
self.status_label.configure(text=f"已自动保存更改 ({path})")
|
||
|
||
def save_config(self, show_message=True):
|
||
"""保存配置文件"""
|
||
# 更新配置数据
|
||
updated = False
|
||
_error_path = None
|
||
|
||
for path, (var, var_type, *args) in self.config_vars.items():
|
||
parts = path.split(".")
|
||
|
||
# 如果路径有多层级
|
||
target = self.config_data
|
||
for p in parts[:-1]:
|
||
if p not in target:
|
||
target[p] = {}
|
||
target = target[p]
|
||
|
||
# 根据变量类型更新值
|
||
try:
|
||
if var_type == "bool":
|
||
if target[parts[-1]] != var.get():
|
||
target[parts[-1]] = var.get()
|
||
updated = True
|
||
elif var_type == "number":
|
||
# 获取原始类型(int或float)
|
||
num_type = args[0] if args else int
|
||
new_value = num_type(var.get())
|
||
if target[parts[-1]] != new_value:
|
||
target[parts[-1]] = new_value
|
||
updated = True
|
||
|
||
elif var_type == "list":
|
||
# 解析JSON字符串为列表
|
||
new_value = json.loads(var.get())
|
||
if json.dumps(target[parts[-1]], sort_keys=True) != json.dumps(new_value, sort_keys=True):
|
||
target[parts[-1]] = new_value
|
||
updated = True
|
||
|
||
else:
|
||
if target[parts[-1]] != var.get():
|
||
target[parts[-1]] = var.get()
|
||
updated = True
|
||
except ValueError as e:
|
||
if show_message:
|
||
messagebox.showerror("格式错误", str(e))
|
||
else:
|
||
self.status_label.configure(text=f"保存失败: {e}")
|
||
return False
|
||
|
||
if not updated and show_message:
|
||
self.status_label.configure(text="无更改,无需保存")
|
||
return True
|
||
|
||
# 保存配置
|
||
if save_config(self.config_data):
|
||
if show_message:
|
||
messagebox.showinfo("成功", "配置已保存!")
|
||
self.original_config = json.dumps(self.config_data, sort_keys=True)
|
||
return True
|
||
else:
|
||
if show_message:
|
||
messagebox.showerror("错误", "保存配置失败!")
|
||
else:
|
||
self.status_label.configure(text="保存失败!")
|
||
return False
|
||
|
||
def reload_config(self):
|
||
"""重新加载配置"""
|
||
if self.check_unsaved_changes():
|
||
self.config_data = load_config()
|
||
if not self.config_data:
|
||
messagebox.showerror("错误", "无法加载配置文件!")
|
||
return
|
||
|
||
# 保存原始配置,用于检测变更
|
||
self.original_config = json.dumps(self.config_data, sort_keys=True)
|
||
|
||
# 重新显示当前分类
|
||
self.show_category(self.active_category)
|
||
|
||
self.status_label.configure(text="配置已重新加载")
|
||
|
||
def check_unsaved_changes(self):
|
||
"""检查是否有未保存的更改"""
|
||
# 临时更新配置数据以进行比较
|
||
temp_config = self.config_data.copy()
|
||
|
||
try:
|
||
for path, (var, var_type, *args) in self.config_vars.items():
|
||
parts = path.split(".")
|
||
|
||
target = temp_config
|
||
for p in parts[:-1]:
|
||
target = target[p]
|
||
|
||
if var_type == "bool":
|
||
target[parts[-1]] = var.get()
|
||
elif var_type == "number":
|
||
num_type = args[0] if args else int
|
||
target[parts[-1]] = num_type(var.get())
|
||
elif var_type == "list":
|
||
target[parts[-1]] = json.loads(var.get())
|
||
else:
|
||
target[parts[-1]] = var.get()
|
||
except (ValueError, json.JSONDecodeError):
|
||
# 如果有无效输入,认为有未保存更改
|
||
return False
|
||
|
||
# 比较原始配置和当前配置
|
||
current_config = json.dumps(temp_config, sort_keys=True)
|
||
|
||
if current_config != self.original_config:
|
||
result = messagebox.askyesnocancel("未保存的更改", "有未保存的更改,是否保存?", icon="warning")
|
||
|
||
if result is None: # 取消
|
||
return False
|
||
elif result: # 是
|
||
return self.save_config()
|
||
|
||
return True
|
||
|
||
def show_about(self):
|
||
"""显示关于对话框"""
|
||
about_window = ctk.CTkToplevel(self)
|
||
about_window.title("关于")
|
||
about_window.geometry("400x200")
|
||
about_window.resizable(False, False)
|
||
about_window.grab_set() # 模态对话框
|
||
|
||
# 居中
|
||
x = self.winfo_x() + (self.winfo_width() - 400) // 2
|
||
y = self.winfo_y() + (self.winfo_height() - 200) // 2
|
||
about_window.geometry(f"+{x}+{y}")
|
||
|
||
# 内容
|
||
ctk.CTkLabel(about_window, text="麦麦配置修改器", font=("Arial", 16, "bold")).pack(pady=(20, 10))
|
||
|
||
ctk.CTkLabel(about_window, text="用于修改MaiBot-Core的配置文件\n配置文件路径: config/bot_config.toml").pack(
|
||
pady=5
|
||
)
|
||
|
||
ctk.CTkLabel(about_window, text="注意: 修改配置前请备份原始配置文件", text_color=("red", "light coral")).pack(
|
||
pady=5
|
||
)
|
||
|
||
ctk.CTkButton(about_window, text="确定", command=about_window.destroy, width=100).pack(pady=15)
|
||
|
||
def on_closing(self):
|
||
"""关闭窗口前检查未保存更改"""
|
||
if self.check_unsaved_changes():
|
||
self.destroy()
|
||
|
||
def backup_config(self):
|
||
"""手动备份当前配置文件"""
|
||
try:
|
||
# 检查配置文件是否存在
|
||
if not os.path.exists(CONFIG_PATH):
|
||
messagebox.showerror("错误", "配置文件不存在!")
|
||
return False
|
||
|
||
# 创建备份目录
|
||
backup_dir = os.path.join(os.path.dirname(CONFIG_PATH), "old")
|
||
if not os.path.exists(backup_dir):
|
||
os.makedirs(backup_dir)
|
||
|
||
# 生成备份文件名(使用时间戳)
|
||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
backup_filename = f"bot_config_{timestamp}.toml.bak"
|
||
backup_path = os.path.join(backup_dir, backup_filename)
|
||
|
||
# 复制文件
|
||
with open(CONFIG_PATH, "r", encoding="utf-8") as src:
|
||
with open(backup_path, "w", encoding="utf-8") as dst:
|
||
dst.write(src.read())
|
||
|
||
messagebox.showinfo("成功", f"配置已备份到:\n{backup_path}")
|
||
self.status_label.configure(text=f"手动备份已创建: {backup_filename}")
|
||
return True
|
||
except Exception as e:
|
||
messagebox.showerror("备份失败", f"备份配置文件失败: {e}")
|
||
return False
|
||
|
||
def view_backups(self):
|
||
"""查看备份文件列表"""
|
||
# 创建备份目录
|
||
backup_dir = os.path.join(os.path.dirname(CONFIG_PATH), "old")
|
||
if not os.path.exists(backup_dir):
|
||
os.makedirs(backup_dir)
|
||
|
||
# 查找备份文件
|
||
backup_files = []
|
||
for filename in os.listdir(backup_dir):
|
||
if filename.startswith("bot_config_") and filename.endswith(".toml.bak"):
|
||
backup_path = os.path.join(backup_dir, filename)
|
||
mod_time = os.path.getmtime(backup_path)
|
||
backup_files.append((filename, backup_path, mod_time))
|
||
|
||
if not backup_files:
|
||
messagebox.showinfo("提示", "未找到备份文件")
|
||
return
|
||
|
||
# 按修改时间排序,最新的在前
|
||
backup_files.sort(key=lambda x: x[2], reverse=True)
|
||
|
||
# 创建备份查看窗口
|
||
backup_window = ctk.CTkToplevel(self)
|
||
backup_window.title("备份文件")
|
||
backup_window.geometry("600x400")
|
||
backup_window.grab_set() # 模态对话框
|
||
|
||
# 居中
|
||
x = self.winfo_x() + (self.winfo_width() - 600) // 2
|
||
y = self.winfo_y() + (self.winfo_height() - 400) // 2
|
||
backup_window.geometry(f"+{x}+{y}")
|
||
|
||
# 创建说明标签
|
||
ctk.CTkLabel(backup_window, text="备份文件列表 (双击可恢复)", font=("Arial", 14, "bold")).pack(
|
||
pady=(10, 5), padx=10, anchor="w"
|
||
)
|
||
|
||
# 创建列表框
|
||
backup_frame = ctk.CTkScrollableFrame(backup_window, width=580, height=300)
|
||
backup_frame.pack(padx=10, pady=10, fill="both", expand=True)
|
||
|
||
# 添加备份文件项
|
||
for _i, (filename, filepath, mod_time) in enumerate(backup_files):
|
||
# 格式化时间为可读格式
|
||
time_str = datetime.datetime.fromtimestamp(mod_time).strftime("%Y-%m-%d %H:%M:%S")
|
||
|
||
# 创建一个框架用于每个备份项
|
||
item_frame = ctk.CTkFrame(backup_frame)
|
||
item_frame.pack(fill="x", padx=5, pady=5)
|
||
|
||
# 显示备份文件信息
|
||
ctk.CTkLabel(item_frame, text=f"{time_str}", font=("Arial", 12, "bold"), width=200).pack(
|
||
side="left", padx=10, pady=10
|
||
)
|
||
|
||
# 文件名
|
||
name_label = ctk.CTkLabel(item_frame, text=filename, font=("Arial", 11))
|
||
name_label.pack(side="left", fill="x", expand=True, padx=5, pady=10)
|
||
|
||
# 恢复按钮
|
||
restore_btn = ctk.CTkButton(
|
||
item_frame, text="恢复", width=80, command=lambda path=filepath: self.restore_backup(path)
|
||
)
|
||
restore_btn.pack(side="right", padx=10, pady=10)
|
||
|
||
# 绑定双击事件
|
||
for widget in (item_frame, name_label):
|
||
widget.bind("<Double-1>", lambda e, path=filepath: self.restore_backup(path))
|
||
|
||
# 关闭按钮
|
||
ctk.CTkButton(backup_window, text="关闭", command=backup_window.destroy, width=100).pack(pady=10)
|
||
|
||
def restore_backup(self, backup_path):
|
||
"""从备份文件恢复配置"""
|
||
if not os.path.exists(backup_path):
|
||
messagebox.showerror("错误", "备份文件不存在!")
|
||
return False
|
||
|
||
# 确认还原
|
||
confirm = messagebox.askyesno(
|
||
"确认",
|
||
f"确定要从以下备份文件恢复配置吗?\n{os.path.basename(backup_path)}\n\n这将覆盖当前的配置!",
|
||
icon="warning",
|
||
)
|
||
|
||
if not confirm:
|
||
return False
|
||
|
||
try:
|
||
# 先备份当前配置
|
||
self.backup_config()
|
||
|
||
# 恢复配置
|
||
with open(backup_path, "r", encoding="utf-8") as src:
|
||
with open(CONFIG_PATH, "w", encoding="utf-8") as dst:
|
||
dst.write(src.read())
|
||
|
||
messagebox.showinfo("成功", "配置已从备份恢复!")
|
||
|
||
# 重新加载配置
|
||
self.reload_config()
|
||
return True
|
||
except Exception as e:
|
||
messagebox.showerror("恢复失败", f"恢复配置失败: {e}")
|
||
return False
|
||
|
||
def show_search_dialog(self):
|
||
"""显示搜索对话框"""
|
||
try:
|
||
search_window = ctk.CTkToplevel(self)
|
||
search_window.title("搜索配置项")
|
||
search_window.geometry("500x400")
|
||
search_window.grab_set() # 模态对话框
|
||
|
||
# 居中
|
||
x = self.winfo_x() + (self.winfo_width() - 500) // 2
|
||
y = self.winfo_y() + (self.winfo_height() - 400) // 2
|
||
search_window.geometry(f"+{x}+{y}")
|
||
|
||
# 搜索框
|
||
search_frame = ctk.CTkFrame(search_window)
|
||
search_frame.pack(fill="x", padx=10, pady=10)
|
||
|
||
search_var = StringVar()
|
||
search_entry = ctk.CTkEntry(
|
||
search_frame, placeholder_text="输入关键词搜索...", width=380, textvariable=search_var
|
||
)
|
||
search_entry.pack(side="left", padx=5, pady=5, fill="x", expand=True)
|
||
|
||
# 结果列表框
|
||
results_frame = ctk.CTkScrollableFrame(search_window, width=480, height=300)
|
||
results_frame.pack(padx=10, pady=5, fill="both", expand=True)
|
||
|
||
# 搜索结果标签
|
||
results_label = ctk.CTkLabel(results_frame, text="请输入关键词进行搜索", anchor="w")
|
||
results_label.pack(fill="x", padx=10, pady=10)
|
||
|
||
# 结果项列表
|
||
results_items = []
|
||
|
||
# 搜索函数
|
||
def perform_search():
|
||
# 清除之前的结果
|
||
for item in results_items:
|
||
item.destroy()
|
||
results_items.clear()
|
||
|
||
keyword = search_var.get().lower()
|
||
if not keyword:
|
||
results_label.configure(text="请输入关键词进行搜索")
|
||
return
|
||
|
||
# 收集所有匹配的配置项
|
||
matches = []
|
||
|
||
def search_config(section_path, config_data):
|
||
for key, value in config_data.items():
|
||
full_path = f"{section_path}.{key}" if section_path else key
|
||
|
||
# 检查键名是否匹配
|
||
if keyword in key.lower():
|
||
matches.append((full_path, value))
|
||
|
||
# 检查描述是否匹配
|
||
description = get_description(full_path)
|
||
if description and keyword in description.lower():
|
||
matches.append((full_path, value))
|
||
|
||
# 检查值是否匹配(仅字符串类型)
|
||
if isinstance(value, str) and keyword in value.lower():
|
||
matches.append((full_path, value))
|
||
|
||
# 递归搜索子项
|
||
if isinstance(value, dict):
|
||
search_config(full_path, value)
|
||
|
||
# 开始搜索
|
||
search_config("", self.config_data)
|
||
|
||
if not matches:
|
||
results_label.configure(text=f"未找到包含 '{keyword}' 的配置项")
|
||
return
|
||
|
||
results_label.configure(text=f"找到 {len(matches)} 个匹配项")
|
||
|
||
# 显示搜索结果
|
||
for full_path, value in matches:
|
||
# 创建一个框架用于每个结果项
|
||
item_frame = ctk.CTkFrame(results_frame)
|
||
item_frame.pack(fill="x", padx=5, pady=3)
|
||
results_items.append(item_frame)
|
||
|
||
# 配置项路径
|
||
path_parts = full_path.split(".")
|
||
section = path_parts[0] if len(path_parts) > 0 else ""
|
||
_key = path_parts[-1] if len(path_parts) > 0 else ""
|
||
|
||
# 获取描述
|
||
description = get_description(full_path)
|
||
desc_text = f" ({description})" if description else ""
|
||
|
||
# 显示完整路径
|
||
path_label = ctk.CTkLabel(
|
||
item_frame,
|
||
text=f"{full_path}{desc_text}",
|
||
font=("Arial", 11, "bold"),
|
||
anchor="w",
|
||
wraplength=450,
|
||
)
|
||
path_label.pack(anchor="w", padx=10, pady=(5, 0), fill="x")
|
||
|
||
# 显示值的预览(截断过长的值)
|
||
value_str = str(value)
|
||
if len(value_str) > 50:
|
||
value_str = value_str[:50] + "..."
|
||
|
||
value_label = ctk.CTkLabel(
|
||
item_frame, text=f"值: {value_str}", font=("Arial", 10), anchor="w", wraplength=450
|
||
)
|
||
value_label.pack(anchor="w", padx=10, pady=(0, 5), fill="x")
|
||
|
||
# 添加"转到"按钮
|
||
goto_btn = ctk.CTkButton(
|
||
item_frame,
|
||
text="转到",
|
||
width=60,
|
||
height=25,
|
||
command=lambda s=section: self.goto_config_item(s, search_window),
|
||
)
|
||
goto_btn.pack(side="right", padx=10, pady=5)
|
||
|
||
# 绑定双击事件
|
||
for widget in (item_frame, path_label, value_label):
|
||
widget.bind("<Double-1>", lambda e, s=section: self.goto_config_item(s, search_window))
|
||
|
||
# 搜索按钮
|
||
search_button = ctk.CTkButton(search_frame, text="搜索", width=80, command=perform_search)
|
||
search_button.pack(side="right", padx=5, pady=5)
|
||
|
||
# 绑定回车键
|
||
search_entry.bind("<Return>", lambda e: perform_search())
|
||
|
||
# 初始聚焦到搜索框
|
||
search_window.after(100, lambda: self.safe_focus(search_entry))
|
||
except Exception as e:
|
||
print(f"显示搜索对话框出错: {e}")
|
||
messagebox.showerror("错误", f"显示搜索对话框失败: {e}")
|
||
|
||
def safe_focus(self, widget):
|
||
"""安全地设置焦点,避免应用崩溃"""
|
||
try:
|
||
if widget.winfo_exists():
|
||
widget.focus_set()
|
||
except Exception as e:
|
||
print(f"设置焦点出错: {e}")
|
||
# 忽略错误
|
||
|
||
def goto_config_item(self, section, dialog=None):
|
||
"""跳转到指定的配置项"""
|
||
if dialog:
|
||
dialog.destroy()
|
||
|
||
# 切换到相应的分类
|
||
if section in self.category_buttons:
|
||
self.show_category(section)
|
||
|
||
def show_import_export_menu(self):
|
||
"""显示导入导出菜单"""
|
||
menu_window = ctk.CTkToplevel(self)
|
||
menu_window.title("导入/导出配置")
|
||
menu_window.geometry("300x200")
|
||
menu_window.resizable(False, False)
|
||
menu_window.grab_set() # 模态对话框
|
||
|
||
# 居中
|
||
x = self.winfo_x() + (self.winfo_width() - 300) // 2
|
||
y = self.winfo_y() + (self.winfo_height() - 200) // 2
|
||
menu_window.geometry(f"+{x}+{y}")
|
||
|
||
# 创建按钮
|
||
ctk.CTkLabel(menu_window, text="配置导入导出", font=("Arial", 16, "bold")).pack(pady=(20, 10))
|
||
|
||
# 导出按钮
|
||
export_btn = ctk.CTkButton(
|
||
menu_window, text="导出配置到文件", command=lambda: self.export_config(menu_window), width=200
|
||
)
|
||
export_btn.pack(pady=10)
|
||
|
||
# 导入按钮
|
||
import_btn = ctk.CTkButton(
|
||
menu_window, text="从文件导入配置", command=lambda: self.import_config(menu_window), width=200
|
||
)
|
||
import_btn.pack(pady=10)
|
||
|
||
# 取消按钮
|
||
cancel_btn = ctk.CTkButton(menu_window, text="取消", command=menu_window.destroy, width=100)
|
||
cancel_btn.pack(pady=10)
|
||
|
||
def export_config(self, parent_window=None):
|
||
"""导出配置到文件"""
|
||
# 先保存当前配置
|
||
if not self.save_config(show_message=False):
|
||
if messagebox.askyesno("警告", "当前配置存在错误,是否仍要导出?"):
|
||
pass
|
||
else:
|
||
return
|
||
|
||
# 选择保存位置
|
||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
default_filename = f"bot_config_export_{timestamp}.toml"
|
||
|
||
file_path = filedialog.asksaveasfilename(
|
||
title="导出配置",
|
||
filetypes=[("TOML 文件", "*.toml"), ("所有文件", "*.*")],
|
||
defaultextension=".toml",
|
||
initialfile=default_filename,
|
||
)
|
||
|
||
if not file_path:
|
||
return
|
||
|
||
try:
|
||
# 复制当前配置文件到选择的位置
|
||
shutil.copy2(CONFIG_PATH, file_path)
|
||
|
||
messagebox.showinfo("成功", f"配置已导出到:\n{file_path}")
|
||
self.status_label.configure(text=f"配置已导出到: {file_path}")
|
||
|
||
if parent_window:
|
||
parent_window.destroy()
|
||
|
||
return True
|
||
except Exception as e:
|
||
messagebox.showerror("导出失败", f"导出配置失败: {e}")
|
||
return False
|
||
|
||
def import_config(self, parent_window=None):
|
||
"""从文件导入配置"""
|
||
# 先检查是否有未保存的更改
|
||
if not self.check_unsaved_changes():
|
||
return
|
||
|
||
# 选择要导入的文件
|
||
file_path = filedialog.askopenfilename(
|
||
title="导入配置", filetypes=[("TOML 文件", "*.toml"), ("所有文件", "*.*")]
|
||
)
|
||
|
||
if not file_path:
|
||
return
|
||
|
||
try:
|
||
# 尝试加载TOML文件以验证格式
|
||
with open(file_path, "r", encoding="utf-8") as f:
|
||
import_data = toml.load(f)
|
||
|
||
# 验证导入文件的基本结构
|
||
if "inner" not in import_data:
|
||
raise ValueError("导入的配置文件没有inner部分,格式不正确")
|
||
|
||
if "version" not in import_data["inner"]:
|
||
raise ValueError("导入的配置文件没有版本信息,格式不正确")
|
||
|
||
# 确认导入
|
||
confirm = messagebox.askyesno(
|
||
"确认导入", f"确定要导入此配置文件吗?\n{file_path}\n\n这将替换当前的配置!", icon="warning"
|
||
)
|
||
|
||
if not confirm:
|
||
return
|
||
|
||
# 先备份当前配置
|
||
self.backup_config()
|
||
|
||
# 复制导入的文件到配置位置
|
||
shutil.copy2(file_path, CONFIG_PATH)
|
||
|
||
messagebox.showinfo("成功", "配置已导入,请重新加载以应用更改")
|
||
|
||
# 重新加载配置
|
||
self.reload_config()
|
||
|
||
if parent_window:
|
||
parent_window.destroy()
|
||
|
||
return True
|
||
except Exception as e:
|
||
messagebox.showerror("导入失败", f"导入配置失败: {e}")
|
||
return False
|
||
|
||
|
||
# 主函数
|
||
def main():
|
||
try:
|
||
app = ConfigUI()
|
||
app.mainloop()
|
||
except Exception as e:
|
||
print(f"程序发生错误: {e}")
|
||
# 显示错误对话框
|
||
|
||
import tkinter as tk
|
||
from tkinter import messagebox
|
||
|
||
root = tk.Tk()
|
||
root.withdraw()
|
||
messagebox.showerror("程序错误", f"程序运行时发生错误:\n{e}")
|
||
root.destroy()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|