From 62c548ad2b6682dc772c62bc77a9d64751195097 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Thu, 11 Sep 2025 16:09:48 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat(chat):=20=E5=AE=9E=E7=8E=B0=E7=94=B1?= =?UTF-8?q?=20LLM=20=E6=8E=A7=E5=88=B6=E7=9A=84=E8=87=AA=E7=84=B6=E5=9B=9E?= =?UTF-8?q?=E5=A4=8D=E5=88=86=E5=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 之前基于标点符号的自动分割逻辑较为僵硬,有时会破坏回复的连贯性,导致对话体验不佳。 本次更新引入了一种由 LLM 主导的回复分割机制: 1. 在 Prompt 中增加了明确的分割指令,引导 LLM 在需要模拟人类对话停顿或转折时,使用 `[SPLIT]` 标记。 2. 后端回复处理逻辑相应更新,优先根据 `[SPLIT]` 标记分割消息。 3. 若 LLM 未提供 `[SPLIT]` 标记,则将整段回复作为单条消息发送,避免了不必要的拆分。 此项改动旨在让消息的发送节奏更贴近真实人类的聊天习惯,从而提升交互的自然感和流畅度。 --- src/chat/replyer/default_generator.py | 17 ++++++++++++++++- src/chat/utils/utils.py | 11 ++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index ef9cce84d..3ad209aa4 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -136,7 +136,7 @@ def init_prompt(): 4. 不要浮夸,不要夸张修辞,不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。 最终请输出一条简短、完整且口语化的回复。 --------------------------------- + -------------------------------- {time_block} {reply_target_block} @@ -1024,6 +1024,21 @@ class DefaultReplyer: prompt = Prompt(template=template_prompt.template, parameters=prompt_parameters) prompt_text = await prompt.build() + # --- 动态添加分割指令 --- + if global_config.response_splitter.enable: + split_instruction = """ +## 分割指令 +你正在通过一个即时聊天软件发送消息。请模仿一个真实人类的打字和发送习惯: +- **简洁明了**: 如果一句话能说清楚,就一次性发出去,不要添加任何标记。 +- **自然断句**: 当你想表达一个转折、一个停顿,或者想补充说明时,就像正常人会先发一部分再发另一部分一样,请在断句处插入 `[SPLIT]` 标记。 +- **动机**: 使用 `[SPLIT]` 的目的是为了让对话节奏更自然,更有层次感,而不是为了分割而分割。 +示例: "我刚刚看到一个超好笑的视频![SPLIT]等我找找发给你~" +""" + # 在 "现在,你说:" 之前插入 + parts = prompt_text.rsplit("现在,你说:", 1) + if len(parts) == 2: + prompt_text = f"{parts[0]}{split_instruction}\n现在,你说:{parts[1]}" + return prompt_text async def build_prompt_rewrite_context( diff --git a/src/chat/utils/utils.py b/src/chat/utils/utils.py index 501bf382d..19f3ced99 100644 --- a/src/chat/utils/utils.py +++ b/src/chat/utils/utils.py @@ -331,8 +331,17 @@ def process_llm_response(text: str, enable_splitter: bool = True, enable_chinese ) if global_config.response_splitter.enable and enable_splitter: - split_sentences = split_into_sentences_w_remove_punctuation(cleaned_text) + logger.info("回复分割器已启用。") + if "[SPLIT]" in cleaned_text: + split_sentences_raw = cleaned_text.split("[SPLIT]") + # 清理每个句子首尾可能由LLM添加的空格或换行符,并移除空句子 + split_sentences = [s.strip() for s in split_sentences_raw if s.strip()] + logger.debug(f"LLM 自定义分割结果: {split_sentences}") + else: + # 如果没有 [SPLIT] 标记,则不进行任何分割 + split_sentences = [cleaned_text] else: + logger.debug("回复分割器已禁用。") split_sentences = [cleaned_text] sentences = [] From 0cb2fa3373f9ece0f517d4c7a2dada2c37bad60d Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Thu, 11 Sep 2025 17:12:01 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat(chat):=20=E4=B8=BA=E5=9B=9E=E5=A4=8D?= =?UTF-8?q?=E5=88=86=E5=89=B2=E5=99=A8=E6=B7=BB=E5=8A=A0=20`llm`=20?= =?UTF-8?q?=E4=B8=8E=20`punctuation`=20=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本次更新重构了回复分割功能,引入了 `split_mode` 配置项,允许用户在两种分割模式之间进行选择,提供了更大的灵活性。 - **`llm` 模式**: 延续了由大语言模型通过 `[SPLIT]` 标记决定断句的功能。此模式下的提示词(Prompt)已进一步优化,以引导模型做出更自然的分割。 - **`punctuation` 模式**: 恢复了传统的基于标点符号的分割逻辑。这已设为新的默认模式,确保用户更新后行为与旧版本保持一致。 此外,`at_user` 插件也进行了适配,以正确处理由 `llm` 模式可能产生的多段消息。 --- src/chat/replyer/default_generator.py | 18 +++++++++++------- src/chat/utils/utils.py | 18 ++++++++++++------ src/config/official_configs.py | 1 + src/plugins/built_in/at_user_plugin/plugin.py | 15 ++++++++++----- template/bot_config_template.toml | 3 ++- 5 files changed, 36 insertions(+), 19 deletions(-) diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index 3ad209aa4..0217f18e6 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -1025,14 +1025,18 @@ class DefaultReplyer: prompt_text = await prompt.build() # --- 动态添加分割指令 --- - if global_config.response_splitter.enable: + if global_config.response_splitter.enable and global_config.response_splitter.split_mode == "llm": split_instruction = """ -## 分割指令 -你正在通过一个即时聊天软件发送消息。请模仿一个真实人类的打字和发送习惯: -- **简洁明了**: 如果一句话能说清楚,就一次性发出去,不要添加任何标记。 -- **自然断句**: 当你想表达一个转折、一个停顿,或者想补充说明时,就像正常人会先发一部分再发另一部分一样,请在断句处插入 `[SPLIT]` 标记。 -- **动机**: 使用 `[SPLIT]` 的目的是为了让对话节奏更自然,更有层次感,而不是为了分割而分割。 -示例: "我刚刚看到一个超好笑的视频![SPLIT]等我找找发给你~" +## 消息分段艺术 +为了模仿真实人类的聊天节奏,你可以在需要时将一条回复分成几段发送。 + +**核心原则**: 只有当分段能**增强表达效果**或**控制信息节奏**时,才在断句处使用 `[SPLIT]` 标记。 + +**参考场景**: +- 当你想表达一个转折或停顿时。 +- 当你想先说结论,再补充说明时。 + +**任务**: 请结合你的智慧和人设,自然地决定是否需要分段。如果需要,请在最恰当的位置插入 `[SPLIT]` 标记。 """ # 在 "现在,你说:" 之前插入 parts = prompt_text.rsplit("现在,你说:", 1) diff --git a/src/chat/utils/utils.py b/src/chat/utils/utils.py index 19f3ced99..675bf4b85 100644 --- a/src/chat/utils/utils.py +++ b/src/chat/utils/utils.py @@ -331,15 +331,21 @@ def process_llm_response(text: str, enable_splitter: bool = True, enable_chinese ) if global_config.response_splitter.enable and enable_splitter: - logger.info("回复分割器已启用。") - if "[SPLIT]" in cleaned_text: + logger.info(f"回复分割器已启用,模式: {global_config.response_splitter.split_mode}。") + + split_mode = global_config.response_splitter.split_mode + + if split_mode == "llm" and "[SPLIT]" in cleaned_text: + logger.debug("检测到 [SPLIT] 标记,使用 LLM 自定义分割。") split_sentences_raw = cleaned_text.split("[SPLIT]") - # 清理每个句子首尾可能由LLM添加的空格或换行符,并移除空句子 split_sentences = [s.strip() for s in split_sentences_raw if s.strip()] - logger.debug(f"LLM 自定义分割结果: {split_sentences}") else: - # 如果没有 [SPLIT] 标记,则不进行任何分割 - split_sentences = [cleaned_text] + if split_mode == "llm": + logger.debug("未检测到 [SPLIT] 标记,本次不进行分割。") + split_sentences = [cleaned_text] + else: # mode == "punctuation" + logger.debug("使用基于标点的传统模式进行分割。") + split_sentences = split_into_sentences_w_remove_punctuation(cleaned_text) else: logger.debug("回复分割器已禁用。") split_sentences = [cleaned_text] diff --git a/src/config/official_configs.py b/src/config/official_configs.py index b97b5443f..1de98b6cc 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -475,6 +475,7 @@ class ResponseSplitterConfig(ValidatedConfigBase): """回复分割器配置类""" enable: bool = Field(default=True, description="启用") + split_mode: str = Field(default="llm", description="分割模式: 'llm' 或 'punctuation'") max_length: int = Field(default=256, description="最大长度") max_sentence_num: int = Field(default=3, description="最大句子数") enable_kaomoji_protection: bool = Field(default=False, description="启用颜文字保护") diff --git a/src/plugins/built_in/at_user_plugin/plugin.py b/src/plugins/built_in/at_user_plugin/plugin.py index 6d67b994c..1cac44fcc 100644 --- a/src/plugins/built_in/at_user_plugin/plugin.py +++ b/src/plugins/built_in/at_user_plugin/plugin.py @@ -28,9 +28,9 @@ class AtAction(BaseAction): # === 功能描述(必须填写)=== action_parameters = {"user_name": "需要艾特用户的名字", "at_message": "艾特用户时要发送的消息"} action_require = [ - "当需要艾特某个用户时使用", - "当你需要提醒特定用户查看消息时使用", - "在回复中需要明确指向某个用户时使用", + "当用户明确要求你去'叫'、'喊'、'提醒'或'艾特'某人时使用", + "当你判断,为了让特定的人看到消息,需要代表用户去呼叫他/她时使用", + "例如:'你去叫一下张三','提醒一下李四开会'", ] llm_judge_prompt = """ 判定是否需要使用艾特用户动作的条件: @@ -150,11 +150,16 @@ class AtAction(BaseAction): logger.error("回复器生成回复失败") return False, "回复生成失败" - final_message = llm_response.get("content", "") - if not final_message: + final_message_raw = llm_response.get("content", "") + if not final_message_raw: logger.warning("回复器生成了空内容") return False, "回复内容为空" + # 对LLM生成的内容进行后处理,解析[SPLIT]标记并将分段消息合并 + from src.chat.utils.utils import process_llm_response + final_message_segments = process_llm_response(final_message_raw, enable_splitter=True, enable_chinese_typo=False) + final_message = " ".join(final_message_segments) + await self.send_command( "SEND_AT_MESSAGE", args={"group_id": self.chat_stream.group_info.group_id, "qq_id": user_id, "text": final_message}, diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 3a7ed77de..a11ce6816 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.8.2" +version = "6.8.3" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -321,6 +321,7 @@ word_replace_rate=0.006 # 整词替换概率 [response_splitter] enable = true # 是否启用回复分割器 +split_mode = "punctuation" # 分割模式: "llm" - 由语言模型决定, "punctuation" - 基于标点符号 max_length = 512 # 回复允许的最大长度 max_sentence_num = 8 # 回复允许的最大句子数 enable_kaomoji_protection = false # 是否启用颜文字保护 From de90d452ccc2c830422d0a03106d2384c175ba4f Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Thu, 11 Sep 2025 17:29:42 +0800 Subject: [PATCH 3/7] =?UTF-8?q?feat(emoji):=20=E4=BC=98=E5=8C=96=E8=A1=A8?= =?UTF-8?q?=E6=83=85=E9=80=89=E6=8B=A9=E9=80=BB=E8=BE=91=E5=B9=B6=E5=BC=95?= =?UTF-8?q?=E5=85=A5=E4=B8=8A=E4=B8=8B=E6=96=87=E6=95=B0=E9=87=8F=E9=99=90?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构了内置插件中的表情发送逻辑,以提高选择的准确性和效率。 旧的机制依赖于预设的情感标签,这可能不准确或缺失。新的实现改为让 LLM 直接从一部分随机抽样的表情包描述中进行选择,这使得决策更贴近上下文。 主要变更: - 将基于情感标签的选择改为基于表情包描述的选择,使表情推荐更精准。 - 新增 `max_context_emojis` 配置项,用于控制每次传递给 LLM 的表情包候选项数量,从而减少 token 消耗并提高响应速度。 --- src/config/official_configs.py | 1 + src/plugins/built_in/core_actions/emoji.py | 197 +++++++++------------ template/bot_config_template.toml | 3 +- 3 files changed, 91 insertions(+), 110 deletions(-) diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 1de98b6cc..2252041f3 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -385,6 +385,7 @@ class EmojiConfig(ValidatedConfigBase): content_filtration: bool = Field(default=False, description="内容过滤") filtration_prompt: str = Field(default="符合公序良俗", description="过滤提示") enable_emotion_analysis: bool = Field(default=True, description="启用情感分析") + max_context_emojis: int = Field(default=30, description="每次随机传递给LLM的表情包最大数量,0为全部") class MemoryConfig(ValidatedConfigBase): diff --git a/src/plugins/built_in/core_actions/emoji.py b/src/plugins/built_in/core_actions/emoji.py index 88f80e9f4..b3f410a4b 100644 --- a/src/plugins/built_in/core_actions/emoji.py +++ b/src/plugins/built_in/core_actions/emoji.py @@ -1,6 +1,7 @@ import random from typing import Tuple from collections import deque +import json # 导入新插件系统 from src.plugin_system import BaseAction, ActionActivationType, ChatMode @@ -10,7 +11,7 @@ from src.common.logger import get_logger # 导入API模块 - 标准Python包方式 from src.plugin_system.apis import llm_api, message_api -from src.chat.emoji_system.emoji_manager import get_emoji_manager +from src.chat.emoji_system.emoji_manager import get_emoji_manager, MaiEmoji from src.chat.utils.utils_image import image_path_to_base64 from src.config.config import global_config @@ -72,125 +73,103 @@ class EmojiAction(BaseAction): reason = self.action_data.get("reason", "表达当前情绪") logger.info(f"{self.log_prefix} 发送表情原因: {reason}") - # 2. 获取所有表情包 + # 2. 获取所有有效的表情包对象 emoji_manager = get_emoji_manager() - all_emojis_obj = [e for e in emoji_manager.emoji_objects if not e.is_deleted] + all_emojis_obj: list[MaiEmoji] = [e for e in emoji_manager.emoji_objects if not e.is_deleted and e.description] if not all_emojis_obj: - logger.warning(f"{self.log_prefix} 无法获取任何表情包") - return False, "无法获取任何表情包" + logger.warning(f"{self.log_prefix} 无法获取任何带有描述的有效表情包") + return False, "无法获取任何带有描述的有效表情包" - # 3. 准备情感数据和后备列表 - emotion_map = {} - all_emojis_data = [] - - for emoji in all_emojis_obj: - b64 = image_path_to_base64(emoji.full_path) - if not b64: - continue - - desc = emoji.description - emotions = emoji.emotion - # 使用 emoji 对象的 hash 作为唯一标识符 - all_emojis_data.append((b64, desc, emoji.hash)) - - for emo in emotions: - if emo not in emotion_map: - emotion_map[emo] = [] - emotion_map[emo].append((b64, desc, emoji.hash)) - - if not all_emojis_data: - logger.warning(f"{self.log_prefix} 无法加载任何有效的表情包数据") - return False, "无法加载任何有效的表情包数据" - - available_emotions = list(emotion_map.keys()) - - chosen_emoji_b64, chosen_emoji_desc, chosen_emoji_hash = None, None, None - - if not available_emotions: - logger.warning(f"{self.log_prefix} 获取到的表情包均无情感标签, 将随机发送") - # 随机选择一个不在历史记录中的表情 - selectable_emojis = [e for e in all_emojis_data if e[2] not in self._sent_emoji_history] - if not selectable_emojis: # 如果都发过了,就从全部里面随机选 - selectable_emojis = all_emojis_data - chosen_emoji_b64, chosen_emoji_desc, chosen_emoji_hash = random.choice(selectable_emojis) + # 3. 根据新配置项决定抽样数量 + sample_size = global_config.emoji.max_context_emojis + if sample_size > 0 and len(all_emojis_obj) > sample_size: + sampled_emojis = random.sample(all_emojis_obj, sample_size) else: - # 获取最近的5条消息内容用于判断 - recent_messages = message_api.get_recent_messages(chat_id=self.chat_id, limit=5) - messages_text = "" - if recent_messages: - messages_text = message_api.build_readable_messages( - messages=recent_messages, - timestamp_mode="normal_no_YMD", - truncate=False, - show_actions=False, - ) + sampled_emojis = all_emojis_obj # 0表示全部 - # 4. 构建prompt让LLM选择多个情感 - prompt = f""" - 你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个情感标签列表中选择最匹配的 **3个** 情感标签,并按匹配度从高到低排序。 - 这是最近的聊天记录: - {messages_text} - - 这是理由:“{reason}” - 这里是可用的情感标签:{available_emotions} - 请直接返回一个包含3个最匹配情感标签的有序列表,例如:["开心", "激动", "有趣"],不要进行任何解释或添加其他多余的文字。 - """ + # 4. 为抽样的表情包创建带编号的描述列表 + prompt_emoji_list = [] + for i, emoji in enumerate(sampled_emojis): + prompt_emoji_list.append(f"{i + 1}. {emoji.description}") + + prompt_emoji_str = "\n".join(prompt_emoji_list) + chosen_emoji_obj: MaiEmoji = None - # 5. 调用LLM - models = llm_api.get_available_models() - chat_model_config = models.get("planner") - if not chat_model_config: - logger.error(f"{self.log_prefix} 未找到 'planner' 模型配置,无法调用LLM") - return False, "未找到 'planner' 模型配置" - - success, chosen_emotions_str, _, _ = await llm_api.generate_with_model( - prompt, model_config=chat_model_config, request_type="emoji_selection" + # 5. 获取最近的5条消息内容用于判断 + recent_messages = message_api.get_recent_messages(chat_id=self.chat_id, limit=5) + messages_text = "" + if recent_messages: + messages_text = message_api.build_readable_messages( + messages=recent_messages, + timestamp_mode="normal_no_YMD", + truncate=False, + show_actions=False, ) - selected_emoji_info = None - if success: - try: - # 解析LLM返回的列表 - import json - chosen_emotions = json.loads(chosen_emotions_str) - if isinstance(chosen_emotions, list): - logger.info(f"{self.log_prefix} LLM选择的情感候选项: {chosen_emotions}") - # 遍历候选项,找到第一个不在历史记录中的表情 - for emotion in chosen_emotions: - matched_key = next((key for key in emotion_map if emotion in key), None) - if matched_key: - # 从匹配到的表情中,随机选一个不在历史记录的 - candidate_emojis = [e for e in emotion_map[matched_key] if e[2] not in self._sent_emoji_history] - if candidate_emojis: - selected_emoji_info = random.choice(candidate_emojis) - break # 找到后立即跳出循环 - else: - logger.warning(f"{self.log_prefix} LLM返回的不是一个列表: {chosen_emotions_str}") - except (json.JSONDecodeError, TypeError): - logger.warning(f"{self.log_prefix} 解析LLM返回的情感列表失败: {chosen_emotions_str}") + # 6. 构建prompt让LLM选择编号 + prompt = f""" + 你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个带编号的表情包描述列表中选择最匹配的 **3个** 表情包,并按匹配度从高到低返回它们的编号。 + 这是最近的聊天记录: + {messages_text} + + 这是理由:“{reason}” + 这里是可用的表情包详细描述列表: + {prompt_emoji_str} + 请直接返回一个包含3个最匹配表情包编号的有序JSON列表,例如:[10, 2, 5],不要进行任何解释或添加其他多余的文字。 + """ - if selected_emoji_info: - chosen_emoji_b64, chosen_emoji_desc, chosen_emoji_hash = selected_emoji_info - logger.info(f"{self.log_prefix} 从候选项中选择表情: {chosen_emoji_desc}") - else: - if not success: - logger.warning(f"{self.log_prefix} LLM调用失败, 将随机选择一个表情包") + # 7. 调用LLM + models = llm_api.get_available_models() + chat_model_config = models.get("planner") + if not chat_model_config: + logger.error(f"{self.log_prefix} 未找到 'planner' 模型配置,无法调用LLM") + return False, "未找到 'planner' 模型配置" + + success, chosen_indices_str, _, _ = await llm_api.generate_with_model( + prompt, model_config=chat_model_config, request_type="emoji_selection" + ) + + selected_emoji_obj = None + if success: + try: + chosen_indices = json.loads(chosen_indices_str) + if isinstance(chosen_indices, list): + logger.info(f"{self.log_prefix} LLM选择的表情编号候选项: {chosen_indices}") + for index in chosen_indices: + if isinstance(index, int) and 1 <= index <= len(sampled_emojis): + candidate_emoji = sampled_emojis[index - 1] + if candidate_emoji.hash not in self._sent_emoji_history: + selected_emoji_obj = candidate_emoji + break else: - logger.warning(f"{self.log_prefix} 所有候选项均在最近发送历史中, 将随机选择") - - selectable_emojis = [e for e in all_emojis_data if e[2] not in self._sent_emoji_history] - if not selectable_emojis: - selectable_emojis = all_emojis_data - chosen_emoji_b64, chosen_emoji_desc, chosen_emoji_hash = random.choice(selectable_emojis) + logger.warning(f"{self.log_prefix} LLM返回的不是一个列表: {chosen_indices_str}") + except (json.JSONDecodeError, TypeError): + logger.warning(f"{self.log_prefix} 解析LLM返回的编号列表失败: {chosen_indices_str}") - # 7. 发送表情包并更新历史记录 - if chosen_emoji_b64 and chosen_emoji_hash: - success = await self.send_emoji(chosen_emoji_b64) - if success: - self._sent_emoji_history.append(chosen_emoji_hash) - logger.info(f"{self.log_prefix} 表情包发送成功: {chosen_emoji_desc}") - logger.debug(f"{self.log_prefix} 最近表情历史: {list(self._sent_emoji_history)}") - return True, f"发送表情包: {chosen_emoji_desc}" + if selected_emoji_obj: + chosen_emoji_obj = selected_emoji_obj + logger.info(f"{self.log_prefix} 从候选项中选择表情: {chosen_emoji_obj.description}") + else: + if not success: + logger.warning(f"{self.log_prefix} LLM调用失败, 将随机选择一个表情包") + else: + logger.warning(f"{self.log_prefix} 所有候选项均在最近发送历史中, 将随机选择") + + selectable_emojis = [e for e in all_emojis_obj if e.hash not in self._sent_emoji_history] + if not selectable_emojis: + selectable_emojis = all_emojis_obj + chosen_emoji_obj = random.choice(selectable_emojis) + + # 8. 发送表情包并更新历史记录 + if chosen_emoji_obj: + emoji_base64 = image_path_to_base64(chosen_emoji_obj.full_path) + if emoji_base64: + send_success = await self.send_emoji(emoji_base64) + if send_success: + self._sent_emoji_history.append(chosen_emoji_obj.hash) + logger.info(f"{self.log_prefix} 表情包发送成功: {chosen_emoji_obj.description}") + logger.debug(f"{self.log_prefix} 最近表情历史: {list(self._sent_emoji_history)}") + return True, f"发送表情包: {chosen_emoji_obj.description}" logger.error(f"{self.log_prefix} 表情包发送失败") return False, "表情包发送失败" diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index a11ce6816..5ce2f5797 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.8.3" +version = "6.8.4" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -246,6 +246,7 @@ steal_emoji = true # 是否偷取表情包,让MoFox-Bot可以将一些表情 content_filtration = false # 是否启用表情包过滤,只有符合该要求的表情包才会被保存 filtration_prompt = "符合公序良俗" # 表情包过滤要求,只有符合该要求的表情包才会被保存 enable_emotion_analysis = false # 是否启用表情包感情关键词二次识别,启用后表情包在第一次识别完毕后将送入第二次大模型识别来总结感情关键词,并构建进回复和决策器的上下文消息中 +max_context_emojis = 30 # 每次随机传递给LLM的表情包详细描述的最大数量,0为全部 [memory] enable_memory = true # 是否启用记忆系统 From ebb575ec535f56447e83af4757d76950a1e19627 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 12 Sep 2025 19:04:27 +0800 Subject: [PATCH 4/7] =?UTF-8?q?refactor(config):=20=E7=A7=BB=E9=99=A4.env?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E5=B9=B6=E5=BC=95=E5=85=A5=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=99=A8=E9=85=8D=E7=BD=AE=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将项目配置系统从依赖.env文件和环境变量迁移到使用Pydantic模型进行集中管理。此举通过移除`python-dotenv`库简化了环境设置,并提高了配置的类型安全性和可维护性。 主要变更包括: - 移除`bot.py`中的.env加载逻辑。 - 新增`ServerConfig`模型来管理服务器的主机和端口。 - 更新`src/common/server.py`和`src/common/message/api.py`以从全局配置对象获取服务器设置,取代了`os.environ`。 - 从配置中移除了已废弃的`MaizoneIntercomConfig`。 - 在`bot_config_template.toml`中添加了新的`[server]`配置部分。 --- bot.py | 9 --------- src/common/message/api.py | 4 ++-- src/common/server.py | 4 ++-- src/config/config.py | 6 ++---- src/config/official_configs.py | 16 +++++++--------- template/bot_config_template.toml | 5 ++++- 6 files changed, 17 insertions(+), 27 deletions(-) diff --git a/bot.py b/bot.py index 51d76e642..aab5cd4f1 100644 --- a/bot.py +++ b/bot.py @@ -7,17 +7,9 @@ import time import platform import traceback from pathlib import Path -from dotenv import load_dotenv from rich.traceback import install from colorama import init, Fore -if os.path.exists(".env"): - load_dotenv(".env", override=True) - print("成功加载环境变量配置") -else: - print("未找到.env文件,请确保程序所需的环境变量被正确设置") - raise FileNotFoundError(".env 文件不存在,请创建并配置所需的环境变量") - # maim_message imports for console input # 最早期初始化日志系统,确保所有后续模块都使用正确的日志格式 @@ -45,7 +37,6 @@ logger.info(f"已设置工作目录为: {script_dir}") confirm_logger = get_logger("confirm") # 获取没有加载env时的环境变量 -env_mask = {key: os.getenv(key) for key in os.environ} uvicorn_server = None driver = None diff --git a/src/common/message/api.py b/src/common/message/api.py index eed85c0a9..a85677f47 100644 --- a/src/common/message/api.py +++ b/src/common/message/api.py @@ -24,8 +24,8 @@ def get_global_api() -> MessageServer: # sourcery skip: extract-method # 设置基本参数 kwargs = { - "host": os.environ["HOST"], - "port": int(os.environ["PORT"]), + "host": global_config.server.host, + "port": int(global_config.server.port), "app": get_global_server().get_app(), } diff --git a/src/common/server.py b/src/common/server.py index 24311e54d..30c55d72d 100644 --- a/src/common/server.py +++ b/src/common/server.py @@ -2,7 +2,7 @@ from fastapi import FastAPI, APIRouter from fastapi.middleware.cors import CORSMiddleware # 新增导入 from typing import Optional from uvicorn import Config, Server as UvicornServer -import os +from src.config.config import global_config from rich.traceback import install install(extra_lines=3) @@ -98,5 +98,5 @@ def get_global_server() -> Server: """获取全局服务器实例""" global global_server if global_server is None: - global_server = Server(host=os.environ["HOST"], port=int(os.environ["PORT"])) + global_server = Server(host=global_config.server.host,port=int(global_config.server.port),) return global_server diff --git a/src/config/config.py b/src/config/config.py index ef2d413dd..a38122300 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -43,8 +43,8 @@ from src.config.official_configs import ( CrossContextConfig, PermissionConfig, CommandConfig, - MaizoneIntercomConfig, PlanningSystemConfig, + ServerConfig, ) from .api_ada_configs import ( @@ -399,9 +399,7 @@ class Config(ValidatedConfigBase): cross_context: CrossContextConfig = Field( default_factory=lambda: CrossContextConfig(), description="跨群聊上下文共享配置" ) - maizone_intercom: MaizoneIntercomConfig = Field( - default_factory=lambda: MaizoneIntercomConfig(), description="Maizone互通组配置" - ) + server: ServerConfig = Field(default_factory=lambda: ServerConfig(), description="主服务器配置") class APIAdapterConfig(ValidatedConfigBase): diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 2252041f3..6a557a342 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -494,6 +494,13 @@ class ExperimentalConfig(ValidatedConfigBase): pfc_chatting: bool = Field(default=False, description="启用PFC聊天") +class ServerConfig(ValidatedConfigBase): + """主服务器配置类""" + + host: str = Field(default="127.0.0.1", description="主服务器监听地址") + port: int = Field(default=8080, description="主服务器监听端口") + + class MaimMessageConfig(ValidatedConfigBase): """maim_message配置类""" @@ -676,15 +683,6 @@ class CrossContextConfig(ValidatedConfigBase): enable: bool = Field(default=False, description="是否启用跨群聊上下文共享功能") groups: List[ContextGroup] = Field(default_factory=list, description="上下文共享组列表") - - -class MaizoneIntercomConfig(ValidatedConfigBase): - """Maizone互通组配置""" - - enable: bool = Field(default=False, description="是否启用Maizone互通组功能") - groups: List[ContextGroup] = Field(default_factory=list, description="Maizone互通组列表") - - class CommandConfig(ValidatedConfigBase): """命令系统配置类""" diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 5ce2f5797..0570c4ec7 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.8.4" +version = "6.8.5" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -481,6 +481,9 @@ insomnia_duration_minutes = [30, 60] # 单次失眠状态的持续时间范围 # 入睡后,经过一段延迟后触发失眠判定的延迟时间(分钟),设置为范围以增加随机性 insomnia_trigger_delay_minutes = [15, 45] +[server] +host = "127.0.0.1" +port = 8080 [cross_context] # 跨群聊/私聊上下文共享配置 # 这是总开关,用于一键启用或禁用此功能 From eb5011ada1f776ca38622f76ef1dc7cd8a2619e5 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 12 Sep 2025 19:08:16 +0800 Subject: [PATCH 5/7] =?UTF-8?q?refactor(chat):=20=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E5=88=86=E6=AE=B5=E6=8C=87=E4=BB=A4=E5=9C=A8=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=E8=AF=8D=E4=B8=AD=E7=9A=84=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将回复分割指令从注入到 "现在,你说:" 之前,改为直接添加到整个提示词的顶部。 这种调整简化了提示词的构建逻辑,并确保分割指令作为最高优先级的上下文被模型处理,从而提高其遵循指令的稳定性。 --- src/chat/replyer/default_generator.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index 0217f18e6..d3a80c2a5 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -1038,10 +1038,8 @@ class DefaultReplyer: **任务**: 请结合你的智慧和人设,自然地决定是否需要分段。如果需要,请在最恰当的位置插入 `[SPLIT]` 标记。 """ - # 在 "现在,你说:" 之前插入 - parts = prompt_text.rsplit("现在,你说:", 1) - if len(parts) == 2: - prompt_text = f"{parts[0]}{split_instruction}\n现在,你说:{parts[1]}" + # 将分段指令添加到提示词顶部 + prompt_text = f"{split_instruction}\n{prompt_text}" return prompt_text From 493cd2b837502f09f7490919bdc998e1727ce4b7 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 12 Sep 2025 19:50:20 +0800 Subject: [PATCH 6/7] =?UTF-8?q?refactor(emoji):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E8=A1=A8=E6=83=85=E5=8F=91=E9=80=81=E9=80=BB=E8=BE=91=E4=B8=BA?= =?UTF-8?q?=E5=9F=BA=E4=BA=8E=E6=83=85=E6=84=9F=E6=A0=87=E7=AD=BE=E9=80=89?= =?UTF-8?q?=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构了`emoji`动作的实现,将原有的LLM选择表情描述的逻辑,改为先由LLM根据聊天内容和理由选择一个最匹配的“情感标签”,然后再从该标签下的表情库中随机选择一个进行发送。 主要变更: - 移除原有的表情抽样、编号和LLM选择编号的复杂流程。 - 引入基于`emotion`元数据的情感标签映射。 - 更新LLM的Prompt,使其专注于选择情感标签而非具体表情。 - 简化了代码逻辑,移除了不再需要的历史记录队列和相关配置项。 - 如果表情没有情感标签或LLM调用失败,则回退到随机发送。 --- src/plugins/built_in/core_actions/emoji.py | 177 ++++++++++----------- 1 file changed, 86 insertions(+), 91 deletions(-) diff --git a/src/plugins/built_in/core_actions/emoji.py b/src/plugins/built_in/core_actions/emoji.py index b3f410a4b..820777b44 100644 --- a/src/plugins/built_in/core_actions/emoji.py +++ b/src/plugins/built_in/core_actions/emoji.py @@ -1,7 +1,5 @@ import random from typing import Tuple -from collections import deque -import json # 导入新插件系统 from src.plugin_system import BaseAction, ActionActivationType, ChatMode @@ -22,7 +20,6 @@ logger = get_logger("emoji") class EmojiAction(BaseAction): """表情动作 - 发送表情包""" - # --- 类级别属性 --- # 激活设置 if global_config.emoji.emoji_activate_type == "llm": activation_type = ActionActivationType.LLM_JUDGE @@ -36,9 +33,6 @@ class EmojiAction(BaseAction): # 动作基本信息 action_name = "emoji" action_description = "发送表情包辅助表达情绪" - - # 最近发送表情的历史记录 - _sent_emoji_history = deque(maxlen=4) # LLM判断提示词 llm_judge_prompt = """ @@ -80,99 +74,100 @@ class EmojiAction(BaseAction): logger.warning(f"{self.log_prefix} 无法获取任何带有描述的有效表情包") return False, "无法获取任何带有描述的有效表情包" - # 3. 根据新配置项决定抽样数量 - sample_size = global_config.emoji.max_context_emojis - if sample_size > 0 and len(all_emojis_obj) > sample_size: - sampled_emojis = random.sample(all_emojis_obj, sample_size) - else: - sampled_emojis = all_emojis_obj # 0表示全部 - - # 4. 为抽样的表情包创建带编号的描述列表 - prompt_emoji_list = [] - for i, emoji in enumerate(sampled_emojis): - prompt_emoji_list.append(f"{i + 1}. {emoji.description}") + # 3. 准备情感数据和后备列表 + emotion_map = {} + all_emojis_data = [] - prompt_emoji_str = "\n".join(prompt_emoji_list) - chosen_emoji_obj: MaiEmoji = None + for emoji in all_emojis_obj: + b64 = image_path_to_base64(emoji.full_path) + if not b64: + continue + + desc = emoji.description + emotions = emoji.emotion + all_emojis_data.append((b64, desc)) - # 5. 获取最近的5条消息内容用于判断 - recent_messages = message_api.get_recent_messages(chat_id=self.chat_id, limit=5) - messages_text = "" - if recent_messages: - messages_text = message_api.build_readable_messages( - messages=recent_messages, - timestamp_mode="normal_no_YMD", - truncate=False, - show_actions=False, + for emo in emotions: + if emo not in emotion_map: + emotion_map[emo] = [] + emotion_map[emo].append((b64, desc)) + + if not all_emojis_data: + logger.warning(f"{self.log_prefix} 无法加载任何有效的表情包数据") + return False, "无法加载任何有效的表情包数据" + + available_emotions = list(emotion_map.keys()) + emoji_base64, emoji_description = "", "" + + if not available_emotions: + logger.warning(f"{self.log_prefix} 获取到的表情包均无情感标签, 将随机发送") + emoji_base64, emoji_description = random.choice(all_emojis_data) + else: + # 获取最近的5条消息内容用于判断 + recent_messages = message_api.get_recent_messages(chat_id=self.chat_id, limit=5) + messages_text = "" + if recent_messages: + messages_text = message_api.build_readable_messages( + messages=recent_messages, + timestamp_mode="normal_no_YMD", + truncate=False, + show_actions=False, + ) + + # 4. 构建prompt让LLM选择情感 + prompt = f""" + 你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个情感标签列表中选择最匹配的一个。 + 这是最近的聊天记录: + {messages_text} + + 这是理由:“{reason}” + 这里是可用的情感标签:{available_emotions} + 请直接返回最匹配的那个情感标签,不要进行任何解释或添加其他多余的文字。 + """ + + if global_config.debug.show_prompt: + logger.info(f"{self.log_prefix} 生成的LLM Prompt: {prompt}") + else: + logger.debug(f"{self.log_prefix} 生成的LLM Prompt: {prompt}") + + # 5. 调用LLM + models = llm_api.get_available_models() + chat_model_config = models.get("planner") + if not chat_model_config: + logger.error(f"{self.log_prefix} 未找到'utils_small'模型配置,无法调用LLM") + return False, "未找到'utils_small'模型配置" + + success, chosen_emotion, _, _ = await llm_api.generate_with_model( + prompt, model_config=chat_model_config, request_type="emoji" ) - # 6. 构建prompt让LLM选择编号 - prompt = f""" - 你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个带编号的表情包描述列表中选择最匹配的 **3个** 表情包,并按匹配度从高到低返回它们的编号。 - 这是最近的聊天记录: - {messages_text} - - 这是理由:“{reason}” - 这里是可用的表情包详细描述列表: - {prompt_emoji_str} - 请直接返回一个包含3个最匹配表情包编号的有序JSON列表,例如:[10, 2, 5],不要进行任何解释或添加其他多余的文字。 - """ - - # 7. 调用LLM - models = llm_api.get_available_models() - chat_model_config = models.get("planner") - if not chat_model_config: - logger.error(f"{self.log_prefix} 未找到 'planner' 模型配置,无法调用LLM") - return False, "未找到 'planner' 模型配置" - - success, chosen_indices_str, _, _ = await llm_api.generate_with_model( - prompt, model_config=chat_model_config, request_type="emoji_selection" - ) - - selected_emoji_obj = None - if success: - try: - chosen_indices = json.loads(chosen_indices_str) - if isinstance(chosen_indices, list): - logger.info(f"{self.log_prefix} LLM选择的表情编号候选项: {chosen_indices}") - for index in chosen_indices: - if isinstance(index, int) and 1 <= index <= len(sampled_emojis): - candidate_emoji = sampled_emojis[index - 1] - if candidate_emoji.hash not in self._sent_emoji_history: - selected_emoji_obj = candidate_emoji - break - else: - logger.warning(f"{self.log_prefix} LLM返回的不是一个列表: {chosen_indices_str}") - except (json.JSONDecodeError, TypeError): - logger.warning(f"{self.log_prefix} 解析LLM返回的编号列表失败: {chosen_indices_str}") - - if selected_emoji_obj: - chosen_emoji_obj = selected_emoji_obj - logger.info(f"{self.log_prefix} 从候选项中选择表情: {chosen_emoji_obj.description}") - else: if not success: - logger.warning(f"{self.log_prefix} LLM调用失败, 将随机选择一个表情包") + logger.warning(f"{self.log_prefix} LLM调用失败: {chosen_emotion}, 将随机选择一个表情包") + emoji_base64, emoji_description = random.choice(all_emojis_data) else: - logger.warning(f"{self.log_prefix} 所有候选项均在最近发送历史中, 将随机选择") - - selectable_emojis = [e for e in all_emojis_obj if e.hash not in self._sent_emoji_history] - if not selectable_emojis: - selectable_emojis = all_emojis_obj - chosen_emoji_obj = random.choice(selectable_emojis) + chosen_emotion = chosen_emotion.strip().replace('"', "").replace("'", "") + logger.info(f"{self.log_prefix} LLM选择的情感: {chosen_emotion}") - # 8. 发送表情包并更新历史记录 - if chosen_emoji_obj: - emoji_base64 = image_path_to_base64(chosen_emoji_obj.full_path) - if emoji_base64: - send_success = await self.send_emoji(emoji_base64) - if send_success: - self._sent_emoji_history.append(chosen_emoji_obj.hash) - logger.info(f"{self.log_prefix} 表情包发送成功: {chosen_emoji_obj.description}") - logger.debug(f"{self.log_prefix} 最近表情历史: {list(self._sent_emoji_history)}") - return True, f"发送表情包: {chosen_emoji_obj.description}" + # 使用模糊匹配来查找最相关的情感标签 + matched_key = next((key for key in emotion_map if chosen_emotion in key), None) + + if matched_key: + emoji_base64, emoji_description = random.choice(emotion_map[matched_key]) + logger.info(f"{self.log_prefix} 找到匹配情感 '{chosen_emotion}' (匹配到: '{matched_key}') 的表情包: {emoji_description}") + else: + logger.warning( + f"{self.log_prefix} LLM选择的情感 '{chosen_emotion}' 不在可用列表中, 将随机选择一个表情包" + ) + emoji_base64, emoji_description = random.choice(all_emojis_data) - logger.error(f"{self.log_prefix} 表情包发送失败") - return False, "表情包发送失败" + # 7. 发送表情包 + success = await self.send_emoji(emoji_base64) + + if not success: + logger.error(f"{self.log_prefix} 表情包发送失败") + return False, "表情包发送失败" + + return True, f"发送表情包: {emoji_description}" except Exception as e: logger.error(f"{self.log_prefix} 表情动作执行失败: {e}", exc_info=True) From d658fd8eb0b533d87a9ee6e94306dc0e7f0cb68e Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 12 Sep 2025 19:54:13 +0800 Subject: [PATCH 7/7] =?UTF-8?q?feat(emoji):=20=E8=AE=B0=E5=BD=95=E8=A1=A8?= =?UTF-8?q?=E6=83=85=E5=8C=85=E5=8F=91=E9=80=81=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在表情包发送成功或失败后,分别记录操作信息。这有助于更好地追踪和调试表情包功能的状态,两行就应该能解决重复发送的问题了,遥遥领先! --- src/plugins/built_in/core_actions/emoji.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plugins/built_in/core_actions/emoji.py b/src/plugins/built_in/core_actions/emoji.py index 820777b44..0e3305e6e 100644 --- a/src/plugins/built_in/core_actions/emoji.py +++ b/src/plugins/built_in/core_actions/emoji.py @@ -165,7 +165,9 @@ class EmojiAction(BaseAction): if not success: logger.error(f"{self.log_prefix} 表情包发送失败") + await self.store_action_info(action_build_into_prompt = True,action_prompt_display =f"发送了一个{chosen_emotion}的表情包,但失败了",action_done= False) return False, "表情包发送失败" + await self.store_action_info(action_build_into_prompt = True,action_prompt_display =f"发送了一个{chosen_emotion}的表情包",action_done= True) return True, f"发送表情包: {emoji_description}"