From 22f979abb59c8a907b04e4df3888ee85d97a9ccd Mon Sep 17 00:00:00 2001 From: Bakadax Date: Fri, 16 May 2025 11:34:40 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E9=87=8D=E6=9E=84=20action=20=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=20message=5Fsender=20segments=20=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/experimental/PFC/PFC_idle/idle_chat.py | 5 +- src/experimental/PFC/action_factory.py | 62 ++ src/experimental/PFC/action_handlers.py | 655 +++++++++++++ src/experimental/PFC/actions.py | 1021 ++------------------ src/experimental/PFC/message_sender.py | 14 +- 5 files changed, 835 insertions(+), 922 deletions(-) create mode 100644 src/experimental/PFC/action_factory.py create mode 100644 src/experimental/PFC/action_handlers.py diff --git a/src/experimental/PFC/PFC_idle/idle_chat.py b/src/experimental/PFC/PFC_idle/idle_chat.py index dff5d954..1b551a32 100644 --- a/src/experimental/PFC/PFC_idle/idle_chat.py +++ b/src/experimental/PFC/PFC_idle/idle_chat.py @@ -19,7 +19,7 @@ from src.chat.utils.chat_message_builder import build_readable_messages from ..chat_observer import ChatObserver from ..message_sender import DirectMessageSender from src.chat.message_receive.chat_stream import ChatStream, chat_manager -from maim_message import UserInfo +from maim_message import UserInfo, Seg from ..pfc_relationship import PfcRepationshipTranslator from rich.traceback import install @@ -534,8 +534,9 @@ class IdleChat: # 发送消息 try: + segments = Seg(type="seglist", data=[Seg(type="text", data=content)]) logger.debug(f"[私聊][{self.private_name}]准备发送主动聊天消息: {content}") - await self.message_sender.send_message(chat_stream=chat_stream, content=content, reply_to_message=None) + await self.message_sender.send_message(chat_stream=chat_stream, segments=segments, reply_to_message=None, content=content) logger.info(f"[私聊][{self.private_name}]成功主动发起聊天: {content}") except Exception as e: logger.error(f"[私聊][{self.private_name}]发送主动聊天消息失败: {str(e)}") diff --git a/src/experimental/PFC/action_factory.py b/src/experimental/PFC/action_factory.py new file mode 100644 index 00000000..e04b88d0 --- /dev/null +++ b/src/experimental/PFC/action_factory.py @@ -0,0 +1,62 @@ +from abc import ABC, abstractmethod +from typing import Optional, Type, TYPE_CHECKING + +# 从 action_handlers.py 导入具体的处理器类 +from .action_handlers import ( # 调整导入路径 + ActionHandler, + DirectReplyHandler, + SendNewMessageHandler, + SayGoodbyeHandler, + SendMemesHandler, + RethinkGoalHandler, + ListeningHandler, + EndConversationHandler, + BlockAndIgnoreHandler, + WaitHandler, + UnknownActionHandler, +) + +if TYPE_CHECKING: + from PFC.conversation import Conversation # 调整导入路径 + +class AbstractActionFactory(ABC): + """抽象动作工厂接口。""" + @abstractmethod + def create_action_handler(self, action_type: str, conversation: "Conversation") -> ActionHandler: + """ + 根据动作类型创建并返回相应的动作处理器。 + + 参数: + action_type (str): 动作的类型字符串。 + conversation (Conversation): 当前对话实例。 + + 返回: + ActionHandler: 对应动作类型的处理器实例。 + """ + pass + +class StandardActionFactory(AbstractActionFactory): + """标准的动作工厂实现。""" + def create_action_handler(self, action_type: str, conversation: "Conversation") -> ActionHandler: + """ + 根据动作类型创建并返回具体的动作处理器实例。 + """ + # 动作类型到处理器类的映射 + handler_map: dict[str, Type[ActionHandler]] = { + "direct_reply": DirectReplyHandler, + "send_new_message": SendNewMessageHandler, + "say_goodbye": SayGoodbyeHandler, + "send_memes": SendMemesHandler, + "rethink_goal": RethinkGoalHandler, + "listening": ListeningHandler, + "end_conversation": EndConversationHandler, + "block_and_ignore": BlockAndIgnoreHandler, + "wait": WaitHandler, + } + handler_class = handler_map.get(action_type) # 获取对应的处理器类 + # 如果找到对应的处理器类 + if handler_class: + return handler_class(conversation) # 创建并返回处理器实例 + else: + # 如果未找到,返回处理未知动作的默认处理器 + return UnknownActionHandler(conversation) \ No newline at end of file diff --git a/src/experimental/PFC/action_handlers.py b/src/experimental/PFC/action_handlers.py new file mode 100644 index 00000000..d939ba41 --- /dev/null +++ b/src/experimental/PFC/action_handlers.py @@ -0,0 +1,655 @@ +from abc import ABC, abstractmethod +import time +import asyncio +import datetime +import traceback +import json +from typing import Optional, Set, TYPE_CHECKING, List, Tuple # 确保导入 List 和 Tuple + +from src.chat.emoji_system.emoji_manager import emoji_manager +from src.common.logger_manager import get_logger +from src.config.config import global_config +from src.chat.utils.chat_message_builder import build_readable_messages +from PFC.pfc_types import ConversationState +from PFC.observation_info import ObservationInfo +from PFC.conversation_info import ConversationInfo +from src.chat.utils.utils_image import image_path_to_base64 +from maim_message import Seg, UserInfo +from src.chat.message_receive.message import MessageSending, MessageSet +from src.chat.message_receive.message_sender import message_manager +# PFC.message_sender 已经包含 DirectMessageSender,这里不再需要单独导入 + +if TYPE_CHECKING: + from PFC.conversation import Conversation + +logger = get_logger("pfc_action_handlers") + + +class ActionHandler(ABC): + """处理动作的抽象基类。""" + def __init__(self, conversation: "Conversation"): + self.conversation = conversation + self.logger = logger + + @abstractmethod + async def execute( + self, + reason: str, + observation_info: Optional[ObservationInfo], + conversation_info: Optional[ConversationInfo], + action_start_time: float, + current_action_record: dict + ) -> tuple[bool, str, str]: + """ + 执行动作。 + + 返回: + 一个元组,包含: + - action_successful (bool): 动作是否成功。 + - final_status (str): 动作的最终状态。 + - final_reason (str): 最终状态的原因。 + """ + pass + + async def _send_reply_or_segments(self, segments_data: list[Seg], content_for_log: str) -> bool: + """ + 辅助函数,用于发送消息(文本或图片段)。 + """ + if not self.conversation.direct_sender: + self.logger.error(f"[私聊][{self.conversation.private_name}] DirectMessageSender 未初始化,无法发送。") + return False + if not self.conversation.chat_stream: + self.logger.error(f"[私聊][{self.conversation.private_name}] ChatStream 未初始化,无法发送。") + return False + + try: + final_segments = Seg(type="seglist", data=segments_data) + await self.conversation.direct_sender.send_message( + chat_stream=self.conversation.chat_stream, + segments=final_segments, + reply_to_message=None, + content=content_for_log + ) + if self.conversation.conversation_info: + self.conversation.conversation_info.my_message_count += 1 + self.conversation.state = ConversationState.ANALYZING + return True + except Exception as e: + self.logger.error(f"[私聊][{self.conversation.private_name}] 发送消息时失败: {str(e)}") + self.logger.error(f"[私聊][{self.conversation.private_name}] {traceback.format_exc()}") + self.conversation.state = ConversationState.ERROR + return False + + async def _update_bot_message_in_history( + self, + send_time: float, + message_content: str, # 对于图片,这应该是描述性的文本 + observation_info: ObservationInfo, + message_id_prefix: str = "bot_sent_" + ): + """在机器人发送消息后,更新 ObservationInfo 中的聊天记录。""" + if not self.conversation.bot_qq_str: + self.logger.warning(f"[私聊][{self.conversation.private_name}] Bot QQ ID 未知,无法更新机器人消息历史。") + return + + bot_message_dict = { + "message_id": f"{message_id_prefix}{send_time}", + "time": send_time, + "user_info": { + "user_id": self.conversation.bot_qq_str, + "user_nickname": global_config.BOT_NICKNAME, + "platform": self.conversation.chat_stream.platform if self.conversation.chat_stream else "unknown_platform", + }, + "processed_plain_text": message_content, + "detailed_plain_text": message_content, + } + observation_info.chat_history.append(bot_message_dict) + observation_info.chat_history_count = len(observation_info.chat_history) + self.logger.debug( + f"[私聊][{self.conversation.private_name}] {global_config.BOT_NICKNAME}发送的消息已添加到 chat_history。当前历史数: {observation_info.chat_history_count}" + ) + + max_history_len = getattr(global_config, "pfc_max_chat_history_for_checker", 50) + if len(observation_info.chat_history) > max_history_len: + observation_info.chat_history = observation_info.chat_history[-max_history_len:] + observation_info.chat_history_count = len(observation_info.chat_history) + + history_slice_for_str = observation_info.chat_history[-30:] + try: + observation_info.chat_history_str = await build_readable_messages( + history_slice_for_str, + replace_bot_name=True, + merge_messages=False, + timestamp_mode="relative", + read_mark=0.0, + ) + except Exception as e_build_hist: + self.logger.error(f"[私聊][{self.conversation.private_name}] 更新 chat_history_str 时出错: {e_build_hist}") + observation_info.chat_history_str = "[构建聊天记录出错]" + + async def _update_post_send_states( + self, + send_time: float, + observation_info: ObservationInfo, + conversation_info: ConversationInfo, + action_type: str, # "direct_reply", "send_new_message", "say_goodbye", "send_memes" + event_description_for_emotion: str + ): + """处理发送消息成功后的通用状态更新。""" + if self.conversation.idle_chat: + await self.conversation.idle_chat.update_last_message_time(send_time) + + # 清理已处理的未读消息 (只清理在发送这条回复之前的、来自他人的消息) + current_unprocessed_messages = getattr(observation_info, "unprocessed_messages", []) + message_ids_to_clear: Set[str] = set() + timestamp_before_sending = send_time - 0.001 # 确保是发送前的时间 + for msg in current_unprocessed_messages: + msg_time = msg.get("time") + msg_id = msg.get("message_id") + sender_id_info = msg.get("user_info", {}) + sender_id = str(sender_id_info.get("user_id")) if sender_id_info else None + + if ( + msg_id + and msg_time + and sender_id != self.conversation.bot_qq_str + and msg_time < timestamp_before_sending + ): + message_ids_to_clear.add(msg_id) + + if message_ids_to_clear: + self.logger.debug( + f"[私聊][{self.conversation.private_name}] 准备清理 {len(message_ids_to_clear)} 条发送前(他人)消息: {message_ids_to_clear}" + ) + await observation_info.clear_processed_messages(message_ids_to_clear) + else: + self.logger.debug(f"[私聊][{self.conversation.private_name}] 没有需要清理的发送前(他人)消息。") + + # 更新追问状态 + other_new_msg_count_during_planning = getattr( + conversation_info, "other_new_messages_during_planning_count", 0 + ) + if action_type in ["direct_reply", "send_new_message", "send_memes"]: + if other_new_msg_count_during_planning > 0 and action_type == "direct_reply": + self.logger.debug( + f"[私聊][{self.conversation.private_name}] 因规划期间收到 {other_new_msg_count_during_planning} 条他人新消息,下一轮强制使用【初始回复】逻辑。" + ) + conversation_info.last_successful_reply_action = None + else: + self.logger.debug( + f"[私聊][{self.conversation.private_name}] 成功执行 '{action_type}', 下一轮【允许】使用追问逻辑。" + ) + conversation_info.last_successful_reply_action = action_type + + # 更新实例消息计数和关系/情绪 + conversation_info.current_instance_message_count += 1 + self.logger.debug( + f"[私聊][{self.conversation.private_name}] 实例消息计数({global_config.BOT_NICKNAME}发送后)增加到: {conversation_info.current_instance_message_count}" + ) + await self._update_relationship_and_emotion(observation_info, conversation_info, event_description_for_emotion) + + async def _update_relationship_and_emotion( + self, + observation_info: ObservationInfo, + conversation_info: ConversationInfo, + event_description: str + ): + """辅助方法:更新关系和情绪状态。""" + if self.conversation.relationship_updater and self.conversation.chat_observer: + await self.conversation.relationship_updater.update_relationship_incremental( + conversation_info=conversation_info, + observation_info=observation_info, + chat_observer_for_history=self.conversation.chat_observer, + ) + if self.conversation.emotion_updater and self.conversation.chat_observer: + await self.conversation.emotion_updater.update_emotion_based_on_context( + conversation_info=conversation_info, + observation_info=observation_info, + chat_observer_for_history=self.conversation.chat_observer, + event_description=event_description, + ) + + +class BaseTextReplyHandler(ActionHandler): + """ + 处理基于文本的回复动作的基类,包含生成-检查-重试的循环。 + 适用于 DirectReplyHandler 和 SendNewMessageHandler。 + """ + async def _generate_and_check_text_reply_loop( + self, + action_type: str, # "direct_reply" or "send_new_message" + observation_info: ObservationInfo, + conversation_info: ConversationInfo, + max_attempts: int + ) -> Tuple[bool, Optional[str], str, bool, bool]: + """ + 管理生成文本回复并检查其适用性的循环。 + 对于 send_new_message,它还处理来自 ReplyGenerator 的初始 JSON 决策。 + + 返回: + is_suitable (bool): 是否找到了合适的回复或作出了发送决策。 + generated_content (Optional[str]): 要发送的内容;如果 ReplyGenerator 决定不发送 (send_new_message),则为 None。 + check_reason (str): 检查器或生成失败的原因。 + need_replan_from_checker (bool): 如果检查器要求重新规划。 + should_send_reply_for_new_message (bool): 特定于 send_new_message,如果 ReplyGenerator 决定发送则为 True。 + """ + reply_attempt_count = 0 + is_suitable = False + generated_content_to_send: Optional[str] = None + final_check_reason = "未开始检查" + need_replan = False + # should_send_reply_for_new_message 仅用于 send_new_message 动作类型 + should_send_reply_for_new_message = True if action_type == "direct_reply" else False # direct_reply 总是尝试发送 + + while reply_attempt_count < max_attempts and not is_suitable and not need_replan: + reply_attempt_count += 1 + log_prefix = f"[私聊][{self.conversation.private_name}] 尝试生成/检查 '{action_type}' (第 {reply_attempt_count}/{max_attempts} 次)..." + self.logger.info(log_prefix) + + self.conversation.state = ConversationState.GENERATING + if not self.conversation.reply_generator: + # 这个应该在 Conversation 初始化时就保证了,但以防万一 + raise RuntimeError(f"ReplyGenerator 未为 {self.conversation.private_name} 初始化") + + raw_llm_output = await self.conversation.reply_generator.generate( + observation_info, conversation_info, action_type=action_type + ) + self.logger.debug(f"{log_prefix} ReplyGenerator.generate 返回: '{raw_llm_output}'") + + current_content_for_check = raw_llm_output + + if action_type == "send_new_message": + parsed_json = None + try: + parsed_json = json.loads(raw_llm_output) + except json.JSONDecodeError: + self.logger.error(f"{log_prefix} ReplyGenerator 返回的不是有效的JSON: {raw_llm_output}") + conversation_info.last_reply_rejection_reason = "回复生成器未返回有效JSON" + conversation_info.last_rejected_reply_content = raw_llm_output + should_send_reply_for_new_message = False # 标记不发送 + is_suitable = True # 决策已做出(不发送),所以认为是 "suitable" 以跳出循环 + final_check_reason = "回复生成器JSON解析失败,决定不发送" + generated_content_to_send = None # 明确不发送内容 + break # 跳出重试循环 + + if parsed_json: + send_decision = parsed_json.get("send", "no").lower() + generated_text_from_json = parsed_json.get("txt", "") # 如果不发送,txt可能是"no" + + if send_decision == "yes": + should_send_reply_for_new_message = True + current_content_for_check = generated_text_from_json + self.logger.info(f"{log_prefix} ReplyGenerator 决定发送消息。内容初步为: '{current_content_for_check[:100]}...'") + else: # send_decision is "no" + should_send_reply_for_new_message = False + is_suitable = True # 决策已做出(不发送) + final_check_reason = "回复生成器决定不发送" + generated_content_to_send = None + self.logger.info(f"{log_prefix} ReplyGenerator 决定不发送消息。") + break # 跳出重试循环 + + # 如果是 direct_reply 或者 send_new_message 且决定要发送,则检查内容 + if not current_content_for_check or current_content_for_check.startswith("抱歉") or current_content_for_check.strip() == "" or (action_type == "send_new_message" and current_content_for_check == "no" and should_send_reply_for_new_message): + warning_msg = f"{log_prefix} 生成内容无效或为错误提示" + if action_type == "send_new_message" and current_content_for_check == "no" and should_send_reply_for_new_message: + warning_msg += " (ReplyGenerator决定发送但文本为'no')" + self.logger.warning(warning_msg + ",将进行下一次尝试 (如果适用)。") + final_check_reason = "生成内容无效" + conversation_info.last_reply_rejection_reason = final_check_reason + conversation_info.last_rejected_reply_content = current_content_for_check + await asyncio.sleep(0.5) + continue + + # --- 内容检查 --- + self.conversation.state = ConversationState.CHECKING + if not self.conversation.reply_checker: + raise RuntimeError(f"ReplyChecker 未为 {self.conversation.private_name} 初始化") + + current_goal_str = "" + if conversation_info.goal_list: + goal_item = conversation_info.goal_list[-1] + current_goal_str = goal_item.get("goal", "") if isinstance(goal_item, dict) else str(goal_item) + + chat_history_for_check = getattr(observation_info, "chat_history", []) + chat_history_text_for_check = getattr(observation_info, "chat_history_str", "") + current_time_value_for_check = observation_info.current_time_str or "获取时间失败" + + if global_config.enable_pfc_reply_checker: + self.logger.debug(f"{log_prefix} 调用 ReplyChecker 检查 (配置已启用)...") + is_suitable_check, reason_check, need_replan_check = await self.conversation.reply_checker.check( + reply=current_content_for_check, goal=current_goal_str, + chat_history=chat_history_for_check, chat_history_text=chat_history_text_for_check, + current_time_str=current_time_value_for_check, retry_count=(reply_attempt_count - 1) + ) + self.logger.info( + f"{log_prefix} ReplyChecker 结果: 合适={is_suitable_check}, 原因='{reason_check}', 需重规划={need_replan_check}" + ) + else: + is_suitable_check, reason_check, need_replan_check = True, "ReplyChecker 已通过配置关闭", False + self.logger.debug(f"{log_prefix} [配置关闭] ReplyChecker 已跳过,默认回复为合适。") + + is_suitable = is_suitable_check + final_check_reason = reason_check + need_replan = need_replan_check + + if not is_suitable: + conversation_info.last_reply_rejection_reason = final_check_reason + conversation_info.last_rejected_reply_content = current_content_for_check + if final_check_reason == "机器人尝试发送重复消息" and not need_replan: + self.logger.warning(f"{log_prefix} 回复因自身重复被拒绝。将重试。") + elif not need_replan and reply_attempt_count < max_attempts: + self.logger.warning(f"{log_prefix} 回复不合适: {final_check_reason}。将重试。") + else: # 需要重规划或达到最大次数 + self.logger.warning(f"{log_prefix} 回复不合适且(需要重规划或已达最大次数): {final_check_reason}") + break # 结束循环 + await asyncio.sleep(0.5) # 重试前暂停 + else: # is_suitable is True + generated_content_to_send = current_content_for_check + conversation_info.last_reply_rejection_reason = None + conversation_info.last_rejected_reply_content = None + break # 成功,跳出循环 + + # 确保即使循环结束,如果 should_send_reply_for_new_message 为 False,则 is_suitable 也为 True(表示决策完成) + if action_type == "send_new_message" and not should_send_reply_for_new_message: + is_suitable = True # 决策已完成(不发送) + generated_content_to_send = None # 确认不发送任何内容 + + return is_suitable, generated_content_to_send, final_check_reason, need_replan, should_send_reply_for_new_message + + +class DirectReplyHandler(BaseTextReplyHandler): + """处理直接回复动作的处理器。""" + async def execute( + self, + reason: str, + observation_info: Optional[ObservationInfo], + conversation_info: Optional[ConversationInfo], + action_start_time: float, + current_action_record: dict + ) -> tuple[bool, str, str]: + if not observation_info or not conversation_info: + return False, "error", "DirectReply 的 ObservationInfo 或 ConversationInfo 为空" + + action_successful = False + final_status = "recall" + final_reason = "直接回复动作未成功执行" + max_reply_attempts: int = getattr(global_config, "pfc_max_reply_attempts", 3) + + is_suitable, generated_content, check_reason, need_replan, _ = await self._generate_and_check_text_reply_loop( + action_type="direct_reply", + observation_info=observation_info, + conversation_info=conversation_info, + max_attempts=max_reply_attempts + ) + + if is_suitable and generated_content: + self.conversation.generated_reply = generated_content + timestamp_before_sending = time.time() + self.conversation.state = ConversationState.SENDING + text_segment = Seg(type="text", data=self.conversation.generated_reply) + send_success = await self._send_reply_or_segments([text_segment], self.conversation.generated_reply) + send_end_time = time.time() + + if send_success: + action_successful = True + final_status = "done" + final_reason = "成功发送直接回复" + await self._update_bot_message_in_history(send_end_time, self.conversation.generated_reply, observation_info) + event_desc = f"你直接回复了消息: '{self.conversation.generated_reply[:50]}...'" + await self._update_post_send_states(send_end_time, observation_info, conversation_info, "direct_reply", event_desc) + else: + final_status = "recall"; final_reason = "发送直接回复时失败"; action_successful = False + conversation_info.last_successful_reply_action = None + conversation_info.my_message_count = 0 + elif need_replan: + final_status = "recall"; final_reason = f"回复检查要求重新规划: {check_reason}" + conversation_info.last_successful_reply_action = None + else: # 达到最大尝试次数或生成内容无效 + final_status = "max_checker_attempts_failed" + final_reason = f"达到最大回复尝试次数或生成内容无效,检查原因: {check_reason}" + action_successful = False + conversation_info.last_successful_reply_action = None + + return action_successful, final_status, final_reason + + +class SendNewMessageHandler(BaseTextReplyHandler): + """处理发送新消息动作的处理器。""" + async def execute( + self, + reason: str, + observation_info: Optional[ObservationInfo], + conversation_info: Optional[ConversationInfo], + action_start_time: float, + current_action_record: dict + ) -> tuple[bool, str, str]: + if not observation_info or not conversation_info: + return False, "error", "SendNewMessage 的 ObservationInfo 或 ConversationInfo 为空" + + action_successful = False + final_status = "recall" + final_reason = "发送新消息动作未成功执行" + max_reply_attempts: int = getattr(global_config, "pfc_max_reply_attempts", 3) + + is_suitable, generated_content, check_reason, need_replan, should_send = await self._generate_and_check_text_reply_loop( + action_type="send_new_message", + observation_info=observation_info, + conversation_info=conversation_info, + max_attempts=max_reply_attempts + ) + + if not should_send: # ReplyGenerator 决定不发送 + self.logger.info(f"[私聊][{self.conversation.private_name}] 动作 'send_new_message': ReplyGenerator 决定不发送。原因: {check_reason}") + final_status = "done_no_reply" + final_reason = check_reason if check_reason else "回复生成器决定不发送消息" + action_successful = True # 决策本身是成功的 + conversation_info.last_successful_reply_action = None + conversation_info.my_message_count = 0 + elif is_suitable and generated_content: # 决定发送且内容合适 + self.conversation.generated_reply = generated_content + timestamp_before_sending = time.time() + self.conversation.state = ConversationState.SENDING + text_segment = Seg(type="text", data=self.conversation.generated_reply) + send_success = await self._send_reply_or_segments([text_segment], self.conversation.generated_reply) + send_end_time = time.time() + + if send_success: + action_successful = True + final_status = "done" + final_reason = "成功发送新消息" + await self._update_bot_message_in_history(send_end_time, self.conversation.generated_reply, observation_info) + event_desc = f"你发送了一条新消息: '{self.conversation.generated_reply[:50]}...'" + await self._update_post_send_states(send_end_time, observation_info, conversation_info, "send_new_message", event_desc) + else: + final_status = "recall"; final_reason = "发送新消息时失败"; action_successful = False + conversation_info.last_successful_reply_action = None + conversation_info.my_message_count = 0 + elif need_replan: + final_status = "recall"; final_reason = f"回复检查要求重新规划: {check_reason}" + conversation_info.last_successful_reply_action = None + else: # 达到最大尝试次数或生成内容无效 + final_status = "max_checker_attempts_failed" + final_reason = f"达到最大回复尝试次数或生成内容无效,检查原因: {check_reason}" + action_successful = False + conversation_info.last_successful_reply_action = None + + return action_successful, final_status, final_reason + + +class SayGoodbyeHandler(ActionHandler): + """处理发送告别语动作的处理器。""" + async def execute( + self, + reason: str, + observation_info: Optional[ObservationInfo], + conversation_info: Optional[ConversationInfo], + action_start_time: float, + current_action_record: dict + ) -> tuple[bool, str, str]: + if not observation_info or not conversation_info: + return False, "error", "SayGoodbye 的 ObservationInfo 或 ConversationInfo 为空" + + action_successful = False + final_status = "recall" + final_reason = "告别语动作未成功执行" + + self.conversation.state = ConversationState.GENERATING + if not self.conversation.reply_generator: + raise RuntimeError("ReplyGenerator 未初始化") + + generated_content = await self.conversation.reply_generator.generate( + observation_info, conversation_info, action_type="say_goodbye" + ) + self.logger.info( + f"[私聊][{self.conversation.private_name}] 动作 'say_goodbye': 生成内容: '{generated_content[:100]}...'" + ) + + if not generated_content or generated_content.startswith("抱歉"): + self.logger.warning( + f"[私聊][{self.conversation.private_name}] 动作 'say_goodbye': 生成内容为空或为错误提示,取消发送。" + ) + final_reason = "生成告别内容无效" + final_status = "done" + self.conversation.should_continue = False + action_successful = True # 即使不发送,结束对话的决策也算完成 + else: + self.conversation.generated_reply = generated_content + self.conversation.state = ConversationState.SENDING + text_segment = Seg(type="text", data=self.conversation.generated_reply) + send_success = await self._send_reply_or_segments([text_segment], self.conversation.generated_reply) + send_end_time = time.time() + + if send_success: + action_successful = True + final_status = "done" + final_reason = "成功发送告别语" + self.conversation.should_continue = False + await self._update_bot_message_in_history(send_end_time, self.conversation.generated_reply, observation_info) + event_desc = f"你发送了告别消息: '{self.conversation.generated_reply[:50]}...'" + await self._update_post_send_states(send_end_time, observation_info, conversation_info, "say_goodbye", event_desc) + else: + final_status = "recall"; final_reason = "发送告别语失败"; action_successful = False + self.conversation.should_continue = True # 发送失败则不结束 + + return action_successful, final_status, final_reason + + +class SendMemesHandler(ActionHandler): + """处理发送表情包动作的处理器。""" + async def execute( + self, + reason: str, + observation_info: Optional[ObservationInfo], + conversation_info: Optional[ConversationInfo], + action_start_time: float, + current_action_record: dict + ) -> tuple[bool, str, str]: + if not observation_info or not conversation_info: + return False, "error", "SendMemes 的 ObservationInfo 或 ConversationInfo 为空" + + action_successful = False + final_status = "recall" + final_reason_prefix = "发送表情包" + final_reason = f"{final_reason_prefix}失败:未知原因" + self.conversation.state = ConversationState.GENERATING + + emoji_query = conversation_info.current_emoji_query + if not emoji_query: + final_reason = f"{final_reason_prefix}失败:缺少表情包查询语句" + conversation_info.last_successful_reply_action = None + return False, "recall", final_reason + + self.logger.info(f"[私聊][{self.conversation.private_name}] 动作 'send_memes': 使用查询 '{emoji_query}' 获取表情包...") + try: + emoji_result = await emoji_manager.get_emoji_for_text(emoji_query) + if emoji_result: + emoji_path, emoji_description = emoji_result + self.logger.info(f"获取到表情包: {emoji_path}, 描述: {emoji_description}") + + if not self.conversation.chat_stream: raise RuntimeError("ChatStream 未初始化") + image_b64_content = image_path_to_base64(emoji_path) + if not image_b64_content: raise ValueError(f"无法转换图片 {emoji_path} 为Base64") + + image_segment = Seg(type="image", data={"file": f"base64://{image_b64_content}"}) + log_content_for_meme = f"[表情: {emoji_description}]" + send_success = await self._send_reply_or_segments([image_segment], log_content_for_meme) + send_end_time = time.time() + + if send_success: + action_successful = True + final_status = "done" + final_reason = f"{final_reason_prefix}成功发送 ({emoji_description})" + await self._update_bot_message_in_history(send_end_time, log_content_for_meme, observation_info, "bot_meme_") + event_desc = f"你发送了一个表情包 ({emoji_description})" + await self._update_post_send_states(send_end_time, observation_info, conversation_info, "send_memes", event_desc) + else: + final_status = "recall"; final_reason = f"{final_reason_prefix}失败:发送时出错" + else: + final_reason = f"{final_reason_prefix}失败:未找到合适表情包" + conversation_info.last_successful_reply_action = None + except Exception as e: + self.logger.error(f"处理表情包动作时出错: {e}", exc_info=True) + final_status = "error"; final_reason = f"{final_reason_prefix}失败:处理时出错 ({e})" + conversation_info.last_successful_reply_action = None + + return action_successful, final_status, final_reason + + +class RethinkGoalHandler(ActionHandler): + """处理重新思考目标动作的处理器。""" + async def execute(self, reason: str, observation_info: Optional[ObservationInfo], conversation_info: Optional[ConversationInfo], action_start_time: float, current_action_record: dict) -> tuple[bool, str, str]: + if not conversation_info or not observation_info: return False, "error", "RethinkGoal 缺少信息" + self.conversation.state = ConversationState.RETHINKING + if not self.conversation.goal_analyzer: raise RuntimeError("GoalAnalyzer 未初始化") + await self.conversation.goal_analyzer.analyze_goal(conversation_info, observation_info) + event_desc = "你重新思考了对话目标和方向" + await self._update_relationship_and_emotion(observation_info, conversation_info, event_desc) + return True, "done", "成功重新思考目标" + +class ListeningHandler(ActionHandler): + """处理倾听动作的处理器。""" + async def execute(self, reason: str, observation_info: Optional[ObservationInfo], conversation_info: Optional[ConversationInfo], action_start_time: float, current_action_record: dict) -> tuple[bool, str, str]: + if not conversation_info or not observation_info: return False, "error", "Listening 缺少信息" + self.conversation.state = ConversationState.LISTENING + if not self.conversation.waiter: raise RuntimeError("Waiter 未初始化") + await self.conversation.waiter.wait_listening(conversation_info) + event_desc = "你决定耐心倾听对方的发言" + await self._update_relationship_and_emotion(observation_info, conversation_info, event_desc) + return True, "done", "进入倾听状态" + +class EndConversationHandler(ActionHandler): + """处理结束对话动作的处理器。""" + async def execute(self, reason: str, observation_info: Optional[ObservationInfo], conversation_info: Optional[ConversationInfo], action_start_time: float, current_action_record: dict) -> tuple[bool, str, str]: + self.logger.info(f"[私聊][{self.conversation.private_name}] 动作 'end_conversation': 收到最终结束指令,停止对话...") + self.conversation.should_continue = False + return True, "done", "对话结束指令已执行" + +class BlockAndIgnoreHandler(ActionHandler): + """处理屏蔽并忽略动作的处理器。""" + async def execute(self, reason: str, observation_info: Optional[ObservationInfo], conversation_info: Optional[ConversationInfo], action_start_time: float, current_action_record: dict) -> tuple[bool, str, str]: + if not conversation_info or not observation_info: return False, "error", "BlockAndIgnore 缺少信息" + self.logger.info(f"[私聊][{self.conversation.private_name}] 动作 'block_and_ignore': 不想再理你了...") + ignore_duration_seconds = 10 * 60 + self.conversation.ignore_until_timestamp = time.time() + ignore_duration_seconds + self.conversation.state = ConversationState.IGNORED + event_desc = "当前对话让你感到不适,你决定暂时不再理会对方" + await self._update_relationship_and_emotion(observation_info, conversation_info, event_desc) + return True, "done", f"已屏蔽并忽略对话 {ignore_duration_seconds // 60} 分钟" + +class WaitHandler(ActionHandler): + """处理等待动作的处理器。""" + async def execute(self, reason: str, observation_info: Optional[ObservationInfo], conversation_info: Optional[ConversationInfo], action_start_time: float, current_action_record: dict) -> tuple[bool, str, str]: + if not conversation_info or not observation_info: return False, "error", "Wait 缺少信息" + self.conversation.state = ConversationState.WAITING + if not self.conversation.waiter: raise RuntimeError("Waiter 未初始化") + timeout_occurred = await self.conversation.waiter.wait(conversation_info) + event_desc = "你等待对方回复,但对方长时间没有回应" if timeout_occurred else "你选择等待对方的回复" + await self._update_relationship_and_emotion(observation_info, conversation_info, event_desc) + return True, "done", "等待动作完成" + +class UnknownActionHandler(ActionHandler): + """处理未知动作的处理器。""" + async def execute(self, reason: str, observation_info: Optional[ObservationInfo], conversation_info: Optional[ConversationInfo], action_start_time: float, current_action_record: dict) -> tuple[bool, str, str]: + action_name = current_action_record.get("action", "未知") + self.logger.warning(f"[私聊][{self.conversation.private_name}] 未知的动作类型: {action_name}") + return False, "recall", f"未知的动作类型: {action_name}" diff --git a/src/experimental/PFC/actions.py b/src/experimental/PFC/actions.py index ad55bc9c..f187acb8 100644 --- a/src/experimental/PFC/actions.py +++ b/src/experimental/PFC/actions.py @@ -2,63 +2,20 @@ import time import asyncio import datetime import traceback -import json -from typing import Optional, Set, TYPE_CHECKING -from src.chat.emoji_system.emoji_manager import emoji_manager +from typing import Optional, TYPE_CHECKING + from src.common.logger_manager import get_logger -from src.config.config import global_config -from src.chat.utils.chat_message_builder import build_readable_messages -from .pfc_types import ConversationState -from .observation_info import ObservationInfo -from .conversation_info import ConversationInfo -from src.chat.utils.utils_image import image_path_to_base64 # 假设路径正确 -from maim_message import Seg, UserInfo # 从 maim_message 导入 Seg 和 UserInfo -from src.chat.message_receive.message import MessageSending, MessageSet # PFC 的发送器依赖这些 -from src.chat.message_receive.message_sender import message_manager # PFC 的发送器依赖这个 +from PFC.pfc_types import ConversationState # 调整导入路径 +from PFC.observation_info import ObservationInfo # 调整导入路径 +from PFC.conversation_info import ConversationInfo # 调整导入路径 + +# 导入工厂类 +from PFC.action_factory import StandardActionFactory # 调整导入路径 if TYPE_CHECKING: - from .conversation import Conversation # 用于类型提示以避免循环导入 + from PFC.conversation import Conversation # 调整导入路径 -logger = get_logger("pfc_actions") - - -async def _send_reply_internal(conversation_instance: "Conversation") -> bool: - """ - 内部辅助函数,用于发送 conversation_instance.generated_reply 中的内容。 - 这之前是 Conversation 类中的 _send_reply 方法。 - """ - # 检查是否有内容可发送 - if not conversation_instance.generated_reply: - logger.warning(f"[私聊][{conversation_instance.private_name}] 没有生成回复内容,无法发送。") - return False - # 检查发送器和聊天流是否已初始化 - if not conversation_instance.direct_sender: - logger.error(f"[私聊][{conversation_instance.private_name}] DirectMessageSender 未初始化,无法发送。") - return False - if not conversation_instance.chat_stream: - logger.error(f"[私聊][{conversation_instance.private_name}] ChatStream 未初始化,无法发送。") - return False - - try: - reply_content = conversation_instance.generated_reply - # 调用发送器发送消息,不指定回复对象 - await conversation_instance.direct_sender.send_message( - chat_stream=conversation_instance.chat_stream, - content=reply_content, - reply_to_message=None, # 私聊通常不需要引用回复 - ) - # 自身发言数量累计 +1 - if conversation_instance.conversation_info: # 确保 conversation_info 存在 - conversation_instance.conversation_info.my_message_count += 1 - # 发送成功后,将状态设置回分析,准备下一轮规划 - conversation_instance.state = ConversationState.ANALYZING - return True # 返回成功 - except Exception as e: - # 捕获发送过程中的异常 - logger.error(f"[私聊][{conversation_instance.private_name}] 发送消息时失败: {str(e)}") - logger.error(f"[私聊][{conversation_instance.private_name}] {traceback.format_exc()}") - conversation_instance.state = ConversationState.ERROR # 发送失败标记错误状态 - return False # 返回失败 +logger = get_logger("pfc_actions") # 模块级别日志记录器 async def handle_action( @@ -70,924 +27,160 @@ async def handle_action( ): """ 处理由 ActionPlanner 规划出的具体行动。 - 这之前是 Conversation 类中的 _handle_action 方法。 + 使用 ActionFactory 创建并执行相应的处理器。 """ - # 检查初始化状态 + # 检查对话实例是否已初始化 if not conversation_instance._initialized: logger.error(f"[私聊][{conversation_instance.private_name}] 尝试在未初始化状态下处理动作 '{action}'。") return - # 确保 observation_info 和 conversation_info 不为 None + # 检查 observation_info 是否为空 if not observation_info: logger.error(f"[私聊][{conversation_instance.private_name}] ObservationInfo 为空,无法处理动作 '{action}'。") - # 在 conversation_info 和 done_action 存在时更新状态 + # 如果 conversation_info 和 done_action 存在且不为空 if conversation_info and hasattr(conversation_info, "done_action") and conversation_info.done_action: - conversation_info.done_action[-1].update( - { - "status": "error", - "final_reason": "ObservationInfo is None", - } - ) - conversation_instance.state = ConversationState.ERROR + # 更新最后一个动作记录的状态和原因 + if conversation_info.done_action: # 再次检查列表是否不为空 + conversation_info.done_action[-1].update( + {"status": "error", "final_reason": "ObservationInfo is None"} + ) + conversation_instance.state = ConversationState.ERROR # 设置对话状态为错误 return - if not conversation_info: # conversation_info 在这里是必需的 + # 检查 conversation_info 是否为空 + if not conversation_info: logger.error(f"[私聊][{conversation_instance.private_name}] ConversationInfo 为空,无法处理动作 '{action}'。") - conversation_instance.state = ConversationState.ERROR + conversation_instance.state = ConversationState.ERROR # 设置对话状态为错误 return logger.info(f"[私聊][{conversation_instance.private_name}] 开始处理动作: {action}, 原因: {reason}") - action_start_time = time.time() # 记录动作开始时间 + action_start_time = time.time() # 记录动作开始时间 - # --- 准备动作历史记录条目 --- + # 当前动作记录 current_action_record = { - "action": action, - "plan_reason": reason, # 记录规划时的原因 - "status": "start", # 初始状态为"开始" - "time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # 记录开始时间 - "final_reason": None, # 最终结果的原因,将在 finally 中设置 + "action": action, # 动作类型 + "plan_reason": reason, # 规划原因 + "status": "start", # 初始状态为 "start" + "time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # 当前时间 + "final_reason": None, # 最终原因,默认为 None } - # 安全地添加到历史记录列表 - if not hasattr(conversation_info, "done_action") or conversation_info.done_action is None: # 防御性检查 + # 如果 done_action 不存在或为空,则初始化 + if not hasattr(conversation_info, "done_action") or conversation_info.done_action is None: conversation_info.done_action = [] - conversation_info.done_action.append(current_action_record) - # 获取当前记录在列表中的索引,方便后续更新状态 - action_index = len(conversation_info.done_action) - 1 + conversation_info.done_action.append(current_action_record) # 添加当前动作记录 + action_index = len(conversation_info.done_action) - 1 # 获取当前动作记录的索引 - # --- 初始化动作执行状态变量 --- - action_successful: bool = False # 标记动作是否成功执行 - final_status: str = "recall" # 动作最终状态,默认为 recall (表示未成功或需重试) - final_reason: str = "动作未成功执行" # 动作最终原因 + action_successful: bool = False # 动作是否成功,默认为 False + final_status: str = "recall" # 最终状态,默认为 "recall" + final_reason: str = "动作未成功执行" # 最终原因,默认为 "动作未成功执行" - # 在此声明变量以避免 UnboundLocalError - is_suitable: bool = False - generated_content_for_check_or_send: str = "" - check_reason: str = "未进行检查" - need_replan_from_checker: bool = False - should_send_reply: bool = True # 默认需要发送 (对于 direct_reply) - is_send_decision_from_rg: bool = False # 标记 send_new_message 的决策是否来自 ReplyGenerator + factory = StandardActionFactory() # 创建标准动作工厂实例 + action_handler = factory.create_action_handler(action, conversation_instance) # 创建动作处理器 try: - # --- 根据不同的 action 类型执行相应的逻辑 --- - - # 1. 处理需要生成、检查、发送的动作 - if action in ["direct_reply", "send_new_message"]: - max_reply_attempts: int = getattr(global_config, "pfc_max_reply_attempts", 3) # 最多尝试次数 (可配置) - reply_attempt_count: int = 0 - # is_suitable, generated_content_for_check_or_send, check_reason, need_replan_from_checker, should_send_reply, is_send_decision_from_rg 已在外部声明 - - while reply_attempt_count < max_reply_attempts and not is_suitable and not need_replan_from_checker: - reply_attempt_count += 1 - log_prefix = f"[私聊][{conversation_instance.private_name}] 尝试生成/检查 '{action}' 回复 (第 {reply_attempt_count}/{max_reply_attempts} 次)..." - logger.info(log_prefix) - - conversation_instance.state = ConversationState.GENERATING - if not conversation_instance.reply_generator: - raise RuntimeError("ReplyGenerator 未初始化") - - raw_llm_output = await conversation_instance.reply_generator.generate( - observation_info, conversation_info, action_type=action - ) - logger.debug(f"{log_prefix} ReplyGenerator.generate 返回: '{raw_llm_output}'") - - text_to_process = raw_llm_output # 默认情况下,处理原始输出 - - if action == "send_new_message": - is_send_decision_from_rg = True # 标记这是 send_new_message 的决策过程 - parsed_json = None - try: - # 尝试解析JSON - parsed_json = json.loads(raw_llm_output) - except json.JSONDecodeError: - logger.error(f"{log_prefix} ReplyGenerator 返回的不是有效的JSON: {raw_llm_output}") - # 如果JSON解析失败,视为RG决定不发送,并给出原因 - conversation_info.last_reply_rejection_reason = "回复生成器未返回有效JSON" - conversation_info.last_rejected_reply_content = raw_llm_output - should_send_reply = False - text_to_process = "no" # 或者一个特定的错误标记 - - if parsed_json: # 如果成功解析 - send_decision = parsed_json.get("send", "no").lower() - generated_text_from_json = parsed_json.get("txt", "no") - - if send_decision == "yes": - should_send_reply = True - text_to_process = generated_text_from_json - logger.info(f"{log_prefix} ReplyGenerator 决定发送消息。内容: '{text_to_process[:100]}...'") - else: # send_decision is "no" - should_send_reply = False - text_to_process = "no" # 保持和 prompt 中一致,txt 为 "no" - logger.info(f"{log_prefix} ReplyGenerator 决定不发送消息。") - # 既然RG决定不发送,就直接跳出重试循环 - break - - # 如果 ReplyGenerator 在 send_new_message 动作中决定不发送,则跳出重试循环 - if action == "send_new_message" and not should_send_reply: - break - - generated_content_for_check_or_send = text_to_process - - # 检查生成的内容是否有效 - if ( - not generated_content_for_check_or_send - or generated_content_for_check_or_send.startswith("抱歉") - or generated_content_for_check_or_send.strip() == "" - or ( - action == "send_new_message" - and generated_content_for_check_or_send == "no" - and should_send_reply - ) - ): # RG决定发送但文本为"no"或空 - warning_msg = f"{log_prefix} 生成内容无效或为错误提示" - if action == "send_new_message" and generated_content_for_check_or_send == "no": # 特殊情况日志 - warning_msg += " (ReplyGenerator决定发送但文本为'no')" - - logger.warning(warning_msg + ",将进行下一次尝试 (如果适用)。") - check_reason = "生成内容无效或选择不发送" # 统一原因 - conversation_info.last_reply_rejection_reason = check_reason - conversation_info.last_rejected_reply_content = generated_content_for_check_or_send - - await asyncio.sleep(0.5) # 暂停一下 - continue # 直接进入下一次循环尝试 - - # --- 内容检查 --- - conversation_instance.state = ConversationState.CHECKING - if not conversation_instance.reply_checker: - raise RuntimeError("ReplyChecker 未初始化") - - # 准备检查器所需参数 - current_goal_str = "" - if conversation_info.goal_list: # 确保 goal_list 存在且不为空 - goal_item = conversation_info.goal_list[-1] - if isinstance(goal_item, dict): - current_goal_str = goal_item.get("goal", "") - elif isinstance(goal_item, str): - current_goal_str = goal_item - - chat_history_for_check = getattr(observation_info, "chat_history", []) - chat_history_text_for_check = getattr(observation_info, "chat_history_str", "") - current_retry_for_checker = reply_attempt_count - 1 # retry_count 从0开始 - current_time_value_for_check = observation_info.current_time_str or "获取时间失败" - - # 调用检查器 - if global_config.enable_pfc_reply_checker: - logger.debug(f"{log_prefix} 调用 ReplyChecker 检查 (配置已启用)...") - ( - is_suitable, - check_reason, - need_replan_from_checker, - ) = await conversation_instance.reply_checker.check( - reply=generated_content_for_check_or_send, - goal=current_goal_str, - chat_history=chat_history_for_check, # 使用完整的历史记录列表 - chat_history_text=chat_history_text_for_check, # 可以是截断的文本 - current_time_str=current_time_value_for_check, - retry_count=current_retry_for_checker, # 传递当前重试次数 - ) - logger.info( - f"{log_prefix} ReplyChecker 结果: 合适={is_suitable}, 原因='{check_reason}', 需重规划={need_replan_from_checker}" - ) - else: # 如果配置关闭 - is_suitable = True - check_reason = "ReplyChecker 已通过配置关闭" - need_replan_from_checker = False - logger.debug(f"{log_prefix} [配置关闭] ReplyChecker 已跳过,默认回复为合适。") - - # 处理检查结果 - if not is_suitable: - conversation_info.last_reply_rejection_reason = check_reason - conversation_info.last_rejected_reply_content = generated_content_for_check_or_send - - # 如果是机器人自身复读,且检查器认为不需要重规划 (这是新版 ReplyChecker 的逻辑) - if check_reason == "机器人尝试发送重复消息" and not need_replan_from_checker: - logger.warning( - f"{log_prefix} 回复因自身重复被拒绝: {check_reason}。将使用相同 Prompt 类型重试。" - ) - if reply_attempt_count < max_reply_attempts: # 还有尝试次数 - await asyncio.sleep(0.5) # 暂停一下 - continue # 进入下一次重试 - else: # 达到最大次数 - logger.warning(f"{log_prefix} 即使是复读,也已达到最大尝试次数。") - break # 结束循环,按失败处理 - elif ( - not need_replan_from_checker and reply_attempt_count < max_reply_attempts - ): # 其他不合适原因,但无需重规划,且可重试 - logger.warning(f"{log_prefix} 回复不合适,原因: {check_reason}。将进行下一次尝试。") - await asyncio.sleep(0.5) # 暂停一下 - continue # 进入下一次重试 - else: # 需要重规划,或达到最大次数 - logger.warning(f"{log_prefix} 回复不合适且(需要重规划或已达最大次数)。原因: {check_reason}") - break # 结束循环,将在循环外部处理 - else: # is_suitable is True - # 找到了合适的回复 - conversation_info.last_reply_rejection_reason = None # 清除之前的拒绝原因 - conversation_info.last_rejected_reply_content = None - break # 成功,跳出循环 - - # --- 循环结束后处理 --- - if action == "send_new_message" and not should_send_reply and is_send_decision_from_rg: - # 这是 reply_generator 决定不发送的情况 - logger.info( - f"[私聊][{conversation_instance.private_name}] 动作 '{action}': ReplyGenerator 决定不发送消息。" - ) - final_status = "done_no_reply" # 一个新的状态,表示动作完成但无回复 - final_reason = "回复生成器决定不发送消息" - action_successful = True # 动作本身(决策)是成功的 - - # 清除追问状态,因为没有实际发送 - conversation_info.last_successful_reply_action = None - conversation_info.my_message_count = 0 # 重置连续发言计数 - # 后续的 plan 循环会检测到这个 "done_no_reply" 状态并使用反思 prompt - - elif is_suitable: # 适用于 direct_reply 或 (send_new_message 且 RG决定发送并通过检查) - logger.debug( - f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 找到合适的回复,准备发送。" - ) - # conversation_info.last_reply_rejection_reason = None # 已在循环内清除 - # conversation_info.last_rejected_reply_content = None - conversation_instance.generated_reply = generated_content_for_check_or_send # 使用检查通过的内容 - timestamp_before_sending = time.time() - logger.debug( - f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 记录发送前时间戳: {timestamp_before_sending:.2f}" - ) - conversation_instance.state = ConversationState.SENDING - send_success = await _send_reply_internal(conversation_instance) # 调用重构后的发送函数 - send_end_time = time.time() # 记录发送完成时间 - - if send_success: - action_successful = True - final_status = "done" # 明确设置 final_status - final_reason = "成功发送" # 明确设置 final_reason - logger.debug(f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 成功发送回复.") - - # --- 新增:将机器人发送的消息添加到 ObservationInfo 的 chat_history --- - if ( - observation_info and conversation_instance.bot_qq_str - ): # 确保 observation_info 和 bot_qq_str 存在 - bot_message_dict = { - "message_id": f"bot_sent_{send_end_time}", # 生成一个唯一ID - "time": send_end_time, - "user_info": { # 构造机器人的 UserInfo - "user_id": conversation_instance.bot_qq_str, - "user_nickname": global_config.BOT_NICKNAME, # 或者 conversation_instance.name - "platform": conversation_instance.chat_stream.platform - if conversation_instance.chat_stream - else "unknown_platform", - }, - "processed_plain_text": conversation_instance.generated_reply, - "detailed_plain_text": conversation_instance.generated_reply, # 简单处理 - # 根据你的消息字典结构,可能还需要其他字段 - } - observation_info.chat_history.append(bot_message_dict) - observation_info.chat_history_count = len(observation_info.chat_history) - logger.debug( - f"[私聊][{conversation_instance.private_name}] {global_config.BOT_NICKNAME}发送的消息已添加到 chat_history。当前历史数: {observation_info.chat_history_count}" - ) - - # 可选:如果 chat_history 过长,进行修剪 (例如,保留最近N条) - max_history_len = getattr(global_config, "pfc_max_chat_history_for_checker", 50) # 例如,可配置 - if len(observation_info.chat_history) > max_history_len: - observation_info.chat_history = observation_info.chat_history[-max_history_len:] - observation_info.chat_history_count = len(observation_info.chat_history) # 更新计数 - - # 更新 chat_history_str (如果 ReplyChecker 也依赖这个字符串) - # 这个更新可能比较消耗资源,如果 checker 只用列表,可以考虑优化此处 - history_slice_for_str = observation_info.chat_history[-30:] # 例如最近30条 - try: - observation_info.chat_history_str = await build_readable_messages( - history_slice_for_str, - replace_bot_name=True, - merge_messages=False, - timestamp_mode="relative", - read_mark=0.0, - ) - except Exception as e_build_hist: - logger.error( - f"[私聊][{conversation_instance.private_name}] 更新 chat_history_str 时出错: {e_build_hist}" - ) - observation_info.chat_history_str = "[构建聊天记录出错]" - # --- 新增结束 --- - - # 更新 idle_chat 的最后消息时间 - # (避免在发送消息后很快触发主动聊天) - if conversation_instance.idle_chat: - await conversation_instance.idle_chat.update_last_message_time(send_end_time) - - # 清理已处理的未读消息 (只清理在发送这条回复之前的、来自他人的消息) - current_unprocessed_messages = getattr(observation_info, "unprocessed_messages", []) - message_ids_to_clear: Set[str] = set() - for msg in current_unprocessed_messages: - msg_time = msg.get("time") - msg_id = msg.get("message_id") - sender_id_info = msg.get("user_info", {}) # 安全获取 user_info - sender_id = str(sender_id_info.get("user_id")) if sender_id_info else None # 安全获取 sender_id - - if ( - msg_id # 确保 msg_id 存在 - and msg_time # 确保 msg_time 存在 - and sender_id != conversation_instance.bot_qq_str # 确保是对方的消息 - and msg_time < timestamp_before_sending # 只清理发送前的 - ): - message_ids_to_clear.add(msg_id) - - if message_ids_to_clear: - logger.debug( - f"[私聊][{conversation_instance.private_name}] 准备清理 {len(message_ids_to_clear)} 条发送前(他人)消息: {message_ids_to_clear}" - ) - await observation_info.clear_processed_messages(message_ids_to_clear) - else: - logger.debug(f"[私聊][{conversation_instance.private_name}] 没有需要清理的发送前(他人)消息。") - - # 更新追问状态 和 关系/情绪状态 - other_new_msg_count_during_planning = getattr( - conversation_info, "other_new_messages_during_planning_count", 0 - ) - - # 如果是 direct_reply 且规划期间有他人新消息,则下次不追问 - if other_new_msg_count_during_planning > 0 and action == "direct_reply": - logger.debug( - f"[私聊][{conversation_instance.private_name}] 因规划期间收到 {other_new_msg_count_during_planning} 条他人新消息,下一轮强制使用【初始回复】逻辑。" - ) - conversation_info.last_successful_reply_action = None - # conversation_info.my_message_count 不在此处重置,因为它刚发了一条 - elif action == "direct_reply" or action == "send_new_message": # 成功发送后 - logger.debug( - f"[私聊][{conversation_instance.private_name}] 成功执行 '{action}', 下一轮【允许】使用追问逻辑。" - ) - conversation_info.last_successful_reply_action = action - - # 更新实例消息计数和关系/情绪 - if conversation_info: # 再次确认 - conversation_info.current_instance_message_count += 1 - logger.debug( - f"[私聊][{conversation_instance.private_name}] 实例消息计数({global_config.BOT_NICKNAME}发送后)增加到: {conversation_info.current_instance_message_count}" - ) - - if conversation_instance.relationship_updater: # 确保存在 - await conversation_instance.relationship_updater.update_relationship_incremental( - conversation_info=conversation_info, - observation_info=observation_info, - chat_observer_for_history=conversation_instance.chat_observer, # 确保 chat_observer 存在 - ) - - sent_reply_summary = ( - conversation_instance.generated_reply[:50] - if conversation_instance.generated_reply - else "空回复" - ) - event_for_emotion_update = f"你刚刚发送了消息: '{sent_reply_summary}...'" - if conversation_instance.emotion_updater: # 确保存在 - await conversation_instance.emotion_updater.update_emotion_based_on_context( - conversation_info=conversation_info, - observation_info=observation_info, - chat_observer_for_history=conversation_instance.chat_observer, # 确保 chat_observer 存在 - event_description=event_for_emotion_update, - ) - else: # 发送失败 - logger.error(f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 发送回复失败。") - final_status = "recall" # 标记为 recall 或 error - final_reason = "发送回复时失败" - action_successful = False # 确保 action_successful 为 False - # 发送失败,重置追问状态和计数 - conversation_info.last_successful_reply_action = None - conversation_info.my_message_count = 0 - - elif need_replan_from_checker: # 如果检查器要求重规划 - logger.warning( - f"[私聊][{conversation_instance.private_name}] 动作 '{action}' 因 ReplyChecker 要求而被取消,将重新规划。原因: {check_reason}" - ) - final_status = "recall" # 标记为 recall - final_reason = f"回复检查要求重新规划: {check_reason}" - # 重置追问状态,因为没有成功发送 - conversation_info.last_successful_reply_action = None - # my_message_count 保持不变,因为没有成功发送 - - else: # 达到最大尝试次数仍未找到合适回复 (is_suitable is False and not need_replan_from_checker) - logger.warning( - f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 达到最大回复尝试次数 ({max_reply_attempts}),ReplyChecker 仍判定不合适。最终检查原因: {check_reason}" - ) - final_status = "max_checker_attempts_failed" - final_reason = f"达到最大回复尝试次数({max_reply_attempts}),ReplyChecker仍判定不合适: {check_reason}" - action_successful = False - if conversation_info: # 确保 conversation_info 存在 - conversation_info.last_successful_reply_action = None - # my_message_count 保持不变 - - # 2. 处理发送告别语动作 (保持简单,不加重试) - elif action == "say_goodbye": - conversation_instance.state = ConversationState.GENERATING - if not conversation_instance.reply_generator: - raise RuntimeError("ReplyGenerator 未初始化") - # 生成告别语 - generated_content = await conversation_instance.reply_generator.generate( - observation_info, - conversation_info, - action_type=action, # action_type='say_goodbye' - ) - logger.info( - f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 生成内容: '{generated_content[:100]}...'" - ) - - # 检查生成内容 - if not generated_content or generated_content.startswith("抱歉"): - logger.warning( - f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 生成内容为空或为错误提示,取消发送。" - ) - final_reason = "生成内容无效" - # 即使生成失败,也按计划结束对话 - final_status = "done" # 标记为 done,因为目的是结束 - conversation_instance.should_continue = False # 停止对话 - logger.info(f"[私聊][{conversation_instance.private_name}] 告别语生成失败,仍按计划结束对话。") - else: - # 发送告别语 - conversation_instance.generated_reply = generated_content - timestamp_before_sending = time.time() - logger.debug( - f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 记录发送前时间戳: {timestamp_before_sending:.2f}" - ) - conversation_instance.state = ConversationState.SENDING - send_success = await _send_reply_internal(conversation_instance) # 调用重构后的发送函数 - send_end_time = time.time() - - if send_success: - action_successful = True # 标记成功 - # final_status 和 final_reason 会在 finally 中设置 - logger.info(f"[私聊][{conversation_instance.private_name}] 成功发送告别语,即将停止对话实例。") - # 更新 idle_chat 的最后消息时间 - # (避免在发送消息后很快触发主动聊天) - if conversation_instance.idle_chat: - await conversation_instance.idle_chat.update_last_message_time(send_end_time) - # 清理发送前的消息 (虽然通常是最后一条,但保持逻辑一致) - current_unprocessed_messages = getattr(observation_info, "unprocessed_messages", []) - message_ids_to_clear: Set[str] = set() - for msg in current_unprocessed_messages: - msg_time = msg.get("time") - msg_id = msg.get("message_id") - sender_id_info = msg.get("user_info", {}) - sender_id = str(sender_id_info.get("user_id")) if sender_id_info else None - if ( - msg_id - and msg_time - and sender_id != conversation_instance.bot_qq_str # 不是自己的消息 - and msg_time < timestamp_before_sending # 发送前 - ): - message_ids_to_clear.add(msg_id) - if message_ids_to_clear: - await observation_info.clear_processed_messages(message_ids_to_clear) - - # 更新关系和情绪 - if conversation_info: # 确保 conversation_info 存在 - conversation_info.current_instance_message_count += 1 - logger.debug( - f"[私聊][{conversation_instance.private_name}] 实例消息计数(告别语后)增加到: {conversation_info.current_instance_message_count}" - ) - - sent_reply_summary = ( - conversation_instance.generated_reply[:50] - if conversation_instance.generated_reply - else "空回复" - ) - event_for_emotion_update = f"你发送了告别消息: '{sent_reply_summary}...'" - if conversation_instance.emotion_updater: # 确保存在 - await conversation_instance.emotion_updater.update_emotion_based_on_context( - conversation_info=conversation_info, - observation_info=observation_info, - chat_observer_for_history=conversation_instance.chat_observer, # 确保 chat_observer 存在 - event_description=event_for_emotion_update, - ) - # 发送成功后结束对话 - conversation_instance.should_continue = False - else: - # 发送失败 - logger.error(f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 发送告别语失败。") - final_status = "recall" # 或 "error" - final_reason = "发送告别语失败" - # 发送失败不能结束对话,让其自然流转或由其他逻辑结束 - conversation_instance.should_continue = True # 保持 should_continue - - # 3. 处理重新思考目标动作 - elif action == "rethink_goal": - conversation_instance.state = ConversationState.RETHINKING - if not conversation_instance.goal_analyzer: - raise RuntimeError("GoalAnalyzer 未初始化") - # 调用 GoalAnalyzer 分析并更新目标 - await conversation_instance.goal_analyzer.analyze_goal(conversation_info, observation_info) - action_successful = True # 标记成功 - event_for_emotion_update = "你重新思考了对话目标和方向" - if ( - conversation_instance.emotion_updater and conversation_info and observation_info - ): # 确保updater和info都存在 - await conversation_instance.emotion_updater.update_emotion_based_on_context( - conversation_info=conversation_info, - observation_info=observation_info, - chat_observer_for_history=conversation_instance.chat_observer, # 确保 chat_observer 存在 - event_description=event_for_emotion_update, - ) - - # 4. 处理倾听动作 - elif action == "listening": - conversation_instance.state = ConversationState.LISTENING - if not conversation_instance.waiter: - raise RuntimeError("Waiter 未初始化") - logger.info(f"[私聊][{conversation_instance.private_name}] 动作 'listening': 进入倾听状态...") - # 调用 Waiter 的倾听等待方法,内部会处理超时 - await conversation_instance.waiter.wait_listening(conversation_info) # 直接传递 conversation_info - action_successful = True # listening 动作本身执行即视为成功,后续由新消息或超时驱动 - event_for_emotion_update = "你决定耐心倾听对方的发言" - if conversation_instance.emotion_updater and conversation_info and observation_info: # 确保都存在 - await conversation_instance.emotion_updater.update_emotion_based_on_context( - conversation_info=conversation_info, - observation_info=observation_info, - chat_observer_for_history=conversation_instance.chat_observer, # 确保 chat_observer 存在 - event_description=event_for_emotion_update, - ) - - # 5. 处理结束对话动作 - elif action == "end_conversation": - logger.info( - f"[私聊][{conversation_instance.private_name}] 动作 'end_conversation': 收到最终结束指令,停止对话..." - ) - action_successful = True # 标记成功 - conversation_instance.should_continue = False # 设置标志以退出循环 - - # 6. 处理屏蔽忽略动作 - elif action == "block_and_ignore": - logger.info(f"[私聊][{conversation_instance.private_name}] 动作 'block_and_ignore': 不想再理你了...") - ignore_duration_seconds = 10 * 60 # 忽略 10 分钟,可配置 - conversation_instance.ignore_until_timestamp = time.time() + ignore_duration_seconds - logger.info( - f"[私聊][{conversation_instance.private_name}] 将忽略此对话直到: {datetime.datetime.fromtimestamp(conversation_instance.ignore_until_timestamp)}" - ) - conversation_instance.state = ConversationState.IGNORED # 设置忽略状态 - action_successful = True # 标记成功 - event_for_emotion_update = "当前对话让你感到不适,你决定暂时不再理会对方" - if conversation_instance.emotion_updater and conversation_info and observation_info: # 确保都存在 - await conversation_instance.emotion_updater.update_emotion_based_on_context( - conversation_info=conversation_info, - observation_info=observation_info, - chat_observer_for_history=conversation_instance.chat_observer, # 确保 chat_observer 存在 - event_description=event_for_emotion_update, - ) - - # X. 处理发送表情包动作 - elif action == "send_memes": - conversation_instance.state = ConversationState.GENERATING - final_reason_prefix = "发送表情包" - action_successful = False # 先假设不成功 - - # 确保 conversation_info 和 observation_info 存在 - if not conversation_info or not observation_info: - logger.error( - f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': ConversationInfo 或 ObservationInfo 为空。" - ) - final_status = "error" - final_reason = f"{final_reason_prefix}失败:内部信息缺失" - # done_action 的更新会在 finally 中处理 - # 理论上这不应该发生,因为调用 handle_action 前应该有检查 - # 但作为防御性编程,可以加上 - if conversation_info: # 即使另一个为空,也尝试更新 - conversation_info.last_successful_reply_action = None - # 直接跳到 finally 块 - # 注意:此处不能直接 return,否则 finally 不会被完整执行。 - # 而是让后续的 final_status 和 action_successful 决定流程。 - # 这里我们通过设置 action_successful = False 和 final_status = "error" 来让 finally 处理 - # 更好的方式可能是直接在 finally 前面抛出异常,但为了简化,我们先这样。 - # 此处保持 action_successful = False,后续的 finally 会处理状态。 - pass # 让代码继续到 try...except...finally 的末尾 - - else: # conversation_info 和 observation_info 都存在 - emoji_query = conversation_info.current_emoji_query - if not emoji_query: - logger.warning( - f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': emoji_query 为空,无法获取表情包。" - ) - final_status = "recall" - final_reason = f"{final_reason_prefix}失败:缺少表情包查询语句" - conversation_info.last_successful_reply_action = None - else: - logger.info( - f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': 使用查询 '{emoji_query}' 获取表情包..." - ) - try: - emoji_result = await emoji_manager.get_emoji_for_text(emoji_query) - - if emoji_result: - emoji_path, emoji_description = emoji_result - logger.info( - f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': 获取到表情包: {emoji_path}, 描述: {emoji_description}" - ) - - if not conversation_instance.chat_stream: - logger.error( - f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': ChatStream 未初始化,无法发送。" - ) - raise RuntimeError("ChatStream 未初始化") - - image_b64_content = image_path_to_base64(emoji_path) - if not image_b64_content: - logger.error( - f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': 无法将图片 {emoji_path} 转换为 base64。" - ) - raise ValueError(f"无法将图片 {emoji_path} 转换为Base64") - - # --- 统一 Seg 构造方式 (与群聊类似) --- - # 直接使用 type="emoji" 和 base64 数据 - message_segment_for_emoji = Seg(type="emoji", data=image_b64_content) - # -------------------------------------- - - bot_user_info = UserInfo( - user_id=global_config.BOT_QQ, - user_nickname=global_config.BOT_NICKNAME, - platform=conversation_instance.chat_stream.platform, - ) - message_id_emoji = f"pfc_meme_{round(time.time(), 3)}" - - # --- 直接使用 DirectMessageSender (如果其 send_message 适配单个 Seg) --- - # 或者如果 DirectMessageSender.send_message 需要 content: str, - # 我们就需要调整 DirectMessageSender 或这里的逻辑。 - # 假设 DirectMessageSender 能被改造或其依赖的 message_manager 能处理 Seg 对象。 - # 我们先按照 PFC/message_sender.py 的结构来尝试构造 MessageSending - # PFC/message_sender.py 中的 DirectMessageSender.send_message(content: str) - # 它内部是 segments = Seg(type="seglist", data=[Seg(type="text", data=content)]) - # 这意味着 DirectMessageSender 目前只设计来发送文本。 - - # *** 为了与群聊发送逻辑一致,并假设底层 message_manager 可以处理任意 Seg *** - # 我们需要绕过 DirectMessageSender 的 send_message(content: str) - # 或者修改 DirectMessageSender 以接受 Seg 对象。 - # 更简单的做法是直接调用与群聊相似的底层发送机制,即构造 MessageSending 并使用 message_manager - - message_to_send = MessageSending( - message_id=message_id_emoji, - chat_stream=conversation_instance.chat_stream, - bot_user_info=bot_user_info, - sender_info=None, # 表情通常不是对特定消息的回复 - message_segment=message_segment_for_emoji, # 直接使用构造的 Seg - reply=None, - is_head=True, - is_emoji=True, - thinking_start_time=action_start_time, # 使用动作开始时间作为近似 - ) - - await message_to_send.process() # 消息预处理 - - message_set_emoji = MessageSet(conversation_instance.chat_stream, message_id_emoji) - message_set_emoji.add_message(message_to_send) - await message_manager.add_message(message_set_emoji) # 使用全局管理器发送 - - logger.info( - f"[私聊][{conversation_instance.private_name}] PFC 动作 'send_memes': 表情包已发送: {emoji_path} ({emoji_description})" - ) - action_successful = True # 标记发送成功 - # final_status 和 final_reason 会在 finally 中设置 - - # --- 后续成功处理逻辑 (与之前相同,但要确保 conversation_info 存在) --- - if conversation_info: - conversation_info.my_message_count += 1 - conversation_info.last_successful_reply_action = action - conversation_info.current_instance_message_count += 1 - logger.debug( - f"[私聊][{conversation_instance.private_name}] 成功执行 '{action}', my_message_count: {conversation_info.my_message_count}, 下一轮将使用【追问】逻辑。" - ) - - current_send_time = time.time() - if conversation_instance.idle_chat: - await conversation_instance.idle_chat.update_last_message_time(current_send_time) - - if observation_info and conversation_instance.bot_qq_str: - bot_meme_message_dict = { - "message_id": message_id_emoji, - "time": current_send_time, - "user_info": bot_user_info.to_dict(), - "processed_plain_text": f"[表情包: {emoji_description}]", - "detailed_plain_text": f"[表情包: {emoji_path} - {emoji_description}]", - "raw_message": "[CQ:image,file=base64://...]", # 示例 - } - observation_info.chat_history.append(bot_meme_message_dict) - observation_info.chat_history_count = len(observation_info.chat_history) - max_history_len = getattr(global_config, "pfc_max_chat_history_for_checker", 50) - if len(observation_info.chat_history) > max_history_len: - observation_info.chat_history = observation_info.chat_history[-max_history_len:] - observation_info.chat_history_count = len(observation_info.chat_history) - history_slice_for_str = observation_info.chat_history[-30:] - try: - observation_info.chat_history_str = await build_readable_messages( - history_slice_for_str, - replace_bot_name=True, - merge_messages=False, - timestamp_mode="relative", - read_mark=0.0, - ) - except Exception as e_build_hist_meme: - logger.error( - f"[私聊][{conversation_instance.private_name}] 更新 chat_history_str (表情包后) 时出错: {e_build_hist_meme}" - ) - - current_unprocessed_messages_emoji = observation_info.unprocessed_messages - message_ids_to_clear_emoji: Set[str] = set() - for msg_item in current_unprocessed_messages_emoji: - msg_time_item = msg_item.get("time") - msg_id_item = msg_item.get("message_id") - sender_id_info_item = msg_item.get("user_info", {}) - sender_id_item = ( - str(sender_id_info_item.get("user_id")) if sender_id_info_item else None - ) - if ( - msg_id_item - and msg_time_item - and sender_id_item != conversation_instance.bot_qq_str - and msg_time_item < current_send_time - ): - message_ids_to_clear_emoji.add(msg_id_item) - if message_ids_to_clear_emoji: - await observation_info.clear_processed_messages(message_ids_to_clear_emoji) - - if conversation_instance.relationship_updater and conversation_info: - await conversation_instance.relationship_updater.update_relationship_incremental( - conversation_info=conversation_info, - observation_info=observation_info, - chat_observer_for_history=conversation_instance.chat_observer, - ) - event_for_emotion_update_emoji = f"你发送了一个表情包 ({emoji_description})" - if conversation_instance.emotion_updater and conversation_info: - await conversation_instance.emotion_updater.update_emotion_based_on_context( - conversation_info=conversation_info, - observation_info=observation_info, - chat_observer_for_history=conversation_instance.chat_observer, - event_description=event_for_emotion_update_emoji, - ) - else: # emoji_result is None - logger.warning( - f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': 未能根据查询 '{emoji_query}' 获取到合适的表情包。" - ) - final_status = "recall" - final_reason = f"{final_reason_prefix}失败:未找到合适表情包" - action_successful = False - if conversation_info: - conversation_info.last_successful_reply_action = None - conversation_info.current_emoji_query = None - - except Exception as get_send_emoji_err: - logger.error( - f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': 处理过程中出错: {get_send_emoji_err}" - ) - logger.error(traceback.format_exc()) - final_status = "recall" # 或 "error" - final_reason = f"{final_reason_prefix}失败:处理表情包时出错 ({get_send_emoji_err})" - action_successful = False - if conversation_info: - conversation_info.last_successful_reply_action = None - conversation_info.current_emoji_query = None - - # 7. 处理等待动作 - elif action == "wait": - conversation_instance.state = ConversationState.WAITING - if not conversation_instance.waiter: - raise RuntimeError("Waiter 未初始化") - logger.info(f"[私聊][{conversation_instance.private_name}] 动作 'wait': 进入等待状态...") - # 调用 Waiter 的常规等待方法,内部处理超时 - # wait 方法返回是否超时 (True=超时, False=未超时/被新消息中断) - timeout_occurred = await conversation_instance.waiter.wait(conversation_info) # 直接传递 conversation_info - action_successful = True # wait 动作本身执行即视为成功 - event_for_emotion_update = "" - if timeout_occurred: # 假设 timeout_occurred 能正确反映是否超时 - event_for_emotion_update = "你等待对方回复,但对方长时间没有回应" - else: - event_for_emotion_update = "你选择等待对方的回复(对方可能很快回复了)" - - if conversation_instance.emotion_updater and conversation_info and observation_info: # 确保都存在 - await conversation_instance.emotion_updater.update_emotion_based_on_context( - conversation_info=conversation_info, - observation_info=observation_info, - chat_observer_for_history=conversation_instance.chat_observer, # 确保 chat_observer 存在 - event_description=event_for_emotion_update, - ) - # wait 动作完成后不需要清理消息,等待新消息或超时触发重新规划 - logger.debug(f"[私聊][{conversation_instance.private_name}] Wait 动作完成,无需在此清理消息。") - - # 8. 处理未知的动作类型 - else: - logger.warning(f"[私聊][{conversation_instance.private_name}] 未知的动作类型: {action}") - final_status = "recall" # 未知动作标记为 recall - final_reason = f"未知的动作类型: {action}" - - # --- 重置非回复动作的追问状态 --- - # 确保执行完非回复动作后,下一次规划不会错误地进入追问逻辑 + # 执行动作处理器 + action_successful, final_status, final_reason = await action_handler.execute( + reason, observation_info, conversation_info, action_start_time, current_action_record + ) + + # 动作执行后的逻辑 (例如更新 last_successful_reply_action 等) + # 此部分之前位于每个 if/elif 块内部 + # 如果动作不是回复类型的动作 if action not in ["direct_reply", "send_new_message", "say_goodbye", "send_memes"]: - conversation_info.last_successful_reply_action = None - # 清理可能残留的拒绝信息 - conversation_info.last_reply_rejection_reason = None - conversation_info.last_rejected_reply_content = None + conversation_info.last_successful_reply_action = None # 清除上次成功回复动作 + conversation_info.last_reply_rejection_reason = None # 清除上次回复拒绝原因 + conversation_info.last_rejected_reply_content = None # 清除上次拒绝的回复内容 + # 如果动作不是发送表情包或发送表情包失败,则清除表情查询 if action != "send_memes" or not action_successful: - if conversation_info and hasattr(conversation_info, "current_emoji_query"): + if hasattr(conversation_info, "current_emoji_query"): conversation_info.current_emoji_query = None - except asyncio.CancelledError: - # 处理任务被取消的异常 + except asyncio.CancelledError: # 捕获任务取消错误 logger.warning(f"[私聊][{conversation_instance.private_name}] 处理动作 '{action}' 时被取消。") - final_status = "cancelled" + final_status = "cancelled" # 设置最终状态为 "cancelled" final_reason = "动作处理被取消" - # 取消时也重置追问状态 - if conversation_info: # 确保 conversation_info 存在 - conversation_info.last_successful_reply_action = None - raise # 重新抛出 CancelledError,让上层知道任务被取消 - except Exception as handle_err: - # 捕获处理动作过程中的其他所有异常 + # 如果 conversation_info 存在 + if conversation_info: + conversation_info.last_successful_reply_action = None # 清除上次成功回复动作 + raise # 重新抛出异常,由循环处理 + except Exception as handle_err: # 捕获其他异常 logger.error(f"[私聊][{conversation_instance.private_name}] 处理动作 '{action}' 时出错: {handle_err}") logger.error(f"[私聊][{conversation_instance.private_name}] {traceback.format_exc()}") - final_status = "error" # 标记为错误状态 + final_status = "error" # 设置最终状态为 "error" final_reason = f"处理动作时出错: {handle_err}" - conversation_instance.state = ConversationState.ERROR # 设置对话状态为错误 - # 出错时重置追问状态 - if conversation_info: # 确保 conversation_info 存在 - conversation_info.last_successful_reply_action = None + conversation_instance.state = ConversationState.ERROR # 设置对话状态为错误 + # 如果 conversation_info 存在 + if conversation_info: + conversation_info.last_successful_reply_action = None # 清除上次成功回复动作 + action_successful = False # 确保动作为不成功 finally: - # --- 统一更新动作历史记录的最终状态和原因 --- - # (确保 conversation_info 和 done_action[action_index] 有效) + # 更新动作历史记录 + # 检查 done_action 属性是否存在且不为空,并且索引有效 if ( - conversation_info - and hasattr(conversation_info, "done_action") + hasattr(conversation_info, "done_action") and conversation_info.done_action and action_index < len(conversation_info.done_action) ): - # 确定最终状态和原因 - if action_successful: # 如果动作本身标记为成功 - if final_status not in ["done", "done_no_reply"]: # 如果没有被特定成功状态覆盖 - final_status = "done" - if not final_reason or final_reason == "动作未成功执行": - if action == "send_memes": - final_reason = f"{final_reason_prefix}成功发送" - # ... (其他动作的默认成功原因) ... - else: - final_reason = f"动作 {action} 成功完成" - else: # action_successful is False - if final_status in ["recall", "start", "unknown"]: # 如果状态还是初始或未定 - # 尝试从 conversation_info 获取更具体的失败原因 - specific_rejection_reason = getattr(conversation_info, "last_reply_rejection_reason", None) - if specific_rejection_reason and (not final_reason or final_reason == "动作未成功执行"): - final_reason = f"执行失败: {specific_rejection_reason}" - elif not final_reason or final_reason == "动作未成功执行": - final_reason = f"动作 {action} 执行失败或被中止" - # 如果 final_status 已经是 "error", "cancelled", "max_checker_attempts_failed" 等,则保留 + # 如果动作成功且最终状态不是 "done" 或 "done_no_reply",则设置为 "done" + if action_successful and final_status not in ["done", "done_no_reply"]: + final_status = "done" + # 如果动作成功且最终原因未设置或为默认值 + if action_successful and (not final_reason or final_reason == "动作未成功执行"): + final_reason = f"动作 {action} 成功完成" + # 如果是发送表情包且 current_emoji_query 存在(理想情况下从处理器获取描述) + if action == "send_memes" and conversation_info.current_emoji_query: + pass # 占位符 - 表情描述最好从处理器的执行结果中获取并用于原因 + # 更新动作记录 conversation_info.done_action[action_index].update( { - "status": final_status, - "time_completed": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "final_reason": final_reason, - "duration_ms": int((time.time() - action_start_time) * 1000), + "status": final_status, # 最终状态 + "time_completed": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # 完成时间 + "final_reason": final_reason, # 最终原因 + "duration_ms": int((time.time() - action_start_time) * 1000), # 持续时间(毫秒) } ) - else: + else: # 如果无法更新动作历史记录 logger.error( - f"[私聊][{conversation_instance.private_name}] 无法更新动作历史记录,索引 {action_index} 无效或列表为空。" + f"[私聊][{conversation_instance.private_name}] 无法更新动作历史记录,done_action 无效或索引 {action_index} 超出范围。" ) - # --- 统一设置 ConversationState --- - if final_status == "done" or final_status == "done_no_reply" or final_status == "recall": - # "recall" 状态也应该回到 ANALYZING 准备重新规划 - conversation_instance.state = ConversationState.ANALYZING - elif final_status == "error" or final_status == "max_checker_attempts_failed": - conversation_instance.state = ConversationState.ERROR - # 对于 "cancelled", "listening", "waiting", "ignored", "ended" 等状态, - # 它们应该在各自的动作逻辑内部或者由外部 (如 loop) 来决定下一个 ConversationState。 - # 例如,end_conversation/say_goodbye 会设置 should_continue=False,loop 会退出。 - # listening/wait 会在动作完成后(可能因为新消息或超时)使 loop 自然进入下一轮 ANALYZING。 - # cancelled 会让 loop 捕获异常并停止。 - # 重置非回复动作的追问状态 (确保 send_memes 被视为回复动作) - if action not in [ - "direct_reply", - "send_new_message", - "say_goodbye", - "send_memes", - ]: # <--- 把 send_memes 加到这里 - if conversation_info: - conversation_info.last_successful_reply_action = None - conversation_info.last_reply_rejection_reason = None - conversation_info.last_rejected_reply_content = None + # 根据最终状态设置对话状态 + if final_status in ["done", "done_no_reply", "recall"]: + conversation_instance.state = ConversationState.ANALYZING # 设置为分析中 + elif final_status in ["error", "max_checker_attempts_failed"]: + conversation_instance.state = ConversationState.ERROR # 设置为错误 + # 其他状态如 LISTENING, WAITING, IGNORED, ENDED 在各自的处理器内部或由循环设置。 - # 清理表情查询(如果动作不是send_memes但查询还存在,或者send_memes失败了) + # 此处移至 try 块以确保即使在发生异常之前也运行 + # 如果动作不是回复类型的动作 + if action not in ["direct_reply", "send_new_message", "say_goodbye", "send_memes"]: + if conversation_info: # 再次检查 conversation_info 是否不为 None + conversation_info.last_successful_reply_action = None # 清除上次成功回复动作 + conversation_info.last_reply_rejection_reason = None # 清除上次回复拒绝原因 + conversation_info.last_rejected_reply_content = None # 清除上次拒绝的回复内容 + # 如果动作不是发送表情包或发送表情包失败 if action != "send_memes" or not action_successful: + # 如果 conversation_info 存在且有 current_emoji_query 属性 if conversation_info and hasattr(conversation_info, "current_emoji_query"): - conversation_info.current_emoji_query = None + conversation_info.current_emoji_query = None # 清除当前表情查询 - log_final_reason_msg = final_reason if final_reason else "无明确原因" + + log_final_reason_msg = final_reason if final_reason else "无明确原因" # 记录的最终原因消息 + # 如果最终状态为 "done",动作成功,且是直接回复或发送新消息,并且有生成的回复 if ( final_status == "done" and action_successful - and action in ["direct_reply", "send_new_message"] # send_memes 的发送内容不同 + and action in ["direct_reply", "send_new_message"] and hasattr(conversation_instance, "generated_reply") and conversation_instance.generated_reply ): log_final_reason_msg += f" (发送内容: '{conversation_instance.generated_reply[:30]}...')" - elif ( - final_status == "done" and action_successful and action == "send_memes" - # emoji_description 在 send_memes 内部获取,这里不再重复记录到 log_final_reason_msg, - # 因为 logger.info 已经记录过发送的表情描述 - ): - pass + # elif final_status == "done" and action_successful and action == "send_memes": + # 表情包的日志记录在其处理器内部或通过下面的通用日志处理 logger.info( f"[私聊][{conversation_instance.private_name}] 动作 '{action}' 处理完成。最终状态: {final_status}, 原因: {log_final_reason_msg}" - ) + ) \ No newline at end of file diff --git a/src/experimental/PFC/message_sender.py b/src/experimental/PFC/message_sender.py index df6e428a..4b1663cc 100644 --- a/src/experimental/PFC/message_sender.py +++ b/src/experimental/PFC/message_sender.py @@ -24,8 +24,10 @@ class DirectMessageSender: async def send_message( self, chat_stream: ChatStream, - content: str, + segments: Seg, reply_to_message: Optional[Message] = None, + is_emoji: Optional[bool] = False, + content: str = None, ) -> None: """发送消息到聊天流 @@ -35,9 +37,6 @@ class DirectMessageSender: reply_to_message: 要回复的消息(可选) """ try: - # 创建消息内容 - segments = Seg(type="seglist", data=[Seg(type="text", data=content)]) - # 获取麦麦的信息 bot_user_info = UserInfo( user_id=global_config.BOT_QQ, @@ -57,7 +56,7 @@ class DirectMessageSender: message_segment=segments, reply=reply_to_message, is_head=True, - is_emoji=False, + is_emoji=is_emoji, thinking_start_time=time.time(), ) @@ -71,7 +70,10 @@ class DirectMessageSender: message_set = MessageSet(chat_stream, message_id) message_set.add_message(message) await message_manager.add_message(message_set) - logger.info(f"[私聊][{self.private_name}]PFC消息已发送: {content}") + if is_emoji: + logger.info(f"[私聊][{self.private_name}]PFC表情消息已发送: {content}") + else: + logger.info(f"[私聊][{self.private_name}]PFC消息已发送: {content}") except Exception as e: logger.error(f"[私聊][{self.private_name}]PFC消息发送失败: {str(e)}") From 58ffb81b03ff56248ca28586d1dc9e030c647240 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Fri, 16 May 2025 11:59:35 +0800 Subject: [PATCH 2/5] fixed --- src/experimental/PFC/action_handlers.py | 19 +++++++++---------- src/experimental/PFC/actions.py | 10 +++++----- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/experimental/PFC/action_handlers.py b/src/experimental/PFC/action_handlers.py index d939ba41..d88079b6 100644 --- a/src/experimental/PFC/action_handlers.py +++ b/src/experimental/PFC/action_handlers.py @@ -10,9 +10,9 @@ from src.chat.emoji_system.emoji_manager import emoji_manager from src.common.logger_manager import get_logger from src.config.config import global_config from src.chat.utils.chat_message_builder import build_readable_messages -from PFC.pfc_types import ConversationState -from PFC.observation_info import ObservationInfo -from PFC.conversation_info import ConversationInfo +from .pfc_types import ConversationState +from .observation_info import ObservationInfo +from .conversation_info import ConversationInfo from src.chat.utils.utils_image import image_path_to_base64 from maim_message import Seg, UserInfo from src.chat.message_receive.message import MessageSending, MessageSet @@ -20,7 +20,7 @@ from src.chat.message_receive.message_sender import message_manager # PFC.message_sender 已经包含 DirectMessageSender,这里不再需要单独导入 if TYPE_CHECKING: - from PFC.conversation import Conversation + from .conversation import Conversation logger = get_logger("pfc_action_handlers") @@ -567,19 +567,18 @@ class SendMemesHandler(ActionHandler): self.logger.info(f"获取到表情包: {emoji_path}, 描述: {emoji_description}") if not self.conversation.chat_stream: raise RuntimeError("ChatStream 未初始化") - image_b64_content = image_path_to_base64(emoji_path) - if not image_b64_content: raise ValueError(f"无法转换图片 {emoji_path} 为Base64") + emoji_b64_cq = image_path_to_base64(emoji_path) + if not emoji_b64_cq: raise ValueError(f"无法转换图片 {emoji_path} 为Base64") - image_segment = Seg(type="image", data={"file": f"base64://{image_b64_content}"}) - log_content_for_meme = f"[表情: {emoji_description}]" - send_success = await self._send_reply_or_segments([image_segment], log_content_for_meme) + image_segment = Seg(type="emoji", data=emoji_b64_cq) + send_success = await self._send_reply_or_segments([image_segment], emoji_description[-20:] + "...") send_end_time = time.time() if send_success: action_successful = True final_status = "done" final_reason = f"{final_reason_prefix}成功发送 ({emoji_description})" - await self._update_bot_message_in_history(send_end_time, log_content_for_meme, observation_info, "bot_meme_") + await self._update_bot_message_in_history(send_end_time, emoji_description, observation_info, "bot_meme_") event_desc = f"你发送了一个表情包 ({emoji_description})" await self._update_post_send_states(send_end_time, observation_info, conversation_info, "send_memes", event_desc) else: diff --git a/src/experimental/PFC/actions.py b/src/experimental/PFC/actions.py index f187acb8..e984e363 100644 --- a/src/experimental/PFC/actions.py +++ b/src/experimental/PFC/actions.py @@ -5,15 +5,15 @@ import traceback from typing import Optional, TYPE_CHECKING from src.common.logger_manager import get_logger -from PFC.pfc_types import ConversationState # 调整导入路径 -from PFC.observation_info import ObservationInfo # 调整导入路径 -from PFC.conversation_info import ConversationInfo # 调整导入路径 +from .pfc_types import ConversationState # 调整导入路径 +from .observation_info import ObservationInfo # 调整导入路径 +from .conversation_info import ConversationInfo # 调整导入路径 # 导入工厂类 -from PFC.action_factory import StandardActionFactory # 调整导入路径 +from .action_factory import StandardActionFactory # 调整导入路径 if TYPE_CHECKING: - from PFC.conversation import Conversation # 调整导入路径 + from .conversation import Conversation # 调整导入路径 logger = get_logger("pfc_actions") # 模块级别日志记录器 From 751a6f91844818dc406ced025c0813b400f7729a Mon Sep 17 00:00:00 2001 From: Bakadax Date: Fri, 16 May 2025 12:30:03 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E8=A1=A5=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/experimental/PFC/action_handlers.py | 763 ++++++++++++++++-------- 1 file changed, 521 insertions(+), 242 deletions(-) diff --git a/src/experimental/PFC/action_handlers.py b/src/experimental/PFC/action_handlers.py index d88079b6..1028cc67 100644 --- a/src/experimental/PFC/action_handlers.py +++ b/src/experimental/PFC/action_handlers.py @@ -4,7 +4,8 @@ import asyncio import datetime import traceback import json -from typing import Optional, Set, TYPE_CHECKING, List, Tuple # 确保导入 List 和 Tuple +import random +from typing import Optional, Set, TYPE_CHECKING, List, Tuple, Dict # 确保导入 Dict from src.chat.emoji_system.emoji_manager import emoji_manager from src.common.logger_manager import get_logger @@ -17,7 +18,6 @@ from src.chat.utils.utils_image import image_path_to_base64 from maim_message import Seg, UserInfo from src.chat.message_receive.message import MessageSending, MessageSet from src.chat.message_receive.message_sender import message_manager -# PFC.message_sender 已经包含 DirectMessageSender,这里不再需要单独导入 if TYPE_CHECKING: from .conversation import Conversation @@ -26,8 +26,17 @@ logger = get_logger("pfc_action_handlers") class ActionHandler(ABC): - """处理动作的抽象基类。""" + """ + 处理动作的抽象基类。 + 每个具体的动作处理器都应继承此类并实现 execute 方法。 + """ def __init__(self, conversation: "Conversation"): + """ + 初始化动作处理器。 + + Args: + conversation (Conversation): 当前对话实例。 + """ self.conversation = conversation self.logger = logger @@ -41,19 +50,33 @@ class ActionHandler(ABC): current_action_record: dict ) -> tuple[bool, str, str]: """ - 执行动作。 + 执行具体的动作逻辑。 - 返回: - 一个元组,包含: - - action_successful (bool): 动作是否成功。 - - final_status (str): 动作的最终状态。 - - final_reason (str): 最终状态的原因。 + Args: + reason (str): 执行此动作的规划原因。 + observation_info (Optional[ObservationInfo]): 当前的观察信息。 + conversation_info (Optional[ConversationInfo]): 当前的对话信息。 + action_start_time (float): 动作开始的时间戳。 + current_action_record (dict): 用于记录此动作执行情况的字典。 + + Returns: + tuple[bool, str, str]: 一个元组,包含: + - action_successful (bool): 动作是否成功执行。 + - final_status (str): 动作的最终状态 (如 "done", "recall", "error")。 + - final_reason (str): 动作最终状态的原因或描述。 """ pass async def _send_reply_or_segments(self, segments_data: list[Seg], content_for_log: str) -> bool: """ - 辅助函数,用于发送消息(文本或图片段)。 + 内部辅助函数,用于将构造好的消息段发送出去。 + + Args: + segments_data (list[Seg]): 包含待发送内容的 Seg 对象列表。 + content_for_log (str): 用于日志记录的消息内容的简短描述。 + + Returns: + bool: 消息是否发送成功。 """ if not self.conversation.direct_sender: self.logger.error(f"[私聊][{self.conversation.private_name}] DirectMessageSender 未初始化,无法发送。") @@ -63,58 +86,69 @@ class ActionHandler(ABC): return False try: + # 将 Seg 对象列表包装在 type="seglist" 的 Seg 对象中 final_segments = Seg(type="seglist", data=segments_data) + # 调用实际的发送方法 await self.conversation.direct_sender.send_message( chat_stream=self.conversation.chat_stream, segments=final_segments, - reply_to_message=None, - content=content_for_log + reply_to_message=None, # 私聊通常不引用回复 + content=content_for_log # 用于发送器内部的日志记录 ) - if self.conversation.conversation_info: - self.conversation.conversation_info.my_message_count += 1 - self.conversation.state = ConversationState.ANALYZING + # 注意: my_message_count 的增加现在由具体的发送逻辑(文本或表情)处理后决定 return True except Exception as e: self.logger.error(f"[私聊][{self.conversation.private_name}] 发送消息时失败: {str(e)}") self.logger.error(f"[私聊][{self.conversation.private_name}] {traceback.format_exc()}") - self.conversation.state = ConversationState.ERROR + self.conversation.state = ConversationState.ERROR # 发送失败则标记错误状态 return False async def _update_bot_message_in_history( self, send_time: float, - message_content: str, # 对于图片,这应该是描述性的文本 + message_content: str, observation_info: ObservationInfo, message_id_prefix: str = "bot_sent_" ): - """在机器人发送消息后,更新 ObservationInfo 中的聊天记录。""" + """ + 在机器人成功发送消息后,将该消息添加到 ObservationInfo 的聊天历史中。 + + Args: + send_time (float): 消息发送成功的时间戳。 + message_content (str): 发送的消息内容(对于文本是其本身,对于表情是其描述)。 + observation_info (ObservationInfo): 当前的观察信息实例。 + message_id_prefix (str, optional): 生成消息ID时使用的前缀。默认为 "bot_sent_"。 + """ if not self.conversation.bot_qq_str: self.logger.warning(f"[私聊][{self.conversation.private_name}] Bot QQ ID 未知,无法更新机器人消息历史。") return - bot_message_dict = { - "message_id": f"{message_id_prefix}{send_time}", + # 构造机器人发送的消息字典 + bot_message_dict: Dict[str, any] = { + "message_id": f"{message_id_prefix}{send_time:.3f}", # 使用更精确的时间戳 "time": send_time, "user_info": { "user_id": self.conversation.bot_qq_str, "user_nickname": global_config.BOT_NICKNAME, "platform": self.conversation.chat_stream.platform if self.conversation.chat_stream else "unknown_platform", }, - "processed_plain_text": message_content, - "detailed_plain_text": message_content, + "processed_plain_text": message_content, # 历史记录中的纯文本使用传入的 message_content + "detailed_plain_text": message_content, # 详细文本也使用相同内容 } observation_info.chat_history.append(bot_message_dict) observation_info.chat_history_count = len(observation_info.chat_history) self.logger.debug( - f"[私聊][{self.conversation.private_name}] {global_config.BOT_NICKNAME}发送的消息已添加到 chat_history。当前历史数: {observation_info.chat_history_count}" + f"[私聊][{self.conversation.private_name}] {global_config.BOT_NICKNAME}发送的消息 ('{message_content[:30]}...')已添加到 chat_history。当前历史数: {observation_info.chat_history_count}" ) + # 限制历史记录长度 max_history_len = getattr(global_config, "pfc_max_chat_history_for_checker", 50) if len(observation_info.chat_history) > max_history_len: observation_info.chat_history = observation_info.chat_history[-max_history_len:] observation_info.chat_history_count = len(observation_info.chat_history) - history_slice_for_str = observation_info.chat_history[-30:] + # 更新用于 Prompt 的历史记录字符串 + history_slice_for_str = observation_info.chat_history[-30:] # 例如最近30条 try: observation_info.chat_history_str = await build_readable_messages( history_slice_for_str, @@ -129,20 +163,32 @@ class ActionHandler(ABC): async def _update_post_send_states( self, - send_time: float, observation_info: ObservationInfo, conversation_info: ConversationInfo, - action_type: str, # "direct_reply", "send_new_message", "say_goodbye", "send_memes" + action_type: str, # 例如 "direct_reply", "send_memes" event_description_for_emotion: str ): - """处理发送消息成功后的通用状态更新。""" - if self.conversation.idle_chat: - await self.conversation.idle_chat.update_last_message_time(send_time) + """ + 在成功发送一条或多条消息(文本/表情)后,处理通用的状态更新。 + 这包括更新 IdleChat、清理未处理消息、更新追问状态以及关系/情绪。 - # 清理已处理的未读消息 (只清理在发送这条回复之前的、来自他人的消息) + Args: + observation_info (ObservationInfo): 当前观察信息。 + conversation_info (ConversationInfo): 当前对话信息。 + action_type (str): 执行的动作类型,用于决定追问逻辑。 + event_description_for_emotion (str): 用于情绪更新的事件描述。 + """ + current_event_time = time.time() # 获取当前时间作为事件发生时间 + + # 更新 IdleChat 的最后消息时间 + if self.conversation.idle_chat: + await self.conversation.idle_chat.update_last_message_time(current_event_time) + + # 清理在本次交互完成(即此函数被调用时)之前的、来自他人的未处理消息 current_unprocessed_messages = getattr(observation_info, "unprocessed_messages", []) message_ids_to_clear: Set[str] = set() - timestamp_before_sending = send_time - 0.001 # 确保是发送前的时间 + timestamp_before_current_interaction_completion = current_event_time - 0.001 # 确保是严格之前 + for msg in current_unprocessed_messages: msg_time = msg.get("time") msg_id = msg.get("message_id") @@ -152,40 +198,30 @@ class ActionHandler(ABC): if ( msg_id and msg_time - and sender_id != self.conversation.bot_qq_str - and msg_time < timestamp_before_sending + and sender_id != self.conversation.bot_qq_str # 是对方的消息 + and msg_time < timestamp_before_current_interaction_completion # 在本次交互完成前 ): message_ids_to_clear.add(msg_id) if message_ids_to_clear: self.logger.debug( - f"[私聊][{self.conversation.private_name}] 准备清理 {len(message_ids_to_clear)} 条发送前(他人)消息: {message_ids_to_clear}" + f"[私聊][{self.conversation.private_name}] 准备清理 {len(message_ids_to_clear)} 条交互完成前(他人)消息: {message_ids_to_clear}" ) await observation_info.clear_processed_messages(message_ids_to_clear) - else: - self.logger.debug(f"[私聊][{self.conversation.private_name}] 没有需要清理的发送前(他人)消息。") - # 更新追问状态 + # 更新追问状态 (last_successful_reply_action) other_new_msg_count_during_planning = getattr( conversation_info, "other_new_messages_during_planning_count", 0 ) if action_type in ["direct_reply", "send_new_message", "send_memes"]: if other_new_msg_count_during_planning > 0 and action_type == "direct_reply": - self.logger.debug( - f"[私聊][{self.conversation.private_name}] 因规划期间收到 {other_new_msg_count_during_planning} 条他人新消息,下一轮强制使用【初始回复】逻辑。" - ) + # 如果是直接回复,且规划期间有新消息,则下次不应追问 conversation_info.last_successful_reply_action = None else: - self.logger.debug( - f"[私聊][{self.conversation.private_name}] 成功执行 '{action_type}', 下一轮【允许】使用追问逻辑。" - ) + # 否则,记录本次成功的回复/表情动作为下次追问的依据 conversation_info.last_successful_reply_action = action_type - - # 更新实例消息计数和关系/情绪 - conversation_info.current_instance_message_count += 1 - self.logger.debug( - f"[私聊][{self.conversation.private_name}] 实例消息计数({global_config.BOT_NICKNAME}发送后)增加到: {conversation_info.current_instance_message_count}" - ) + + # 更新关系和情绪状态 await self._update_relationship_and_emotion(observation_info, conversation_info, event_description_for_emotion) async def _update_relationship_and_emotion( @@ -194,13 +230,22 @@ class ActionHandler(ABC): conversation_info: ConversationInfo, event_description: str ): - """辅助方法:更新关系和情绪状态。""" + """ + 辅助方法:调用关系更新器和情绪更新器。 + + Args: + observation_info (ObservationInfo): 当前观察信息。 + conversation_info (ConversationInfo): 当前对话信息。 + event_description (str): 触发更新的事件描述。 + """ + # 更新关系值(增量) if self.conversation.relationship_updater and self.conversation.chat_observer: await self.conversation.relationship_updater.update_relationship_incremental( conversation_info=conversation_info, observation_info=observation_info, chat_observer_for_history=self.conversation.chat_observer, ) + # 更新情绪状态 if self.conversation.emotion_updater and self.conversation.chat_observer: await self.conversation.emotion_updater.update_emotion_based_on_context( conversation_info=conversation_info, @@ -209,6 +254,47 @@ class ActionHandler(ABC): event_description=event_description, ) + async def _fetch_and_prepare_emoji_segment(self, emoji_query: str) -> Optional[Tuple[Seg, str, str]]: + """ + 根据表情查询字符串获取表情图片,将其转换为 Base64 编码, + 并准备好发送所需的 Seg 对象和相关描述文本。 + + Args: + emoji_query (str): 用于搜索表情的查询字符串。 + + Returns: + Optional[Tuple[Seg, str, str]]: 如果成功,返回一个元组包含: + - emoji_segment (Seg): 构造好的用于发送的表情 Seg 对象。 + - full_emoji_description (str): 表情的完整描述。 + - log_content_for_emoji (str): 用于日志记录的表情描述(可能是截断的)。 + 如果失败,则返回 None。 + """ + self.logger.info(f"[私聊][{self.conversation.private_name}] 尝试获取表情,查询: '{emoji_query}'") + try: + emoji_result = await emoji_manager.get_emoji_for_text(emoji_query) + if emoji_result: + emoji_path, full_emoji_description = emoji_result + self.logger.info(f"获取到表情包: {emoji_path}, 描述: {full_emoji_description}") + + # 将图片路径转换为纯 Base64 字符串 + emoji_b64_content = image_path_to_base64(emoji_path) + if not emoji_b64_content: + self.logger.error(f"无法将图片 {emoji_path} 转换为Base64。") + return None + + # 根据用户提供的片段,Seg type="emoji" data 为纯 Base64 字符串 + emoji_segment = Seg(type="emoji", data=emoji_b64_content) + # 用于发送器日志的截断描述 + log_content_for_emoji = full_emoji_description[-20:] + "..." + + return emoji_segment, full_emoji_description, log_content_for_emoji + else: + self.logger.warning(f"未能根据查询 '{emoji_query}' 获取到合适的表情包。") + return None + except Exception as e: + self.logger.error(f"获取或准备表情图片时出错: {e}", exc_info=True) + return None + class BaseTextReplyHandler(ActionHandler): """ @@ -226,43 +312,52 @@ class BaseTextReplyHandler(ActionHandler): 管理生成文本回复并检查其适用性的循环。 对于 send_new_message,它还处理来自 ReplyGenerator 的初始 JSON 决策。 - 返回: - is_suitable (bool): 是否找到了合适的回复或作出了发送决策。 - generated_content (Optional[str]): 要发送的内容;如果 ReplyGenerator 决定不发送 (send_new_message),则为 None。 - check_reason (str): 检查器或生成失败的原因。 - need_replan_from_checker (bool): 如果检查器要求重新规划。 - should_send_reply_for_new_message (bool): 特定于 send_new_message,如果 ReplyGenerator 决定发送则为 True。 + Args: + action_type (str): 当前动作类型 ("direct_reply" 或 "send_new_message")。 + observation_info (ObservationInfo): 当前观察信息。 + conversation_info (ConversationInfo): 当前对话信息。 + max_attempts (int): 最大尝试次数。 + + Returns: + Tuple[bool, Optional[str], str, bool, bool]: + - is_suitable (bool): 是否找到了合适的回复或作出了发送决策。 + - generated_content_to_send (Optional[str]): 检查通过后要发送的文本内容; + 如果 ReplyGenerator 决定不发送 (仅对 send_new_message),则为 None。 + - final_check_reason (str): 检查器或生成失败的原因。 + - need_replan (bool): 如果检查器明确要求重新规划。 + - should_send_reply_for_new_message (bool): 特定于 send_new_message, + 如果 ReplyGenerator 决定发送则为 True,否则为 False。对于 direct_reply,此值恒为 True。 """ reply_attempt_count = 0 - is_suitable = False - generated_content_to_send: Optional[str] = None - final_check_reason = "未开始检查" - need_replan = False - # should_send_reply_for_new_message 仅用于 send_new_message 动作类型 - should_send_reply_for_new_message = True if action_type == "direct_reply" else False # direct_reply 总是尝试发送 + is_suitable = False # 标记内容是否通过检查 + generated_content_to_send: Optional[str] = None # 最终要发送的文本 + final_check_reason = "未开始检查" # 最终检查原因 + need_replan = False # 是否需要重新规划 + # direct_reply 总是尝试发送;send_new_message 的初始值取决于RG + should_send_reply_for_new_message = True if action_type == "direct_reply" else False while reply_attempt_count < max_attempts and not is_suitable and not need_replan: reply_attempt_count += 1 log_prefix = f"[私聊][{self.conversation.private_name}] 尝试生成/检查 '{action_type}' (第 {reply_attempt_count}/{max_attempts} 次)..." self.logger.info(log_prefix) - self.conversation.state = ConversationState.GENERATING + self.conversation.state = ConversationState.GENERATING # 设置状态为生成中 if not self.conversation.reply_generator: - # 这个应该在 Conversation 初始化时就保证了,但以防万一 raise RuntimeError(f"ReplyGenerator 未为 {self.conversation.private_name} 初始化") + # 调用 ReplyGenerator 生成原始回复 raw_llm_output = await self.conversation.reply_generator.generate( observation_info, conversation_info, action_type=action_type ) self.logger.debug(f"{log_prefix} ReplyGenerator.generate 返回: '{raw_llm_output}'") + current_content_for_check = raw_llm_output # 当前待检查的内容 - current_content_for_check = raw_llm_output - + # 如果是 send_new_message 动作,需要解析 JSON 判断是否发送 if action_type == "send_new_message": parsed_json = None try: parsed_json = json.loads(raw_llm_output) - except json.JSONDecodeError: + except json.JSONDecodeError: # JSON 解析失败 self.logger.error(f"{log_prefix} ReplyGenerator 返回的不是有效的JSON: {raw_llm_output}") conversation_info.last_reply_rejection_reason = "回复生成器未返回有效JSON" conversation_info.last_rejected_reply_content = raw_llm_output @@ -272,15 +367,15 @@ class BaseTextReplyHandler(ActionHandler): generated_content_to_send = None # 明确不发送内容 break # 跳出重试循环 - if parsed_json: + if parsed_json: # JSON 解析成功 send_decision = parsed_json.get("send", "no").lower() generated_text_from_json = parsed_json.get("txt", "") # 如果不发送,txt可能是"no" - if send_decision == "yes": + if send_decision == "yes": # ReplyGenerator 决定发送 should_send_reply_for_new_message = True current_content_for_check = generated_text_from_json self.logger.info(f"{log_prefix} ReplyGenerator 决定发送消息。内容初步为: '{current_content_for_check[:100]}...'") - else: # send_decision is "no" + else: # ReplyGenerator 决定不发送 should_send_reply_for_new_message = False is_suitable = True # 决策已做出(不发送) final_check_reason = "回复生成器决定不发送" @@ -288,23 +383,27 @@ class BaseTextReplyHandler(ActionHandler): self.logger.info(f"{log_prefix} ReplyGenerator 决定不发送消息。") break # 跳出重试循环 - # 如果是 direct_reply 或者 send_new_message 且决定要发送,则检查内容 - if not current_content_for_check or current_content_for_check.startswith("抱歉") or current_content_for_check.strip() == "" or (action_type == "send_new_message" and current_content_for_check == "no" and should_send_reply_for_new_message): + # 检查生成的内容是否有效(适用于 direct_reply 或 send_new_message 且决定发送的情况) + if not current_content_for_check or \ + current_content_for_check.startswith("抱歉") or \ + current_content_for_check.strip() == "" or \ + (action_type == "send_new_message" and current_content_for_check == "no" and should_send_reply_for_new_message): warning_msg = f"{log_prefix} 生成内容无效或为错误提示" if action_type == "send_new_message" and current_content_for_check == "no" and should_send_reply_for_new_message: warning_msg += " (ReplyGenerator决定发送但文本为'no')" self.logger.warning(warning_msg + ",将进行下一次尝试 (如果适用)。") - final_check_reason = "生成内容无效" + final_check_reason = "生成内容无效" # 更新检查原因 conversation_info.last_reply_rejection_reason = final_check_reason conversation_info.last_rejected_reply_content = current_content_for_check - await asyncio.sleep(0.5) - continue + await asyncio.sleep(0.5) # 暂停后重试 + continue # 进入下一次循环 # --- 内容检查 --- - self.conversation.state = ConversationState.CHECKING + self.conversation.state = ConversationState.CHECKING # 设置状态为检查中 if not self.conversation.reply_checker: raise RuntimeError(f"ReplyChecker 未为 {self.conversation.private_name} 初始化") + # 准备检查器所需参数 current_goal_str = "" if conversation_info.goal_list: goal_item = conversation_info.goal_list[-1] @@ -314,6 +413,7 @@ class BaseTextReplyHandler(ActionHandler): chat_history_text_for_check = getattr(observation_info, "chat_history_str", "") current_time_value_for_check = observation_info.current_time_str or "获取时间失败" + # 调用 ReplyChecker if global_config.enable_pfc_reply_checker: self.logger.debug(f"{log_prefix} 调用 ReplyChecker 检查 (配置已启用)...") is_suitable_check, reason_check, need_replan_check = await self.conversation.reply_checker.check( @@ -324,41 +424,152 @@ class BaseTextReplyHandler(ActionHandler): self.logger.info( f"{log_prefix} ReplyChecker 结果: 合适={is_suitable_check}, 原因='{reason_check}', 需重规划={need_replan_check}" ) - else: + else: # ReplyChecker 未启用 is_suitable_check, reason_check, need_replan_check = True, "ReplyChecker 已通过配置关闭", False self.logger.debug(f"{log_prefix} [配置关闭] ReplyChecker 已跳过,默认回复为合适。") - is_suitable = is_suitable_check - final_check_reason = reason_check - need_replan = need_replan_check + is_suitable = is_suitable_check # 更新内容是否合适 + final_check_reason = reason_check # 更新检查原因 + need_replan = need_replan_check # 更新是否需要重规划 - if not is_suitable: + if not is_suitable: # 如果内容不合适 conversation_info.last_reply_rejection_reason = final_check_reason conversation_info.last_rejected_reply_content = current_content_for_check if final_check_reason == "机器人尝试发送重复消息" and not need_replan: self.logger.warning(f"{log_prefix} 回复因自身重复被拒绝。将重试。") - elif not need_replan and reply_attempt_count < max_attempts: + elif not need_replan and reply_attempt_count < max_attempts: # 如果不需要重规划且还有尝试次数 self.logger.warning(f"{log_prefix} 回复不合适: {final_check_reason}。将重试。") - else: # 需要重规划或达到最大次数 + else: # 需要重规划或已达到最大尝试次数 self.logger.warning(f"{log_prefix} 回复不合适且(需要重规划或已达最大次数): {final_check_reason}") break # 结束循环 await asyncio.sleep(0.5) # 重试前暂停 - else: # is_suitable is True - generated_content_to_send = current_content_for_check - conversation_info.last_reply_rejection_reason = None - conversation_info.last_rejected_reply_content = None + else: # 内容合适 + generated_content_to_send = current_content_for_check # 设置最终要发送的内容 + conversation_info.last_reply_rejection_reason = None # 清除上次拒绝原因 + conversation_info.last_rejected_reply_content = None # 清除上次拒绝内容 break # 成功,跳出循环 - # 确保即使循环结束,如果 should_send_reply_for_new_message 为 False,则 is_suitable 也为 True(表示决策完成) + # 确保 send_new_message 在 RG 决定不发送时,is_suitable 为 True,generated_content_to_send 为 None if action_type == "send_new_message" and not should_send_reply_for_new_message: is_suitable = True # 决策已完成(不发送) generated_content_to_send = None # 确认不发送任何内容 return is_suitable, generated_content_to_send, final_check_reason, need_replan, should_send_reply_for_new_message + async def _process_and_send_reply_with_optional_emoji( + self, + action_type: str, # "direct_reply" or "send_new_message" + observation_info: ObservationInfo, + conversation_info: ConversationInfo, + max_reply_attempts: int + ) -> Tuple[bool, bool, List[str], Optional[str], bool, str, bool]: + """ + 核心共享方法:处理文本生成/检查,获取表情,并按顺序发送。 + + Args: + action_type (str): "direct_reply" 或 "send_new_message"。 + observation_info (ObservationInfo): 当前观察信息。 + conversation_info (ConversationInfo): 当前对话信息。 + max_reply_attempts (int): 文本生成的最大尝试次数。 + + Returns: + Tuple[bool, bool, List[str], Optional[str], bool, str, bool]: + - sent_text_successfully (bool): 文本是否成功发送。 + - sent_emoji_successfully (bool): 表情是否成功发送。 + - final_reason_parts (List[str]): 描述发送结果的字符串列表。 + - full_emoji_description_if_sent (Optional[str]): 如果表情发送成功,其完整描述。 + - need_replan_from_text_check (bool): 文本检查是否要求重规划。 + - text_check_failure_reason (str): 文本检查失败的原因(如果适用)。 + - rg_decided_not_to_send_text (bool): ReplyGenerator是否决定不发送文本 (仅send_new_message)。 + """ + sent_text_successfully = False + sent_emoji_successfully = False + final_reason_parts: List[str] = [] + full_emoji_description_if_sent: Optional[str] = None + + # 1. 处理文本部分 + is_suitable_text, generated_text_content, text_check_reason, need_replan_text, rg_decided_to_send_text = \ + await self._generate_and_check_text_reply_loop( + action_type=action_type, + observation_info=observation_info, + conversation_info=conversation_info, + max_attempts=max_reply_attempts + ) + + text_to_send: Optional[str] = None + # 对于 send_new_message,只有当 RG 决定发送且内容合适时才有文本 + if action_type == "send_new_message": + if rg_decided_to_send_text and is_suitable_text and generated_text_content: + text_to_send = generated_text_content + # 对于 direct_reply,只要内容合适就有文本 + elif action_type == "direct_reply": + if is_suitable_text and generated_text_content: + text_to_send = generated_text_content + + rg_decided_not_to_send_text = (action_type == "send_new_message" and not rg_decided_to_send_text) + + # 2. 处理表情部分 + emoji_prepared_info: Optional[Tuple[Seg, str, str]] = None # (segment, full_description, log_description) + emoji_query = conversation_info.current_emoji_query + if emoji_query: + emoji_prepared_info = await self._fetch_and_prepare_emoji_segment(emoji_query) + # 清理查询,无论是否成功获取,避免重复使用 + conversation_info.current_emoji_query = None # 重要:在这里清理 + + # 3. 决定发送顺序并发送 + send_order: List[str] = [] + if text_to_send and emoji_prepared_info: # 文本和表情都有 + send_order = ["text", "emoji"] if random.random() < 0.5 else ["emoji", "text"] + elif text_to_send: # 只有文本 + send_order = ["text"] + elif emoji_prepared_info: # 只有表情 (可能是 direct_reply 带表情,或 send_new_message 时 RG 不发文本但有表情) + send_order = ["emoji"] + + for item_type in send_order: + current_send_time = time.time() # 每次发送前获取精确时间 + if item_type == "text" and text_to_send: + self.conversation.generated_reply = text_to_send # 用于日志和历史记录 + text_segment = Seg(type="text", data=text_to_send) + if await self._send_reply_or_segments([text_segment], text_to_send): + sent_text_successfully = True + await self._update_bot_message_in_history(current_send_time, text_to_send, observation_info) + if self.conversation.conversation_info: + self.conversation.conversation_info.current_instance_message_count +=1 + self.conversation.conversation_info.my_message_count += 1 # 文本发送成功,增加计数 + final_reason_parts.append(f"成功发送文本 ('{text_to_send[:20]}...')") + else: + final_reason_parts.append("发送文本失败") + # 如果文本发送失败,通常不应继续发送表情,除非有特殊需求 + break + elif item_type == "emoji" and emoji_prepared_info: + emoji_segment, full_emoji_desc, log_emoji_desc = emoji_prepared_info + if await self._send_reply_or_segments([emoji_segment], log_emoji_desc): + sent_emoji_successfully = True + full_emoji_description_if_sent = full_emoji_desc + await self._update_bot_message_in_history(current_send_time, full_emoji_desc, observation_info, "bot_emoji_") + if self.conversation.conversation_info: + self.conversation.conversation_info.current_instance_message_count +=1 + self.conversation.conversation_info.my_message_count += 1 # 表情发送成功,增加计数 + final_reason_parts.append(f"成功发送表情 ({full_emoji_desc})") + else: + final_reason_parts.append("发送表情失败") + # 如果表情发送失败,但文本已成功,也应记录 + if not text_to_send : # 如果只有表情且表情失败 + break + + return ( + sent_text_successfully, + sent_emoji_successfully, + final_reason_parts, + full_emoji_description_if_sent, + need_replan_text, + text_check_reason if not is_suitable_text else "文本检查通过或未执行", # 返回文本检查失败的原因 + rg_decided_not_to_send_text + ) + class DirectReplyHandler(BaseTextReplyHandler): - """处理直接回复动作的处理器。""" + """处理直接回复动作(direct_reply)的处理器。""" async def execute( self, reason: str, @@ -367,54 +578,71 @@ class DirectReplyHandler(BaseTextReplyHandler): action_start_time: float, current_action_record: dict ) -> tuple[bool, str, str]: + """ + 执行直接回复动作。 + 会尝试生成文本回复,并根据 current_emoji_query 发送附带表情。 + """ if not observation_info or not conversation_info: - return False, "error", "DirectReply 的 ObservationInfo 或 ConversationInfo 为空" + self.logger.error(f"[私聊][{self.conversation.private_name}] DirectReplyHandler: ObservationInfo 或 ConversationInfo 为空。") + return False, "error", "内部信息缺失,无法执行直接回复" - action_successful = False - final_status = "recall" - final_reason = "直接回复动作未成功执行" + action_successful = False # 整体动作是否成功 + final_status = "recall" # 默认最终状态 + final_reason = "直接回复动作未成功执行" # 默认最终原因 max_reply_attempts: int = getattr(global_config, "pfc_max_reply_attempts", 3) - is_suitable, generated_content, check_reason, need_replan, _ = await self._generate_and_check_text_reply_loop( + ( + sent_text_successfully, + sent_emoji_successfully, + reason_parts, + full_emoji_desc, + need_replan_from_text_check, + text_check_failure_reason, + _ # rg_decided_not_to_send_text, direct_reply 不关心这个 + ) = await self._process_and_send_reply_with_optional_emoji( action_type="direct_reply", observation_info=observation_info, conversation_info=conversation_info, - max_attempts=max_reply_attempts + max_reply_attempts=max_reply_attempts ) - if is_suitable and generated_content: - self.conversation.generated_reply = generated_content - timestamp_before_sending = time.time() - self.conversation.state = ConversationState.SENDING - text_segment = Seg(type="text", data=self.conversation.generated_reply) - send_success = await self._send_reply_or_segments([text_segment], self.conversation.generated_reply) - send_end_time = time.time() + # 根据发送结果决定最终状态 + if sent_text_successfully or sent_emoji_successfully: + action_successful = True + final_status = "done" + final_reason = "; ".join(reason_parts) if reason_parts else "成功完成操作" + + # 统一调用发送后状态更新 + event_desc_parts = [] + if sent_text_successfully and self.conversation.generated_reply : event_desc_parts.append(f"你回复了: '{self.conversation.generated_reply[:30]}...'") + if sent_emoji_successfully and full_emoji_desc: event_desc_parts.append(f"并发送了表情: '{full_emoji_desc}'") + event_desc = " ".join(event_desc_parts) if event_desc_parts else "机器人发送了消息" + await self._update_post_send_states(observation_info, conversation_info, "direct_reply", event_desc) - if send_success: - action_successful = True - final_status = "done" - final_reason = "成功发送直接回复" - await self._update_bot_message_in_history(send_end_time, self.conversation.generated_reply, observation_info) - event_desc = f"你直接回复了消息: '{self.conversation.generated_reply[:50]}...'" - await self._update_post_send_states(send_end_time, observation_info, conversation_info, "direct_reply", event_desc) - else: - final_status = "recall"; final_reason = "发送直接回复时失败"; action_successful = False - conversation_info.last_successful_reply_action = None - conversation_info.my_message_count = 0 - elif need_replan: - final_status = "recall"; final_reason = f"回复检查要求重新规划: {check_reason}" - conversation_info.last_successful_reply_action = None - else: # 达到最大尝试次数或生成内容无效 - final_status = "max_checker_attempts_failed" - final_reason = f"达到最大回复尝试次数或生成内容无效,检查原因: {check_reason}" + elif need_replan_from_text_check: # 文本检查要求重规划 + final_status = "recall" + final_reason = f"文本回复检查要求重新规划: {text_check_failure_reason}" + conversation_info.last_successful_reply_action = None # 重置追问状态 + else: # 文本和表情都未能发送,或者文本检查失败且不需重规划(已达最大尝试) + final_status = "max_checker_attempts_failed" if not need_replan_from_text_check else "recall" + final_reason = f"直接回复失败。文本检查: {text_check_failure_reason}. " + ("; ".join(reason_parts) if reason_parts else "") action_successful = False - conversation_info.last_successful_reply_action = None + conversation_info.last_successful_reply_action = None # 重置追问状态 - return action_successful, final_status, final_reason + # 清理 my_message_count (如果动作整体不成功,但部分发送了,需要调整) + if not action_successful and conversation_info: + # _process_and_send_reply_with_optional_emoji 内部会增加 my_message_count + # 如果这里 action_successful 为 False,说明可能部分发送了但整体认为是失败 + # 这种情况下 my_message_count 可能需要调整,但目前逻辑是每次成功发送都加1, + # 如果 action_successful 为 False,则 last_successful_reply_action 会被清空, + # 避免了不成功的追问。my_message_count 的精确回滚比较复杂,暂时依赖 last_successful_reply_action。 + pass + + return action_successful, final_status, final_reason.strip() class SendNewMessageHandler(BaseTextReplyHandler): - """处理发送新消息动作的处理器。""" + """处理发送新消息动作(send_new_message)的处理器。""" async def execute( self, reason: str, @@ -423,61 +651,81 @@ class SendNewMessageHandler(BaseTextReplyHandler): action_start_time: float, current_action_record: dict ) -> tuple[bool, str, str]: + """ + 执行发送新消息动作。 + 会先通过 ReplyGenerator 判断是否要发送文本,如果发送,则生成并检查文本。 + 同时,也可能根据 current_emoji_query 发送附带表情。 + """ if not observation_info or not conversation_info: - return False, "error", "SendNewMessage 的 ObservationInfo 或 ConversationInfo 为空" + self.logger.error(f"[私聊][{self.conversation.private_name}] SendNewMessageHandler: ObservationInfo 或 ConversationInfo 为空。") + return False, "error", "内部信息缺失,无法执行发送新消息" - action_successful = False - final_status = "recall" - final_reason = "发送新消息动作未成功执行" + action_successful = False # 整体动作是否成功 + final_status = "recall" # 默认最终状态 + final_reason = "发送新消息动作未成功执行" # 默认最终原因 max_reply_attempts: int = getattr(global_config, "pfc_max_reply_attempts", 3) - is_suitable, generated_content, check_reason, need_replan, should_send = await self._generate_and_check_text_reply_loop( + ( + sent_text_successfully, + sent_emoji_successfully, + reason_parts, + full_emoji_desc, + need_replan_from_text_check, + text_check_failure_reason, + rg_decided_not_to_send_text # 重要:获取RG是否决定不发文本 + ) = await self._process_and_send_reply_with_optional_emoji( action_type="send_new_message", observation_info=observation_info, conversation_info=conversation_info, - max_attempts=max_reply_attempts + max_reply_attempts=max_reply_attempts ) - if not should_send: # ReplyGenerator 决定不发送 - self.logger.info(f"[私聊][{self.conversation.private_name}] 动作 'send_new_message': ReplyGenerator 决定不发送。原因: {check_reason}") - final_status = "done_no_reply" - final_reason = check_reason if check_reason else "回复生成器决定不发送消息" - action_successful = True # 决策本身是成功的 - conversation_info.last_successful_reply_action = None - conversation_info.my_message_count = 0 - elif is_suitable and generated_content: # 决定发送且内容合适 - self.conversation.generated_reply = generated_content - timestamp_before_sending = time.time() - self.conversation.state = ConversationState.SENDING - text_segment = Seg(type="text", data=self.conversation.generated_reply) - send_success = await self._send_reply_or_segments([text_segment], self.conversation.generated_reply) - send_end_time = time.time() - - if send_success: + # 根据发送结果和RG的决策决定最终状态 + if rg_decided_not_to_send_text: # ReplyGenerator 明确决定不发送文本 + if sent_emoji_successfully: # 但表情成功发送了 action_successful = True - final_status = "done" - final_reason = "成功发送新消息" - await self._update_bot_message_in_history(send_end_time, self.conversation.generated_reply, observation_info) - event_desc = f"你发送了一条新消息: '{self.conversation.generated_reply[:50]}...'" - await self._update_post_send_states(send_end_time, observation_info, conversation_info, "send_new_message", event_desc) - else: - final_status = "recall"; final_reason = "发送新消息时失败"; action_successful = False - conversation_info.last_successful_reply_action = None - conversation_info.my_message_count = 0 - elif need_replan: - final_status = "recall"; final_reason = f"回复检查要求重新规划: {check_reason}" + final_status = "done" # 整体算完成,因为有内容发出 + final_reason = f"回复生成器决定不发送文本,但成功发送了附带表情 ({full_emoji_desc or '未知表情'})" + # 即使只发了表情,也算一次交互,可以更新post_send_states + event_desc = f"你发送了表情: '{full_emoji_desc or '未知表情'}' (文本未发送)" + await self._update_post_send_states(observation_info, conversation_info, "send_new_message", event_desc) + else: # RG不发文本,表情也没发出去或失败 + action_successful = True # 决策本身是成功的(决定不发) + final_status = "done_no_reply" # 标记为完成但无回复 + final_reason = text_check_failure_reason if text_check_failure_reason and text_check_failure_reason != "文本检查通过或未执行" else "回复生成器决定不发送消息,且无表情或表情发送失败" + conversation_info.last_successful_reply_action = None # 因为没有文本发出 + if self.conversation.conversation_info: # 确保 my_message_count 被重置 + self.conversation.conversation_info.my_message_count = 0 + elif sent_text_successfully or sent_emoji_successfully: # RG决定发文本(或未明确反对),且至少有一个发出去了 + action_successful = True + final_status = "done" + final_reason = "; ".join(reason_parts) if reason_parts else "成功完成操作" + + event_desc_parts = [] + if sent_text_successfully and self.conversation.generated_reply: event_desc_parts.append(f"你发送了新消息: '{self.conversation.generated_reply[:30]}...'") + if sent_emoji_successfully and full_emoji_desc: event_desc_parts.append(f"并发送了表情: '{full_emoji_desc}'") + event_desc = " ".join(event_desc_parts) if event_desc_parts else "机器人发送了消息" + await self._update_post_send_states(observation_info, conversation_info, "send_new_message", event_desc) + + elif need_replan_from_text_check: # 文本检查要求重规划 + final_status = "recall" + final_reason = f"文本回复检查要求重新规划: {text_check_failure_reason}" conversation_info.last_successful_reply_action = None - else: # 达到最大尝试次数或生成内容无效 - final_status = "max_checker_attempts_failed" - final_reason = f"达到最大回复尝试次数或生成内容无效,检查原因: {check_reason}" + else: # 文本和表情都未能发送(且RG没有明确说不发文本),或者文本检查失败且不需重规划 + final_status = "max_checker_attempts_failed" if not need_replan_from_text_check else "recall" + final_reason = f"发送新消息失败。文本检查: {text_check_failure_reason}. " + ("; ".join(reason_parts) if reason_parts else "") action_successful = False conversation_info.last_successful_reply_action = None + + if not action_successful and conversation_info: + # 同 DirectReplyHandler,my_message_count 的精确回滚依赖 last_successful_reply_action 的清除 + pass - return action_successful, final_status, final_reason + return action_successful, final_status, final_reason.strip() class SayGoodbyeHandler(ActionHandler): - """处理发送告别语动作的处理器。""" + """处理发送告别语动作(say_goodbye)的处理器。""" async def execute( self, reason: str, @@ -486,17 +734,23 @@ class SayGoodbyeHandler(ActionHandler): action_start_time: float, current_action_record: dict ) -> tuple[bool, str, str]: + """ + 执行发送告别语的动作。 + 会生成告别文本并发送,然后标记对话结束。 + """ if not observation_info or not conversation_info: - return False, "error", "SayGoodbye 的 ObservationInfo 或 ConversationInfo 为空" + self.logger.error(f"[私聊][{self.conversation.private_name}] SayGoodbyeHandler: ObservationInfo 或 ConversationInfo 为空。") + return False, "error", "内部信息缺失,无法执行告别" action_successful = False - final_status = "recall" - final_reason = "告别语动作未成功执行" + final_status = "recall" # 默认状态 + final_reason = "告别语动作未成功执行" # 默认原因 - self.conversation.state = ConversationState.GENERATING + self.conversation.state = ConversationState.GENERATING # 设置状态为生成中 if not self.conversation.reply_generator: - raise RuntimeError("ReplyGenerator 未初始化") + raise RuntimeError(f"ReplyGenerator 未为 {self.conversation.private_name} 初始化") + # 生成告别语内容 generated_content = await self.conversation.reply_generator.generate( observation_info, conversation_info, action_type="say_goodbye" ) @@ -504,38 +758,42 @@ class SayGoodbyeHandler(ActionHandler): f"[私聊][{self.conversation.private_name}] 动作 'say_goodbye': 生成内容: '{generated_content[:100]}...'" ) - if not generated_content or generated_content.startswith("抱歉"): + if not generated_content or generated_content.startswith("抱歉"): # 如果生成内容无效 self.logger.warning( f"[私聊][{self.conversation.private_name}] 动作 'say_goodbye': 生成内容为空或为错误提示,取消发送。" ) final_reason = "生成告别内容无效" - final_status = "done" - self.conversation.should_continue = False - action_successful = True # 即使不发送,结束对话的决策也算完成 - else: + final_status = "done" # 即使不发送,结束对话的决策也算完成 + self.conversation.should_continue = False # 标记对话结束 + action_successful = True # 动作(决策结束)本身算成功 + else: # 如果生成内容有效 self.conversation.generated_reply = generated_content - self.conversation.state = ConversationState.SENDING + self.conversation.state = ConversationState.SENDING # 设置状态为发送中 text_segment = Seg(type="text", data=self.conversation.generated_reply) send_success = await self._send_reply_or_segments([text_segment], self.conversation.generated_reply) send_end_time = time.time() - if send_success: + if send_success: # 如果发送成功 action_successful = True final_status = "done" final_reason = "成功发送告别语" - self.conversation.should_continue = False + self.conversation.should_continue = False # 标记对话结束 + if self.conversation.conversation_info: + self.conversation.conversation_info.current_instance_message_count +=1 + self.conversation.conversation_info.my_message_count += 1 # 告别语也算一次发言 await self._update_bot_message_in_history(send_end_time, self.conversation.generated_reply, observation_info) event_desc = f"你发送了告别消息: '{self.conversation.generated_reply[:50]}...'" - await self._update_post_send_states(send_end_time, observation_info, conversation_info, "say_goodbye", event_desc) - else: + # 注意:由于 should_continue 已设为 False,后续的 idle chat 更新可能意义不大,但情绪更新仍可进行 + await self._update_post_send_states(observation_info, conversation_info, "say_goodbye", event_desc) + else: # 如果发送失败 final_status = "recall"; final_reason = "发送告别语失败"; action_successful = False - self.conversation.should_continue = True # 发送失败则不结束 + self.conversation.should_continue = True # 发送失败则不立即结束对话,让其自然流转 return action_successful, final_status, final_reason class SendMemesHandler(ActionHandler): - """处理发送表情包动作的处理器。""" + """处理发送表情包动作(send_memes)的处理器。""" async def execute( self, reason: str, @@ -544,111 +802,132 @@ class SendMemesHandler(ActionHandler): action_start_time: float, current_action_record: dict ) -> tuple[bool, str, str]: + """ + 执行发送表情包的动作。 + 会根据 current_emoji_query 获取并发送表情。 + """ if not observation_info or not conversation_info: - return False, "error", "SendMemes 的 ObservationInfo 或 ConversationInfo 为空" + self.logger.error(f"[私聊][{self.conversation.private_name}] SendMemesHandler: ObservationInfo 或 ConversationInfo 为空。") + return False, "error", "内部信息缺失,无法发送表情包" action_successful = False - final_status = "recall" + final_status = "recall" # 默认状态 final_reason_prefix = "发送表情包" - final_reason = f"{final_reason_prefix}失败:未知原因" - self.conversation.state = ConversationState.GENERATING + final_reason = f"{final_reason_prefix}失败:未知原因" # 默认原因 + self.conversation.state = ConversationState.GENERATING # 或 SENDING_MEME emoji_query = conversation_info.current_emoji_query - if not emoji_query: + if not emoji_query: # 如果没有表情查询 final_reason = f"{final_reason_prefix}失败:缺少表情包查询语句" - conversation_info.last_successful_reply_action = None + # 此动作不依赖文本回复的追问状态,所以不修改 last_successful_reply_action return False, "recall", final_reason + + # 清理表情查询,因为我们要处理它了 + conversation_info.current_emoji_query = None - self.logger.info(f"[私聊][{self.conversation.private_name}] 动作 'send_memes': 使用查询 '{emoji_query}' 获取表情包...") - try: - emoji_result = await emoji_manager.get_emoji_for_text(emoji_query) - if emoji_result: - emoji_path, emoji_description = emoji_result - self.logger.info(f"获取到表情包: {emoji_path}, 描述: {emoji_description}") + emoji_prepared_info = await self._fetch_and_prepare_emoji_segment(emoji_query) - if not self.conversation.chat_stream: raise RuntimeError("ChatStream 未初始化") - emoji_b64_cq = image_path_to_base64(emoji_path) - if not emoji_b64_cq: raise ValueError(f"无法转换图片 {emoji_path} 为Base64") + if emoji_prepared_info: # 如果成功获取并准备了表情 + emoji_segment, full_emoji_description, log_emoji_description = emoji_prepared_info + send_success = await self._send_reply_or_segments([emoji_segment], log_emoji_description) + send_end_time = time.time() - image_segment = Seg(type="emoji", data=emoji_b64_cq) - send_success = await self._send_reply_or_segments([image_segment], emoji_description[-20:] + "...") - send_end_time = time.time() - - if send_success: - action_successful = True - final_status = "done" - final_reason = f"{final_reason_prefix}成功发送 ({emoji_description})" - await self._update_bot_message_in_history(send_end_time, emoji_description, observation_info, "bot_meme_") - event_desc = f"你发送了一个表情包 ({emoji_description})" - await self._update_post_send_states(send_end_time, observation_info, conversation_info, "send_memes", event_desc) - else: - final_status = "recall"; final_reason = f"{final_reason_prefix}失败:发送时出错" - else: - final_reason = f"{final_reason_prefix}失败:未找到合适表情包" - conversation_info.last_successful_reply_action = None - except Exception as e: - self.logger.error(f"处理表情包动作时出错: {e}", exc_info=True) - final_status = "error"; final_reason = f"{final_reason_prefix}失败:处理时出错 ({e})" - conversation_info.last_successful_reply_action = None + if send_success: # 如果发送成功 + action_successful = True + final_status = "done" + final_reason = f"{final_reason_prefix}成功发送 ({full_emoji_description})" + if self.conversation.conversation_info: + self.conversation.conversation_info.current_instance_message_count +=1 + self.conversation.conversation_info.my_message_count += 1 # 表情也算一次发言 + await self._update_bot_message_in_history(send_end_time, full_emoji_description, observation_info, "bot_meme_") + event_desc = f"你发送了一个表情包 ({full_emoji_description})" + await self._update_post_send_states(observation_info, conversation_info, "send_memes", event_desc) + else: # 如果发送失败 + final_status = "recall"; final_reason = f"{final_reason_prefix}失败:发送时出错" + else: # 如果未能获取或准备表情 + final_reason = f"{final_reason_prefix}失败:未找到或准备表情失败 ({emoji_query})" + # last_successful_reply_action 保持不变 return action_successful, final_status, final_reason class RethinkGoalHandler(ActionHandler): - """处理重新思考目标动作的处理器。""" + """处理重新思考目标动作(rethink_goal)的处理器。""" async def execute(self, reason: str, observation_info: Optional[ObservationInfo], conversation_info: Optional[ConversationInfo], action_start_time: float, current_action_record: dict) -> tuple[bool, str, str]: - if not conversation_info or not observation_info: return False, "error", "RethinkGoal 缺少信息" - self.conversation.state = ConversationState.RETHINKING - if not self.conversation.goal_analyzer: raise RuntimeError("GoalAnalyzer 未初始化") - await self.conversation.goal_analyzer.analyze_goal(conversation_info, observation_info) + """执行重新思考对话目标的动作。""" + if not conversation_info or not observation_info: + self.logger.error(f"[私聊][{self.conversation.private_name}] RethinkGoalHandler: ObservationInfo 或 ConversationInfo 为空。") + return False, "error", "内部信息缺失,无法重新思考目标" + self.conversation.state = ConversationState.RETHINKING # 设置状态为重新思考中 + if not self.conversation.goal_analyzer: + raise RuntimeError(f"GoalAnalyzer 未为 {self.conversation.private_name} 初始化") + await self.conversation.goal_analyzer.analyze_goal(conversation_info, observation_info) # 调用目标分析器 event_desc = "你重新思考了对话目标和方向" - await self._update_relationship_and_emotion(observation_info, conversation_info, event_desc) + await self._update_relationship_and_emotion(observation_info, conversation_info, event_desc) # 更新关系和情绪 return True, "done", "成功重新思考目标" class ListeningHandler(ActionHandler): - """处理倾听动作的处理器。""" + """处理倾听动作(listening)的处理器。""" async def execute(self, reason: str, observation_info: Optional[ObservationInfo], conversation_info: Optional[ConversationInfo], action_start_time: float, current_action_record: dict) -> tuple[bool, str, str]: - if not conversation_info or not observation_info: return False, "error", "Listening 缺少信息" - self.conversation.state = ConversationState.LISTENING - if not self.conversation.waiter: raise RuntimeError("Waiter 未初始化") - await self.conversation.waiter.wait_listening(conversation_info) + """执行倾听对方发言的动作。""" + if not conversation_info or not observation_info: + self.logger.error(f"[私聊][{self.conversation.private_name}] ListeningHandler: ObservationInfo 或 ConversationInfo 为空。") + return False, "error", "内部信息缺失,无法执行倾听" + self.conversation.state = ConversationState.LISTENING # 设置状态为倾听中 + if not self.conversation.waiter: + raise RuntimeError(f"Waiter 未为 {self.conversation.private_name} 初始化") + await self.conversation.waiter.wait_listening(conversation_info) # 调用等待器的倾听方法 event_desc = "你决定耐心倾听对方的发言" - await self._update_relationship_and_emotion(observation_info, conversation_info, event_desc) + await self._update_relationship_and_emotion(observation_info, conversation_info, event_desc) # 更新关系和情绪 + # listening 动作完成后,状态会由新消息或超时驱动,最终回到 ANALYZING return True, "done", "进入倾听状态" class EndConversationHandler(ActionHandler): - """处理结束对话动作的处理器。""" + """处理结束对话动作(end_conversation)的处理器。""" async def execute(self, reason: str, observation_info: Optional[ObservationInfo], conversation_info: Optional[ConversationInfo], action_start_time: float, current_action_record: dict) -> tuple[bool, str, str]: + """执行结束当前对话的动作。""" self.logger.info(f"[私聊][{self.conversation.private_name}] 动作 'end_conversation': 收到最终结束指令,停止对话...") - self.conversation.should_continue = False + self.conversation.should_continue = False # 标记对话不应继续,主循环会因此退出 + # 注意:最终的关系评估通常在 Conversation.stop() 方法中进行 return True, "done", "对话结束指令已执行" class BlockAndIgnoreHandler(ActionHandler): - """处理屏蔽并忽略动作的处理器。""" + """处理屏蔽并忽略对话动作(block_and_ignore)的处理器。""" async def execute(self, reason: str, observation_info: Optional[ObservationInfo], conversation_info: Optional[ConversationInfo], action_start_time: float, current_action_record: dict) -> tuple[bool, str, str]: - if not conversation_info or not observation_info: return False, "error", "BlockAndIgnore 缺少信息" + """执行屏蔽并忽略当前对话一段时间的动作。""" + if not conversation_info or not observation_info: # 防御性检查 + self.logger.error(f"[私聊][{self.conversation.private_name}] BlockAndIgnoreHandler: ObservationInfo 或 ConversationInfo 为空。") + return False, "error", "内部信息缺失,无法执行屏蔽" self.logger.info(f"[私聊][{self.conversation.private_name}] 动作 'block_and_ignore': 不想再理你了...") - ignore_duration_seconds = 10 * 60 - self.conversation.ignore_until_timestamp = time.time() + ignore_duration_seconds - self.conversation.state = ConversationState.IGNORED + ignore_duration_seconds = 10 * 60 # 例如忽略10分钟,可以配置 + self.conversation.ignore_until_timestamp = time.time() + ignore_duration_seconds # 设置忽略截止时间 + self.conversation.state = ConversationState.IGNORED # 设置状态为已忽略 event_desc = "当前对话让你感到不适,你决定暂时不再理会对方" - await self._update_relationship_and_emotion(observation_info, conversation_info, event_desc) + await self._update_relationship_and_emotion(observation_info, conversation_info, event_desc) # 更新关系和情绪 + # should_continue 仍为 True,但主循环会检查 ignore_until_timestamp return True, "done", f"已屏蔽并忽略对话 {ignore_duration_seconds // 60} 分钟" class WaitHandler(ActionHandler): - """处理等待动作的处理器。""" + """处理等待动作(wait)的处理器。""" async def execute(self, reason: str, observation_info: Optional[ObservationInfo], conversation_info: Optional[ConversationInfo], action_start_time: float, current_action_record: dict) -> tuple[bool, str, str]: - if not conversation_info or not observation_info: return False, "error", "Wait 缺少信息" - self.conversation.state = ConversationState.WAITING - if not self.conversation.waiter: raise RuntimeError("Waiter 未初始化") - timeout_occurred = await self.conversation.waiter.wait(conversation_info) + """执行等待对方回复的动作。""" + if not conversation_info or not observation_info: # 防御性检查 + self.logger.error(f"[私聊][{self.conversation.private_name}] WaitHandler: ObservationInfo 或 ConversationInfo 为空。") + return False, "error", "内部信息缺失,无法执行等待" + self.conversation.state = ConversationState.WAITING # 设置状态为等待中 + if not self.conversation.waiter: + raise RuntimeError(f"Waiter 未为 {self.conversation.private_name} 初始化") + timeout_occurred = await self.conversation.waiter.wait(conversation_info) # 调用等待器的常规等待方法 event_desc = "你等待对方回复,但对方长时间没有回应" if timeout_occurred else "你选择等待对方的回复" - await self._update_relationship_and_emotion(observation_info, conversation_info, event_desc) + await self._update_relationship_and_emotion(observation_info, conversation_info, event_desc) # 更新关系和情绪 + # wait 动作完成后,状态会由新消息或超时驱动,最终回到 ANALYZING return True, "done", "等待动作完成" class UnknownActionHandler(ActionHandler): - """处理未知动作的处理器。""" + """处理未知或无效动作的处理器。""" async def execute(self, reason: str, observation_info: Optional[ObservationInfo], conversation_info: Optional[ConversationInfo], action_start_time: float, current_action_record: dict) -> tuple[bool, str, str]: - action_name = current_action_record.get("action", "未知") - self.logger.warning(f"[私聊][{self.conversation.private_name}] 未知的动作类型: {action_name}") - return False, "recall", f"未知的动作类型: {action_name}" + """处理无法识别的动作类型。""" + action_name = current_action_record.get("action", "未知动作类型") # 从记录中获取动作名 + self.logger.warning(f"[私聊][{self.conversation.private_name}] 接收到未知的动作类型: {action_name}") + return False, "recall", f"未知的动作类型: {action_name}" # 标记为需要重新规划 + From b1fcc6b745c5fd3fd277745ae52e33d97b7ab44c Mon Sep 17 00:00:00 2001 From: Bakadax Date: Fri, 16 May 2025 12:51:21 +0800 Subject: [PATCH 4/5] ruff --- src/experimental/PFC/action_factory.py | 2 +- src/experimental/PFC/action_handlers.py | 24 ++++++++++++++---------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/experimental/PFC/action_factory.py b/src/experimental/PFC/action_factory.py index e04b88d0..d33590f4 100644 --- a/src/experimental/PFC/action_factory.py +++ b/src/experimental/PFC/action_factory.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Optional, Type, TYPE_CHECKING +from typing import Type, TYPE_CHECKING # 从 action_handlers.py 导入具体的处理器类 from .action_handlers import ( # 调整导入路径 diff --git a/src/experimental/PFC/action_handlers.py b/src/experimental/PFC/action_handlers.py index 1028cc67..310c2b27 100644 --- a/src/experimental/PFC/action_handlers.py +++ b/src/experimental/PFC/action_handlers.py @@ -1,7 +1,6 @@ from abc import ABC, abstractmethod import time import asyncio -import datetime import traceback import json import random @@ -15,9 +14,7 @@ from .pfc_types import ConversationState from .observation_info import ObservationInfo from .conversation_info import ConversationInfo from src.chat.utils.utils_image import image_path_to_base64 -from maim_message import Seg, UserInfo -from src.chat.message_receive.message import MessageSending, MessageSet -from src.chat.message_receive.message_sender import message_manager +from maim_message import Seg if TYPE_CHECKING: from .conversation import Conversation @@ -614,8 +611,10 @@ class DirectReplyHandler(BaseTextReplyHandler): # 统一调用发送后状态更新 event_desc_parts = [] - if sent_text_successfully and self.conversation.generated_reply : event_desc_parts.append(f"你回复了: '{self.conversation.generated_reply[:30]}...'") - if sent_emoji_successfully and full_emoji_desc: event_desc_parts.append(f"并发送了表情: '{full_emoji_desc}'") + if sent_text_successfully and self.conversation.generated_reply: + event_desc_parts.append(f"你回复了: '{self.conversation.generated_reply[:30]}...'") + if sent_emoji_successfully and full_emoji_desc: + event_desc_parts.append(f"并发送了表情: '{full_emoji_desc}'") event_desc = " ".join(event_desc_parts) if event_desc_parts else "机器人发送了消息" await self._update_post_send_states(observation_info, conversation_info, "direct_reply", event_desc) @@ -702,8 +701,10 @@ class SendNewMessageHandler(BaseTextReplyHandler): final_reason = "; ".join(reason_parts) if reason_parts else "成功完成操作" event_desc_parts = [] - if sent_text_successfully and self.conversation.generated_reply: event_desc_parts.append(f"你发送了新消息: '{self.conversation.generated_reply[:30]}...'") - if sent_emoji_successfully and full_emoji_desc: event_desc_parts.append(f"并发送了表情: '{full_emoji_desc}'") + if sent_text_successfully and self.conversation.generated_reply: + event_desc_parts.append(f"你发送了新消息: '{self.conversation.generated_reply[:30]}...'") + if sent_emoji_successfully and full_emoji_desc: + event_desc_parts.append(f"并发送了表情: '{full_emoji_desc}'") event_desc = " ".join(event_desc_parts) if event_desc_parts else "机器人发送了消息" await self._update_post_send_states(observation_info, conversation_info, "send_new_message", event_desc) @@ -786,7 +787,9 @@ class SayGoodbyeHandler(ActionHandler): # 注意:由于 should_continue 已设为 False,后续的 idle chat 更新可能意义不大,但情绪更新仍可进行 await self._update_post_send_states(observation_info, conversation_info, "say_goodbye", event_desc) else: # 如果发送失败 - final_status = "recall"; final_reason = "发送告别语失败"; action_successful = False + final_status = "recall" + final_reason = "发送告别语失败" + action_successful = False self.conversation.should_continue = True # 发送失败则不立即结束对话,让其自然流转 return action_successful, final_status, final_reason @@ -843,7 +846,8 @@ class SendMemesHandler(ActionHandler): event_desc = f"你发送了一个表情包 ({full_emoji_description})" await self._update_post_send_states(observation_info, conversation_info, "send_memes", event_desc) else: # 如果发送失败 - final_status = "recall"; final_reason = f"{final_reason_prefix}失败:发送时出错" + final_status = "recall" + final_reason = f"{final_reason_prefix}失败:发送时出错" else: # 如果未能获取或准备表情 final_reason = f"{final_reason_prefix}失败:未找到或准备表情失败 ({emoji_query})" # last_successful_reply_action 保持不变 From 484983094fee90dc5df43ccfc20944224a534bdb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 16 May 2025 04:51:50 +0000 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/experimental/PFC/PFC_idle/idle_chat.py | 4 +- src/experimental/PFC/action_factory.py | 14 +- src/experimental/PFC/action_handlers.py | 535 +++++++++++++-------- src/experimental/PFC/actions.py | 106 ++-- 4 files changed, 387 insertions(+), 272 deletions(-) diff --git a/src/experimental/PFC/PFC_idle/idle_chat.py b/src/experimental/PFC/PFC_idle/idle_chat.py index 1b551a32..59734b23 100644 --- a/src/experimental/PFC/PFC_idle/idle_chat.py +++ b/src/experimental/PFC/PFC_idle/idle_chat.py @@ -536,7 +536,9 @@ class IdleChat: try: segments = Seg(type="seglist", data=[Seg(type="text", data=content)]) logger.debug(f"[私聊][{self.private_name}]准备发送主动聊天消息: {content}") - await self.message_sender.send_message(chat_stream=chat_stream, segments=segments, reply_to_message=None, content=content) + await self.message_sender.send_message( + chat_stream=chat_stream, segments=segments, reply_to_message=None, content=content + ) logger.info(f"[私聊][{self.private_name}]成功主动发起聊天: {content}") except Exception as e: logger.error(f"[私聊][{self.private_name}]发送主动聊天消息失败: {str(e)}") diff --git a/src/experimental/PFC/action_factory.py b/src/experimental/PFC/action_factory.py index d33590f4..043ad740 100644 --- a/src/experimental/PFC/action_factory.py +++ b/src/experimental/PFC/action_factory.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod from typing import Type, TYPE_CHECKING # 从 action_handlers.py 导入具体的处理器类 -from .action_handlers import ( # 调整导入路径 +from .action_handlers import ( # 调整导入路径 ActionHandler, DirectReplyHandler, SendNewMessageHandler, @@ -17,10 +17,12 @@ from .action_handlers import ( # 调整导入路径 ) if TYPE_CHECKING: - from PFC.conversation import Conversation # 调整导入路径 + from PFC.conversation import Conversation # 调整导入路径 + class AbstractActionFactory(ABC): """抽象动作工厂接口。""" + @abstractmethod def create_action_handler(self, action_type: str, conversation: "Conversation") -> ActionHandler: """ @@ -35,8 +37,10 @@ class AbstractActionFactory(ABC): """ pass + class StandardActionFactory(AbstractActionFactory): """标准的动作工厂实现。""" + def create_action_handler(self, action_type: str, conversation: "Conversation") -> ActionHandler: """ 根据动作类型创建并返回具体的动作处理器实例。 @@ -53,10 +57,10 @@ class StandardActionFactory(AbstractActionFactory): "block_and_ignore": BlockAndIgnoreHandler, "wait": WaitHandler, } - handler_class = handler_map.get(action_type) # 获取对应的处理器类 + handler_class = handler_map.get(action_type) # 获取对应的处理器类 # 如果找到对应的处理器类 if handler_class: - return handler_class(conversation) # 创建并返回处理器实例 + return handler_class(conversation) # 创建并返回处理器实例 else: # 如果未找到,返回处理未知动作的默认处理器 - return UnknownActionHandler(conversation) \ No newline at end of file + return UnknownActionHandler(conversation) diff --git a/src/experimental/PFC/action_handlers.py b/src/experimental/PFC/action_handlers.py index 310c2b27..1f183c6e 100644 --- a/src/experimental/PFC/action_handlers.py +++ b/src/experimental/PFC/action_handlers.py @@ -4,7 +4,7 @@ import asyncio import traceback import json import random -from typing import Optional, Set, TYPE_CHECKING, List, Tuple, Dict # 确保导入 Dict +from typing import Optional, Set, TYPE_CHECKING, List, Tuple, Dict # 确保导入 Dict from src.chat.emoji_system.emoji_manager import emoji_manager from src.common.logger_manager import get_logger @@ -27,6 +27,7 @@ class ActionHandler(ABC): 处理动作的抽象基类。 每个具体的动作处理器都应继承此类并实现 execute 方法。 """ + def __init__(self, conversation: "Conversation"): """ 初始化动作处理器。 @@ -44,7 +45,7 @@ class ActionHandler(ABC): observation_info: Optional[ObservationInfo], conversation_info: Optional[ConversationInfo], action_start_time: float, - current_action_record: dict + current_action_record: dict, ) -> tuple[bool, str, str]: """ 执行具体的动作逻辑。 @@ -89,15 +90,15 @@ class ActionHandler(ABC): await self.conversation.direct_sender.send_message( chat_stream=self.conversation.chat_stream, segments=final_segments, - reply_to_message=None, # 私聊通常不引用回复 - content=content_for_log # 用于发送器内部的日志记录 + reply_to_message=None, # 私聊通常不引用回复 + content=content_for_log, # 用于发送器内部的日志记录 ) # 注意: my_message_count 的增加现在由具体的发送逻辑(文本或表情)处理后决定 return True except Exception as e: self.logger.error(f"[私聊][{self.conversation.private_name}] 发送消息时失败: {str(e)}") self.logger.error(f"[私聊][{self.conversation.private_name}] {traceback.format_exc()}") - self.conversation.state = ConversationState.ERROR # 发送失败则标记错误状态 + self.conversation.state = ConversationState.ERROR # 发送失败则标记错误状态 return False async def _update_bot_message_in_history( @@ -105,7 +106,7 @@ class ActionHandler(ABC): send_time: float, message_content: str, observation_info: ObservationInfo, - message_id_prefix: str = "bot_sent_" + message_id_prefix: str = "bot_sent_", ): """ 在机器人成功发送消息后,将该消息添加到 ObservationInfo 的聊天历史中。 @@ -122,15 +123,17 @@ class ActionHandler(ABC): # 构造机器人发送的消息字典 bot_message_dict: Dict[str, any] = { - "message_id": f"{message_id_prefix}{send_time:.3f}", # 使用更精确的时间戳 + "message_id": f"{message_id_prefix}{send_time:.3f}", # 使用更精确的时间戳 "time": send_time, "user_info": { "user_id": self.conversation.bot_qq_str, "user_nickname": global_config.BOT_NICKNAME, - "platform": self.conversation.chat_stream.platform if self.conversation.chat_stream else "unknown_platform", + "platform": self.conversation.chat_stream.platform + if self.conversation.chat_stream + else "unknown_platform", }, - "processed_plain_text": message_content, # 历史记录中的纯文本使用传入的 message_content - "detailed_plain_text": message_content, # 详细文本也使用相同内容 + "processed_plain_text": message_content, # 历史记录中的纯文本使用传入的 message_content + "detailed_plain_text": message_content, # 详细文本也使用相同内容 } observation_info.chat_history.append(bot_message_dict) observation_info.chat_history_count = len(observation_info.chat_history) @@ -145,7 +148,7 @@ class ActionHandler(ABC): observation_info.chat_history_count = len(observation_info.chat_history) # 更新用于 Prompt 的历史记录字符串 - history_slice_for_str = observation_info.chat_history[-30:] # 例如最近30条 + history_slice_for_str = observation_info.chat_history[-30:] # 例如最近30条 try: observation_info.chat_history_str = await build_readable_messages( history_slice_for_str, @@ -162,8 +165,8 @@ class ActionHandler(ABC): self, observation_info: ObservationInfo, conversation_info: ConversationInfo, - action_type: str, # 例如 "direct_reply", "send_memes" - event_description_for_emotion: str + action_type: str, # 例如 "direct_reply", "send_memes" + event_description_for_emotion: str, ): """ 在成功发送一条或多条消息(文本/表情)后,处理通用的状态更新。 @@ -175,7 +178,7 @@ class ActionHandler(ABC): action_type (str): 执行的动作类型,用于决定追问逻辑。 event_description_for_emotion (str): 用于情绪更新的事件描述。 """ - current_event_time = time.time() # 获取当前时间作为事件发生时间 + current_event_time = time.time() # 获取当前时间作为事件发生时间 # 更新 IdleChat 的最后消息时间 if self.conversation.idle_chat: @@ -184,7 +187,7 @@ class ActionHandler(ABC): # 清理在本次交互完成(即此函数被调用时)之前的、来自他人的未处理消息 current_unprocessed_messages = getattr(observation_info, "unprocessed_messages", []) message_ids_to_clear: Set[str] = set() - timestamp_before_current_interaction_completion = current_event_time - 0.001 # 确保是严格之前 + timestamp_before_current_interaction_completion = current_event_time - 0.001 # 确保是严格之前 for msg in current_unprocessed_messages: msg_time = msg.get("time") @@ -195,8 +198,8 @@ class ActionHandler(ABC): if ( msg_id and msg_time - and sender_id != self.conversation.bot_qq_str # 是对方的消息 - and msg_time < timestamp_before_current_interaction_completion # 在本次交互完成前 + and sender_id != self.conversation.bot_qq_str # 是对方的消息 + and msg_time < timestamp_before_current_interaction_completion # 在本次交互完成前 ): message_ids_to_clear.add(msg_id) @@ -207,9 +210,7 @@ class ActionHandler(ABC): await observation_info.clear_processed_messages(message_ids_to_clear) # 更新追问状态 (last_successful_reply_action) - other_new_msg_count_during_planning = getattr( - conversation_info, "other_new_messages_during_planning_count", 0 - ) + other_new_msg_count_during_planning = getattr(conversation_info, "other_new_messages_during_planning_count", 0) if action_type in ["direct_reply", "send_new_message", "send_memes"]: if other_new_msg_count_during_planning > 0 and action_type == "direct_reply": # 如果是直接回复,且规划期间有新消息,则下次不应追问 @@ -217,15 +218,12 @@ class ActionHandler(ABC): else: # 否则,记录本次成功的回复/表情动作为下次追问的依据 conversation_info.last_successful_reply_action = action_type - + # 更新关系和情绪状态 await self._update_relationship_and_emotion(observation_info, conversation_info, event_description_for_emotion) async def _update_relationship_and_emotion( - self, - observation_info: ObservationInfo, - conversation_info: ConversationInfo, - event_description: str + self, observation_info: ObservationInfo, conversation_info: ConversationInfo, event_description: str ): """ 辅助方法:调用关系更新器和情绪更新器。 @@ -272,18 +270,18 @@ class ActionHandler(ABC): if emoji_result: emoji_path, full_emoji_description = emoji_result self.logger.info(f"获取到表情包: {emoji_path}, 描述: {full_emoji_description}") - + # 将图片路径转换为纯 Base64 字符串 emoji_b64_content = image_path_to_base64(emoji_path) if not emoji_b64_content: self.logger.error(f"无法将图片 {emoji_path} 转换为Base64。") return None - + # 根据用户提供的片段,Seg type="emoji" data 为纯 Base64 字符串 emoji_segment = Seg(type="emoji", data=emoji_b64_content) # 用于发送器日志的截断描述 log_content_for_emoji = full_emoji_description[-20:] + "..." - + return emoji_segment, full_emoji_description, log_content_for_emoji else: self.logger.warning(f"未能根据查询 '{emoji_query}' 获取到合适的表情包。") @@ -298,12 +296,13 @@ class BaseTextReplyHandler(ActionHandler): 处理基于文本的回复动作的基类,包含生成-检查-重试的循环。 适用于 DirectReplyHandler 和 SendNewMessageHandler。 """ + async def _generate_and_check_text_reply_loop( self, - action_type: str, # "direct_reply" or "send_new_message" + action_type: str, # "direct_reply" or "send_new_message" observation_info: ObservationInfo, conversation_info: ConversationInfo, - max_attempts: int + max_attempts: int, ) -> Tuple[bool, Optional[str], str, bool, bool]: """ 管理生成文本回复并检查其适用性的循环。 @@ -326,10 +325,10 @@ class BaseTextReplyHandler(ActionHandler): 如果 ReplyGenerator 决定发送则为 True,否则为 False。对于 direct_reply,此值恒为 True。 """ reply_attempt_count = 0 - is_suitable = False # 标记内容是否通过检查 - generated_content_to_send: Optional[str] = None # 最终要发送的文本 - final_check_reason = "未开始检查" # 最终检查原因 - need_replan = False # 是否需要重新规划 + is_suitable = False # 标记内容是否通过检查 + generated_content_to_send: Optional[str] = None # 最终要发送的文本 + final_check_reason = "未开始检查" # 最终检查原因 + need_replan = False # 是否需要重新规划 # direct_reply 总是尝试发送;send_new_message 的初始值取决于RG should_send_reply_for_new_message = True if action_type == "direct_reply" else False @@ -338,7 +337,7 @@ class BaseTextReplyHandler(ActionHandler): log_prefix = f"[私聊][{self.conversation.private_name}] 尝试生成/检查 '{action_type}' (第 {reply_attempt_count}/{max_attempts} 次)..." self.logger.info(log_prefix) - self.conversation.state = ConversationState.GENERATING # 设置状态为生成中 + self.conversation.state = ConversationState.GENERATING # 设置状态为生成中 if not self.conversation.reply_generator: raise RuntimeError(f"ReplyGenerator 未为 {self.conversation.private_name} 初始化") @@ -347,56 +346,68 @@ class BaseTextReplyHandler(ActionHandler): observation_info, conversation_info, action_type=action_type ) self.logger.debug(f"{log_prefix} ReplyGenerator.generate 返回: '{raw_llm_output}'") - current_content_for_check = raw_llm_output # 当前待检查的内容 + current_content_for_check = raw_llm_output # 当前待检查的内容 # 如果是 send_new_message 动作,需要解析 JSON 判断是否发送 if action_type == "send_new_message": parsed_json = None try: parsed_json = json.loads(raw_llm_output) - except json.JSONDecodeError: # JSON 解析失败 + except json.JSONDecodeError: # JSON 解析失败 self.logger.error(f"{log_prefix} ReplyGenerator 返回的不是有效的JSON: {raw_llm_output}") conversation_info.last_reply_rejection_reason = "回复生成器未返回有效JSON" conversation_info.last_rejected_reply_content = raw_llm_output - should_send_reply_for_new_message = False # 标记不发送 - is_suitable = True # 决策已做出(不发送),所以认为是 "suitable" 以跳出循环 + should_send_reply_for_new_message = False # 标记不发送 + is_suitable = True # 决策已做出(不发送),所以认为是 "suitable" 以跳出循环 final_check_reason = "回复生成器JSON解析失败,决定不发送" - generated_content_to_send = None # 明确不发送内容 - break # 跳出重试循环 + generated_content_to_send = None # 明确不发送内容 + break # 跳出重试循环 - if parsed_json: # JSON 解析成功 + if parsed_json: # JSON 解析成功 send_decision = parsed_json.get("send", "no").lower() - generated_text_from_json = parsed_json.get("txt", "") # 如果不发送,txt可能是"no" + generated_text_from_json = parsed_json.get("txt", "") # 如果不发送,txt可能是"no" - if send_decision == "yes": # ReplyGenerator 决定发送 + if send_decision == "yes": # ReplyGenerator 决定发送 should_send_reply_for_new_message = True current_content_for_check = generated_text_from_json - self.logger.info(f"{log_prefix} ReplyGenerator 决定发送消息。内容初步为: '{current_content_for_check[:100]}...'") - else: # ReplyGenerator 决定不发送 + self.logger.info( + f"{log_prefix} ReplyGenerator 决定发送消息。内容初步为: '{current_content_for_check[:100]}...'" + ) + else: # ReplyGenerator 决定不发送 should_send_reply_for_new_message = False - is_suitable = True # 决策已做出(不发送) + is_suitable = True # 决策已做出(不发送) final_check_reason = "回复生成器决定不发送" generated_content_to_send = None self.logger.info(f"{log_prefix} ReplyGenerator 决定不发送消息。") - break # 跳出重试循环 - + break # 跳出重试循环 + # 检查生成的内容是否有效(适用于 direct_reply 或 send_new_message 且决定发送的情况) - if not current_content_for_check or \ - current_content_for_check.startswith("抱歉") or \ - current_content_for_check.strip() == "" or \ - (action_type == "send_new_message" and current_content_for_check == "no" and should_send_reply_for_new_message): + if ( + not current_content_for_check + or current_content_for_check.startswith("抱歉") + or current_content_for_check.strip() == "" + or ( + action_type == "send_new_message" + and current_content_for_check == "no" + and should_send_reply_for_new_message + ) + ): warning_msg = f"{log_prefix} 生成内容无效或为错误提示" - if action_type == "send_new_message" and current_content_for_check == "no" and should_send_reply_for_new_message: - warning_msg += " (ReplyGenerator决定发送但文本为'no')" + if ( + action_type == "send_new_message" + and current_content_for_check == "no" + and should_send_reply_for_new_message + ): + warning_msg += " (ReplyGenerator决定发送但文本为'no')" self.logger.warning(warning_msg + ",将进行下一次尝试 (如果适用)。") - final_check_reason = "生成内容无效" # 更新检查原因 + final_check_reason = "生成内容无效" # 更新检查原因 conversation_info.last_reply_rejection_reason = final_check_reason conversation_info.last_rejected_reply_content = current_content_for_check - await asyncio.sleep(0.5) # 暂停后重试 - continue # 进入下一次循环 + await asyncio.sleep(0.5) # 暂停后重试 + continue # 进入下一次循环 # --- 内容检查 --- - self.conversation.state = ConversationState.CHECKING # 设置状态为检查中 + self.conversation.state = ConversationState.CHECKING # 设置状态为检查中 if not self.conversation.reply_checker: raise RuntimeError(f"ReplyChecker 未为 {self.conversation.private_name} 初始化") @@ -414,51 +425,60 @@ class BaseTextReplyHandler(ActionHandler): if global_config.enable_pfc_reply_checker: self.logger.debug(f"{log_prefix} 调用 ReplyChecker 检查 (配置已启用)...") is_suitable_check, reason_check, need_replan_check = await self.conversation.reply_checker.check( - reply=current_content_for_check, goal=current_goal_str, - chat_history=chat_history_for_check, chat_history_text=chat_history_text_for_check, - current_time_str=current_time_value_for_check, retry_count=(reply_attempt_count - 1) + reply=current_content_for_check, + goal=current_goal_str, + chat_history=chat_history_for_check, + chat_history_text=chat_history_text_for_check, + current_time_str=current_time_value_for_check, + retry_count=(reply_attempt_count - 1), ) self.logger.info( f"{log_prefix} ReplyChecker 结果: 合适={is_suitable_check}, 原因='{reason_check}', 需重规划={need_replan_check}" ) - else: # ReplyChecker 未启用 + else: # ReplyChecker 未启用 is_suitable_check, reason_check, need_replan_check = True, "ReplyChecker 已通过配置关闭", False self.logger.debug(f"{log_prefix} [配置关闭] ReplyChecker 已跳过,默认回复为合适。") - is_suitable = is_suitable_check # 更新内容是否合适 - final_check_reason = reason_check # 更新检查原因 - need_replan = need_replan_check # 更新是否需要重规划 + is_suitable = is_suitable_check # 更新内容是否合适 + final_check_reason = reason_check # 更新检查原因 + need_replan = need_replan_check # 更新是否需要重规划 - if not is_suitable: # 如果内容不合适 + if not is_suitable: # 如果内容不合适 conversation_info.last_reply_rejection_reason = final_check_reason conversation_info.last_rejected_reply_content = current_content_for_check if final_check_reason == "机器人尝试发送重复消息" and not need_replan: self.logger.warning(f"{log_prefix} 回复因自身重复被拒绝。将重试。") - elif not need_replan and reply_attempt_count < max_attempts: # 如果不需要重规划且还有尝试次数 + elif not need_replan and reply_attempt_count < max_attempts: # 如果不需要重规划且还有尝试次数 self.logger.warning(f"{log_prefix} 回复不合适: {final_check_reason}。将重试。") - else: # 需要重规划或已达到最大尝试次数 + else: # 需要重规划或已达到最大尝试次数 self.logger.warning(f"{log_prefix} 回复不合适且(需要重规划或已达最大次数): {final_check_reason}") - break # 结束循环 - await asyncio.sleep(0.5) # 重试前暂停 - else: # 内容合适 - generated_content_to_send = current_content_for_check # 设置最终要发送的内容 - conversation_info.last_reply_rejection_reason = None # 清除上次拒绝原因 - conversation_info.last_rejected_reply_content = None # 清除上次拒绝内容 - break # 成功,跳出循环 - + break # 结束循环 + await asyncio.sleep(0.5) # 重试前暂停 + else: # 内容合适 + generated_content_to_send = current_content_for_check # 设置最终要发送的内容 + conversation_info.last_reply_rejection_reason = None # 清除上次拒绝原因 + conversation_info.last_rejected_reply_content = None # 清除上次拒绝内容 + break # 成功,跳出循环 + # 确保 send_new_message 在 RG 决定不发送时,is_suitable 为 True,generated_content_to_send 为 None if action_type == "send_new_message" and not should_send_reply_for_new_message: - is_suitable = True # 决策已完成(不发送) - generated_content_to_send = None # 确认不发送任何内容 + is_suitable = True # 决策已完成(不发送) + generated_content_to_send = None # 确认不发送任何内容 - return is_suitable, generated_content_to_send, final_check_reason, need_replan, should_send_reply_for_new_message + return ( + is_suitable, + generated_content_to_send, + final_check_reason, + need_replan, + should_send_reply_for_new_message, + ) async def _process_and_send_reply_with_optional_emoji( self, - action_type: str, # "direct_reply" or "send_new_message" + action_type: str, # "direct_reply" or "send_new_message" observation_info: ObservationInfo, conversation_info: ConversationInfo, - max_reply_attempts: int + max_reply_attempts: int, ) -> Tuple[bool, bool, List[str], Optional[str], bool, str, bool]: """ 核心共享方法:处理文本生成/检查,获取表情,并按顺序发送。 @@ -483,15 +503,20 @@ class BaseTextReplyHandler(ActionHandler): sent_emoji_successfully = False final_reason_parts: List[str] = [] full_emoji_description_if_sent: Optional[str] = None - + # 1. 处理文本部分 - is_suitable_text, generated_text_content, text_check_reason, need_replan_text, rg_decided_to_send_text = \ - await self._generate_and_check_text_reply_loop( - action_type=action_type, - observation_info=observation_info, - conversation_info=conversation_info, - max_attempts=max_reply_attempts - ) + ( + is_suitable_text, + generated_text_content, + text_check_reason, + need_replan_text, + rg_decided_to_send_text, + ) = await self._generate_and_check_text_reply_loop( + action_type=action_type, + observation_info=observation_info, + conversation_info=conversation_info, + max_attempts=max_reply_attempts, + ) text_to_send: Optional[str] = None # 对于 send_new_message,只有当 RG 决定发送且内容合适时才有文本 @@ -502,90 +527,95 @@ class BaseTextReplyHandler(ActionHandler): elif action_type == "direct_reply": if is_suitable_text and generated_text_content: text_to_send = generated_text_content - - rg_decided_not_to_send_text = (action_type == "send_new_message" and not rg_decided_to_send_text) + + rg_decided_not_to_send_text = action_type == "send_new_message" and not rg_decided_to_send_text # 2. 处理表情部分 - emoji_prepared_info: Optional[Tuple[Seg, str, str]] = None # (segment, full_description, log_description) + emoji_prepared_info: Optional[Tuple[Seg, str, str]] = None # (segment, full_description, log_description) emoji_query = conversation_info.current_emoji_query if emoji_query: emoji_prepared_info = await self._fetch_and_prepare_emoji_segment(emoji_query) # 清理查询,无论是否成功获取,避免重复使用 - conversation_info.current_emoji_query = None # 重要:在这里清理 - + conversation_info.current_emoji_query = None # 重要:在这里清理 + # 3. 决定发送顺序并发送 send_order: List[str] = [] - if text_to_send and emoji_prepared_info: # 文本和表情都有 + if text_to_send and emoji_prepared_info: # 文本和表情都有 send_order = ["text", "emoji"] if random.random() < 0.5 else ["emoji", "text"] - elif text_to_send: # 只有文本 + elif text_to_send: # 只有文本 send_order = ["text"] - elif emoji_prepared_info: # 只有表情 (可能是 direct_reply 带表情,或 send_new_message 时 RG 不发文本但有表情) + elif emoji_prepared_info: # 只有表情 (可能是 direct_reply 带表情,或 send_new_message 时 RG 不发文本但有表情) send_order = ["emoji"] for item_type in send_order: - current_send_time = time.time() # 每次发送前获取精确时间 + current_send_time = time.time() # 每次发送前获取精确时间 if item_type == "text" and text_to_send: - self.conversation.generated_reply = text_to_send # 用于日志和历史记录 + self.conversation.generated_reply = text_to_send # 用于日志和历史记录 text_segment = Seg(type="text", data=text_to_send) if await self._send_reply_or_segments([text_segment], text_to_send): sent_text_successfully = True await self._update_bot_message_in_history(current_send_time, text_to_send, observation_info) if self.conversation.conversation_info: - self.conversation.conversation_info.current_instance_message_count +=1 - self.conversation.conversation_info.my_message_count += 1 # 文本发送成功,增加计数 + self.conversation.conversation_info.current_instance_message_count += 1 + self.conversation.conversation_info.my_message_count += 1 # 文本发送成功,增加计数 final_reason_parts.append(f"成功发送文本 ('{text_to_send[:20]}...')") else: final_reason_parts.append("发送文本失败") # 如果文本发送失败,通常不应继续发送表情,除非有特殊需求 - break + break elif item_type == "emoji" and emoji_prepared_info: emoji_segment, full_emoji_desc, log_emoji_desc = emoji_prepared_info if await self._send_reply_or_segments([emoji_segment], log_emoji_desc): sent_emoji_successfully = True full_emoji_description_if_sent = full_emoji_desc - await self._update_bot_message_in_history(current_send_time, full_emoji_desc, observation_info, "bot_emoji_") + await self._update_bot_message_in_history( + current_send_time, full_emoji_desc, observation_info, "bot_emoji_" + ) if self.conversation.conversation_info: - self.conversation.conversation_info.current_instance_message_count +=1 - self.conversation.conversation_info.my_message_count += 1 # 表情发送成功,增加计数 + self.conversation.conversation_info.current_instance_message_count += 1 + self.conversation.conversation_info.my_message_count += 1 # 表情发送成功,增加计数 final_reason_parts.append(f"成功发送表情 ({full_emoji_desc})") else: final_reason_parts.append("发送表情失败") # 如果表情发送失败,但文本已成功,也应记录 - if not text_to_send : # 如果只有表情且表情失败 + if not text_to_send: # 如果只有表情且表情失败 break - + return ( sent_text_successfully, sent_emoji_successfully, final_reason_parts, full_emoji_description_if_sent, need_replan_text, - text_check_reason if not is_suitable_text else "文本检查通过或未执行", # 返回文本检查失败的原因 - rg_decided_not_to_send_text + text_check_reason if not is_suitable_text else "文本检查通过或未执行", # 返回文本检查失败的原因 + rg_decided_not_to_send_text, ) class DirectReplyHandler(BaseTextReplyHandler): """处理直接回复动作(direct_reply)的处理器。""" + async def execute( self, reason: str, observation_info: Optional[ObservationInfo], conversation_info: Optional[ConversationInfo], action_start_time: float, - current_action_record: dict + current_action_record: dict, ) -> tuple[bool, str, str]: """ 执行直接回复动作。 会尝试生成文本回复,并根据 current_emoji_query 发送附带表情。 """ if not observation_info or not conversation_info: - self.logger.error(f"[私聊][{self.conversation.private_name}] DirectReplyHandler: ObservationInfo 或 ConversationInfo 为空。") + self.logger.error( + f"[私聊][{self.conversation.private_name}] DirectReplyHandler: ObservationInfo 或 ConversationInfo 为空。" + ) return False, "error", "内部信息缺失,无法执行直接回复" - action_successful = False # 整体动作是否成功 - final_status = "recall" # 默认最终状态 - final_reason = "直接回复动作未成功执行" # 默认最终原因 + action_successful = False # 整体动作是否成功 + final_status = "recall" # 默认最终状态 + final_reason = "直接回复动作未成功执行" # 默认最终原因 max_reply_attempts: int = getattr(global_config, "pfc_max_reply_attempts", 3) ( @@ -595,12 +625,12 @@ class DirectReplyHandler(BaseTextReplyHandler): full_emoji_desc, need_replan_from_text_check, text_check_failure_reason, - _ # rg_decided_not_to_send_text, direct_reply 不关心这个 + _, # rg_decided_not_to_send_text, direct_reply 不关心这个 ) = await self._process_and_send_reply_with_optional_emoji( action_type="direct_reply", observation_info=observation_info, conversation_info=conversation_info, - max_reply_attempts=max_reply_attempts + max_reply_attempts=max_reply_attempts, ) # 根据发送结果决定最终状态 @@ -608,7 +638,7 @@ class DirectReplyHandler(BaseTextReplyHandler): action_successful = True final_status = "done" final_reason = "; ".join(reason_parts) if reason_parts else "成功完成操作" - + # 统一调用发送后状态更新 event_desc_parts = [] if sent_text_successfully and self.conversation.generated_reply: @@ -618,16 +648,18 @@ class DirectReplyHandler(BaseTextReplyHandler): event_desc = " ".join(event_desc_parts) if event_desc_parts else "机器人发送了消息" await self._update_post_send_states(observation_info, conversation_info, "direct_reply", event_desc) - elif need_replan_from_text_check: # 文本检查要求重规划 + elif need_replan_from_text_check: # 文本检查要求重规划 final_status = "recall" final_reason = f"文本回复检查要求重新规划: {text_check_failure_reason}" - conversation_info.last_successful_reply_action = None # 重置追问状态 - else: # 文本和表情都未能发送,或者文本检查失败且不需重规划(已达最大尝试) + conversation_info.last_successful_reply_action = None # 重置追问状态 + else: # 文本和表情都未能发送,或者文本检查失败且不需重规划(已达最大尝试) final_status = "max_checker_attempts_failed" if not need_replan_from_text_check else "recall" - final_reason = f"直接回复失败。文本检查: {text_check_failure_reason}. " + ("; ".join(reason_parts) if reason_parts else "") + final_reason = f"直接回复失败。文本检查: {text_check_failure_reason}. " + ( + "; ".join(reason_parts) if reason_parts else "" + ) action_successful = False - conversation_info.last_successful_reply_action = None # 重置追问状态 - + conversation_info.last_successful_reply_action = None # 重置追问状态 + # 清理 my_message_count (如果动作整体不成功,但部分发送了,需要调整) if not action_successful and conversation_info: # _process_and_send_reply_with_optional_emoji 内部会增加 my_message_count @@ -642,13 +674,14 @@ class DirectReplyHandler(BaseTextReplyHandler): class SendNewMessageHandler(BaseTextReplyHandler): """处理发送新消息动作(send_new_message)的处理器。""" + async def execute( self, reason: str, observation_info: Optional[ObservationInfo], conversation_info: Optional[ConversationInfo], action_start_time: float, - current_action_record: dict + current_action_record: dict, ) -> tuple[bool, str, str]: """ 执行发送新消息动作。 @@ -656,12 +689,14 @@ class SendNewMessageHandler(BaseTextReplyHandler): 同时,也可能根据 current_emoji_query 发送附带表情。 """ if not observation_info or not conversation_info: - self.logger.error(f"[私聊][{self.conversation.private_name}] SendNewMessageHandler: ObservationInfo 或 ConversationInfo 为空。") + self.logger.error( + f"[私聊][{self.conversation.private_name}] SendNewMessageHandler: ObservationInfo 或 ConversationInfo 为空。" + ) return False, "error", "内部信息缺失,无法执行发送新消息" - action_successful = False # 整体动作是否成功 - final_status = "recall" # 默认最终状态 - final_reason = "发送新消息动作未成功执行" # 默认最终原因 + action_successful = False # 整体动作是否成功 + final_status = "recall" # 默认最终状态 + final_reason = "发送新消息动作未成功执行" # 默认最终原因 max_reply_attempts: int = getattr(global_config, "pfc_max_reply_attempts", 3) ( @@ -671,35 +706,39 @@ class SendNewMessageHandler(BaseTextReplyHandler): full_emoji_desc, need_replan_from_text_check, text_check_failure_reason, - rg_decided_not_to_send_text # 重要:获取RG是否决定不发文本 + rg_decided_not_to_send_text, # 重要:获取RG是否决定不发文本 ) = await self._process_and_send_reply_with_optional_emoji( action_type="send_new_message", observation_info=observation_info, conversation_info=conversation_info, - max_reply_attempts=max_reply_attempts + max_reply_attempts=max_reply_attempts, ) # 根据发送结果和RG的决策决定最终状态 - if rg_decided_not_to_send_text: # ReplyGenerator 明确决定不发送文本 - if sent_emoji_successfully: # 但表情成功发送了 + if rg_decided_not_to_send_text: # ReplyGenerator 明确决定不发送文本 + if sent_emoji_successfully: # 但表情成功发送了 action_successful = True - final_status = "done" # 整体算完成,因为有内容发出 + final_status = "done" # 整体算完成,因为有内容发出 final_reason = f"回复生成器决定不发送文本,但成功发送了附带表情 ({full_emoji_desc or '未知表情'})" # 即使只发了表情,也算一次交互,可以更新post_send_states event_desc = f"你发送了表情: '{full_emoji_desc or '未知表情'}' (文本未发送)" await self._update_post_send_states(observation_info, conversation_info, "send_new_message", event_desc) - else: # RG不发文本,表情也没发出去或失败 - action_successful = True # 决策本身是成功的(决定不发) - final_status = "done_no_reply" # 标记为完成但无回复 - final_reason = text_check_failure_reason if text_check_failure_reason and text_check_failure_reason != "文本检查通过或未执行" else "回复生成器决定不发送消息,且无表情或表情发送失败" - conversation_info.last_successful_reply_action = None # 因为没有文本发出 - if self.conversation.conversation_info: # 确保 my_message_count 被重置 - self.conversation.conversation_info.my_message_count = 0 - elif sent_text_successfully or sent_emoji_successfully: # RG决定发文本(或未明确反对),且至少有一个发出去了 + else: # RG不发文本,表情也没发出去或失败 + action_successful = True # 决策本身是成功的(决定不发) + final_status = "done_no_reply" # 标记为完成但无回复 + final_reason = ( + text_check_failure_reason + if text_check_failure_reason and text_check_failure_reason != "文本检查通过或未执行" + else "回复生成器决定不发送消息,且无表情或表情发送失败" + ) + conversation_info.last_successful_reply_action = None # 因为没有文本发出 + if self.conversation.conversation_info: # 确保 my_message_count 被重置 + self.conversation.conversation_info.my_message_count = 0 + elif sent_text_successfully or sent_emoji_successfully: # RG决定发文本(或未明确反对),且至少有一个发出去了 action_successful = True final_status = "done" final_reason = "; ".join(reason_parts) if reason_parts else "成功完成操作" - + event_desc_parts = [] if sent_text_successfully and self.conversation.generated_reply: event_desc_parts.append(f"你发送了新消息: '{self.conversation.generated_reply[:30]}...'") @@ -708,46 +747,51 @@ class SendNewMessageHandler(BaseTextReplyHandler): event_desc = " ".join(event_desc_parts) if event_desc_parts else "机器人发送了消息" await self._update_post_send_states(observation_info, conversation_info, "send_new_message", event_desc) - elif need_replan_from_text_check: # 文本检查要求重规划 + elif need_replan_from_text_check: # 文本检查要求重规划 final_status = "recall" final_reason = f"文本回复检查要求重新规划: {text_check_failure_reason}" conversation_info.last_successful_reply_action = None - else: # 文本和表情都未能发送(且RG没有明确说不发文本),或者文本检查失败且不需重规划 + else: # 文本和表情都未能发送(且RG没有明确说不发文本),或者文本检查失败且不需重规划 final_status = "max_checker_attempts_failed" if not need_replan_from_text_check else "recall" - final_reason = f"发送新消息失败。文本检查: {text_check_failure_reason}. " + ("; ".join(reason_parts) if reason_parts else "") + final_reason = f"发送新消息失败。文本检查: {text_check_failure_reason}. " + ( + "; ".join(reason_parts) if reason_parts else "" + ) action_successful = False conversation_info.last_successful_reply_action = None - + if not action_successful and conversation_info: # 同 DirectReplyHandler,my_message_count 的精确回滚依赖 last_successful_reply_action 的清除 pass - + return action_successful, final_status, final_reason.strip() class SayGoodbyeHandler(ActionHandler): """处理发送告别语动作(say_goodbye)的处理器。""" + async def execute( self, reason: str, observation_info: Optional[ObservationInfo], conversation_info: Optional[ConversationInfo], action_start_time: float, - current_action_record: dict + current_action_record: dict, ) -> tuple[bool, str, str]: """ 执行发送告别语的动作。 会生成告别文本并发送,然后标记对话结束。 """ if not observation_info or not conversation_info: - self.logger.error(f"[私聊][{self.conversation.private_name}] SayGoodbyeHandler: ObservationInfo 或 ConversationInfo 为空。") + self.logger.error( + f"[私聊][{self.conversation.private_name}] SayGoodbyeHandler: ObservationInfo 或 ConversationInfo 为空。" + ) return False, "error", "内部信息缺失,无法执行告别" action_successful = False - final_status = "recall" # 默认状态 - final_reason = "告别语动作未成功执行" # 默认原因 + final_status = "recall" # 默认状态 + final_reason = "告别语动作未成功执行" # 默认原因 - self.conversation.state = ConversationState.GENERATING # 设置状态为生成中 + self.conversation.state = ConversationState.GENERATING # 设置状态为生成中 if not self.conversation.reply_generator: raise RuntimeError(f"ReplyGenerator 未为 {self.conversation.private_name} 初始化") @@ -759,96 +803,103 @@ class SayGoodbyeHandler(ActionHandler): f"[私聊][{self.conversation.private_name}] 动作 'say_goodbye': 生成内容: '{generated_content[:100]}...'" ) - if not generated_content or generated_content.startswith("抱歉"): # 如果生成内容无效 + if not generated_content or generated_content.startswith("抱歉"): # 如果生成内容无效 self.logger.warning( f"[私聊][{self.conversation.private_name}] 动作 'say_goodbye': 生成内容为空或为错误提示,取消发送。" ) final_reason = "生成告别内容无效" - final_status = "done" # 即使不发送,结束对话的决策也算完成 - self.conversation.should_continue = False # 标记对话结束 - action_successful = True # 动作(决策结束)本身算成功 - else: # 如果生成内容有效 + final_status = "done" # 即使不发送,结束对话的决策也算完成 + self.conversation.should_continue = False # 标记对话结束 + action_successful = True # 动作(决策结束)本身算成功 + else: # 如果生成内容有效 self.conversation.generated_reply = generated_content - self.conversation.state = ConversationState.SENDING # 设置状态为发送中 + self.conversation.state = ConversationState.SENDING # 设置状态为发送中 text_segment = Seg(type="text", data=self.conversation.generated_reply) send_success = await self._send_reply_or_segments([text_segment], self.conversation.generated_reply) send_end_time = time.time() - if send_success: # 如果发送成功 + if send_success: # 如果发送成功 action_successful = True final_status = "done" final_reason = "成功发送告别语" - self.conversation.should_continue = False # 标记对话结束 + self.conversation.should_continue = False # 标记对话结束 if self.conversation.conversation_info: - self.conversation.conversation_info.current_instance_message_count +=1 - self.conversation.conversation_info.my_message_count += 1 # 告别语也算一次发言 - await self._update_bot_message_in_history(send_end_time, self.conversation.generated_reply, observation_info) + self.conversation.conversation_info.current_instance_message_count += 1 + self.conversation.conversation_info.my_message_count += 1 # 告别语也算一次发言 + await self._update_bot_message_in_history( + send_end_time, self.conversation.generated_reply, observation_info + ) event_desc = f"你发送了告别消息: '{self.conversation.generated_reply[:50]}...'" # 注意:由于 should_continue 已设为 False,后续的 idle chat 更新可能意义不大,但情绪更新仍可进行 await self._update_post_send_states(observation_info, conversation_info, "say_goodbye", event_desc) - else: # 如果发送失败 + else: # 如果发送失败 final_status = "recall" final_reason = "发送告别语失败" action_successful = False - self.conversation.should_continue = True # 发送失败则不立即结束对话,让其自然流转 + self.conversation.should_continue = True # 发送失败则不立即结束对话,让其自然流转 return action_successful, final_status, final_reason class SendMemesHandler(ActionHandler): """处理发送表情包动作(send_memes)的处理器。""" + async def execute( self, reason: str, observation_info: Optional[ObservationInfo], conversation_info: Optional[ConversationInfo], action_start_time: float, - current_action_record: dict + current_action_record: dict, ) -> tuple[bool, str, str]: """ 执行发送表情包的动作。 会根据 current_emoji_query 获取并发送表情。 """ if not observation_info or not conversation_info: - self.logger.error(f"[私聊][{self.conversation.private_name}] SendMemesHandler: ObservationInfo 或 ConversationInfo 为空。") + self.logger.error( + f"[私聊][{self.conversation.private_name}] SendMemesHandler: ObservationInfo 或 ConversationInfo 为空。" + ) return False, "error", "内部信息缺失,无法发送表情包" action_successful = False - final_status = "recall" # 默认状态 + final_status = "recall" # 默认状态 final_reason_prefix = "发送表情包" - final_reason = f"{final_reason_prefix}失败:未知原因" # 默认原因 - self.conversation.state = ConversationState.GENERATING # 或 SENDING_MEME + final_reason = f"{final_reason_prefix}失败:未知原因" # 默认原因 + self.conversation.state = ConversationState.GENERATING # 或 SENDING_MEME emoji_query = conversation_info.current_emoji_query - if not emoji_query: # 如果没有表情查询 + if not emoji_query: # 如果没有表情查询 final_reason = f"{final_reason_prefix}失败:缺少表情包查询语句" # 此动作不依赖文本回复的追问状态,所以不修改 last_successful_reply_action return False, "recall", final_reason - + # 清理表情查询,因为我们要处理它了 conversation_info.current_emoji_query = None emoji_prepared_info = await self._fetch_and_prepare_emoji_segment(emoji_query) - if emoji_prepared_info: # 如果成功获取并准备了表情 + if emoji_prepared_info: # 如果成功获取并准备了表情 emoji_segment, full_emoji_description, log_emoji_description = emoji_prepared_info send_success = await self._send_reply_or_segments([emoji_segment], log_emoji_description) send_end_time = time.time() - if send_success: # 如果发送成功 + if send_success: # 如果发送成功 action_successful = True final_status = "done" final_reason = f"{final_reason_prefix}成功发送 ({full_emoji_description})" if self.conversation.conversation_info: - self.conversation.conversation_info.current_instance_message_count +=1 - self.conversation.conversation_info.my_message_count += 1 # 表情也算一次发言 - await self._update_bot_message_in_history(send_end_time, full_emoji_description, observation_info, "bot_meme_") + self.conversation.conversation_info.current_instance_message_count += 1 + self.conversation.conversation_info.my_message_count += 1 # 表情也算一次发言 + await self._update_bot_message_in_history( + send_end_time, full_emoji_description, observation_info, "bot_meme_" + ) event_desc = f"你发送了一个表情包 ({full_emoji_description})" await self._update_post_send_states(observation_info, conversation_info, "send_memes", event_desc) - else: # 如果发送失败 + else: # 如果发送失败 final_status = "recall" final_reason = f"{final_reason_prefix}失败:发送时出错" - else: # 如果未能获取或准备表情 + else: # 如果未能获取或准备表情 final_reason = f"{final_reason_prefix}失败:未找到或准备表情失败 ({emoji_query})" # last_successful_reply_action 保持不变 @@ -857,81 +908,143 @@ class SendMemesHandler(ActionHandler): class RethinkGoalHandler(ActionHandler): """处理重新思考目标动作(rethink_goal)的处理器。""" - async def execute(self, reason: str, observation_info: Optional[ObservationInfo], conversation_info: Optional[ConversationInfo], action_start_time: float, current_action_record: dict) -> tuple[bool, str, str]: + + async def execute( + self, + reason: str, + observation_info: Optional[ObservationInfo], + conversation_info: Optional[ConversationInfo], + action_start_time: float, + current_action_record: dict, + ) -> tuple[bool, str, str]: """执行重新思考对话目标的动作。""" if not conversation_info or not observation_info: - self.logger.error(f"[私聊][{self.conversation.private_name}] RethinkGoalHandler: ObservationInfo 或 ConversationInfo 为空。") + self.logger.error( + f"[私聊][{self.conversation.private_name}] RethinkGoalHandler: ObservationInfo 或 ConversationInfo 为空。" + ) return False, "error", "内部信息缺失,无法重新思考目标" - self.conversation.state = ConversationState.RETHINKING # 设置状态为重新思考中 + self.conversation.state = ConversationState.RETHINKING # 设置状态为重新思考中 if not self.conversation.goal_analyzer: raise RuntimeError(f"GoalAnalyzer 未为 {self.conversation.private_name} 初始化") - await self.conversation.goal_analyzer.analyze_goal(conversation_info, observation_info) # 调用目标分析器 + await self.conversation.goal_analyzer.analyze_goal(conversation_info, observation_info) # 调用目标分析器 event_desc = "你重新思考了对话目标和方向" - await self._update_relationship_and_emotion(observation_info, conversation_info, event_desc) # 更新关系和情绪 + await self._update_relationship_and_emotion(observation_info, conversation_info, event_desc) # 更新关系和情绪 return True, "done", "成功重新思考目标" + class ListeningHandler(ActionHandler): """处理倾听动作(listening)的处理器。""" - async def execute(self, reason: str, observation_info: Optional[ObservationInfo], conversation_info: Optional[ConversationInfo], action_start_time: float, current_action_record: dict) -> tuple[bool, str, str]: + + async def execute( + self, + reason: str, + observation_info: Optional[ObservationInfo], + conversation_info: Optional[ConversationInfo], + action_start_time: float, + current_action_record: dict, + ) -> tuple[bool, str, str]: """执行倾听对方发言的动作。""" if not conversation_info or not observation_info: - self.logger.error(f"[私聊][{self.conversation.private_name}] ListeningHandler: ObservationInfo 或 ConversationInfo 为空。") + self.logger.error( + f"[私聊][{self.conversation.private_name}] ListeningHandler: ObservationInfo 或 ConversationInfo 为空。" + ) return False, "error", "内部信息缺失,无法执行倾听" - self.conversation.state = ConversationState.LISTENING # 设置状态为倾听中 + self.conversation.state = ConversationState.LISTENING # 设置状态为倾听中 if not self.conversation.waiter: raise RuntimeError(f"Waiter 未为 {self.conversation.private_name} 初始化") - await self.conversation.waiter.wait_listening(conversation_info) # 调用等待器的倾听方法 + await self.conversation.waiter.wait_listening(conversation_info) # 调用等待器的倾听方法 event_desc = "你决定耐心倾听对方的发言" - await self._update_relationship_and_emotion(observation_info, conversation_info, event_desc) # 更新关系和情绪 + await self._update_relationship_and_emotion(observation_info, conversation_info, event_desc) # 更新关系和情绪 # listening 动作完成后,状态会由新消息或超时驱动,最终回到 ANALYZING return True, "done", "进入倾听状态" + class EndConversationHandler(ActionHandler): """处理结束对话动作(end_conversation)的处理器。""" - async def execute(self, reason: str, observation_info: Optional[ObservationInfo], conversation_info: Optional[ConversationInfo], action_start_time: float, current_action_record: dict) -> tuple[bool, str, str]: + + async def execute( + self, + reason: str, + observation_info: Optional[ObservationInfo], + conversation_info: Optional[ConversationInfo], + action_start_time: float, + current_action_record: dict, + ) -> tuple[bool, str, str]: """执行结束当前对话的动作。""" - self.logger.info(f"[私聊][{self.conversation.private_name}] 动作 'end_conversation': 收到最终结束指令,停止对话...") - self.conversation.should_continue = False # 标记对话不应继续,主循环会因此退出 + self.logger.info( + f"[私聊][{self.conversation.private_name}] 动作 'end_conversation': 收到最终结束指令,停止对话..." + ) + self.conversation.should_continue = False # 标记对话不应继续,主循环会因此退出 # 注意:最终的关系评估通常在 Conversation.stop() 方法中进行 return True, "done", "对话结束指令已执行" + class BlockAndIgnoreHandler(ActionHandler): """处理屏蔽并忽略对话动作(block_and_ignore)的处理器。""" - async def execute(self, reason: str, observation_info: Optional[ObservationInfo], conversation_info: Optional[ConversationInfo], action_start_time: float, current_action_record: dict) -> tuple[bool, str, str]: + + async def execute( + self, + reason: str, + observation_info: Optional[ObservationInfo], + conversation_info: Optional[ConversationInfo], + action_start_time: float, + current_action_record: dict, + ) -> tuple[bool, str, str]: """执行屏蔽并忽略当前对话一段时间的动作。""" - if not conversation_info or not observation_info: # 防御性检查 - self.logger.error(f"[私聊][{self.conversation.private_name}] BlockAndIgnoreHandler: ObservationInfo 或 ConversationInfo 为空。") + if not conversation_info or not observation_info: # 防御性检查 + self.logger.error( + f"[私聊][{self.conversation.private_name}] BlockAndIgnoreHandler: ObservationInfo 或 ConversationInfo 为空。" + ) return False, "error", "内部信息缺失,无法执行屏蔽" self.logger.info(f"[私聊][{self.conversation.private_name}] 动作 'block_and_ignore': 不想再理你了...") - ignore_duration_seconds = 10 * 60 # 例如忽略10分钟,可以配置 - self.conversation.ignore_until_timestamp = time.time() + ignore_duration_seconds # 设置忽略截止时间 - self.conversation.state = ConversationState.IGNORED # 设置状态为已忽略 + ignore_duration_seconds = 10 * 60 # 例如忽略10分钟,可以配置 + self.conversation.ignore_until_timestamp = time.time() + ignore_duration_seconds # 设置忽略截止时间 + self.conversation.state = ConversationState.IGNORED # 设置状态为已忽略 event_desc = "当前对话让你感到不适,你决定暂时不再理会对方" - await self._update_relationship_and_emotion(observation_info, conversation_info, event_desc) # 更新关系和情绪 + await self._update_relationship_and_emotion(observation_info, conversation_info, event_desc) # 更新关系和情绪 # should_continue 仍为 True,但主循环会检查 ignore_until_timestamp return True, "done", f"已屏蔽并忽略对话 {ignore_duration_seconds // 60} 分钟" + class WaitHandler(ActionHandler): """处理等待动作(wait)的处理器。""" - async def execute(self, reason: str, observation_info: Optional[ObservationInfo], conversation_info: Optional[ConversationInfo], action_start_time: float, current_action_record: dict) -> tuple[bool, str, str]: + + async def execute( + self, + reason: str, + observation_info: Optional[ObservationInfo], + conversation_info: Optional[ConversationInfo], + action_start_time: float, + current_action_record: dict, + ) -> tuple[bool, str, str]: """执行等待对方回复的动作。""" - if not conversation_info or not observation_info: # 防御性检查 - self.logger.error(f"[私聊][{self.conversation.private_name}] WaitHandler: ObservationInfo 或 ConversationInfo 为空。") + if not conversation_info or not observation_info: # 防御性检查 + self.logger.error( + f"[私聊][{self.conversation.private_name}] WaitHandler: ObservationInfo 或 ConversationInfo 为空。" + ) return False, "error", "内部信息缺失,无法执行等待" - self.conversation.state = ConversationState.WAITING # 设置状态为等待中 + self.conversation.state = ConversationState.WAITING # 设置状态为等待中 if not self.conversation.waiter: raise RuntimeError(f"Waiter 未为 {self.conversation.private_name} 初始化") - timeout_occurred = await self.conversation.waiter.wait(conversation_info) # 调用等待器的常规等待方法 + timeout_occurred = await self.conversation.waiter.wait(conversation_info) # 调用等待器的常规等待方法 event_desc = "你等待对方回复,但对方长时间没有回应" if timeout_occurred else "你选择等待对方的回复" - await self._update_relationship_and_emotion(observation_info, conversation_info, event_desc) # 更新关系和情绪 + await self._update_relationship_and_emotion(observation_info, conversation_info, event_desc) # 更新关系和情绪 # wait 动作完成后,状态会由新消息或超时驱动,最终回到 ANALYZING return True, "done", "等待动作完成" + class UnknownActionHandler(ActionHandler): """处理未知或无效动作的处理器。""" - async def execute(self, reason: str, observation_info: Optional[ObservationInfo], conversation_info: Optional[ConversationInfo], action_start_time: float, current_action_record: dict) -> tuple[bool, str, str]: - """处理无法识别的动作类型。""" - action_name = current_action_record.get("action", "未知动作类型") # 从记录中获取动作名 - self.logger.warning(f"[私聊][{self.conversation.private_name}] 接收到未知的动作类型: {action_name}") - return False, "recall", f"未知的动作类型: {action_name}" # 标记为需要重新规划 + async def execute( + self, + reason: str, + observation_info: Optional[ObservationInfo], + conversation_info: Optional[ConversationInfo], + action_start_time: float, + current_action_record: dict, + ) -> tuple[bool, str, str]: + """处理无法识别的动作类型。""" + action_name = current_action_record.get("action", "未知动作类型") # 从记录中获取动作名 + self.logger.warning(f"[私聊][{self.conversation.private_name}] 接收到未知的动作类型: {action_name}") + return False, "recall", f"未知的动作类型: {action_name}" # 标记为需要重新规划 diff --git a/src/experimental/PFC/actions.py b/src/experimental/PFC/actions.py index e984e363..5c8233ba 100644 --- a/src/experimental/PFC/actions.py +++ b/src/experimental/PFC/actions.py @@ -5,17 +5,17 @@ import traceback from typing import Optional, TYPE_CHECKING from src.common.logger_manager import get_logger -from .pfc_types import ConversationState # 调整导入路径 -from .observation_info import ObservationInfo # 调整导入路径 -from .conversation_info import ConversationInfo # 调整导入路径 +from .pfc_types import ConversationState # 调整导入路径 +from .observation_info import ObservationInfo # 调整导入路径 +from .conversation_info import ConversationInfo # 调整导入路径 # 导入工厂类 -from .action_factory import StandardActionFactory # 调整导入路径 +from .action_factory import StandardActionFactory # 调整导入路径 if TYPE_CHECKING: - from .conversation import Conversation # 调整导入路径 + from .conversation import Conversation # 调整导入路径 -logger = get_logger("pfc_actions") # 模块级别日志记录器 +logger = get_logger("pfc_actions") # 模块级别日志记录器 async def handle_action( @@ -40,41 +40,39 @@ async def handle_action( # 如果 conversation_info 和 done_action 存在且不为空 if conversation_info and hasattr(conversation_info, "done_action") and conversation_info.done_action: # 更新最后一个动作记录的状态和原因 - if conversation_info.done_action: # 再次检查列表是否不为空 - conversation_info.done_action[-1].update( - {"status": "error", "final_reason": "ObservationInfo is None"} - ) - conversation_instance.state = ConversationState.ERROR # 设置对话状态为错误 + if conversation_info.done_action: # 再次检查列表是否不为空 + conversation_info.done_action[-1].update({"status": "error", "final_reason": "ObservationInfo is None"}) + conversation_instance.state = ConversationState.ERROR # 设置对话状态为错误 return # 检查 conversation_info 是否为空 if not conversation_info: logger.error(f"[私聊][{conversation_instance.private_name}] ConversationInfo 为空,无法处理动作 '{action}'。") - conversation_instance.state = ConversationState.ERROR # 设置对话状态为错误 + conversation_instance.state = ConversationState.ERROR # 设置对话状态为错误 return logger.info(f"[私聊][{conversation_instance.private_name}] 开始处理动作: {action}, 原因: {reason}") - action_start_time = time.time() # 记录动作开始时间 + action_start_time = time.time() # 记录动作开始时间 # 当前动作记录 current_action_record = { - "action": action, # 动作类型 - "plan_reason": reason, # 规划原因 - "status": "start", # 初始状态为 "start" - "time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # 当前时间 - "final_reason": None, # 最终原因,默认为 None + "action": action, # 动作类型 + "plan_reason": reason, # 规划原因 + "status": "start", # 初始状态为 "start" + "time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # 当前时间 + "final_reason": None, # 最终原因,默认为 None } # 如果 done_action 不存在或为空,则初始化 if not hasattr(conversation_info, "done_action") or conversation_info.done_action is None: conversation_info.done_action = [] - conversation_info.done_action.append(current_action_record) # 添加当前动作记录 - action_index = len(conversation_info.done_action) - 1 # 获取当前动作记录的索引 + conversation_info.done_action.append(current_action_record) # 添加当前动作记录 + action_index = len(conversation_info.done_action) - 1 # 获取当前动作记录的索引 - action_successful: bool = False # 动作是否成功,默认为 False - final_status: str = "recall" # 最终状态,默认为 "recall" - final_reason: str = "动作未成功执行" # 最终原因,默认为 "动作未成功执行" + action_successful: bool = False # 动作是否成功,默认为 False + final_status: str = "recall" # 最终状态,默认为 "recall" + final_reason: str = "动作未成功执行" # 最终原因,默认为 "动作未成功执行" - factory = StandardActionFactory() # 创建标准动作工厂实例 - action_handler = factory.create_action_handler(action, conversation_instance) # 创建动作处理器 + factory = StandardActionFactory() # 创建标准动作工厂实例 + action_handler = factory.create_action_handler(action, conversation_instance) # 创建动作处理器 try: # 执行动作处理器 @@ -86,33 +84,33 @@ async def handle_action( # 此部分之前位于每个 if/elif 块内部 # 如果动作不是回复类型的动作 if action not in ["direct_reply", "send_new_message", "say_goodbye", "send_memes"]: - conversation_info.last_successful_reply_action = None # 清除上次成功回复动作 - conversation_info.last_reply_rejection_reason = None # 清除上次回复拒绝原因 - conversation_info.last_rejected_reply_content = None # 清除上次拒绝的回复内容 + conversation_info.last_successful_reply_action = None # 清除上次成功回复动作 + conversation_info.last_reply_rejection_reason = None # 清除上次回复拒绝原因 + conversation_info.last_rejected_reply_content = None # 清除上次拒绝的回复内容 # 如果动作不是发送表情包或发送表情包失败,则清除表情查询 if action != "send_memes" or not action_successful: if hasattr(conversation_info, "current_emoji_query"): conversation_info.current_emoji_query = None - except asyncio.CancelledError: # 捕获任务取消错误 + except asyncio.CancelledError: # 捕获任务取消错误 logger.warning(f"[私聊][{conversation_instance.private_name}] 处理动作 '{action}' 时被取消。") - final_status = "cancelled" # 设置最终状态为 "cancelled" + final_status = "cancelled" # 设置最终状态为 "cancelled" final_reason = "动作处理被取消" # 如果 conversation_info 存在 if conversation_info: - conversation_info.last_successful_reply_action = None # 清除上次成功回复动作 - raise # 重新抛出异常,由循环处理 - except Exception as handle_err: # 捕获其他异常 + conversation_info.last_successful_reply_action = None # 清除上次成功回复动作 + raise # 重新抛出异常,由循环处理 + except Exception as handle_err: # 捕获其他异常 logger.error(f"[私聊][{conversation_instance.private_name}] 处理动作 '{action}' 时出错: {handle_err}") logger.error(f"[私聊][{conversation_instance.private_name}] {traceback.format_exc()}") - final_status = "error" # 设置最终状态为 "error" + final_status = "error" # 设置最终状态为 "error" final_reason = f"处理动作时出错: {handle_err}" - conversation_instance.state = ConversationState.ERROR # 设置对话状态为错误 + conversation_instance.state = ConversationState.ERROR # 设置对话状态为错误 # 如果 conversation_info 存在 if conversation_info: - conversation_info.last_successful_reply_action = None # 清除上次成功回复动作 - action_successful = False # 确保动作为不成功 + conversation_info.last_successful_reply_action = None # 清除上次成功回复动作 + action_successful = False # 确保动作为不成功 finally: # 更新动作历史记录 @@ -130,45 +128,43 @@ async def handle_action( final_reason = f"动作 {action} 成功完成" # 如果是发送表情包且 current_emoji_query 存在(理想情况下从处理器获取描述) if action == "send_memes" and conversation_info.current_emoji_query: - pass # 占位符 - 表情描述最好从处理器的执行结果中获取并用于原因 + pass # 占位符 - 表情描述最好从处理器的执行结果中获取并用于原因 # 更新动作记录 conversation_info.done_action[action_index].update( { - "status": final_status, # 最终状态 - "time_completed": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # 完成时间 - "final_reason": final_reason, # 最终原因 - "duration_ms": int((time.time() - action_start_time) * 1000), # 持续时间(毫秒) + "status": final_status, # 最终状态 + "time_completed": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # 完成时间 + "final_reason": final_reason, # 最终原因 + "duration_ms": int((time.time() - action_start_time) * 1000), # 持续时间(毫秒) } ) - else: # 如果无法更新动作历史记录 + else: # 如果无法更新动作历史记录 logger.error( f"[私聊][{conversation_instance.private_name}] 无法更新动作历史记录,done_action 无效或索引 {action_index} 超出范围。" ) - # 根据最终状态设置对话状态 if final_status in ["done", "done_no_reply", "recall"]: - conversation_instance.state = ConversationState.ANALYZING # 设置为分析中 + conversation_instance.state = ConversationState.ANALYZING # 设置为分析中 elif final_status in ["error", "max_checker_attempts_failed"]: - conversation_instance.state = ConversationState.ERROR # 设置为错误 + conversation_instance.state = ConversationState.ERROR # 设置为错误 # 其他状态如 LISTENING, WAITING, IGNORED, ENDED 在各自的处理器内部或由循环设置。 # 此处移至 try 块以确保即使在发生异常之前也运行 # 如果动作不是回复类型的动作 if action not in ["direct_reply", "send_new_message", "say_goodbye", "send_memes"]: - if conversation_info: # 再次检查 conversation_info 是否不为 None - conversation_info.last_successful_reply_action = None # 清除上次成功回复动作 - conversation_info.last_reply_rejection_reason = None # 清除上次回复拒绝原因 - conversation_info.last_rejected_reply_content = None # 清除上次拒绝的回复内容 + if conversation_info: # 再次检查 conversation_info 是否不为 None + conversation_info.last_successful_reply_action = None # 清除上次成功回复动作 + conversation_info.last_reply_rejection_reason = None # 清除上次回复拒绝原因 + conversation_info.last_rejected_reply_content = None # 清除上次拒绝的回复内容 # 如果动作不是发送表情包或发送表情包失败 if action != "send_memes" or not action_successful: # 如果 conversation_info 存在且有 current_emoji_query 属性 if conversation_info and hasattr(conversation_info, "current_emoji_query"): - conversation_info.current_emoji_query = None # 清除当前表情查询 + conversation_info.current_emoji_query = None # 清除当前表情查询 - - log_final_reason_msg = final_reason if final_reason else "无明确原因" # 记录的最终原因消息 + log_final_reason_msg = final_reason if final_reason else "无明确原因" # 记录的最终原因消息 # 如果最终状态为 "done",动作成功,且是直接回复或发送新消息,并且有生成的回复 if ( final_status == "done" @@ -179,8 +175,8 @@ async def handle_action( ): log_final_reason_msg += f" (发送内容: '{conversation_instance.generated_reply[:30]}...')" # elif final_status == "done" and action_successful and action == "send_memes": - # 表情包的日志记录在其处理器内部或通过下面的通用日志处理 + # 表情包的日志记录在其处理器内部或通过下面的通用日志处理 logger.info( f"[私聊][{conversation_instance.private_name}] 动作 '{action}' 处理完成。最终状态: {final_status}, 原因: {log_final_reason_msg}" - ) \ No newline at end of file + )