diff --git a/src/chat/planner_actions/action_manager.py b/src/chat/planner_actions/action_manager.py index f52e40657..502ee396a 100644 --- a/src/chat/planner_actions/action_manager.py +++ b/src/chat/planner_actions/action_manager.py @@ -277,6 +277,11 @@ class ChatterActionManager: logger.debug(f"{log_prefix} 并行执行:回复生成任务已被取消") return {"action_type": "reply", "success": False, "reply_text": "", "loop_info": None} + # 从action_data中提取should_quote_reply参数 + should_quote_reply = None + if action_data and isinstance(action_data, dict): + should_quote_reply = action_data.get("should_quote_reply", None) + # 发送并存储回复 loop_info, reply_text, cycle_timers_reply = await self._send_and_store_reply( chat_stream, @@ -286,6 +291,7 @@ class ChatterActionManager: {}, # cycle_timers thinking_id, [], # actions + should_quote_reply, # 传递should_quote_reply参数 ) # 记录回复动作到目标消息 @@ -474,6 +480,7 @@ class ChatterActionManager: cycle_timers: dict[str, float], thinking_id, actions, + should_quote_reply: bool | None = None, ) -> tuple[dict[str, Any], str, dict[str, float]]: """ 发送并存储回复信息 @@ -486,13 +493,16 @@ class ChatterActionManager: cycle_timers: 循环计时器 thinking_id: 思考ID actions: 动作列表 + should_quote_reply: 是否应该引用回复原消息,None表示自动决定 Returns: Tuple[Dict[str, Any], str, Dict[str, float]]: 循环信息, 回复文本, 循环计时器 """ # 发送回复 with Timer("回复发送", cycle_timers): - reply_text = await self.send_response(chat_stream, response_set, loop_start_time, action_message) + reply_text = await self.send_response( + chat_stream, response_set, loop_start_time, action_message, should_quote_reply + ) # 存储reply action信息 person_info_manager = get_person_info_manager() @@ -551,16 +561,18 @@ class ChatterActionManager: return loop_info, reply_text, cycle_timers - async def send_response(self, chat_stream, reply_set, thinking_start_time, message_data) -> str: + async def send_response( + self, chat_stream, reply_set, thinking_start_time, message_data, should_quote_reply: bool | None = None + ) -> str: """ 发送回复内容的具体实现 Args: chat_stream: ChatStream实例 reply_set: 回复内容集合,包含多个回复段 - reply_to: 回复目标 thinking_start_time: 思考开始时间 message_data: 消息数据 + should_quote_reply: 是否应该引用回复原消息,None表示自动决定 Returns: str: 完整的回复文本 @@ -614,12 +626,24 @@ class ChatterActionManager: # 发送第一段回复 if not first_replied: - # 私聊场景不使用引用回复(因为只有两个人对话,引用是多余的) - # 群聊场景使用引用回复(帮助定位回复的目标消息) + # 决定是否引用回复 is_private_chat = not bool(chat_stream.group_info) - set_reply_flag = bool(message_data) and not is_private_chat + + # 如果明确指定了should_quote_reply,则使用指定值 + if should_quote_reply is not None: + set_reply_flag = should_quote_reply and bool(message_data) + logger.debug( + f"📤 [ActionManager] 使用planner指定的引用设置: should_quote_reply={should_quote_reply}" + ) + else: + # 否则使用默认逻辑:默认不引用,让对话更流畅自然 + set_reply_flag = False + logger.debug( + f"📤 [ActionManager] 使用默认引用逻辑: 默认不引用(is_private={is_private_chat})" + ) + logger.debug( - f"📤 [ActionManager] 准备发送第一段回复。message_data: {message_data}, is_private: {is_private_chat}, set_reply: {set_reply_flag}" + f"📤 [ActionManager] 准备发送第一段回复。message_data: {message_data}, set_reply: {set_reply_flag}" ) await send_api.text_to_stream( text=data, diff --git a/src/common/data_models/info_data_model.py b/src/common/data_models/info_data_model.py index e9ed04162..d53921c4b 100644 --- a/src/common/data_models/info_data_model.py +++ b/src/common/data_models/info_data_model.py @@ -27,6 +27,7 @@ class ActionPlannerInfo(BaseDataModel): action_data: dict | None = None action_message: Optional["DatabaseMessages"] = None available_actions: dict[str, "ActionInfo"] | None = None + should_quote_reply: bool | None = None # 是否应该引用回复原消息,None表示由系统自动决定 @dataclass 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 55052a93d..36b0aa357 100644 --- a/src/plugins/built_in/affinity_flow_chatter/plan_executor.py +++ b/src/plugins/built_in/affinity_flow_chatter/plan_executor.py @@ -197,15 +197,24 @@ class ChatterPlanExecutor: "reply_content": "", } # 构建回复动作参数 + action_data = action_info.action_data or {} + + # 如果action_info中有should_quote_reply且action_data中没有,则添加到action_data中 + if action_info.should_quote_reply is not None and "should_quote_reply" not in action_data: + action_data["should_quote_reply"] = action_info.should_quote_reply + action_params = { "chat_id": plan.chat_id, "target_message": action_info.action_message, "reasoning": action_info.reasoning, - "action_data": action_info.action_data or {}, + "action_data": action_data, "clear_unread_messages": clear_unread, } - logger.debug(f"📬 [PlanExecutor] 准备调用 ActionManager,target_message: {action_info.action_message}") + logger.debug( + f"📬 [PlanExecutor] 准备调用 ActionManager,target_message: {action_info.action_message}, " + f"should_quote_reply: {action_info.should_quote_reply}" + ) # 通过动作管理器执行回复 execution_result = await self.action_manager.execute_action( diff --git a/src/plugins/built_in/affinity_flow_chatter/plan_filter.py b/src/plugins/built_in/affinity_flow_chatter/plan_filter.py index 9efd795a5..ec11c30f6 100644 --- a/src/plugins/built_in/affinity_flow_chatter/plan_filter.py +++ b/src/plugins/built_in/affinity_flow_chatter/plan_filter.py @@ -607,6 +607,14 @@ class ChatterPlanFilter: except Exception: logger.warning("无法将目标消息转换为DatabaseMessages对象") + # 从action_data中提取should_quote_reply参数 + should_quote_reply = action_data.get("should_quote_reply", None) + # 将should_quote_reply转换为布尔值(如果是字符串的话) + if isinstance(should_quote_reply, str): + should_quote_reply = should_quote_reply.lower() in ["true", "1", "yes"] + elif not isinstance(should_quote_reply, bool): + should_quote_reply = None + parsed_actions.append( ActionPlannerInfo( action_type=action, @@ -614,6 +622,7 @@ class ChatterPlanFilter: action_data=action_data, action_message=action_message_obj, available_actions=plan.available_actions, + should_quote_reply=should_quote_reply, # 传递should_quote_reply参数 ) ) except Exception as e: diff --git a/src/plugins/built_in/affinity_flow_chatter/planner.py b/src/plugins/built_in/affinity_flow_chatter/planner.py index 8fc75b4ef..128b309eb 100644 --- a/src/plugins/built_in/affinity_flow_chatter/planner.py +++ b/src/plugins/built_in/affinity_flow_chatter/planner.py @@ -319,6 +319,7 @@ class ChatterActionPlanner: reasoning="Normal模式: 兴趣度达到阈值,直接回复", action_data={"target_message_id": target_message.message_id}, action_message=target_message, + should_quote_reply=False, # Normal模式默认不引用回复,保持对话流畅 ) # Normal模式下直接构建最小化的Plan,跳过generator和action_modifier 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 0e760fc15..212c15f9d 100644 --- a/src/plugins/built_in/affinity_flow_chatter/planner_prompts.py +++ b/src/plugins/built_in/affinity_flow_chatter/planner_prompts.py @@ -81,38 +81,56 @@ def init_prompts(): "reasoning": "选择该动作的理由", "action_data": {{ "target_message_id": "m123", - "content": "你的回复内容" + "content": "你的回复内容", + "should_quote_reply": false }} }} ] }} ``` -示例(多重回复,并行): +示例(多重回复,并行 - 需要区分回复对象时才引用): ```json {{ "thinking": "在这里写下你的思绪流...", "actions": [ {{ "action_type": "reply", - "reasoning": "理由A", + "reasoning": "理由A - 这个消息较早且需要明确回复对象", "action_data": {{ "target_message_id": "m124", - "content": "对A的回复" + "content": "对A的回复", + "should_quote_reply": true }} }}, {{ "action_type": "reply", - "reasoning": "理由B", + "reasoning": "理由B - 这是对最新消息的自然接续", "action_data": {{ "target_message_id": "m125", - "content": "对B的回复" + "content": "对B的回复", + "should_quote_reply": false }} }} ] }} ``` +# 引用回复控制(should_quote_reply) +在群聊中回复消息时,你可以通过 `should_quote_reply` 参数控制是否引用原消息: +- **true**: 明确引用原消息(适用于需要明确指向特定消息时,如回答问题、回应多人之一、回复较早的消息) +- **false**: 不引用原消息(适用于自然对话流、接续最新话题、轻松闲聊等场景) +- **不填写**: 系统将自动决定(默认不引用,让对话更流畅) + +**【重要】默认策略:大多数情况下应该使用 `false` 以保持对话自然流畅** + +**使用建议**: +- 当对话自然流畅、你的回复是接续最新话题时,**建议明确设为 `false`** 以避免打断对话节奏 +- 当需要明确回复某个特定用户或特定问题时,设为 `true` 以帮助定位 +- 当群聊中多人同时发言,你要回复其中一个较早的消息(非最新消息)时,设为 `true` +- 当有人直接@你或明确向你提问时,可以考虑设为 `true` 表明你在回复他 +- 私聊场景**必须**设为 `false` 或不填(因为只有两个人,引用是多余的) + # 强制规则 - 需要目标消息的动作(reply/poke_user/set_emoji_like 等),必须提供准确的 target_message_id(来自未读历史里的 标签)。 - 当动作需要额外参数时,必须在 action_data 中补全。