diff --git a/README.md b/README.md index 562168a54..325e3ad22 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@

-## 新版0.6.0部署前先阅读:https://docs.mai-mai.org/manual/usage/mmc_q_a +## 新版0.6.x部署前先阅读:https://docs.mai-mai.org/manual/usage/mmc_q_a ## 📝 项目简介 @@ -62,7 +62,7 @@ ### 📢 版本信息 -**最新版本: v0.6.0** ([查看更新日志](changelogs/changelog.md)) +**最新版本: v0.6.2** ([查看更新日志](changelogs/changelog.md)) > [!WARNING] > 请阅读教程后更新!!!!!!! > 请阅读教程后更新!!!!!!! @@ -86,7 +86,7 @@ ### ⚠️ 重要提示 -- 升级到v0.6.0版本前请务必阅读:[升级指南](https://docs.mai-mai.org/manual/usage/mmc_q_a) +- 升级到v0.6.x版本前请务必阅读:[升级指南](https://docs.mai-mai.org/manual/usage/mmc_q_a) - 本版本基于MaiCore重构,通过nonebot插件与QQ平台交互 - 项目处于活跃开发阶段,功能和API可能随时调整 @@ -115,21 +115,22 @@ | 模块 | 主要功能 | 特点 | |------|---------|------| -| 💬 聊天系统 | • 思维流/推理聊天
• 关键词主动发言
• 多模型支持
• 动态prompt构建
• 私聊功能(PFC) | 拟人化交互 | -| 🧠 思维流系统 | • 实时思考生成
• 自动启停机制
• 日程系统联动 | 智能化决策 | -| 🧠 记忆系统 2.0 | • 优化记忆抽取
• 海马体记忆机制
• 聊天记录概括 | 持久化记忆 | -| 😊 表情包系统 | • 情绪匹配发送
• GIF支持
• 自动收集与审查 | 丰富表达 | +| 💬 聊天系统 | • 心流/推理聊天
• 关键词主动发言
• 多模型支持
• 动态prompt构建
• 私聊功能(PFC) | 拟人化交互 | +| 🧠 心流系统 | • 实时思考生成
• 自动启停机制
• 日程系统联动
• 工具调用能力 | 智能化决策 | +| 🧠 记忆系统 | • 优化记忆抽取
• 海马体记忆机制
• 聊天记录概括 | 持久化记忆 | +| 😊 表情系统 | • 情绪匹配发送
• GIF支持
• 自动收集与审查 | 丰富表达 | | 📅 日程系统 | • 动态日程生成
• 自定义想象力
• 思维流联动 | 智能规划 | -| 👥 关系系统 2.0 | • 关系管理优化
• 丰富接口支持
• 个性化交互 | 深度社交 | +| 👥 关系系统 | • 关系管理优化
• 丰富接口支持
• 个性化交互 | 深度社交 | | 📊 统计系统 | • 使用数据统计
• LLM调用记录
• 实时控制台显示 | 数据可视 | | 🔧 系统功能 | • 优雅关闭机制
• 自动数据保存
• 异常处理完善 | 稳定可靠 | +| 🛠️ 工具系统 | • 知识获取工具
• 自动注册机制
• 多工具支持 | 扩展功能 | ## 📐 项目架构 ```mermaid graph TD A[MaiCore] --> B[对话系统] - A --> C[思维流系统] + A --> C[心流系统] A --> D[记忆系统] A --> E[情感系统] B --> F[多模型支持] diff --git a/changelogs/changelog.md b/changelogs/changelog.md index 4ba8b0245..0ddb486bf 100644 --- a/changelogs/changelog.md +++ b/changelogs/changelog.md @@ -17,6 +17,7 @@ - 需要配置支持工具调用的模型才能使用完整功能 #### 心流系统 +- 新增了上下文压缩缓存功能,可以有更持久的记忆 - 新增了心流系统的README.md文件,详细介绍了系统架构、主要功能和工作流程。 - 优化了心流系统的逻辑,包括子心流自动清理和合理配置更新间隔。 - 改进了心流观察系统,优化了提示词设计和系统表现,使心流运行更加稳定高效。 @@ -27,6 +28,8 @@ - 新增了`ReplyGenerator`类,用于根据观察信息和对话信息生成回复。 - 优化了消息队列管理系统,支持按时间顺序处理消息。 +#### 现在可以启用更好的表情包发送系统 + ### 💻 系统架构优化 #### 部署支持 diff --git a/src/do_tool/tool_can_use/send_emoji.py b/src/do_tool/tool_can_use/send_emoji.py new file mode 100644 index 000000000..090f301cf --- /dev/null +++ b/src/do_tool/tool_can_use/send_emoji.py @@ -0,0 +1,28 @@ +from src.do_tool.tool_can_use.base_tool import BaseTool +from src.common.logger import get_module_logger + +from typing import Dict, Any + +logger = get_module_logger("send_emoji_tool") + + +class SendEmojiTool(BaseTool): + """发送表情包的工具""" + + name = "send_emoji" + description = "当你觉得需要表达情感,或者帮助表达,可以使用这个工具发送表情包" + parameters = { + "type": "object", + "properties": { + "text": {"type": "string", "description": "要发送的表情包描述"} + }, + "required": ["text"], + } + + async def execute(self, function_args: Dict[str, Any], message_txt: str) -> Dict[str, Any]: + text = function_args.get("text", message_txt) + return { + "name": "send_emoji", + "content": text, + } + diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index e0f670e42..b93ae8fd4 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -324,27 +324,35 @@ def random_remove_punctuation(text: str) -> str: def process_llm_response(text: str) -> List[str]: - # processed_response = process_text_with_typos(content) - # 对西文字符段落的回复长度设置为汉字字符的两倍 - max_length = global_config.response_max_length * 3 + # 提取被 () 或 [] 包裹的内容 + pattern = re.compile(r'[\(\[].*?[\)\]]') + _extracted_contents = pattern.findall(text) + # 去除 () 和 [] 及其包裹的内容 + cleaned_text = pattern.sub('', text) + logger.debug(f"{text}去除括号处理后的文本: {cleaned_text}") + + # 对清理后的文本进行进一步处理 + max_length = global_config.response_max_length * 2 max_sentence_num = global_config.response_max_sentence_num - if len(text) > max_length and not is_western_paragraph(text): - logger.warning(f"回复过长 ({len(text)} 字符),返回默认回复") + if len(cleaned_text) > max_length and not is_western_paragraph(cleaned_text): + logger.warning(f"回复过长 ({len(cleaned_text)} 字符),返回默认回复") return ["懒得说"] - elif len(text) > 200: - logger.warning(f"回复过长 ({len(text)} 字符),返回默认回复") + elif len(cleaned_text) > 200: + logger.warning(f"回复过长 ({len(cleaned_text)} 字符),返回默认回复") return ["懒得说"] - # 处理长消息 + typo_generator = ChineseTypoGenerator( error_rate=global_config.chinese_typo_error_rate, min_freq=global_config.chinese_typo_min_freq, tone_error_rate=global_config.chinese_typo_tone_error_rate, word_replace_rate=global_config.chinese_typo_word_replace_rate, ) + if global_config.enable_response_splitter: - split_sentences = split_into_sentences_w_remove_punctuation(text) + split_sentences = split_into_sentences_w_remove_punctuation(cleaned_text) else: - split_sentences = [text] + split_sentences = [cleaned_text] + sentences = [] for sentence in split_sentences: if global_config.chinese_typo_enable: @@ -354,11 +362,12 @@ def process_llm_response(text: str) -> List[str]: sentences.append(typo_corrections) else: sentences.append(sentence) - # 检查分割后的消息数量是否过多(超过3条) if len(sentences) > max_sentence_num: logger.warning(f"分割后消息数量过多 ({len(sentences)} 条),返回默认回复") return [f"{global_config.BOT_NICKNAME}不知道哦"] + + # sentences.extend(extracted_contents) return sentences diff --git a/src/plugins/chat_module/think_flow_chat/think_flow_chat.py b/src/plugins/chat_module/think_flow_chat/think_flow_chat.py index 1ce782f1a..535b7ff44 100644 --- a/src/plugins/chat_module/think_flow_chat/think_flow_chat.py +++ b/src/plugins/chat_module/think_flow_chat/think_flow_chat.py @@ -108,33 +108,35 @@ class ThinkFlowChat: message_manager.add_message(message_set) return first_bot_msg - async def _handle_emoji(self, message, chat, response): + async def _handle_emoji(self, message, chat, response, send_emoji = ""): """处理表情包""" - if random() < global_config.emoji_chance: + if send_emoji: + emoji_raw = await emoji_manager.get_emoji_for_text(send_emoji) + else: emoji_raw = await emoji_manager.get_emoji_for_text(response) - if emoji_raw: - emoji_path, description = emoji_raw - emoji_cq = image_path_to_base64(emoji_path) + if emoji_raw: + emoji_path, description = emoji_raw + emoji_cq = image_path_to_base64(emoji_path) - thinking_time_point = round(message.message_info.time, 2) + thinking_time_point = round(message.message_info.time, 2) - message_segment = Seg(type="emoji", data=emoji_cq) - bot_message = MessageSending( - message_id="mt" + str(thinking_time_point), - chat_stream=chat, - bot_user_info=UserInfo( - user_id=global_config.BOT_QQ, - user_nickname=global_config.BOT_NICKNAME, - platform=message.message_info.platform, - ), - sender_info=message.message_info.user_info, - message_segment=message_segment, - reply=message, - is_head=False, - is_emoji=True, - ) + message_segment = Seg(type="emoji", data=emoji_cq) + bot_message = MessageSending( + message_id="mt" + str(thinking_time_point), + chat_stream=chat, + bot_user_info=UserInfo( + user_id=global_config.BOT_QQ, + user_nickname=global_config.BOT_NICKNAME, + platform=message.message_info.platform, + ), + sender_info=message.message_info.user_info, + message_segment=message_segment, + reply=message, + is_head=False, + is_emoji=True, + ) - message_manager.add_message(bot_message) + message_manager.add_message(bot_message) async def _update_relationship(self, message: MessageRecv, response_set): """更新关系情绪""" @@ -264,6 +266,7 @@ class ThinkFlowChat: update_relationship = "" get_mid_memory_id = [] tool_result_info = {} + send_emoji = "" try: with Timer("思考前使用工具", timing_results): tool_result = await self.tool_user.use_tool( @@ -302,6 +305,9 @@ class ThinkFlowChat: # 特殊判定:change_relationship if tool_name == "change_relationship": update_relationship = tool_data[0]["content"] + + if tool_name == "send_emoji": + send_emoji = tool_data[0]["content"] except Exception as e: logger.error(f"思考前工具调用失败: {e}") @@ -357,7 +363,13 @@ class ThinkFlowChat: # 处理表情包 try: with Timer("处理表情包", timing_results): - await self._handle_emoji(message, chat, response_set) + if global_config.emoji_chance == 1: + if send_emoji: + logger.info(f"麦麦决定发送表情包{send_emoji}") + await self._handle_emoji(message, chat, response_set, send_emoji) + else: + if random() < global_config.emoji_chance: + await self._handle_emoji(message, chat, response_set) except Exception as e: logger.error(f"心流处理表情包失败: {e}") diff --git a/src/plugins/chat_module/think_flow_chat/think_flow_prompt_builder.py b/src/plugins/chat_module/think_flow_chat/think_flow_prompt_builder.py index f02172625..ed7ca72f3 100644 --- a/src/plugins/chat_module/think_flow_chat/think_flow_prompt_builder.py +++ b/src/plugins/chat_module/think_flow_chat/think_flow_prompt_builder.py @@ -52,7 +52,7 @@ def init_prompt(): 你的名字叫{bot_name},{prompt_identity}。 {chat_target},你希望在群里回复:{content}。现在请你根据以下信息修改回复内容。将这个回复修改的更加日常且口语化的回复,平淡一些,回复尽量简短一些。不要回复的太有条理。 {prompt_ger},不要刻意突出自身学科背景,注意只输出回复内容。 -{moderation_prompt}。注意:不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""", +{moderation_prompt}。注意:不要输出多余内容(包括前后缀,冒号和引号,at或 @等 )。""", "heart_flow_prompt_response", ) diff --git a/temp_utils_ui/temp_ui.py b/temp_utils_ui/temp_ui.py deleted file mode 100644 index c38d815cb..000000000 --- a/temp_utils_ui/temp_ui.py +++ /dev/null @@ -1,1249 +0,0 @@ -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("", 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": - # 获取原始类型(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("", 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}") - # 显示错误对话框 - - import tkinter as tk - from tkinter import messagebox - - root = tk.Tk() - root.withdraw() - messagebox.showerror("程序错误", f"程序运行时发生错误:\n{e}") - root.destroy() - - -if __name__ == "__main__": - main() diff --git a/temp_utils_ui/thingking_ui.py b/temp_utils_ui/thingking_ui.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 50a39affc..92fb886a8 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -86,7 +86,7 @@ compress_length_limit = 5 #最多压缩份数,超过该数值的压缩上下 [message] max_context_size = 12 # 麦麦获得的上文数量,建议12,太短太长都会导致脑袋尖尖 -emoji_chance = 0.2 # 麦麦使用表情包的概率 +emoji_chance = 0.2 # 麦麦使用表情包的概率,设置为1让麦麦自己决定发不发 thinking_timeout = 60 # 麦麦最长思考时间,超过这个时间的思考会放弃 max_response_length = 256 # 麦麦回答的最大token数 message_buffer = true # 启用消息缓冲器?启用此项以解决消息的拆分问题,但会使麦麦的回复延迟 diff --git a/从0.6.0升级0.6.1请先看我.txt b/从0.6.0升级0.6.2请先看我.txt similarity index 100% rename from 从0.6.0升级0.6.1请先看我.txt rename to 从0.6.0升级0.6.2请先看我.txt diff --git a/配置文件修改器(临时测试用,以config为准).exe b/配置文件修改器(临时测试用,以config为准).exe deleted file mode 100644 index dcb699074..000000000 Binary files a/配置文件修改器(临时测试用,以config为准).exe and /dev/null differ