diff --git a/plugins/set_emoji_like/plugin.py b/plugins/set_emoji_like/plugin.py index 925873fb9..9e569cbb2 100644 --- a/plugins/set_emoji_like/plugin.py +++ b/plugins/set_emoji_like/plugin.py @@ -46,7 +46,7 @@ class SetEmojiLikeAction(BaseAction): # === 基本信息(必须填写)=== action_name = "set_emoji_like" - action_description = "为消息设置表情回应/贴表情" + action_description = "为一个已存在的消息添加点赞或表情回应(也叫‘贴表情’)" activation_type = ActionActivationType.ALWAYS # 消息接收时激活(?) chat_type_allow = ChatType.GROUP parallel_action = True @@ -64,10 +64,9 @@ class SetEmojiLikeAction(BaseAction): "set": "是否设置回应 (True/False)", } action_require = [ - "当需要对消息贴表情时使用", - "当你想回应某条消息但又不想发文字时使用", - "不要连续发送,如果你已经贴表情包,就不要选择此动作", - "当你想用贴表情回应某条消息时使用", + "当需要对一个已存在消息进行‘贴表情’回应时使用", + "这是一个对旧消息的操作,而不是发送新消息", + "如果你想发送一个新的表情包消息,请使用 'emoji' 动作", ] llm_judge_prompt = """ 判定是否需要使用贴表情动作的条件: diff --git a/src/chat/chat_loop/cycle_processor.py b/src/chat/chat_loop/cycle_processor.py index 3bb585697..52092ee1f 100644 --- a/src/chat/chat_loop/cycle_processor.py +++ b/src/chat/chat_loop/cycle_processor.py @@ -299,63 +299,67 @@ class CycleProcessor: "error": str(e), } - # 创建所有动作的后台任务 - action_tasks = [asyncio.create_task(execute_action(action)) for action in actions] - - # 并行执行所有任务 - results = await asyncio.gather(*action_tasks, return_exceptions=True) - - # 处理执行结果 + # 分离 reply 动作和其他动作 + reply_actions = [a for a in actions if a.get("action_type") == "reply"] + other_actions = [a for a in actions if a.get("action_type") != "reply"] + reply_loop_info = None reply_text_from_reply = "" - action_success = False - action_reply_text = "" - action_command = "" + other_actions_results = [] - for i, result in enumerate(results): - if isinstance(result, BaseException): - logger.error(f"{self.log_prefix} 动作执行异常: {result}") - continue - - action_info = actions[i] - if result["action_type"] != "reply": - action_success = result["success"] - action_reply_text = result["reply_text"] - action_command = result.get("command", "") - elif result["action_type"] == "reply": - if result["success"]: - reply_loop_info = result["loop_info"] - reply_text_from_reply = result["reply_text"] + # 1. 首先串行执行所有 reply 动作(通常只有一个) + if reply_actions: + logger.info(f"{self.log_prefix} 正在执行文本回复...") + for action in reply_actions: + result = await execute_action(action) + if isinstance(result, Exception): + logger.error(f"{self.log_prefix} 回复动作执行异常: {result}") + continue + if result.get("success"): + reply_loop_info = result.get("loop_info") + reply_text_from_reply = result.get("reply_text", "") else: logger.warning(f"{self.log_prefix} 回复动作执行失败") + # 2. 然后并行执行所有其他动作 + if other_actions: + logger.info(f"{self.log_prefix} 正在执行附加动作: {[a.get('action_type') for a in other_actions]}") + other_action_tasks = [asyncio.create_task(execute_action(action)) for action in other_actions] + results = await asyncio.gather(*other_action_tasks, return_exceptions=True) + for i, result in enumerate(results): + if isinstance(result, BaseException): + logger.error(f"{self.log_prefix} 附加动作执行异常: {result}") + continue + other_actions_results.append(result) + # 构建最终的循环信息 if reply_loop_info: - # 如果有回复信息,使用回复的loop_info作为基础 loop_info = reply_loop_info - # 更新动作执行信息 - loop_info["loop_action_info"].update( - { - "action_taken": action_success, - "command": action_command, - "taken_time": time.time(), - } - ) + # 将其他动作的结果合并到loop_info中 + if "other_actions" not in loop_info["loop_action_info"]: + loop_info["loop_action_info"]["other_actions"] = [] + loop_info["loop_action_info"]["other_actions"].extend(other_actions_results) reply_text = reply_text_from_reply else: # 没有回复信息,构建纯动作的loop_info + # 即使没有回复,也要正确处理其他动作 + final_action_taken = any(res.get("success", False) for res in other_actions_results) + final_reply_text = " ".join(res.get("reply_text", "") for res in other_actions_results if res.get("reply_text")) + final_command = " ".join(res.get("command", "") for res in other_actions_results if res.get("command")) + loop_info = { "loop_plan_info": { "action_result": actions, }, "loop_action_info": { - "action_taken": action_success, - "reply_text": action_reply_text, - "command": action_command, + "action_taken": final_action_taken, + "reply_text": final_reply_text, + "command": final_command, "taken_time": time.time(), + "other_actions": other_actions_results, }, } - reply_text = action_reply_text + reply_text = final_reply_text # 停止正在输入状态 if ENABLE_S4U: @@ -421,7 +425,7 @@ class CycleProcessor: if fallback_action and fallback_action != action: logger.info(f"{self.context.log_prefix} 使用回退动作: {fallback_action}") action_handler = self.context.action_manager.create_action( - action_name=fallback_action if isinstance(fallback_action, list) else fallback_action, + action_name=fallback_action, action_data=action_data, reasoning=f"原动作'{action}'不可用,自动回退。{reasoning}", cycle_timers=cycle_timers, diff --git a/src/chat/planner_actions/action_modifier.py b/src/chat/planner_actions/action_modifier.py index 705f723c8..df7ecdccb 100644 --- a/src/chat/planner_actions/action_modifier.py +++ b/src/chat/planner_actions/action_modifier.py @@ -205,7 +205,9 @@ class ActionModifier: elif activation_type == ActionActivationType.RANDOM: probability = action_info.random_activation_probability - if random.random() >= probability: + if probability >= 1.0: + continue # 概率为100%或更高,直接激活 + 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 f0f850eb7..526873b43 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -76,6 +76,8 @@ def init_prompt(): {action_options_text} +- 如果用户明确要求使用某个动作,请优先选择该动作。 +- 当一个动作可以作为另一个动作的补充时,你应该同时选择它们。例如,在回复的同时可以发送表情包(emoji)。 你必须从上面列出的可用action中选择一个或多个,并说明触发action的消息id(不是消息原文)和选择该action的原因。消息id格式:m+数字 请根据动作示例,以严格的 JSON 格式输出,返回一个包含所有选定动作的JSON列表。如果只选择一个动作,也请将其包含在列表中。如果没有任何合适的动作,返回一个空列表[]。不要输出markdown格式```json等内容,直接输出且仅包含 JSON 列表内容: @@ -407,6 +409,23 @@ 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}") + final_actions.append({ + "action_type": action_name, + "reasoning": "根据100%概率设置强制添加", + "action_data": {}, + "action_message": self.get_latest_message(used_message_id_list), + "available_actions": available_actions, + }) + if not final_actions: final_actions = [ { @@ -531,9 +550,13 @@ class ActionPlanner: if mode == ChatMode.FOCUS: no_action_block = """ -- 'no_reply' 表示不进行回复,等待合适的回复时机 -- 当你刚刚发送了消息,没有人回复时,选择no_reply -- 当你一次发送了太多消息,为了避免打扰聊天节奏,选择no_reply +动作:no_action +动作描述:不选择任何动作 +{{ + "action": "no_action", + "reason":"不动作的原因" +}} + 动作:no_reply 动作描述:不进行回复,等待合适的回复时机 - 当你刚刚发送了消息,没有人回复时,选择no_reply diff --git a/src/plugins/built_in/core_actions/emoji.py b/src/plugins/built_in/core_actions/emoji.py index 25e09d8d6..0944591be 100644 --- a/src/plugins/built_in/core_actions/emoji.py +++ b/src/plugins/built_in/core_actions/emoji.py @@ -8,10 +8,9 @@ from src.plugin_system import BaseAction, ActionActivationType, ChatMode from src.common.logger import get_logger # 导入API模块 - 标准Python包方式 -from src.plugin_system.apis import emoji_api, llm_api, message_api - -# 注释:不再需要导入NoReplyAction,因为计数器管理已移至heartFC_chat.py -# from src.plugins.built_in.core_actions.no_reply import NoReplyAction +from src.plugin_system.apis import llm_api, message_api +from src.chat.emoji_system.emoji_manager import get_emoji_manager +from src.chat.utils.utils_image import image_path_to_base64 from src.config.config import global_config @@ -60,7 +59,6 @@ class EmojiAction(BaseAction): associated_types = ["emoji"] async def execute(self) -> Tuple[bool, str]: - # sourcery skip: assign-if-exp, introduce-default-else, swap-if-else-branches, use-named-expression """执行表情动作""" logger.info(f"{self.log_prefix} 决定发送表情") @@ -69,30 +67,46 @@ class EmojiAction(BaseAction): reason = self.action_data.get("reason", "表达当前情绪") logger.info(f"{self.log_prefix} 发送表情原因: {reason}") - # 2. 随机获取20个表情包 - sampled_emojis = await emoji_api.get_random(30) - if not sampled_emojis: - logger.warning(f"{self.log_prefix} 无法获取随机表情包") - return False, "无法获取随机表情包" + # 2. 获取所有表情包 + emoji_manager = get_emoji_manager() + all_emojis_obj = [e for e in emoji_manager.emoji_objects if not e.is_deleted] + if not all_emojis_obj: + logger.warning(f"{self.log_prefix} 无法获取任何表情包") + return False, "无法获取任何表情包" - # 3. 准备情感数据 + # 3. 准备情感数据和后备列表 emotion_map = {} - for b64, desc, emo in sampled_emojis: - if emo not in emotion_map: - emotion_map[emo] = [] - emotion_map[emo].append((b64, desc)) + 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 + all_emojis_data.append((b64, desc)) + + 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(sampled_emojis) + 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: - # 使用message_api构建可读的消息字符串 messages_text = message_api.build_readable_messages( messages=recent_messages, timestamp_mode="normal_no_YMD", @@ -118,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("utils_small") if not chat_model_config: logger.error(f"{self.log_prefix} 未找到'utils_small'模型配置,无法调用LLM") return False, "未找到'utils_small'模型配置" @@ -128,21 +142,20 @@ class EmojiAction(BaseAction): ) if not success: - logger.error(f"{self.log_prefix} LLM调用失败: {chosen_emotion}") - return False, f"LLM调用失败: {chosen_emotion}" - - chosen_emotion = chosen_emotion.strip().replace('"', "").replace("'", "") - logger.info(f"{self.log_prefix} LLM选择的情感: {chosen_emotion}") - - # 6. 根据选择的情感匹配表情包 - 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}") + 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} LLM选择的情感 '{chosen_emotion}' 不在可用列表中, 将随机选择一个表情包" - ) - emoji_base64, emoji_description, _ = random.choice(sampled_emojis) + 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}") + else: + logger.warning( + f"{self.log_prefix} LLM选择的情感 '{chosen_emotion}' 不在可用列表中, 将随机选择一个表情包" + ) + emoji_base64, emoji_description = random.choice(all_emojis_data) # 7. 发送表情包 success = await self.send_emoji(emoji_base64) @@ -151,9 +164,6 @@ class EmojiAction(BaseAction): logger.error(f"{self.log_prefix} 表情包发送失败") return False, "表情包发送失败" - # 注释:重置NoReplyAction的连续计数器现在由heartFC_chat.py统一管理 - # NoReplyAction.reset_consecutive_count() - return True, f"发送表情包: {emoji_description}" except Exception as e: