From f6ce69c45b6f0b3f31bce7fb2fa463e3db5c6c46 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Tue, 30 Sep 2025 16:25:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(chat):=20=E5=AE=9E=E7=8E=B0=E5=A4=9A?= =?UTF-8?q?=E9=87=8D=E5=9B=9E=E5=A4=8D=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=85=81?= =?UTF-8?q?=E8=AE=B8=E5=8D=95=E8=BD=AE=E5=A4=84=E7=90=86=E5=A4=9A=E6=9D=A1?= =?UTF-8?q?=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本次更新引入了核心的多重回复功能,使得 Bot 能够在单次规划中响应多条不同的未读消息。这显著提升了其在活跃群聊中的交互效率和上下文处理能力。 主要变更包括: - **Planner Prompts 更新**: - 增加了明确的指令和示例,引导 LLM 在检测到多个需要回应的消息时,生成一个包含多个 `reply` 动作的 `actions` 列表。 - **PlanExecutor 逻辑增强**: - `_execute_reply_actions` 方法被重构,以串行方式执行计划中的所有回复动作。 - 引入了新的控制逻辑,确保只有在执行完最后一个 `reply` 动作后,才会清除未读消息队列。 - **ActionManager 功能扩展**: - `execute_action` 方法新增 `clear_unread_messages` 参数(默认为 `True`),允许调用方控制是否在动作执行后清空未读消息。 - `PlanExecutor` 在调用非 `reply` 动作或非最后一个 `reply` 动作时,会显式将此参数设为 `False`,防止过早清除消息。 --- src/chat/planner_actions/action_manager.py | 8 ++-- .../affinity_flow_chatter/plan_executor.py | 39 ++++++++++++---- .../affinity_flow_chatter/planner_prompts.py | 44 +++++++++++++++++-- 3 files changed, 76 insertions(+), 15 deletions(-) diff --git a/src/chat/planner_actions/action_manager.py b/src/chat/planner_actions/action_manager.py index 28d64f94c..f92956bc2 100644 --- a/src/chat/planner_actions/action_manager.py +++ b/src/chat/planner_actions/action_manager.py @@ -145,6 +145,7 @@ class ChatterActionManager: action_data: Optional[dict] = None, thinking_id: Optional[str] = None, log_prefix: str = "", + clear_unread_messages: bool = True, ) -> Any: """ 执行单个动作的通用函数 @@ -217,7 +218,8 @@ class ChatterActionManager: if success: await self._record_action_to_message(chat_stream, action_name, target_message, action_data) # 自动清空所有未读消息 - await self._clear_all_unread_messages(chat_stream.stream_id, action_name) + if clear_unread_messages: + await self._clear_all_unread_messages(chat_stream.stream_id, action_name) # 重置打断计数 await self._reset_interruption_count_after_action(chat_stream.stream_id) @@ -262,8 +264,8 @@ class ChatterActionManager: # 记录回复动作到目标消息 await self._record_action_to_message(chat_stream, "reply", target_message, action_data) - # 自动清空所有未读消息 - await self._clear_all_unread_messages(chat_stream.stream_id, "reply") + if clear_unread_messages: + await self._clear_all_unread_messages(chat_stream.stream_id, "reply") # 回复成功,重置打断计数 await self._reset_interruption_count_after_action(chat_stream.stream_id) diff --git a/src/plugins/built_in/affinity_flow_chatter/plan_executor.py b/src/plugins/built_in/affinity_flow_chatter/plan_executor.py index 3aa1a28c0..821d52bd6 100644 --- a/src/plugins/built_in/affinity_flow_chatter/plan_executor.py +++ b/src/plugins/built_in/affinity_flow_chatter/plan_executor.py @@ -111,16 +111,26 @@ class ChatterPlanExecutor: } async def _execute_reply_actions(self, reply_actions: List[ActionPlannerInfo], plan: Plan) -> Dict[str, any]: - """执行回复动作""" + """串行执行所有回复动作""" results = [] + total_actions = len(reply_actions) + if total_actions > 1: + logger.info(f"[多重回复] 开始执行 {total_actions} 个回复任务。") - for action_info in reply_actions: - result = await self._execute_single_reply_action(action_info, plan) + for i, action_info in enumerate(reply_actions): + is_last_action = i == total_actions - 1 + if total_actions > 1: + logger.info(f"[多重回复] 正在执行第 {i+1}/{total_actions} 个回复...") + + # 传递 clear_unread 参数 + result = await self._execute_single_reply_action(action_info, plan, clear_unread=is_last_action) results.append(result) + if total_actions > 1: + logger.info(f"[多重回复] 所有回复任务执行完毕。") return {"results": results} - async def _execute_single_reply_action(self, action_info: ActionPlannerInfo, plan: Plan) -> Dict[str, any]: + async def _execute_single_reply_action(self, action_info: ActionPlannerInfo, plan: Plan, clear_unread: bool = True) -> Dict[str, any]: """执行单个回复动作""" start_time = time.time() success = False @@ -152,17 +162,29 @@ class ChatterPlanExecutor: "target_message": action_info.action_message, "reasoning": action_info.reasoning, "action_data": action_info.action_data or {}, + "clear_unread_messages": clear_unread, } logger.debug(f"📬 [PlanExecutor] 准备调用 ActionManager,target_message: {action_info.action_message}") # 通过动作管理器执行回复 - reply_content = await self.action_manager.execute_action( + execution_result = await self.action_manager.execute_action( action_name=action_info.action_type, **action_params ) + + # 从返回结果中提取真正的回复文本 + if isinstance(execution_result, dict): + reply_content = execution_result.get("reply_text", "") + success = execution_result.get("success", False) + else: + # 兼容旧的返回值(虽然可能性不大) + reply_content = str(execution_result) if execution_result else "" + success = bool(reply_content) - success = True - logger.info(f"回复动作 '{action_info.action_type}' 执行成功。") + if success: + logger.info(f"回复动作 '{action_info.action_type}' 执行成功。") + else: + raise Exception(execution_result.get("error", "未知错误")) except Exception as e: error_message = str(e) @@ -181,7 +203,7 @@ class ChatterPlanExecutor: "error_message": error_message, "execution_time": execution_time, "reasoning": action_info.reasoning, - "reply_content": reply_content[:200] + "..." if len(reply_content) > 200 else reply_content, + "reply_content": reply_content[:200] + "..." if reply_content and len(reply_content) > 200 else reply_content, } async def _execute_other_actions(self, other_actions: List[ActionPlannerInfo], plan: Plan) -> Dict[str, any]: @@ -251,6 +273,7 @@ class ChatterPlanExecutor: "target_message": action_info.action_message, "reasoning": action_info.reasoning, "action_data": action_data, + "clear_unread_messages": False, # 其他动作不应清除未读消息 } # 通过动作管理器执行动作 diff --git a/src/plugins/built_in/affinity_flow_chatter/planner_prompts.py b/src/plugins/built_in/affinity_flow_chatter/planner_prompts.py index c8f448067..fdf07b296 100644 --- a/src/plugins/built_in/affinity_flow_chatter/planner_prompts.py +++ b/src/plugins/built_in/affinity_flow_chatter/planner_prompts.py @@ -44,10 +44,11 @@ def init_prompts(): 1. **重要:已读历史消息仅作为当前聊天情景的参考,帮助你理解对话上下文。** 2. **重要:所有动作的执行对象只能是未读历史消息中的消息,不能对已读消息执行动作。** 3. 在未读历史消息中,优先对兴趣值高的消息做出动作(兴趣值标注在消息末尾)。 -4. 首先,决定是否要对未读消息进行 `reply`(如果有)。 -5. 然后,评估当前的对话气氛和用户情绪,判断是否需要一个**辅助动作**来让你的回应更生动、更符合你的性格。 -6. 如果需要,选择一个最合适的辅助动作与 `reply`(如果有) 组合。 -7. 如果用户明确要求了某个动作,请务必优先满足。 +4. **核心:如果有多条未读消息都需要回应(例如多人@你),你应该并行处理,在`actions`列表中生成多个`reply`动作。** +5. 首先,决定是否要对未读消息进行 `reply`(如果有)。 +6. 然后,评估当前的对话气氛和用户情绪,判断是否需要一个**辅助动作**来让你的回应更生动、更符合你的性格。 +7. 如果需要,选择一个最合适的辅助动作与 `reply`(如果有) 组合。 +8. 如果用户明确要求了某个动作,请务必优先满足。 **重要提醒:** - **回复消息时必须遵循对话的流程,不要重复已经说过的话。** @@ -70,6 +71,7 @@ def init_prompts(): ## 可用动作列表 {action_options_text} +### 单动作示例: ```json {{ "thinking": "在这里写下你的思绪流...", @@ -86,6 +88,40 @@ def init_prompts(): }} ``` +### **多重回复示例 (核心功能)** +当有多人与你互动时,你需要同时回应他们,甚至可以同时处理三个! +```json +{{ + "thinking": "哇,群里好热闹呀!张三、李四、王五都在@我!让我看看...张三在问我昨天推荐的电影好不好看,这个得好好分享一下观后感。李四在说他家的猫咪学会了新技能,好可爱,得夸夸他!王五好像遇到点麻烦,在问我一个技术问题,这个得优先、详细地解答一下!得一个个来!", + "actions": [ + {{ + "action_type": "reply", + "reasoning": "回应张三关于电影的提问,并分享我的看法。", + "action_data": {{ + "target_message_id": "m124", + "content": "张三!你问的那部电影我昨天也看啦,真的超赞!特别是最后那个反转,简直让人意想不到!" + }} + }}, + {{ + "action_type": "reply", + "reasoning": "回应李四分享的趣事,表达赞美和羡慕。", + "action_data": {{ + "target_message_id": "m125", + "content": "哇,李四你家猫咪也太聪明了吧!居然会握手了!好羡慕呀!" + }} + }}, + {{ + "action_type": "reply", + "reasoning": "优先回应王五的技术求助,并提供详细的解答。", + "action_data": {{ + "target_message_id": "m126", + "content": "王五别急,你说的那个问题我之前也遇到过。你试试看是不是配置文件里的`enable_magic`选项没有设置成`true`?如果还不行你再把错误截图发我看看。" + }} + }} + ] +}} +``` + **强制规则**: - 对于每一个需要目标消息的动作(如`reply`, `poke_user`, `set_emoji_like`),你 **必须** 在`action_data`中提供准确的`target_message_id`,这个ID来源于`## 未读历史消息`中消息前的``标签。 - 当你选择的动作需要参数时(例如 `set_emoji_like` 需要 `emoji` 参数),你 **必须** 在 `action_data` 中提供所有必需的参数及其对应的值。