From df984717f77892742a14a11a4c313f63e9150746 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Tue, 9 Sep 2025 18:50:37 +0800 Subject: [PATCH 01/13] =?UTF-8?q?feat(planner):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=8A=A8=E4=BD=9C=E8=A7=84=E5=88=92=E5=99=A8=E4=BB=A5=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E7=BB=84=E5=90=88=E5=8A=A8=E4=BD=9C=E5=92=8C=E6=A6=82?= =?UTF-8?q?=E7=8E=87=E6=80=A7=E8=A1=A8=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重写了 Planner 的主 Prompt,引导 LLM 将回复(reply)视为主动作,将表情(emoji)等视为辅助动作,从而更好地生成组合动作,使响应更生动。 - 移除了旧的“100%概率动作强制添加”逻辑,并替换为新的“概率性表情”机制。现在,当生成回复时,会根据配置的概率(emoji_chance)自动附加一个 emoji 动作。 - 改进了 emoji 动作的情感匹配逻辑,从精确匹配改为模糊匹配,提高了根据 LLM 输出找到合适表情的成功率。 - 修复了随机类型动作在激活概率计算时的一个边界条件问题。 --- src/chat/planner_actions/action_modifier.py | 5 +- src/chat/planner_actions/planner.py | 78 +++++++++++++++------ src/plugins/built_in/core_actions/emoji.py | 9 ++- 3 files changed, 64 insertions(+), 28 deletions(-) diff --git a/src/chat/planner_actions/action_modifier.py b/src/chat/planner_actions/action_modifier.py index df7ecdccb..e9cc1d106 100644 --- a/src/chat/planner_actions/action_modifier.py +++ b/src/chat/planner_actions/action_modifier.py @@ -205,9 +205,8 @@ class ActionModifier: elif activation_type == ActionActivationType.RANDOM: probability = action_info.random_activation_probability - if probability >= 1.0: - continue # 概率为100%或更高,直接激活 - if random.random() > probability: + probability = action_info.random_activation_probability + if random.random() >= probability: reason = f"RANDOM类型未触发(概率{probability})" deactivated_actions.append((action_name, reason)) logger.debug(f"{self.log_prefix}未激活动作: {action_name},原因: {reason}") diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index 526873b43..0ddbfcb73 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -57,30 +57,64 @@ def init_prompt(): {moderation_prompt} -现在请你根据聊天内容和用户的最新消息选择合适的action和触发action的消息: +**任务: 构建一个完整的响应** +你的任务是根据当前的聊天内容,构建一个完整的、人性化的响应。一个完整的响应由两部分组成: +1. **主要动作**: 这是响应的核心,通常是 `reply`(文本回复)。 +2. **辅助动作 (可选)**: 这是为了增强表达效果的附加动作,例如 `emoji`(发送表情包)或 `poke_user`(戳一戳)。 + +**决策流程:** +1. 首先,决定是否要进行 `reply`。 +2. 然后,评估当前的对话气氛和用户情绪,判断是否需要一个**辅助动作**来让你的回应更生动、更符合你的性格。 +3. 如果需要,选择一个最合适的辅助动作与 `reply` 组合。 +4. 如果用户明确要求了某个动作,请务必优先满足。 + +**可用动作:** {actions_before_now_block} {no_action_block} 动作:reply 动作描述:参与聊天回复,发送文本进行表达 -- 你想要闲聊或者随便附 +- 你想要闲聊或者随便附和 - {mentioned_bonus} - 如果你刚刚进行了回复,不要对同一个话题重复回应 - 不要回复自己发送的消息 {{ "action": "reply", - "target_message_id":"触发action的消息id", - "reason":"回复的原因" + "target_message_id": "触发action的消息id", + "reason": "回复的原因" }} {action_options_text} -- 如果用户明确要求使用某个动作,请优先选择该动作。 -- 当一个动作可以作为另一个动作的补充时,你应该同时选择它们。例如,在回复的同时可以发送表情包(emoji)。 -你必须从上面列出的可用action中选择一个或多个,并说明触发action的消息id(不是消息原文)和选择该action的原因。消息id格式:m+数字 -请根据动作示例,以严格的 JSON 格式输出,返回一个包含所有选定动作的JSON列表。如果只选择一个动作,也请将其包含在列表中。如果没有任何合适的动作,返回一个空列表[]。不要输出markdown格式```json等内容,直接输出且仅包含 JSON 列表内容: +**输出格式:** +你必须以严格的 JSON 格式输出,返回一个包含所有选定动作的JSON列表。如果没有任何合适的动作,返回一个空列表[]。 + +**单动作示例 (仅回复):** +[ + {{ + "action": "reply", + "target_message_id": "m123", + "reason": "回答用户的问题" + }} +] + +**组合动作示例 (回复 + 表情包):** +[ + {{ + "action": "reply", + "target_message_id": "m123", + "reason": "回答用户的问题" + }}, + {{ + "action": "emoji", + "target_message_id": "m123", + "reason": "用一个可爱的表情来缓和气氛" + }} +] + +不要输出markdown格式```json等内容,直接输出且仅包含 JSON 列表内容: """, "planner_prompt", ) @@ -148,9 +182,9 @@ def init_prompt(): 动作描述:{action_description} {action_require} {{ - "action": "{action_name}",{action_parameters}, - "target_message_id":"触发action的消息id", - "reason":"触发action的原因" + "action": "{action_name}", + "target_message_id": "触发action的消息id", + "reason": "触发action的原因"{action_parameters} }} """, "action_prompt", @@ -409,18 +443,18 @@ class ActionPlanner: # --- 3. 后处理 --- final_actions = self._filter_no_actions(final_actions) - # === 强制后处理:确保100%概率的动作在回复时被附带 === - has_reply_action = any(a.get("action_type") == "reply" for a in final_actions) - if has_reply_action: - for action_name, action_info in available_actions.items(): - if action_info.activation_type == ActionActivationType.RANDOM and action_info.random_activation_probability >= 1.0: - # 检查此动作是否已被选择 - is_already_chosen = any(a.get("action_type") == action_name for a in final_actions) - if not is_already_chosen: - logger.info(f"{self.log_prefix}强制添加100%概率动作: {action_name}") + # === 概率模式后处理:根据配置决定是否强制添加 emoji 动作 === + if global_config.emoji.emoji_activate_type == 'random': + has_reply_action = any(a.get("action_type") == "reply" for a in final_actions) + if has_reply_action: + # 检查此动作是否已被选择 + is_already_chosen = any(a.get("action_type") == 'emoji' for a in final_actions) + if not is_already_chosen: + if random.random() < global_config.emoji.emoji_chance: + logger.info(f"{self.log_prefix}根据概率 '{global_config.emoji.emoji_chance}' 添加 emoji 动作") final_actions.append({ - "action_type": action_name, - "reasoning": "根据100%概率设置强制添加", + "action_type": 'emoji', + "reasoning": f"根据概率 {global_config.emoji.emoji_chance} 自动添加", "action_data": {}, "action_message": self.get_latest_message(used_message_id_list), "available_actions": available_actions, diff --git a/src/plugins/built_in/core_actions/emoji.py b/src/plugins/built_in/core_actions/emoji.py index 0944591be..e040d9c37 100644 --- a/src/plugins/built_in/core_actions/emoji.py +++ b/src/plugins/built_in/core_actions/emoji.py @@ -148,9 +148,12 @@ class EmojiAction(BaseAction): chosen_emotion = chosen_emotion.strip().replace('"', "").replace("'", "") logger.info(f"{self.log_prefix} LLM选择的情感: {chosen_emotion}") - if chosen_emotion in emotion_map: - emoji_base64, emoji_description = random.choice(emotion_map[chosen_emotion]) - logger.info(f"{self.log_prefix} 找到匹配情感 '{chosen_emotion}' 的表情包: {emoji_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}' 不在可用列表中, 将随机选择一个表情包" From e1ebf41f8d3ba594570bbe1f9a96880ac0492ba6 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Tue, 9 Sep 2025 22:14:27 +0800 Subject: [PATCH 02/13] =?UTF-8?q?feat(reminder):=20=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E5=AE=9A=E6=97=B6=E6=8F=90=E9=86=92=E7=B3=BB=E7=BB=9F=EF=BC=8C?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=99=BA=E8=83=BD=E7=94=A8=E6=88=B7=E8=AF=86?= =?UTF-8?q?=E5=88=AB=E4=B8=8E=E4=B8=8A=E4=B8=8B=E6=96=87=E6=84=9F=E7=9F=A5?= =?UTF-8?q?=E5=9B=9E=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对定时提醒功能进行了重大重构和增强,使其更加智能和人性化。 主要更新包括: - **智能用户识别**: 引入LLM从提醒内容中提取需要@的目标用户,取代了原有的简单正则表达式匹配,能够更准确地理解自然语言指令,如“提醒阿范喝水”。 - **专用提醒回复**: 当提醒任务不包含特定目标用户时(如“提醒我喝水”),系统不再简单回退,而是会调用一个专用的LLM流程,生成一条符合Bot性格的、温暖贴心的提醒消息。 - **上下文传递**: 创建提醒时会保存完整的原始消息,并在触发提醒时将其传递给处理流程。这使得LLM在执行@操作或生成回复时能拥有完整上下文,避免了记忆割裂感。 - **@用户匹配优化**: 增强了`at_user`动作的底层用户匹配逻辑,采用“精确匹配 -> 包含匹配 -> 模糊匹配”的多层策略,显著提高了在群聊中查找目标用户的准确率和鲁棒性。 - **提示词优化**: 全面优化了提醒流程中涉及的LLM提示词,无论是用户提取还是最终的@消息生成,都更具情景感,使Bot的回复更加自然流畅。 --- .../chat_loop/proactive/event_scheduler.py | 10 +- .../chat_loop/proactive/proactive_thinker.py | 238 +++++++++++++----- .../heart_flow/heartflow_message_processor.py | 9 +- src/plugins/built_in/at_user_plugin/plugin.py | 60 ++++- 4 files changed, 244 insertions(+), 73 deletions(-) diff --git a/src/chat/chat_loop/proactive/event_scheduler.py b/src/chat/chat_loop/proactive/event_scheduler.py index 767360dd8..5e6ca0c51 100644 --- a/src/chat/chat_loop/proactive/event_scheduler.py +++ b/src/chat/chat_loop/proactive/event_scheduler.py @@ -205,7 +205,7 @@ async def schedule_reminder( ) -async def _execute_reminder_callback(subheartflow_id: str, reminder_text: str): +async def _execute_reminder_callback(subheartflow_id: str, reminder_text: str, original_message: str = None): """执行提醒回调函数""" try: # 获取对应的subheartflow实例 @@ -219,11 +219,15 @@ async def _execute_reminder_callback(subheartflow_id: str, reminder_text: str): # 创建主动思考事件,触发完整的思考流程 from src.chat.chat_loop.proactive.events import ProactiveTriggerEvent + # 使用原始消息来构造reason,如果没有原始消息则使用处理后的内容 + reason_content = original_message if original_message else reminder_text + event = ProactiveTriggerEvent( source="reminder_system", - reason=f"定时提醒:{reminder_text}", + reason=f"定时提醒:{reason_content}", # 这里传递完整的原始消息 metadata={ "reminder_text": reminder_text, + "original_message": original_message, "trigger_time": datetime.now().isoformat() } ) @@ -231,7 +235,7 @@ async def _execute_reminder_callback(subheartflow_id: str, reminder_text: str): # 通过subflow的HeartFChatting实例触发主动思考 await subflow.heart_fc_instance.proactive_thinker.think(event) - logger.info(f"已触发提醒的主动思考,内容: {reminder_text}") + logger.info(f"已触发提醒的主动思考,内容: {reminder_text},没有传递那条消息吗?{original_message}") except Exception as e: logger.error(f"执行提醒回调时发生错误: {e}") diff --git a/src/chat/chat_loop/proactive/proactive_thinker.py b/src/chat/chat_loop/proactive/proactive_thinker.py index e4086aec1..0a28c6e98 100644 --- a/src/chat/chat_loop/proactive/proactive_thinker.py +++ b/src/chat/chat_loop/proactive/proactive_thinker.py @@ -120,75 +120,60 @@ class ProactiveThinker: trigger_event (ProactiveTriggerEvent): 触发事件。 """ try: - # 如果是提醒事件,跳过规划器,直接构建默认动作 + # 如果是提醒事件,直接使用当前上下文执行at_user动作 if trigger_event.source == "reminder_system": # 1. 获取上下文信息 metadata = trigger_event.metadata or {} - action_message = metadata reminder_content = trigger_event.reason.replace("定时提醒:", "").strip() - # 2. 确定目标用户名 + # 2. 使用LLM智能解析目标用户名 target_user_name = None - match = re.search(r"艾特一下([^,,\s]+)", reminder_content) - if match: - target_user_name = match.group(1) - else: - from src.person_info.person_info import get_person_info_manager - user_id = metadata.get("user_id") - platform = metadata.get("platform") - if user_id and platform: - person_id = get_person_info_manager().get_person_id(platform, user_id) - target_user_name = await get_person_info_manager().get_value(person_id, "person_name") + + # 首先尝试从完整的原始信息中解析(如果有的话) + full_content = trigger_event.reason + logger.info(f"{self.context.log_prefix} 解析提醒内容: '{full_content}'") + + target_user_name = await self._extract_target_user_with_llm(full_content) if not target_user_name: logger.warning(f"无法从提醒 '{reminder_content}' 中确定目标用户,回退") - raise Exception("无法确定目标用户") - - # 3. 构建动作 - action_result = { - "action_type": "at_user", - "reasoning": "执行定时提醒", - "action_data": { - "user_name": target_user_name, - "at_message": reminder_content - }, - "action_message": action_message - } - - # 4. 执行或回退 - try: - original_chat_id = metadata.get("chat_id") - if not original_chat_id: - if trigger_event.related_message_id: - db_message = await db_get(Messages, {"message_id": trigger_event.related_message_id}, single_result=True) or {} - original_chat_id = db_message.get("chat_id") - - if not original_chat_id: - raise Exception("提醒事件中缺少chat_id") - - from src.chat.heart_flow.heartflow import heartflow - subflow = await heartflow.get_or_create_subheartflow(original_chat_id) - if not subflow: - raise Exception(f"无法为chat_id {original_chat_id} 获取subflow") - - success, _, _ = await subflow.heart_fc_instance.cycle_processor._handle_action( - action=action_result["action_type"], - reasoning=action_result["reasoning"], - action_data=action_result["action_data"], - cycle_timers={}, - thinking_id="", - action_message=action_result["action_message"], - ) - if not success: - raise Exception("at_user action failed") - except Exception as e: - logger.warning(f"{self.context.log_prefix} at_user动作执行失败: {e},回退到proactive_reply") + # 回退到生成普通提醒消息 fallback_action = { "action_type": "proactive_reply", - "action_data": {"topic": trigger_event.reason}, - "action_message": action_message + "action_data": {"topic": f"定时提醒:{reminder_content}"}, + "action_message": metadata } - await self._generate_proactive_content_and_send(fallback_action, trigger_event) + await self._generate_reminder_proactive_reply(fallback_action, trigger_event, reminder_content) + return + + # 3. 直接使用当前上下文的cycle_processor执行at_user动作 + try: + success, _, _ = await self.cycle_processor._handle_action( + action="at_user", + reasoning="执行定时提醒", + action_data={ + "user_name": target_user_name, + "at_message": reminder_content + }, + cycle_timers={}, + thinking_id="", + action_message=metadata, + ) + if success: + logger.info(f"{self.context.log_prefix} 成功执行定时提醒艾特用户 {target_user_name}") + return + else: + raise Exception("at_user action failed") + except Exception as e: + logger.warning(f"{self.context.log_prefix} at_user动作执行失败: {e},回退到专用提醒回复") + # 回退到专用的定时提醒回复 + fallback_action = { + "action_type": "proactive_reply", + "action_data": {"topic": f"定时提醒:{reminder_content}"}, + "action_message": metadata + } + await self._generate_reminder_proactive_reply(fallback_action, trigger_event, reminder_content) + return else: # 对于其他来源的主动思考,正常调用规划器 @@ -213,6 +198,145 @@ class ProactiveThinker: except Exception as e: logger.error(f"{self.context.log_prefix} 主动思考执行异常: {e}") logger.error(traceback.format_exc()) + async def _extract_target_user_with_llm(self, reminder_content: str) -> str: + """ + 使用LLM从提醒内容中提取目标用户名 + + Args: + reminder_content: 完整的提醒内容 + + Returns: + 提取出的用户名,如果找不到则返回None + """ + try: + from src.llm_models.utils_model import LLMRequest + from src.config.config import model_config + + bot_name = global_config.bot.nickname + user_extraction_prompt = f''' +从以下提醒消息中提取需要被提醒的目标用户名。 + +**重要认知**:你的名字是"{bot_name}"。当消息中提到"{bot_name}"时,通常是在称呼你,而不是要提醒的目标。你需要找出除了你自己之外的那个目标用户。 + +提醒消息: "{reminder_content}" + +规则: +1. 用户名通常在"提醒"、"艾特"、"叫"等动词后面。 +2. **绝对不能**提取你自己的名字("{bot_name}")作为目标。 +3. 只提取最关键的人名,不要包含多余的词语(比如时间、动作)。 +4. 如果消息中除了你自己的名字外,没有明确提到其他目标用户名,请回答"无"。 + +示例: +- 消息: "定时提醒:{bot_name},提醒阿范一分钟后去写模组" -> "阿范" +- 消息: "定时提醒:一分钟后提醒一闪喝水" -> "一闪" +- 消息: "定时提醒:艾特绿皮" -> "绿皮" +- 消息: "定时提醒:喝水" -> "无" +- 消息: "定时提醒:{bot_name},记得休息" -> "无" + +请直接输出提取到的用户名,如果不存在则输出"无"。 +''' + + llm_request = LLMRequest( + model_set=model_config.model_task_config.utils_small, + request_type="reminder_user_extraction" + ) + + response, _ = await llm_request.generate_response_async(prompt=user_extraction_prompt) + + if response and response.strip() != "无": + logger.info(f"LLM成功提取目标用户: '{response.strip()}'") + return response.strip() + else: + logger.warning(f"LLM未能从 '{reminder_content}' 中提取目标用户") + return None + + except Exception as e: + logger.error(f"使用LLM提取用户名时出错: {e}") + return None + + async def _generate_reminder_proactive_reply(self, action_result: Dict[str, Any], trigger_event: ProactiveTriggerEvent, reminder_content: str): + """ + 为定时提醒事件生成专用的主动回复 + + Args: + action_result: 动作结果 + trigger_event: 触发事件 + reminder_content: 提醒内容 + """ + try: + logger.info(f"{self.context.log_prefix} 生成定时提醒专用回复: '{reminder_content}'") + + # 获取基本信息 + bot_name = global_config.bot.nickname + personality = global_config.personality + identity_block = ( + f"你的名字是{bot_name}。\n" + f"关于你:{personality.personality_core},并且{personality.personality_side}。\n" + f"你的身份是{personality.identity},平时说话风格是{personality.reply_style}。" + ) + mood_block = f"你现在的心情是:{mood_manager.get_mood_by_chat_id(self.context.stream_id).mood_state}" + + # 获取日程信息 + schedule_block = "你今天没有日程安排。" + if global_config.planning_system.schedule_enable: + if current_activity := schedule_manager.get_current_activity(): + schedule_block = f"你当前正在:{current_activity}。" + + # 为定时提醒定制的专用提示词 + reminder_prompt = f""" +## 你的角色 +{identity_block} + +## 你的心情 +{mood_block} + +## 你今天的日程安排 +{schedule_block} + +## 定时提醒任务 +你收到了一个定时提醒:"{reminder_content}" +这是一个自动触发的提醒事件,你需要根据提醒内容发送一条友好的提醒消息。 + +## 任务要求 +- 这是一个定时提醒,要体现出你的贴心和关怀 +- 根据提醒内容的具体情况(如"喝水"、"休息"等)给出相应的提醒 +- 保持你一贯的温暖、俏皮风格 +- 可以加上一些鼓励或关心的话语 +- 直接输出提醒消息,不要解释为什么要提醒 + +请生成一条温暖贴心的提醒消息。 +""" + + response_text = await generator_api.generate_response_custom( + chat_stream=self.context.chat_stream, + prompt=reminder_prompt, + request_type="chat.replyer.reminder", + ) + + if response_text: + response_set = process_human_text( + content=response_text, + enable_splitter=global_config.response_splitter.enable, + enable_chinese_typo=global_config.chinese_typo.enable, + ) + await self.cycle_processor.response_handler.send_response( + response_set, time.time(), action_result.get("action_message") + ) + await store_action_info( + chat_stream=self.context.chat_stream, + action_name="reminder_reply", + action_data={"reminder_content": reminder_content, "response": response_text}, + action_prompt_display=f"定时提醒回复: {reminder_content}", + action_done=True, + ) + logger.info(f"{self.context.log_prefix} 成功发送定时提醒回复: {response_text}") + else: + logger.error(f"{self.context.log_prefix} 定时提醒回复生成失败。") + + except Exception as e: + logger.error(f"{self.context.log_prefix} 生成定时提醒回复时异常: {e}") + logger.error(traceback.format_exc()) + async def _get_reminder_context(self, message_id: str) -> str: """获取提醒消息的上下文""" diff --git a/src/chat/heart_flow/heartflow_message_processor.py b/src/chat/heart_flow/heartflow_message_processor.py index 65c0fa61c..ca7e787b8 100644 --- a/src/chat/heart_flow/heartflow_message_processor.py +++ b/src/chat/heart_flow/heartflow_message_processor.py @@ -160,9 +160,13 @@ class HeartFCMessageReceiver: from src.chat.chat_loop.proactive.events import ProactiveTriggerEvent reminder_content = metadata.get('content', '提醒时间到了') + # 使用原始消息内容作为reason,如果没有则使用处理后的内容 + original_message = metadata.get('original_message', '') + reason_content = original_message if original_message else reminder_content + event = ProactiveTriggerEvent( source="reminder_system", - reason=f"定时提醒:{reminder_content}", + reason=f"定时提醒:{reason_content}", metadata=metadata, related_message_id=metadata.get("original_message_id") ) @@ -200,7 +204,8 @@ class HeartFCMessageReceiver: "content": reminder_event.content, "confidence": reminder_event.confidence, "created_at": datetime.now().isoformat(), - "original_message_id": message.message_info.message_id + "original_message_id": message.message_info.message_id, + "original_message": message.processed_plain_text # 保存完整的原始消息 } success = await event_scheduler.schedule_event( diff --git a/src/plugins/built_in/at_user_plugin/plugin.py b/src/plugins/built_in/at_user_plugin/plugin.py index e01f9bf9c..f05de7672 100644 --- a/src/plugins/built_in/at_user_plugin/plugin.py +++ b/src/plugins/built_in/at_user_plugin/plugin.py @@ -70,15 +70,49 @@ class AtAction(BaseAction): if not member_list: return False, "群成员列表为空" - # 使用模糊匹配找到最接近的用户名 - choices = {member["card"] or member["nickname"]: member["user_id"] for member in member_list} - best_match, score = process.extractOne(user_name, choices.keys()) + # 优化用户匹配逻辑 + best_match = None + user_id = None + + # 1. 完全精确匹配 + for member in member_list: + card = member.get("card", "") + nickname = member.get("nickname", "") + if user_name == card or user_name == nickname: + best_match = card if user_name == card else nickname + user_id = member["user_id"] + logger.info(f"找到完全精确匹配: '{user_name}' -> '{best_match}' (ID: {user_id})") + break - if score < 30: # 设置一个匹配度阈值 - logger.info(f"找不到与 '{user_name}' 高度匹配的用户 (最佳匹配: {best_match}, 分数: {score})") - return False, "用户不存在" + # 2. 包含关系匹配 + if not best_match: + containing_matches = [] + for member in member_list: + card = member.get("card", "") + nickname = member.get("nickname", "") + if user_name in card: + containing_matches.append((card, member["user_id"])) + elif user_name in nickname: + containing_matches.append((nickname, member["user_id"])) - user_id = choices[best_match] + if containing_matches: + # 选择最短的匹配项,因为通常更精确 + best_match, user_id = min(containing_matches, key=lambda x: len(x[0])) + logger.info(f"找到包含关系匹配: '{user_name}' -> '{best_match}' (ID: {user_id})") + + # 3. 模糊匹配作为兜底 + if not best_match: + choices = {member["card"] or member["nickname"]: member["user_id"] for member in member_list} + fuzzy_match, score = process.extractOne(user_name, choices.keys()) + if score >= 60: # 维持较高的阈值 + best_match = fuzzy_match + user_id = choices[best_match] + logger.info(f"找到模糊匹配: '{user_name}' -> '{best_match}' (ID: {user_id}, Score: {score})") + + if not best_match: + logger.warning(f"所有匹配策略都未能找到用户: '{user_name}'") + return False, "用户不存在" + user_info = {"user_id": user_id, "user_nickname": best_match} try: @@ -93,13 +127,17 @@ class AtAction(BaseAction): return False, "聊天流不存在" replyer = DefaultReplyer(chat_stream) - extra_info = f"你需要艾特用户 {user_name} 并回复他们说: {at_message}" - + # 优化提示词,消除记忆割裂感 + reminder_task = at_message.replace("定时提醒:", "").strip() + extra_info = f"""你之前记下了一个提醒任务:'{reminder_task}' +现在时间到了,你需要去提醒用户 '{user_name}'。 +请像一个朋友一样,自然地完成这个提醒,而不是生硬地复述任务。""" + success, llm_response, _ = await replyer.generate_reply_with_context( - reply_to=f"{user_name}:{at_message}", + reply_to=f"是时候提醒'{user_name}'了", # 内部上下文,更符合执行任务的语境 extra_info=extra_info, enable_tool=False, - from_plugin=False + from_plugin=True # 标记为插件调用,以便LLM更好地理解上下文 ) if not success or not llm_response: From f0da19c6f91f428b80b61cf205bea7d20bd94b87 Mon Sep 17 00:00:00 2001 From: Furina-1013-create <189647097+Furina-1013-create@users.noreply.github.com> Date: Tue, 9 Sep 2025 22:59:15 +0800 Subject: [PATCH 03/13] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 2 +- test_planner_personality.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 test_planner_personality.py diff --git a/requirements.txt b/requirements.txt index 0bfafb8da..edc7b9cb8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -65,6 +65,6 @@ asyncio tavily-python google-generativeai lunar_python - +fuzzywuzzy python-multipart aiofiles \ No newline at end of file diff --git a/test_planner_personality.py b/test_planner_personality.py new file mode 100644 index 000000000..e69de29bb From d95f73d52ff90def6fc54101be3c08f5b7f4c7bf Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Wed, 10 Sep 2025 09:43:51 +0800 Subject: [PATCH 04/13] =?UTF-8?q?fix(chat):=20=E4=BF=AE=E5=A4=8D=E7=A9=BA?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E5=BC=95=E7=94=A8=E5=B9=B6=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E5=8A=A8=E4=BD=9C=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 `DefaultReplyer` 中增加了对 `reply_message` 的空值检查,以避免在无回复上下文时引发错误。 - 在 `ActionPlanner` 中将 'do_nothing' 添加到非目标动作列表中,以正确处理此新增的无操作指令。 - 将 `EmojiAction` 使用的 LLM 模型从 'utils_small' 更新为 'planner',以适应模型配置的变更。 --- src/chat/planner_actions/planner.py | 4 ++-- src/chat/replyer/default_generator.py | 3 +++ src/plugins/built_in/core_actions/emoji.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index 0ddbfcb73..abf543ec0 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -319,7 +319,7 @@ class ActionPlanner: action_data = {k: v for k, v in action_json.items() if k not in ["action", "reason"]} target_message = None - if action not in ["no_action", "no_reply"]: + if action not in ["no_action", "no_reply", "do_nothing"]: if target_message_id := action_json.get("target_message_id"): target_message = self.find_message_by_id(target_message_id, message_id_list) if target_message is None: @@ -329,7 +329,7 @@ class ActionPlanner: logger.warning(f"{self.log_prefix}动作'{action}'缺少target_message_id") available_action_names = [name for name, _ in current_available_actions] - if action not in ["no_action", "no_reply", "reply"] and action not in available_action_names: + if action not in ["no_action", "no_reply", "reply", "do_nothing"] and action not in available_action_names: logger.warning( f"{self.log_prefix}LLM 返回了当前不可用或无效的动作: '{action}' (可用: {available_action_names}),将强制使用 'no_action'" ) diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index 7045b60e6..ef9cce84d 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -823,6 +823,9 @@ class DefaultReplyer: sender, target = self._parse_reply_target(reply_to) else: # 获取 platform,如果不存在则从 chat_stream 获取,如果还是 None 则使用默认值 + if reply_message is None: + logger.warning("reply_message 为 None,无法构建prompt") + return "" platform = reply_message.get("chat_info_platform") person_id = person_info_manager.get_person_id( platform, # type: ignore diff --git a/src/plugins/built_in/core_actions/emoji.py b/src/plugins/built_in/core_actions/emoji.py index e040d9c37..154903d4c 100644 --- a/src/plugins/built_in/core_actions/emoji.py +++ b/src/plugins/built_in/core_actions/emoji.py @@ -132,7 +132,7 @@ class EmojiAction(BaseAction): # 5. 调用LLM models = llm_api.get_available_models() - chat_model_config = models.get("utils_small") + chat_model_config = models.get("planner") if not chat_model_config: logger.error(f"{self.log_prefix} 未找到'utils_small'模型配置,无法调用LLM") return False, "未找到'utils_small'模型配置" From 9f094bfb32dc0077902f0a57e674dd8a2a80e003 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Wed, 10 Sep 2025 10:11:25 +0800 Subject: [PATCH 05/13] =?UTF-8?q?feat(emoji):=20=E5=BC=95=E5=85=A5?= =?UTF-8?q?=E8=A1=A8=E6=83=85=E5=8F=91=E9=80=81=E5=8E=86=E5=8F=B2=E4=BB=A5?= =?UTF-8?q?=E9=81=BF=E5=85=8D=E9=87=8D=E5=A4=8D=E5=8F=91=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为了提升表情发送的自然性和多样性,本次更新引入了发送历史记录机制,并优化了选择逻辑。 - 新增一个长度为4的双端队列,用于存储最近发送过的表情哈希,以避免在短期内重复。 - 修改LLM提示,要求其返回一个包含3个最匹配情感的有序列表,以提供更多候选表情。 - 重构选择逻辑:在LLM推荐或随机选择时,会优先选取未在最近历史中出现过的表情。 - 仅当所有候选表情都已在近期发送过时,才会退回至在完整表情库中进行选择。 --- src/plugins/built_in/core_actions/emoji.py | 104 +++++++++++++-------- 1 file changed, 65 insertions(+), 39 deletions(-) diff --git a/src/plugins/built_in/core_actions/emoji.py b/src/plugins/built_in/core_actions/emoji.py index 154903d4c..88f80e9f4 100644 --- a/src/plugins/built_in/core_actions/emoji.py +++ b/src/plugins/built_in/core_actions/emoji.py @@ -1,5 +1,6 @@ import random from typing import Tuple +from collections import deque # 导入新插件系统 from src.plugin_system import BaseAction, ActionActivationType, ChatMode @@ -20,6 +21,7 @@ logger = get_logger("emoji") class EmojiAction(BaseAction): """表情动作 - 发送表情包""" + # --- 类级别属性 --- # 激活设置 if global_config.emoji.emoji_activate_type == "llm": activation_type = ActionActivationType.LLM_JUDGE @@ -33,6 +35,9 @@ class EmojiAction(BaseAction): # 动作基本信息 action_name = "emoji" action_description = "发送表情包辅助表达情绪" + + # 最近发送表情的历史记录 + _sent_emoji_history = deque(maxlen=4) # LLM判断提示词 llm_judge_prompt = """ @@ -85,23 +90,29 @@ class EmojiAction(BaseAction): desc = emoji.description emotions = emoji.emotion - all_emojis_data.append((b64, desc)) + # 使用 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)) + 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()) - emoji_base64, emoji_description = "", "" + + chosen_emoji_b64, chosen_emoji_desc, chosen_emoji_hash = None, None, None if not available_emotions: logger.warning(f"{self.log_prefix} 获取到的表情包均无情感标签, 将随机发送") - emoji_base64, emoji_description = random.choice(all_emojis_data) + # 随机选择一个不在历史记录中的表情 + 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) else: # 获取最近的5条消息内容用于判断 recent_messages = message_api.get_recent_messages(chat_id=self.chat_id, limit=5) @@ -114,60 +125,75 @@ class EmojiAction(BaseAction): show_actions=False, ) - # 4. 构建prompt让LLM选择情感 + # 4. 构建prompt让LLM选择多个情感 prompt = f""" - 你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个情感标签列表中选择最匹配的一个。 + 你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个情感标签列表中选择最匹配的 **3个** 情感标签,并按匹配度从高到低排序。 这是最近的聊天记录: {messages_text} 这是理由:“{reason}” 这里是可用的情感标签:{available_emotions} - 请直接返回最匹配的那个情感标签,不要进行任何解释或添加其他多余的文字。 + 请直接返回一个包含3个最匹配情感标签的有序列表,例如:["开心", "激动", "有趣"],不要进行任何解释或添加其他多余的文字。 """ - 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'模型配置" + logger.error(f"{self.log_prefix} 未找到 'planner' 模型配置,无法调用LLM") + return False, "未找到 'planner' 模型配置" - success, chosen_emotion, _, _ = await llm_api.generate_with_model( - prompt, model_config=chat_model_config, request_type="emoji" + success, chosen_emotions_str, _, _ = await llm_api.generate_with_model( + prompt, model_config=chat_model_config, request_type="emoji_selection" ) - if not success: - logger.warning(f"{self.log_prefix} LLM调用失败: {chosen_emotion}, 将随机选择一个表情包") - emoji_base64, emoji_description = random.choice(all_emojis_data) + 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}") + + 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: - chosen_emotion = chosen_emotion.strip().replace('"', "").replace("'", "") - logger.info(f"{self.log_prefix} LLM选择的情感: {chosen_emotion}") - - # 使用模糊匹配来查找最相关的情感标签 - 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}") + if not success: + logger.warning(f"{self.log_prefix} LLM调用失败, 将随机选择一个表情包") else: - logger.warning( - f"{self.log_prefix} LLM选择的情感 '{chosen_emotion}' 不在可用列表中, 将随机选择一个表情包" - ) - emoji_base64, emoji_description = random.choice(all_emojis_data) + 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) - # 7. 发送表情包 - success = await self.send_emoji(emoji_base64) + # 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 not success: - logger.error(f"{self.log_prefix} 表情包发送失败") - return False, "表情包发送失败" - - return True, f"发送表情包: {emoji_description}" + logger.error(f"{self.log_prefix} 表情包发送失败") + return False, "表情包发送失败" except Exception as e: logger.error(f"{self.log_prefix} 表情动作执行失败: {e}", exc_info=True) From 8e2aa532522f86969f31948fec24f54b520e6805 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Wed, 10 Sep 2025 15:17:03 +0800 Subject: [PATCH 06/13] =?UTF-8?q?fix(chat):=20=E5=B0=86=20proactive=5Frepl?= =?UTF-8?q?y=20=E5=8A=A8=E4=BD=9C=E6=B7=BB=E5=8A=A0=E5=88=B0=E8=B1=81?= =?UTF-8?q?=E5=85=8D=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主动回复(proactive_reply)动作不针对特定消息,也不属于需要验证的可用工具。 此更改将其添加到 planner 的逻辑检查豁免中,以防止因缺少 `target_message_id` 或不在可用动作列表中而被错误地判定为无效。 --- src/chat/planner_actions/planner.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index abf543ec0..3163697d4 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -319,7 +319,7 @@ class ActionPlanner: action_data = {k: v for k, v in action_json.items() if k not in ["action", "reason"]} target_message = None - if action not in ["no_action", "no_reply", "do_nothing"]: + if action not in ["no_action", "no_reply", "do_nothing", "proactive_reply"]: if target_message_id := action_json.get("target_message_id"): target_message = self.find_message_by_id(target_message_id, message_id_list) if target_message is None: @@ -329,7 +329,7 @@ class ActionPlanner: logger.warning(f"{self.log_prefix}动作'{action}'缺少target_message_id") available_action_names = [name for name, _ in current_available_actions] - if action not in ["no_action", "no_reply", "reply", "do_nothing"] and action not in available_action_names: + if action not in ["no_action", "no_reply", "reply", "do_nothing", "proactive_reply"] and action not in available_action_names: logger.warning( f"{self.log_prefix}LLM 返回了当前不可用或无效的动作: '{action}' (可用: {available_action_names}),将强制使用 'no_action'" ) From 2e6c628cb91da61ff69eaf978b194c43eb385374 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Wed, 10 Sep 2025 17:10:24 +0800 Subject: [PATCH 07/13] =?UTF-8?q?feat(reminder):=20=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E6=8F=90=E9=86=92=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=8F=AF=E5=B0=86?= =?UTF-8?q?=E2=80=9C=E6=88=91=E2=80=9D=E8=AF=86=E5=88=AB=E4=B8=BA=E7=9B=AE?= =?UTF-8?q?=E6=A0=87=E7=94=A8=E6=88=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 通过向LLM提示词中传递消息发送者昵称,系统现在能够正确解析包含第一人称代词(如“我”)的提醒任务。这解决了之前无法为用户设置“提醒我”这类个人提醒的问题,使其交互更加自然。 此外,还优化了`@user`插件中生成提醒内容的提示词,明确指示LLM不要在回复中包含`@`或用户名,以避免系统自动@后出现重复的用户名,提升了提醒消息的质量。 --- .../chat_loop/proactive/proactive_thinker.py | 21 ++++++++++++------- .../heart_flow/heartflow_message_processor.py | 1 + src/plugins/built_in/at_user_plugin/plugin.py | 8 ++++++- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/chat/chat_loop/proactive/proactive_thinker.py b/src/chat/chat_loop/proactive/proactive_thinker.py index 0a28c6e98..432d96844 100644 --- a/src/chat/chat_loop/proactive/proactive_thinker.py +++ b/src/chat/chat_loop/proactive/proactive_thinker.py @@ -133,7 +133,8 @@ class ProactiveThinker: full_content = trigger_event.reason logger.info(f"{self.context.log_prefix} 解析提醒内容: '{full_content}'") - target_user_name = await self._extract_target_user_with_llm(full_content) + sender_name = metadata.get("sender_name") + target_user_name = await self._extract_target_user_with_llm(full_content, sender_name) if not target_user_name: logger.warning(f"无法从提醒 '{reminder_content}' 中确定目标用户,回退") @@ -198,12 +199,13 @@ class ProactiveThinker: except Exception as e: logger.error(f"{self.context.log_prefix} 主动思考执行异常: {e}") logger.error(traceback.format_exc()) - async def _extract_target_user_with_llm(self, reminder_content: str) -> str: + async def _extract_target_user_with_llm(self, reminder_content: str, sender_name: str) -> str: """ 使用LLM从提醒内容中提取目标用户名 Args: reminder_content: 完整的提醒内容 + sender_name: 消息发送者的昵称 Returns: 提取出的用户名,如果找不到则返回None @@ -216,20 +218,23 @@ class ProactiveThinker: user_extraction_prompt = f''' 从以下提醒消息中提取需要被提醒的目标用户名。 -**重要认知**:你的名字是"{bot_name}"。当消息中提到"{bot_name}"时,通常是在称呼你,而不是要提醒的目标。你需要找出除了你自己之外的那个目标用户。 +**重要认知**: +- 你的名字是"{bot_name}"。当消息中提到"{bot_name}"时,通常是在称呼你。 +- 消息的发送者是"{sender_name}"。当消息中出现"我"、"咱"等第一人称代词时,指代的就是"{sender_name}"。 提醒消息: "{reminder_content}" 规则: -1. 用户名通常在"提醒"、"艾特"、"叫"等动词后面。 -2. **绝对不能**提取你自己的名字("{bot_name}")作为目标。 -3. 只提取最关键的人名,不要包含多余的词语(比如时间、动作)。 -4. 如果消息中除了你自己的名字外,没有明确提到其他目标用户名,请回答"无"。 +1. 分析消息,找出真正需要被提醒的人。 +2. 如果提醒目标是第一人称(如"我"),那么目标就是发送者"{sender_name}"。 +3. **绝对不能**提取你自己的名字("{bot_name}")作为目标。 +4. 只提取最关键的人名,不要包含多余的词语。 +5. 如果没有明确的提醒目标(既不是其他人,也不是发送者自己),请回答"无"。 示例: +- 消息: "定时提醒:{bot_name},10分钟后提醒我去打深渊" -> "{sender_name}" - 消息: "定时提醒:{bot_name},提醒阿范一分钟后去写模组" -> "阿范" - 消息: "定时提醒:一分钟后提醒一闪喝水" -> "一闪" -- 消息: "定时提醒:艾特绿皮" -> "绿皮" - 消息: "定时提醒:喝水" -> "无" - 消息: "定时提醒:{bot_name},记得休息" -> "无" diff --git a/src/chat/heart_flow/heartflow_message_processor.py b/src/chat/heart_flow/heartflow_message_processor.py index ca7e787b8..734f6a301 100644 --- a/src/chat/heart_flow/heartflow_message_processor.py +++ b/src/chat/heart_flow/heartflow_message_processor.py @@ -199,6 +199,7 @@ class HeartFCMessageReceiver: metadata = { "type": "reminder", "user_id": reminder_event.user_id, + "sender_name": userinfo.user_nickname, # 添加发送者昵称 "platform": chat.platform, "chat_id": chat.stream_id, "content": reminder_event.content, diff --git a/src/plugins/built_in/at_user_plugin/plugin.py b/src/plugins/built_in/at_user_plugin/plugin.py index f05de7672..6d67b994c 100644 --- a/src/plugins/built_in/at_user_plugin/plugin.py +++ b/src/plugins/built_in/at_user_plugin/plugin.py @@ -131,7 +131,13 @@ class AtAction(BaseAction): reminder_task = at_message.replace("定时提醒:", "").strip() extra_info = f"""你之前记下了一个提醒任务:'{reminder_task}' 现在时间到了,你需要去提醒用户 '{user_name}'。 -请像一个朋友一样,自然地完成这个提醒,而不是生硬地复述任务。""" + +**重要规则**: +- 你的任务**只**是生成提醒的**内容**。 +- **绝对不要**在你的回复中包含任何`@`符号或者目标用户的名字。真正的@操作会由系统自动完成。 +- 像一个朋友一样,自然地完成这个提醒,而不是生硬地复述任务。 + +请直接输出提醒的**内容**。""" success, llm_response, _ = await replyer.generate_reply_with_context( reply_to=f"是时候提醒'{user_name}'了", # 内部上下文,更符合执行任务的语境 From cf5b7e9083d6616347891f899ee8003a4f24cc75 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Wed, 10 Sep 2025 23:06:41 +0800 Subject: [PATCH 08/13] =?UTF-8?q?fix(chat):=20=E9=81=BF=E5=85=8D=E5=9C=A8`?= =?UTF-8?q?message=5Fdata`=E4=B8=BA=E7=A9=BA=E6=97=B6=E4=BA=A7=E7=94=9F`At?= =?UTF-8?q?tributeError?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat_loop/response_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat/chat_loop/response_handler.py b/src/chat/chat_loop/response_handler.py index 354addc45..889371009 100644 --- a/src/chat/chat_loop/response_handler.py +++ b/src/chat/chat_loop/response_handler.py @@ -139,7 +139,7 @@ class ResponseHandler: need_reply = new_message_count >= random.randint(2, 4) reply_text = "" - is_proactive_thinking = message_data.get("message_type") == "proactive_thinking" + is_proactive_thinking = (message_data.get("message_type") == "proactive_thinking") if message_data else True first_replied = False for reply_seg in reply_set: From 8f65953b9d1dd37551e75bcae9f108e963619139 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Thu, 11 Sep 2025 08:50:05 +0800 Subject: [PATCH 09/13] =?UTF-8?q?feat(chat):=20=E5=BC=95=E5=85=A5=E4=B8=93?= =?UTF-8?q?=E6=B3=A8=E6=A8=A1=E5=BC=8F=E5=AE=89=E9=9D=99=E7=BE=A4=E7=BB=84?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 `focus_mode_quiet_groups` 配置项,允许用户指定在专注模式下需要保持安静的群组列表。 在此列表中的群组,机器人只有在被明确提及(艾特)时才会做出回应。这有助于在某些活跃度高但不需要机器人持续参与的群组中减少打扰。 该功能兼容了不同QQ适配器(如 `qq` 和 `napcat`)的平台名称。 --- src/chat/chat_loop/heartFC_chat.py | 31 ++++++++++++++++++++++++++++++ src/config/official_configs.py | 3 +++ template/bot_config_template.toml | 8 +++++++- 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/chat/chat_loop/heartFC_chat.py b/src/chat/chat_loop/heartFC_chat.py index b93931cbb..6f63cff1b 100644 --- a/src/chat/chat_loop/heartFC_chat.py +++ b/src/chat/chat_loop/heartFC_chat.py @@ -379,6 +379,37 @@ class HeartFChatting: self.context.last_message_time = time.time() self.context.last_read_time = time.time() + # --- 专注模式安静群组检查 --- + quiet_groups = global_config.chat.focus_mode_quiet_groups + if quiet_groups and self.context.chat_stream: + is_group_chat = self.context.chat_stream.group_info is not None + if is_group_chat: + try: + platform = self.context.chat_stream.platform + group_id = self.context.chat_stream.group_info.group_id + + # 兼容不同QQ适配器的平台名称 + is_qq_platform = platform in ["qq", "napcat"] + + current_chat_identifier = f"{platform}:{group_id}" + config_identifier_for_qq = f"qq:{group_id}" + + is_in_quiet_list = (current_chat_identifier in quiet_groups or + (is_qq_platform and config_identifier_for_qq in quiet_groups)) + + if is_in_quiet_list: + is_mentioned_in_batch = False + for msg in recent_messages: + if msg.get("is_mentioned"): + is_mentioned_in_batch = True + break + + if not is_mentioned_in_batch: + logger.info(f"{self.context.log_prefix} 在专注安静模式下,因未被提及而忽略了消息。") + return True # 消耗消息但不做回复 + except Exception as e: + logger.error(f"{self.context.log_prefix} 检查专注安静群组时出错: {e}") + # 处理唤醒度逻辑 if current_sleep_state in [SleepState.SLEEPING, SleepState.PREPARING_SLEEP, SleepState.INSOMNIA]: self._handle_wakeup_messages(recent_messages) diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 97facecc3..3230888b9 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -75,6 +75,9 @@ class ChatConfig(ValidatedConfigBase): at_bot_inevitable_reply: bool = Field(default=False, description="@机器人的必然回复") talk_frequency_adjust: list[list[str]] = Field(default_factory=lambda: [], description="聊天频率调整") focus_value: float = Field(default=1.0, description="专注值") + focus_mode_quiet_groups: List[str] = Field( + default_factory=list, description='专注模式下需要保持安静的群组列表, 格式: ["platform:group_id1", "platform:group_id2"]' + ) force_reply_private: bool = Field(default=False, description="强制回复私聊") group_chat_mode: Literal["auto", "normal", "focus"] = Field(default="auto", description="群聊模式") timestamp_display_mode: Literal["normal", "normal_no_YMD", "relative"] = Field( diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 73fdebd32..07c791c6b 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.8.0" +version = "6.8.1" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -111,6 +111,12 @@ talk_frequency = 1 focus_value = 1 # MoFox-Bot的专注思考能力,越高越容易持续连续对话 +# 在专注模式下,只在被艾特或提及时才回复的群组列表 +# 这可以让你在某些群里保持“高冷”,只在被需要时才发言 +# 格式为: ["platform:group_id1", "platform:group_id2"] +# 例如: ["qq:123456789", "qq:987654321"] +focus_mode_quiet_groups = [] + # breaking模式配置 enable_breaking_mode = true # 是否启用自动进入breaking模式,关闭后不会自动进入breaking形式 From bb1563afab8bdb20acdabaa0256ccf8197434cc4 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Thu, 11 Sep 2025 13:24:53 +0800 Subject: [PATCH 10/13] =?UTF-8?q?fix(chat):=20=E9=98=B2=E6=AD=A2=E6=9C=BA?= =?UTF-8?q?=E5=99=A8=E4=BA=BA=E5=9B=9E=E5=A4=8D=E8=87=AA=E8=BA=AB=E6=B6=88?= =?UTF-8?q?=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加配置选项 `allow_reply_self` 控制是否允许机器人回复自己发送的消息 当此选项为 false 时,在回复动作执行前检查目标用户 ID,如果是机器人自身则跳过回复 同时删除无用的测试文件 `test_planner_personality.py --- src/chat/chat_loop/cycle_processor.py | 4 ++++ src/config/official_configs.py | 1 + template/bot_config_template.toml | 4 +++- test_planner_personality.py | 0 4 files changed, 8 insertions(+), 1 deletion(-) delete mode 100644 test_planner_personality.py diff --git a/src/chat/chat_loop/cycle_processor.py b/src/chat/chat_loop/cycle_processor.py index 52092ee1f..f7bcd20fe 100644 --- a/src/chat/chat_loop/cycle_processor.py +++ b/src/chat/chat_loop/cycle_processor.py @@ -311,6 +311,10 @@ class CycleProcessor: if reply_actions: logger.info(f"{self.log_prefix} 正在执行文本回复...") for action in reply_actions: + target_user_id = action.get("action_message",{}).get("chat_info_user_id","") + if target_user_id == global_config.bot.qq_account and not global_config.chat.allow_reply_self: + logger.warning("选取的reply的目标为bot自己,跳过reply action") + continue result = await execute_action(action) if isinstance(result, Exception): logger.error(f"{self.log_prefix} 回复动作执行异常: {result}") diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 3230888b9..b97b5443f 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -73,6 +73,7 @@ class ChatConfig(ValidatedConfigBase): talk_frequency: float = Field(default=1.0, description="聊天频率") mentioned_bot_inevitable_reply: bool = Field(default=False, description="提到机器人的必然回复") at_bot_inevitable_reply: bool = Field(default=False, description="@机器人的必然回复") + allow_reply_self: bool = Field(default=False, description="是否允许回复自己说的话") talk_frequency_adjust: list[list[str]] = Field(default_factory=lambda: [], description="聊天频率调整") focus_value: float = Field(default=1.0, description="专注值") focus_mode_quiet_groups: List[str] = Field( diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 07c791c6b..3a7ed77de 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.8.1" +version = "6.8.2" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -123,6 +123,8 @@ enable_breaking_mode = true # 是否启用自动进入breaking模式,关闭后 # 强制私聊回复 force_reply_private = false # 是否强制私聊回复,开启后私聊将强制回复 +allow_reply_self = false # 是否允许回复自己说的话 + max_context_size = 25 # 上下文长度 thinking_timeout = 40 # MoFox-Bot一次回复最长思考规划时间,超过这个时间的思考会放弃(往往是api反应太慢) replyer_random_probability = 0.5 # 首要replyer模型被选择的概率 diff --git a/test_planner_personality.py b/test_planner_personality.py deleted file mode 100644 index e69de29bb..000000000 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 11/13] =?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 12/13] =?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 13/13] =?UTF-8?q?feat(emoji):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E8=A1=A8=E6=83=85=E9=80=89=E6=8B=A9=E9=80=BB=E8=BE=91=E5=B9=B6?= =?UTF-8?q?=E5=BC=95=E5=85=A5=E4=B8=8A=E4=B8=8B=E6=96=87=E6=95=B0=E9=87=8F?= =?UTF-8?q?=E9=99=90=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 # 是否启用记忆系统