diff --git a/.gitignore b/.gitignore index 2bac2dac9..08023cbc1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ mongodb/ NapCat.Framework.Windows.Once/ log/ logs/ +run_ad.bat MaiBot-Napcat-Adapter-main /test /src/test diff --git a/src/plugins/PFC/pfc.py b/src/plugins/PFC/pfc.py index 4500625ac..446b280c3 100644 --- a/src/plugins/PFC/pfc.py +++ b/src/plugins/PFC/pfc.py @@ -18,6 +18,7 @@ from .chat_observer import ChatObserver from .pfc_KnowledgeFetcher import KnowledgeFetcher from .reply_checker import ReplyChecker from .pfc_utils import get_items_from_json +from src.individuality.individuality import Individuality import time logger = get_module_logger("pfc") @@ -51,7 +52,7 @@ class ActionPlanner: max_tokens=1000, request_type="action_planning" ) - self.personality_info = " ".join(global_config.PROMPT_PERSONALITY) + self.personality_info = Individuality.get_instance().get_prompt(type = "personality", x_person = 2, level = 2) self.name = global_config.BOT_NICKNAME self.chat_observer = ChatObserver.get_instance(stream_id) @@ -67,7 +68,6 @@ class ActionPlanner: Args: goal: 对话目标 - method: 实现方式 reasoning: 目标原因 action_history: 行动历史记录 @@ -166,11 +166,14 @@ class GoalAnalyzer: request_type="conversation_goal" ) - self.personality_info = " ".join(global_config.PROMPT_PERSONALITY) + self.personality_info = Individuality.get_instance().get_prompt(type = "personality", x_person = 2, level = 2) self.name = global_config.BOT_NICKNAME self.nick_name = global_config.BOT_ALIAS_NAMES self.chat_observer = ChatObserver.get_instance(stream_id) + # 多目标存储结构 + self.goals = [] # 存储多个目标 + self.max_goals = 3 # 同时保持的最大目标数量 self.current_goal_and_reason = None async def analyze_goal(self) -> Tuple[str, str, str]: @@ -197,12 +200,29 @@ class GoalAnalyzer: chat_history_text += f"{time_str},{sender}:{msg.get('processed_plain_text', '')}\n" personality_text = f"你的名字是{self.name},{self.personality_info}" + + # 构建当前已有目标的文本 + existing_goals_text = "" + if self.goals: + existing_goals_text = "当前已有的对话目标:\n" + for i, (goal, _, reason) in enumerate(self.goals): + existing_goals_text += f"{i+1}. 目标: {goal}, 原因: {reason}\n" - prompt = f"""{personality_text}。现在你在参与一场QQ聊天,请分析以下聊天记录,并根据你的性格特征确定一个明确的对话目标。 -这个目标应该反映出对话的意图和期望的结果。 + prompt = f"""{personality_text}。现在你在参与一场QQ聊天,请分析以下聊天记录,并根据你的性格特征确定多个明确的对话目标。 +这些目标应该反映出对话的不同方面和意图。 + +{existing_goals_text} + 聊天记录: {chat_history_text} -请以JSON格式输出,包含以下字段: + +请分析当前对话并确定最适合的对话目标。你可以: +1. 保持现有目标不变 +2. 修改现有目标 +3. 添加新目标 +4. 删除不再相关的目标 + +请以JSON格式输出一个当前最主要的对话目标,包含以下字段: 1. goal: 对话目标(简短的一句话) 2. reasoning: 对话原因,为什么设定这个目标(简要解释) @@ -232,7 +252,16 @@ class GoalAnalyzer: # 使用默认的方法 method = "以友好的态度回应" - return goal, method, reasoning + + # 更新目标列表 + await self._update_goals(goal, method, reasoning) + + # 返回当前最主要的目标 + if self.goals: + current_goal, current_method, current_reasoning = self.goals[0] + return current_goal, current_method, current_reasoning + else: + return goal, method, reasoning except Exception as e: logger.error(f"分析对话目标时出错: {str(e)},重试第{retry + 1}次") @@ -242,8 +271,69 @@ class GoalAnalyzer: # 所有重试都失败后的默认返回 return "保持友好的对话", "以友好的态度回应", "确保对话顺利进行" + + async def _update_goals(self, new_goal: str, method: str, reasoning: str): + """更新目标列表 + + Args: + new_goal: 新的目标 + method: 实现目标的方法 + reasoning: 目标的原因 + """ + # 检查新目标是否与现有目标相似 + for i, (existing_goal, _, _) in enumerate(self.goals): + if self._calculate_similarity(new_goal, existing_goal) > 0.7: # 相似度阈值 + # 更新现有目标 + self.goals[i] = (new_goal, method, reasoning) + # 将此目标移到列表前面(最主要的位置) + self.goals.insert(0, self.goals.pop(i)) + return + + # 添加新目标到列表前面 + self.goals.insert(0, (new_goal, method, reasoning)) + + # 限制目标数量 + if len(self.goals) > self.max_goals: + self.goals.pop() # 移除最老的目标 + + def _calculate_similarity(self, goal1: str, goal2: str) -> float: + """简单计算两个目标之间的相似度 + + 这里使用一个简单的实现,实际可以使用更复杂的文本相似度算法 + + Args: + goal1: 第一个目标 + goal2: 第二个目标 + + Returns: + float: 相似度得分 (0-1) + """ + # 简单实现:检查重叠字数比例 + words1 = set(goal1) + words2 = set(goal2) + overlap = len(words1.intersection(words2)) + total = len(words1.union(words2)) + return overlap / total if total > 0 else 0 + + async def get_all_goals(self) -> List[Tuple[str, str, str]]: + """获取所有当前目标 + + Returns: + List[Tuple[str, str, str]]: 目标列表,每项为(目标, 方法, 原因) + """ + return self.goals.copy() + + async def get_alternative_goals(self) -> List[Tuple[str, str, str]]: + """获取除了当前主要目标外的其他备选目标 + + Returns: + List[Tuple[str, str, str]]: 备选目标列表 + """ + if len(self.goals) <= 1: + return [] + return self.goals[1:].copy() - async def analyze_conversation(self,goal,reasoning): + async def analyze_conversation(self, goal, reasoning): messages = self.chat_observer.get_message_history() chat_history_text = "" for msg in messages: @@ -293,6 +383,16 @@ class GoalAnalyzer: if not success: return False, False, "确保对话顺利进行" + # 如果当前目标达成,从目标列表中移除 + if result["goal_achieved"] and not result["stop_conversation"]: + for i, (g, _, _) in enumerate(self.goals): + if g == goal: + self.goals.pop(i) + # 如果还有其他目标,不停止对话 + if self.goals: + result["stop_conversation"] = False + break + return result["goal_achieved"], result["stop_conversation"], result["reason"] except Exception as e: @@ -304,7 +404,7 @@ class Waiter: """快 速 等 待""" def __init__(self, stream_id: str): self.chat_observer = ChatObserver.get_instance(stream_id) - self.personality_info = " ".join(global_config.PROMPT_PERSONALITY) + self.personality_info = Individuality.get_instance().get_prompt(type = "personality", x_person = 2, level = 2) self.name = global_config.BOT_NICKNAME async def wait(self) -> bool: @@ -318,8 +418,8 @@ class Waiter: await asyncio.sleep(1) logger.info("等待中...") # 检查是否超过60秒 - if time.time() - wait_start_time > 60: - logger.info("等待超过60秒,结束对话") + if time.time() - wait_start_time > 300: + logger.info("等待超过300秒,结束对话") return True logger.info("等待结束") return False @@ -335,7 +435,7 @@ class ReplyGenerator: max_tokens=300, request_type="reply_generation" ) - self.personality_info = " ".join(global_config.PROMPT_PERSONALITY) + self.personality_info = Individuality.get_instance().get_prompt(type = "personality", x_person = 2, level = 2) self.name = global_config.BOT_NICKNAME self.chat_observer = ChatObserver.get_instance(stream_id) self.reply_checker = ReplyChecker(stream_id) @@ -643,28 +743,76 @@ class Conversation: ) if not is_suitable: - logger.warning(f"生成的回复不合适,原因: {reason}") - if need_replan: - self.state = ConversationState.RETHINKING - self.current_goal, self.current_method, self.goal_reasoning = await self.goal_analyzer.analyze_goal() - return - else: - # 重新生成回复 + logger.warning(f"生成的回复不合适,原因: {reason}") + if need_replan: + # 尝试切换到其他备选目标 + alternative_goals = await self.goal_analyzer.get_alternative_goals() + if alternative_goals: + # 有备选目标,尝试使用下一个目标 + self.current_goal, self.current_method, self.goal_reasoning = alternative_goals[0] + logger.info(f"切换到备选目标: {self.current_goal}") + # 使用新目标生成回复 self.generated_reply = await self.reply_generator.generate( self.current_goal, self.current_method, [self._convert_to_message(msg) for msg in messages], - self.knowledge_cache, - self.generated_reply # 将不合适的回复作为previous_reply传入 + self.knowledge_cache ) + # 检查使用新目标生成的回复是否合适 + is_suitable, reason, _ = await self.reply_generator.check_reply( + self.generated_reply, + self.current_goal + ) + if is_suitable: + # 如果新目标的回复合适,调整目标优先级 + await self.goal_analyzer._update_goals( + self.current_goal, + self.current_method, + self.goal_reasoning + ) + else: + # 如果新目标还是不合适,重新思考目标 + self.state = ConversationState.RETHINKING + self.current_goal, self.current_method, self.goal_reasoning = await self.goal_analyzer.analyze_goal() + return + else: + # 没有备选目标,重新分析 + self.state = ConversationState.RETHINKING + self.current_goal, self.current_method, self.goal_reasoning = await self.goal_analyzer.analyze_goal() + return + else: + # 重新生成回复 + self.generated_reply = await self.reply_generator.generate( + self.current_goal, + self.current_method, + [self._convert_to_message(msg) for msg in messages], + self.knowledge_cache, + self.generated_reply # 将不合适的回复作为previous_reply传入 + ) while self.chat_observer.check(): if not is_suitable: logger.warning(f"生成的回复不合适,原因: {reason}") if need_replan: - self.state = ConversationState.RETHINKING - self.current_goal, self.current_method, self.goal_reasoning = await self.goal_analyzer.analyze_goal() - return + # 尝试切换到其他备选目标 + alternative_goals = await self.goal_analyzer.get_alternative_goals() + if alternative_goals: + # 有备选目标,尝试使用下一个目标 + self.current_goal, self.current_method, self.goal_reasoning = alternative_goals[0] + logger.info(f"切换到备选目标: {self.current_goal}") + # 使用新目标生成回复 + self.generated_reply = await self.reply_generator.generate( + self.current_goal, + self.current_method, + [self._convert_to_message(msg) for msg in messages], + self.knowledge_cache + ) + is_suitable = True # 假设使用新目标后回复是合适的 + else: + # 没有备选目标,重新分析 + self.state = ConversationState.RETHINKING + self.current_goal, self.current_method, self.goal_reasoning = await self.goal_analyzer.analyze_goal() + return else: # 重新生成回复 self.generated_reply = await self.reply_generator.generate( @@ -705,9 +853,31 @@ class Conversation: if not is_suitable: logger.warning(f"生成的回复不合适,原因: {reason}") if need_replan: - self.state = ConversationState.RETHINKING - self.current_goal, self.current_method, self.goal_reasoning = await self.goal_analyzer.analyze_goal() - return + # 尝试切换到其他备选目标 + alternative_goals = await self.goal_analyzer.get_alternative_goals() + if alternative_goals: + # 有备选目标,尝试使用 + self.current_goal, self.current_method, self.goal_reasoning = alternative_goals[0] + logger.info(f"切换到备选目标: {self.current_goal}") + # 使用新目标获取知识并生成回复 + knowledge, sources = await self.knowledge_fetcher.fetch( + self.current_goal, + [self._convert_to_message(msg) for msg in messages] + ) + if knowledge != "未找到相关知识": + self.knowledge_cache[sources] = knowledge + + self.generated_reply = await self.reply_generator.generate( + self.current_goal, + self.current_method, + [self._convert_to_message(msg) for msg in messages], + self.knowledge_cache + ) + else: + # 没有备选目标,重新分析 + self.state = ConversationState.RETHINKING + self.current_goal, self.current_method, self.goal_reasoning = await self.goal_analyzer.analyze_goal() + return else: # 重新生成回复 self.generated_reply = await self.reply_generator.generate( @@ -727,6 +897,16 @@ class Conversation: elif action == "judge_conversation": self.state = ConversationState.JUDGING self.goal_achieved, self.stop_conversation, self.reason = await self.goal_analyzer.analyze_conversation(self.current_goal, self.goal_reasoning) + + # 如果当前目标达成但还有其他目标 + if self.goal_achieved and not self.stop_conversation: + alternative_goals = await self.goal_analyzer.get_alternative_goals() + if alternative_goals: + # 切换到下一个目标 + self.current_goal, self.current_method, self.goal_reasoning = alternative_goals[0] + logger.info(f"当前目标已达成,切换到新目标: {self.current_goal}") + return + if self.stop_conversation: await self._stop_conversation() diff --git a/temp_utils_ui/temp_ui.py b/temp_utils_ui/temp_ui.py new file mode 100644 index 000000000..32b3aebd0 --- /dev/null +++ b/temp_utils_ui/temp_ui.py @@ -0,0 +1,1382 @@ +import os +import sys +import toml +import customtkinter as ctk +from tkinter import messagebox, StringVar, Text, filedialog +import json +from typing import Dict, Any, List, Union, Optional +import re +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_spliter": "回复分割器", + "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_spliter.enable_response_spliter": "是否启用回复分割器", + "response_spliter.response_max_length": "回复允许的最大长度", + "response_spliter.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("", 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("", 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("", 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("", 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("", 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("", 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": + try: + # 获取原始类型(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 + except ValueError: + error_path = path + raise ValueError(f"{path} 必须是一个有效的数字!") + elif var_type == "list": + try: + # 解析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 + except json.JSONDecodeError: + error_path = path + raise ValueError(f"{path} 必须是有效的JSON格式!") + 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("", 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("", 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("", 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}") + # 显示错误对话框 + try: + import tkinter as tk + from tkinter import messagebox + root = tk.Tk() + root.withdraw() + messagebox.showerror("程序错误", f"程序运行时发生错误:\n{e}") + root.destroy() + except: + pass + +if __name__ == "__main__": + main() diff --git a/temp_utils_ui/thingking_ui.py b/temp_utils_ui/thingking_ui.py new file mode 100644 index 000000000..e69de29bb diff --git a/配置文件修改器.exe b/配置文件修改器.exe new file mode 100644 index 000000000..dcb699074 Binary files /dev/null and b/配置文件修改器.exe differ