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] =?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 # 是否启用记忆系统