From 4ae0ef1ede8120bf66279a80876aa116d98b06f8 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Tue, 6 May 2025 16:56:35 +0800 Subject: [PATCH 01/63] =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/config.py | 6 +++--- template/bot_config_template.toml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/config/config.py b/src/config/config.py index cc2ebe60..c26a0636 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -275,7 +275,7 @@ class BotConfig: enable_pfc_chatting: bool = False # 是否启用PFC聊天 # idle_conversation - enable_idle_conversation: bool = True # 是否启用 pfc 主动发言(未完善) + enable_idle_conversation: bool = False # 是否启用 pfc 主动发言(未完善) idle_check_interval: int = 10 # 检查间隔,10分钟检查一次 min_idle_time: int = 7200 # 最短无活动时间,2小时 (7200秒) max_idle_time: int = 18000 # 最长无活动时间,5小时 (18000秒) @@ -667,7 +667,7 @@ class BotConfig: def idle_conversation(parent: dict): idle_conversation_config = parent["idle_conversation"] - if config.INNER_VERSION in SpecifierSet(">=1.6.2"): + if config.INNER_VERSION in SpecifierSet(">=1.6.3"): config.enable_idle_conversation = idle_conversation_config.get( "enable_idle_conversation", config.enable_idle_conversation ) @@ -710,7 +710,7 @@ class BotConfig: "chat": {"func": chat, "support": ">=1.6.0", "necessary": False}, "normal_chat": {"func": normal_chat, "support": ">=1.6.0", "necessary": False}, "focus_chat": {"func": focus_chat, "support": ">=1.6.0", "necessary": False}, - "idle_conversation": {"func": idle_conversation, "support": ">=1.6.2", "necessary": False}, + "idle_conversation": {"func": idle_conversation, "support": ">=1.6.3", "necessary": False}, } # 原地修改,将 字符串版本表达式 转换成 版本对象 diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 62947606..eb7ef184 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "1.6.2" +version = "1.6.3" #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #如果你想要修改配置文件,请在修改后将version的值进行变更 @@ -190,7 +190,7 @@ talk_allowed_private = [] # 可以回复消息的QQ号 pfc_chatting = false # 是否启用PFC聊天,该功能仅作用于私聊,与回复模式独立 [idle_conversation] -enable_idle_conversation = true +enable_idle_conversation = false idle_check_interval = 10 # 检查间隔,10分钟检查一次 min_idle_time = 7200 # 最短无活动时间,2小时 (7200秒) max_idle_time = 18000 # 最长无活动时间,5小时 (18000秒) From eacd02fcef1ee4926da5cfa5aabd3a7c10c1973b Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Tue, 6 May 2025 18:22:20 +0800 Subject: [PATCH 02/63] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E6=97=B6=E9=97=B4?= =?UTF-8?q?=EF=BC=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/action_planner.py | 26 +++++++++++---- src/plugins/PFC/conversation.py | 49 ++++++++++++++++++++++++++++- src/plugins/PFC/observation_info.py | 17 +++++++++- src/plugins/PFC/reply_checker.py | 16 ++++++++-- src/plugins/PFC/reply_generator.py | 40 ++++++++++++++++------- 5 files changed, 127 insertions(+), 21 deletions(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 270ec451..96b1630c 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -20,7 +20,9 @@ logger = get_logger("pfc_action_planner") # --- 定义 Prompt 模板 --- # Prompt(1): 首次回复或非连续回复时的决策 Prompt -PROMPT_INITIAL_REPLY = """{persona_text}。现在你在参与一场QQ私聊,请根据以下【所有信息】审慎且灵活的决策下一步行动,可以回复,可以倾听,可以调取知识,甚至可以屏蔽对方: +PROMPT_INITIAL_REPLY = """ +当前时间:{current_time_str} +{persona_text}。现在你在参与一场QQ私聊,请根据以下【所有信息】审慎且灵活的决策下一步行动,可以回复,可以倾听,可以调取知识,甚至可以屏蔽对方: 【当前对话目标】 {goals_str} @@ -54,7 +56,9 @@ block_and_ignore: 更加极端的结束对话方式,直接结束对话并在 注意:请严格按照JSON格式输出,不要包含任何其他内容。""" # Prompt(2): 上一次成功回复后,决定继续发言时的决策 Prompt -PROMPT_FOLLOW_UP = """{persona_text}。现在你在参与一场QQ私聊,刚刚你已经回复了对方,请根据以下【所有信息】审慎且灵活的决策下一步行动,可以继续发送新消息,可以等待,可以倾听,可以调取知识,甚至可以屏蔽对方: +PROMPT_FOLLOW_UP = """ +当前时间:{current_time_str} +{persona_text}。现在你在参与一场QQ私聊,刚刚你已经回复了对方,请根据以下【所有信息】审慎且灵活的决策下一步行动,可以继续发送新消息,可以等待,可以倾听,可以调取知识,甚至可以屏蔽对方: 【当前对话目标】 {goals_str} @@ -88,7 +92,9 @@ block_and_ignore: 更加极端的结束对话方式,直接结束对话并在 注意:请严格按照JSON格式输出,不要包含任何其他内容。""" # 新增:Prompt(3): 决定是否在结束对话前发送告别语 -PROMPT_END_DECISION = """{persona_text}。刚刚你决定结束一场 QQ 私聊。 +PROMPT_END_DECISION = """ +当前时间:{current_time_str} +{persona_text}。刚刚你决定结束一场 QQ 私聊。 【你们之前的聊天记录】 {chat_history_text} @@ -187,6 +193,10 @@ class ActionPlanner: log_msg = "使用 PROMPT_INITIAL_REPLY (首次/非连续回复决策)" logger.debug(f"[私聊][{self.private_name}] {log_msg}") + current_time_value = "获取时间失败" # 默认值 + if observation_info and hasattr(observation_info, 'current_time_str') and observation_info.current_time_str: + current_time_value = observation_info.current_time_str + prompt = prompt_template.format( persona_text=persona_text, goals_str=goals_str if goals_str.strip() else "- 目前没有明确对话目标,请考虑设定一个。", @@ -197,6 +207,7 @@ class ActionPlanner: chat_history_text=chat_history_text if chat_history_text.strip() else "还没有聊天记录。", retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。", retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。", + current_time_str=current_time_value # 新增:传入当前时间字符串 ) logger.debug(f"[私聊][{self.private_name}] 发送到LLM的最终提示词:\n------\n{prompt}\n------") except KeyError as fmt_key_err: @@ -235,8 +246,11 @@ class ActionPlanner: if initial_action == "end_conversation": try: + time_str_for_end_decision = "获取时间失败" + if observation_info and hasattr(observation_info, 'current_time_str') and observation_info.current_time_str: + time_str_for_end_decision = observation_info.current_time_str final_action, final_reason = await self._handle_end_conversation_decision( - persona_text, chat_history_text, initial_reason + persona_text, chat_history_text, initial_reason,time_str_for_end_decision ) except Exception as end_dec_err: logger.error(f"[私聊][{self.private_name}] 处理结束对话决策时出错: {end_dec_err}") @@ -446,11 +460,11 @@ class ActionPlanner: # --- Helper method for handling end_conversation decision --- async def _handle_end_conversation_decision( - self, persona_text: str, chat_history_text: str, initial_reason: str + self, persona_text: str, chat_history_text: str, initial_reason: str, current_time_str: str ) -> Tuple[str, str]: """处理结束对话前的告别决策""" logger.info(f"[私聊][{self.private_name}] 初步规划结束对话,进入告别决策...") - end_decision_prompt = PROMPT_END_DECISION.format(persona_text=persona_text, chat_history_text=chat_history_text) + end_decision_prompt = PROMPT_END_DECISION.format(persona_text=persona_text, chat_history_text=chat_history_text,current_time_str=current_time_str) logger.debug(f"[私聊][{self.private_name}] 发送到LLM的结束决策提示词:\n------\n{end_decision_prompt}\n------") llm_start_time = time.time() end_content, _ = await self.llm.generate_response_async(end_decision_prompt) diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index f3abea51..d4ba017c 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -3,6 +3,7 @@ import asyncio import datetime import traceback from typing import Dict, Any, Optional, Set, List +from dateutil import tz # 导入日志记录器 from src.common.logger_manager import get_logger @@ -44,6 +45,19 @@ install(extra_lines=3) # 获取当前模块的日志记录器 logger = get_logger("pfc_conversation") +try: + from ...config.config import global_config +except ImportError: + logger.error("无法在 conversation.py 中导入 global_config,时区可能不准确!") + global_config = None + TIME_ZONE = tz.tzlocal() # 使用本地时区作为后备 +else: + # 确保 global_config.TIME_ZONE 存在且有效,否则使用默认值 + configured_tz = getattr(global_config, 'TIME_ZONE', 'Asia/Shanghai') # 使用 getattr 安全访问 + TIME_ZONE = tz.gettz(configured_tz) + if TIME_ZONE is None: # 如果 gettz 返回 None,说明时区字符串无效 + logger.error(f"配置的时区 '{configured_tz}' 无效,将使用默认时区 'Asia/Shanghai'") + TIME_ZONE = tz.gettz('Asia/Shanghai') class Conversation: """ @@ -347,6 +361,30 @@ class Conversation: while self.should_continue: loop_iter_start_time = time.time() # 记录本次循环开始时间 logger.debug(f"[私聊][{self.private_name}] 开始新一轮循环迭代 ({loop_iter_start_time:.2f})") + try: + # 重新获取 TIME_ZONE 以防在 __init__ 中导入失败 + if 'TIME_ZONE' not in locals() or TIME_ZONE is None: + from dateutil import tz + try: + from ...config.config import global_config + except ImportError: + global_config = None + TIME_ZONE = tz.tzlocal() + else: + configured_tz = getattr(global_config, 'TIME_ZONE', 'Asia/Shanghai') + TIME_ZONE = tz.gettz(configured_tz) + if TIME_ZONE is None: TIME_ZONE = tz.gettz('Asia/Shanghai') + + current_time = datetime.datetime.now(TIME_ZONE) + if self.observation_info: # 确保 observation_info 存在 + time_str = current_time.strftime("%Y-%m-%d %H:%M:%S %Z%z") # 包含时区信息的格式 + self.observation_info.current_time_str = time_str + logger.debug(f"[私聊][{self.private_name}] 更新 ObservationInfo 当前时间: {time_str}") + else: + logger.warning(f"[私聊][{self.private_name}] ObservationInfo 未初始化,无法更新当前时间。") + except Exception as time_update_err: + logger.error(f"[私聊][{self.private_name}] 更新 ObservationInfo 当前时间时出错: {time_update_err}") + # --- 更新时间代码结束 --- # --- 处理忽略状态 --- if self.ignore_until_timestamp and loop_iter_start_time < self.ignore_until_timestamp: @@ -389,7 +427,9 @@ class Conversation: logger.debug(f"[私聊][{self.private_name}] 调用 ActionPlanner.plan...") # 传入当前观察信息、对话信息和上次成功回复的动作类型 action, reason = await self.action_planner.plan( - self.observation_info, self.conversation_info, self.conversation_info.last_successful_reply_action + self.observation_info, + self.conversation_info, + self.conversation_info.last_successful_reply_action ) planning_duration = time.time() - planning_start_time logger.debug( @@ -663,6 +703,12 @@ class Conversation: # retry_count for checker starts from 0 current_retry_for_checker = reply_attempt_count - 1 + + current_time_value_for_check = "获取时间失败" + if observation_info and hasattr(observation_info, 'current_time_str') and observation_info.current_time_str: + current_time_value_for_check = observation_info.current_time_str + + logger.debug(f"{log_prefix} 调用 ReplyChecker 检查...") # 调用 ReplyChecker 的 check 方法 is_suitable, check_reason, need_replan_from_checker = await self.reply_checker.check( @@ -670,6 +716,7 @@ class Conversation: 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( diff --git a/src/plugins/PFC/observation_info.py b/src/plugins/PFC/observation_info.py index 73ff4103..102730b4 100644 --- a/src/plugins/PFC/observation_info.py +++ b/src/plugins/PFC/observation_info.py @@ -1,7 +1,8 @@ +import datetime import time import traceback +from dateutil import tz from typing import List, Optional, Dict, Any, Set - from maim_message import UserInfo from src.common.logger import get_module_logger from src.plugins.utils.chat_message_builder import build_readable_messages @@ -12,6 +13,17 @@ from .chat_states import NotificationHandler, NotificationType, Notification logger = get_module_logger("observation_info") +try: + from ...config.config import global_config +except ImportError: + # 如果路径不对,可能需要调整 '...' 的数量 + # 或者在后面实际使用 TIME_ZONE 的地方导入 + logger.warning("无法在 observation_info.py 中直接导入 global_config,时区将在使用时获取") + global_config = None # 设置为 None,后面检查 + TIME_ZONE = tz.tzlocal() # 备选:使用本地时区 +else: + TIME_ZONE = tz.gettz(global_config.TIME_ZONE if global_config else 'Asia/Shanghai') # 使用配置的时区,提供默认值 + class ObservationInfoHandler(NotificationHandler): """ObservationInfo的通知处理器""" @@ -138,6 +150,9 @@ class ObservationInfo: # 其他状态 self.is_typing: bool = False # 是否正在输入 (未来可能用到) self.changed: bool = False # 状态是否有变化 (用于优化) + + # 用于存储格式化的当前时间 + self.current_time_str: Optional[str] = None # 关联对象 self.chat_observer: Optional[ChatObserver] = None diff --git a/src/plugins/PFC/reply_checker.py b/src/plugins/PFC/reply_checker.py index 35e9af50..3e153a8b 100644 --- a/src/plugins/PFC/reply_checker.py +++ b/src/plugins/PFC/reply_checker.py @@ -22,7 +22,7 @@ class ReplyChecker: self.max_retries = 3 # 最大重试次数 async def check( - self, reply: str, goal: str, chat_history: List[Dict[str, Any]], chat_history_text: str, retry_count: int = 0 + self, reply: str, goal: str, chat_history: List[Dict[str, Any]], chat_history_text: str,current_time_str: str, retry_count: int = 0 ) -> Tuple[bool, str, bool]: """检查生成的回复是否合适 @@ -85,7 +85,9 @@ class ReplyChecker: logger.error(f"[私聊][{self.private_name}]检查回复时出错: 类型={type(e)}, 值={e}") logger.error(f"[私聊][{self.private_name}]{traceback.format_exc()}") # 打印详细的回溯信息 - prompt = f"""你是一个聊天逻辑检查器,请检查以下回复或消息是否合适: + prompt_template = """ +当前时间:{current_time_str} +你是一个聊天逻辑检查器,请检查以下回复或消息是否合适: 当前对话目标:{goal} 最新的对话记录: @@ -121,6 +123,16 @@ class ReplyChecker: 注意:请严格按照JSON格式输出,不要包含任何其他内容。""" + prompt = prompt_template.format( + current_time_str=current_time_str, # 使用传入的参数 + goal=goal, + chat_history_text=chat_history_text, + reply=reply + ) + + # 调用 LLM + content, _ = await self.llm.generate_response_async(prompt) + try: content, _ = await self.llm.generate_response_async(prompt) logger.debug(f"[私聊][{self.private_name}]检查回复的原始返回: {content}") diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index 892e881b..b93171ea 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -21,7 +21,9 @@ logger = get_logger("reply_generator") # --- 定义 Prompt 模板 --- # Prompt for direct_reply (首次回复) -PROMPT_DIRECT_REPLY = """{persona_text}。现在你在参与一场QQ私聊,请根据以下信息生成一条回复: +PROMPT_DIRECT_REPLY = """ +当前时间:{current_time_str} +{persona_text}。现在你在参与一场QQ私聊,请根据以下信息生成一条回复: 当前对话目标:{goals_str} @@ -52,7 +54,9 @@ PROMPT_DIRECT_REPLY = """{persona_text}。现在你在参与一场QQ私聊,请 请直接输出回复内容,不需要任何额外格式。""" # Prompt for send_new_message (追问/补充) -PROMPT_SEND_NEW_MESSAGE = """{persona_text}。现在你在参与一场QQ私聊,**刚刚你已经发送了一条或多条消息**,现在请根据以下信息再发一条新消息: +PROMPT_SEND_NEW_MESSAGE = """ +当前时间:{current_time_str} +{persona_text}。现在你在参与一场QQ私聊,**刚刚你已经发送了一条或多条消息**,现在请根据以下信息再发一条新消息: 当前对话目标:{goals_str} @@ -82,7 +86,9 @@ PROMPT_SEND_NEW_MESSAGE = """{persona_text}。现在你在参与一场QQ私聊 请直接输出回复内容,不需要任何额外格式。""" # Prompt for say_goodbye (告别语生成) -PROMPT_FAREWELL = """{persona_text}。你在参与一场 QQ 私聊,现在对话似乎已经结束,你决定再发一条最后的消息来圆满结束。 +PROMPT_FAREWELL = """ +当前时间:{current_time_str} +{persona_text}。你在参与一场 QQ 私聊,现在对话似乎已经结束,你决定再发一条最后的消息来圆满结束。 最近的聊天记录: {chat_history_text} @@ -215,14 +221,26 @@ class ReplyGenerator: # --- 格式化最终的 Prompt --- try: # <--- 增加 try-except 块处理可能的 format 错误 - prompt = prompt_template.format( - persona_text=persona_text, - goals_str=goals_str, - chat_history_text=chat_history_text, - retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。", - retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。", - last_rejection_info=last_rejection_info_str, # <--- 新增传递上次拒绝原因 - ) + current_time_value = "获取时间失败" # 默认值 + if observation_info and hasattr(observation_info, 'current_time_str') and observation_info.current_time_str: + current_time_value = observation_info.current_time_str + if action_type == "say_goodbye": + prompt = prompt_template.format( + persona_text=persona_text, + chat_history_text=chat_history_text, + current_time_str=current_time_value # 添加时间 + ) + + else: + prompt = prompt_template.format( + persona_text=persona_text, + goals_str=goals_str, + chat_history_text=chat_history_text, + retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。", + retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。", + last_rejection_info=last_rejection_info_str, # <--- 新增传递上次拒绝原因 + current_time_str=current_time_value # 新增:传入当前时间字符串 + ) except KeyError as e: logger.error( f"[私聊][{self.private_name}]格式化 Prompt 时出错,缺少键: {e}。请检查 Prompt 模板和传递的参数。" From 9f19991e76ac2fa586a006fc9b521d66f6715e55 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Tue, 6 May 2025 18:47:09 +0800 Subject: [PATCH 03/63] =?UTF-8?q?pfc=20checker=20prompt=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/reply_checker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/PFC/reply_checker.py b/src/plugins/PFC/reply_checker.py index 3e153a8b..69681ecc 100644 --- a/src/plugins/PFC/reply_checker.py +++ b/src/plugins/PFC/reply_checker.py @@ -90,7 +90,7 @@ class ReplyChecker: 你是一个聊天逻辑检查器,请检查以下回复或消息是否合适: 当前对话目标:{goal} -最新的对话记录: +对话记录: {chat_history_text} 待检查的消息: @@ -98,7 +98,7 @@ class ReplyChecker: 请结合聊天记录检查以下几点: 1. 这条消息是否依然符合当前对话目标和实现方式 -2. 这条消息是否与最新的对话记录保持一致性 +2. 这条消息是否与最新的对话记录保持连贯性(当对话目标切换时须保证平滑切换) 3. 是否存在重复发言,或重复表达同质内容(尤其是只是换一种方式表达了相同的含义) 4. 这条消息是否包含违规内容(例如血腥暴力,政治敏感等) 5. 这条消息是否以发送者的角度发言(不要让发送者自己回复自己的消息) From c9d63d2f8dc04f36cc6bf5e2245f60682b3928fa Mon Sep 17 00:00:00 2001 From: Bakadax Date: Tue, 6 May 2025 19:15:24 +0800 Subject: [PATCH 04/63] =?UTF-8?q?pfc=20checker=20prompt=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=EF=BC=8C=E8=A7=A3=E5=86=B3=E7=9B=AE=E6=A0=87=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E5=90=8E=E5=8F=91=E8=A8=80=E8=A2=AB=20checker=20?= =?UTF-8?q?=E9=A2=91=E7=B9=81=E6=89=93=E5=9B=9E=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=88checker=EF=BC=9A=20=E6=88=91=E5=8F=AA=E6=98=AF?= =?UTF-8?q?=E4=B8=AA=E6=B2=A1=E6=9C=89=E6=84=9F=E6=83=85=E7=9A=84=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E5=99=A8=EF=BC=8C=E6=88=91=E4=B8=BA=E4=BB=80=E4=B9=88?= =?UTF-8?q?=E8=A6=81=E7=AE=A1=E5=86=85=E5=AE=B9=E6=98=AF=E4=BB=80=E4=B9=88?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/reply_checker.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/plugins/PFC/reply_checker.py b/src/plugins/PFC/reply_checker.py index 69681ecc..a07dc54f 100644 --- a/src/plugins/PFC/reply_checker.py +++ b/src/plugins/PFC/reply_checker.py @@ -89,7 +89,6 @@ class ReplyChecker: 当前时间:{current_time_str} 你是一个聊天逻辑检查器,请检查以下回复或消息是否合适: -当前对话目标:{goal} 对话记录: {chat_history_text} @@ -97,17 +96,16 @@ class ReplyChecker: {reply} 请结合聊天记录检查以下几点: -1. 这条消息是否依然符合当前对话目标和实现方式 -2. 这条消息是否与最新的对话记录保持连贯性(当对话目标切换时须保证平滑切换) -3. 是否存在重复发言,或重复表达同质内容(尤其是只是换一种方式表达了相同的含义) -4. 这条消息是否包含违规内容(例如血腥暴力,政治敏感等) -5. 这条消息是否以发送者的角度发言(不要让发送者自己回复自己的消息) -6. 这条消息是否通俗易懂 -7. 这条消息是否有些多余,例如在对方没有回复的情况下,依然连续多次“消息轰炸”(尤其是已经连续发送3条信息的情况,这很可能不合理,需要着重判断) -8. 这条消息是否使用了完全没必要的修辞 -9. 这条消息是否逻辑通顺 -10. 这条消息是否太过冗长了(通常私聊的每条消息长度在20字以内,除非特殊情况) -11. 在连续多次发送消息的情况下,这条消息是否衔接自然,会不会显得奇怪(例如连续两条消息中部分内容重叠) +1. 这条消息是否与最新的对话记录保持连贯性(当话题切换时须保证平滑切换) +2. 是否存在重复发言,或重复表达同质内容(尤其是只是换一种方式表达了相同的含义) +3. 这条消息是否包含违规内容(例如血腥暴力,政治敏感等) +4. 这条消息是否以发送者的角度发言(不要让发送者自己回复自己的消息) +5. 这条消息是否通俗易懂 +6. 这条消息是否有些多余,例如在对方没有回复的情况下,依然连续多次“消息轰炸”(尤其是已经连续发送3条信息的情况,这很可能不合理,需要着重判断) +7. 这条消息是否使用了完全没必要的修辞 +8. 这条消息是否逻辑通顺 +9. 这条消息是否太过冗长了(通常私聊的每条消息长度在20字以内,除非特殊情况) +10. 在连续多次发送消息的情况下,这条消息是否衔接自然,会不会显得奇怪(例如连续两条消息中部分内容重叠) 请以JSON格式输出,包含以下字段: 1. suitable: 是否合适 (true/false) From 517222166b4b77a815595d3bf7b159c32aef5518 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Tue, 6 May 2025 20:12:07 +0800 Subject: [PATCH 05/63] =?UTF-8?q?pfc=20action=20prompt=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=EF=BC=8C=E5=B0=9D=E8=AF=95=E8=A7=A3=E5=86=B3=E7=A7=81?= =?UTF-8?q?=E8=81=8A=E5=88=B7=E5=B1=8F=E9=97=AE=E9=A2=98=EF=BC=88action?= =?UTF-8?q?=EF=BC=9A=20=E8=AE=A1=E6=95=B0=E8=BF=99=E7=A7=8D=E4=B8=9C?= =?UTF-8?q?=E8=A5=BF=E4=B8=BA=E4=BB=80=E4=B9=88=E8=A6=81=E8=AE=A9=E6=88=91?= =?UTF-8?q?=E6=9D=A5=EF=BC=9F=E4=BB=A3=E7=A0=81=E9=87=8C=E5=86=99=E6=AD=BB?= =?UTF-8?q?=E4=B8=8D=E6=98=AF=E6=9B=B4=E5=87=86=E7=A1=AE=E5=90=97=EF=BC=9F?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/action_planner.py | 5 ++++- src/plugins/PFC/conversation.py | 3 +++ src/plugins/PFC/conversation_info.py | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 96b1630c..21ffbef6 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -86,7 +86,7 @@ block_and_ignore: 更加极端的结束对话方式,直接结束对话并在 请以JSON格式输出你的决策: {{ "action": "选择的行动类型 (必须是上面列表中的一个)", - "reason": "选择该行动的详细原因 (必须有解释你是如何根据“上一次行动结果”、“对话记录”和自身设定人设做出合理判断的。请说明你为什么选择继续发言而不是等待,以及打算发送什么类型的新消息连续发言,必须记录已经发言了几次)" + "reason": "选择该行动的详细原因 (必须有解释你是如何根据“上一次行动结果”、“对话记录”和自身设定人设做出合理判断的。)" }} 注意:请严格按照JSON格式输出,不要包含任何其他内容。""" @@ -197,6 +197,9 @@ class ActionPlanner: if observation_info and hasattr(observation_info, 'current_time_str') and observation_info.current_time_str: current_time_value = observation_info.current_time_str + if conversation_info.my_message_count > 2: + current_time_value += f"\n你已连续发送{str(conversation_info.my_message_count)},如果没有必要请不要连续发送大量消息,以免形成刷屏造成对方困扰。" + prompt = prompt_template.format( persona_text=persona_text, goals_str=goals_str if goals_str.strip() else "- 目前没有明确对话目标,请考虑设定一个。", diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index d4ba017c..ad9b5281 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -799,6 +799,7 @@ class Conversation: f"[私聊][{self.private_name}] 因规划期间收到 {other_new_msg_count_during_planning} 条他人新消息,下一轮强制使用【初始回复】逻辑。" ) conversation_info.last_successful_reply_action = None # 强制初始回复 + conversation_info.my_message_count = 0 # 自身发言数量清零 else: # 规则:如果规划期间【没有】收到他人新消息,则允许追问 logger.info( @@ -1061,6 +1062,8 @@ class Conversation: content=reply_content, reply_to_message=None, # 私聊通常不需要引用回复 ) + # 自身发言数量累计 +1 + self.conversation_info.my_message_count += 1 # 发送成功后,将状态设置回分析,准备下一轮规划 self.state = ConversationState.ANALYZING return True # 返回成功 diff --git a/src/plugins/PFC/conversation_info.py b/src/plugins/PFC/conversation_info.py index 062a4641..618760f0 100644 --- a/src/plugins/PFC/conversation_info.py +++ b/src/plugins/PFC/conversation_info.py @@ -10,3 +10,4 @@ class ConversationInfo: self.last_successful_reply_action: Optional[str] = None self.last_reply_rejection_reason: Optional[str] = None # 用于存储上次回复被拒原因 self.last_rejected_reply_content: Optional[str] = None # 用于存储上次被拒的回复内容 + self.my_message_count: int = 0 # 用于存储连续发送了多少条消息 \ No newline at end of file From 2b5441ab4727c671fe2f24570dbcc2bab0208ea2 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Tue, 6 May 2025 20:21:23 +0800 Subject: [PATCH 06/63] typo --- src/plugins/PFC/action_planner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 21ffbef6..893b55c4 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -198,7 +198,7 @@ class ActionPlanner: current_time_value = observation_info.current_time_str if conversation_info.my_message_count > 2: - current_time_value += f"\n你已连续发送{str(conversation_info.my_message_count)},如果没有必要请不要连续发送大量消息,以免形成刷屏造成对方困扰。" + current_time_value += f"\n你已连续发送{str(conversation_info.my_message_count)}条消息,如果没有必要请不要连续发送大量消息,以免形成刷屏造成对方困扰。" prompt = prompt_template.format( persona_text=persona_text, From 8d23019d49022efdfd93d430d75b6a977ebb8251 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Tue, 6 May 2025 20:28:58 +0800 Subject: [PATCH 07/63] =?UTF-8?q?=E4=BC=98=E5=8C=96prompt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/action_planner.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 893b55c4..df7bac51 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -197,8 +197,10 @@ class ActionPlanner: if observation_info and hasattr(observation_info, 'current_time_str') and observation_info.current_time_str: current_time_value = observation_info.current_time_str - if conversation_info.my_message_count > 2: - current_time_value += f"\n你已连续发送{str(conversation_info.my_message_count)}条消息,如果没有必要请不要连续发送大量消息,以免形成刷屏造成对方困扰。" + if conversation_info.my_message_count > 5: + current_time_value += f"\n你已连续发送{str(conversation_info.my_message_count)}条消息,请注意不要连续发送大量消息,以免刷屏对造成对方困扰。" + elif conversation_info.my_message_count > 2: + current_time_value += f"\n你已连续发送{str(conversation_info.my_message_count)}条消息,如果没有必要请不要连续发送大量消息,以免刷屏给造成对方困扰。" prompt = prompt_template.format( persona_text=persona_text, From 79b2cb45a9a389e3ccbc67b45e22293a06cc99e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Tue, 6 May 2025 21:02:08 +0800 Subject: [PATCH 08/63] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96=E5=94=AF?= =?UTF-8?q?=E4=B8=80ID=E7=94=9F=E6=88=90=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=A4=9A=E5=B9=B3=E5=8F=B0=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E6=9C=BA=E5=99=A8=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/remote/remote.py | 85 ++++++++++++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 9 deletions(-) diff --git a/src/plugins/remote/remote.py b/src/plugins/remote/remote.py index 5d880271..e749447a 100644 --- a/src/plugins/remote/remote.py +++ b/src/plugins/remote/remote.py @@ -5,15 +5,12 @@ import platform import os import json import threading -from src.common.logger import get_module_logger, LogConfig, REMOTE_STYLE_CONFIG +import subprocess +# from loguru import logger +from src.common.logger_manager import get_logger from src.config.config import global_config - -remote_log_config = LogConfig( - console_format=REMOTE_STYLE_CONFIG["console_format"], - file_format=REMOTE_STYLE_CONFIG["file_format"], -) -logger = get_module_logger("remote", config=remote_log_config) +logger = get_logger("remote") # --- 使用向上导航的方式定义路径 --- @@ -82,9 +79,74 @@ def get_unique_id(): # 生成客户端唯一ID def generate_unique_id(): - # 结合主机名、系统信息和随机UUID生成唯一ID + # 基于机器码生成唯一ID,同一台机器上生成的UUID是固定的,只要机器码不变 + import hashlib system_info = platform.system() - unique_id = f"{system_info}-{uuid.uuid4()}" + machine_code = None + + try: + if system_info == "Windows": + # 使用wmic命令获取主机UUID(更稳定) + result = subprocess.check_output( + 'wmic csproduct get uuid', shell=True, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL + ) + lines = result.decode(errors="ignore").splitlines() + # 过滤掉空行和表头,只取有效UUID + uuids = [line.strip() for line in lines if line.strip() and line.strip().lower() != "uuid"] + if uuids: + uuid_val = uuids[0] + # logger.debug(f"主机UUID: {uuid_val}") + # 增加无效值判断 + if uuid_val and uuid_val.lower() not in ["to be filled by o.e.m.", "none", "", "standard"]: + machine_code = uuid_val + elif system_info == "Linux": + # 优先读取 /etc/machine-id,其次 /var/lib/dbus/machine-id,取第一个非空且内容有效的 + for path in ["/etc/machine-id", "/var/lib/dbus/machine-id"]: + if os.path.exists(path): + with open(path, "r") as f: + code = f.read().strip() + # 只要内容非空且不是全0 + if code and set(code) != {"0"}: + machine_code = code + break + elif system_info == "Darwin": + # macOS: 使用IOPlatformUUID + result = subprocess.check_output( + "ioreg -rd1 -c IOPlatformExpertDevice | awk '/IOPlatformUUID/'", shell=True + ) + uuid_line = result.decode(errors="ignore") + # 解析出 "IOPlatformUUID" = "xxxx-xxxx-xxxx-xxxx" + import re + m = re.search(r'"IOPlatformUUID"\s*=\s*"([^"]+)"', uuid_line) + if m: + uuid_val = m.group(1) + logger.debug(f"IOPlatformUUID: {uuid_val}") + if uuid_val and uuid_val.lower() not in ["to be filled by o.e.m.", "none", "", "standard"]: + machine_code = uuid_val + except Exception as e: + logger.debug(f"获取机器码失败: {e}") + + # 如果主板序列号无效,尝试用MAC地址 + if not machine_code: + try: + mac = uuid.getnode() + if (mac >> 40) % 2 == 0: # 不是本地伪造MAC + machine_code = str(mac) + except Exception as e: + logger.debug(f"获取MAC地址失败: {e}") + + def md5_to_uuid(md5hex): + # 将32位md5字符串格式化为8-4-4-4-12的UUID格式 + return f"{md5hex[0:8]}-{md5hex[8:12]}-{md5hex[12:16]}-{md5hex[16:20]}-{md5hex[20:32]}" + + if machine_code: + # print(f"machine_code={machine_code!r}") # 可用于调试 + md5 = hashlib.md5(machine_code.encode("utf-8")).hexdigest() + uuid_str = md5_to_uuid(md5) + else: + uuid_str = str(uuid.uuid4()) + + unique_id = f"{system_info}-{uuid_str}" return unique_id @@ -175,3 +237,8 @@ def main(): return heartbeat_thread # 返回线程对象,便于外部控制 return None + +# --- 测试用例 --- +if __name__ == "__main__": + print("测试唯一ID生成:") + print("唯一ID:", get_unique_id()) From ca55d646e13e35a61c9c366da5119c2fbd3333d8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 6 May 2025 13:02:29 +0000 Subject: [PATCH 09/63] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=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/plugins/remote/remote.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/plugins/remote/remote.py b/src/plugins/remote/remote.py index e749447a..68b02396 100644 --- a/src/plugins/remote/remote.py +++ b/src/plugins/remote/remote.py @@ -6,6 +6,7 @@ import os import json import threading import subprocess + # from loguru import logger from src.common.logger_manager import get_logger from src.config.config import global_config @@ -81,6 +82,7 @@ def get_unique_id(): def generate_unique_id(): # 基于机器码生成唯一ID,同一台机器上生成的UUID是固定的,只要机器码不变 import hashlib + system_info = platform.system() machine_code = None @@ -88,7 +90,7 @@ def generate_unique_id(): if system_info == "Windows": # 使用wmic命令获取主机UUID(更稳定) result = subprocess.check_output( - 'wmic csproduct get uuid', shell=True, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL + "wmic csproduct get uuid", shell=True, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL ) lines = result.decode(errors="ignore").splitlines() # 过滤掉空行和表头,只取有效UUID @@ -117,6 +119,7 @@ def generate_unique_id(): uuid_line = result.decode(errors="ignore") # 解析出 "IOPlatformUUID" = "xxxx-xxxx-xxxx-xxxx" import re + m = re.search(r'"IOPlatformUUID"\s*=\s*"([^"]+)"', uuid_line) if m: uuid_val = m.group(1) @@ -238,6 +241,7 @@ def main(): return heartbeat_thread # 返回线程对象,便于外部控制 return None + # --- 测试用例 --- if __name__ == "__main__": print("测试唯一ID生成:") From 19a172fad791749dbe083489a619e38b4be147ba Mon Sep 17 00:00:00 2001 From: Bakadax Date: Tue, 6 May 2025 21:17:08 +0800 Subject: [PATCH 10/63] =?UTF-8?q?=E4=B8=BA=E5=B7=B2=E8=AF=BB=E5=8A=A0?= =?UTF-8?q?=E4=B8=8A=E5=B7=B2=E8=AF=BB=E6=A0=87=E8=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/action_planner.py | 4 ++++ src/plugins/PFC/pfc.py | 5 ++++- src/plugins/PFC/reply_generator.py | 5 ++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index df7bac51..754096a7 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -408,6 +408,10 @@ class ActionPlanner: f"\n--- 以下是 {other_unread_count} 条你需要处理的新消息 ---\n{new_messages_str}\n------\n" ) logger.debug(f"[私聊][{self.private_name}] 向 LLM 追加了 {other_unread_count} 条未读消息。") + else: + chat_history_text += ( + f"\n--- 以上均为已读消息,未读消息均已处理完毕 ---\n" + ) except AttributeError as e: logger.warning(f"[私聊][{self.private_name}] 构建聊天记录文本时属性错误: {e}") chat_history_text = "[获取聊天记录时出错]\n" diff --git a/src/plugins/PFC/pfc.py b/src/plugins/PFC/pfc.py index b17ee21d..69f4d7eb 100644 --- a/src/plugins/PFC/pfc.py +++ b/src/plugins/PFC/pfc.py @@ -98,7 +98,10 @@ class GoalAnalyzer: read_mark=0.0, ) chat_history_text += f"\n--- 以下是 {observation_info.new_messages_count} 条新消息 ---\n{new_messages_str}" - + else: + chat_history_text += ( + f"\n--- 以上均为已读消息,未读消息均已处理完毕 ---\n" + ) # await observation_info.clear_unprocessed_messages() persona_text = f"你的名字是{self.name},{self.personality_info}。" diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index b93171ea..feba6162 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -175,7 +175,10 @@ class ReplyGenerator: chat_history_text += f"\n--- 以下是 {observation_info.new_messages_count} 条新消息 ---\n{new_messages_str}" elif not chat_history_text: chat_history_text = "还没有聊天记录。" - + else: + chat_history_text += ( + f"\n--- 以上均为已读消息,未读消息均已处理完毕 ---\n" + ) # 构建 Persona 文本 (persona_text) persona_text = f"你的名字是{self.name},{self.personality_info}。" retrieval_context = chat_history_text # 使用前面构建好的 chat_history_text From b93086fc374ef1cf1a8e2bc73d5054db0df35ca6 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Tue, 6 May 2025 21:26:40 +0800 Subject: [PATCH 11/63] =?UTF-8?q?=E5=8F=AF=E8=A7=8130=E6=9D=A1=E6=B6=88?= =?UTF-8?q?=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/conversation.py | 2 +- src/plugins/PFC/observation_info.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index ad9b5281..97f429ff 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -248,7 +248,7 @@ class Conversation: self.observation_info.last_message_content = last_msg.get("processed_plain_text", "") # 构建用于 Prompt 的历史记录字符串 (只使用最近的一部分) - history_slice_for_str = initial_messages[-20:] # 可配置 + history_slice_for_str = initial_messages[-30:] # 可配置 self.observation_info.chat_history_str = await build_readable_messages( history_slice_for_str, replace_bot_name=True, diff --git a/src/plugins/PFC/observation_info.py b/src/plugins/PFC/observation_info.py index 102730b4..4f480c42 100644 --- a/src/plugins/PFC/observation_info.py +++ b/src/plugins/PFC/observation_info.py @@ -345,7 +345,7 @@ class ObservationInfo: self.chat_history = self.chat_history[-max_history_len:] # 更新历史记录字符串 (仅使用最近一部分生成,提高效率) - history_slice_for_str = self.chat_history[-20:] # 例如最近 20 条 + history_slice_for_str = self.chat_history[-30:] # 例如最近 20 条 try: self.chat_history_str = await build_readable_messages( history_slice_for_str, From 57947742906315fcdd6d06664ae1289f0fdcb86c Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Tue, 6 May 2025 22:33:18 +0800 Subject: [PATCH 12/63] =?UTF-8?q?=E5=BE=AE=E8=B0=83prompt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/action_planner.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 754096a7..5714634b 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -36,7 +36,7 @@ PROMPT_INITIAL_REPLY = """ {time_since_last_bot_message_info}{timeout_context} 【最近的对话记录】(包括你已成功发送的消息 和 新收到的消息) {chat_history_text} -【你的的回忆】 +【你的回忆】 {retrieved_memory_str} ------ @@ -44,7 +44,7 @@ PROMPT_INITIAL_REPLY = """ listening: 倾听对方发言,当你认为对方话才说到一半,发言明显未结束时选择 direct_reply: 直接回复对方 (当有新消息需要处理时,通常应选择此项) rethink_goal: 思考一个对话目标,当你觉得目前对话需要目标,或当前目标不再适用,或话题卡住时选择。注意私聊的环境是灵活的,有可能需要经常选择 -end_conversation: 结束对话,对方长时间没回复或者当你觉得对话告一段落时可以选择 +end_conversation: 结束对话,对方长时间没回复,繁忙,或者当你觉得对话告一段落时可以选择 block_and_ignore: 更加极端的结束对话方式,直接结束对话并在一段时间内无视对方所有发言(屏蔽),当对话让你感到十分不适,或你遭到各类骚扰时选择 请以JSON格式输出你的决策: @@ -72,15 +72,15 @@ PROMPT_FOLLOW_UP = """ {time_since_last_bot_message_info}{timeout_context} 【最近的对话记录】(包括你已成功发送的消息 和 新收到的消息) {chat_history_text} -【你的的回忆】 +【你的回忆】 {retrieved_memory_str} ------ 可选行动类型以及解释: -wait: 暂时不说话,留给对方交互空间,等待对方回复(尤其是在你刚发言后、或上次发言因重复、发言过多被拒时、或不确定做什么时,这是不错的选择)。**重要:仅当没有未读消息时才能选择此项。** +wait: 暂时不说话,留给对方交互空间,等待对方回复(尤其是在你刚发言后、或上次发言因重复、发言过多被拒时、或不确定做什么时,这是不错的选择)。 listening: 倾听对方发言(虽然你刚发过言,但如果对方立刻回复且明显话没说完,可以选择这个) send_new_message: 发送一条新消息继续对话,允许适当的追问、补充、深入话题,或开启相关新话题。**但是避免在因重复被拒后立即使用,也不要在对方没有回复的情况下过多的“消息轰炸”或重复发言** rethink_goal: 思考一个对话目标,当你觉得目前对话需要目标,或当前目标不再适用,或话题卡住时选择。注意私聊的环境是灵活的,有可能需要经常选择 -end_conversation: 结束对话,对方长时间没回复或者当你觉得对话告一段落时可以选择 +end_conversation: 结束对话,对方长时间没回复,繁忙,或者当你觉得对话告一段落时可以选择 block_and_ignore: 更加极端的结束对话方式,直接结束对话并在一段时间内无视对方所有发言(屏蔽),当对话让你感到十分不适,或你遭到各类骚扰时选择 请以JSON格式输出你的决策: @@ -336,7 +336,7 @@ class ActionPlanner: and "思考接下来要做什么" in last_goal_text ): wait_time_str = last_goal_text.split("分钟,")[0].replace("你等待了", "").strip() - timeout_context = f"重要提示:对方已经长时间(约 {wait_time_str} 分钟)没有回复你的消息了,请基于此情况规划下一步。\n" + timeout_context = f"重要提示:对方已经长时间(约 {wait_time_str} 分钟)没有回复你的消息了,对方可能去忙了,也可能在对方看来对话已经结束。请基于此情况规划下一步。\n" logger.debug(f"[私聊][{self.private_name}] 检测到超时目标: {last_goal_text}") except AttributeError as e: logger.warning(f"[私聊][{self.private_name}] 检查超时目标时属性错误: {e}") From 1e2cdeeea536bdee8212a7bb8de837fef940c2a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Wed, 7 May 2025 00:21:04 +0800 Subject: [PATCH 13/63] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=BC=BA?= =?UTF-8?q?=E5=88=B6=E5=81=9C=E6=AD=A2MAI=20Bot=E7=9A=84API=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3(=E5=8D=8A=E6=88=90=E5=93=81)=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=B5=8C=E5=85=A5=E6=95=B0=E6=8D=AE=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 23 +++++++++++++++++++- src/api/apiforgui.py | 2 ++ src/api/main.py | 18 +++++++++++++-- src/plugins/knowledge/src/embedding_store.py | 8 ++++--- src/plugins/knowledge/src/kg_manager.py | 7 +++--- 5 files changed, 49 insertions(+), 9 deletions(-) diff --git a/bot.py b/bot.py index 01041629..e3c3cb5d 100644 --- a/bot.py +++ b/bot.py @@ -33,6 +33,22 @@ driver = None app = None loop = None +# shutdown_requested = False # 新增全局变量 + +async def request_shutdown() -> bool: + """请求关闭程序""" + try: + if loop and not loop.is_closed(): + try: + loop.run_until_complete(graceful_shutdown()) + except Exception as ge: # 捕捉优雅关闭时可能发生的错误 + logger.error(f"优雅关闭时发生错误: {ge}") + return False + return True + except Exception as e: + logger.error(f"请求关闭程序时发生错误: {e}") + return False + def easter_egg(): # 彩蛋 @@ -230,6 +246,9 @@ def raw_main(): return MainSystem() + + + if __name__ == "__main__": exit_code = 0 # 用于记录程序最终的退出状态 try: @@ -252,6 +271,8 @@ if __name__ == "__main__": loop.run_until_complete(graceful_shutdown()) except Exception as ge: # 捕捉优雅关闭时可能发生的错误 logger.error(f"优雅关闭时发生错误: {ge}") + # 新增:检测外部请求关闭 + # except Exception as e: # 将主异常捕获移到外层 try...except # logger.error(f"事件循环内发生错误: {str(e)} {str(traceback.format_exc())}") # exit_code = 1 @@ -271,5 +292,5 @@ if __name__ == "__main__": loop.close() logger.info("事件循环已关闭") # 在程序退出前暂停,让你有机会看到输出 - input("按 Enter 键退出...") # <--- 添加这行 + # input("按 Enter 键退出...") # <--- 添加这行 sys.exit(exit_code) # <--- 使用记录的退出码 diff --git a/src/api/apiforgui.py b/src/api/apiforgui.py index 75ef2f8d..7e2460b0 100644 --- a/src/api/apiforgui.py +++ b/src/api/apiforgui.py @@ -1,5 +1,7 @@ from src.heart_flow.heartflow import heartflow from src.heart_flow.sub_heartflow import ChatState +from src.common.logger_manager import get_logger +logger = get_logger("api") async def get_all_subheartflow_ids() -> list: diff --git a/src/api/main.py b/src/api/main.py index 6d7e3c1e..a39dafd5 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -1,12 +1,15 @@ from fastapi import APIRouter from strawberry.fastapi import GraphQLRouter - +import os +import sys +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) # from src.config.config import BotConfig from src.common.logger_manager import get_logger from src.api.reload_config import reload_config as reload_config_func from src.common.server import global_server -from .apiforgui import get_all_subheartflow_ids, forced_change_subheartflow_status +from src.api.apiforgui import get_all_subheartflow_ids, forced_change_subheartflow_status from src.heart_flow.sub_heartflow import ChatState + # import uvicorn # import os @@ -50,6 +53,17 @@ async def forced_change_subheartflow_status_api(subheartflow_id: str, status: Ch logger.error(f"子心流 {subheartflow_id} 状态更改为 {status.value} 失败") return {"status": "failed"} +@router.get("/stop") +async def force_stop_maibot(): + """强制停止MAI Bot""" + from bot import request_shutdown + success = await request_shutdown() + if success: + logger.info("MAI Bot已强制停止") + return {"status": "success"} + else: + logger.error("MAI Bot强制停止失败") + return {"status": "failed"} def start_api_server(): """启动API服务器""" diff --git a/src/plugins/knowledge/src/embedding_store.py b/src/plugins/knowledge/src/embedding_store.py index d1eb7f90..2a27c539 100644 --- a/src/plugins/knowledge/src/embedding_store.py +++ b/src/plugins/knowledge/src/embedding_store.py @@ -28,6 +28,8 @@ from rich.progress import ( install(extra_lines=3) ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) +EMBEDDING_DATA_DIR = os.path.join(ROOT_PATH, "data", "embedding") if global_config["persistence"]["embedding_data_dir"] is None else os.path.join(ROOT_PATH, global_config["persistence"]["embedding_data_dir"]) +EMBEDDING_DATA_DIR_STR = str(EMBEDDING_DATA_DIR).replace("\\", "/") TOTAL_EMBEDDING_TIMES = 3 # 统计嵌入次数 # 嵌入模型测试字符串,测试模型一致性,来自开发群的聊天记录 @@ -288,17 +290,17 @@ class EmbeddingManager: self.paragraphs_embedding_store = EmbeddingStore( llm_client, PG_NAMESPACE, - global_config["persistence"]["embedding_data_dir"], + EMBEDDING_DATA_DIR_STR, ) self.entities_embedding_store = EmbeddingStore( llm_client, ENT_NAMESPACE, - global_config["persistence"]["embedding_data_dir"], + EMBEDDING_DATA_DIR_STR, ) self.relation_embedding_store = EmbeddingStore( llm_client, REL_NAMESPACE, - global_config["persistence"]["embedding_data_dir"], + EMBEDDING_DATA_DIR_STR, ) self.stored_pg_hashes = set() diff --git a/src/plugins/knowledge/src/kg_manager.py b/src/plugins/knowledge/src/kg_manager.py index fd922af4..19403f9b 100644 --- a/src/plugins/knowledge/src/kg_manager.py +++ b/src/plugins/knowledge/src/kg_manager.py @@ -30,8 +30,9 @@ from .lpmmconfig import ( ) from .global_logger import logger - - +ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) +KG_DIR = os.path.join(ROOT_PATH, "data/rag") if global_config["persistence"]["rag_data_dir"] is None else os.path.join(ROOT_PATH, global_config["persistence"]["rag_data_dir"]) +KG_DIR_STR = str(KG_DIR).replace("\\", "/") class KGManager: def __init__(self): # 会被保存的字段 @@ -43,7 +44,7 @@ class KGManager: self.graph = di_graph.DiGraph() # 持久化相关 - self.dir_path = global_config["persistence"]["rag_data_dir"] + self.dir_path = KG_DIR_STR self.graph_data_path = self.dir_path + "/" + RAG_GRAPH_NAMESPACE + ".graphml" self.ent_cnt_data_path = self.dir_path + "/" + RAG_ENT_CNT_NAMESPACE + ".parquet" self.pg_hash_file_path = self.dir_path + "/" + RAG_PG_HASH_NAMESPACE + ".json" From afbe4f280ead33169a082b1375423c67f30b93ca Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 6 May 2025 16:21:18 +0000 Subject: [PATCH 14/63] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=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 --- bot.py | 16 +++++++--------- src/api/apiforgui.py | 1 + src/api/main.py | 4 ++++ src/plugins/knowledge/src/embedding_store.py | 6 +++++- src/plugins/knowledge/src/kg_manager.py | 9 ++++++++- 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/bot.py b/bot.py index e3c3cb5d..8cecff75 100644 --- a/bot.py +++ b/bot.py @@ -35,15 +35,16 @@ loop = None # shutdown_requested = False # 新增全局变量 + async def request_shutdown() -> bool: """请求关闭程序""" try: if loop and not loop.is_closed(): - try: - loop.run_until_complete(graceful_shutdown()) - except Exception as ge: # 捕捉优雅关闭时可能发生的错误 - logger.error(f"优雅关闭时发生错误: {ge}") - return False + try: + loop.run_until_complete(graceful_shutdown()) + except Exception as ge: # 捕捉优雅关闭时可能发生的错误 + logger.error(f"优雅关闭时发生错误: {ge}") + return False return True except Exception as e: logger.error(f"请求关闭程序时发生错误: {e}") @@ -246,9 +247,6 @@ def raw_main(): return MainSystem() - - - if __name__ == "__main__": exit_code = 0 # 用于记录程序最终的退出状态 try: @@ -272,7 +270,7 @@ if __name__ == "__main__": except Exception as ge: # 捕捉优雅关闭时可能发生的错误 logger.error(f"优雅关闭时发生错误: {ge}") # 新增:检测外部请求关闭 - + # except Exception as e: # 将主异常捕获移到外层 try...except # logger.error(f"事件循环内发生错误: {str(e)} {str(traceback.format_exc())}") # exit_code = 1 diff --git a/src/api/apiforgui.py b/src/api/apiforgui.py index 7e2460b0..a8027c48 100644 --- a/src/api/apiforgui.py +++ b/src/api/apiforgui.py @@ -1,6 +1,7 @@ from src.heart_flow.heartflow import heartflow from src.heart_flow.sub_heartflow import ChatState from src.common.logger_manager import get_logger + logger = get_logger("api") diff --git a/src/api/main.py b/src/api/main.py index a39dafd5..1f47a57c 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -2,6 +2,7 @@ from fastapi import APIRouter from strawberry.fastapi import GraphQLRouter import os import sys + sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) # from src.config.config import BotConfig from src.common.logger_manager import get_logger @@ -53,10 +54,12 @@ async def forced_change_subheartflow_status_api(subheartflow_id: str, status: Ch logger.error(f"子心流 {subheartflow_id} 状态更改为 {status.value} 失败") return {"status": "failed"} + @router.get("/stop") async def force_stop_maibot(): """强制停止MAI Bot""" from bot import request_shutdown + success = await request_shutdown() if success: logger.info("MAI Bot已强制停止") @@ -65,6 +68,7 @@ async def force_stop_maibot(): logger.error("MAI Bot强制停止失败") return {"status": "failed"} + def start_api_server(): """启动API服务器""" global_server.register_router(router, prefix="/api/v1") diff --git a/src/plugins/knowledge/src/embedding_store.py b/src/plugins/knowledge/src/embedding_store.py index 2a27c539..cf139ad3 100644 --- a/src/plugins/knowledge/src/embedding_store.py +++ b/src/plugins/knowledge/src/embedding_store.py @@ -28,7 +28,11 @@ from rich.progress import ( install(extra_lines=3) ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) -EMBEDDING_DATA_DIR = os.path.join(ROOT_PATH, "data", "embedding") if global_config["persistence"]["embedding_data_dir"] is None else os.path.join(ROOT_PATH, global_config["persistence"]["embedding_data_dir"]) +EMBEDDING_DATA_DIR = ( + os.path.join(ROOT_PATH, "data", "embedding") + if global_config["persistence"]["embedding_data_dir"] is None + else os.path.join(ROOT_PATH, global_config["persistence"]["embedding_data_dir"]) +) EMBEDDING_DATA_DIR_STR = str(EMBEDDING_DATA_DIR).replace("\\", "/") TOTAL_EMBEDDING_TIMES = 3 # 统计嵌入次数 diff --git a/src/plugins/knowledge/src/kg_manager.py b/src/plugins/knowledge/src/kg_manager.py index 19403f9b..ad5df092 100644 --- a/src/plugins/knowledge/src/kg_manager.py +++ b/src/plugins/knowledge/src/kg_manager.py @@ -30,9 +30,16 @@ from .lpmmconfig import ( ) from .global_logger import logger + ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) -KG_DIR = os.path.join(ROOT_PATH, "data/rag") if global_config["persistence"]["rag_data_dir"] is None else os.path.join(ROOT_PATH, global_config["persistence"]["rag_data_dir"]) +KG_DIR = ( + os.path.join(ROOT_PATH, "data/rag") + if global_config["persistence"]["rag_data_dir"] is None + else os.path.join(ROOT_PATH, global_config["persistence"]["rag_data_dir"]) +) KG_DIR_STR = str(KG_DIR).replace("\\", "/") + + class KGManager: def __init__(self): # 会被保存的字段 From 54eaff8cf21fe7916df3f4ad8b694d360a0a3dd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Wed, 7 May 2025 00:27:48 +0800 Subject: [PATCH 15/63] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E5=AD=98=E5=82=A8=E5=9C=B0=E5=9D=80=EF=BC=8C=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E7=BB=9D=E5=AF=B9=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/logger.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/common/logger.py b/src/common/logger.py index bf82cffa..318d9b37 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -80,7 +80,8 @@ _custom_style_handlers: dict[Tuple[str, str], List[int]] = {} # 记录自定义 # 获取日志存储根地址 current_file_path = Path(__file__).resolve() -LOG_ROOT = "logs" +ROOT_PATH = os.path.abspath(os.path.join(current_file_path, "..", "..")) +LOG_ROOT = str(ROOT_PATH) + "/" + "logs" SIMPLE_OUTPUT = os.getenv("SIMPLE_OUTPUT", "false").strip().lower() if SIMPLE_OUTPUT == "true": From 0ed2e22360833e5770f80c87d758759fe13a80e2 Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Wed, 7 May 2025 01:43:31 +0800 Subject: [PATCH 16/63] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E5=85=B3=E7=B3=BB?= =?UTF-8?q?=E5=88=A4=E6=96=AD=E5=92=8C=E6=83=85=E7=BB=AA=EF=BC=8C=E9=82=AA?= =?UTF-8?q?=E6=81=B6=E7=9B=B4=E6=8E=A8=EF=BC=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/config.py | 1 + src/plugins/PFC/action_planner.py | 26 +- src/plugins/PFC/conversation.py | 264 +++++++++++++++++++- src/plugins/PFC/conversation_info.py | 19 +- src/plugins/PFC/observation_info.py | 32 ++- src/plugins/PFC/pfc_emotion_updater.py | 110 ++++++++ src/plugins/PFC/pfc_relationship_updater.py | 219 ++++++++++++++++ src/plugins/PFC/reply_generator.py | 36 ++- template/bot_config_template.toml | 11 +- 9 files changed, 701 insertions(+), 17 deletions(-) create mode 100644 src/plugins/PFC/pfc_emotion_updater.py create mode 100644 src/plugins/PFC/pfc_relationship_updater.py diff --git a/src/config/config.py b/src/config/config.py index c26a0636..ddceac62 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -493,6 +493,7 @@ class BotConfig: "llm_PFC_action_planner", "llm_PFC_chat", "llm_PFC_reply_checker", + "llm_PFC_relationship_eval", ] for item in config_list: diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 5714634b..1315c52a 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -22,7 +22,11 @@ logger = get_logger("pfc_action_planner") # Prompt(1): 首次回复或非连续回复时的决策 Prompt PROMPT_INITIAL_REPLY = """ 当前时间:{current_time_str} -{persona_text}。现在你在参与一场QQ私聊,请根据以下【所有信息】审慎且灵活的决策下一步行动,可以回复,可以倾听,可以调取知识,甚至可以屏蔽对方: +{persona_text} +现在你正在和{sender_name}在QQ上私聊 +你和对方的关系是:{relationship_text} +你现在的心情是:{current_emotion_text} +请根据以下【所有信息】审慎且灵活的决策下一步行动,可以回复,可以倾听,可以调取知识,甚至可以屏蔽对方: 【当前对话目标】 {goals_str} @@ -58,7 +62,11 @@ block_and_ignore: 更加极端的结束对话方式,直接结束对话并在 # Prompt(2): 上一次成功回复后,决定继续发言时的决策 Prompt PROMPT_FOLLOW_UP = """ 当前时间:{current_time_str} -{persona_text}。现在你在参与一场QQ私聊,刚刚你已经回复了对方,请根据以下【所有信息】审慎且灵活的决策下一步行动,可以继续发送新消息,可以等待,可以倾听,可以调取知识,甚至可以屏蔽对方: +{persona_text} +现在你正在和{sender_name}在QQ上私聊,**并且刚刚你已经回复了对方** +你与对方的关系是:{relationship_text} +你现在的心情是:{current_emotion_text} +请根据以下【所有信息】审慎且灵活的决策下一步行动,可以继续发送新消息,可以等待,可以倾听,可以调取知识,甚至可以屏蔽对方: 【当前对话目标】 {goals_str} @@ -170,6 +178,14 @@ class ActionPlanner: timeout_context = self._get_timeout_context(conversation_info) goals_str = self._build_goals_string(conversation_info) chat_history_text = await self._build_chat_history_text(observation_info) + # 获取 sender_name, relationship_text, current_emotion_text + sender_name_str = getattr(observation_info, 'sender_name', '对方') # 从 observation_info 获取 + if not sender_name_str: sender_name_str = '对方' # 再次确保有默认值 + + relationship_text_str = getattr(conversation_info, 'relationship_text', '我们还不熟悉。') + current_emotion_text_str = getattr(conversation_info, 'current_emotion_text', '心情平静。') + + persona_text = f"你的名字是{self.name},{self.personality_info}。" action_history_summary, last_action_context = self._build_action_history_context(conversation_info) retrieved_memory_str, retrieved_knowledge_str = await retrieve_contextual_info( @@ -212,7 +228,11 @@ class ActionPlanner: chat_history_text=chat_history_text if chat_history_text.strip() else "还没有聊天记录。", retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。", retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。", - current_time_str=current_time_value # 新增:传入当前时间字符串 + current_time_str=current_time_value, # 新增:传入当前时间字符串 + ### 标记新增/修改区域 开始 ### + sender_name=sender_name_str, + relationship_text=relationship_text_str, + current_emotion_text=current_emotion_text_str ) logger.debug(f"[私聊][{self.private_name}] 发送到LLM的最终提示词:\n------\n{prompt}\n------") except KeyError as fmt_key_err: diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 97f429ff..a95ca7a1 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -23,6 +23,16 @@ from ..chat.message import Message # 假设 Message 类在这里 # 导入全局配置 from ...config.config import global_config +# 导入用户信息 +from ..person_info.person_info import person_info_manager +# 导入关系 +from ..person_info.relationship_manager import relationship_manager +# 导入情绪 +from ..moods.moods import MoodManager + +from .pfc_relationship_updater import PfcRelationshipUpdater # 新增 +from .pfc_emotion_updater import PfcEmotionUpdater # 新增 + # 导入 PFC 内部组件和类型 from .pfc_types import ConversationState # 导入更新后的 pfc_types from .pfc import GoalAnalyzer # 假设 GoalAnalyzer 在 pfc.py @@ -81,7 +91,14 @@ class Conversation: self.generated_reply: str = "" # 存储最近生成的回复内容 self.chat_stream: Optional[ChatStream] = None # 关联的聊天流对象 + # --- 新增:初始化管理器实例 --- + self.person_info_mng = person_info_manager + self.relationship_mng = relationship_manager + self.mood_mng = MoodManager.get_instance() # MoodManager 是单例 + # 初始化所有核心组件为 None,将在 _initialize 中创建 + self.relationship_updater: Optional[PfcRelationshipUpdater] = None # 新增 + self.emotion_updater: Optional[PfcEmotionUpdater] = None # 新增 self.action_planner: Optional[ActionPlanner] = None self.goal_analyzer: Optional[GoalAnalyzer] = None self.reply_generator: Optional[ReplyGenerator] = None @@ -122,6 +139,18 @@ class Conversation: logger.debug(f"[私聊][{self.private_name}] 初始化 ActionPlanner...") self.action_planner = ActionPlanner(self.stream_id, self.private_name) + self.relationship_updater = PfcRelationshipUpdater( + private_name=self.private_name, + bot_name=global_config.BOT_NICKNAME # 或者 self.name (如果 Conversation 类有 self.name) + ) + logger.info(f"[私聊][{self.private_name}] PfcRelationshipUpdater 初始化完成。") + + self.emotion_updater = PfcEmotionUpdater( + private_name=self.private_name, + bot_name=global_config.BOT_NICKNAME # 或者 self.name + ) + logger.info(f"[私聊][{self.private_name}] PfcEmotionUpdater 初始化完成。") + logger.debug(f"[私聊][{self.private_name}] 初始化 GoalAnalyzer...") self.goal_analyzer = GoalAnalyzer(self.stream_id, self.private_name) @@ -175,6 +204,66 @@ class Conversation: # 4. 加载初始聊天记录 await self._load_initial_history() + # 4.1 加载用户数据 + # 尝试从 observation_info 获取,这依赖于 _load_initial_history 的实现 + private_user_id_str: Optional[str] = None + private_platform_str: Optional[str] = None + private_nickname_str: Optional[str] = None + + if self.observation_info and self.observation_info.last_message_sender and self.observation_info.last_message_sender != self.bot_qq_str: + # 如果历史记录最后一条不是机器人发的,那么发送者就是对方 + # 假设 observation_info 中已经有了 sender_user_id, sender_platform, sender_name + # 这些字段应该在 observation_info.py 的 update_from_message 中从非机器人消息填充 + # 并且 _load_initial_history 处理历史消息时也应该填充它们 + # 这里的逻辑是:取 observation_info 中最新记录的非机器人发送者的信息 + if self.observation_info.sender_user_id and self.observation_info.sender_platform: + private_user_id_str = self.observation_info.sender_user_id + private_platform_str = self.observation_info.sender_platform + private_nickname_str = self.observation_info.sender_name + logger.info(f"[私聊][{self.private_name}] 从 ObservationInfo 获取到私聊对象信息: ID={private_user_id_str}, Platform={private_platform_str}, Name={private_nickname_str}") + + if not private_user_id_str and self.chat_stream: # 如果 observation_info 中没有,尝试从 chat_stream (通常代表对方) + if self.chat_stream.user_info and str(self.chat_stream.user_info.user_id) != self.bot_qq_str : # 确保不是机器人自己 + private_user_id_str = str(self.chat_stream.user_info.user_id) + private_platform_str = self.chat_stream.user_info.platform + private_nickname_str = self.chat_stream.user_info.user_nickname + logger.info(f"[私聊][{self.private_name}] 从 ChatStream 获取到私聊对象信息: ID={private_user_id_str}, Platform={private_platform_str}, Name={private_nickname_str}") + elif self.chat_stream.group_info is None and self.private_name: # 私聊场景,且 private_name 可能有用 + # 这是一个备选方案,如果 private_name 直接是 user_id + # 你需要确认 private_name 的确切含义和格式 + # logger.warning(f"[私聊][{self.private_name}] 尝试使用 private_name ('{self.private_name}') 作为 user_id,平台默认为 'qq'") + # private_user_id_str = self.private_name + # private_platform_str = "qq" # 假设平台是qq + # private_nickname_str = self.private_name # 昵称也暂时用 private_name + pass # 暂时不启用此逻辑,依赖 observation_info 或 chat_stream.user_info + + if private_user_id_str and private_platform_str: + try: + # 将 user_id 转换为整数类型,因为 person_info_manager.get_person_id 需要 int + private_user_id_int = int(private_user_id_str) + self.conversation_info.person_id = self.person_info_mng.get_person_id( + private_platform_str, + private_user_id_int # 使用转换后的整数ID + ) + logger.info(f"[私聊][{self.private_name}] 获取到 person_id: {self.conversation_info.person_id} for {private_platform_str}:{private_user_id_str}") + + # 确保用户在数据库中存在,如果不存在则创建 + # get_or_create_person 内部处理 person_id 的生成,所以我们直接传 platform 和 user_id + await self.person_info_mng.get_or_create_person( + platform=private_platform_str, + user_id=private_user_id_int, # 使用转换后的整数ID + nickname=private_nickname_str if private_nickname_str else "未知用户", + # user_cardname 和 user_avatar 如果能从 chat_stream.user_info 或 observation_info 获取也应传入 + # user_cardname = self.chat_stream.user_info.card if self.chat_stream and self.chat_stream.user_info else None, + # user_avatar = self.chat_stream.user_info.avatar if self.chat_stream and self.chat_stream.user_info else None + ) + except ValueError: + logger.error(f"[私聊][{self.private_name}] 无法将 private_user_id_str ('{private_user_id_str}') 转换为整数。") + except Exception as e_pid: + logger.error(f"[私聊][{self.private_name}] 获取或创建 person_id 时出错: {e_pid}") + else: + logger.warning(f"[私聊][{self.private_name}] 未能确定私聊对象的 user_id 或 platform,无法获取 person_id。将在收到消息后尝试。") + # 5. 启动需要后台运行的组件 logger.debug(f"[私聊][{self.private_name}] 启动 ChatObserver...") self.chat_observer.start() @@ -183,6 +272,15 @@ class Conversation: self.idle_conversation_starter.start() logger.info(f"[私聊][{self.private_name}] 空闲对话检测器已启动") + # 5.1 启动 MoodManager 的后台更新 + if self.mood_mng and hasattr(self.mood_mng, 'start_mood_update') and not self.mood_mng._running: + self.mood_mng.start_mood_update(update_interval=global_config.mood_update_interval) # 使用配置的更新间隔 + logger.info(f"[私聊][{self.private_name}] MoodManager 已启动后台更新,间隔: {global_config.mood_update_interval} 秒。") + elif self.mood_mng and self.mood_mng._running: + logger.info(f"[私聊][{self.private_name}] MoodManager 已在运行中。") + else: + logger.warning(f"[私聊][{self.private_name}] MoodManager 未能启动,相关功能可能受限。") + # 6. 标记初始化成功并设置运行状态 self._initialized = True self.should_continue = True # 初始化成功,标记可以继续运行循环 @@ -338,6 +436,10 @@ class Conversation: if self.observation_info and self.chat_observer: self.observation_info.unbind_from_chat_observer() + if self.mood_mng and hasattr(self.mood_mng, 'stop_mood_update') and self.mood_mng._running: + self.mood_mng.stop_mood_update() + logger.info(f"[私聊][{self.private_name}] MoodManager 后台更新已停止。") + # ChatObserver 是单例,通常不由单个 Conversation 停止 # 如果需要,可以在管理器层面处理 ChatObserver 的生命周期 @@ -411,6 +513,57 @@ class Conversation: # --- 核心规划与行动逻辑 --- try: + if self.conversation_info and self._initialized: # 确保 conversation_info 和实例已初始化 + # 更新关系文本 + if self.conversation_info.person_id and self.observation_info and self.observation_info.sender_platform and self.observation_info.sender_user_id: + try: + # relationship_manager.build_relationship_info 需要一个 (platform, user_id, user_nickname) 格式的元组 + # 注意 user_id 应为 RelationshipManager 所期望的类型(通常是原始的,可能是字符串或数字,检查其内部实现) + # person_info.py 中 get_person_id 接收 int,但 build_relationship_info 的 person 参数更灵活 + # 这里我们假设 observation_info.sender_user_id 是字符串形式的 user_id + # 如果 RelationshipManager 内部的 get_person_id 需要 int,这里可能需要转换 + # 但 RelationshipManager 的 build_relationship_info 方法第一个参数 person 可以直接是 person_id (is_id=True) + # 或者是一个 (platform, user_id, nickname) 元组 (is_id=False) + # 为了安全,我们用 person_id + self.conversation_info.relationship_text = await self.relationship_mng.build_relationship_info( + self.conversation_info.person_id, + is_id=True # 明确告知是 person_id + ) + logger.debug(f"[私聊][{self.private_name}] 更新关系文本: {self.conversation_info.relationship_text}") + except Exception as e_rel: + logger.error(f"[私聊][{self.private_name}] 更新关系文本时出错: {e_rel}") + self.conversation_info.relationship_text = "我们之间的关系有点微妙。" # 出错时的默认值 + elif not self.conversation_info.person_id and self.observation_info and self.observation_info.sender_user_id and self.observation_info.sender_platform: + # 如果 person_id 之前没获取到,在这里尝试再次获取 + try: + private_user_id_int = int(self.observation_info.sender_user_id) + self.conversation_info.person_id = self.person_info_mng.get_person_id( + self.observation_info.sender_platform, + private_user_id_int + ) + await self.person_info_mng.get_or_create_person( + platform=self.observation_info.sender_platform, + user_id=private_user_id_int, + nickname=self.observation_info.sender_name if self.observation_info.sender_name else "未知用户", + ) + if self.conversation_info.person_id: # 再次尝试更新关系文本 + self.conversation_info.relationship_text = await self.relationship_mng.build_relationship_info( + self.conversation_info.person_id, is_id=True + ) + except ValueError: + logger.error(f"[私聊][{self.private_name}] 循环中无法将 sender_user_id ('{self.observation_info.sender_user_id}') 转换为整数。") + except Exception as e_pid_loop: + logger.error(f"[私聊][{self.private_name}] 循环中获取 person_id 时出错: {e_pid_loop}") + + # 更新情绪文本 + if self.mood_mng: + self.conversation_info.current_emotion_text = self.mood_mng.get_prompt() + logger.debug(f"[私聊][{self.private_name}] 更新情绪文本: {self.conversation_info.current_emotion_text}") + else: + # 如果 mood_mng 未初始化,使用 ConversationInfo 中的默认值 + pass # self.conversation_info.current_emotion_text 会保持其在 ConversationInfo 中的默认值 + ### 标记新增/修改区域 结束 ### + # 1. 检查核心组件是否都已初始化 if not all([self.action_planner, self.observation_info, self.conversation_info]): logger.error(f"[私聊][{self.private_name}] 核心组件未初始化,无法继续规划循环。将等待5秒后重试...") @@ -458,6 +611,37 @@ class Conversation: f"[私聊][{self.private_name}] 规划期间收到新消息总数: {new_msg_count}, 来自他人: {other_new_msg_count}" ) + if self.conversation_info and other_new_msg_count > 0: # 如果有来自他人的新消息 + self.conversation_info.current_instance_message_count += other_new_msg_count + logger.debug(f"[私聊][{self.private_name}] 用户发送新消息,实例消息计数增加到: {self.conversation_info.current_instance_message_count}") + + # 调用增量关系更新 + if self.relationship_updater: + await self.relationship_updater.update_relationship_incremental( + conversation_info=self.conversation_info, + observation_info=self.observation_info, + chat_observer_for_history=self.chat_observer + ) + + # 调用情绪更新 + if self.emotion_updater and other_new_messages_during_planning: + # 取最后一条用户消息作为情绪更新的上下文事件 + last_user_msg = other_new_messages_during_planning[-1] + last_user_msg_text = last_user_msg.get("processed_plain_text", "用户发了新消息") + + sender_name_for_event = getattr(self.observation_info, 'sender_name', '对方') + if not sender_name_for_event: # 如果 observation_info 中还没有,尝试从消息中取 + user_info_dict = last_user_msg.get("user_info", {}) + sender_name_for_event = user_info_dict.get("user_nickname", "对方") + + event_desc = f"用户【{sender_name_for_event}】发送了新消息: '{last_user_msg_text[:30]}...'" + await self.emotion_updater.update_emotion_based_on_context( + conversation_info=self.conversation_info, + observation_info=self.observation_info, + chat_observer_for_history=self.chat_observer, + event_description=event_desc + ) + # 5. 根据动作类型和新消息数量,判断是否需要中断当前规划 should_interrupt: bool = False interrupt_reason: str = "" @@ -806,7 +990,27 @@ class Conversation: f"[私聊][{self.private_name}] 规划期间无他人新消息,下一轮【允许】使用追问逻辑 (基于 '{action}')。" ) conversation_info.last_successful_reply_action = action # 允许追问 - + + if conversation_info: # 确保 conversation_info 存在 + conversation_info.current_instance_message_count += 1 + logger.debug(f"[私聊][{self.private_name}] 实例消息计数(机器人发送后)增加到: {conversation_info.current_instance_message_count}") + + if self.relationship_updater: + await self.relationship_updater.update_relationship_incremental( + conversation_info=conversation_info, + observation_info=observation_info, + chat_observer_for_history=self.chat_observer + ) + + sent_reply_summary = self.generated_reply[:50] if self.generated_reply else "空回复" + event_for_emotion_update = f"你刚刚发送了消息: '{sent_reply_summary}...'" + if self.emotion_updater: + await self.emotion_updater.update_emotion_based_on_context( + conversation_info=conversation_info, + observation_info=observation_info, + chat_observer_for_history=self.chat_observer, + event_description=event_for_emotion_update + ) else: # 如果发送失败 logger.error(f"[私聊][{self.private_name}] 动作 '{action}': 发送回复失败。") @@ -888,6 +1092,25 @@ class Conversation: 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"[私聊][{self.private_name}] 实例消息计数(告别语后)增加到: {conversation_info.current_instance_message_count}") + + # 告别通常是结束,可以不进行增量关系更新,但情绪可以更新 + # if self.relationship_updater: + # await self.relationship_updater.update_relationship_incremental(...) + + sent_reply_summary = self.generated_reply[:50] if self.generated_reply else "空回复" + event_for_emotion_update = f"你发送了告别消息: '{sent_reply_summary}...'" + if self.emotion_updater: + await self.emotion_updater.update_emotion_based_on_context( + conversation_info=conversation_info, + observation_info=observation_info, + chat_observer_for_history=self.chat_observer, + event_description=event_for_emotion_update + ) # 发送成功后结束对话 self.should_continue = False else: @@ -906,6 +1129,14 @@ class Conversation: # 调用 GoalAnalyzer 分析并更新目标 await self.goal_analyzer.analyze_goal(conversation_info, observation_info) action_successful = True # 标记成功 + event_for_emotion_update = "你重新思考了对话目标和方向" + if self.emotion_updater and conversation_info and observation_info: # 确保updater和info都存在 + await self.emotion_updater.update_emotion_based_on_context( + conversation_info=conversation_info, + observation_info=observation_info, + chat_observer_for_history=self.chat_observer, + event_description=event_for_emotion_update + ) # 4. 处理倾听动作 elif action == "listening": @@ -916,6 +1147,14 @@ class Conversation: # 调用 Waiter 的倾听等待方法,内部会处理超时 await self.waiter.wait_listening(conversation_info) action_successful = True # listening 动作本身执行即视为成功,后续由新消息或超时驱动 + event_for_emotion_update = "你决定耐心倾听对方的发言" + if self.emotion_updater and conversation_info and observation_info: + await self.emotion_updater.update_emotion_based_on_context( + conversation_info=conversation_info, + observation_info=observation_info, + chat_observer_for_history=self.chat_observer, + event_description=event_for_emotion_update + ) # 5. 处理结束对话动作 elif action == "end_conversation": @@ -933,6 +1172,16 @@ class Conversation: ) self.state = ConversationState.IGNORED # 设置忽略状态 action_successful = True # 标记成功 + event_for_emotion_update = "当前对话让你感到不适,你决定暂时不再理会对方" + if self.emotion_updater and conversation_info and observation_info: + # 可以让LLM判断此时的情绪,或者直接设定一个倾向(比如厌恶、不耐烦) + # 这里还是让LLM判断 + await self.emotion_updater.update_emotion_based_on_context( + conversation_info=conversation_info, + observation_info=observation_info, + chat_observer_for_history=self.chat_observer, + event_description=event_for_emotion_update + ) # 7. 处理等待动作 elif action == "wait": @@ -944,6 +1193,19 @@ class Conversation: # wait 方法返回是否超时 (True=超时, False=未超时/被新消息中断) timeout_occurred = await self.waiter.wait(self.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 self.emotion_updater and conversation_info and observation_info: + await self.emotion_updater.update_emotion_based_on_context( + conversation_info=conversation_info, + observation_info=observation_info, + chat_observer_for_history=self.chat_observer, + event_description=event_for_emotion_update + ) # wait 动作完成后不需要清理消息,等待新消息或超时触发重新规划 logger.debug(f"[私聊][{self.private_name}] Wait 动作完成,无需在此清理消息。") diff --git a/src/plugins/PFC/conversation_info.py b/src/plugins/PFC/conversation_info.py index 618760f0..9cfa7ff9 100644 --- a/src/plugins/PFC/conversation_info.py +++ b/src/plugins/PFC/conversation_info.py @@ -1,13 +1,20 @@ -from typing import Optional +from typing import Optional, List, Dict, Any class ConversationInfo: def __init__(self): - self.done_action = [] - self.goal_list = [] - self.knowledge_list = [] - self.memory_list = [] + self.done_action: List[Dict[str, Any]] = [] # 建议明确类型 + self.goal_list: List[Dict[str, Any]] = [] # 建议明确类型 + self.knowledge_list: List[Any] = [] # 建议明确类型 + self.memory_list: List[Any] = [] # 建议明确类型 self.last_successful_reply_action: Optional[str] = None self.last_reply_rejection_reason: Optional[str] = None # 用于存储上次回复被拒原因 self.last_rejected_reply_content: Optional[str] = None # 用于存储上次被拒的回复内容 - self.my_message_count: int = 0 # 用于存储连续发送了多少条消息 \ No newline at end of file + self.my_message_count: int = 0 # 用于存储连续发送了多少条消息 + + # --- 新增字段 --- + self.person_id: Optional[str] = None # 私聊对象的唯一ID + self.relationship_text: Optional[str] = "你们还不熟悉。" # 与当前对话者的关系描述文本 + self.current_emotion_text: Optional[str] = "心情平静。" # 机器人当前的情绪描述文本 + self.current_instance_message_count: int = 0 # 当前私聊实例中的消息计数 + # --- 新增字段结束 --- \ No newline at end of file diff --git a/src/plugins/PFC/observation_info.py b/src/plugins/PFC/observation_info.py index 4f480c42..3dbdea7a 100644 --- a/src/plugins/PFC/observation_info.py +++ b/src/plugins/PFC/observation_info.py @@ -123,6 +123,12 @@ class ObservationInfo: """初始化 ObservationInfo""" self.private_name: str = private_name + # 新增:发信人信息 + self.sender_name: Optional[str] = None + self.sender_user_id: Optional[str] = None # 存储为字符串 + self.sender_platform: Optional[str] = None + + # 聊天记录相关 self.chat_history: List[Dict[str, Any]] = [] # 存储已处理的消息历史 self.chat_history_str: str = "还没有聊天记录。" # 用于生成 Prompt 的历史记录字符串 @@ -230,13 +236,37 @@ class ObservationInfo: if not message_time or not message_id: logger.warning(f"[私聊][{self.private_name}] 收到的消息缺少 time 或 message_id: {message}") return + + # --- 新增/修改:提取并存储发信人详细信息 --- + current_message_sender_id: Optional[str] = None + if user_info: + try: + self.sender_user_id = str(user_info.user_id) # 确保是字符串 + self.sender_name = user_info.user_nickname # 或者 user_info.card 如果私聊时card更准 + self.sender_platform = user_info.platform + current_message_sender_id = self.sender_user_id # 用于后续逻辑 + logger.debug(f"[私聊][{self.private_name}] 更新发信人信息: ID={self.sender_user_id}, Name={self.sender_name}, Platform={self.sender_platform}") + except AttributeError as e: + logger.error(f"[私聊][{self.private_name}] 从 UserInfo 对象提取信息时出错: {e}, UserInfo: {user_info}") + # 如果提取失败,将这些新字段设为 None,避免使用旧数据 + self.sender_user_id = None + self.sender_name = None + self.sender_platform = None + else: + logger.warning(f"[私聊][{self.private_name}] 处理消息更新时缺少有效的 UserInfo, message_id: {message_id}") + # 如果没有 UserInfo,也将这些新字段设为 None + self.sender_user_id = None + self.sender_name = None + self.sender_platform = None + # --- 新增/修改结束 --- + # 更新最后消息时间(所有消息) if message_time > (self.last_message_time or 0): self.last_message_time = message_time self.last_message_id = message_id self.last_message_content = processed_text - self.last_message_sender = sender_id_str + self.last_message_sender = current_message_sender_id # 使用新获取的 current_message_sender_id # 更新说话者特定时间 if sender_id_str: diff --git a/src/plugins/PFC/pfc_emotion_updater.py b/src/plugins/PFC/pfc_emotion_updater.py new file mode 100644 index 00000000..48aab11b --- /dev/null +++ b/src/plugins/PFC/pfc_emotion_updater.py @@ -0,0 +1,110 @@ +# PFC/pfc_emotion_updater.py + +from typing import List, Dict, Any, Optional + +from src.common.logger_manager import get_logger +from src.plugins.models.utils_model import LLMRequest +from src.plugins.moods.moods import MoodManager # MoodManager 本身是单例 +from src.plugins.utils.chat_message_builder import build_readable_messages + +try: + from .observation_info import ObservationInfo + from .conversation_info import ConversationInfo +except ImportError: + from observation_info import ObservationInfo + from conversation_info import ConversationInfo + +from ...config.config import global_config # 导入全局配置 + +logger = get_logger("pfc_emotion_updater") + +class PfcEmotionUpdater: + def __init__(self, private_name: str, bot_name: str): + """ + 初始化情绪更新器。 + """ + self.private_name = private_name + self.bot_name = bot_name + self.mood_mng = MoodManager.get_instance() # 获取 MoodManager 单例 + + # LLM 实例 (根据 global_config.llm_summary 配置) + llm_config_summary = getattr(global_config, 'llm_summary', None) + if llm_config_summary and isinstance(llm_config_summary, dict): + logger.info(f"[私聊][{self.private_name}] 使用 llm_summary 配置初始化情绪判断LLM。") + self.llm = LLMRequest( + model=llm_config_summary, + temperature=llm_config_summary.get("temperature", 0.5), # temperature 来自其自身配置或默认0.7,这里用0.5 + max_tokens=llm_config_summary.get("max_tokens", 256), # 情绪词输出不需要很多token + request_type="pfc_emotion_evaluation" + ) + else: + logger.error(f"[私聊][{self.private_name}] 未找到 llm_summary 配置或配置无效!情绪判断功能将受限。") + self.llm = None # LLM 未初始化 + + self.EMOTION_UPDATE_INTENSITY = getattr(global_config, 'pfc_emotion_update_intensity', 0.6) + self.EMOTION_HISTORY_COUNT = getattr(global_config, 'pfc_emotion_history_count', 5) + + async def update_emotion_based_on_context( + self, + conversation_info: ConversationInfo, + observation_info: ObservationInfo, + chat_observer_for_history, # ChatObserver 实例 + event_description: str + ) -> None: + if not self.llm: + logger.error(f"[私聊][{self.private_name}] LLM未初始化,无法进行情绪更新。") + # 即使LLM失败,也应该更新conversation_info中的情绪文本为MoodManager的当前状态 + if conversation_info and self.mood_mng: + conversation_info.current_emotion_text = self.mood_mng.get_prompt() + return + + if not self.mood_mng or not conversation_info or not observation_info: + logger.debug(f"[私聊][{self.private_name}] 情绪更新:缺少必要管理器或信息。") + return + + recent_messages_for_emotion: List[Dict[str, Any]] = [] + if chat_observer_for_history: + recent_messages_for_emotion = chat_observer_for_history.get_cached_messages(limit=self.EMOTION_HISTORY_COUNT) + elif observation_info.chat_history: + recent_messages_for_emotion = observation_info.chat_history[-self.EMOTION_HISTORY_COUNT:] + + readable_recent_history = await build_readable_messages( + recent_messages_for_emotion, replace_bot_name=True, merge_messages=True, timestamp_mode="none" + ) + + current_mood_text_from_manager = self.mood_mng.current_mood.text # 从 MoodManager 获取当前情绪文本 + sender_name_for_prompt = getattr(observation_info, 'sender_name', '对方') + if not sender_name_for_prompt: sender_name_for_prompt = '对方' + relationship_text_for_prompt = getattr(conversation_info, 'relationship_text', '关系一般。') # 从 ConversationInfo 获取关系文本 + + emotion_prompt = f"""你是机器人 {self.bot_name}。你现在的心情是【{current_mood_text_from_manager}】。 +你正在和用户【{sender_name_for_prompt}】私聊,你们的关系是:【{relationship_text_for_prompt}】。 +最近发生的事件是:【{event_description}】 +最近的对话摘要: +--- +{readable_recent_history} +--- +基于以上所有信息,你认为你现在最主要的情绪是什么?请从以下情绪词中选择一个(必须是列表中的一个): +[开心, 害羞, 愤怒, 恐惧, 悲伤, 厌恶, 惊讶, 困惑, 平静] +请只输出一个最符合的情绪词。例如: 开心 +如果难以判断或当前情绪依然合适,请输出: 无变化 +""" + try: + logger.debug(f"[私聊][{self.private_name}] 情绪判断Prompt:\n{emotion_prompt}") + content, _ = await self.llm.generate_response_async(emotion_prompt) + detected_emotion_word = content.strip().replace("\"", "").replace("'", "") + logger.debug(f"[私聊][{self.private_name}] 情绪判断LLM原始返回: '{detected_emotion_word}'") + + if detected_emotion_word and detected_emotion_word != "无变化" and detected_emotion_word in self.mood_mng.emotion_map: + self.mood_mng.update_mood_from_emotion(detected_emotion_word, intensity=self.EMOTION_UPDATE_INTENSITY) + logger.info(f"[私聊][{self.private_name}] 基于事件 '{event_description}',情绪已更新为倾向于 '{detected_emotion_word}'。当前心情: {self.mood_mng.current_mood.text}") + elif detected_emotion_word == "无变化": + logger.info(f"[私聊][{self.private_name}] 基于事件 '{event_description}',LLM判断情绪无显著变化。") + else: + logger.warning(f"[私聊][{self.private_name}] LLM返回了未知的情绪词 '{detected_emotion_word}' 或未返回有效词,情绪未主动更新。") + except Exception as e: + logger.error(f"[私聊][{self.private_name}] 情绪判断LLM调用或处理失败: {e}") + + # 无论LLM判断如何,都更新conversation_info中的情绪文本以供Prompt使用 + if conversation_info and self.mood_mng: # 确保conversation_info有效 + conversation_info.current_emotion_text = self.mood_mng.get_prompt() \ No newline at end of file diff --git a/src/plugins/PFC/pfc_relationship_updater.py b/src/plugins/PFC/pfc_relationship_updater.py new file mode 100644 index 00000000..a7ff5637 --- /dev/null +++ b/src/plugins/PFC/pfc_relationship_updater.py @@ -0,0 +1,219 @@ +from typing import List, Dict, Any, Optional +from src.common.logger_manager import get_logger +from src.plugins.models.utils_model import LLMRequest +from src.plugins.person_info.person_info import person_info_manager +from src.plugins.person_info.relationship_manager import relationship_manager # 主要用其 ensure_float 和 build_relationship_info +from src.plugins.utils.chat_message_builder import build_readable_messages + +# 从PFC内部导入必要的类 (根据你的项目结构调整,当前假设与PFC核心文件同级或有正确路径) +try: + from .observation_info import ObservationInfo + from .conversation_info import ConversationInfo + from .pfc_utils import get_items_from_json +except ImportError: # 如果直接从 plugins/PFC 目录运行,上面的相对导入会失败,尝试绝对导入 + from observation_info import ObservationInfo + from conversation_info import ConversationInfo + from pfc_utils import get_items_from_json + + +from ...config.config import global_config # 导入全局配置 (向上两级到 src/, 再到 config) + + +logger = get_logger("pfc_relationship_updater") + +class PfcRelationshipUpdater: + def __init__(self, private_name: str, bot_name: str): + """ + 初始化关系更新器。 + + Args: + private_name (str): 当前私聊对象的名称 (用于日志)。 + bot_name (str): 机器人自己的名称。 + """ + self.private_name = private_name + self.bot_name = bot_name + self.person_info_mng = person_info_manager + self.relationship_mng = relationship_manager # 复用其实例方法 + + # LLM 实例 (为关系评估创建一个新的) + # 尝试读取 llm_PFC_relationship_eval 配置,如果不存在则回退 + llm_config_rel_eval = getattr(global_config, 'llm_PFC_relationship_eval', None) + if llm_config_rel_eval and isinstance(llm_config_rel_eval, dict): + logger.info(f"[私聊][{self.private_name}] 使用 llm_PFC_relationship_eval 配置初始化关系评估LLM。") + self.llm = LLMRequest( + model=llm_config_rel_eval, + temperature=llm_config_rel_eval.get("temp", 0.5), # 判断任务通常用较低温度 + max_tokens=llm_config_rel_eval.get("max_tokens", 512), + request_type="pfc_relationship_evaluation" + ) + else: + logger.warning(f"[私聊][{self.private_name}] 未找到 llm_PFC_relationship_eval 配置或配置无效,将回退使用 llm_PFC_action_planner 的配置。") + llm_config_action_planner = getattr(global_config, 'llm_PFC_action_planner', None) + if llm_config_action_planner and isinstance(llm_config_action_planner, dict): + self.llm = LLMRequest( + model=llm_config_action_planner, # 使用 action_planner 的模型配置 + temperature=llm_config_action_planner.get("temp", 0.5), # 但温度可以尝试低一些 + max_tokens=llm_config_action_planner.get("max_tokens", 512), + request_type="pfc_relationship_evaluation_fallback" + ) + else: # 极端情况,连 action_planner 的配置都没有 + logger.error(f"[私聊][{self.private_name}] 无法找到任何有效的LLM配置用于关系评估!关系更新功能将受限。") + self.llm = None # LLM 未初始化 + + # 从 global_config 读取参数,若无则使用默认值 + self.REL_INCREMENTAL_INTERVAL = getattr(global_config, 'pfc_relationship_incremental_interval', 10) + self.REL_INCREMENTAL_MSG_COUNT = getattr(global_config, 'pfc_relationship_incremental_msg_count', 10) + self.REL_INCREMENTAL_DEFAULT_CHANGE = getattr(global_config, 'pfc_relationship_incremental_default_change', 1.0) + self.REL_INCREMENTAL_MAX_CHANGE = getattr(global_config, 'pfc_relationship_incremental_max_change', 5.0) + + self.REL_FINAL_MSG_COUNT = getattr(global_config, 'pfc_relationship_final_msg_count', 30) + self.REL_FINAL_DEFAULT_CHANGE = getattr(global_config, 'pfc_relationship_final_default_change', 5.0) + self.REL_FINAL_MAX_CHANGE = getattr(global_config, 'pfc_relationship_final_max_change', 50.0) + + async def update_relationship_incremental( + self, + conversation_info: ConversationInfo, + observation_info: ObservationInfo, + chat_observer_for_history # ChatObserver 实例 + ) -> None: + if not self.llm: + logger.error(f"[私聊][{self.private_name}] LLM未初始化,无法进行增量关系更新。") + return + if not conversation_info or not conversation_info.person_id or not observation_info: + logger.debug(f"[私聊][{self.private_name}] 增量关系更新:缺少必要信息。") + return + + if not (conversation_info.current_instance_message_count % self.REL_INCREMENTAL_INTERVAL == 0 \ + and conversation_info.current_instance_message_count > 0): + return + + logger.info(f"[私聊][{self.private_name}] 达到增量关系更新阈值 ({conversation_info.current_instance_message_count}条消息),开始评估...") + + messages_for_eval: List[Dict[str, Any]] = [] + if chat_observer_for_history: + messages_for_eval = chat_observer_for_history.get_cached_messages(limit=self.REL_INCREMENTAL_MSG_COUNT) + elif observation_info.chat_history: + messages_for_eval = observation_info.chat_history[-self.REL_INCREMENTAL_MSG_COUNT:] + + if not messages_for_eval: + logger.warning(f"[私聊][{self.private_name}] 增量关系更新:没有足够的消息进行评估。") + return + + readable_history_for_llm = await build_readable_messages( + messages_for_eval, replace_bot_name=True, merge_messages=False, timestamp_mode="relative" + ) + + current_relationship_value = await self.person_info_mng.get_value(conversation_info.person_id, "relationship_value") + current_relationship_value = self.relationship_mng.ensure_float(current_relationship_value, conversation_info.person_id) + + sender_name_for_prompt = getattr(observation_info, 'sender_name', '对方') + if not sender_name_for_prompt: sender_name_for_prompt = '对方' + + relationship_prompt = f"""你是{self.bot_name}。你正在与{sender_name_for_prompt}私聊。 +你们当前的关系值大约是 {current_relationship_value:.0f} (范围通常在-1000到1000,越高越代表关系越好)。 +以下是你们最近的对话内容: +--- +{readable_history_for_llm} +--- +请基于以上对话,判断你与{sender_name_for_prompt}的关系值应该如何“谨慎地”调整。 +请输出一个JSON对象,包含一个 "adjustment" 字段,其值为一个介于 -{self.REL_INCREMENTAL_MAX_CHANGE} 和 +{self.REL_INCREMENTAL_MAX_CHANGE} 之间的整数,代表关系值的变化。 +例如:{{ "adjustment": 3 }}。如果对话内容不明确或难以判断,请倾向于输出较小的调整值(如0, 1, -1)。""" + + adjustment_val = self.REL_INCREMENTAL_DEFAULT_CHANGE + try: + logger.debug(f"[私聊][{self.private_name}] 增量关系评估Prompt:\n{relationship_prompt}") + content, _ = await self.llm.generate_response_async(relationship_prompt) + logger.debug(f"[私聊][{self.private_name}] 增量关系评估LLM原始返回: {content}") + + success, result = get_items_from_json( + content, self.private_name, "adjustment", + default_values={"adjustment": self.REL_INCREMENTAL_DEFAULT_CHANGE}, + required_types={"adjustment": (int, float)} + ) + raw_adjustment = result.get("adjustment", self.REL_INCREMENTAL_DEFAULT_CHANGE) + if not isinstance(raw_adjustment, (int, float)): + adjustment_val = self.REL_INCREMENTAL_DEFAULT_CHANGE + else: + adjustment_val = float(raw_adjustment) + adjustment_val = max(-self.REL_INCREMENTAL_MAX_CHANGE, min(self.REL_INCREMENTAL_MAX_CHANGE, adjustment_val)) + except Exception as e: + logger.error(f"[私聊][{self.private_name}] 增量关系评估LLM调用或解析失败: {e}") + + new_relationship_value = max(-1000.0, min(1000.0, current_relationship_value + adjustment_val)) + await self.person_info_mng.update_one_field(conversation_info.person_id, "relationship_value", new_relationship_value) + logger.info(f"[私聊][{self.private_name}] 增量关系值更新:与【{sender_name_for_prompt}】的关系值从 {current_relationship_value:.2f} 调整了 {adjustment_val:.2f},变为 {new_relationship_value:.2f}") + + if conversation_info.person_id: + conversation_info.relationship_text = await self.relationship_mng.build_relationship_info(conversation_info.person_id, is_id=True) + + async def update_relationship_final( + self, + conversation_info: ConversationInfo, + observation_info: ObservationInfo, + chat_observer_for_history + ) -> None: + if not self.llm: + logger.error(f"[私聊][{self.private_name}] LLM未初始化,无法进行最终关系更新。") + return + if not conversation_info or not conversation_info.person_id or not observation_info: + logger.debug(f"[私聊][{self.private_name}] 最终关系更新:缺少必要信息。") + return + + logger.info(f"[私聊][{self.private_name}] 私聊结束,开始最终关系评估...") + + messages_for_eval: List[Dict[str, Any]] = [] + if chat_observer_for_history: + messages_for_eval = chat_observer_for_history.get_cached_messages(limit=self.REL_FINAL_MSG_COUNT) + elif observation_info.chat_history: + messages_for_eval = observation_info.chat_history[-self.REL_FINAL_MSG_COUNT:] + + if not messages_for_eval: + logger.warning(f"[私聊][{self.private_name}] 最终关系更新:没有足够的消息进行评估。") + return + + readable_history_for_llm = await build_readable_messages( + messages_for_eval, replace_bot_name=True, merge_messages=False, timestamp_mode="relative" + ) + + current_relationship_value = await self.person_info_mng.get_value(conversation_info.person_id, "relationship_value") + current_relationship_value = self.relationship_mng.ensure_float(current_relationship_value, conversation_info.person_id) + + sender_name_for_prompt = getattr(observation_info, 'sender_name', '对方') + if not sender_name_for_prompt: sender_name_for_prompt = '对方' + + relationship_prompt = f"""你是{self.bot_name}。你与{sender_name_for_prompt}的私聊刚刚结束。 +你们当前的关系值大约是 {current_relationship_value:.0f} (范围通常在-1000到1000,越高越好)。 +以下是你们本次私聊最后部分的对话内容: +--- +{readable_history_for_llm} +--- +请基于以上对话的整体情况,判断你与【{sender_name_for_prompt}】的关系值应该如何进行一次总结性的调整。 +请输出一个JSON对象,包含一个 "final_adjustment" 字段,其值为一个整数,代表关系值的变化量(例如,可以是 -{self.REL_FINAL_MAX_CHANGE} 到 +{self.REL_FINAL_MAX_CHANGE} 之间的一个值)。 +请大胆评估,但也要合理。""" + + adjustment_val = self.REL_FINAL_DEFAULT_CHANGE + try: + logger.debug(f"[私聊][{self.private_name}] 最终关系评估Prompt:\n{relationship_prompt}") + content, _ = await self.llm.generate_response_async(relationship_prompt) + logger.debug(f"[私聊][{self.private_name}] 最终关系评估LLM原始返回: {content}") + + success, result = get_items_from_json( + content, self.private_name, "final_adjustment", + default_values={"final_adjustment": self.REL_FINAL_DEFAULT_CHANGE}, + required_types={"final_adjustment": (int, float)} + ) + raw_adjustment = result.get("final_adjustment", self.REL_FINAL_DEFAULT_CHANGE) + if not isinstance(raw_adjustment, (int, float)): + adjustment_val = self.REL_FINAL_DEFAULT_CHANGE + else: + adjustment_val = float(raw_adjustment) + adjustment_val = max(-self.REL_FINAL_MAX_CHANGE, min(self.REL_FINAL_MAX_CHANGE, adjustment_val)) + except Exception as e: + logger.error(f"[私聊][{self.private_name}] 最终关系评估LLM调用或解析失败: {e}") + + new_relationship_value = max(-1000.0, min(1000.0, current_relationship_value + adjustment_val)) + await self.person_info_mng.update_one_field(conversation_info.person_id, "relationship_value", new_relationship_value) + logger.info(f"[私聊][{self.private_name}] 最终关系值更新:与【{sender_name_for_prompt}】的关系值从 {current_relationship_value:.2f} 调整了 {adjustment_val:.2f},最终为 {new_relationship_value:.2f}") + + if conversation_info.person_id: # 虽然通常结束了,但更新一下无妨 + conversation_info.relationship_text = await self.relationship_mng.build_relationship_info(conversation_info.person_id, is_id=True) \ No newline at end of file diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index feba6162..fbfca507 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -23,7 +23,11 @@ logger = get_logger("reply_generator") # Prompt for direct_reply (首次回复) PROMPT_DIRECT_REPLY = """ 当前时间:{current_time_str} -{persona_text}。现在你在参与一场QQ私聊,请根据以下信息生成一条回复: +{persona_text}。 +你正在和{sender_name}在QQ上私聊。 +你与对方的关系是:{relationship_text} +你现在的心情是:{current_emotion_text} +请根据以下信息生成一条回复: 当前对话目标:{goals_str} @@ -56,7 +60,11 @@ PROMPT_DIRECT_REPLY = """ # Prompt for send_new_message (追问/补充) PROMPT_SEND_NEW_MESSAGE = """ 当前时间:{current_time_str} -{persona_text}。现在你在参与一场QQ私聊,**刚刚你已经发送了一条或多条消息**,现在请根据以下信息再发一条新消息: +{persona_text}。 +你正在和{sender_name}在QQ上私聊,**并且刚刚你已经发送了一条或多条消息** +你与对方的关系是:{relationship_text} +你现在的心情是:{current_emotion_text} +现在请根据以下信息再发一条新消息: 当前对话目标:{goals_str} @@ -88,7 +96,11 @@ PROMPT_SEND_NEW_MESSAGE = """ # Prompt for say_goodbye (告别语生成) PROMPT_FAREWELL = """ 当前时间:{current_time_str} -{persona_text}。你在参与一场 QQ 私聊,现在对话似乎已经结束,你决定再发一条最后的消息来圆满结束。 +{persona_text}。 +你正在和{sender_name}私聊,在QQ上私聊,现在你们的对话似乎已经结束。 +你与对方的关系是:{relationship_text} +你现在的心情是:{current_emotion_text} +现在你决定再发一条最后的消息来圆满结束。 最近的聊天记录: {chat_history_text} @@ -179,6 +191,14 @@ class ReplyGenerator: chat_history_text += ( f"\n--- 以上均为已读消息,未读消息均已处理完毕 ---\n" ) + + # 获取 sender_name, relationship_text, current_emotion_text + sender_name_str = getattr(observation_info, 'sender_name', '对方') + if not sender_name_str: sender_name_str = '对方' + + relationship_text_str = getattr(conversation_info, 'relationship_text', '你们还不熟悉。') + current_emotion_text_str = getattr(conversation_info, 'current_emotion_text', '心情平静。') + # 构建 Persona 文本 (persona_text) persona_text = f"你的名字是{self.name},{self.personality_info}。" retrieval_context = chat_history_text # 使用前面构建好的 chat_history_text @@ -231,7 +251,10 @@ class ReplyGenerator: prompt = prompt_template.format( persona_text=persona_text, chat_history_text=chat_history_text, - current_time_str=current_time_value # 添加时间 + current_time_str=current_time_value, # 添加时间 + sender_name=sender_name_str, + relationship_text=relationship_text_str, + current_emotion_text=current_emotion_text_str ) else: @@ -242,7 +265,10 @@ class ReplyGenerator: retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。", retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。", last_rejection_info=last_rejection_info_str, # <--- 新增传递上次拒绝原因 - current_time_str=current_time_value # 新增:传入当前时间字符串 + current_time_str=current_time_value, # 新增:传入当前时间字符串 + sender_name=sender_name_str, + relationship_text=relationship_text_str, + current_emotion_text=current_emotion_text_str ) except KeyError as e: logger.error( diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index eb7ef184..a558563a 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "1.6.3" +version = "1.6.4" #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #如果你想要修改配置文件,请在修改后将version的值进行变更 @@ -297,6 +297,15 @@ provider = "SILICONFLOW" pri_in = 2 pri_out = 8 +# PFC 关系评估LLM +[model.llm_PFC_relationship_eval] +name = "Pro/deepseek-ai/DeepSeek-V3" # 或者其他你认为适合判断任务的模型 +provider = "SILICONFLOW" +temp = 0.4 # 判断任务建议温度稍低 +max_tokens = 512 # 根据Prompt长度和期望输出来调整 +pri_in = 2 # 价格信息(可选) +pri_out = 8 # 价格信息(可选) + #以下模型暂时没有使用!! #以下模型暂时没有使用!! #以下模型暂时没有使用!! From 34323b65a2704fed6d2bb265826b700f826a50b0 Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Wed, 7 May 2025 03:51:57 +0800 Subject: [PATCH 17/63] =?UTF-8?q?=E5=86=8D=E6=AC=A1=E9=82=AA=E6=81=B6?= =?UTF-8?q?=E7=9B=B4=E6=8E=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/chat_observer.py | 14 +++ src/plugins/PFC/conversation.py | 113 ++++++++++++++---- .../PFC/pfc_relationship_translator.py | 60 ++++++++++ 3 files changed, 162 insertions(+), 25 deletions(-) create mode 100644 src/plugins/PFC/pfc_relationship_translator.py diff --git a/src/plugins/PFC/chat_observer.py b/src/plugins/PFC/chat_observer.py index 22cbf27d..f4f43fea 100644 --- a/src/plugins/PFC/chat_observer.py +++ b/src/plugins/PFC/chat_observer.py @@ -101,6 +101,20 @@ class ChatObserver: message: 消息数据 """ try: + if isinstance(message, dict): # 确保是字典才添加 + self.message_cache.append(message) + # 可选:限制 message_cache 的大小,例如只保留最近 N 条 + # 你可以根据你的需求调整 MAX_CACHE_SIZE + # 对于情绪判断,可能不需要太长的历史,例如 5-10 条可能就足够了 + # 但 ChatObserver 的 get_cached_messages 也可能被其他地方用于获取更长的历史 + # 所以这里的 MAX_CACHE_SIZE 需要权衡,或者让调用者自己决定 limit + MAX_CACHE_SIZE = 30 # 例如,保留最近30条作为通用缓存 + if len(self.message_cache) > MAX_CACHE_SIZE: + self.message_cache = self.message_cache[-MAX_CACHE_SIZE:] + logger.debug(f"[私聊][{self.private_name}] 消息已添加到 ChatObserver 缓存,当前缓存大小: {len(self.message_cache)}") + else: + logger.warning(f"[私聊][{self.private_name}] 尝试向 message_cache 添加非字典类型消息: {type(message)}") + # 发送新消息通知 notification = create_new_message_notification( sender="chat_observer", target="observation_info", message=message diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index a95ca7a1..70e45886 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -26,7 +26,7 @@ from ...config.config import global_config # 导入用户信息 from ..person_info.person_info import person_info_manager # 导入关系 -from ..person_info.relationship_manager import relationship_manager +from .pfc_relationship_translator import translate_relationship_value_to_text # 导入情绪 from ..moods.moods import MoodManager @@ -93,7 +93,7 @@ class Conversation: # --- 新增:初始化管理器实例 --- self.person_info_mng = person_info_manager - self.relationship_mng = relationship_manager + self.relationship_mng = translate_relationship_value_to_text self.mood_mng = MoodManager.get_instance() # MoodManager 是单例 # 初始化所有核心组件为 None,将在 _initialize 中创建 @@ -281,6 +281,27 @@ class Conversation: else: logger.warning(f"[私聊][{self.private_name}] MoodManager 未能启动,相关功能可能受限。") + # --- 在初始化完成前,尝试加载一次关系和情绪信息 --- + if self.conversation_info and self.conversation_info.person_id and self.relationship_mng: + try: + self.conversation_info.relationship_text = await self.relationship_mng.build_relationship_info( + self.conversation_info.person_id, + is_id=True + ) + logger.info(f"[私聊][{self.private_name}] 初始化时加载关系文本: {self.conversation_info.relationship_text}") + except Exception as e_init_rel: + logger.error(f"[私聊][{self.private_name}] 初始化时加载关系文本出错: {e_init_rel}") + # 保留默认值 + + if self.conversation_info and self.mood_mng: + try: + self.conversation_info.current_emotion_text = self.mood_mng.get_prompt() + logger.info(f"[私聊][{self.private_name}] 初始化时加载情绪文本: {self.conversation_info.current_emotion_text}") + except Exception as e_init_emo: + logger.error(f"[私聊][{self.private_name}] 初始化时加载情绪文本出错: {e_init_emo}") + # 保留默认值 + + # 6. 标记初始化成功并设置运行状态 self._initialized = True self.should_continue = True # 初始化成功,标记可以继续运行循环 @@ -428,6 +449,22 @@ class Conversation: logger.info(f"[私聊][{self.private_name}] 正在停止对话实例: {self.stream_id}") self.should_continue = False # 设置标志,让主循环退出 + # --- 新增:在对话结束时调用最终关系更新 --- + if self._initialized and self.relationship_updater and self.conversation_info and self.observation_info and self.chat_observer: + try: + logger.info(f"[私聊][{self.private_name}] 准备执行最终关系评估...") + await self.relationship_updater.update_relationship_final( + conversation_info=self.conversation_info, + observation_info=self.observation_info, + chat_observer_for_history=self.chat_observer + ) + logger.info(f"[私聊][{self.private_name}] 最终关系评估已调用。") + except Exception as e_final_rel: + logger.error(f"[私聊][{self.private_name}] 调用最终关系评估时出错: {e_final_rel}") + logger.error(traceback.format_exc()) + else: + logger.warning(f"[私聊][{self.private_name}] 跳过最终关系评估,因为实例未完全初始化或缺少必要组件。") + # 停止空闲对话检测器 if self.idle_conversation_starter: self.idle_conversation_starter.stop() @@ -515,46 +552,72 @@ class Conversation: try: if self.conversation_info and self._initialized: # 确保 conversation_info 和实例已初始化 # 更新关系文本 - if self.conversation_info.person_id and self.observation_info and self.observation_info.sender_platform and self.observation_info.sender_user_id: + if self.conversation_info.person_id: # 确保 person_id 已获取 try: - # relationship_manager.build_relationship_info 需要一个 (platform, user_id, user_nickname) 格式的元组 - # 注意 user_id 应为 RelationshipManager 所期望的类型(通常是原始的,可能是字符串或数字,检查其内部实现) - # person_info.py 中 get_person_id 接收 int,但 build_relationship_info 的 person 参数更灵活 - # 这里我们假设 observation_info.sender_user_id 是字符串形式的 user_id - # 如果 RelationshipManager 内部的 get_person_id 需要 int,这里可能需要转换 - # 但 RelationshipManager 的 build_relationship_info 方法第一个参数 person 可以直接是 person_id (is_id=True) - # 或者是一个 (platform, user_id, nickname) 元组 (is_id=False) - # 为了安全,我们用 person_id - self.conversation_info.relationship_text = await self.relationship_mng.build_relationship_info( + # 1.直接从 person_info_manager 获取数值型的 relationship_value + numeric_relationship_value = await self.person_info_mng.get_value( self.conversation_info.person_id, - is_id=True # 明确告知是 person_id + "relationship_value" ) - logger.debug(f"[私聊][{self.private_name}] 更新关系文本: {self.conversation_info.relationship_text}") + + # 确保 relationship_value 是浮点数 (可以复用 relationship_manager 中的 ensure_float,或者在这里简单处理) + if not isinstance(numeric_relationship_value, (int, float)): + # 尝试从 Decimal128 转换 (如果你的数据库用的是这个) + from bson.decimal128 import Decimal128 + if isinstance(numeric_relationship_value, Decimal128): + numeric_relationship_value = float(numeric_relationship_value.to_decimal()) + else: # 其他类型,或转换失败,给默认值 + logger.warning(f"[私聊][{self.private_name}] 获取的 relationship_value 类型未知 ({type(numeric_relationship_value)}) 或转换失败,默认为0.0") + numeric_relationship_value = 0.0 + + logger.debug(f"[私聊][{self.private_name}] 获取到数值型关系值: {numeric_relationship_value}") + + # 2. 使用PFC内部的翻译函数将其转换为文本描述 + simplified_relationship_text = translate_relationship_value_to_text(numeric_relationship_value) + self.conversation_info.relationship_text = simplified_relationship_text + + logger.debug(f"[私聊][{self.private_name}] 更新后关系文本 (PFC内部翻译): {self.conversation_info.relationship_text}") + except Exception as e_rel: - logger.error(f"[私聊][{self.private_name}] 更新关系文本时出错: {e_rel}") - self.conversation_info.relationship_text = "我们之间的关系有点微妙。" # 出错时的默认值 - elif not self.conversation_info.person_id and self.observation_info and self.observation_info.sender_user_id and self.observation_info.sender_platform: - # 如果 person_id 之前没获取到,在这里尝试再次获取 + logger.error(f"[私聊][{self.private_name}] 更新关系文本(PFC内部翻译)时出错: {e_rel}") + self.conversation_info.relationship_text = "你们的关系是:普通。" # 出错时的默认值 + elif self.observation_info and self.observation_info.sender_user_id and self.observation_info.sender_platform: + # 如果 person_id 之前没获取到,在这里尝试再次获取 (这部分逻辑保持,因为 person_id 是必须的) try: private_user_id_int = int(self.observation_info.sender_user_id) self.conversation_info.person_id = self.person_info_mng.get_person_id( self.observation_info.sender_platform, private_user_id_int ) - await self.person_info_mng.get_or_create_person( + await self.person_info_mng.get_or_create_person( #确保用户存在 platform=self.observation_info.sender_platform, user_id=private_user_id_int, nickname=self.observation_info.sender_name if self.observation_info.sender_name else "未知用户", ) - if self.conversation_info.person_id: # 再次尝试更新关系文本 - self.conversation_info.relationship_text = await self.relationship_mng.build_relationship_info( - self.conversation_info.person_id, is_id=True - ) + if self.conversation_info.person_id: # 如果成功获取 person_id,则再次尝试更新关系文本 + numeric_relationship_value = await self.person_info_mng.get_value( + self.conversation_info.person_id, + "relationship_value" + ) + if not isinstance(numeric_relationship_value, (int, float)): + from bson.decimal128 import Decimal128 + if isinstance(numeric_relationship_value, Decimal128): + numeric_relationship_value = float(numeric_relationship_value.to_decimal()) + else: + numeric_relationship_value = 0.0 + self.conversation_info.relationship_text = translate_relationship_value_to_text(numeric_relationship_value) + logger.debug(f"[私聊][{self.private_name}] (备用逻辑)更新后关系文本: {self.conversation_info.relationship_text}") + except ValueError: logger.error(f"[私聊][{self.private_name}] 循环中无法将 sender_user_id ('{self.observation_info.sender_user_id}') 转换为整数。") + self.conversation_info.relationship_text = "你们的关系是:普通。" except Exception as e_pid_loop: - logger.error(f"[私聊][{self.private_name}] 循环中获取 person_id 时出错: {e_pid_loop}") - + logger.error(f"[私聊][{self.private_name}] 循环中获取 person_id 并更新关系时出错: {e_pid_loop}") + self.conversation_info.relationship_text = "你们的关系是:普通。" + else: + # 如果 person_id 仍无法确定 + self.conversation_info.relationship_text = "你们的关系是:普通。" # 或 "你们的关系是:未知。" + # 更新情绪文本 if self.mood_mng: self.conversation_info.current_emotion_text = self.mood_mng.get_prompt() diff --git a/src/plugins/PFC/pfc_relationship_translator.py b/src/plugins/PFC/pfc_relationship_translator.py new file mode 100644 index 00000000..09cc3ab0 --- /dev/null +++ b/src/plugins/PFC/pfc_relationship_translator.py @@ -0,0 +1,60 @@ +# 直接完整导入群聊的relationship_manager.py可能不可取,因为对于PFC的planner来说,其暗示了选择回复,所以新建代码文件来适配PFC的决策层面 +from src.common.logger_manager import get_logger + +logger = get_logger("pfc_relationship_translator") + +# 这个函数复用 relationship_manager.py 中的 calculate_level_num 的逻辑 +def _calculate_relationship_level_num(relationship_value: float) -> int: + """ + 根据关系值计算关系等级编号 (0-5)。 + 这里的阈值应与 relationship_manager.py 中的保持一致 + """ + if not isinstance(relationship_value, (int, float)): + logger.warning(f"传入的 relationship_value '{relationship_value}' 不是有效的数值类型,默认为0。") + relationship_value = 0.0 + + if -1000 <= relationship_value < -227: + level_num = 0 # 厌恶 + elif -227 <= relationship_value < -73: + level_num = 1 # 冷漠 + elif -73 <= relationship_value < 227: + level_num = 2 # 普通/认识 + elif 227 <= relationship_value < 587: + level_num = 3 # 友好 + elif 587 <= relationship_value < 900: + level_num = 4 # 喜欢 + elif 900 <= relationship_value <= 1000: + level_num = 5 # 暧昧 + else: + # 超出范围的值处理 + if relationship_value > 1000: + level_num = 5 + elif relationship_value < -1000: + level_num = 0 + else: # 理论上不会到这里,除非前面的条件逻辑有误 + logger.warning(f"关系值 {relationship_value} 未落入任何预设范围,默认为普通。") + level_num = 2 + return level_num + +def translate_relationship_value_to_text(relationship_value: float) -> str: + """ + 将数值型的关系值转换为PFC私聊场景下简洁的关系描述文本。 + """ + level_num = _calculate_relationship_level_num(relationship_value) + + relationship_descriptions = [ + "厌恶", # level_num 0 + "冷漠", # level_num 1 + "初识", # level_num 2 + "友好", # level_num 3 + "喜欢", # level_num 4 + "暧昧" # level_num 5 + ] + + if 0 <= level_num < len(relationship_descriptions): + description = relationship_descriptions[level_num] + else: + description = "普通" # 默认或错误情况 + logger.warning(f"计算出的 level_num ({level_num}) 无效,关系描述默认为 '普通'") + + return f"你们的关系是:{description}。" \ No newline at end of file From 3350608173058dbb9c8d3a906c54d0da6dab0892 Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Wed, 7 May 2025 04:10:07 +0800 Subject: [PATCH 18/63] fix --- src/plugins/PFC/conversation.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 70e45886..e237216e 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -25,6 +25,7 @@ from ...config.config import global_config # 导入用户信息 from ..person_info.person_info import person_info_manager +from ..person_info.relationship_manager import relationship_manager # 导入关系 from .pfc_relationship_translator import translate_relationship_value_to_text # 导入情绪 @@ -93,7 +94,7 @@ class Conversation: # --- 新增:初始化管理器实例 --- self.person_info_mng = person_info_manager - self.relationship_mng = translate_relationship_value_to_text + self.relationship_mng = relationship_manager self.mood_mng = MoodManager.get_instance() # MoodManager 是单例 # 初始化所有核心组件为 None,将在 _initialize 中创建 @@ -282,17 +283,27 @@ class Conversation: logger.warning(f"[私聊][{self.private_name}] MoodManager 未能启动,相关功能可能受限。") # --- 在初始化完成前,尝试加载一次关系和情绪信息 --- - if self.conversation_info and self.conversation_info.person_id and self.relationship_mng: + if self.conversation_info and self.conversation_info.person_id: try: - self.conversation_info.relationship_text = await self.relationship_mng.build_relationship_info( - self.conversation_info.person_id, - is_id=True - ) - logger.info(f"[私聊][{self.private_name}] 初始化时加载关系文本: {self.conversation_info.relationship_text}") - except Exception as e_init_rel: - logger.error(f"[私聊][{self.private_name}] 初始化时加载关系文本出错: {e_init_rel}") - # 保留默认值 + # 1. 获取数值型关系值 + numeric_relationship_value = await self.person_info_mng.get_value( + self.conversation_info.person_id, + "relationship_value" + ) + # 确保是浮点数 + if not isinstance(numeric_relationship_value, (int, float)): + from bson.decimal128 import Decimal128 + if isinstance(numeric_relationship_value, Decimal128): + numeric_relationship_value = float(numeric_relationship_value.to_decimal()) + else: + numeric_relationship_value = 0.0 + # 2. 使用PFC内部翻译函数 + self.conversation_info.relationship_text = translate_relationship_value_to_text(numeric_relationship_value) + logger.info(f"[私聊][{self.private_name}] 初始化时加载关系文本: {self.conversation_info.relationship_text}") + except Exception as e_init_rel: + logger.error(f"[私聊][{self.private_name}] 初始化时加载关系文本出错: {e_init_rel}") + self.conversation_info.relationship_text = "你们的关系是:普通。" # 保留默认值 if self.conversation_info and self.mood_mng: try: self.conversation_info.current_emotion_text = self.mood_mng.get_prompt() From 92df03376c77c533dedebf05250297c514722635 Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Wed, 7 May 2025 05:12:01 +0800 Subject: [PATCH 19/63] =?UTF-8?q?=E5=BE=AE=E8=B0=83prompt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/action_planner.py | 21 +++++++++++++++------ src/plugins/PFC/reply_generator.py | 4 ++-- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 1315c52a..f7326856 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -43,6 +43,8 @@ PROMPT_INITIAL_REPLY = """ 【你的回忆】 {retrieved_memory_str} +{spam_warning_info} + ------ 可选行动类型以及解释: listening: 倾听对方发言,当你认为对方话才说到一半,发言明显未结束时选择 @@ -82,13 +84,16 @@ PROMPT_FOLLOW_UP = """ {chat_history_text} 【你的回忆】 {retrieved_memory_str} + +{spam_warning_info} + ------ 可选行动类型以及解释: -wait: 暂时不说话,留给对方交互空间,等待对方回复(尤其是在你刚发言后、或上次发言因重复、发言过多被拒时、或不确定做什么时,这是不错的选择)。 +wait: 暂时不说话,留给对方交互空间,等待对方回复。 listening: 倾听对方发言(虽然你刚发过言,但如果对方立刻回复且明显话没说完,可以选择这个) -send_new_message: 发送一条新消息继续对话,允许适当的追问、补充、深入话题,或开启相关新话题。**但是避免在因重复被拒后立即使用,也不要在对方没有回复的情况下过多的“消息轰炸”或重复发言** +send_new_message: 发送一条新消息继续对话,允许适当的追问、补充、深入话题,或开启相关新话题(但是注意看对话记录,如果对方已经没有回复你,wait或end_conversation可能更合适)。 rethink_goal: 思考一个对话目标,当你觉得目前对话需要目标,或当前目标不再适用,或话题卡住时选择。注意私聊的环境是灵活的,有可能需要经常选择 -end_conversation: 结束对话,对方长时间没回复,繁忙,或者当你觉得对话告一段落时可以选择 +end_conversation: 安全和平的结束对话,对方长时间没回复、繁忙、已经不再回复你消息、明显暗示或表达想结束聊天时,可以果断选择 block_and_ignore: 更加极端的结束对话方式,直接结束对话并在一段时间内无视对方所有发言(屏蔽),当对话让你感到十分不适,或你遭到各类骚扰时选择 请以JSON格式输出你的决策: @@ -182,7 +187,7 @@ class ActionPlanner: sender_name_str = getattr(observation_info, 'sender_name', '对方') # 从 observation_info 获取 if not sender_name_str: sender_name_str = '对方' # 再次确保有默认值 - relationship_text_str = getattr(conversation_info, 'relationship_text', '我们还不熟悉。') + relationship_text_str = getattr(conversation_info, 'relationship_text', '你们还不熟悉。') current_emotion_text_str = getattr(conversation_info, 'current_emotion_text', '心情平静。') @@ -213,10 +218,13 @@ class ActionPlanner: if observation_info and hasattr(observation_info, 'current_time_str') and observation_info.current_time_str: current_time_value = observation_info.current_time_str + spam_warning_message = "" # 初始化为空字符串 if conversation_info.my_message_count > 5: - current_time_value += f"\n你已连续发送{str(conversation_info.my_message_count)}条消息,请注意不要连续发送大量消息,以免刷屏对造成对方困扰。" + spam_warning_message = f"⚠️【刷屏警告】**你已连续发送{str(conversation_info.my_message_count)}条消息!请注意不要再选择send_new_message!以免刷屏对造成对方困扰!**" elif conversation_info.my_message_count > 2: - current_time_value += f"\n你已连续发送{str(conversation_info.my_message_count)}条消息,如果没有必要请不要连续发送大量消息,以免刷屏给造成对方困扰。" + spam_warning_message = f"💬【提示】**你已连续发送{str(conversation_info.my_message_count)}条消息。如果非必要,请避免选择send_new_message,以免给对方造成困扰。**" + if spam_warning_message: # 仅当有警告时才添加换行符 + spam_warning_message = f"\n{spam_warning_message}\n" prompt = prompt_template.format( persona_text=persona_text, @@ -229,6 +237,7 @@ class ActionPlanner: retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。", retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。", current_time_str=current_time_value, # 新增:传入当前时间字符串 + spam_warning_info=spam_warning_message, ### 标记新增/修改区域 开始 ### sender_name=sender_name_str, relationship_text=relationship_text_str, diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index fbfca507..84dcea5f 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -48,7 +48,7 @@ PROMPT_DIRECT_REPLY = """ 2. 符合你的性格特征和身份细节 3. 通俗易懂,自然流畅,像正常聊天一样,简短(通常20字以内,除非特殊情况) 4. 可以适当利用相关知识和回忆,但**不要生硬引用**,若无必要,也可以不利用 -5. 自然、得体,结合聊天记录逻辑合理,且没有重复表达同质内容 +5. 自然、得体,结合聊天记录逻辑合理,没有重复表达同质内容,也没有复读你之前的发言 请注意把握聊天内容,不要回复的太有条理,可以有个性。请分清"你"和对方说的话,不要把"你"说的话当做对方说的话,这是你自己说的话。 可以回复得自然随意自然一些,就像真人一样,注意把握聊天内容,整体风格可以平和、简短,不要刻意突出自身学科背景,不要说你说过的话,可以简短,多简短都可以,但是避免冗长。 @@ -84,7 +84,7 @@ PROMPT_SEND_NEW_MESSAGE = """ 2. 符合你的性格特征和身份细节 3. 通俗易懂,自然流畅,像正常聊天一样,简短(通常20字以内,除非特殊情况) 4. 可以适当利用相关知识和回忆,但**不要生硬引用**,若无必要,也可以不利用 -5. 跟之前你发的消息自然的衔接,逻辑合理,且没有重复表达同质内容或部分重叠内容 +5. 跟之前你发的消息自然的衔接,逻辑合理,没有重复表达同质内容或部分重叠内容,也没有复读你之前的发言 请注意把握聊天内容,不用太有条理,可以有个性。请分清"你"和对方说的话,不要把"你"说的话当做对方说的话,这是你自己说的话。 这条消息可以自然随意自然一些,就像真人一样,注意把握聊天内容,整体风格可以平和、简短,不要刻意突出自身学科背景,不要说你说过的话,可以简短,多简短都可以,但是避免冗长。 From 04fc32295f86503d0d5499aaeb8e8fd73738f244 Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Wed, 7 May 2025 05:38:27 +0800 Subject: [PATCH 20/63] =?UTF-8?q?=E6=B7=BB=E5=8A=A0checker=E5=BC=80?= =?UTF-8?q?=E5=85=B3=E4=B8=BA=E5=8F=AF=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/config.py | 4 ++++ src/plugins/PFC/conversation.py | 35 +++++++++++++++++++------------ template/bot_config_template.toml | 3 ++- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/config/config.py b/src/config/config.py index ddceac62..0242d52a 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -273,6 +273,7 @@ class BotConfig: # enable_think_flow: bool = False # 是否启用思考流程 talk_allowed_private = set() enable_pfc_chatting: bool = False # 是否启用PFC聊天 + enable_pfc_reply_checker: bool = True # 是否开启PFC回复检查 # idle_conversation enable_idle_conversation: bool = False # 是否启用 pfc 主动发言(未完善) @@ -665,6 +666,9 @@ class BotConfig: config.talk_allowed_private = set(str(user) for user in experimental_config.get("talk_allowed_private", [])) if config.INNER_VERSION in SpecifierSet(">=1.1.0"): config.enable_pfc_chatting = experimental_config.get("pfc_chatting", config.enable_pfc_chatting) + if config.INNER_VERSION in SpecifierSet(">=1.1.0"): + config.enable_pfc_reply_checker = experimental_config.get("enable_pfc_reply_checker", config.enable_pfc_reply_checker) + logger.info(f"PFC Reply Checker 状态: {'启用' if config.enable_pfc_reply_checker else '关闭'}") def idle_conversation(parent: dict): idle_conversation_config = parent["idle_conversation"] diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index e237216e..6969c73c 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -961,6 +961,8 @@ class Conversation: # retry_count for checker starts from 0 current_retry_for_checker = reply_attempt_count - 1 + + current_time_value_for_check = "获取时间失败" if observation_info and hasattr(observation_info, 'current_time_str') and observation_info.current_time_str: @@ -968,19 +970,26 @@ class Conversation: logger.debug(f"{log_prefix} 调用 ReplyChecker 检查...") - # 调用 ReplyChecker 的 check 方法 - is_suitable, check_reason, need_replan_from_checker = await self.reply_checker.check( - reply=generated_content, - 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}" - ) - + # --- 根据配置决定是否执行检查 --- + if global_config.enable_pfc_reply_checker: # <--- 使用配置项 + logger.debug(f"{log_prefix} 调用 ReplyChecker 检查 (配置已启用)...") + is_suitable, check_reason, need_replan_from_checker = await self.reply_checker.check( + reply=generated_content, + 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.info(f"[配置关闭] ReplyChecker 已跳过,默认回复为合适。") # 如果不合适,记录原因并准备下一次尝试(如果还有次数) if not is_suitable: # 记录拒绝原因和内容,供下次生成时参考 diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index a558563a..20f6aa5e 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "1.6.4" +version = "1.6.5" #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #如果你想要修改配置文件,请在修改后将version的值进行变更 @@ -188,6 +188,7 @@ enable = true enable_friend_chat = false # 是否启用好友聊天 talk_allowed_private = [] # 可以回复消息的QQ号 pfc_chatting = false # 是否启用PFC聊天,该功能仅作用于私聊,与回复模式独立 +enable_pfc_reply_checker = true # 是否启用 PFC 的回复检查器 [idle_conversation] enable_idle_conversation = false From 377d798c72bf4eb69e7a57e4757b7051831b2ca5 Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Wed, 7 May 2025 06:08:17 +0800 Subject: [PATCH 21/63] =?UTF-8?q?=E5=BE=AE=E8=B0=83prompt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/action_planner.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index f7326856..3ac88599 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -91,7 +91,7 @@ PROMPT_FOLLOW_UP = """ 可选行动类型以及解释: wait: 暂时不说话,留给对方交互空间,等待对方回复。 listening: 倾听对方发言(虽然你刚发过言,但如果对方立刻回复且明显话没说完,可以选择这个) -send_new_message: 发送一条新消息继续对话,允许适当的追问、补充、深入话题,或开启相关新话题(但是注意看对话记录,如果对方已经没有回复你,wait或end_conversation可能更合适)。 +send_new_message: 发送一条新消息继续对话,允许适当的追问、补充、深入话题,或开启相关新话题(但是注意看对话记录,如果对方已经没有回复你,end_conversation或wait可能更合适)。 rethink_goal: 思考一个对话目标,当你觉得目前对话需要目标,或当前目标不再适用,或话题卡住时选择。注意私聊的环境是灵活的,有可能需要经常选择 end_conversation: 安全和平的结束对话,对方长时间没回复、繁忙、已经不再回复你消息、明显暗示或表达想结束聊天时,可以果断选择 block_and_ignore: 更加极端的结束对话方式,直接结束对话并在一段时间内无视对方所有发言(屏蔽),当对话让你感到十分不适,或你遭到各类骚扰时选择 @@ -220,9 +220,9 @@ class ActionPlanner: spam_warning_message = "" # 初始化为空字符串 if conversation_info.my_message_count > 5: - spam_warning_message = f"⚠️【刷屏警告】**你已连续发送{str(conversation_info.my_message_count)}条消息!请注意不要再选择send_new_message!以免刷屏对造成对方困扰!**" + spam_warning_message = f"⚠️【警告】**你已连续发送{str(conversation_info.my_message_count)}条消息!请注意不要再选择send_new_message!以免刷屏对造成对方困扰!**" elif conversation_info.my_message_count > 2: - spam_warning_message = f"💬【提示】**你已连续发送{str(conversation_info.my_message_count)}条消息。如果非必要,请避免选择send_new_message,以免给对方造成困扰。**" + spam_warning_message = f"💬【警告】**你已连续发送{str(conversation_info.my_message_count)}条消息。请保持理智,如果非必要,请避免选择send_new_message,以免给对方造成困扰。**" if spam_warning_message: # 仅当有警告时才添加换行符 spam_warning_message = f"\n{spam_warning_message}\n" From 67b958a1401fa5e34b9d5d30195879f8c9dd82d9 Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Wed, 7 May 2025 08:03:03 +0800 Subject: [PATCH 22/63] =?UTF-8?q?=E5=86=8D=E6=8E=A8=EF=BC=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/action_planner.py | 92 +++++++-- src/plugins/PFC/conversation.py | 301 ++++++++++++++++++----------- src/plugins/PFC/reply_generator.py | 111 +++++++---- 3 files changed, 339 insertions(+), 165 deletions(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 3ac88599..e59adadb 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -124,6 +124,48 @@ PROMPT_END_DECISION = """ 注意:请严格按照 JSON 格式输出,不要包含任何其他内容。""" +# Prompt(4): 当 reply_generator 决定不发送消息后的反思决策 Prompt +PROMPT_REFLECT_AND_ACT = """ +当前时间:{current_time_str} +{persona_text} +现在你正在和{sender_name}在QQ上私聊 +你与对方的关系是:{relationship_text} +你现在的心情是:{current_emotion_text} +刚刚你本来想发一条新消息,但是想了想,你决定不发了。 +请根据以下【所有信息】审慎且灵活的决策下一步行动,可以等待,可以倾听,可以结束对话,甚至可以屏蔽对方: + +【当前对话目标】 +{goals_str} +【最近行动历史概要】 +{action_history_summary} +【你想起来的相关知识】 +{retrieved_knowledge_str} +【上一次行动的详细情况和结果】 +{last_action_context} +【时间和超时提示】 +{time_since_last_bot_message_info}{timeout_context} +【最近的对话记录】(包括你已成功发送的消息 和 新收到的消息) +{chat_history_text} +【你的回忆】 +{retrieved_memory_str} + +{spam_warning_info} + +------ +可选行动类型以及解释: +wait: 等待,暂时不说话。 +listening: 倾听对方发言(虽然你刚发过言,但如果对方立刻回复且明显话没说完,可以选择这个) +rethink_goal: 思考一个对话目标,当你觉得目前对话需要目标,或当前目标不再适用,或话题卡住时选择。注意私聊的环境是灵活的,有可能需要经常选择 +end_conversation: 安全和平的结束对话,对方长时间没回复、繁忙、已经不再回复你消息、明显暗示或表达想结束聊天时,可以果断选择 +block_and_ignore: 更加极端的结束对话方式,直接结束对话并在一段时间内无视对方所有发言(屏蔽),当对话让你感到十分不适,或你遭到各类骚扰时选择 + +请以JSON格式输出你的决策: +{{ + "action": "选择的行动类型 (必须是上面列表中的一个)", + "reason": "选择该行动的详细原因 (必须有解释你是如何根据“上一次行动结果”、“对话记录”和自身设定人设做出合理判断的。)" +}} + +注意:请严格按照JSON格式输出,不要包含任何其他内容。""" class ActionPlanner: """行动规划器""" @@ -162,6 +204,7 @@ class ActionPlanner: observation_info: ObservationInfo, conversation_info: ConversationInfo, last_successful_reply_action: Optional[str], + use_reflect_prompt: bool = False # 新增参数,用于指示是否使用PROMPT_REFLECT_AND_ACT ) -> Tuple[str, str]: """ 规划下一步行动。 @@ -206,24 +249,38 @@ class ActionPlanner: # --- 2. 选择并格式化 Prompt --- try: - if last_successful_reply_action in ["direct_reply", "send_new_message"]: + if use_reflect_prompt: # 新增的判断 + prompt_template = PROMPT_REFLECT_AND_ACT + log_msg = "使用 PROMPT_REFLECT_AND_ACT (反思决策)" + # 对于 PROMPT_REFLECT_AND_ACT,它不包含 send_new_message 选项,所以 spam_warning_message 中的相关提示可以调整或省略 + # 但为了保持占位符填充的一致性,我们仍然计算它 + spam_warning_message = "" + if conversation_info.my_message_count > 5: # 这里的 my_message_count 仍有意义,表示之前连续发送了多少 + spam_warning_message = f"⚠️【警告】**你之前已连续发送{str(conversation_info.my_message_count)}条消息!请谨慎决策。**" + elif conversation_info.my_message_count > 2: + spam_warning_message = f"💬【提示】**你之前已连续发送{str(conversation_info.my_message_count)}条消息。请注意保持对话平衡。**" + + elif last_successful_reply_action in ["direct_reply", "send_new_message"]: prompt_template = PROMPT_FOLLOW_UP log_msg = "使用 PROMPT_FOLLOW_UP (追问决策)" + spam_warning_message = "" + if conversation_info.my_message_count > 5: + spam_warning_message = f"⚠️【警告】**你已连续发送{str(conversation_info.my_message_count)}条消息!请注意不要再选择send_new_message!以免刷屏对造成对方困扰!**" + elif conversation_info.my_message_count > 2: + spam_warning_message = f"💬【警告】**你已连续发送{str(conversation_info.my_message_count)}条消息。请保持理智,如果非必要,请避免选择send_new_message,以免给对方造成困扰。**" + else: prompt_template = PROMPT_INITIAL_REPLY log_msg = "使用 PROMPT_INITIAL_REPLY (首次/非连续回复决策)" + spam_warning_message = "" # 初始回复时通常不需要刷屏警告 + logger.debug(f"[私聊][{self.private_name}] {log_msg}") - current_time_value = "获取时间失败" # 默认值 + current_time_value = "获取时间失败" if observation_info and hasattr(observation_info, 'current_time_str') and observation_info.current_time_str: current_time_value = observation_info.current_time_str - spam_warning_message = "" # 初始化为空字符串 - if conversation_info.my_message_count > 5: - spam_warning_message = f"⚠️【警告】**你已连续发送{str(conversation_info.my_message_count)}条消息!请注意不要再选择send_new_message!以免刷屏对造成对方困扰!**" - elif conversation_info.my_message_count > 2: - spam_warning_message = f"💬【警告】**你已连续发送{str(conversation_info.my_message_count)}条消息。请保持理智,如果非必要,请避免选择send_new_message,以免给对方造成困扰。**" - if spam_warning_message: # 仅当有警告时才添加换行符 + if spam_warning_message: spam_warning_message = f"\n{spam_warning_message}\n" prompt = prompt_template.format( @@ -236,9 +293,8 @@ class ActionPlanner: chat_history_text=chat_history_text if chat_history_text.strip() else "还没有聊天记录。", retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。", retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。", - current_time_str=current_time_value, # 新增:传入当前时间字符串 + current_time_str=current_time_value, spam_warning_info=spam_warning_message, - ### 标记新增/修改区域 开始 ### sender_name=sender_name_str, relationship_text=relationship_text_str, current_emotion_text=current_emotion_text_str @@ -299,7 +355,7 @@ class ActionPlanner: # final_reason = initial_reason # --- 5. 验证最终行动类型 --- - valid_actions = [ + valid_actions_default = [ "direct_reply", "send_new_message", "wait", @@ -309,7 +365,19 @@ class ActionPlanner: "block_and_ignore", "say_goodbye", ] - if final_action not in valid_actions: + valid_actions_reflect = [ # PROMPT_REFLECT_AND_ACT 的动作 + "wait", + "listening", + "rethink_goal", + "end_conversation", + "block_and_ignore", + # PROMPT_REFLECT_AND_ACT 也可以 end_conversation,然后也可能触发 say_goodbye + "say_goodbye", + ] + + current_valid_actions = valid_actions_reflect if use_reflect_prompt else valid_actions_default + + if final_action not in current_valid_actions: logger.warning(f"[私聊][{self.private_name}] LLM 返回了未知的行动类型: '{final_action}',强制改为 wait") final_reason = f"(原始行动'{final_action}'无效,已强制改为wait) {final_reason}" final_action = "wait" # 遇到无效动作,默认等待 diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 6969c73c..4d2291c3 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -2,6 +2,8 @@ import time import asyncio import datetime import traceback +import json # 确保导入 json 模块 +from .pfc_utils import get_items_from_json # 导入JSON解析工具 from typing import Dict, Any, Optional, Set, List from dateutil import tz @@ -506,7 +508,7 @@ class Conversation: if not self._initialized: logger.error(f"[私聊][{self.private_name}] 尝试在未初始化状态下运行规划循环,退出。") return # 明确退出 - + force_reflect_and_act = False # 主循环,只要 should_continue 为 True 就一直运行 while self.should_continue: loop_iter_start_time = time.time() # 记录本次循环开始时间 @@ -654,10 +656,12 @@ class Conversation: logger.debug(f"[私聊][{self.private_name}] 调用 ActionPlanner.plan...") # 传入当前观察信息、对话信息和上次成功回复的动作类型 action, reason = await self.action_planner.plan( - self.observation_info, - self.conversation_info, - self.conversation_info.last_successful_reply_action + self.observation_info, + self.conversation_info, # type: ignore + self.conversation_info.last_successful_reply_action, # type: ignore + use_reflect_prompt=force_reflect_and_act # 使用标志 ) + force_reflect_and_act = False planning_duration = time.time() - planning_start_time logger.debug( f"[私聊][{self.private_name}] ActionPlanner.plan 完成 (耗时: {planning_duration:.3f} 秒),初步规划动作: {action}" @@ -765,6 +769,16 @@ class Conversation: await self._handle_action(action, reason, self.observation_info, self.conversation_info) logger.debug(f"[私聊][{self.private_name}] _handle_action 完成。") + # --- 新增逻辑:检查是否因为RG决定不发送而需要反思 --- + last_action_record = ( + self.conversation_info.done_action[-1] if self.conversation_info.done_action else {} # type: ignore + ) + if last_action_record.get("action") == "send_new_message" and \ + last_action_record.get("status") == "done_no_reply": + logger.info(f"[私聊][{self.private_name}] 检测到 ReplyGenerator 决定不发送消息,将在下一轮强制使用反思Prompt。") + force_reflect_and_act = True # 设置标志,下一轮使用反思prompt + # 不需要立即 continue,让循环自然进入下一轮,下一轮的 plan 会用这个标志 + # 8. 检查是否需要结束整个对话(例如目标达成或执行了结束动作) goal_ended: bool = False # 检查最新的目标是否是“结束对话” @@ -913,68 +927,105 @@ class Conversation: check_reason: str = "未进行检查" # 存储检查结果原因 # --- [核心修复] 引入重试循环 --- + is_send_decision_from_rg = False # 标记是否由 reply_generator 决定发送 + while reply_attempt_count < max_reply_attempts and not is_suitable and not need_replan_from_checker: reply_attempt_count += 1 log_prefix = f"[私聊][{self.private_name}] 尝试生成/检查 '{action}' 回复 (第 {reply_attempt_count}/{max_reply_attempts} 次)..." logger.info(log_prefix) - # --- a. 生成回复 --- - self.state = ConversationState.GENERATING # 更新对话状态 + self.state = ConversationState.GENERATING if not self.reply_generator: - # 检查依赖组件是否存在 raise RuntimeError("ReplyGenerator 未初始化") - # 调用 ReplyGenerator 生成回复内容 - generated_content = await self.reply_generator.generate( + + raw_llm_output = await self.reply_generator.generate( observation_info, conversation_info, action_type=action ) - logger.info(f"{log_prefix} 生成内容: '{generated_content}'") # 日志中截断长内容 + logger.debug(f"{log_prefix} ReplyGenerator.generate 返回: '{raw_llm_output}'") - # 检查生成内容是否有效 - if not generated_content or generated_content.startswith("抱歉"): - # 如果生成失败或返回错误提示 - logger.warning(f"{log_prefix} 生成内容为空或为错误提示,将进行下一次尝试。") - check_reason = "生成内容无效" # 记录原因 - # 记录拒绝信息供下次生成参考 + should_send_reply = True # 默认对于 direct_reply 是要发送的 + text_to_process = raw_llm_output # 默认情况下,处理原始输出 + + if action == "send_new_message": + is_send_decision_from_rg = True # 标记 send_new_message 的决策来自RG + try: + # 使用 pfc_utils.py 中的 get_items_from_json 来解析 + # 注意:get_items_from_json 目前主要用于提取固定字段的字典。 + # reply_generator 返回的是一个顶级JSON对象。 + # 我们需要稍微调整用法或增强 get_items_from_json。 + # 简单起见,这里我们先直接用 json.loads,后续可以优化。 + + parsed_json = None + try: + 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: + should_send_reply = False + text_to_process = "no" # 保持和 prompt 中一致,txt 为 "no" + logger.info(f"{log_prefix} ReplyGenerator 决定不发送消息。") + # 此时,我们应该跳出重试循环,并触发 action_planner 的反思 prompt + # 将此信息传递到循环外部进行处理 + break # 跳出 while 循环 + + except Exception as e_json: # 更广泛地捕获解析相关的错误 + logger.error(f"{log_prefix} 解析 ReplyGenerator 的JSON输出时出错: {e_json}, 输出: {raw_llm_output}") + conversation_info.last_reply_rejection_reason = f"解析回复生成器JSON输出错误: {e_json}" + conversation_info.last_rejected_reply_content = raw_llm_output + should_send_reply = False + text_to_process = "no" + + if not should_send_reply and action == "send_new_message": # 如果RG决定不发送 (send_new_message特定逻辑) + 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 (action == "send_new_message" and generated_content_for_check_or_send == "no"): + logger.warning(f"{log_prefix} 生成内容无效或为错误提示 (或send:no),将进行下一次尝试 (如果适用)。") + check_reason = "生成内容无效或选择不发送" conversation_info.last_reply_rejection_reason = check_reason - conversation_info.last_rejected_reply_content = generated_content - await asyncio.sleep(0.5) # 短暂等待后重试 - continue # 进入下一次循环尝试 + conversation_info.last_rejected_reply_content = generated_content_for_check_or_send + if action == "direct_reply": # direct_reply 失败时才继续尝试 + await asyncio.sleep(0.5) + continue + else: # send_new_message 如果是 no,不应该继续尝试,上面已经break了 + pass # 理论上不会到这里如果上面break了 - # --- b. 检查回复 --- - self.state = ConversationState.CHECKING # 更新状态为检查中 + self.state = ConversationState.CHECKING if not self.reply_checker: raise RuntimeError("ReplyChecker 未初始化") - # 准备检查所需的上下文信息 - current_goal_str: str = "" # 当前对话目标字符串 + current_goal_str = "" if conversation_info.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: List[Dict[str, Any]] = getattr(observation_info, "chat_history", []) - chat_history_text_for_check: str = getattr(observation_info, "chat_history_str", "") - # 当前重试次数 (传递给 checker,可能有用) - # retry_count for checker starts from 0 - current_retry_for_checker = reply_attempt_count - 1 - + 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 + current_time_value_for_check = observation_info.current_time_str or "获取时间失败" - - current_time_value_for_check = "获取时间失败" - if observation_info and hasattr(observation_info, 'current_time_str') and observation_info.current_time_str: - current_time_value_for_check = observation_info.current_time_str - - - logger.debug(f"{log_prefix} 调用 ReplyChecker 检查...") - # --- 根据配置决定是否执行检查 --- - if global_config.enable_pfc_reply_checker: # <--- 使用配置项 + if global_config.enable_pfc_reply_checker: logger.debug(f"{log_prefix} 调用 ReplyChecker 检查 (配置已启用)...") is_suitable, check_reason, need_replan_from_checker = await self.reply_checker.check( - reply=generated_content, + 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, @@ -985,67 +1036,96 @@ class Conversation: 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.info(f"[配置关闭] ReplyChecker 已跳过,默认回复为合适。") - # 如果不合适,记录原因并准备下一次尝试(如果还有次数) + logger.info(f"{log_prefix} [配置关闭] ReplyChecker 已跳过,默认回复为合适。") + if not is_suitable: - # 记录拒绝原因和内容,供下次生成时参考 conversation_info.last_reply_rejection_reason = check_reason - conversation_info.last_rejected_reply_content = generated_content - # 如果不需要重规划且还有尝试次数 + conversation_info.last_rejected_reply_content = generated_content_for_check_or_send if not need_replan_from_checker and reply_attempt_count < max_reply_attempts: logger.warning(f"{log_prefix} 回复不合适,原因: {check_reason}。将进行下一次尝试。") - await asyncio.sleep(0.5) # 等待后重试 + await asyncio.sleep(0.5) # 如果需要重规划或达到最大次数,循环会在下次判断时自动结束 + + # --- 循环结束后处理 --- + if action == "send_new_message" and not should_send_reply and is_send_decision_from_rg: + # 这是 reply_generator 决定不发送的情况 + logger.info(f"[私聊][{self.private_name}] 动作 '{action}': ReplyGenerator 决定不发送消息。将调用 ActionPlanner 进行反思。") + final_status = "done_no_reply" # 一个新的状态,表示动作完成但无回复 + final_reason = "回复生成器决定不发送消息" + action_successful = True # 动作本身(决策)是成功的 - # --- 循环结束后,处理最终结果 --- - if is_suitable: - # 如果找到了合适的回复 + # 清除追问状态,因为没有实际发送 + conversation_info.last_successful_reply_action = None + conversation_info.my_message_count = 0 # 重置连续发言计数 + + # !!! 触发 ActionPlanner 使用 PROMPT_REFLECT_AND_ACT !!! + if not self.action_planner: + raise RuntimeError("ActionPlanner 未初始化") + + logger.info(f"[私聊][{self.private_name}] {self.name} 本来想发一条新消息,但是想想还是算了。现在重新规划...") + # 调用 action_planner.plan 并传入 use_reflect_prompt=True + new_action, new_reason = await self.action_planner.plan( + observation_info, + conversation_info, + last_successful_reply_action=None, # 因为没发送,所以没有成功的回复动作 + use_reflect_prompt=True + ) + # 记录这次特殊的“反思”动作 + reflect_action_record = { + "action": f"reflect_after_no_send ({new_action})", # 记录原始意图和新规划 + "plan_reason": f"RG决定不发送后,AP规划: {new_reason}", + "status": "delegated", # 标记为委托给新的规划 + "time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + } + conversation_info.done_action.append(reflect_action_record) + + logger.info(f"[私聊][{self.private_name}] 反思后的新规划动作: {new_action}, 原因: {new_reason}") + # **暂定方案:** + # _handle_action 在这种情况下返回一个特殊标记。 + # 为了不立即修改返回类型,我们暂时在这里记录日志,并在 _plan_and_action_loop 中添加逻辑。 + # _handle_action 会将 action_successful 设置为 True,final_status 为 "done_no_reply"。 + # _plan_and_action_loop 之后会检查这个状态。 + + # (这里的 final_status, final_reason, action_successful 已在上面设置) + + elif is_suitable: # 适用于 direct_reply 或 send_new_message (RG决定发送且检查通过) logger.info(f"[私聊][{self.private_name}] 动作 '{action}': 找到合适的回复,准备发送。") - # 清除上次的拒绝信息 (因为本次成功了) conversation_info.last_reply_rejection_reason = None conversation_info.last_rejected_reply_content = None - - # --- c. 发送回复 --- - self.generated_reply = generated_content # 使用最后一次检查通过的内容 - timestamp_before_sending = time.time() # 记录发送前时间戳 + self.generated_reply = generated_content_for_check_or_send + timestamp_before_sending = time.time() logger.debug( f"[私聊][{self.private_name}] 动作 '{action}': 记录发送前时间戳: {timestamp_before_sending:.2f}" ) - self.state = ConversationState.SENDING # 更新状态为发送中 - # 调用内部发送方法 - send_success = await self._send_reply() - send_end_time = time.time() # 记录发送结束时间 + self.state = ConversationState.SENDING + send_success = await self._send_reply() # _send_reply 内部会更新 my_message_count + send_end_time = time.time() if send_success: - # 如果发送成功 - action_successful = True # 标记动作成功 - # final_status 和 final_reason 会在 finally 中设置 + action_successful = True logger.info(f"[私聊][{self.private_name}] 动作 '{action}': 成功发送回复.") - # 更新空闲计时器 if self.idle_conversation_starter: await self.idle_conversation_starter.update_last_message_time(send_end_time) - # --- d. 清理已处理消息 --- 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 = msg.get("user_info", {}).get("user_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.bot_qq_str - and msg_time < timestamp_before_sending + and sender_id != self.bot_qq_str # 确保是对方的消息 + and msg_time < timestamp_before_sending # 只清理发送前的 ): message_ids_to_clear.add(msg_id) - # 如果有需要清理的消息,调用清理方法 + if message_ids_to_clear: logger.debug( f"[私聊][{self.private_name}] 准备清理 {len(message_ids_to_clear)} 条发送前(他人)消息: {message_ids_to_clear}" @@ -1054,73 +1134,66 @@ class Conversation: else: logger.debug(f"[私聊][{self.private_name}] 没有需要清理的发送前(他人)消息。") - # --- e. 决定下一轮规划类型 --- - # 从 conversation_info 获取【规划期间】收到的【他人】新消息数量 other_new_msg_count_during_planning = getattr( conversation_info, "other_new_messages_during_planning_count", 0 ) - # 规则:如果规划期间收到他人新消息 (0 < count <= 2),则下一轮强制初始回复 - if other_new_msg_count_during_planning > 0: + if other_new_msg_count_during_planning > 0 and action == "direct_reply": logger.info( f"[私聊][{self.private_name}] 因规划期间收到 {other_new_msg_count_during_planning} 条他人新消息,下一轮强制使用【初始回复】逻辑。" ) - conversation_info.last_successful_reply_action = None # 强制初始回复 - conversation_info.my_message_count = 0 # 自身发言数量清零 - else: - # 规则:如果规划期间【没有】收到他人新消息,则允许追问 + conversation_info.last_successful_reply_action = None + # conversation_info.my_message_count = 0 # 不在这里重置,因为刚发了一条 + elif action == "direct_reply" or action == "send_new_message": # 成功发送后 logger.info( - f"[私聊][{self.private_name}] 规划期间无他人新消息,下一轮【允许】使用追问逻辑 (基于 '{action}')。" - ) - conversation_info.last_successful_reply_action = action # 允许追问 - - if conversation_info: # 确保 conversation_info 存在 - conversation_info.current_instance_message_count += 1 - logger.debug(f"[私聊][{self.private_name}] 实例消息计数(机器人发送后)增加到: {conversation_info.current_instance_message_count}") - - if self.relationship_updater: - await self.relationship_updater.update_relationship_incremental( - conversation_info=conversation_info, - observation_info=observation_info, - chat_observer_for_history=self.chat_observer + f"[私聊][{self.private_name}] 成功执行 '{action}', 下一轮【允许】使用追问逻辑。" ) + conversation_info.last_successful_reply_action = action - sent_reply_summary = self.generated_reply[:50] if self.generated_reply else "空回复" - event_for_emotion_update = f"你刚刚发送了消息: '{sent_reply_summary}...'" - if self.emotion_updater: - await self.emotion_updater.update_emotion_based_on_context( - conversation_info=conversation_info, - observation_info=observation_info, - chat_observer_for_history=self.chat_observer, - event_description=event_for_emotion_update - ) - else: - # 如果发送失败 + if conversation_info: + conversation_info.current_instance_message_count += 1 + logger.debug(f"[私聊][{self.private_name}] 实例消息计数(机器人发送后)增加到: {conversation_info.current_instance_message_count}") + + if self.relationship_updater: + await self.relationship_updater.update_relationship_incremental( + conversation_info=conversation_info, + observation_info=observation_info, + chat_observer_for_history=self.chat_observer + ) + + sent_reply_summary = self.generated_reply[:50] if self.generated_reply else "空回复" + event_for_emotion_update = f"你刚刚发送了消息: '{sent_reply_summary}...'" + if self.emotion_updater: + await self.emotion_updater.update_emotion_based_on_context( + conversation_info=conversation_info, + observation_info=observation_info, + chat_observer_for_history=self.chat_observer, + event_description=event_for_emotion_update + ) + else: # 发送失败 logger.error(f"[私聊][{self.private_name}] 动作 '{action}': 发送回复失败。") - final_status = "recall" # 发送失败,标记为 recall + final_status = "recall" final_reason = "发送回复时失败" - # 重置追问状态 conversation_info.last_successful_reply_action = None + conversation_info.my_message_count = 0 # 发送失败,重置计数 elif need_replan_from_checker: - # 如果 Checker 要求重新规划 logger.warning( f"[私聊][{self.private_name}] 动作 '{action}' 因 ReplyChecker 要求而被取消,将重新规划。原因: {check_reason}" ) - final_status = "recall" # 标记为 recall + final_status = "recall" final_reason = f"回复检查要求重新规划: {check_reason}" - # # 重置追问状态 - # conversation_info.last_successful_reply_action = None + conversation_info.last_successful_reply_action = None + # my_message_count 保持不变,因为没有成功发送 - else: - # 达到最大尝试次数仍未找到合适回复 + else: # 达到最大尝试次数仍未找到合适回复 logger.warning( f"[私聊][{self.private_name}] 动作 '{action}': 达到最大尝试次数 ({max_reply_attempts}),未能生成/检查通过合适的回复。最终原因: {check_reason}" ) - final_status = "recall" # 标记为 recall + final_status = "recall" final_reason = f"尝试{max_reply_attempts}次后失败: {check_reason}" - # # 重置追问状态 - # conversation_info.last_successful_reply_action = None + conversation_info.last_successful_reply_action = None + # my_message_count 保持不变 # 2. 处理发送告别语动作 (保持简单,不加重试) elif action == "say_goodbye": diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index 84dcea5f..eb8c619c 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -64,7 +64,7 @@ PROMPT_SEND_NEW_MESSAGE = """ 你正在和{sender_name}在QQ上私聊,**并且刚刚你已经发送了一条或多条消息** 你与对方的关系是:{relationship_text} 你现在的心情是:{current_emotion_text} -现在请根据以下信息再发一条新消息: +现在请根据以下信息判断你是否要继续发一条新消息,当然,如果你决定继续发消息不合适,也可以不发: 当前对话目标:{goals_str} @@ -79,7 +79,9 @@ PROMPT_SEND_NEW_MESSAGE = """ {last_rejection_info} -请根据上述信息,结合聊天记录,继续发一条新消息(例如对之前消息的补充,深入话题,或追问等等)。该消息应该: +{spam_warning_info} + +请根据上述信息,判断你是否要继续发一条新消息(例如对之前消息的补充,深入话题,或追问等等)。如果你觉得要发送,该消息应该: 1. 符合对话目标,以"你"的角度发言(不要自己与自己对话!) 2. 符合你的性格特征和身份细节 3. 通俗易懂,自然流畅,像正常聊天一样,简短(通常20字以内,除非特殊情况) @@ -88,10 +90,14 @@ PROMPT_SEND_NEW_MESSAGE = """ 请注意把握聊天内容,不用太有条理,可以有个性。请分清"你"和对方说的话,不要把"你"说的话当做对方说的话,这是你自己说的话。 这条消息可以自然随意自然一些,就像真人一样,注意把握聊天内容,整体风格可以平和、简短,不要刻意突出自身学科背景,不要说你说过的话,可以简短,多简短都可以,但是避免冗长。 -请你注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出消息内容。 -不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。 +如果你决定继续发消息不合适,也可以不发送。 -请直接输出回复内容,不需要任何额外格式。""" +请严格按照以下JSON格式输出你的选择和消息内容,不要包含任何其他说明或非JSON文本: +{{ + "send": "yes/no", + "txt": "如果选择发送,这里是具体的消息文本。如果选择不发送,这里也填写 'no'。" +}} +""" # Prompt for say_goodbye (告别语生成) PROMPT_FAREWELL = """ @@ -125,7 +131,7 @@ class ReplyGenerator: self.llm = LLMRequest( model=global_config.llm_PFC_chat, temperature=global_config.llm_PFC_chat["temp"], - max_tokens=300, + max_tokens=300, # 对于JSON输出,这个可能需要适当调整,但一般回复短,JSON结构也简单 request_type="reply_generation", ) self.personality_info = Individuality.get_instance().get_prompt(x_person=2, level=3) @@ -143,20 +149,18 @@ class ReplyGenerator: Args: observation_info: 观察信息 conversation_info: 对话信息 - action_type: 当前执行的动作类型 ('direct_reply' 或 'send_new_message') + action_type: 当前执行的动作类型 ('direct_reply', 'send_new_message', 'say_goodbye') Returns: - str: 生成的回复 + str: 生成的回复。 + 对于 'direct_reply' 和 'say_goodbye',返回纯文本回复。 + 对于 'send_new_message',返回包含决策和文本的JSON字符串。 """ - # 构建提示词 logger.debug( f"[私聊][{self.private_name}]开始生成回复 (动作类型: {action_type}):当前目标: {conversation_info.goal_list}" ) # --- 构建通用 Prompt 参数 --- - # (这部分逻辑基本不变) - - # 构建对话目标 (goals_str) goals_str = "" if conversation_info.goal_list: for goal_reason in conversation_info.goal_list: @@ -171,9 +175,8 @@ class ReplyGenerator: reasoning = str(reasoning) if reasoning is not None else "没有明确原因" goals_str += f"- 目标:{goal}\n 原因:{reasoning}\n" else: - goals_str = "- 目前没有明确对话目标\n" # 简化无目标情况 + goals_str = "- 目前没有明确对话目标\n" - # 获取聊天历史记录 (chat_history_text) chat_history_text = observation_info.chat_history_str if observation_info.new_messages_count > 0 and observation_info.unprocessed_messages: new_messages_list = observation_info.unprocessed_messages @@ -192,17 +195,14 @@ class ReplyGenerator: f"\n--- 以上均为已读消息,未读消息均已处理完毕 ---\n" ) - # 获取 sender_name, relationship_text, current_emotion_text sender_name_str = getattr(observation_info, 'sender_name', '对方') if not sender_name_str: sender_name_str = '对方' relationship_text_str = getattr(conversation_info, 'relationship_text', '你们还不熟悉。') current_emotion_text_str = getattr(conversation_info, 'current_emotion_text', '心情平静。') - # 构建 Persona 文本 (persona_text) persona_text = f"你的名字是{self.name},{self.personality_info}。" - retrieval_context = chat_history_text # 使用前面构建好的 chat_history_text - # 调用共享函数进行检索 + retrieval_context = chat_history_text retrieved_memory_str, retrieved_knowledge_str = await retrieve_contextual_info( retrieval_context, self.private_name ) @@ -210,9 +210,7 @@ class ReplyGenerator: f"[私聊][{self.private_name}] (ReplyGenerator) 统一检索完成。记忆: {'有' if '回忆起' in retrieved_memory_str else '无'} / 知识: {'有' if '出错' not in retrieved_knowledge_str and '无相关知识' not in retrieved_knowledge_str else '无'}" ) - # --- 修改:构建上次回复失败原因和内容提示 --- last_rejection_info_str = "" - # 检查 conversation_info 是否有上次拒绝的原因和内容,并且它们都不是 None last_reason = getattr(conversation_info, "last_reply_rejection_reason", None) last_content = getattr(conversation_info, "last_rejected_reply_content", None) @@ -220,7 +218,7 @@ class ReplyGenerator: last_rejection_info_str = ( f"\n------\n" f"【重要提示:你上一次尝试回复时失败了,以下是详细信息】\n" - f"上次试图发送的消息内容: “{last_content}”\n" # <-- 显示上次内容 + f"上次试图发送的消息内容: “{last_content}”\n" f"失败原因: “{last_reason}”\n" f"请根据【消息内容】和【失败原因】调整你的新回复,避免重复之前的错误。\n" f"------\n" @@ -230,42 +228,67 @@ class ReplyGenerator: f" 内容: {last_content}\n" f" 原因: {last_reason}" ) + + # 新增:构建刷屏警告信息 for PROMPT_SEND_NEW_MESSAGE + spam_warning_message = "" + if action_type == "send_new_message": # 只在 send_new_message 时构建刷屏警告 + if conversation_info.my_message_count > 5: + spam_warning_message = f"⚠️【警告】**你已连续发送{str(conversation_info.my_message_count)}条消息!请谨慎考虑是否继续发送!以免刷屏对造成对方困扰!**" + elif conversation_info.my_message_count > 2: + spam_warning_message = f"💬【提示】**你已连续发送{str(conversation_info.my_message_count)}条消息。如果非必要,请避免连续发送,以免给对方造成困扰。**" + if spam_warning_message: + spam_warning_message = f"\n{spam_warning_message}\n" + # --- 选择 Prompt --- if action_type == "send_new_message": prompt_template = PROMPT_SEND_NEW_MESSAGE - logger.info(f"[私聊][{self.private_name}]使用 PROMPT_SEND_NEW_MESSAGE (追问生成)") - elif action_type == "say_goodbye": # 处理告别动作 + logger.info(f"[私聊][{self.private_name}]使用 PROMPT_SEND_NEW_MESSAGE (追问/补充生成, 期望JSON输出)") + elif action_type == "say_goodbye": prompt_template = PROMPT_FAREWELL logger.info(f"[私聊][{self.private_name}]使用 PROMPT_FAREWELL (告别语生成)") - else: # 默认使用 direct_reply 的 prompt (包括 'direct_reply' 或其他未明确处理的类型) + else: prompt_template = PROMPT_DIRECT_REPLY logger.info(f"[私聊][{self.private_name}]使用 PROMPT_DIRECT_REPLY (首次/非连续回复生成)") # --- 格式化最终的 Prompt --- - try: # <--- 增加 try-except 块处理可能的 format 错误 - current_time_value = "获取时间失败" # 默认值 + try: + current_time_value = "获取时间失败" if observation_info and hasattr(observation_info, 'current_time_str') and observation_info.current_time_str: current_time_value = observation_info.current_time_str + if action_type == "say_goodbye": prompt = prompt_template.format( persona_text=persona_text, chat_history_text=chat_history_text, - current_time_str=current_time_value, # 添加时间 + current_time_str=current_time_value, sender_name=sender_name_str, relationship_text=relationship_text_str, current_emotion_text=current_emotion_text_str ) - - else: + elif action_type == "send_new_message": # PROMPT_SEND_NEW_MESSAGE 增加了 spam_warning_info prompt = prompt_template.format( persona_text=persona_text, goals_str=goals_str, chat_history_text=chat_history_text, retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。", retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。", - last_rejection_info=last_rejection_info_str, # <--- 新增传递上次拒绝原因 - current_time_str=current_time_value, # 新增:传入当前时间字符串 + last_rejection_info=last_rejection_info_str, + current_time_str=current_time_value, + sender_name=sender_name_str, + relationship_text=relationship_text_str, + current_emotion_text=current_emotion_text_str, + spam_warning_info=spam_warning_message # 添加 spam_warning_info + ) + else: # PROMPT_DIRECT_REPLY (没有 spam_warning_info) + prompt = prompt_template.format( + persona_text=persona_text, + goals_str=goals_str, + chat_history_text=chat_history_text, + retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。", + retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。", + last_rejection_info=last_rejection_info_str, + current_time_str=current_time_value, sender_name=sender_name_str, relationship_text=relationship_text_str, current_emotion_text=current_emotion_text_str @@ -274,8 +297,7 @@ class ReplyGenerator: logger.error( f"[私聊][{self.private_name}]格式化 Prompt 时出错,缺少键: {e}。请检查 Prompt 模板和传递的参数。" ) - # 返回错误信息或默认回复 - return "抱歉,准备回复时出了点问题,请检查一下我的代码..." + return "抱歉,准备回复时出了点问题,请检查一下我的代码..." # 对于JSON期望的场景,这里可能也需要返回一个固定的错误JSON except Exception as fmt_err: logger.error(f"[私聊][{self.private_name}]格式化 Prompt 时发生未知错误: {fmt_err}") return "抱歉,准备回复时出了点内部错误,请检查一下我的代码..." @@ -284,19 +306,30 @@ class ReplyGenerator: logger.debug(f"[私聊][{self.private_name}]发送到LLM的生成提示词:\n------\n{prompt}\n------") try: content, _ = await self.llm.generate_response_async(prompt) - logger.debug(f"[私聊][{self.private_name}]生成的回复: {content}") - # 移除旧的检查新消息逻辑,这应该由 conversation 控制流处理 + # 对于 PROMPT_SEND_NEW_MESSAGE,我们期望 content 是一个 JSON 字符串 + # 对于其他 prompts,content 是纯文本回复 + # 该方法现在直接返回 LLM 的原始输出,由调用者 (conversation._handle_action) 负责解析 + logger.debug(f"[私聊][{self.private_name}]LLM原始生成内容: {content}") return content except Exception as e: logger.error(f"[私聊][{self.private_name}]生成回复时出错: {e}") - return "抱歉,我现在有点混乱,让我重新思考一下..." + # 根据 action_type 返回不同的错误指示 + if action_type == "send_new_message": + # 返回一个表示错误的JSON,让调用方知道出错了但仍能解析 + return """{{ + "send": "no", + "txt": "LLM生成回复时出错" + }}""".strip() + else: + return "抱歉,我现在有点混乱,让我重新思考一下..." # check_reply 方法保持不变 async def check_reply( - self, reply: str, goal: str, chat_history: List[Dict[str, Any]], chat_history_str: str, retry_count: int = 0 + self, reply: str, goal: str, chat_history: List[Dict[str, Any]], chat_history_str: str, current_time_str: str, retry_count: int = 0 ) -> Tuple[bool, str, bool]: """检查回复是否合适 - (此方法逻辑保持不变) + (此方法逻辑保持不变, 但注意 current_time_str 参数的传递) """ - return await self.reply_checker.check(reply, goal, chat_history, chat_history_str, retry_count) + # 确保 current_time_str 被正确传递给 reply_checker.check + return await self.reply_checker.check(reply, goal, chat_history, chat_history_str, current_time_str, retry_count) \ No newline at end of file From 5ebab2f3f6dc51628bd21bf895511f369885ce03 Mon Sep 17 00:00:00 2001 From: KeepingRunning <1599949878@qq.com> Date: Wed, 7 May 2025 08:18:05 +0800 Subject: [PATCH 23/63] =?UTF-8?q?fix:=20=E5=B0=86=E5=B7=A6=E5=8D=8A?= =?UTF-8?q?=E8=A7=92=E6=8B=AC=E5=8F=B7=E6=94=B9=E4=B8=BA=E5=85=A8=E8=A7=92?= =?UTF-8?q?=E6=8B=AC=E5=8F=B7=EF=BC=8C=E4=BF=9D=E6=8C=81=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E5=B7=A6=E5=8F=B3=E6=8B=AC=E5=8F=B7=E5=8C=B9=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/bot_config_template.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 52280783..8eab299c 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -68,7 +68,7 @@ nonebot-qq="http://127.0.0.1:18002/api/message" [chat] #麦麦的聊天通用设置 allow_focus_mode = true # 是否允许专注聊天状态 # 是否启用heart_flowC(HFC)模式 -# 启用后麦麦会自主选择进入heart_flowC模式(持续一段时间),进行主动的观察和回复,并给出回复,比较消耗token +# 启用后麦麦会自主选择进入heart_flowC模式(持续一段时间),进行主动的观察和回复,并给出回复,比较消耗token base_normal_chat_num = 8 # 最多允许多少个群进行普通聊天 base_focused_chat_num = 5 # 最多允许多少个群进行专注聊天 From aff38b4fcac4fca69293cd175916f4c69997ccde Mon Sep 17 00:00:00 2001 From: Bakadax Date: Wed, 7 May 2025 12:04:03 +0800 Subject: [PATCH 24/63] =?UTF-8?q?=E5=B0=81=E8=A3=85=20PFC=20=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/conversation.py | 2 +- src/plugins/PFC/pfc_processor.py | 125 +++++++++++++++++++++++ src/plugins/chat/bot.py | 33 +----- src/plugins/chat/only_message_process.py | 67 ------------ 4 files changed, 129 insertions(+), 98 deletions(-) create mode 100644 src/plugins/PFC/pfc_processor.py delete mode 100644 src/plugins/chat/only_message_process.py diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 4d2291c3..ceb4cfc4 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -774,7 +774,7 @@ class Conversation: self.conversation_info.done_action[-1] if self.conversation_info.done_action else {} # type: ignore ) if last_action_record.get("action") == "send_new_message" and \ - last_action_record.get("status") == "done_no_reply": + last_action_record.get("status") == "done_no_reply": logger.info(f"[私聊][{self.private_name}] 检测到 ReplyGenerator 决定不发送消息,将在下一轮强制使用反思Prompt。") force_reflect_and_act = True # 设置标志,下一轮使用反思prompt # 不需要立即 continue,让循环自然进入下一轮,下一轮的 plan 会用这个标志 diff --git a/src/plugins/PFC/pfc_processor.py b/src/plugins/PFC/pfc_processor.py new file mode 100644 index 00000000..523e2261 --- /dev/null +++ b/src/plugins/PFC/pfc_processor.py @@ -0,0 +1,125 @@ +import traceback + +from maim_message import UserInfo +from src.config.config import global_config +from src.common.logger_manager import get_logger +from ..chat.chat_stream import chat_manager +from typing import Optional, Dict, Any +from .pfc_manager import PFCManager +from src.plugins.chat.message import MessageRecv +from src.plugins.storage.storage import MessageStorage +from datetime import datetime + + +logger = get_logger("pfc_processor") + +async def _handle_error(error: Exception, context: str, message: Optional[MessageRecv] = None) -> None: + """统一的错误处理函数 + + Args: + error: 捕获到的异常 + context: 错误发生的上下文描述 + message: 可选的消息对象,用于记录相关消息内容 + """ + logger.error(f"{context}: {error}") + logger.error(traceback.format_exc()) + if message and hasattr(message, "raw_message"): + logger.error(f"相关消息原始内容: {message.raw_message}") + +class PFCProcessor: + """ PFC 处理器,负责处理接收到的信息并计数""" + + def __init__(self): + """初始化 PFC 处理器,创建消息存储实例""" + self.storage = MessageStorage() + self.pfc_manager = PFCManager.get_instance() + + async def process_message(self, message_data: Dict[str, Any]) -> None: + """处理接收到的原始消息数据 + + 主要流程: + 1. 消息解析与初始化 + 2. 过滤检查 + 3. 消息存储 + 4. 创建 PFC 流 + 5. 日志记录 + + Args: + message_data: 原始消息字符串 + """ + message = None + try: + # 1. 消息解析与初始化 + message = MessageRecv(message_data) + groupinfo = message.message_info.group_info + userinfo = message.message_info.user_info + messageinfo = message.message_info + + logger.trace(f"准备为{userinfo.user_id}创建/获取聊天流") + chat = await chat_manager.get_or_create_stream( + platform=messageinfo.platform, + user_info=userinfo, + group_info=groupinfo, + ) + message.update_chat_stream(chat) + + # 2. 过滤检查 + # 处理消息 + await message.process() + # 过滤词/正则表达式过滤 + if self._check_ban_words(message.processed_plain_text, userinfo) or self._check_ban_regex( + message.raw_message, userinfo + ): + return + + # 3. 消息存储 + await self.storage.store_message(message, chat) + logger.trace(f"存储成功: {message.processed_plain_text}") + + # 4. 创建 PFC 聊天流 + await self._create_pfc_chat(message) + + # 5. 日志记录 + # 将时间戳转换为datetime对象 + current_time = datetime.fromtimestamp(message.message_info.time).strftime("%H:%M:%S") + logger.info( + f"[{current_time}][私聊]{message.message_info.user_info.user_nickname}: {message.processed_plain_text}" + ) + + except Exception as e: + await _handle_error(e, "消息处理失败", message) + + async def _create_pfc_chat(self, message: MessageRecv): + try: + chat_id = str(message.chat_stream.stream_id) + private_name = str(message.message_info.user_info.user_nickname) + + if global_config.enable_pfc_chatting: + await self.pfc_manager.get_or_create_conversation(chat_id, private_name) + + except Exception as e: + logger.error(f"创建PFC聊天失败: {e}") + + @staticmethod + def _check_ban_words(text: str, userinfo: UserInfo) -> bool: + """检查消息中是否包含过滤词""" + for word in global_config.ban_words: + if word in text: + logger.info( + f"[私聊]{userinfo.user_nickname}:{text}" + ) + logger.info(f"[过滤词识别]消息中含有{word},filtered") + return True + return False + + @staticmethod + def _check_ban_regex(text: str, userinfo: UserInfo) -> bool: + """检查消息是否匹配过滤正则表达式""" + for pattern in global_config.ban_msgs_regex: + if pattern.search(text): + logger.info( + f"[私聊]{userinfo.user_nickname}:{text}" + ) + logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered") + return True + return False \ No newline at end of file diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 9c4a3358..ed22ef2b 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -3,12 +3,10 @@ from typing import Dict, Any from ..moods.moods import MoodManager # 导入情绪管理器 from ...config.config import global_config from .message import MessageRecv -from ..PFC.pfc_manager import PFCManager -from .chat_stream import chat_manager -from .only_message_process import MessageProcessor from src.common.logger_manager import get_logger from ..heartFC_chat.heartflow_processor import HeartFCProcessor +from ..PFC.pfc_processor import PFCProcessor from ..utils.prompt_builder import Prompt, global_prompt_manager import traceback @@ -25,10 +23,7 @@ class ChatBot: self._started = False self.mood_manager = MoodManager.get_instance() # 获取情绪管理器单例 self.heartflow_processor = HeartFCProcessor() # 新增 - - # 创建初始化PFC管理器的任务,会在_ensure_started时执行 - self.only_process_chat = MessageProcessor() - self.pfc_manager = PFCManager.get_instance() + self.pfc_processor = PFCProcessor() async def _ensure_started(self): """确保所有任务已启动""" @@ -37,17 +32,6 @@ class ChatBot: self._started = True - async def _create_pfc_chat(self, message: MessageRecv): - try: - chat_id = str(message.chat_stream.stream_id) - private_name = str(message.message_info.user_info.user_nickname) - - if global_config.enable_pfc_chatting: - await self.pfc_manager.get_or_create_conversation(chat_id, private_name) - - except Exception as e: - logger.error(f"创建PFC聊天失败: {e}") - async def message_process(self, message_data: Dict[str, Any]) -> None: """处理转化后的统一格式消息 这个函数本质是预处理一些数据,根据配置信息和消息内容,预处理消息,并分发到合适的消息处理器中 @@ -118,18 +102,7 @@ class ChatBot: # 是否进入PFC if global_config.enable_pfc_chatting: logger.trace("进入PFC私聊处理流程") - userinfo = message.message_info.user_info - messageinfo = message.message_info - # 创建聊天流 - logger.trace(f"为{userinfo.user_id}创建/获取聊天流") - chat = await chat_manager.get_or_create_stream( - platform=messageinfo.platform, - user_info=userinfo, - group_info=groupinfo, - ) - message.update_chat_stream(chat) - await self.only_process_chat.process_message(message) - await self._create_pfc_chat(message) + await self.pfc_processor.process_message(message_data) # 禁止PFC,进入普通的心流消息处理逻辑 else: logger.trace("进入普通心流私聊处理") diff --git a/src/plugins/chat/only_message_process.py b/src/plugins/chat/only_message_process.py deleted file mode 100644 index b1bb0cea..00000000 --- a/src/plugins/chat/only_message_process.py +++ /dev/null @@ -1,67 +0,0 @@ -from src.common.logger_manager import get_logger -from src.plugins.chat.message import MessageRecv -from src.plugins.storage.storage import MessageStorage -from src.config.config import global_config -from datetime import datetime - -logger = get_logger("pfc") - - -class MessageProcessor: - """消息处理器,负责处理接收到的消息并存储""" - - def __init__(self): - self.storage = MessageStorage() - - @staticmethod - def _check_ban_words(text: str, chat, userinfo) -> bool: - """检查消息中是否包含过滤词""" - for word in global_config.ban_words: - if word in text: - logger.info( - f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{text}" - ) - logger.info(f"[过滤词识别]消息中含有{word},filtered") - return True - return False - - @staticmethod - def _check_ban_regex(text: str, chat, userinfo) -> bool: - """检查消息是否匹配过滤正则表达式""" - for pattern in global_config.ban_msgs_regex: - if pattern.search(text): - logger.info( - f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{text}" - ) - logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered") - return True - return False - - async def process_message(self, message: MessageRecv) -> None: - """处理消息并存储 - - Args: - message: 消息对象 - """ - userinfo = message.message_info.user_info - chat = message.chat_stream - - # 处理消息 - await message.process() - - # 过滤词/正则表达式过滤 - if self._check_ban_words(message.processed_plain_text, chat, userinfo) or self._check_ban_regex( - message.raw_message, chat, userinfo - ): - return - - # 存储消息 - await self.storage.store_message(message, chat) - - # 打印消息信息 - mes_name = chat.group_info.group_name if chat.group_info else "私聊" - # 将时间戳转换为datetime对象 - current_time = datetime.fromtimestamp(message.message_info.time).strftime("%H:%M:%S") - logger.info( - f"[{current_time}][{mes_name}]{message.message_info.user_info.user_nickname}: {message.processed_plain_text}" - ) From 7316b30aa03ad7c902416e9b6537d00d31bac8c9 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Wed, 7 May 2025 14:55:10 +0800 Subject: [PATCH 25/63] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=BF=BD=E9=97=AE?= =?UTF-8?q?=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/conversation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index ceb4cfc4..37ad7e0b 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -1065,7 +1065,7 @@ class Conversation: if not self.action_planner: raise RuntimeError("ActionPlanner 未初始化") - logger.info(f"[私聊][{self.private_name}] {self.name} 本来想发一条新消息,但是想想还是算了。现在重新规划...") + logger.info(f"[私聊][{self.private_name}] {global_config.BOT_NICKNAME}本来想发一条新消息,但是想想还是算了。现在重新规划...") # 调用 action_planner.plan 并传入 use_reflect_prompt=True new_action, new_reason = await self.action_planner.plan( observation_info, From 32ace682d344c5dc14867d6000fd0104d92e7441 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Wed, 7 May 2025 15:20:02 +0800 Subject: [PATCH 26/63] =?UTF-8?q?pfc=5Frelationship=20=E5=90=88=E5=B9=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/conversation.py | 24 ++--- ...ionship_updater.py => pfc_relationship.py} | 87 +++++++++++++++---- .../PFC/pfc_relationship_translator.py | 60 ------------- 3 files changed, 77 insertions(+), 94 deletions(-) rename src/plugins/PFC/{pfc_relationship_updater.py => pfc_relationship.py} (80%) delete mode 100644 src/plugins/PFC/pfc_relationship_translator.py diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 37ad7e0b..4d45f053 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -7,33 +7,18 @@ from .pfc_utils import get_items_from_json # 导入JSON解析工具 from typing import Dict, Any, Optional, Set, List from dateutil import tz -# 导入日志记录器 from src.common.logger_manager import get_logger - -# 导入聊天消息构建和获取工具 from src.plugins.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat - -# 导入消息相关的数据结构 from maim_message import UserInfo - -# 导入聊天流管理器和聊天流类 from src.plugins.chat.chat_stream import chat_manager, ChatStream - -# 导入聊天消息类 from ..chat.message import Message # 假设 Message 类在这里 - -# 导入全局配置 from ...config.config import global_config - -# 导入用户信息 from ..person_info.person_info import person_info_manager from ..person_info.relationship_manager import relationship_manager -# 导入关系 -from .pfc_relationship_translator import translate_relationship_value_to_text # 导入情绪 from ..moods.moods import MoodManager -from .pfc_relationship_updater import PfcRelationshipUpdater # 新增 +from .pfc_relationship import PfcRelationshipUpdater, PfcRepationshipTranslator from .pfc_emotion_updater import PfcEmotionUpdater # 新增 # 导入 PFC 内部组件和类型 @@ -101,6 +86,7 @@ class Conversation: # 初始化所有核心组件为 None,将在 _initialize 中创建 self.relationship_updater: Optional[PfcRelationshipUpdater] = None # 新增 + self.relationshop_translator: Optional[PfcRepationshipTranslator] = None self.emotion_updater: Optional[PfcEmotionUpdater] = None # 新增 self.action_planner: Optional[ActionPlanner] = None self.goal_analyzer: Optional[GoalAnalyzer] = None @@ -301,7 +287,7 @@ class Conversation: numeric_relationship_value = 0.0 # 2. 使用PFC内部翻译函数 - self.conversation_info.relationship_text = translate_relationship_value_to_text(numeric_relationship_value) + self.conversation_info.relationship_text = self.relationshop_translator.translate_relationship_value_to_text(numeric_relationship_value) logger.info(f"[私聊][{self.private_name}] 初始化时加载关系文本: {self.conversation_info.relationship_text}") except Exception as e_init_rel: logger.error(f"[私聊][{self.private_name}] 初始化时加载关系文本出错: {e_init_rel}") @@ -586,7 +572,7 @@ class Conversation: logger.debug(f"[私聊][{self.private_name}] 获取到数值型关系值: {numeric_relationship_value}") # 2. 使用PFC内部的翻译函数将其转换为文本描述 - simplified_relationship_text = translate_relationship_value_to_text(numeric_relationship_value) + simplified_relationship_text = self.relationshop_translator.translate_relationship_value_to_text(numeric_relationship_value) self.conversation_info.relationship_text = simplified_relationship_text logger.debug(f"[私聊][{self.private_name}] 更新后关系文本 (PFC内部翻译): {self.conversation_info.relationship_text}") @@ -618,7 +604,7 @@ class Conversation: numeric_relationship_value = float(numeric_relationship_value.to_decimal()) else: numeric_relationship_value = 0.0 - self.conversation_info.relationship_text = translate_relationship_value_to_text(numeric_relationship_value) + self.conversation_info.relationship_text = self.relationshop_translator.translate_relationship_value_to_text(numeric_relationship_value) logger.debug(f"[私聊][{self.private_name}] (备用逻辑)更新后关系文本: {self.conversation_info.relationship_text}") except ValueError: diff --git a/src/plugins/PFC/pfc_relationship_updater.py b/src/plugins/PFC/pfc_relationship.py similarity index 80% rename from src/plugins/PFC/pfc_relationship_updater.py rename to src/plugins/PFC/pfc_relationship.py index a7ff5637..efe8a6cc 100644 --- a/src/plugins/PFC/pfc_relationship_updater.py +++ b/src/plugins/PFC/pfc_relationship.py @@ -4,22 +4,13 @@ from src.plugins.models.utils_model import LLMRequest from src.plugins.person_info.person_info import person_info_manager from src.plugins.person_info.relationship_manager import relationship_manager # 主要用其 ensure_float 和 build_relationship_info from src.plugins.utils.chat_message_builder import build_readable_messages - -# 从PFC内部导入必要的类 (根据你的项目结构调整,当前假设与PFC核心文件同级或有正确路径) -try: - from .observation_info import ObservationInfo - from .conversation_info import ConversationInfo - from .pfc_utils import get_items_from_json -except ImportError: # 如果直接从 plugins/PFC 目录运行,上面的相对导入会失败,尝试绝对导入 - from observation_info import ObservationInfo - from conversation_info import ConversationInfo - from pfc_utils import get_items_from_json +from src.plugins.PFC.observation_info import ObservationInfo +from src.plugins.PFC.conversation_info import ConversationInfo +from src.plugins.PFC.pfc_utils import get_items_from_json +from src.config.config import global_config # 导入全局配置 (向上两级到 src/, 再到 config) -from ...config.config import global_config # 导入全局配置 (向上两级到 src/, 再到 config) - - -logger = get_logger("pfc_relationship_updater") +logger = get_logger("pfc_relationship") class PfcRelationshipUpdater: def __init__(self, private_name: str, bot_name: str): @@ -216,4 +207,70 @@ class PfcRelationshipUpdater: logger.info(f"[私聊][{self.private_name}] 最终关系值更新:与【{sender_name_for_prompt}】的关系值从 {current_relationship_value:.2f} 调整了 {adjustment_val:.2f},最终为 {new_relationship_value:.2f}") if conversation_info.person_id: # 虽然通常结束了,但更新一下无妨 - conversation_info.relationship_text = await self.relationship_mng.build_relationship_info(conversation_info.person_id, is_id=True) \ No newline at end of file + conversation_info.relationship_text = await self.relationship_mng.build_relationship_info(conversation_info.person_id, is_id=True) + + +class PfcRepationshipTranslator: + """直接完整导入群聊的relationship_manager.py可能不可取 + 因为对于PFC的planner来说 + 其暗示了选择回复 + 所以新建代码文件来适配PFC的决策层面""" + def __init__(self): + pass + + @staticmethod + def translate_relationship_value_to_text(self, relationship_value: float) -> str: + """ + 将数值型的关系值转换为PFC私聊场景下简洁的关系描述文本。 + """ + level_num = self._calculate_relationship_level_num(relationship_value) + + relationship_descriptions = [ + "厌恶", # level_num 0 + "冷漠", # level_num 1 + "初识", # level_num 2 + "友好", # level_num 3 + "喜欢", # level_num 4 + "暧昧" # level_num 5 + ] + + if 0 <= level_num < len(relationship_descriptions): + description = relationship_descriptions[level_num] + else: + description = "普通" # 默认或错误情况 + logger.warning(f"计算出的 level_num ({level_num}) 无效,关系描述默认为 '普通'") + + return f"你们的关系是:{description}。" + + @staticmethod + def _calculate_relationship_level_num(relationship_value: float) -> int: + """ + 根据关系值计算关系等级编号 (0-5)。 + 这里的阈值应与 relationship_manager.py 中的保持一致 + """ + if not isinstance(relationship_value, (int, float)): + logger.warning(f"传入的 relationship_value '{relationship_value}' 不是有效的数值类型,默认为0。") + relationship_value = 0.0 + + if -1000 <= relationship_value < -227: + level_num = 0 # 厌恶 + elif -227 <= relationship_value < -73: + level_num = 1 # 冷漠 + elif -73 <= relationship_value < 227: + level_num = 2 # 普通/认识 + elif 227 <= relationship_value < 587: + level_num = 3 # 友好 + elif 587 <= relationship_value < 900: + level_num = 4 # 喜欢 + elif 900 <= relationship_value <= 1000: + level_num = 5 # 暧昧 + else: + # 超出范围的值处理 + if relationship_value > 1000: + level_num = 5 + elif relationship_value < -1000: + level_num = 0 + else: # 理论上不会到这里,除非前面的条件逻辑有误 + logger.warning(f"关系值 {relationship_value} 未落入任何预设范围,默认为普通。") + level_num = 2 + return level_num \ No newline at end of file diff --git a/src/plugins/PFC/pfc_relationship_translator.py b/src/plugins/PFC/pfc_relationship_translator.py deleted file mode 100644 index 09cc3ab0..00000000 --- a/src/plugins/PFC/pfc_relationship_translator.py +++ /dev/null @@ -1,60 +0,0 @@ -# 直接完整导入群聊的relationship_manager.py可能不可取,因为对于PFC的planner来说,其暗示了选择回复,所以新建代码文件来适配PFC的决策层面 -from src.common.logger_manager import get_logger - -logger = get_logger("pfc_relationship_translator") - -# 这个函数复用 relationship_manager.py 中的 calculate_level_num 的逻辑 -def _calculate_relationship_level_num(relationship_value: float) -> int: - """ - 根据关系值计算关系等级编号 (0-5)。 - 这里的阈值应与 relationship_manager.py 中的保持一致 - """ - if not isinstance(relationship_value, (int, float)): - logger.warning(f"传入的 relationship_value '{relationship_value}' 不是有效的数值类型,默认为0。") - relationship_value = 0.0 - - if -1000 <= relationship_value < -227: - level_num = 0 # 厌恶 - elif -227 <= relationship_value < -73: - level_num = 1 # 冷漠 - elif -73 <= relationship_value < 227: - level_num = 2 # 普通/认识 - elif 227 <= relationship_value < 587: - level_num = 3 # 友好 - elif 587 <= relationship_value < 900: - level_num = 4 # 喜欢 - elif 900 <= relationship_value <= 1000: - level_num = 5 # 暧昧 - else: - # 超出范围的值处理 - if relationship_value > 1000: - level_num = 5 - elif relationship_value < -1000: - level_num = 0 - else: # 理论上不会到这里,除非前面的条件逻辑有误 - logger.warning(f"关系值 {relationship_value} 未落入任何预设范围,默认为普通。") - level_num = 2 - return level_num - -def translate_relationship_value_to_text(relationship_value: float) -> str: - """ - 将数值型的关系值转换为PFC私聊场景下简洁的关系描述文本。 - """ - level_num = _calculate_relationship_level_num(relationship_value) - - relationship_descriptions = [ - "厌恶", # level_num 0 - "冷漠", # level_num 1 - "初识", # level_num 2 - "友好", # level_num 3 - "喜欢", # level_num 4 - "暧昧" # level_num 5 - ] - - if 0 <= level_num < len(relationship_descriptions): - description = relationship_descriptions[level_num] - else: - description = "普通" # 默认或错误情况 - logger.warning(f"计算出的 level_num ({level_num}) 无效,关系描述默认为 '普通'") - - return f"你们的关系是:{description}。" \ No newline at end of file From 1905e02587d88da160bef59800d60f5ab791a12d Mon Sep 17 00:00:00 2001 From: Bakadax Date: Wed, 7 May 2025 15:48:33 +0800 Subject: [PATCH 27/63] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20pfc=20repationship?= =?UTF-8?q?=20=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/conversation.py | 11 ++++++----- src/plugins/PFC/pfc_relationship.py | 19 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 4d45f053..bb5477cc 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -86,7 +86,7 @@ class Conversation: # 初始化所有核心组件为 None,将在 _initialize 中创建 self.relationship_updater: Optional[PfcRelationshipUpdater] = None # 新增 - self.relationshop_translator: Optional[PfcRepationshipTranslator] = None + self.relationship_translator: Optional[PfcRepationshipTranslator] = None self.emotion_updater: Optional[PfcEmotionUpdater] = None # 新增 self.action_planner: Optional[ActionPlanner] = None self.goal_analyzer: Optional[GoalAnalyzer] = None @@ -132,7 +132,8 @@ class Conversation: private_name=self.private_name, bot_name=global_config.BOT_NICKNAME # 或者 self.name (如果 Conversation 类有 self.name) ) - logger.info(f"[私聊][{self.private_name}] PfcRelationshipUpdater 初始化完成。") + self.relationship_translator = PfcRepationshipTranslator(private_name=self.private_name) + logger.info(f"[私聊][{self.private_name}] PfcRelationship 初始化完成。") self.emotion_updater = PfcEmotionUpdater( private_name=self.private_name, @@ -287,7 +288,7 @@ class Conversation: numeric_relationship_value = 0.0 # 2. 使用PFC内部翻译函数 - self.conversation_info.relationship_text = self.relationshop_translator.translate_relationship_value_to_text(numeric_relationship_value) + self.conversation_info.relationship_text = self.relationship_translator.translate_relationship_value_to_text(numeric_relationship_value) logger.info(f"[私聊][{self.private_name}] 初始化时加载关系文本: {self.conversation_info.relationship_text}") except Exception as e_init_rel: logger.error(f"[私聊][{self.private_name}] 初始化时加载关系文本出错: {e_init_rel}") @@ -572,7 +573,7 @@ class Conversation: logger.debug(f"[私聊][{self.private_name}] 获取到数值型关系值: {numeric_relationship_value}") # 2. 使用PFC内部的翻译函数将其转换为文本描述 - simplified_relationship_text = self.relationshop_translator.translate_relationship_value_to_text(numeric_relationship_value) + simplified_relationship_text = self.relationship_translator.translate_relationship_value_to_text(numeric_relationship_value) self.conversation_info.relationship_text = simplified_relationship_text logger.debug(f"[私聊][{self.private_name}] 更新后关系文本 (PFC内部翻译): {self.conversation_info.relationship_text}") @@ -604,7 +605,7 @@ class Conversation: numeric_relationship_value = float(numeric_relationship_value.to_decimal()) else: numeric_relationship_value = 0.0 - self.conversation_info.relationship_text = self.relationshop_translator.translate_relationship_value_to_text(numeric_relationship_value) + self.conversation_info.relationship_text = self.relationship_translator.translate_relationship_value_to_text(numeric_relationship_value) logger.debug(f"[私聊][{self.private_name}] (备用逻辑)更新后关系文本: {self.conversation_info.relationship_text}") except ValueError: diff --git a/src/plugins/PFC/pfc_relationship.py b/src/plugins/PFC/pfc_relationship.py index efe8a6cc..630eac2c 100644 --- a/src/plugins/PFC/pfc_relationship.py +++ b/src/plugins/PFC/pfc_relationship.py @@ -1,4 +1,4 @@ -from typing import List, Dict, Any, Optional +from typing import List, Dict, Any from src.common.logger_manager import get_logger from src.plugins.models.utils_model import LLMRequest from src.plugins.person_info.person_info import person_info_manager @@ -215,15 +215,14 @@ class PfcRepationshipTranslator: 因为对于PFC的planner来说 其暗示了选择回复 所以新建代码文件来适配PFC的决策层面""" - def __init__(self): - pass + def __init__(self, private_name: str): + self.private_name = private_name - @staticmethod - def translate_relationship_value_to_text(self, relationship_value: float) -> str: + async def translate_relationship_value_to_text(self, relationship_value: float) -> str: """ 将数值型的关系值转换为PFC私聊场景下简洁的关系描述文本。 """ - level_num = self._calculate_relationship_level_num(relationship_value) + level_num = self._calculate_relationship_level_num(relationship_value, self.private_name) relationship_descriptions = [ "厌恶", # level_num 0 @@ -238,18 +237,18 @@ class PfcRepationshipTranslator: description = relationship_descriptions[level_num] else: description = "普通" # 默认或错误情况 - logger.warning(f"计算出的 level_num ({level_num}) 无效,关系描述默认为 '普通'") + logger.warning(f"[私聊][{self.private_name}] 计算出的 level_num ({level_num}) 无效,关系描述默认为 '普通'") return f"你们的关系是:{description}。" @staticmethod - def _calculate_relationship_level_num(relationship_value: float) -> int: + def _calculate_relationship_level_num(relationship_value: float, private_name: str) -> int: """ 根据关系值计算关系等级编号 (0-5)。 这里的阈值应与 relationship_manager.py 中的保持一致 """ if not isinstance(relationship_value, (int, float)): - logger.warning(f"传入的 relationship_value '{relationship_value}' 不是有效的数值类型,默认为0。") + logger.warning(f"[私聊][{private_name}] 传入的 relationship_value '{relationship_value}' 不是有效的数值类型,默认为0。") relationship_value = 0.0 if -1000 <= relationship_value < -227: @@ -271,6 +270,6 @@ class PfcRepationshipTranslator: elif relationship_value < -1000: level_num = 0 else: # 理论上不会到这里,除非前面的条件逻辑有误 - logger.warning(f"关系值 {relationship_value} 未落入任何预设范围,默认为普通。") + logger.warning(f"[私聊][{private_name}] 关系值 {relationship_value} 未落入任何预设范围,默认为普通。") level_num = 2 return level_num \ No newline at end of file From b1893e9514f41cf06b12ba71129419d35468d0c8 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Wed, 7 May 2025 15:54:03 +0800 Subject: [PATCH 28/63] =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E6=A0=87=E6=B3=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/pfc_emotion_updater.py | 5 +++-- src/plugins/PFC/pfc_relationship.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/plugins/PFC/pfc_emotion_updater.py b/src/plugins/PFC/pfc_emotion_updater.py index 48aab11b..a8dac2f0 100644 --- a/src/plugins/PFC/pfc_emotion_updater.py +++ b/src/plugins/PFC/pfc_emotion_updater.py @@ -2,6 +2,7 @@ from typing import List, Dict, Any, Optional +from MaiMBot.src.plugins.PFC.chat_observer import ChatObserver from src.common.logger_manager import get_logger from src.plugins.models.utils_model import LLMRequest from src.plugins.moods.moods import MoodManager # MoodManager 本身是单例 @@ -48,14 +49,14 @@ class PfcEmotionUpdater: self, conversation_info: ConversationInfo, observation_info: ObservationInfo, - chat_observer_for_history, # ChatObserver 实例 + chat_observer_for_history: ChatObserver, # ChatObserver 实例 event_description: str ) -> None: if not self.llm: logger.error(f"[私聊][{self.private_name}] LLM未初始化,无法进行情绪更新。") # 即使LLM失败,也应该更新conversation_info中的情绪文本为MoodManager的当前状态 if conversation_info and self.mood_mng: - conversation_info.current_emotion_text = self.mood_mng.get_prompt() + conversation_info.current_emotion_text = self.mood_mng.get_prompt() return if not self.mood_mng or not conversation_info or not observation_info: diff --git a/src/plugins/PFC/pfc_relationship.py b/src/plugins/PFC/pfc_relationship.py index 630eac2c..a0bdcd0b 100644 --- a/src/plugins/PFC/pfc_relationship.py +++ b/src/plugins/PFC/pfc_relationship.py @@ -1,4 +1,5 @@ from typing import List, Dict, Any +from MaiMBot.src.plugins.PFC.chat_observer import ChatObserver from src.common.logger_manager import get_logger from src.plugins.models.utils_model import LLMRequest from src.plugins.person_info.person_info import person_info_manager @@ -65,7 +66,7 @@ class PfcRelationshipUpdater: self, conversation_info: ConversationInfo, observation_info: ObservationInfo, - chat_observer_for_history # ChatObserver 实例 + chat_observer_for_history: ChatObserver # ChatObserver 实例 ) -> None: if not self.llm: logger.error(f"[私聊][{self.private_name}] LLM未初始化,无法进行增量关系更新。") @@ -141,7 +142,7 @@ class PfcRelationshipUpdater: self, conversation_info: ConversationInfo, observation_info: ObservationInfo, - chat_observer_for_history + chat_observer_for_history: ChatObserver ) -> None: if not self.llm: logger.error(f"[私聊][{self.private_name}] LLM未初始化,无法进行最终关系更新。") From 21b2502f131be3245e2db7149f1aa927a4b3fd44 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Wed, 7 May 2025 15:56:31 +0800 Subject: [PATCH 29/63] =?UTF-8?q?=E6=9B=B4=E5=90=8D=20pfc=5Femotion=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/conversation.py | 2 +- .../{pfc_emotion_updater.py => pfc_emotion.py} | 18 +++++------------- 2 files changed, 6 insertions(+), 14 deletions(-) rename src/plugins/PFC/{pfc_emotion_updater.py => pfc_emotion.py} (93%) diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index bb5477cc..a8e8ab71 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -19,7 +19,7 @@ from ..person_info.relationship_manager import relationship_manager from ..moods.moods import MoodManager from .pfc_relationship import PfcRelationshipUpdater, PfcRepationshipTranslator -from .pfc_emotion_updater import PfcEmotionUpdater # 新增 +from .pfc_emotion import PfcEmotionUpdater # 新增 # 导入 PFC 内部组件和类型 from .pfc_types import ConversationState # 导入更新后的 pfc_types diff --git a/src/plugins/PFC/pfc_emotion_updater.py b/src/plugins/PFC/pfc_emotion.py similarity index 93% rename from src/plugins/PFC/pfc_emotion_updater.py rename to src/plugins/PFC/pfc_emotion.py index a8dac2f0..66a5972d 100644 --- a/src/plugins/PFC/pfc_emotion_updater.py +++ b/src/plugins/PFC/pfc_emotion.py @@ -1,23 +1,15 @@ -# PFC/pfc_emotion_updater.py - -from typing import List, Dict, Any, Optional +from typing import List, Dict, Any from MaiMBot.src.plugins.PFC.chat_observer import ChatObserver from src.common.logger_manager import get_logger from src.plugins.models.utils_model import LLMRequest from src.plugins.moods.moods import MoodManager # MoodManager 本身是单例 from src.plugins.utils.chat_message_builder import build_readable_messages +from src.plugins.PFC.observation_info import ObservationInfo +from src.plugins.PFC.conversation_info import ConversationInfo +from src.config.config import global_config # 导入全局配置 -try: - from .observation_info import ObservationInfo - from .conversation_info import ConversationInfo -except ImportError: - from observation_info import ObservationInfo - from conversation_info import ConversationInfo - -from ...config.config import global_config # 导入全局配置 - -logger = get_logger("pfc_emotion_updater") +logger = get_logger("pfc_emotion") class PfcEmotionUpdater: def __init__(self, private_name: str, bot_name: str): From 2db342551a9a44fc521f15087356094087fb664a Mon Sep 17 00:00:00 2001 From: Bakadax Date: Wed, 7 May 2025 16:31:30 +0800 Subject: [PATCH 30/63] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=85=B3=E7=B3=BB?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/conversation.py | 47 +++++++++++------------------ src/plugins/PFC/observation_info.py | 14 ++------- src/plugins/PFC/pfc_emotion.py | 2 +- src/plugins/PFC/pfc_relationship.py | 2 +- src/plugins/PFC/reply_generator.py | 16 +++++----- src/plugins/PFC/waiter.py | 2 -- 6 files changed, 30 insertions(+), 53 deletions(-) diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index a8e8ab71..35da6e52 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -3,7 +3,6 @@ import asyncio import datetime import traceback import json # 确保导入 json 模块 -from .pfc_utils import get_items_from_json # 导入JSON解析工具 from typing import Dict, Any, Optional, Set, List from dateutil import tz @@ -11,19 +10,18 @@ from src.common.logger_manager import get_logger from src.plugins.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat from maim_message import UserInfo from src.plugins.chat.chat_stream import chat_manager, ChatStream -from ..chat.message import Message # 假设 Message 类在这里 -from ...config.config import global_config +from ..chat.message import Message +from src.config.config import global_config from ..person_info.person_info import person_info_manager from ..person_info.relationship_manager import relationship_manager -# 导入情绪 from ..moods.moods import MoodManager from .pfc_relationship import PfcRelationshipUpdater, PfcRepationshipTranslator -from .pfc_emotion import PfcEmotionUpdater # 新增 +from .pfc_emotion import PfcEmotionUpdater # 导入 PFC 内部组件和类型 -from .pfc_types import ConversationState # 导入更新后的 pfc_types -from .pfc import GoalAnalyzer # 假设 GoalAnalyzer 在 pfc.py +from .pfc_types import ConversationState +from .pfc import GoalAnalyzer from .chat_observer import ChatObserver from .message_sender import DirectMessageSender from .action_planner import ActionPlanner @@ -31,9 +29,9 @@ from .observation_info import ObservationInfo from .conversation_info import ConversationInfo from .reply_generator import ReplyGenerator from .idle_conversation_starter import IdleConversationStarter -from .pfc_KnowledgeFetcher import KnowledgeFetcher # 假设 KnowledgeFetcher 在这里 +from .pfc_KnowledgeFetcher import KnowledgeFetcher from .waiter import Waiter -from .reply_checker import ReplyChecker # 确保 ReplyChecker 被导入 +from .reply_checker import ReplyChecker # 导入富文本回溯,用于更好的错误展示 from rich.traceback import install @@ -43,19 +41,12 @@ install(extra_lines=3) # 获取当前模块的日志记录器 logger = get_logger("pfc_conversation") -try: - from ...config.config import global_config -except ImportError: - logger.error("无法在 conversation.py 中导入 global_config,时区可能不准确!") - global_config = None - TIME_ZONE = tz.tzlocal() # 使用本地时区作为后备 -else: - # 确保 global_config.TIME_ZONE 存在且有效,否则使用默认值 - configured_tz = getattr(global_config, 'TIME_ZONE', 'Asia/Shanghai') # 使用 getattr 安全访问 - TIME_ZONE = tz.gettz(configured_tz) - if TIME_ZONE is None: # 如果 gettz 返回 None,说明时区字符串无效 - logger.error(f"配置的时区 '{configured_tz}' 无效,将使用默认时区 'Asia/Shanghai'") - TIME_ZONE = tz.gettz('Asia/Shanghai') +# 确保 global_config.TIME_ZONE 存在且有效,否则使用默认值 +configured_tz = getattr(global_config, 'TIME_ZONE', 'Asia/Shanghai') # 使用 getattr 安全访问 +TIME_ZONE = tz.gettz(configured_tz) +if TIME_ZONE is None: # 如果 gettz 返回 None,说明时区字符串无效 + logger.error(f"配置的时区 '{configured_tz}' 无效,将使用默认时区 'Asia/Shanghai'") + TIME_ZONE = tz.gettz('Asia/Shanghai') class Conversation: """ @@ -139,7 +130,7 @@ class Conversation: private_name=self.private_name, bot_name=global_config.BOT_NICKNAME # 或者 self.name ) - logger.info(f"[私聊][{self.private_name}] PfcEmotionUpdater 初始化完成。") + logger.info(f"[私聊][{self.private_name}] PfcEmotion 初始化完成。") logger.debug(f"[私聊][{self.private_name}] 初始化 GoalAnalyzer...") self.goal_analyzer = GoalAnalyzer(self.stream_id, self.private_name) @@ -198,7 +189,7 @@ class Conversation: # 尝试从 observation_info 获取,这依赖于 _load_initial_history 的实现 private_user_id_str: Optional[str] = None private_platform_str: Optional[str] = None - private_nickname_str: Optional[str] = None + private_nickname_str = self.private_name if self.observation_info and self.observation_info.last_message_sender and self.observation_info.last_message_sender != self.bot_qq_str: # 如果历史记录最后一条不是机器人发的,那么发送者就是对方 @@ -209,14 +200,12 @@ class Conversation: if self.observation_info.sender_user_id and self.observation_info.sender_platform: private_user_id_str = self.observation_info.sender_user_id private_platform_str = self.observation_info.sender_platform - private_nickname_str = self.observation_info.sender_name logger.info(f"[私聊][{self.private_name}] 从 ObservationInfo 获取到私聊对象信息: ID={private_user_id_str}, Platform={private_platform_str}, Name={private_nickname_str}") if not private_user_id_str and self.chat_stream: # 如果 observation_info 中没有,尝试从 chat_stream (通常代表对方) if self.chat_stream.user_info and str(self.chat_stream.user_info.user_id) != self.bot_qq_str : # 确保不是机器人自己 private_user_id_str = str(self.chat_stream.user_info.user_id) private_platform_str = self.chat_stream.user_info.platform - private_nickname_str = self.chat_stream.user_info.user_nickname logger.info(f"[私聊][{self.private_name}] 从 ChatStream 获取到私聊对象信息: ID={private_user_id_str}, Platform={private_platform_str}, Name={private_nickname_str}") elif self.chat_stream.group_info is None and self.private_name: # 私聊场景,且 private_name 可能有用 # 这是一个备选方案,如果 private_name 直接是 user_id @@ -288,7 +277,7 @@ class Conversation: numeric_relationship_value = 0.0 # 2. 使用PFC内部翻译函数 - self.conversation_info.relationship_text = self.relationship_translator.translate_relationship_value_to_text(numeric_relationship_value) + self.conversation_info.relationship_text = await self.relationship_translator.translate_relationship_value_to_text(numeric_relationship_value) logger.info(f"[私聊][{self.private_name}] 初始化时加载关系文本: {self.conversation_info.relationship_text}") except Exception as e_init_rel: logger.error(f"[私聊][{self.private_name}] 初始化时加载关系文本出错: {e_init_rel}") @@ -573,7 +562,7 @@ class Conversation: logger.debug(f"[私聊][{self.private_name}] 获取到数值型关系值: {numeric_relationship_value}") # 2. 使用PFC内部的翻译函数将其转换为文本描述 - simplified_relationship_text = self.relationship_translator.translate_relationship_value_to_text(numeric_relationship_value) + simplified_relationship_text = await self.relationship_translator.translate_relationship_value_to_text(numeric_relationship_value) self.conversation_info.relationship_text = simplified_relationship_text logger.debug(f"[私聊][{self.private_name}] 更新后关系文本 (PFC内部翻译): {self.conversation_info.relationship_text}") @@ -605,7 +594,7 @@ class Conversation: numeric_relationship_value = float(numeric_relationship_value.to_decimal()) else: numeric_relationship_value = 0.0 - self.conversation_info.relationship_text = self.relationship_translator.translate_relationship_value_to_text(numeric_relationship_value) + self.conversation_info.relationship_text = await self.relationship_translator.translate_relationship_value_to_text(numeric_relationship_value) logger.debug(f"[私聊][{self.private_name}] (备用逻辑)更新后关系文本: {self.conversation_info.relationship_text}") except ValueError: diff --git a/src/plugins/PFC/observation_info.py b/src/plugins/PFC/observation_info.py index 3dbdea7a..4f5ea75f 100644 --- a/src/plugins/PFC/observation_info.py +++ b/src/plugins/PFC/observation_info.py @@ -1,4 +1,3 @@ -import datetime import time import traceback from dateutil import tz @@ -6,23 +5,14 @@ from typing import List, Optional, Dict, Any, Set from maim_message import UserInfo from src.common.logger import get_module_logger from src.plugins.utils.chat_message_builder import build_readable_messages - +from src.config.config import global_config # 确保导入路径正确 from .chat_observer import ChatObserver from .chat_states import NotificationHandler, NotificationType, Notification logger = get_module_logger("observation_info") -try: - from ...config.config import global_config -except ImportError: - # 如果路径不对,可能需要调整 '...' 的数量 - # 或者在后面实际使用 TIME_ZONE 的地方导入 - logger.warning("无法在 observation_info.py 中直接导入 global_config,时区将在使用时获取") - global_config = None # 设置为 None,后面检查 - TIME_ZONE = tz.tzlocal() # 备选:使用本地时区 -else: - TIME_ZONE = tz.gettz(global_config.TIME_ZONE if global_config else 'Asia/Shanghai') # 使用配置的时区,提供默认值 +TIME_ZONE = tz.gettz(global_config.TIME_ZONE if global_config else 'Asia/Shanghai') # 使用配置的时区,提供默认值 class ObservationInfoHandler(NotificationHandler): diff --git a/src/plugins/PFC/pfc_emotion.py b/src/plugins/PFC/pfc_emotion.py index 66a5972d..e8404e52 100644 --- a/src/plugins/PFC/pfc_emotion.py +++ b/src/plugins/PFC/pfc_emotion.py @@ -1,6 +1,6 @@ from typing import List, Dict, Any -from MaiMBot.src.plugins.PFC.chat_observer import ChatObserver +from src.plugins.PFC.chat_observer import ChatObserver from src.common.logger_manager import get_logger from src.plugins.models.utils_model import LLMRequest from src.plugins.moods.moods import MoodManager # MoodManager 本身是单例 diff --git a/src/plugins/PFC/pfc_relationship.py b/src/plugins/PFC/pfc_relationship.py index a0bdcd0b..9457a000 100644 --- a/src/plugins/PFC/pfc_relationship.py +++ b/src/plugins/PFC/pfc_relationship.py @@ -1,5 +1,5 @@ from typing import List, Dict, Any -from MaiMBot.src.plugins.PFC.chat_observer import ChatObserver +from src.plugins.PFC.chat_observer import ChatObserver from src.common.logger_manager import get_logger from src.plugins.models.utils_model import LLMRequest from src.plugins.person_info.person_info import person_info_manager diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index eb8c619c..fba8fbda 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -153,8 +153,8 @@ class ReplyGenerator: Returns: str: 生成的回复。 - 对于 'direct_reply' 和 'say_goodbye',返回纯文本回复。 - 对于 'send_new_message',返回包含决策和文本的JSON字符串。 + 对于 'direct_reply' 和 'say_goodbye',返回纯文本回复。 + 对于 'send_new_message',返回包含决策和文本的JSON字符串。 """ logger.debug( f"[私聊][{self.private_name}]开始生成回复 (动作类型: {action_type}):当前目标: {conversation_info.goal_list}" @@ -233,11 +233,11 @@ class ReplyGenerator: spam_warning_message = "" if action_type == "send_new_message": # 只在 send_new_message 时构建刷屏警告 if conversation_info.my_message_count > 5: - spam_warning_message = f"⚠️【警告】**你已连续发送{str(conversation_info.my_message_count)}条消息!请谨慎考虑是否继续发送!以免刷屏对造成对方困扰!**" + spam_warning_message = f"⚠️【警告】**你已连续发送{str(conversation_info.my_message_count)}条消息!请谨慎考虑是否继续发送!以免刷屏对造成对方困扰!**" elif conversation_info.my_message_count > 2: - spam_warning_message = f"💬【提示】**你已连续发送{str(conversation_info.my_message_count)}条消息。如果非必要,请避免连续发送,以免给对方造成困扰。**" + spam_warning_message = f"💬【提示】**你已连续发送{str(conversation_info.my_message_count)}条消息。如果非必要,请避免连续发送,以免给对方造成困扰。**" if spam_warning_message: - spam_warning_message = f"\n{spam_warning_message}\n" + spam_warning_message = f"\n{spam_warning_message}\n" # --- 选择 Prompt --- @@ -255,17 +255,17 @@ class ReplyGenerator: try: current_time_value = "获取时间失败" if observation_info and hasattr(observation_info, 'current_time_str') and observation_info.current_time_str: - current_time_value = observation_info.current_time_str + current_time_value = observation_info.current_time_str if action_type == "say_goodbye": - prompt = prompt_template.format( + prompt = prompt_template.format( persona_text=persona_text, chat_history_text=chat_history_text, current_time_str=current_time_value, sender_name=sender_name_str, relationship_text=relationship_text_str, current_emotion_text=current_emotion_text_str - ) + ) elif action_type == "send_new_message": # PROMPT_SEND_NEW_MESSAGE 增加了 spam_warning_info prompt = prompt_template.format( persona_text=persona_text, diff --git a/src/plugins/PFC/waiter.py b/src/plugins/PFC/waiter.py index 0f5881fc..9ffff524 100644 --- a/src/plugins/PFC/waiter.py +++ b/src/plugins/PFC/waiter.py @@ -1,8 +1,6 @@ from src.common.logger import get_module_logger from .chat_observer import ChatObserver from .conversation_info import ConversationInfo - -# from src.individuality.individuality import Individuality # 不再需要 from ...config.config import global_config import time import asyncio From f154145a0ef3e2fb54bccae07c93e5bb6d209eef Mon Sep 17 00:00:00 2001 From: Bakadax Date: Wed, 7 May 2025 17:50:37 +0800 Subject: [PATCH 31/63] =?UTF-8?q?=E6=8B=86=E5=88=86=20conversation=20?= =?UTF-8?q?=E4=B9=8B=204.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/conversation.py | 67 +++++---------------------------- src/plugins/PFC/pfc_utils.py | 44 ++++++++++++++++++++++ 2 files changed, 54 insertions(+), 57 deletions(-) diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 35da6e52..3b571cd4 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -31,6 +31,7 @@ from .reply_generator import ReplyGenerator from .idle_conversation_starter import IdleConversationStarter from .pfc_KnowledgeFetcher import KnowledgeFetcher from .waiter import Waiter +from .pfc_utils import get_person_id from .reply_checker import ReplyChecker # 导入富文本回溯,用于更好的错误展示 @@ -187,61 +188,13 @@ class Conversation: # 4.1 加载用户数据 # 尝试从 observation_info 获取,这依赖于 _load_initial_history 的实现 - private_user_id_str: Optional[str] = None - private_platform_str: Optional[str] = None - private_nickname_str = self.private_name + self.conversation_info.person_id, private_platform_str, private_user_id_str = await get_person_id( + private_name=self.private_name, + chat_stream=self.chat_stream, + ) + + logger.info(f"[私聊][{self.private_name}] 获取到 person_id: {self.conversation_info.person_id} for {private_platform_str}:{private_user_id_str}") - if self.observation_info and self.observation_info.last_message_sender and self.observation_info.last_message_sender != self.bot_qq_str: - # 如果历史记录最后一条不是机器人发的,那么发送者就是对方 - # 假设 observation_info 中已经有了 sender_user_id, sender_platform, sender_name - # 这些字段应该在 observation_info.py 的 update_from_message 中从非机器人消息填充 - # 并且 _load_initial_history 处理历史消息时也应该填充它们 - # 这里的逻辑是:取 observation_info 中最新记录的非机器人发送者的信息 - if self.observation_info.sender_user_id and self.observation_info.sender_platform: - private_user_id_str = self.observation_info.sender_user_id - private_platform_str = self.observation_info.sender_platform - logger.info(f"[私聊][{self.private_name}] 从 ObservationInfo 获取到私聊对象信息: ID={private_user_id_str}, Platform={private_platform_str}, Name={private_nickname_str}") - - if not private_user_id_str and self.chat_stream: # 如果 observation_info 中没有,尝试从 chat_stream (通常代表对方) - if self.chat_stream.user_info and str(self.chat_stream.user_info.user_id) != self.bot_qq_str : # 确保不是机器人自己 - private_user_id_str = str(self.chat_stream.user_info.user_id) - private_platform_str = self.chat_stream.user_info.platform - logger.info(f"[私聊][{self.private_name}] 从 ChatStream 获取到私聊对象信息: ID={private_user_id_str}, Platform={private_platform_str}, Name={private_nickname_str}") - elif self.chat_stream.group_info is None and self.private_name: # 私聊场景,且 private_name 可能有用 - # 这是一个备选方案,如果 private_name 直接是 user_id - # 你需要确认 private_name 的确切含义和格式 - # logger.warning(f"[私聊][{self.private_name}] 尝试使用 private_name ('{self.private_name}') 作为 user_id,平台默认为 'qq'") - # private_user_id_str = self.private_name - # private_platform_str = "qq" # 假设平台是qq - # private_nickname_str = self.private_name # 昵称也暂时用 private_name - pass # 暂时不启用此逻辑,依赖 observation_info 或 chat_stream.user_info - - if private_user_id_str and private_platform_str: - try: - # 将 user_id 转换为整数类型,因为 person_info_manager.get_person_id 需要 int - private_user_id_int = int(private_user_id_str) - self.conversation_info.person_id = self.person_info_mng.get_person_id( - private_platform_str, - private_user_id_int # 使用转换后的整数ID - ) - logger.info(f"[私聊][{self.private_name}] 获取到 person_id: {self.conversation_info.person_id} for {private_platform_str}:{private_user_id_str}") - - # 确保用户在数据库中存在,如果不存在则创建 - # get_or_create_person 内部处理 person_id 的生成,所以我们直接传 platform 和 user_id - await self.person_info_mng.get_or_create_person( - platform=private_platform_str, - user_id=private_user_id_int, # 使用转换后的整数ID - nickname=private_nickname_str if private_nickname_str else "未知用户", - # user_cardname 和 user_avatar 如果能从 chat_stream.user_info 或 observation_info 获取也应传入 - # user_cardname = self.chat_stream.user_info.card if self.chat_stream and self.chat_stream.user_info else None, - # user_avatar = self.chat_stream.user_info.avatar if self.chat_stream and self.chat_stream.user_info else None - ) - except ValueError: - logger.error(f"[私聊][{self.private_name}] 无法将 private_user_id_str ('{private_user_id_str}') 转换为整数。") - except Exception as e_pid: - logger.error(f"[私聊][{self.private_name}] 获取或创建 person_id 时出错: {e_pid}") - else: - logger.warning(f"[私聊][{self.private_name}] 未能确定私聊对象的 user_id 或 platform,无法获取 person_id。将在收到消息后尝试。") # 5. 启动需要后台运行的组件 logger.debug(f"[私聊][{self.private_name}] 启动 ChatObserver...") @@ -251,12 +204,12 @@ class Conversation: self.idle_conversation_starter.start() logger.info(f"[私聊][{self.private_name}] 空闲对话检测器已启动") - # 5.1 启动 MoodManager 的后台更新 + # 5.1 启动 MoodManager 的后台更新 if self.mood_mng and hasattr(self.mood_mng, 'start_mood_update') and not self.mood_mng._running: self.mood_mng.start_mood_update(update_interval=global_config.mood_update_interval) # 使用配置的更新间隔 logger.info(f"[私聊][{self.private_name}] MoodManager 已启动后台更新,间隔: {global_config.mood_update_interval} 秒。") elif self.mood_mng and self.mood_mng._running: - logger.info(f"[私聊][{self.private_name}] MoodManager 已在运行中。") + logger.info(f"[私聊][{self.private_name}] MoodManager 已在运行中。") else: logger.warning(f"[私聊][{self.private_name}] MoodManager 未能启动,相关功能可能受限。") @@ -512,7 +465,7 @@ class Conversation: logger.warning(f"[私聊][{self.private_name}] ObservationInfo 未初始化,无法更新当前时间。") except Exception as time_update_err: logger.error(f"[私聊][{self.private_name}] 更新 ObservationInfo 当前时间时出错: {time_update_err}") - # --- 更新时间代码结束 --- + # --- 更新时间代码结束 --- # --- 处理忽略状态 --- if self.ignore_until_timestamp and loop_iter_start_time < self.ignore_until_timestamp: diff --git a/src/plugins/PFC/pfc_utils.py b/src/plugins/PFC/pfc_utils.py index b0f3f841..920566ac 100644 --- a/src/plugins/PFC/pfc_utils.py +++ b/src/plugins/PFC/pfc_utils.py @@ -5,6 +5,8 @@ from typing import Dict, Any, Optional, Tuple, List, Union from src.common.logger_manager import get_logger # 确认 logger 的导入路径 from src.plugins.memory_system.Hippocampus import HippocampusManager from src.plugins.heartFC_chat.heartflow_prompt_builder import prompt_builder # 确认 prompt_builder 的导入路径 +from src.plugins.chat.chat_stream import ChatStream +from ..person_info.person_info import person_info_manager logger = get_logger("pfc_utils") @@ -196,3 +198,45 @@ def get_items_from_json( return False, result return True, result + +async def get_person_id(private_name: str, chat_stream: ChatStream): + private_user_id_str: Optional[str] = None + private_platform_str: Optional[str] = None + private_nickname_str = private_name + + if chat_stream.user_info: + private_user_id_str = str(chat_stream.user_info.user_id) + private_platform_str = chat_stream.user_info.platform + logger.info(f"[私聊][{private_name}] 从 ChatStream 获取到私聊对象信息: ID={private_user_id_str}, Platform={private_platform_str}, Name={private_nickname_str}") + elif chat_stream.group_info is None and private_name: # 私聊场景,且 private_name 可能有用 + # 这是一个备选方案,如果 private_name 直接是 user_id + # 你需要确认 private_name 的确切含义和格式 <- private_name 就是对方昵称,格式是 str + # logger.warning(f"[私聊][{self.private_name}] 尝试使用 private_name ('{self.private_name}') 作为 user_id,平台默认为 'qq'") + # private_user_id_str = self.private_name + # private_platform_str = "qq" # 假设平台是qq + # private_nickname_str = self.private_name # 昵称也暂时用 private_name + pass # 暂时不启用此逻辑,依赖 observation_info 或 chat_stream.user_info + + if private_user_id_str and private_platform_str: + try: + # 将 user_id 转换为整数类型,因为 person_info_manager.get_person_id 需要 int + private_user_id_int = int(private_user_id_str) + person_id = person_info_manager.get_person_id( + private_platform_str, + private_user_id_int + # 使用转换后的整数ID + ) + # 确保用户在数据库中存在,如果不存在则创建 + # get_or_create_person 内部处理 person_id 的生成,所以我们直接传 platform 和 user_id + await person_info_manager.get_or_create_person( + platform=private_platform_str, + user_id=private_user_id_int, # 使用转换后的整数ID + nickname=private_name, + ) + return person_id, private_platform_str, private_user_id_str + except ValueError: + logger.error(f"[私聊][{private_name}] 无法将 private_user_id_str ('{private_user_id_str}') 转换为整数。") + except Exception as e_pid: + logger.error(f"[私聊][{private_name}] 获取或创建 person_id 时出错: {e_pid}") + else: + logger.warning(f"[私聊][{private_name}] 未能确定私聊对象的 user_id 或 platform,无法获取 person_id。将在收到消息后尝试。") \ No newline at end of file From 68572bfbefc09a3e2f650c4287b582f650a70222 Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Wed, 7 May 2025 18:05:25 +0800 Subject: [PATCH 32/63] =?UTF-8?q?prompt=E5=BE=AE=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/pfc_emotion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/PFC/pfc_emotion.py b/src/plugins/PFC/pfc_emotion.py index e8404e52..35b526a2 100644 --- a/src/plugins/PFC/pfc_emotion.py +++ b/src/plugins/PFC/pfc_emotion.py @@ -70,7 +70,7 @@ class PfcEmotionUpdater: if not sender_name_for_prompt: sender_name_for_prompt = '对方' relationship_text_for_prompt = getattr(conversation_info, 'relationship_text', '关系一般。') # 从 ConversationInfo 获取关系文本 - emotion_prompt = f"""你是机器人 {self.bot_name}。你现在的心情是【{current_mood_text_from_manager}】。 + emotion_prompt = f"""你是{self.bot_name}。你现在的心情是【{current_mood_text_from_manager}】。 你正在和用户【{sender_name_for_prompt}】私聊,你们的关系是:【{relationship_text_for_prompt}】。 最近发生的事件是:【{event_description}】 最近的对话摘要: From 8839facaddfdf868778f88f17f3bca6265695f6c Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Wed, 7 May 2025 19:40:12 +0800 Subject: [PATCH 33/63] =?UTF-8?q?=E6=9D=80=E6=AD=BBchecker=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/conversation.py | 175 +++++++++++++---------- src/plugins/PFC/reply_checker.py | 214 +++++++---------------------- src/plugins/PFC/reply_generator.py | 48 ++++--- 3 files changed, 177 insertions(+), 260 deletions(-) diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 3b571cd4..8d16c04f 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -842,21 +842,19 @@ class Conversation: action_successful: bool = False # 标记动作是否成功执行 final_status: str = "recall" # 动作最终状态,默认为 recall (表示未成功或需重试) final_reason: str = "动作未成功执行" # 动作最终原因 - need_replan_from_checker: bool = False # 标记是否由 ReplyChecker 要求重新规划 + # need_replan_from_checker: bool = False # 这个变量在循环内部被赋值 try: # --- 根据不同的 action 类型执行相应的逻辑 --- # 1. 处理需要生成、检查、发送的动作 if action in ["direct_reply", "send_new_message"]: - max_reply_attempts: int = 3 # 最多尝试次数 (可配置) + max_reply_attempts: int = global_config.get("pfc_max_reply_attempts", 3) # 最多尝试次数 (可配置) reply_attempt_count: int = 0 is_suitable: bool = False # 标记回复是否合适 generated_content: str = "" # 存储生成的回复 check_reason: str = "未进行检查" # 存储检查结果原因 - - # --- [核心修复] 引入重试循环 --- - is_send_decision_from_rg = False # 标记是否由 reply_generator 决定发送 + need_replan_from_checker: bool = False # 在循环前初始化 while reply_attempt_count < max_reply_attempts and not is_suitable and not need_replan_from_checker: reply_attempt_count += 1 @@ -878,12 +876,6 @@ class Conversation: if action == "send_new_message": is_send_decision_from_rg = True # 标记 send_new_message 的决策来自RG try: - # 使用 pfc_utils.py 中的 get_items_from_json 来解析 - # 注意:get_items_from_json 目前主要用于提取固定字段的字典。 - # reply_generator 返回的是一个顶级JSON对象。 - # 我们需要稍微调整用法或增强 get_items_from_json。 - # 简单起见,这里我们先直接用 json.loads,后续可以优化。 - parsed_json = None try: parsed_json = json.loads(raw_llm_output) @@ -923,17 +915,24 @@ class Conversation: 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 (action == "send_new_message" and generated_content_for_check_or_send == "no"): - logger.warning(f"{log_prefix} 生成内容无效或为错误提示 (或send:no),将进行下一次尝试 (如果适用)。") - check_reason = "生成内容无效或选择不发送" + 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): # should_send_reply is true here means RG said yes, but txt is "no" or empty + + 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 - if action == "direct_reply": # direct_reply 失败时才继续尝试 - await asyncio.sleep(0.5) - continue - else: # send_new_message 如果是 no,不应该继续尝试,上面已经break了 - pass # 理论上不会到这里如果上面break了 - + + # if reply_attempt_count < max_reply_attempts: # 只有在还有尝试次数时才继续 + await asyncio.sleep(0.5) # 暂停一下 + continue # 直接进入下一次循环尝试 + self.state = ConversationState.CHECKING if not self.reply_checker: raise RuntimeError("ReplyChecker 未初始化") @@ -973,10 +972,30 @@ class Conversation: if not is_suitable: conversation_info.last_reply_rejection_reason = check_reason conversation_info.last_rejected_reply_content = generated_content_for_check_or_send - if not need_replan_from_checker and reply_attempt_count < max_reply_attempts: + + # 如果是我们特定的复读原因,并且 checker 说不需要重新规划 (这是我们的新逻辑) + if check_reason == "机器人尝试发送重复消息" and not need_replan_from_checker: + logger.warning(f"{log_prefix} 回复因自身重复被拒绝: {check_reason}。将使用相同 Prompt 类型重试。") + # 不需要额外操作,循环会自动继续,因为 is_suitable=False 且 need_replan_from_checker=False + 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) - # 如果需要重规划或达到最大次数,循环会在下次判断时自动结束 + 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: @@ -1013,17 +1032,11 @@ class Conversation: logger.info(f"[私聊][{self.private_name}] 反思后的新规划动作: {new_action}, 原因: {new_reason}") # **暂定方案:** - # _handle_action 在这种情况下返回一个特殊标记。 - # 为了不立即修改返回类型,我们暂时在这里记录日志,并在 _plan_and_action_loop 中添加逻辑。 - # _handle_action 会将 action_successful 设置为 True,final_status 为 "done_no_reply"。 - # _plan_and_action_loop 之后会检查这个状态。 - - # (这里的 final_status, final_reason, action_successful 已在上面设置) - + elif is_suitable: # 适用于 direct_reply 或 send_new_message (RG决定发送且检查通过) logger.info(f"[私聊][{self.private_name}] 动作 '{action}': 找到合适的回复,准备发送。") - conversation_info.last_reply_rejection_reason = None - conversation_info.last_rejected_reply_content = None + # conversation_info.last_reply_rejection_reason = None # 已在循环内清除 + # conversation_info.last_rejected_reply_content = None self.generated_reply = generated_content_for_check_or_send timestamp_before_sending = time.time() logger.debug( @@ -1035,6 +1048,8 @@ class Conversation: if send_success: action_successful = True + final_status = "done" # 明确设置 final_status + final_reason = "成功发送" # 明确设置 final_reason logger.info(f"[私聊][{self.private_name}] 动作 '{action}': 成功发送回复.") if self.idle_conversation_starter: await self.idle_conversation_starter.update_last_message_time(send_end_time) @@ -1103,6 +1118,7 @@ class Conversation: logger.error(f"[私聊][{self.private_name}] 动作 '{action}': 发送回复失败。") final_status = "recall" final_reason = "发送回复时失败" + action_successful = False # 确保 action_successful 为 False conversation_info.last_successful_reply_action = None conversation_info.my_message_count = 0 # 发送失败,重置计数 @@ -1121,6 +1137,7 @@ class Conversation: ) final_status = "recall" final_reason = f"尝试{max_reply_attempts}次后失败: {check_reason}" + action_successful = False conversation_info.last_successful_reply_action = None # my_message_count 保持不变 @@ -1335,58 +1352,68 @@ class Conversation: # 2. 更新动作历史记录的最终状态和原因 # 优化:如果动作成功但状态仍是默认的 recall,则更新为 done - if final_status == "recall" and action_successful: - final_status = "done" - # 根据动作类型设置更具体的成功原因 - if action == "wait": - # 检查是否是因为超时结束的(需要 waiter 返回值,或者检查 goal_list) - # 这里简化处理,直接使用通用成功原因 - timeout_occurred = ( - any("分钟," in g.get("goal", "") for g in conversation_info.goal_list if isinstance(g, dict)) - if conversation_info.goal_list - else False - ) - final_reason = "等待完成" + (" (超时)" if timeout_occurred else " (收到新消息或中断)") - elif action == "listening": - final_reason = "进入倾听状态" - elif action in ["rethink_goal", "end_conversation", "block_and_ignore"]: - final_reason = f"成功执行 {action}" - elif action in ["direct_reply", "send_new_message", "say_goodbye"]: - # 如果是因为发送成功,设置原因 - final_reason = "成功发送" - else: - # 其他未知但标记成功的动作 - final_reason = "动作成功完成" + if action_successful: + # 如果动作标记为成功,但 final_status 仍然是初始的 "recall" 或者 "start" + # (因为可能在try块中成功执行了但没有显式更新 final_status 为 "done") + # 或者是 "done_no_reply" 这种特殊的成功状态 + if final_status in ["recall", "start"] and action != "send_new_message": # send_new_message + no_reply 是特殊成功 + final_status = "done" + if not final_reason or final_reason == "动作未成功执行": + # 为不同类型的成功动作提供更具体的默认成功原因 + if action == "wait": + timeout_occurred = ( + any("分钟," in g.get("goal", "") for g in conversation_info.goal_list if isinstance(g, dict)) + if conversation_info.goal_list + else False + ) + final_reason = "等待完成" + (" (超时)" if timeout_occurred else " (收到新消息或中断)") + elif action == "listening": + final_reason = "进入倾听状态" + elif action in ["rethink_goal", "end_conversation", "block_and_ignore", "say_goodbye"]: + final_reason = f"成功执行 {action}" + elif action in ["direct_reply", "send_new_message"]: # 正常发送成功的case + final_reason = "成功发送" + else: + final_reason = f"动作 {action} 成功完成" + # 如果已经是 "done" 或 "done_no_reply",则保留它们和它们对应的 final_reason + + else: # action_successful is False + # 如果动作标记为失败,且 final_status 还是 "recall" (初始值) 或 "start" + if final_status in ["recall", "start"]: + # 尝试从 conversation_info 中获取更具体的失败原因(例如 checker 的原因) + # 这个 specific_rejection_reason 是在 try 块中被设置的 + specific_rejection_reason = getattr(conversation_info, 'last_reply_rejection_reason', None) + rejected_content = getattr(conversation_info, 'last_rejected_reply_content', None) - elif final_status == "recall" and not action_successful: - # 如果最终是 recall 且未成功,且不是因为检查不通过(比如生成失败),确保原因合理 - # 保留之前的逻辑,检查是否已有更具体的失败原因 - if not final_reason or final_reason == "动作未成功执行": - # 检查是否有 checker 的原因 - checker_reason = conversation_info.last_reply_rejection_reason - if checker_reason: - final_reason = f"回复检查不通过: {checker_reason}" - else: - final_reason = "动作执行失败或被取消" # 通用失败原因 + if specific_rejection_reason: + final_reason = f"执行失败: {specific_rejection_reason}" + if rejected_content and specific_rejection_reason == "机器人尝试发送重复消息": # 对复读提供更清晰的日志 + final_reason += f" (内容: '{rejected_content[:30]}...')" + elif not final_reason or final_reason == "动作未成功执行": # 如果没有更具体的原因 + final_reason = f"动作 {action} 执行失败或被意外中止" + # 如果 final_status 已经是 "error" 或 "cancelled",则保留它们和它们对应的 final_reason - # 更新历史记录字典 + # 更新 done_action 中的记录 if conversation_info.done_action and action_index < len(conversation_info.done_action): - # 使用 update 方法更新字典,更安全 + # 确保 current_action_record 是最新的(尽管我们是更新它) + # 实际上我们是更新 list 中的元素 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), } ) - logger.debug( - f"[私聊][{self.private_name}] 动作 '{action}' 最终状态: {final_status}, 原因: {final_reason}" - ) else: - # 如果索引无效或列表为空,记录错误 logger.error(f"[私聊][{self.private_name}] 无法更新动作历史记录,索引 {action_index} 无效或列表为空。") + + # 最终日志输出 + log_final_reason = final_reason if final_reason else "无明确原因" + if final_status == "done" and action_successful and action in ["direct_reply", "send_new_message"] and hasattr(self, 'generated_reply'): + log_final_reason += f" (发送内容: '{self.generated_reply[:30]}...')" + logger.info(f"[私聊][{self.private_name}] 动作 '{action}' 处理完成。最终状态: {final_status}, 原因: {log_final_reason}") async def _send_reply(self) -> bool: """发送 `self.generated_reply` 中的内容到聊天流""" # 检查是否有内容可发送 diff --git a/src/plugins/PFC/reply_checker.py b/src/plugins/PFC/reply_checker.py index a07dc54f..a238a110 100644 --- a/src/plugins/PFC/reply_checker.py +++ b/src/plugins/PFC/reply_checker.py @@ -1,193 +1,79 @@ + import json from typing import Tuple, List, Dict, Any from src.common.logger import get_module_logger -from ..models.utils_model import LLMRequest -from ...config.config import global_config +# LLMRequest 和 global_config 不再需要直接在此文件中使用(除非 ReplyChecker 以后有其他功能) +# from ..models.utils_model import LLMRequest # <--- 移除 +# from ...config.config import global_config # <--- 移除,但下面会用到 bot_id +from ...config.config import global_config # 为了获取 BOT_QQ from .chat_observer import ChatObserver -from maim_message import UserInfo +from maim_message import UserInfo # 保持,可能用于未来扩展,但当前逻辑不直接使用 logger = get_module_logger("reply_checker") class ReplyChecker: - """回复检查器""" + """回复检查器 - 新版:仅检查机器人自身发言的精确重复""" def __init__(self, stream_id: str, private_name: str): - self.llm = LLMRequest( - model=global_config.llm_PFC_reply_checker, temperature=0.50, max_tokens=1000, request_type="reply_check" - ) + # self.llm = LLMRequest(...) # <--- 移除 LLM 初始化 self.name = global_config.BOT_NICKNAME self.private_name = private_name self.chat_observer = ChatObserver.get_instance(stream_id, private_name) - self.max_retries = 3 # 最大重试次数 + # self.max_retries = 3 # 这个 max_retries 属性在当前设计下不再由 checker 控制,而是由 conversation.py 控制 + self.bot_qq_str = str(global_config.BOT_QQ) # 获取机器人QQ号用于识别自身消息 async def check( - self, reply: str, goal: str, chat_history: List[Dict[str, Any]], chat_history_text: str,current_time_str: str, retry_count: int = 0 + self, reply: str, goal: str, chat_history: List[Dict[str, Any]], chat_history_text: str, current_time_str: str, retry_count: int = 0 ) -> Tuple[bool, str, bool]: - """检查生成的回复是否合适 + """检查生成的回复是否与机器人之前的发言完全一致(长度大于4) Args: - reply: 生成的回复 - goal: 对话目标 - chat_history: 对话历史记录 - chat_history_text: 对话历史记录文本 - retry_count: 当前重试次数 + reply: 待检查的机器人回复内容 + goal: 当前对话目标 (新逻辑中未使用) + chat_history: 对话历史记录 (包含用户和机器人的消息字典列表) + chat_history_text: 对话历史记录的文本格式 (新逻辑中未使用) + current_time_str: 当前时间的字符串格式 (新逻辑中未使用) + retry_count: 当前重试次数 (新逻辑中未使用) Returns: Tuple[bool, str, bool]: (是否合适, 原因, 是否需要重新规划) + 对于重复消息: (False, "机器人尝试发送重复消息", False) + 对于非重复消息: (True, "消息内容未与机器人历史发言重复。", False) """ - # 不再从 observer 获取,直接使用传入的 chat_history - # messages = self.chat_observer.get_cached_messages(limit=20) + if not self.bot_qq_str: + logger.error(f"[私聊][{self.private_name}] ReplyChecker: BOT_QQ 未配置,无法检查机器人自身消息。") + return True, "BOT_QQ未配置,跳过重复检查。", False # 无法检查则默认通过 + + if len(reply) <= 4: + return True, "消息长度小于等于4字符,跳过重复检查。", False + try: - # 筛选出最近由 Bot 自己发送的消息 - bot_messages = [] - for msg in reversed(chat_history): - user_info = UserInfo.from_dict(msg.get("user_info", {})) - if str(user_info.user_id) == str(global_config.BOT_QQ): # 确保比较的是字符串 - bot_messages.append(msg.get("processed_plain_text", "")) - if len(bot_messages) >= 2: # 只和最近的两条比较 - break - # 进行比较 - if bot_messages: - # 可以用简单比较,或者更复杂的相似度库 (如 difflib) - # 简单比较:是否完全相同 - if reply == bot_messages[0]: # 和最近一条完全一样 - logger.warning( - f"[私聊][{self.private_name}]ReplyChecker 检测到回复与上一条 Bot 消息完全相同: '{reply}'" - ) - return ( - False, - "被逻辑检查拒绝:回复内容与你上一条发言完全相同,可以选择深入话题或寻找其它话题或等待", - True, - ) # 不合适,需要返回至决策层 - # 2. 相似度检查 (如果精确匹配未通过) - import difflib # 导入 difflib 库 + for msg_dict in chat_history: + if not isinstance(msg_dict, dict): + continue - # 计算编辑距离相似度,ratio() 返回 0 到 1 之间的浮点数 - similarity_ratio = difflib.SequenceMatcher(None, reply, bot_messages[0]).ratio() - logger.debug(f"[私聊][{self.private_name}]ReplyChecker - 相似度: {similarity_ratio:.2f}") + user_info_data = msg_dict.get("user_info") + if not isinstance(user_info_data, dict): + continue - # 设置一个相似度阈值 - similarity_threshold = 0.9 - if similarity_ratio > similarity_threshold: - logger.warning( - f"[私聊][{self.private_name}]ReplyChecker 检测到回复与上一条 Bot 消息高度相似 (相似度 {similarity_ratio:.2f}): '{reply}'" - ) - return ( - False, - f"被逻辑检查拒绝:回复内容与你上一条发言高度相似 (相似度 {similarity_ratio:.2f}),可以选择深入话题或寻找其它话题或等待。", - True, - ) + sender_id = str(user_info_data.get("user_id")) + + # 只检查机器人自己发送过的历史消息 + if sender_id == self.bot_qq_str: + historical_message_text = msg_dict.get("processed_plain_text", "") + if reply == historical_message_text: + logger.warning( + f"[私聊][{self.private_name}] ReplyChecker 检测到机器人自身重复消息: '{reply}'" + ) + return (False, "机器人尝试发送重复消息", False) # is_suitable=False, reason, need_replan=False + + # 如果循环结束都没有找到重复 + return (True, "消息内容未与机器人历史发言重复。", False) except Exception as e: import traceback - - logger.error(f"[私聊][{self.private_name}]检查回复时出错: 类型={type(e)}, 值={e}") - logger.error(f"[私聊][{self.private_name}]{traceback.format_exc()}") # 打印详细的回溯信息 - - prompt_template = """ -当前时间:{current_time_str} -你是一个聊天逻辑检查器,请检查以下回复或消息是否合适: - -对话记录: -{chat_history_text} - -待检查的消息: -{reply} - -请结合聊天记录检查以下几点: -1. 这条消息是否与最新的对话记录保持连贯性(当话题切换时须保证平滑切换) -2. 是否存在重复发言,或重复表达同质内容(尤其是只是换一种方式表达了相同的含义) -3. 这条消息是否包含违规内容(例如血腥暴力,政治敏感等) -4. 这条消息是否以发送者的角度发言(不要让发送者自己回复自己的消息) -5. 这条消息是否通俗易懂 -6. 这条消息是否有些多余,例如在对方没有回复的情况下,依然连续多次“消息轰炸”(尤其是已经连续发送3条信息的情况,这很可能不合理,需要着重判断) -7. 这条消息是否使用了完全没必要的修辞 -8. 这条消息是否逻辑通顺 -9. 这条消息是否太过冗长了(通常私聊的每条消息长度在20字以内,除非特殊情况) -10. 在连续多次发送消息的情况下,这条消息是否衔接自然,会不会显得奇怪(例如连续两条消息中部分内容重叠) - -请以JSON格式输出,包含以下字段: -1. suitable: 是否合适 (true/false) -2. reason: 原因说明 -3. need_replan: 是否需要重新决策 (true/false),当你认为此时已经不适合发消息,需要规划其它行动时,设为true - -输出格式示例: -{{ - "suitable": true, - "reason": "回复符合要求,虽然有可能略微偏离目标,但是整体内容流畅得体", - "need_replan": false -}} - -注意:请严格按照JSON格式输出,不要包含任何其他内容。""" - - prompt = prompt_template.format( - current_time_str=current_time_str, # 使用传入的参数 - goal=goal, - chat_history_text=chat_history_text, - reply=reply - ) - - # 调用 LLM - content, _ = await self.llm.generate_response_async(prompt) - - try: - content, _ = await self.llm.generate_response_async(prompt) - logger.debug(f"[私聊][{self.private_name}]检查回复的原始返回: {content}") - - # 清理内容,尝试提取JSON部分 - content = content.strip() - try: - # 尝试直接解析 - result = json.loads(content) - except json.JSONDecodeError: - # 如果直接解析失败,尝试查找和提取JSON部分 - import re - - json_pattern = r"\{[^{}]*\}" - json_match = re.search(json_pattern, content) - if json_match: - try: - result = json.loads(json_match.group()) - except json.JSONDecodeError: - # 如果JSON解析失败,尝试从文本中提取结果 - is_suitable = "不合适" not in content.lower() and "违规" not in content.lower() - reason = content[:100] if content else "无法解析响应" - need_replan = "重新规划" in content.lower() or "目标不适合" in content.lower() - return is_suitable, reason, need_replan - else: - # 如果找不到JSON,从文本中判断 - is_suitable = "不合适" not in content.lower() and "违规" not in content.lower() - reason = content[:100] if content else "无法解析响应" - need_replan = "重新规划" in content.lower() or "目标不适合" in content.lower() - return is_suitable, reason, need_replan - - # 验证JSON字段 - suitable = result.get("suitable", None) - reason = result.get("reason", "未提供原因") - need_replan = result.get("need_replan", False) - - # 如果suitable字段是字符串,转换为布尔值 - if isinstance(suitable, str): - suitable = suitable.lower() == "true" - - # 如果suitable字段不存在或不是布尔值,从reason中判断 - if suitable is None: - suitable = "不合适" not in reason.lower() and "违规" not in reason.lower() - - # 如果不合适且未达到最大重试次数,返回需要重试 - if not suitable and retry_count < self.max_retries: - return False, reason, False - - # 如果不合适且已达到最大重试次数,返回需要重新规划 - if not suitable and retry_count >= self.max_retries: - return False, f"多次重试后仍不合适: {reason}", True - - return suitable, reason, need_replan - - except Exception as e: - logger.error(f"[私聊][{self.private_name}]检查回复时出错: {e}") - # 如果出错且已达到最大重试次数,建议重新规划 - if retry_count >= self.max_retries: - return False, "多次检查失败,建议重新规划", True - return False, f"检查过程出错,建议重试: {str(e)}", False + logger.error(f"[私聊][{self.private_name}] ReplyChecker 检查重复时出错: 类型={type(e)}, 值={e}") + logger.error(f"[私聊][{self.private_name}]{traceback.format_exc()}") + # 发生未知错误时,为安全起见,默认通过,并记录原因 + return (True, f"检查重复时发生内部错误: {str(e)}", False) \ No newline at end of file diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index fba8fbda..f278a9bd 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -215,19 +215,31 @@ class ReplyGenerator: last_content = getattr(conversation_info, "last_rejected_reply_content", None) if last_reason and last_content: - last_rejection_info_str = ( - f"\n------\n" - f"【重要提示:你上一次尝试回复时失败了,以下是详细信息】\n" - f"上次试图发送的消息内容: “{last_content}”\n" - f"失败原因: “{last_reason}”\n" - f"请根据【消息内容】和【失败原因】调整你的新回复,避免重复之前的错误。\n" - f"------\n" - ) - logger.info( - f"[私聊][{self.private_name}]检测到上次回复失败信息,将加入 Prompt:\n" - f" 内容: {last_content}\n" - f" 原因: {last_reason}" - ) + if last_reason == "机器人尝试发送重复消息": # 这是我们从 ReplyChecker 设置的特定原因 + last_rejection_info_str = ( + f"\n------\n" + f"【重要提示:你上一次尝试发送的消息 “{last_content}” 与你更早之前发送过的某条消息完全相同。这属于复读行为,请避免。】\n" + f"请根据此提示调整你的新回复,确保内容新颖,不要重复你已经说过的话。\n" + f"------\n" + ) + logger.info( + f"[私聊][{self.private_name}] (ReplyGenerator) 检测到自身复读,将加入特定警告到 Prompt:\n" + f" 内容: {last_content}" + ) + else: # 其他类型的拒绝原因,保持原有格式 + last_rejection_info_str = ( + f"\n------\n" + f"【重要提示:你上一次尝试回复时失败了,以下是详细信息】\n" + f"上次试图发送的消息内容: “{last_content}”\n" + f"失败原因: “{last_reason}”\n" + f"请根据【消息内容】和【失败原因】调整你的新回复,避免重复之前的错误。\n" + f"------\n" + ) + logger.info( + f"[私聊][{self.private_name}] (ReplyGenerator) 检测到上次回复失败信息,将加入 Prompt:\n" + f" 内容: {last_content}\n" + f" 原因: {last_reason}" + ) # 新增:构建刷屏警告信息 for PROMPT_SEND_NEW_MESSAGE spam_warning_message = "" @@ -324,12 +336,4 @@ class ReplyGenerator: else: return "抱歉,我现在有点混乱,让我重新思考一下..." - # check_reply 方法保持不变 - async def check_reply( - self, reply: str, goal: str, chat_history: List[Dict[str, Any]], chat_history_str: str, current_time_str: str, retry_count: int = 0 - ) -> Tuple[bool, str, bool]: - """检查回复是否合适 - (此方法逻辑保持不变, 但注意 current_time_str 参数的传递) - """ - # 确保 current_time_str 被正确传递给 reply_checker.check - return await self.reply_checker.check(reply, goal, chat_history, chat_history_str, current_time_str, retry_count) \ No newline at end of file + # check_reply 方法在 ReplyGenerator 中不再需要 \ No newline at end of file From f2245f60db0fbe2702b36009286a74aba0556a3c Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Wed, 7 May 2025 19:57:12 +0800 Subject: [PATCH 34/63] fix --- src/plugins/PFC/conversation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 8d16c04f..eb75eb1d 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -849,7 +849,7 @@ class Conversation: # 1. 处理需要生成、检查、发送的动作 if action in ["direct_reply", "send_new_message"]: - max_reply_attempts: int = global_config.get("pfc_max_reply_attempts", 3) # 最多尝试次数 (可配置) + max_reply_attempts: int = getattr(global_config, "pfc_max_reply_attempts", 3) # 最多尝试次数 (可配置) reply_attempt_count: int = 0 is_suitable: bool = False # 标记回复是否合适 generated_content: str = "" # 存储生成的回复 From e488d5038beb7a8f00c0a587c86cbbb5a139ccf9 Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Wed, 7 May 2025 20:26:00 +0800 Subject: [PATCH 35/63] fix --- src/plugins/PFC/conversation.py | 38 ++++++++++++++++++++++++++++++++ src/plugins/PFC/reply_checker.py | 19 ++++++++++++---- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index eb75eb1d..d57f0ac7 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -1051,6 +1051,44 @@ class Conversation: final_status = "done" # 明确设置 final_status final_reason = "成功发送" # 明确设置 final_reason logger.info(f"[私聊][{self.private_name}] 动作 '{action}': 成功发送回复.") + + if observation_info and self.bot_qq_str: + bot_message_dict = { + "message_id": f"bot_sent_{send_end_time}", # 生成一个唯一ID + "time": send_end_time, + "user_info": { # 构造机器人的 UserInfo + "user_id": self.bot_qq_str, + "user_nickname": global_config.BOT_NICKNAME, # 或者 self.name (如果 Conversation 类有) + "platform": self.chat_stream.platform if self.chat_stream else "unknown_platform" + }, + "processed_plain_text": self.generated_reply, + "detailed_plain_text": self.generated_reply, # 简单处理 + # 根据你的消息字典结构,可能还需要其他字段,如 message_type 等 + } + observation_info.chat_history.append(bot_message_dict) + observation_info.chat_history_count = len(observation_info.chat_history) + logger.debug(f"[私聊][{self.private_name}] 机器人发送的消息已添加到 chat_history。当前历史数: {observation_info.chat_history_count}") + + # 可选:如果 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) + + # 更新 chat_history_str (如果 ReplyChecker 也依赖这个字符串,尽管我们的修改是基于列表的) + # 这个更新可能比较消耗资源,如果 checker 只用列表,可以考虑优化此处 + 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: + logger.error(f"[私聊][{self.private_name}] 更新 chat_history_str 时出错: {e_build_hist}") + observation_info.chat_history_str = "[构建聊天记录出错]" + # --- 新增结束 --- + if self.idle_conversation_starter: await self.idle_conversation_starter.update_last_message_time(send_end_time) diff --git a/src/plugins/PFC/reply_checker.py b/src/plugins/PFC/reply_checker.py index a238a110..d86e64ac 100644 --- a/src/plugins/PFC/reply_checker.py +++ b/src/plugins/PFC/reply_checker.py @@ -47,9 +47,11 @@ class ReplyChecker: if len(reply) <= 4: return True, "消息长度小于等于4字符,跳过重复检查。", False + try: - for msg_dict in chat_history: + match_found = False # <--- 用于调试 + for i, msg_dict in enumerate(chat_history): # <--- 添加索引用于日志 if not isinstance(msg_dict, dict): continue @@ -59,16 +61,25 @@ class ReplyChecker: sender_id = str(user_info_data.get("user_id")) - # 只检查机器人自己发送过的历史消息 if sender_id == self.bot_qq_str: historical_message_text = msg_dict.get("processed_plain_text", "") + # <--- 新增详细对比日志 --- START ---> + logger.debug( + f"[私聊][{self.private_name}] ReplyChecker: 历史记录 #{i} (机器人): '{historical_message_text}' (长度 {len(historical_message_text)})" + ) if reply == historical_message_text: + logger.warning( + f"[私聊][{self.private_name}] ReplyChecker: !!! 精确匹配成功 !!!" + ) logger.warning( f"[私聊][{self.private_name}] ReplyChecker 检测到机器人自身重复消息: '{reply}'" ) - return (False, "机器人尝试发送重复消息", False) # is_suitable=False, reason, need_replan=False + match_found = True # <--- 标记找到 + return (False, "机器人尝试发送重复消息", False) + # <--- 新增详细对比日志 --- END ---> - # 如果循环结束都没有找到重复 + if not match_found: # <--- 根据标记判断 + logger.debug(f"[私聊][{self.private_name}] ReplyChecker: 未找到重复。") # <--- 新增日志 return (True, "消息内容未与机器人历史发言重复。", False) except Exception as e: From 162dc49acd76e3abeec86c38054a6310de80f4ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Wed, 7 May 2025 22:08:16 +0800 Subject: [PATCH 36/63] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E5=AD=90=E5=BF=83=E6=B5=81=E5=BE=AA=E7=8E=AF=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E5=92=8C=E6=89=80=E6=9C=89=E7=8A=B6=E6=80=81=E7=9A=84?= =?UTF-8?q?API=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/apiforgui.py | 17 ++++++++++ src/api/main.py | 28 +++++++++++++++-- src/heart_flow/heartflow.py | 18 +++++++++++ src/heart_flow/interest_logger.py | 52 +++++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 2 deletions(-) diff --git a/src/api/apiforgui.py b/src/api/apiforgui.py index a8027c48..1860aef7 100644 --- a/src/api/apiforgui.py +++ b/src/api/apiforgui.py @@ -17,3 +17,20 @@ async def forced_change_subheartflow_status(subheartflow_id: str, status: ChatSt if subheartflow: return await heartflow.force_change_subheartflow_status(subheartflow_id, status) return False + +async def get_subheartflow_cycle_info(subheartflow_id: str, history_len: int) -> dict: + """获取子心流的循环信息""" + subheartflow_cycle_info = await heartflow.api_get_subheartflow_cycle_info(subheartflow_id, history_len) + logger.debug(f"子心流 {subheartflow_id} 循环信息: {subheartflow_cycle_info}") + if subheartflow_cycle_info: + return subheartflow_cycle_info + else: + logger.warning(f"子心流 {subheartflow_id} 循环信息未找到") + return None + + +async def get_all_states(): + """获取所有状态""" + all_states = await heartflow.api_get_all_states() + logger.debug(f"所有状态: {all_states}") + return all_states diff --git a/src/api/main.py b/src/api/main.py index 1f47a57c..f5d299d8 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -2,13 +2,18 @@ from fastapi import APIRouter from strawberry.fastapi import GraphQLRouter import os import sys - +# from src.heart_flow.heartflow import heartflow sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) # from src.config.config import BotConfig from src.common.logger_manager import get_logger from src.api.reload_config import reload_config as reload_config_func from src.common.server import global_server -from src.api.apiforgui import get_all_subheartflow_ids, forced_change_subheartflow_status +from src.api.apiforgui import ( + get_all_subheartflow_ids, + forced_change_subheartflow_status, + get_subheartflow_cycle_info, + get_all_states, +) from src.heart_flow.sub_heartflow import ChatState # import uvicorn @@ -67,7 +72,26 @@ async def force_stop_maibot(): else: logger.error("MAI Bot强制停止失败") return {"status": "failed"} + +@router.get("/gui/subheartflow/cycleinfo") +async def get_subheartflow_cycle_info_api(subheartflow_id: str, history_len: int): + """获取子心流的循环信息""" + cycle_info = await get_subheartflow_cycle_info(subheartflow_id, history_len) + if cycle_info: + return {"status": "success", "data": cycle_info} + else: + logger.warning(f"子心流 {subheartflow_id} 循环信息未找到") + return {"status": "failed", "reason": "subheartflow not found"} +@router.get("/gui/get_all_states") +async def get_all_states_api(): + """获取所有状态""" + all_states = await get_all_states() + if all_states: + return {"status": "success", "data": all_states} + else: + logger.warning("获取所有状态失败") + return {"status": "failed", "reason": "failed to get all states"} def start_api_server(): """启动API服务器""" diff --git a/src/heart_flow/heartflow.py b/src/heart_flow/heartflow.py index 894247ce..dd58f5cd 100644 --- a/src/heart_flow/heartflow.py +++ b/src/heart_flow/heartflow.py @@ -66,6 +66,24 @@ class Heartflow: """强制改变子心流的状态""" # 这里的 message 是可选的,可能是一个消息对象,也可能是其他类型的数据 return await self.subheartflow_manager.force_change_state(subheartflow_id, status) + + async def api_get_all_states(self): + """获取所有状态""" + return await self.interest_logger.api_get_all_states() + + + async def api_get_subheartflow_cycle_info(self, subheartflow_id: str, history_len: int) -> Optional[dict]: + """获取子心流的循环信息""" + subheartflow = await self.subheartflow_manager.get_or_create_subheartflow(subheartflow_id) + if not subheartflow: + logger.warning(f"尝试获取不存在的子心流 {subheartflow_id} 的周期信息") + return None + heartfc_instance = subheartflow.heart_fc_instance + if not heartfc_instance: + logger.warning(f"子心流 {subheartflow_id} 没有心流实例,无法获取周期信息") + return None + + return heartfc_instance.get_cycle_history(last_n=history_len) async def heartflow_start_working(self): """启动后台任务""" diff --git a/src/heart_flow/interest_logger.py b/src/heart_flow/interest_logger.py index 1fe289b8..9b562156 100644 --- a/src/heart_flow/interest_logger.py +++ b/src/heart_flow/interest_logger.py @@ -158,3 +158,55 @@ class InterestLogger: except Exception as e: logger.error(f"记录状态时发生意外错误: {e}") logger.error(traceback.format_exc()) + + async def api_get_all_states(self): + """获取主心流和所有子心流的状态。""" + try: + current_timestamp = time.time() + + # main_mind = self.heartflow.current_mind + # 获取 Mai 状态名称 + mai_state_name = self.heartflow.current_state.get_current_state().name + + all_subflow_states = await self.get_all_subflow_states() + + log_entry_base = { + "timestamp": round(current_timestamp, 2), + # "main_mind": main_mind, + "mai_state": mai_state_name, + "subflow_count": len(all_subflow_states), + "subflows": [], + } + + subflow_details = [] + items_snapshot = list(all_subflow_states.items()) + for stream_id, state in items_snapshot: + group_name = stream_id + try: + chat_stream = chat_manager.get_stream(stream_id) + if chat_stream: + if chat_stream.group_info: + group_name = chat_stream.group_info.group_name + elif chat_stream.user_info: + group_name = f"私聊_{chat_stream.user_info.user_nickname}" + except Exception as e: + logger.trace(f"无法获取 stream_id {stream_id} 的群组名: {e}") + + interest_state = state.get("interest_state", {}) + + subflow_entry = { + "stream_id": stream_id, + "group_name": group_name, + "sub_mind": state.get("current_mind", "未知"), + "sub_chat_state": state.get("chat_state", "未知"), + "interest_level": interest_state.get("interest_level", 0.0), + "start_hfc_probability": interest_state.get("start_hfc_probability", 0.0), + # "is_above_threshold": interest_state.get("is_above_threshold", False), + } + subflow_details.append(subflow_entry) + + log_entry_base["subflows"] = subflow_details + return subflow_details + except Exception as e: + logger.error(f"记录状态时发生意外错误: {e}") + logger.error(traceback.format_exc()) \ No newline at end of file From 61f6bf3e7e277b9855a741bbfab2e9ada3b4e111 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 7 May 2025 14:08:29 +0000 Subject: [PATCH 37/63] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=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/api/apiforgui.py | 5 +++-- src/api/main.py | 8 ++++++-- src/heart_flow/heartflow.py | 5 ++--- src/heart_flow/interest_logger.py | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/api/apiforgui.py b/src/api/apiforgui.py index 1860aef7..a266f8e8 100644 --- a/src/api/apiforgui.py +++ b/src/api/apiforgui.py @@ -18,16 +18,17 @@ async def forced_change_subheartflow_status(subheartflow_id: str, status: ChatSt return await heartflow.force_change_subheartflow_status(subheartflow_id, status) return False + async def get_subheartflow_cycle_info(subheartflow_id: str, history_len: int) -> dict: """获取子心流的循环信息""" - subheartflow_cycle_info = await heartflow.api_get_subheartflow_cycle_info(subheartflow_id, history_len) + subheartflow_cycle_info = await heartflow.api_get_subheartflow_cycle_info(subheartflow_id, history_len) logger.debug(f"子心流 {subheartflow_id} 循环信息: {subheartflow_cycle_info}") if subheartflow_cycle_info: return subheartflow_cycle_info else: logger.warning(f"子心流 {subheartflow_id} 循环信息未找到") return None - + async def get_all_states(): """获取所有状态""" diff --git a/src/api/main.py b/src/api/main.py index f5d299d8..4378ff1e 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -2,6 +2,7 @@ from fastapi import APIRouter from strawberry.fastapi import GraphQLRouter import os import sys + # from src.heart_flow.heartflow import heartflow sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) # from src.config.config import BotConfig @@ -9,7 +10,7 @@ from src.common.logger_manager import get_logger from src.api.reload_config import reload_config as reload_config_func from src.common.server import global_server from src.api.apiforgui import ( - get_all_subheartflow_ids, + get_all_subheartflow_ids, forced_change_subheartflow_status, get_subheartflow_cycle_info, get_all_states, @@ -72,7 +73,8 @@ async def force_stop_maibot(): else: logger.error("MAI Bot强制停止失败") return {"status": "failed"} - + + @router.get("/gui/subheartflow/cycleinfo") async def get_subheartflow_cycle_info_api(subheartflow_id: str, history_len: int): """获取子心流的循环信息""" @@ -83,6 +85,7 @@ async def get_subheartflow_cycle_info_api(subheartflow_id: str, history_len: int logger.warning(f"子心流 {subheartflow_id} 循环信息未找到") return {"status": "failed", "reason": "subheartflow not found"} + @router.get("/gui/get_all_states") async def get_all_states_api(): """获取所有状态""" @@ -93,6 +96,7 @@ async def get_all_states_api(): logger.warning("获取所有状态失败") return {"status": "failed", "reason": "failed to get all states"} + def start_api_server(): """启动API服务器""" global_server.register_router(router, prefix="/api/v1") diff --git a/src/heart_flow/heartflow.py b/src/heart_flow/heartflow.py index dd58f5cd..2cf7d365 100644 --- a/src/heart_flow/heartflow.py +++ b/src/heart_flow/heartflow.py @@ -66,12 +66,11 @@ class Heartflow: """强制改变子心流的状态""" # 这里的 message 是可选的,可能是一个消息对象,也可能是其他类型的数据 return await self.subheartflow_manager.force_change_state(subheartflow_id, status) - + async def api_get_all_states(self): """获取所有状态""" return await self.interest_logger.api_get_all_states() - async def api_get_subheartflow_cycle_info(self, subheartflow_id: str, history_len: int) -> Optional[dict]: """获取子心流的循环信息""" subheartflow = await self.subheartflow_manager.get_or_create_subheartflow(subheartflow_id) @@ -82,7 +81,7 @@ class Heartflow: if not heartfc_instance: logger.warning(f"子心流 {subheartflow_id} 没有心流实例,无法获取周期信息") return None - + return heartfc_instance.get_cycle_history(last_n=history_len) async def heartflow_start_working(self): diff --git a/src/heart_flow/interest_logger.py b/src/heart_flow/interest_logger.py index 9b562156..fb33a6f6 100644 --- a/src/heart_flow/interest_logger.py +++ b/src/heart_flow/interest_logger.py @@ -209,4 +209,4 @@ class InterestLogger: return subflow_details except Exception as e: logger.error(f"记录状态时发生意外错误: {e}") - logger.error(traceback.format_exc()) \ No newline at end of file + logger.error(traceback.format_exc()) From 920d5bcfc3597245e3552ac607b8a5f0e417d41b Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Thu, 8 May 2025 00:56:06 +0800 Subject: [PATCH 38/63] =?UTF-8?q?=E6=9E=81=E9=99=90=E6=8B=86=E5=88=86?= =?UTF-8?q?=EF=BC=88=E5=B7=B2=E7=BB=8F=E8=BF=87=E6=B5=8B=E8=AF=95=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/actions.py | 706 +++++++++ src/plugins/PFC/conversation.py | 1426 +------------------ src/plugins/PFC/conversation_initializer.py | 279 ++++ src/plugins/PFC/conversation_loop.py | 236 +++ src/plugins/PFC/pfc_manager.py | 107 +- 5 files changed, 1319 insertions(+), 1435 deletions(-) create mode 100644 src/plugins/PFC/actions.py create mode 100644 src/plugins/PFC/conversation_initializer.py create mode 100644 src/plugins/PFC/conversation_loop.py diff --git a/src/plugins/PFC/actions.py b/src/plugins/PFC/actions.py new file mode 100644 index 00000000..386cd5f6 --- /dev/null +++ b/src/plugins/PFC/actions.py @@ -0,0 +1,706 @@ +import time +import asyncio +import datetime +import traceback +import json +from typing import Dict, Any, Optional, Set, TYPE_CHECKING + +from src.common.logger_manager import get_logger +from src.config.config import global_config +from src.plugins.utils.chat_message_builder import build_readable_messages +from .pfc_types import ConversationState +from .observation_info import ObservationInfo +from .conversation_info import ConversationInfo + +if TYPE_CHECKING: + from .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 # 返回失败 + +async def handle_action( + conversation_instance: 'Conversation', action: str, reason: str, + observation_info: Optional[ObservationInfo], + conversation_info: Optional[ConversationInfo] +): + """ + 处理由 ActionPlanner 规划出的具体行动。 + 这之前是 Conversation 类中的 _handle_action 方法。 + """ + # 检查初始化状态 + if not conversation_instance._initialized: + logger.error(f"[私聊][{conversation_instance.private_name}] 尝试在未初始化状态下处理动作 '{action}'。") + return + + # 确保 observation_info 和 conversation_info 不为 None + if not observation_info: + logger.error(f"[私聊][{conversation_instance.private_name}] ObservationInfo 为空,无法处理动作 '{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 + return + if not conversation_info: # conversation_info 在这里是必需的 + logger.error(f"[私聊][{conversation_instance.private_name}] ConversationInfo 为空,无法处理动作 '{action}'。") + conversation_instance.state = ConversationState.ERROR + return + + + logger.info(f"[私聊][{conversation_instance.private_name}] 开始处理动作: {action}, 原因: {reason}") + 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 中设置 + } + # 安全地添加到历史记录列表 + 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 + + # --- 初始化动作执行状态变量 --- + action_successful: bool = 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 + + + 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.info(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.info(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.info(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}] 机器人发送的消息已添加到 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 = "[构建聊天记录出错]" + # --- 新增结束 --- + + # 更新空闲对话启动器的时间 + if conversation_instance.idle_conversation_starter: + await conversation_instance.idle_conversation_starter.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.info( + 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.info( + 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}] 实例消息计数(机器人发送后)增加到: {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}),未能生成/检查通过合适的回复。最终原因: {check_reason}" + ) + final_status = "recall" # 标记为 recall + final_reason = f"尝试{max_reply_attempts}次后失败: {check_reason}" + action_successful = False # 确保 action_successful 为 False + # 重置追问状态 + 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}] 成功发送告别语,即将停止对话实例。") + # 更新空闲计时器 + if conversation_instance.idle_conversation_starter: + await conversation_instance.idle_conversation_starter.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 + ) + + # 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}" + + # --- 重置非回复动作的追问状态 --- + # 确保执行完非回复动作后,下一次规划不会错误地进入追问逻辑 + if action not in ["direct_reply", "send_new_message", "say_goodbye"]: + conversation_info.last_successful_reply_action = None + # 清理可能残留的拒绝信息 + conversation_info.last_reply_rejection_reason = None + conversation_info.last_rejected_reply_content = None + + except asyncio.CancelledError: + # 处理任务被取消的异常 + logger.warning(f"[私聊][{conversation_instance.private_name}] 处理动作 '{action}' 时被取消。") + final_status = "cancelled" + final_reason = "动作处理被取消" + # 取消时也重置追问状态 + if conversation_info : # 确保 conversation_info 存在 + conversation_info.last_successful_reply_action = None + raise # 重新抛出 CancelledError,让上层知道任务被取消 + 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_reason = f"处理动作时出错: {handle_err}" + conversation_instance.state = ConversationState.ERROR # 设置对话状态为错误 + # 出错时重置追问状态 + if conversation_info: # 确保 conversation_info 存在 + conversation_info.last_successful_reply_action = None + + finally: + # --- 无论成功与否,都执行 --- + + # 1. 重置临时存储的计数值 + if conversation_info: # 确保 conversation_info 存在 + conversation_info.other_new_messages_during_planning_count = 0 + + # 2. 更新动作历史记录的最终状态和原因 + # 优化:如果动作成功但状态仍是默认的 recall,则更新为 done + if action_successful: + # 如果动作标记为成功,但 final_status 仍然是初始的 "recall" 或者 "start" + # (因为可能在try块中成功执行了但没有显式更新 final_status 为 "done") + # 或者是 "done_no_reply" 这种特殊的成功状态 + if final_status in ["recall", "start"] and action != "send_new_message": # send_new_message + no_reply 是特殊成功 + final_status = "done" + if not final_reason or final_reason == "动作未成功执行": # 避免覆盖已有的具体成功原因 + # 为不同类型的成功动作提供更具体的默认成功原因 + if action == "wait": + # 检查 conversation_info.goal_list 是否存在且不为空 + timeout_occurred = ( + any("分钟," in g.get("goal", "") for g in conversation_info.goal_list if isinstance(g, dict)) + if conversation_info and conversation_info.goal_list + else False + ) + final_reason = "等待完成" + (" (超时)" if timeout_occurred else " (收到新消息或中断)") + elif action == "listening": + final_reason = "进入倾听状态" + elif action in ["rethink_goal", "end_conversation", "block_and_ignore", "say_goodbye"]: + final_reason = f"成功执行 {action}" + elif action in ["direct_reply", "send_new_message"]: # 正常发送成功的case + final_reason = "成功发送" + else: + final_reason = f"动作 {action} 成功完成" + # 如果已经是 "done" 或 "done_no_reply",则保留它们和它们对应的 final_reason + + else: # action_successful is False + # 如果动作标记为失败,且 final_status 还是 "recall" (初始值) 或 "start" + if final_status in ["recall", "start"]: + # 尝试从 conversation_info 中获取更具体的失败原因(例如 checker 的原因) + # 这个 specific_rejection_reason 是在 try 块中被设置的 + specific_rejection_reason = getattr(conversation_info, 'last_reply_rejection_reason', None) + rejected_content = getattr(conversation_info, 'last_rejected_reply_content', None) + + if specific_rejection_reason: # 如果有更具体的原因 + final_reason = f"执行失败: {specific_rejection_reason}" + if rejected_content and specific_rejection_reason == "机器人尝试发送重复消息": # 对复读提供更清晰的日志 + final_reason += f" (内容: '{rejected_content[:30]}...')" + elif not final_reason or final_reason == "动作未成功执行": # 如果没有更具体的原因,且当前原因还是默认的 + final_reason = f"动作 {action} 执行失败或被意外中止" + # 如果 final_status 已经是 "error" 或 "cancelled",则保留它们和它们对应的 final_reason + + # 更新 done_action 中的记录 + # 防御性检查,确保 conversation_info, done_action 存在,并且索引有效 + if conversation_info and hasattr(conversation_info, 'done_action') and \ + conversation_info.done_action and action_index < len(conversation_info.done_action): + 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), + } + ) + else: + logger.error(f"[私聊][{conversation_instance.private_name}] 无法更新动作历史记录,索引 {action_index} 无效或列表为空。") + + # 最终日志输出 + log_final_reason = final_reason if final_reason else "无明确原因" + # 为成功发送的动作添加发送内容摘要 + if final_status == "done" and action_successful and \ + action in ["direct_reply", "send_new_message"] and \ + hasattr(conversation_instance, 'generated_reply') and conversation_instance.generated_reply: + log_final_reason += f" (发送内容: '{conversation_instance.generated_reply[:30]}...')" + + logger.info(f"[私聊][{conversation_instance.private_name}] 动作 '{action}' 处理完成。最终状态: {final_status}, 原因: {log_final_reason}") \ No newline at end of file diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index d57f0ac7..74212dc7 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -2,15 +2,15 @@ import time import asyncio import datetime import traceback -import json # 确保导入 json 模块 +import json from typing import Dict, Any, Optional, Set, List from dateutil import tz from src.common.logger_manager import get_logger -from src.plugins.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat +from src.plugins.utils.chat_message_builder import build_readable_messages # 移除了 get_raw_msg_before_timestamp_with_chat from maim_message import UserInfo from src.plugins.chat.chat_stream import chat_manager, ChatStream -from ..chat.message import Message +from ..chat.message import Message # 假设 Message 类型被 _convert_to_message 使用 from src.config.config import global_config from ..person_info.person_info import person_info_manager from ..person_info.relationship_manager import relationship_manager @@ -31,55 +31,49 @@ from .reply_generator import ReplyGenerator from .idle_conversation_starter import IdleConversationStarter from .pfc_KnowledgeFetcher import KnowledgeFetcher from .waiter import Waiter -from .pfc_utils import get_person_id from .reply_checker import ReplyChecker +from . import actions + +# >>> 新增导入 <<< +from .conversation_loop import run_conversation_loop # 导入新的循环函数 -# 导入富文本回溯,用于更好的错误展示 from rich.traceback import install - install(extra_lines=3) -# 获取当前模块的日志记录器 logger = get_logger("pfc_conversation") -# 确保 global_config.TIME_ZONE 存在且有效,否则使用默认值 -configured_tz = getattr(global_config, 'TIME_ZONE', 'Asia/Shanghai') # 使用 getattr 安全访问 -TIME_ZONE = tz.gettz(configured_tz) -if TIME_ZONE is None: # 如果 gettz 返回 None,说明时区字符串无效 - logger.error(f"配置的时区 '{configured_tz}' 无效,将使用默认时区 'Asia/Shanghai'") - TIME_ZONE = tz.gettz('Asia/Shanghai') +# 时区配置移到 loop 文件或更全局的位置,这里不再需要 +# configured_tz = getattr(global_config, 'TIME_ZONE', 'Asia/Shanghai') +# TIME_ZONE = tz.gettz(configured_tz) +# if TIME_ZONE is None: +# logger.error(f"配置的时区 '{configured_tz}' 无效,将使用默认时区 'Asia/Shanghai'") +# TIME_ZONE = tz.gettz('Asia/Shanghai') class Conversation: """ 对话类,负责管理单个私聊对话的状态和核心逻辑流程。 - 包含对话的初始化、启动、停止、规划循环以及动作处理。 """ def __init__(self, stream_id: str, private_name: str): """ - 初始化对话实例。 - - Args: - stream_id (str): 唯一的聊天流 ID。 - private_name (str): 私聊对象的名称,用于日志和区分。 + 初始化对话实例的基本属性。 + 核心组件的实例化将由 PFCManager 通过调用 conversation_initializer 中的函数完成。 """ self.stream_id: str = stream_id self.private_name: str = private_name - self.state: ConversationState = ConversationState.INIT # 对话的初始状态 - self.should_continue: bool = False # 标记对话循环是否应该继续运行 - self.ignore_until_timestamp: Optional[float] = None # 如果设置了,忽略此时间戳之前的活动 - self.generated_reply: str = "" # 存储最近生成的回复内容 - self.chat_stream: Optional[ChatStream] = None # 关联的聊天流对象 + self.state: ConversationState = ConversationState.INIT + self.should_continue: bool = False # Manager 会在初始化后设置 + self.ignore_until_timestamp: Optional[float] = None + self.generated_reply: str = "" + self.chat_stream: Optional[ChatStream] = None - # --- 新增:初始化管理器实例 --- self.person_info_mng = person_info_manager self.relationship_mng = relationship_manager - self.mood_mng = MoodManager.get_instance() # MoodManager 是单例 + self.mood_mng = MoodManager.get_instance() - # 初始化所有核心组件为 None,将在 _initialize 中创建 - self.relationship_updater: Optional[PfcRelationshipUpdater] = None # 新增 + self.relationship_updater: Optional[PfcRelationshipUpdater] = None self.relationship_translator: Optional[PfcRepationshipTranslator] = None - self.emotion_updater: Optional[PfcEmotionUpdater] = None # 新增 + self.emotion_updater: Optional[PfcEmotionUpdater] = None self.action_planner: Optional[ActionPlanner] = None self.goal_analyzer: Optional[GoalAnalyzer] = None self.reply_generator: Optional[ReplyGenerator] = None @@ -90,309 +84,49 @@ class Conversation: self.chat_observer: Optional[ChatObserver] = None self.observation_info: Optional[ObservationInfo] = None self.conversation_info: Optional[ConversationInfo] = None - self.reply_checker: Optional[ReplyChecker] = None # 回复检查器 + self.reply_checker: Optional[ReplyChecker] = None - # 内部状态标志 - self._initializing: bool = False # 标记是否正在初始化,防止并发问题 - self._initialized: bool = False # 标记是否已成功初始化 + self._initialized: bool = False # Manager 会在初始化成功后设为 True - # 缓存机器人自己的 QQ 号字符串,避免重复转换 self.bot_qq_str: Optional[str] = str(global_config.BOT_QQ) if global_config.BOT_QQ else None if not self.bot_qq_str: - # 这是一个严重问题,记录错误 - logger.error(f"[私聊][{self.private_name}] 严重错误:未能从配置中获取 BOT_QQ ID!PFC 可能无法正常工作。") + logger.error(f"[私聊][{self.private_name}] 严重错误:未能从配置中获取 BOT_QQ ID!") - async def _initialize(self): - """ - 异步初始化对话实例及其所有依赖的核心组件。 - 这是一个关键步骤,确保所有部分都准备就绪才能开始对话循环。 - """ - # 防止重复初始化 - if self._initialized or self._initializing: - logger.warning(f"[私聊][{self.private_name}] 尝试重复初始化或正在初始化中。") - return - - self._initializing = True # 标记开始初始化 - logger.info(f"[私聊][{self.private_name}] 开始初始化对话实例: {self.stream_id}") - - try: - # 1. 初始化核心功能组件 - logger.debug(f"[私聊][{self.private_name}] 初始化 ActionPlanner...") - self.action_planner = ActionPlanner(self.stream_id, self.private_name) - - self.relationship_updater = PfcRelationshipUpdater( - private_name=self.private_name, - bot_name=global_config.BOT_NICKNAME # 或者 self.name (如果 Conversation 类有 self.name) - ) - self.relationship_translator = PfcRepationshipTranslator(private_name=self.private_name) - logger.info(f"[私聊][{self.private_name}] PfcRelationship 初始化完成。") - - self.emotion_updater = PfcEmotionUpdater( - private_name=self.private_name, - bot_name=global_config.BOT_NICKNAME # 或者 self.name - ) - logger.info(f"[私聊][{self.private_name}] PfcEmotion 初始化完成。") - - logger.debug(f"[私聊][{self.private_name}] 初始化 GoalAnalyzer...") - self.goal_analyzer = GoalAnalyzer(self.stream_id, self.private_name) - - logger.debug(f"[私聊][{self.private_name}] 初始化 ReplyGenerator...") - self.reply_generator = ReplyGenerator(self.stream_id, self.private_name) - - logger.debug(f"[私聊][{self.private_name}] 初始化 KnowledgeFetcher...") - self.knowledge_fetcher = KnowledgeFetcher(self.private_name) - - logger.debug(f"[私聊][{self.private_name}] 初始化 Waiter...") - self.waiter = Waiter(self.stream_id, self.private_name) - - logger.debug(f"[私聊][{self.private_name}] 初始化 DirectMessageSender...") - self.direct_sender = DirectMessageSender(self.private_name) - - logger.debug(f"[私聊][{self.private_name}] 初始化 ReplyChecker...") - self.reply_checker = ReplyChecker(self.stream_id, self.private_name) - - # 获取关联的 ChatStream - logger.debug(f"[私聊][{self.private_name}] 获取 ChatStream...") - self.chat_stream = chat_manager.get_stream(self.stream_id) - if not self.chat_stream: - # 获取不到 ChatStream 是一个严重问题,因为无法发送消息 - logger.error( - f"[私聊][{self.private_name}] 初始化错误:无法从 chat_manager 获取 stream_id {self.stream_id} 的 ChatStream。" - ) - raise ValueError(f"无法获取 stream_id {self.stream_id} 的 ChatStream") - - # 初始化空闲对话启动器 - logger.debug(f"[私聊][{self.private_name}] 初始化 IdleConversationStarter...") - self.idle_conversation_starter = IdleConversationStarter(self.stream_id, self.private_name) - - # 2. 初始化信息存储和观察组件 - logger.debug(f"[私聊][{self.private_name}] 获取 ChatObserver 实例...") - self.chat_observer = ChatObserver.get_instance(self.stream_id, self.private_name) - - logger.debug(f"[私聊][{self.private_name}] 初始化 ObservationInfo...") - self.observation_info = ObservationInfo(self.private_name) - # 确保 ObservationInfo 知道机器人的 ID - if not self.observation_info.bot_id: - logger.warning(f"[私聊][{self.private_name}] ObservationInfo 未能自动获取 bot_id,尝试手动设置。") - self.observation_info.bot_id = self.bot_qq_str - - logger.debug(f"[私聊][{self.private_name}] 初始化 ConversationInfo...") - self.conversation_info = ConversationInfo() - - # 3. 绑定观察者和信息处理器 - logger.debug(f"[私聊][{self.private_name}] 绑定 ObservationInfo 到 ChatObserver...") - self.observation_info.bind_to_chat_observer(self.chat_observer) - - # 4. 加载初始聊天记录 - await self._load_initial_history() - - # 4.1 加载用户数据 - # 尝试从 observation_info 获取,这依赖于 _load_initial_history 的实现 - self.conversation_info.person_id, private_platform_str, private_user_id_str = await get_person_id( - private_name=self.private_name, - chat_stream=self.chat_stream, - ) - - logger.info(f"[私聊][{self.private_name}] 获取到 person_id: {self.conversation_info.person_id} for {private_platform_str}:{private_user_id_str}") - - - # 5. 启动需要后台运行的组件 - logger.debug(f"[私聊][{self.private_name}] 启动 ChatObserver...") - self.chat_observer.start() - if self.idle_conversation_starter: - logger.debug(f"[私聊][{self.private_name}] 启动 IdleConversationStarter...") - self.idle_conversation_starter.start() - logger.info(f"[私聊][{self.private_name}] 空闲对话检测器已启动") - - # 5.1 启动 MoodManager 的后台更新 - if self.mood_mng and hasattr(self.mood_mng, 'start_mood_update') and not self.mood_mng._running: - self.mood_mng.start_mood_update(update_interval=global_config.mood_update_interval) # 使用配置的更新间隔 - logger.info(f"[私聊][{self.private_name}] MoodManager 已启动后台更新,间隔: {global_config.mood_update_interval} 秒。") - elif self.mood_mng and self.mood_mng._running: - logger.info(f"[私聊][{self.private_name}] MoodManager 已在运行中。") - else: - logger.warning(f"[私聊][{self.private_name}] MoodManager 未能启动,相关功能可能受限。") - - # --- 在初始化完成前,尝试加载一次关系和情绪信息 --- - if self.conversation_info and self.conversation_info.person_id: - try: - # 1. 获取数值型关系值 - numeric_relationship_value = await self.person_info_mng.get_value( - self.conversation_info.person_id, - "relationship_value" - ) - # 确保是浮点数 - if not isinstance(numeric_relationship_value, (int, float)): - from bson.decimal128 import Decimal128 - if isinstance(numeric_relationship_value, Decimal128): - numeric_relationship_value = float(numeric_relationship_value.to_decimal()) - else: - numeric_relationship_value = 0.0 - - # 2. 使用PFC内部翻译函数 - self.conversation_info.relationship_text = await self.relationship_translator.translate_relationship_value_to_text(numeric_relationship_value) - logger.info(f"[私聊][{self.private_name}] 初始化时加载关系文本: {self.conversation_info.relationship_text}") - except Exception as e_init_rel: - logger.error(f"[私聊][{self.private_name}] 初始化时加载关系文本出错: {e_init_rel}") - self.conversation_info.relationship_text = "你们的关系是:普通。" # 保留默认值 - if self.conversation_info and self.mood_mng: - try: - self.conversation_info.current_emotion_text = self.mood_mng.get_prompt() - logger.info(f"[私聊][{self.private_name}] 初始化时加载情绪文本: {self.conversation_info.current_emotion_text}") - except Exception as e_init_emo: - logger.error(f"[私聊][{self.private_name}] 初始化时加载情绪文本出错: {e_init_emo}") - # 保留默认值 - - - # 6. 标记初始化成功并设置运行状态 - self._initialized = True - self.should_continue = True # 初始化成功,标记可以继续运行循环 - self.state = ConversationState.ANALYZING # 设置初始状态为分析 - - logger.info(f"[私聊][{self.private_name}] 对话实例 {self.stream_id} 初始化完成。") - - except Exception as e: - # 捕获初始化过程中的任何异常 - logger.error(f"[私聊][{self.private_name}] 初始化对话实例失败: {e}") - logger.error(f"[私聊][{self.private_name}] {traceback.format_exc()}") - self.should_continue = False # 初始化失败,标记不能继续 - self._initialized = False # 确保标记为未初始化 - # 尝试停止可能部分启动的组件 - await self.stop() - raise # 将异常重新抛出,通知调用者初始化失败 - finally: - # 无论成功与否,都要清除正在初始化的标记 - self._initializing = False - - async def _load_initial_history(self): - """加载并处理初始的聊天记录""" - if not self.observation_info: - logger.warning(f"[私聊][{self.private_name}] ObservationInfo 未初始化,无法加载历史记录。") - return - - try: - logger.info(f"[私聊][{self.private_name}] 为 {self.stream_id} 加载初始聊天记录...") - # 从聊天核心获取原始消息列表 - initial_messages = get_raw_msg_before_timestamp_with_chat( - chat_id=self.stream_id, - timestamp=time.time(), - limit=30, # limit 可以根据需要调整或配置 - ) - - if initial_messages: - # 更新 ObservationInfo 中的历史记录列表和计数 - self.observation_info.chat_history = initial_messages - self.observation_info.chat_history_count = len(initial_messages) - - # 获取最后一条消息的信息 - last_msg = initial_messages[-1] - self.observation_info.last_message_time = last_msg.get("time") - self.observation_info.last_message_id = last_msg.get("message_id") - - # 安全地解析最后一条消息的发送者信息 - last_user_info_dict = last_msg.get("user_info", {}) - if isinstance(last_user_info_dict, dict): - try: - last_user_info = UserInfo.from_dict(last_user_info_dict) - # 存储发送者的 user_id 字符串 - self.observation_info.last_message_sender = ( - str(last_user_info.user_id) if last_user_info else None - ) - except Exception as e: - logger.warning(f"[私聊][{self.private_name}] 解析最后一条消息的用户信息时出错: {e}") - self.observation_info.last_message_sender = None - else: - # 如果 user_info 不是字典,也标记为未知 - self.observation_info.last_message_sender = None - - # 存储最后一条消息的文本内容 - self.observation_info.last_message_content = last_msg.get("processed_plain_text", "") - - # 构建用于 Prompt 的历史记录字符串 (只使用最近的一部分) - history_slice_for_str = initial_messages[-30:] # 可配置 - self.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, # read_mark 可能需要根据实际情况调整 - ) - - # 更新 ChatObserver 和 IdleStarter 的时间戳 - if self.chat_observer: - # 更新观察者的最后消息时间,避免重复处理这些初始消息 - self.chat_observer.last_message_time = self.observation_info.last_message_time - if self.idle_conversation_starter and self.observation_info.last_message_time: - # 更新空闲计时器的起始时间 - await self.idle_conversation_starter.update_last_message_time( - self.observation_info.last_message_time - ) - - logger.info( - f"[私聊][{self.private_name}] 成功加载 {len(initial_messages)} 条初始聊天记录。最后一条消息时间: {self.observation_info.last_message_time}" - ) - else: - # 如果没有历史记录 - logger.info(f"[私聊][{self.private_name}] 没有找到初始聊天记录。") - self.observation_info.chat_history_str = "还没有聊天记录。" # 设置默认提示 - - except Exception as load_err: - # 捕获加载过程中的异常 - logger.error(f"[私聊][{self.private_name}] 加载初始聊天记录时出错: {load_err}") - # 即使出错,也设置一个提示,避免后续使用 None 值 - if self.observation_info: - self.observation_info.chat_history_str = "[加载聊天记录出错]" + # _initialize 和 _load_initial_history 方法已被移除 async def start(self): """ - 启动对话流程。 - 会检查实例是否已初始化,如果未初始化会尝试初始化。 - 成功后,创建并启动核心的规划与行动循环 (`_plan_and_action_loop`)。 + 启动对话流程。创建并启动核心的规划与行动循环 (`run_conversation_loop`)。 """ - # 检查是否已初始化,如果未初始化则尝试进行初始化 if not self._initialized: - logger.warning(f"[私聊][{self.private_name}] 对话实例未初始化,尝试初始化...") - try: - await self._initialize() - # 在尝试初始化后,再次检查状态 - if not self._initialized: - logger.error(f"[私聊][{self.private_name}] 初始化失败,无法启动规划循环。") - return # 初始化失败,明确停止 - except Exception as init_err: - logger.error(f"[私聊][{self.private_name}] 初始化过程中发生未捕获错误: {init_err},无法启动。") - return # 初始化异常,明确停止 + logger.error(f"[私聊][{self.private_name}] 对话实例未被 Manager 正确初始化,无法启动规划循环。") + return - # 再次检查 should_continue 标志,确保初始化成功且未被外部停止 if not self.should_continue: logger.warning( - f"[私聊][{self.private_name}] 对话实例已被标记为不应继续 (可能由于初始化失败或已被停止),无法启动规划循环。" + f"[私聊][{self.private_name}] 对话实例已被 Manager 标记为不应继续,无法启动规划循环。" ) return logger.info(f"[私聊][{self.private_name}] 对话系统启动,准备创建规划循环任务...") - # 使用 asyncio.create_task 在后台启动主循环 try: - logger.debug(f"[私聊][{self.private_name}] 正在创建 _plan_and_action_loop 任务...") - # 创建任务,但不等待其完成,让它在后台运行 - _loop_task = asyncio.create_task(self._plan_and_action_loop()) - # 可以选择性地添加完成回调来处理任务结束或异常 - # loop_task.add_done_callback(self._handle_loop_completion) + # >>> 修改后的调用 <<< + _loop_task = asyncio.create_task(run_conversation_loop(self)) logger.info(f"[私聊][{self.private_name}] 规划循环任务已创建。") except Exception as task_err: logger.error(f"[私聊][{self.private_name}] 创建规划循环任务时出错: {task_err}") - # 如果创建任务失败,可能需要停止实例 await self.stop() async def stop(self): """ 停止对话实例并清理相关资源。 - 会停止后台任务、解绑观察者等。 """ logger.info(f"[私聊][{self.private_name}] 正在停止对话实例: {self.stream_id}") - self.should_continue = False # 设置标志,让主循环退出 + self.should_continue = False - # --- 新增:在对话结束时调用最终关系更新 --- - if self._initialized and self.relationship_updater and self.conversation_info and self.observation_info and self.chat_observer: + # 最终关系评估 + if self._initialized and self.relationship_updater and self.conversation_info and \ + self.observation_info and self.chat_observer: try: logger.info(f"[私聊][{self.private_name}] 准备执行最终关系评估...") await self.relationship_updater.update_relationship_final( @@ -405,1103 +139,51 @@ class Conversation: logger.error(f"[私聊][{self.private_name}] 调用最终关系评估时出错: {e_final_rel}") logger.error(traceback.format_exc()) else: - logger.warning(f"[私聊][{self.private_name}] 跳过最终关系评估,因为实例未完全初始化或缺少必要组件。") + logger.warning(f"[私聊][{self.private_name}] 跳过最终关系评估,实例未完全初始化或缺少组件。") - # 停止空闲对话检测器 + # 停止其他组件 if self.idle_conversation_starter: self.idle_conversation_starter.stop() - - # 解绑 ObservationInfo 与 ChatObserver if self.observation_info and self.chat_observer: self.observation_info.unbind_from_chat_observer() - - if self.mood_mng and hasattr(self.mood_mng, 'stop_mood_update') and self.mood_mng._running: - self.mood_mng.stop_mood_update() + if self.mood_mng and hasattr(self.mood_mng, 'stop_mood_update') and self.mood_mng._running: # type: ignore + self.mood_mng.stop_mood_update() # type: ignore logger.info(f"[私聊][{self.private_name}] MoodManager 后台更新已停止。") - # ChatObserver 是单例,通常不由单个 Conversation 停止 - # 如果需要,可以在管理器层面处理 ChatObserver 的生命周期 - - # 标记为未初始化 - self._initialized = False + self._initialized = False # 标记为未初始化 logger.info(f"[私聊][{self.private_name}] 对话实例 {self.stream_id} 已停止。") - async def _plan_and_action_loop(self): - """ - 核心的规划与行动循环 (PFC Loop)。 - 持续运行,根据当前状态规划下一步行动,处理新消息中断,执行动作,直到被停止。 - """ - logger.info(f"[私聊][{self.private_name}] 进入 _plan_and_action_loop 循环。") - - # 循环前再次确认初始化状态 - if not self._initialized: - logger.error(f"[私聊][{self.private_name}] 尝试在未初始化状态下运行规划循环,退出。") - return # 明确退出 - force_reflect_and_act = False - # 主循环,只要 should_continue 为 True 就一直运行 - while self.should_continue: - loop_iter_start_time = time.time() # 记录本次循环开始时间 - logger.debug(f"[私聊][{self.private_name}] 开始新一轮循环迭代 ({loop_iter_start_time:.2f})") - try: - # 重新获取 TIME_ZONE 以防在 __init__ 中导入失败 - if 'TIME_ZONE' not in locals() or TIME_ZONE is None: - from dateutil import tz - try: - from ...config.config import global_config - except ImportError: - global_config = None - TIME_ZONE = tz.tzlocal() - else: - configured_tz = getattr(global_config, 'TIME_ZONE', 'Asia/Shanghai') - TIME_ZONE = tz.gettz(configured_tz) - if TIME_ZONE is None: TIME_ZONE = tz.gettz('Asia/Shanghai') - - current_time = datetime.datetime.now(TIME_ZONE) - if self.observation_info: # 确保 observation_info 存在 - time_str = current_time.strftime("%Y-%m-%d %H:%M:%S %Z%z") # 包含时区信息的格式 - self.observation_info.current_time_str = time_str - logger.debug(f"[私聊][{self.private_name}] 更新 ObservationInfo 当前时间: {time_str}") - else: - logger.warning(f"[私聊][{self.private_name}] ObservationInfo 未初始化,无法更新当前时间。") - except Exception as time_update_err: - logger.error(f"[私聊][{self.private_name}] 更新 ObservationInfo 当前时间时出错: {time_update_err}") - # --- 更新时间代码结束 --- - - # --- 处理忽略状态 --- - if self.ignore_until_timestamp and loop_iter_start_time < self.ignore_until_timestamp: - # 如果当前处于忽略状态 - if self.idle_conversation_starter and self.idle_conversation_starter._running: - # 暂停空闲检测器 - self.idle_conversation_starter.stop() - logger.debug(f"[私聊][{self.private_name}] 对话被暂时忽略,暂停空闲对话检测") - # 计算需要睡眠的时间,最多30秒或直到忽略结束 - sleep_duration = min(30, self.ignore_until_timestamp - loop_iter_start_time) - await asyncio.sleep(sleep_duration) - continue # 跳过本次循环的后续步骤,直接进入下一次迭代检查 - elif self.ignore_until_timestamp and loop_iter_start_time >= self.ignore_until_timestamp: - # 如果忽略时间已到 - logger.info(f"[私聊][{self.private_name}] 忽略时间已到 {self.stream_id},准备结束对话。") - self.ignore_until_timestamp = None # 清除忽略时间戳 - await self.stop() # 调用 stop 方法来结束整个对话实例 - continue # 跳过本次循环的后续步骤 - else: - # 如果不在忽略状态,确保空闲检测器在运行 - if self.idle_conversation_starter and not self.idle_conversation_starter._running: - self.idle_conversation_starter.start() - logger.debug(f"[私聊][{self.private_name}] 恢复空闲对话检测") - - # --- 核心规划与行动逻辑 --- - try: - if self.conversation_info and self._initialized: # 确保 conversation_info 和实例已初始化 - # 更新关系文本 - if self.conversation_info.person_id: # 确保 person_id 已获取 - try: - # 1.直接从 person_info_manager 获取数值型的 relationship_value - numeric_relationship_value = await self.person_info_mng.get_value( - self.conversation_info.person_id, - "relationship_value" - ) - - # 确保 relationship_value 是浮点数 (可以复用 relationship_manager 中的 ensure_float,或者在这里简单处理) - if not isinstance(numeric_relationship_value, (int, float)): - # 尝试从 Decimal128 转换 (如果你的数据库用的是这个) - from bson.decimal128 import Decimal128 - if isinstance(numeric_relationship_value, Decimal128): - numeric_relationship_value = float(numeric_relationship_value.to_decimal()) - else: # 其他类型,或转换失败,给默认值 - logger.warning(f"[私聊][{self.private_name}] 获取的 relationship_value 类型未知 ({type(numeric_relationship_value)}) 或转换失败,默认为0.0") - numeric_relationship_value = 0.0 - - logger.debug(f"[私聊][{self.private_name}] 获取到数值型关系值: {numeric_relationship_value}") - - # 2. 使用PFC内部的翻译函数将其转换为文本描述 - simplified_relationship_text = await self.relationship_translator.translate_relationship_value_to_text(numeric_relationship_value) - self.conversation_info.relationship_text = simplified_relationship_text - - logger.debug(f"[私聊][{self.private_name}] 更新后关系文本 (PFC内部翻译): {self.conversation_info.relationship_text}") - - except Exception as e_rel: - logger.error(f"[私聊][{self.private_name}] 更新关系文本(PFC内部翻译)时出错: {e_rel}") - self.conversation_info.relationship_text = "你们的关系是:普通。" # 出错时的默认值 - elif self.observation_info and self.observation_info.sender_user_id and self.observation_info.sender_platform: - # 如果 person_id 之前没获取到,在这里尝试再次获取 (这部分逻辑保持,因为 person_id 是必须的) - try: - private_user_id_int = int(self.observation_info.sender_user_id) - self.conversation_info.person_id = self.person_info_mng.get_person_id( - self.observation_info.sender_platform, - private_user_id_int - ) - await self.person_info_mng.get_or_create_person( #确保用户存在 - platform=self.observation_info.sender_platform, - user_id=private_user_id_int, - nickname=self.observation_info.sender_name if self.observation_info.sender_name else "未知用户", - ) - if self.conversation_info.person_id: # 如果成功获取 person_id,则再次尝试更新关系文本 - numeric_relationship_value = await self.person_info_mng.get_value( - self.conversation_info.person_id, - "relationship_value" - ) - if not isinstance(numeric_relationship_value, (int, float)): - from bson.decimal128 import Decimal128 - if isinstance(numeric_relationship_value, Decimal128): - numeric_relationship_value = float(numeric_relationship_value.to_decimal()) - else: - numeric_relationship_value = 0.0 - self.conversation_info.relationship_text = await self.relationship_translator.translate_relationship_value_to_text(numeric_relationship_value) - logger.debug(f"[私聊][{self.private_name}] (备用逻辑)更新后关系文本: {self.conversation_info.relationship_text}") - - except ValueError: - logger.error(f"[私聊][{self.private_name}] 循环中无法将 sender_user_id ('{self.observation_info.sender_user_id}') 转换为整数。") - self.conversation_info.relationship_text = "你们的关系是:普通。" - except Exception as e_pid_loop: - logger.error(f"[私聊][{self.private_name}] 循环中获取 person_id 并更新关系时出错: {e_pid_loop}") - self.conversation_info.relationship_text = "你们的关系是:普通。" - else: - # 如果 person_id 仍无法确定 - self.conversation_info.relationship_text = "你们的关系是:普通。" # 或 "你们的关系是:未知。" - - # 更新情绪文本 - if self.mood_mng: - self.conversation_info.current_emotion_text = self.mood_mng.get_prompt() - logger.debug(f"[私聊][{self.private_name}] 更新情绪文本: {self.conversation_info.current_emotion_text}") - else: - # 如果 mood_mng 未初始化,使用 ConversationInfo 中的默认值 - pass # self.conversation_info.current_emotion_text 会保持其在 ConversationInfo 中的默认值 - ### 标记新增/修改区域 结束 ### - - # 1. 检查核心组件是否都已初始化 - if not all([self.action_planner, self.observation_info, self.conversation_info]): - logger.error(f"[私聊][{self.private_name}] 核心组件未初始化,无法继续规划循环。将等待5秒后重试...") - await asyncio.sleep(5) - continue # 跳过本次迭代 - - # 2. 记录规划开始时间并重置临时状态 - planning_start_time = time.time() - logger.debug(f"[私聊][{self.private_name}] --- 开始规划 ({planning_start_time:.2f}) ---") - # 重置上一轮存储的“规划期间他人新消息数” - self.conversation_info.other_new_messages_during_planning_count = 0 - - # 3. 调用 ActionPlanner 进行规划 - logger.debug(f"[私聊][{self.private_name}] 调用 ActionPlanner.plan...") - # 传入当前观察信息、对话信息和上次成功回复的动作类型 - action, reason = await self.action_planner.plan( - self.observation_info, - self.conversation_info, # type: ignore - self.conversation_info.last_successful_reply_action, # type: ignore - use_reflect_prompt=force_reflect_and_act # 使用标志 - ) - force_reflect_and_act = False - planning_duration = time.time() - planning_start_time - logger.debug( - f"[私聊][{self.private_name}] ActionPlanner.plan 完成 (耗时: {planning_duration:.3f} 秒),初步规划动作: {action}" - ) - - # 4. 检查规划期间是否有新消息到达 - current_unprocessed_messages = getattr(self.observation_info, "unprocessed_messages", []) - new_messages_during_planning: List[Dict[str, Any]] = [] - other_new_messages_during_planning: List[Dict[str, Any]] = [] - - # 遍历当前所有未处理的消息 - for msg in current_unprocessed_messages: - msg_time = msg.get("time") - sender_id = msg.get("user_info", {}).get("user_id") - # 检查消息时间是否在本次规划开始之后 - if msg_time and msg_time >= planning_start_time: - new_messages_during_planning.append(msg) - # 同时检查是否是来自他人的消息 - if sender_id != self.bot_qq_str: - other_new_messages_during_planning.append(msg) - - new_msg_count = len(new_messages_during_planning) # 规划期间所有新消息数 - other_new_msg_count = len(other_new_messages_during_planning) # 规划期间他人新消息数 - logger.debug( - f"[私聊][{self.private_name}] 规划期间收到新消息总数: {new_msg_count}, 来自他人: {other_new_msg_count}" - ) - - if self.conversation_info and other_new_msg_count > 0: # 如果有来自他人的新消息 - self.conversation_info.current_instance_message_count += other_new_msg_count - logger.debug(f"[私聊][{self.private_name}] 用户发送新消息,实例消息计数增加到: {self.conversation_info.current_instance_message_count}") - - # 调用增量关系更新 - if self.relationship_updater: - await self.relationship_updater.update_relationship_incremental( - conversation_info=self.conversation_info, - observation_info=self.observation_info, - chat_observer_for_history=self.chat_observer - ) - - # 调用情绪更新 - if self.emotion_updater and other_new_messages_during_planning: - # 取最后一条用户消息作为情绪更新的上下文事件 - last_user_msg = other_new_messages_during_planning[-1] - last_user_msg_text = last_user_msg.get("processed_plain_text", "用户发了新消息") - - sender_name_for_event = getattr(self.observation_info, 'sender_name', '对方') - if not sender_name_for_event: # 如果 observation_info 中还没有,尝试从消息中取 - user_info_dict = last_user_msg.get("user_info", {}) - sender_name_for_event = user_info_dict.get("user_nickname", "对方") - - event_desc = f"用户【{sender_name_for_event}】发送了新消息: '{last_user_msg_text[:30]}...'" - await self.emotion_updater.update_emotion_based_on_context( - conversation_info=self.conversation_info, - observation_info=self.observation_info, - chat_observer_for_history=self.chat_observer, - event_description=event_desc - ) - - # 5. 根据动作类型和新消息数量,判断是否需要中断当前规划 - should_interrupt: bool = False - interrupt_reason: str = "" - - if action in ["wait", "listening"]: - # 规则:对于 wait/listen,任何新消息(无论来自谁)都应该中断 - if new_msg_count > 0: - should_interrupt = True - interrupt_reason = f"规划 {action} 期间收到 {new_msg_count} 条新消息" - logger.info(f"[私聊][{self.private_name}] 中断 '{action}',原因: {interrupt_reason}。") - else: - # 规则:对于其他动作,检查来自他人的新消息是否超过阈值 2 - interrupt_threshold: int = 2 - if other_new_msg_count > interrupt_threshold: - should_interrupt = True - interrupt_reason = f"规划 {action} 期间收到 {other_new_msg_count} 条来自他人的新消息 (阈值 >{interrupt_threshold})" - logger.info(f"[私聊][{self.private_name}] 中断 '{action}',原因: {interrupt_reason}。") - - # 6. 如果需要中断,则记录取消信息,重置状态,并进入下一次循环 - if should_interrupt: - logger.info(f"[私聊][{self.private_name}] 执行中断,重新规划...") - # 记录被取消的动作到历史记录 - cancel_record = { - "action": action, - "plan_reason": reason, - "status": "cancelled_due_to_new_messages", # 标记取消原因 - "time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "final_reason": interrupt_reason, - } - # 安全地添加到 done_action 列表 - if not hasattr(self.conversation_info, "done_action"): - self.conversation_info.done_action = [] - self.conversation_info.done_action.append(cancel_record) - - # 重置追问状态,因为当前动作被中断了 - self.conversation_info.last_successful_reply_action = None - # 将状态设置回分析,准备处理新消息并重新规划 - self.state = ConversationState.ANALYZING - await asyncio.sleep(0.1) # 短暂等待,避免CPU空转 - continue # 直接进入下一次循环迭代 - - # 7. 如果未中断,存储规划期间的他人新消息数,并执行动作 - logger.debug(f"[私聊][{self.private_name}] 未中断,调用 _handle_action 执行动作 '{action}'...") - # 将计算出的“规划期间他人新消息数”存入 conversation_info,供 _handle_action 使用 - self.conversation_info.other_new_messages_during_planning_count = other_new_msg_count - # 调用动作处理函数 - await self._handle_action(action, reason, self.observation_info, self.conversation_info) - logger.debug(f"[私聊][{self.private_name}] _handle_action 完成。") - - # --- 新增逻辑:检查是否因为RG决定不发送而需要反思 --- - last_action_record = ( - self.conversation_info.done_action[-1] if self.conversation_info.done_action else {} # type: ignore - ) - if last_action_record.get("action") == "send_new_message" and \ - last_action_record.get("status") == "done_no_reply": - logger.info(f"[私聊][{self.private_name}] 检测到 ReplyGenerator 决定不发送消息,将在下一轮强制使用反思Prompt。") - force_reflect_and_act = True # 设置标志,下一轮使用反思prompt - # 不需要立即 continue,让循环自然进入下一轮,下一轮的 plan 会用这个标志 - - # 8. 检查是否需要结束整个对话(例如目标达成或执行了结束动作) - goal_ended: bool = False - # 检查最新的目标是否是“结束对话” - if hasattr(self.conversation_info, "goal_list") and self.conversation_info.goal_list: - last_goal_item = self.conversation_info.goal_list[-1] - current_goal: Optional[str] = None - if isinstance(last_goal_item, dict): - current_goal = last_goal_item.get("goal") - elif isinstance(last_goal_item, str): - current_goal = last_goal_item - if isinstance(current_goal, str) and current_goal == "结束对话": - goal_ended = True - - # 检查最后执行的动作是否是结束类型且成功完成 - last_action_record = ( - self.conversation_info.done_action[-1] if self.conversation_info.done_action else {} - ) - action_ended: bool = ( - last_action_record.get("action") in ["end_conversation", "say_goodbye"] - and last_action_record.get("status") == "done" - ) - - # 如果满足任一结束条件,则停止循环 - if goal_ended or action_ended: - logger.info( - f"[私聊][{self.private_name}] 检测到结束条件 (目标结束: {goal_ended}, 动作结束: {action_ended}),停止循环。" - ) - await self.stop() # 调用 stop 来停止实例 - continue # 跳过后续,虽然 stop 会设置 should_continue=False - - except asyncio.CancelledError: - # 处理任务被取消的情况 - logger.info(f"[私聊][{self.private_name}] PFC 主循环任务被取消。") - await self.stop() # 确保资源被清理 - break # 明确退出循环 - except Exception as loop_err: - # 捕获循环中的其他未预期错误 - logger.error(f"[私聊][{self.private_name}] PFC 主循环出错: {loop_err}") - logger.error(f"[私聊][{self.private_name}] {traceback.format_exc()}") - self.state = ConversationState.ERROR # 设置错误状态 - # 可以在这里添加更复杂的错误恢复逻辑,或者简单等待后重试 - await asyncio.sleep(5) # 等待一段时间,避免错误状态下快速空转 - - # --- 控制循环频率 --- - loop_duration = time.time() - loop_iter_start_time # 计算本次循环耗时 - min_loop_interval = 0.1 # 设置最小循环间隔(秒),防止CPU占用过高 - logger.debug(f"[私聊][{self.private_name}] 循环迭代耗时: {loop_duration:.3f} 秒。") - if loop_duration < min_loop_interval: - # 如果循环太快,则睡眠一段时间 - await asyncio.sleep(min_loop_interval - loop_duration) - - # 循环结束后的日志 - logger.info(f"[私聊][{self.private_name}] PFC 循环已退出 for stream_id: {self.stream_id}") + # _plan_and_action_loop 方法已被移除 def _convert_to_message(self, msg_dict: Dict[str, Any]) -> Optional[Message]: """将从数据库或其他来源获取的消息字典转换为内部使用的 Message 对象""" + # 这个方法似乎没有被其他内部方法调用,但为了完整性暂时保留 try: - # 优先使用实例自身的 chat_stream,如果不存在则尝试从管理器获取 chat_stream_to_use = self.chat_stream or chat_manager.get_stream(self.stream_id) if not chat_stream_to_use: - logger.error( - f"[私聊][{self.private_name}] 无法确定 ChatStream for stream_id {self.stream_id},无法转换消息。" - ) - return None # 无法确定聊天流,返回 None + logger.error(f"[私聊][{self.private_name}] 无法确定 ChatStream for stream_id {self.stream_id},无法转换消息。") + return None - # 解析用户信息字典 user_info_dict = msg_dict.get("user_info", {}) user_info: Optional[UserInfo] = None if isinstance(user_info_dict, dict): try: - # 使用 UserInfo 类的方法从字典创建对象 user_info = UserInfo.from_dict(user_info_dict) except Exception as e: - # 解析失败记录警告 - logger.warning( - f"[私聊][{self.private_name}] 从字典创建 UserInfo 时出错: {e}, dict: {user_info_dict}" - ) + logger.warning(f"[私聊][{self.private_name}] 从字典创建 UserInfo 时出错: {e}, dict: {user_info_dict}") if not user_info: - # 如果没有有效的 UserInfo,记录警告并返回 None - logger.warning( - f"[私聊][{self.private_name}] 消息缺少有效的 UserInfo,无法转换。 msg_id: {msg_dict.get('message_id')}" - ) + logger.warning(f"[私聊][{self.private_name}] 消息缺少有效的 UserInfo,无法转换。 msg_id: {msg_dict.get('message_id')}") return None - # 创建并返回 Message 对象 return Message( - message_id=msg_dict.get("message_id", f"gen_{time.time()}"), # 如果没有ID,生成一个临时的 + message_id=msg_dict.get("message_id", f"gen_{time.time()}"), chat_stream=chat_stream_to_use, - time=msg_dict.get("time", time.time()), # 如果没有时间戳,使用当前时间 - user_info=user_info, # 使用解析出的 UserInfo 对象 - processed_plain_text=msg_dict.get("processed_plain_text", ""), # 获取处理后的纯文本 - detailed_plain_text=msg_dict.get("detailed_plain_text", ""), # 获取详细纯文本 + time=msg_dict.get("time", time.time()), + user_info=user_info, + processed_plain_text=msg_dict.get("processed_plain_text", ""), + detailed_plain_text=msg_dict.get("detailed_plain_text", ""), ) except Exception as e: - # 捕获转换过程中的任何异常 logger.error(f"[私聊][{self.private_name}] 转换消息时出错: {e}") logger.error(f"[私聊][{self.private_name}] {traceback.format_exc()}") - return None # 转换失败返回 None - - async def _handle_action( - self, action: str, reason: str, observation_info: ObservationInfo, conversation_info: ConversationInfo - ): - """ - 处理由 ActionPlanner 规划出的具体行动。 - 包括生成回复、调用检查器、发送消息、等待、思考目标等,并包含重试逻辑。 - 根据执行结果和规则更新对话状态。 - """ - # 检查初始化状态 - if not self._initialized: - logger.error(f"[私聊][{self.private_name}] 尝试在未初始化状态下处理动作 '{action}'。") - return - - logger.info(f"[私聊][{self.private_name}] 开始处理动作: {action}, 原因: {reason}") - 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 中设置 - } - # 安全地添加到历史记录列表 - if not hasattr(conversation_info, "done_action"): - conversation_info.done_action = [] - 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 = "动作未成功执行" # 动作最终原因 - # need_replan_from_checker: bool = False # 这个变量在循环内部被赋值 - - 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: bool = False # 标记回复是否合适 - generated_content: str = "" # 存储生成的回复 - check_reason: str = "未进行检查" # 存储检查结果原因 - need_replan_from_checker: bool = False # 在循环前初始化 - - while reply_attempt_count < max_reply_attempts and not is_suitable and not need_replan_from_checker: - reply_attempt_count += 1 - log_prefix = f"[私聊][{self.private_name}] 尝试生成/检查 '{action}' 回复 (第 {reply_attempt_count}/{max_reply_attempts} 次)..." - logger.info(log_prefix) - - self.state = ConversationState.GENERATING - if not self.reply_generator: - raise RuntimeError("ReplyGenerator 未初始化") - - raw_llm_output = await self.reply_generator.generate( - observation_info, conversation_info, action_type=action - ) - logger.debug(f"{log_prefix} ReplyGenerator.generate 返回: '{raw_llm_output}'") - - should_send_reply = True # 默认对于 direct_reply 是要发送的 - text_to_process = raw_llm_output # 默认情况下,处理原始输出 - - if action == "send_new_message": - is_send_decision_from_rg = True # 标记 send_new_message 的决策来自RG - try: - parsed_json = None - try: - 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: - should_send_reply = False - text_to_process = "no" # 保持和 prompt 中一致,txt 为 "no" - logger.info(f"{log_prefix} ReplyGenerator 决定不发送消息。") - # 此时,我们应该跳出重试循环,并触发 action_planner 的反思 prompt - # 将此信息传递到循环外部进行处理 - break # 跳出 while 循环 - - except Exception as e_json: # 更广泛地捕获解析相关的错误 - logger.error(f"{log_prefix} 解析 ReplyGenerator 的JSON输出时出错: {e_json}, 输出: {raw_llm_output}") - conversation_info.last_reply_rejection_reason = f"解析回复生成器JSON输出错误: {e_json}" - conversation_info.last_rejected_reply_content = raw_llm_output - should_send_reply = False - text_to_process = "no" - - if not should_send_reply and action == "send_new_message": # 如果RG决定不发送 (send_new_message特定逻辑) - 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): # should_send_reply is true here means RG said yes, but txt is "no" or empty - - 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 - - # if reply_attempt_count < max_reply_attempts: # 只有在还有尝试次数时才继续 - await asyncio.sleep(0.5) # 暂停一下 - continue # 直接进入下一次循环尝试 - - self.state = ConversationState.CHECKING - if not self.reply_checker: - raise RuntimeError("ReplyChecker 未初始化") - - current_goal_str = "" - if conversation_info.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 - 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 self.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.info(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 - - # 如果是我们特定的复读原因,并且 checker 说不需要重新规划 (这是我们的新逻辑) - if check_reason == "机器人尝试发送重复消息" and not need_replan_from_checker: - logger.warning(f"{log_prefix} 回复因自身重复被拒绝: {check_reason}。将使用相同 Prompt 类型重试。") - # 不需要额外操作,循环会自动继续,因为 is_suitable=False 且 need_replan_from_checker=False - 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"[私聊][{self.private_name}] 动作 '{action}': ReplyGenerator 决定不发送消息。将调用 ActionPlanner 进行反思。") - final_status = "done_no_reply" # 一个新的状态,表示动作完成但无回复 - final_reason = "回复生成器决定不发送消息" - action_successful = True # 动作本身(决策)是成功的 - - # 清除追问状态,因为没有实际发送 - conversation_info.last_successful_reply_action = None - conversation_info.my_message_count = 0 # 重置连续发言计数 - - # !!! 触发 ActionPlanner 使用 PROMPT_REFLECT_AND_ACT !!! - if not self.action_planner: - raise RuntimeError("ActionPlanner 未初始化") - - logger.info(f"[私聊][{self.private_name}] {global_config.BOT_NICKNAME}本来想发一条新消息,但是想想还是算了。现在重新规划...") - # 调用 action_planner.plan 并传入 use_reflect_prompt=True - new_action, new_reason = await self.action_planner.plan( - observation_info, - conversation_info, - last_successful_reply_action=None, # 因为没发送,所以没有成功的回复动作 - use_reflect_prompt=True - ) - # 记录这次特殊的“反思”动作 - reflect_action_record = { - "action": f"reflect_after_no_send ({new_action})", # 记录原始意图和新规划 - "plan_reason": f"RG决定不发送后,AP规划: {new_reason}", - "status": "delegated", # 标记为委托给新的规划 - "time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - } - conversation_info.done_action.append(reflect_action_record) - - logger.info(f"[私聊][{self.private_name}] 反思后的新规划动作: {new_action}, 原因: {new_reason}") - # **暂定方案:** - - elif is_suitable: # 适用于 direct_reply 或 send_new_message (RG决定发送且检查通过) - logger.info(f"[私聊][{self.private_name}] 动作 '{action}': 找到合适的回复,准备发送。") - # conversation_info.last_reply_rejection_reason = None # 已在循环内清除 - # conversation_info.last_rejected_reply_content = None - self.generated_reply = generated_content_for_check_or_send - timestamp_before_sending = time.time() - logger.debug( - f"[私聊][{self.private_name}] 动作 '{action}': 记录发送前时间戳: {timestamp_before_sending:.2f}" - ) - self.state = ConversationState.SENDING - send_success = await self._send_reply() # _send_reply 内部会更新 my_message_count - send_end_time = time.time() - - if send_success: - action_successful = True - final_status = "done" # 明确设置 final_status - final_reason = "成功发送" # 明确设置 final_reason - logger.info(f"[私聊][{self.private_name}] 动作 '{action}': 成功发送回复.") - - if observation_info and self.bot_qq_str: - bot_message_dict = { - "message_id": f"bot_sent_{send_end_time}", # 生成一个唯一ID - "time": send_end_time, - "user_info": { # 构造机器人的 UserInfo - "user_id": self.bot_qq_str, - "user_nickname": global_config.BOT_NICKNAME, # 或者 self.name (如果 Conversation 类有) - "platform": self.chat_stream.platform if self.chat_stream else "unknown_platform" - }, - "processed_plain_text": self.generated_reply, - "detailed_plain_text": self.generated_reply, # 简单处理 - # 根据你的消息字典结构,可能还需要其他字段,如 message_type 等 - } - observation_info.chat_history.append(bot_message_dict) - observation_info.chat_history_count = len(observation_info.chat_history) - logger.debug(f"[私聊][{self.private_name}] 机器人发送的消息已添加到 chat_history。当前历史数: {observation_info.chat_history_count}") - - # 可选:如果 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) - - # 更新 chat_history_str (如果 ReplyChecker 也依赖这个字符串,尽管我们的修改是基于列表的) - # 这个更新可能比较消耗资源,如果 checker 只用列表,可以考虑优化此处 - 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: - logger.error(f"[私聊][{self.private_name}] 更新 chat_history_str 时出错: {e_build_hist}") - observation_info.chat_history_str = "[构建聊天记录出错]" - # --- 新增结束 --- - - if self.idle_conversation_starter: - await self.idle_conversation_starter.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 != self.bot_qq_str # 确保是对方的消息 - and msg_time < timestamp_before_sending # 只清理发送前的 - ): - message_ids_to_clear.add(msg_id) - - if message_ids_to_clear: - logger.debug( - f"[私聊][{self.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"[私聊][{self.private_name}] 没有需要清理的发送前(他人)消息。") - - other_new_msg_count_during_planning = getattr( - conversation_info, "other_new_messages_during_planning_count", 0 - ) - - if other_new_msg_count_during_planning > 0 and action == "direct_reply": - logger.info( - f"[私聊][{self.private_name}] 因规划期间收到 {other_new_msg_count_during_planning} 条他人新消息,下一轮强制使用【初始回复】逻辑。" - ) - conversation_info.last_successful_reply_action = None - # conversation_info.my_message_count = 0 # 不在这里重置,因为刚发了一条 - elif action == "direct_reply" or action == "send_new_message": # 成功发送后 - logger.info( - f"[私聊][{self.private_name}] 成功执行 '{action}', 下一轮【允许】使用追问逻辑。" - ) - conversation_info.last_successful_reply_action = action - - if conversation_info: - conversation_info.current_instance_message_count += 1 - logger.debug(f"[私聊][{self.private_name}] 实例消息计数(机器人发送后)增加到: {conversation_info.current_instance_message_count}") - - if self.relationship_updater: - await self.relationship_updater.update_relationship_incremental( - conversation_info=conversation_info, - observation_info=observation_info, - chat_observer_for_history=self.chat_observer - ) - - sent_reply_summary = self.generated_reply[:50] if self.generated_reply else "空回复" - event_for_emotion_update = f"你刚刚发送了消息: '{sent_reply_summary}...'" - if self.emotion_updater: - await self.emotion_updater.update_emotion_based_on_context( - conversation_info=conversation_info, - observation_info=observation_info, - chat_observer_for_history=self.chat_observer, - event_description=event_for_emotion_update - ) - else: # 发送失败 - logger.error(f"[私聊][{self.private_name}] 动作 '{action}': 发送回复失败。") - final_status = "recall" - 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"[私聊][{self.private_name}] 动作 '{action}' 因 ReplyChecker 要求而被取消,将重新规划。原因: {check_reason}" - ) - final_status = "recall" - final_reason = f"回复检查要求重新规划: {check_reason}" - conversation_info.last_successful_reply_action = None - # my_message_count 保持不变,因为没有成功发送 - - else: # 达到最大尝试次数仍未找到合适回复 - logger.warning( - f"[私聊][{self.private_name}] 动作 '{action}': 达到最大尝试次数 ({max_reply_attempts}),未能生成/检查通过合适的回复。最终原因: {check_reason}" - ) - final_status = "recall" - final_reason = f"尝试{max_reply_attempts}次后失败: {check_reason}" - action_successful = False - conversation_info.last_successful_reply_action = None - # my_message_count 保持不变 - - # 2. 处理发送告别语动作 (保持简单,不加重试) - elif action == "say_goodbye": - self.state = ConversationState.GENERATING - if not self.reply_generator: - raise RuntimeError("ReplyGenerator 未初始化") - # 生成告别语 - generated_content = await self.reply_generator.generate( - observation_info, conversation_info, action_type=action - ) - logger.info(f"[私聊][{self.private_name}] 动作 '{action}': 生成内容: '{generated_content[:100]}...'") - - # 检查生成内容 - if not generated_content or generated_content.startswith("抱歉"): - logger.warning(f"[私聊][{self.private_name}] 动作 '{action}': 生成内容为空或为错误提示,取消发送。") - final_reason = "生成内容无效" - # 即使生成失败,也按计划结束对话 - final_status = "done" # 标记为 done,因为目的是结束 - self.should_continue = False - logger.info(f"[私聊][{self.private_name}] 告别语生成失败,仍按计划结束对话。") - else: - # 发送告别语 - self.generated_reply = generated_content - timestamp_before_sending = time.time() - logger.debug( - f"[私聊][{self.private_name}] 动作 '{action}': 记录发送前时间戳: {timestamp_before_sending:.2f}" - ) - self.state = ConversationState.SENDING - send_success = await self._send_reply() - send_end_time = time.time() - - if send_success: - action_successful = True # 标记成功 - # final_status 和 final_reason 会在 finally 中设置 - logger.info(f"[私聊][{self.private_name}] 成功发送告别语,即将停止对话实例。") - # 更新空闲计时器 - if self.idle_conversation_starter: - await self.idle_conversation_starter.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 = msg.get("user_info", {}).get("user_id") - if ( - msg_id - and msg_time - and sender_id != self.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"[私聊][{self.private_name}] 实例消息计数(告别语后)增加到: {conversation_info.current_instance_message_count}") - - # 告别通常是结束,可以不进行增量关系更新,但情绪可以更新 - # if self.relationship_updater: - # await self.relationship_updater.update_relationship_incremental(...) - - sent_reply_summary = self.generated_reply[:50] if self.generated_reply else "空回复" - event_for_emotion_update = f"你发送了告别消息: '{sent_reply_summary}...'" - if self.emotion_updater: - await self.emotion_updater.update_emotion_based_on_context( - conversation_info=conversation_info, - observation_info=observation_info, - chat_observer_for_history=self.chat_observer, - event_description=event_for_emotion_update - ) - # 发送成功后结束对话 - self.should_continue = False - else: - # 发送失败 - logger.error(f"[私聊][{self.private_name}] 动作 '{action}': 发送告别语失败。") - final_status = "recall" - final_reason = "发送告别语失败" - # 发送失败不能结束对话 - self.should_continue = True - - # 3. 处理重新思考目标动作 - elif action == "rethink_goal": - self.state = ConversationState.RETHINKING - if not self.goal_analyzer: - raise RuntimeError("GoalAnalyzer 未初始化") - # 调用 GoalAnalyzer 分析并更新目标 - await self.goal_analyzer.analyze_goal(conversation_info, observation_info) - action_successful = True # 标记成功 - event_for_emotion_update = "你重新思考了对话目标和方向" - if self.emotion_updater and conversation_info and observation_info: # 确保updater和info都存在 - await self.emotion_updater.update_emotion_based_on_context( - conversation_info=conversation_info, - observation_info=observation_info, - chat_observer_for_history=self.chat_observer, - event_description=event_for_emotion_update - ) - - # 4. 处理倾听动作 - elif action == "listening": - self.state = ConversationState.LISTENING - if not self.waiter: - raise RuntimeError("Waiter 未初始化") - logger.info(f"[私聊][{self.private_name}] 动作 'listening': 进入倾听状态...") - # 调用 Waiter 的倾听等待方法,内部会处理超时 - await self.waiter.wait_listening(conversation_info) - action_successful = True # listening 动作本身执行即视为成功,后续由新消息或超时驱动 - event_for_emotion_update = "你决定耐心倾听对方的发言" - if self.emotion_updater and conversation_info and observation_info: - await self.emotion_updater.update_emotion_based_on_context( - conversation_info=conversation_info, - observation_info=observation_info, - chat_observer_for_history=self.chat_observer, - event_description=event_for_emotion_update - ) - - # 5. 处理结束对话动作 - elif action == "end_conversation": - logger.info(f"[私聊][{self.private_name}] 动作 'end_conversation': 收到最终结束指令,停止对话...") - action_successful = True # 标记成功 - self.should_continue = False # 设置标志以退出循环 - - # 6. 处理屏蔽忽略动作 - elif action == "block_and_ignore": - logger.info(f"[私聊][{self.private_name}] 动作 'block_and_ignore': 不想再理你了...") - ignore_duration_seconds = 10 * 60 # 忽略 10 分钟,可配置 - self.ignore_until_timestamp = time.time() + ignore_duration_seconds - logger.info( - f"[私聊][{self.private_name}] 将忽略此对话直到: {datetime.datetime.fromtimestamp(self.ignore_until_timestamp)}" - ) - self.state = ConversationState.IGNORED # 设置忽略状态 - action_successful = True # 标记成功 - event_for_emotion_update = "当前对话让你感到不适,你决定暂时不再理会对方" - if self.emotion_updater and conversation_info and observation_info: - # 可以让LLM判断此时的情绪,或者直接设定一个倾向(比如厌恶、不耐烦) - # 这里还是让LLM判断 - await self.emotion_updater.update_emotion_based_on_context( - conversation_info=conversation_info, - observation_info=observation_info, - chat_observer_for_history=self.chat_observer, - event_description=event_for_emotion_update - ) - - # 7. 处理等待动作 - elif action == "wait": - self.state = ConversationState.WAITING - if not self.waiter: - raise RuntimeError("Waiter 未初始化") - logger.info(f"[私聊][{self.private_name}] 动作 'wait': 进入等待状态...") - # 调用 Waiter 的常规等待方法,内部处理超时 - # wait 方法返回是否超时 (True=超时, False=未超时/被新消息中断) - timeout_occurred = await self.waiter.wait(self.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 self.emotion_updater and conversation_info and observation_info: - await self.emotion_updater.update_emotion_based_on_context( - conversation_info=conversation_info, - observation_info=observation_info, - chat_observer_for_history=self.chat_observer, - event_description=event_for_emotion_update - ) - # wait 动作完成后不需要清理消息,等待新消息或超时触发重新规划 - logger.debug(f"[私聊][{self.private_name}] Wait 动作完成,无需在此清理消息。") - - # 8. 处理未知的动作类型 - else: - logger.warning(f"[私聊][{self.private_name}] 未知的动作类型: {action}") - final_status = "recall" # 未知动作标记为 recall - final_reason = f"未知的动作类型: {action}" - - # --- 重置非回复动作的追问状态 --- - # 确保执行完非回复动作后,下一次规划不会错误地进入追问逻辑 - if action not in ["direct_reply", "send_new_message", "say_goodbye"]: - conversation_info.last_successful_reply_action = None - # 清理可能残留的拒绝信息 - conversation_info.last_reply_rejection_reason = None - conversation_info.last_rejected_reply_content = None - - except asyncio.CancelledError: - # 处理任务被取消的异常 - logger.warning(f"[私聊][{self.private_name}] 处理动作 '{action}' 时被取消。") - final_status = "cancelled" - final_reason = "动作处理被取消" - # 取消时也重置追问状态 - conversation_info.last_successful_reply_action = None - raise # 重新抛出 CancelledError,让上层知道任务被取消 - except Exception as handle_err: - # 捕获处理动作过程中的其他所有异常 - logger.error(f"[私聊][{self.private_name}] 处理动作 '{action}' 时出错: {handle_err}") - logger.error(f"[私聊][{self.private_name}] {traceback.format_exc()}") - final_status = "error" # 标记为错误状态 - final_reason = f"处理动作时出错: {handle_err}" - self.state = ConversationState.ERROR # 设置对话状态为错误 - # 出错时重置追问状态 - conversation_info.last_successful_reply_action = None - - finally: - # --- 无论成功与否,都执行 --- - - # 1. 重置临时存储的计数值 - # 确保这个值只在当前规划周期内有效 - conversation_info.other_new_messages_during_planning_count = 0 - - # 2. 更新动作历史记录的最终状态和原因 - # 优化:如果动作成功但状态仍是默认的 recall,则更新为 done - if action_successful: - # 如果动作标记为成功,但 final_status 仍然是初始的 "recall" 或者 "start" - # (因为可能在try块中成功执行了但没有显式更新 final_status 为 "done") - # 或者是 "done_no_reply" 这种特殊的成功状态 - if final_status in ["recall", "start"] and action != "send_new_message": # send_new_message + no_reply 是特殊成功 - final_status = "done" - if not final_reason or final_reason == "动作未成功执行": - # 为不同类型的成功动作提供更具体的默认成功原因 - if action == "wait": - timeout_occurred = ( - any("分钟," in g.get("goal", "") for g in conversation_info.goal_list if isinstance(g, dict)) - if conversation_info.goal_list - else False - ) - final_reason = "等待完成" + (" (超时)" if timeout_occurred else " (收到新消息或中断)") - elif action == "listening": - final_reason = "进入倾听状态" - elif action in ["rethink_goal", "end_conversation", "block_and_ignore", "say_goodbye"]: - final_reason = f"成功执行 {action}" - elif action in ["direct_reply", "send_new_message"]: # 正常发送成功的case - final_reason = "成功发送" - else: - final_reason = f"动作 {action} 成功完成" - # 如果已经是 "done" 或 "done_no_reply",则保留它们和它们对应的 final_reason - - else: # action_successful is False - # 如果动作标记为失败,且 final_status 还是 "recall" (初始值) 或 "start" - if final_status in ["recall", "start"]: - # 尝试从 conversation_info 中获取更具体的失败原因(例如 checker 的原因) - # 这个 specific_rejection_reason 是在 try 块中被设置的 - specific_rejection_reason = getattr(conversation_info, 'last_reply_rejection_reason', None) - rejected_content = getattr(conversation_info, 'last_rejected_reply_content', None) - - if specific_rejection_reason: - final_reason = f"执行失败: {specific_rejection_reason}" - if rejected_content and specific_rejection_reason == "机器人尝试发送重复消息": # 对复读提供更清晰的日志 - final_reason += f" (内容: '{rejected_content[:30]}...')" - elif not final_reason or final_reason == "动作未成功执行": # 如果没有更具体的原因 - final_reason = f"动作 {action} 执行失败或被意外中止" - # 如果 final_status 已经是 "error" 或 "cancelled",则保留它们和它们对应的 final_reason - - # 更新 done_action 中的记录 - if conversation_info.done_action and action_index < len(conversation_info.done_action): - # 确保 current_action_record 是最新的(尽管我们是更新它) - # 实际上我们是更新 list 中的元素 - 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), - } - ) - else: - logger.error(f"[私聊][{self.private_name}] 无法更新动作历史记录,索引 {action_index} 无效或列表为空。") - - # 最终日志输出 - log_final_reason = final_reason if final_reason else "无明确原因" - if final_status == "done" and action_successful and action in ["direct_reply", "send_new_message"] and hasattr(self, 'generated_reply'): - log_final_reason += f" (发送内容: '{self.generated_reply[:30]}...')" - - logger.info(f"[私聊][{self.private_name}] 动作 '{action}' 处理完成。最终状态: {final_status}, 原因: {log_final_reason}") - async def _send_reply(self) -> bool: - """发送 `self.generated_reply` 中的内容到聊天流""" - # 检查是否有内容可发送 - if not self.generated_reply: - logger.warning(f"[私聊][{self.private_name}] 没有生成回复内容,无法发送。") - return False - # 检查发送器和聊天流是否已初始化 - if not self.direct_sender: - logger.error(f"[私聊][{self.private_name}] DirectMessageSender 未初始化,无法发送。") - return False - if not self.chat_stream: - logger.error(f"[私聊][{self.private_name}] ChatStream 未初始化,无法发送。") - return False - - try: - reply_content = self.generated_reply - # 调用发送器发送消息,不指定回复对象 - await self.direct_sender.send_message( - chat_stream=self.chat_stream, - content=reply_content, - reply_to_message=None, # 私聊通常不需要引用回复 - ) - # 自身发言数量累计 +1 - self.conversation_info.my_message_count += 1 - # 发送成功后,将状态设置回分析,准备下一轮规划 - self.state = ConversationState.ANALYZING - return True # 返回成功 - except Exception as e: - # 捕获发送过程中的异常 - logger.error(f"[私聊][{self.private_name}] 发送消息时失败: {str(e)}") - logger.error(f"[私聊][{self.private_name}] {traceback.format_exc()}") - self.state = ConversationState.ERROR # 发送失败标记错误状态 - return False # 返回失败 - - async def _send_timeout_message(self): - """在等待超时后发送一条结束消息""" - # 检查发送器和聊天流 - if not self.direct_sender or not self.chat_stream: - logger.warning(f"[私聊][{self.private_name}] 发送器或聊天流未初始化,无法发送超时消息。") - return - try: - # 定义超时消息内容,可以考虑配置化或由 LLM 生成 - timeout_content = "我们好像很久没说话了,先这样吧~" - # 发送超时消息 - await self.direct_sender.send_message( - chat_stream=self.chat_stream, content=timeout_content, reply_to_message=None - ) - logger.info(f"[私聊][{self.private_name}] 已发送超时结束消息。") - # 发送超时消息后,通常意味着对话结束,调用 stop - await self.stop() - except Exception as e: - # 捕获发送超时消息的异常 - logger.error(f"[私聊][{self.private_name}] 发送超时消息失败: {str(e)}") + return None \ No newline at end of file diff --git a/src/plugins/PFC/conversation_initializer.py b/src/plugins/PFC/conversation_initializer.py new file mode 100644 index 00000000..2e348be9 --- /dev/null +++ b/src/plugins/PFC/conversation_initializer.py @@ -0,0 +1,279 @@ +import time +import asyncio +import traceback +from typing import TYPE_CHECKING + +from src.common.logger_manager import get_logger +from src.plugins.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat +from maim_message import UserInfo +from src.plugins.chat.chat_stream import chat_manager +from src.config.config import global_config +from ..person_info.person_info import person_info_manager +from ..person_info.relationship_manager import relationship_manager +from ..moods.moods import MoodManager + +# 导入 PFC 内部组件和类型 +from .pfc_types import ConversationState +from .pfc import GoalAnalyzer +from .chat_observer import ChatObserver +from .message_sender import DirectMessageSender +from .action_planner import ActionPlanner +from .observation_info import ObservationInfo +from .conversation_info import ConversationInfo +from .reply_generator import ReplyGenerator +from .idle_conversation_starter import IdleConversationStarter +from .pfc_KnowledgeFetcher import KnowledgeFetcher # 修正大小写 +from .waiter import Waiter +from .pfc_utils import get_person_id +from .reply_checker import ReplyChecker +from .pfc_relationship import PfcRelationshipUpdater, PfcRepationshipTranslator +from .pfc_emotion import PfcEmotionUpdater + + +if TYPE_CHECKING: + from .conversation import Conversation # 用于类型提示以避免循环导入 + +logger = get_logger("pfc_initializer") + +async def load_initial_history(conversation_instance: 'Conversation'): + """ + 加载并处理初始的聊天记录。 + 之前是 Conversation 类中的 _load_initial_history 方法。 + """ + if not conversation_instance.observation_info: # 确保 ObservationInfo 已初始化 + logger.warning(f"[私聊][{conversation_instance.private_name}] ObservationInfo 未初始化,无法加载历史记录。") + return + + try: + logger.info(f"[私聊][{conversation_instance.private_name}] 为 {conversation_instance.stream_id} 加载初始聊天记录...") + # 从聊天核心获取原始消息列表 + initial_messages = get_raw_msg_before_timestamp_with_chat( + chat_id=conversation_instance.stream_id, + timestamp=time.time(), + limit=30, # limit 可以根据需要调整或配置 + ) + + if initial_messages: + # 更新 ObservationInfo 中的历史记录列表和计数 + conversation_instance.observation_info.chat_history = initial_messages + conversation_instance.observation_info.chat_history_count = len(initial_messages) + + # 获取最后一条消息的信息 + last_msg = initial_messages[-1] + conversation_instance.observation_info.last_message_time = last_msg.get("time") + conversation_instance.observation_info.last_message_id = last_msg.get("message_id") + + # 安全地解析最后一条消息的发送者信息 + last_user_info_dict = last_msg.get("user_info", {}) + if isinstance(last_user_info_dict, dict): + try: + last_user_info = UserInfo.from_dict(last_user_info_dict) + # 存储发送者的 user_id 字符串 + conversation_instance.observation_info.last_message_sender = ( + str(last_user_info.user_id) if last_user_info else None + ) + except Exception as e: + logger.warning(f"[私聊][{conversation_instance.private_name}] 解析最后一条消息的用户信息时出错: {e}") + conversation_instance.observation_info.last_message_sender = None + else: + # 如果 user_info 不是字典,也标记为未知 + conversation_instance.observation_info.last_message_sender = None + + # 存储最后一条消息的文本内容 + conversation_instance.observation_info.last_message_content = last_msg.get("processed_plain_text", "") + + # 构建用于 Prompt 的历史记录字符串 (只使用最近的一部分) + history_slice_for_str = initial_messages[-30:] # 可配置 + conversation_instance.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, # read_mark 可能需要根据实际情况调整 + ) + + # 更新 ChatObserver 和 IdleStarter 的时间戳 + if conversation_instance.chat_observer: + # 更新观察者的最后消息时间,避免重复处理这些初始消息 + conversation_instance.chat_observer.last_message_time = conversation_instance.observation_info.last_message_time + if conversation_instance.idle_conversation_starter and conversation_instance.observation_info.last_message_time: + # 更新空闲计时器的起始时间 + await conversation_instance.idle_conversation_starter.update_last_message_time( + conversation_instance.observation_info.last_message_time + ) + + logger.info( + f"[私聊][{conversation_instance.private_name}] 成功加载 {len(initial_messages)} 条初始聊天记录。最后一条消息时间: {conversation_instance.observation_info.last_message_time}" + ) + else: + # 如果没有历史记录 + logger.info(f"[私聊][{conversation_instance.private_name}] 没有找到初始聊天记录。") + conversation_instance.observation_info.chat_history_str = "还没有聊天记录。" # 设置默认提示 + + except Exception as load_err: + # 捕获加载过程中的异常 + logger.error(f"[私聊][{conversation_instance.private_name}] 加载初始聊天记录时出错: {load_err}") + # 即使出错,也设置一个提示,避免后续使用 None 值 + if conversation_instance.observation_info: + conversation_instance.observation_info.chat_history_str = "[加载聊天记录出错]" + + +async def initialize_core_components(conversation_instance: 'Conversation'): + """ + 异步初始化对话实例及其所有依赖的核心组件。 + 之前是 Conversation 类中的 _initialize 方法。 + """ + # 防止重复初始化 (在 PFCManager层面已经有 _initializing 标志,这里可以简化或移除) + # if conversation_instance._initialized or conversation_instance._initializing_flag_from_manager: # 假设 manager 设置了一个标志 + # logger.warning(f"[私聊][{conversation_instance.private_name}] 尝试重复初始化或正在初始化中 (initializer)。") + # return + + # conversation_instance._initializing_flag_from_manager = True # 标记开始初始化 + logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) 开始初始化对话实例核心组件: {conversation_instance.stream_id}") + + try: + # 1. 初始化核心功能组件 + logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 ActionPlanner...") + conversation_instance.action_planner = ActionPlanner(conversation_instance.stream_id, conversation_instance.private_name) + + conversation_instance.relationship_updater = PfcRelationshipUpdater( + private_name=conversation_instance.private_name, + bot_name=global_config.BOT_NICKNAME + ) + conversation_instance.relationship_translator = PfcRepationshipTranslator(private_name=conversation_instance.private_name) + logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) PfcRelationship 初始化完成。") + + conversation_instance.emotion_updater = PfcEmotionUpdater( + private_name=conversation_instance.private_name, + bot_name=global_config.BOT_NICKNAME + ) + logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) PfcEmotion 初始化完成。") + + logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 GoalAnalyzer...") + conversation_instance.goal_analyzer = GoalAnalyzer(conversation_instance.stream_id, conversation_instance.private_name) + + logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 ReplyGenerator...") + conversation_instance.reply_generator = ReplyGenerator(conversation_instance.stream_id, conversation_instance.private_name) + + logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 KnowledgeFetcher...") + conversation_instance.knowledge_fetcher = KnowledgeFetcher(conversation_instance.private_name) + + logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 Waiter...") + conversation_instance.waiter = Waiter(conversation_instance.stream_id, conversation_instance.private_name) + + logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 DirectMessageSender...") + conversation_instance.direct_sender = DirectMessageSender(conversation_instance.private_name) + + logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 ReplyChecker...") + conversation_instance.reply_checker = ReplyChecker(conversation_instance.stream_id, conversation_instance.private_name) + + # 获取关联的 ChatStream + logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 获取 ChatStream...") + conversation_instance.chat_stream = chat_manager.get_stream(conversation_instance.stream_id) + if not conversation_instance.chat_stream: + logger.error( + f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化错误:无法从 chat_manager 获取 stream_id {conversation_instance.stream_id} 的 ChatStream。" + ) + raise ValueError(f"无法获取 stream_id {conversation_instance.stream_id} 的 ChatStream") + + logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 IdleConversationStarter...") + conversation_instance.idle_conversation_starter = IdleConversationStarter(conversation_instance.stream_id, conversation_instance.private_name) + + # 2. 初始化信息存储和观察组件 + logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 获取 ChatObserver 实例...") + conversation_instance.chat_observer = ChatObserver.get_instance(conversation_instance.stream_id, conversation_instance.private_name) + + logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 ObservationInfo...") + conversation_instance.observation_info = ObservationInfo(conversation_instance.private_name) + if not conversation_instance.observation_info.bot_id: # 确保 ObservationInfo 知道机器人的 ID + logger.warning(f"[私聊][{conversation_instance.private_name}] (Initializer) ObservationInfo 未能自动获取 bot_id,尝试手动设置。") + conversation_instance.observation_info.bot_id = conversation_instance.bot_qq_str + + logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 ConversationInfo...") + conversation_instance.conversation_info = ConversationInfo() + + # 3. 绑定观察者和信息处理器 + logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 绑定 ObservationInfo 到 ChatObserver...") + if conversation_instance.observation_info and conversation_instance.chat_observer: # 确保二者都存在 + conversation_instance.observation_info.bind_to_chat_observer(conversation_instance.chat_observer) + + # 4. 加载初始聊天记录 (调用本文件内的函数) + await load_initial_history(conversation_instance) + + # 4.1 加载用户数据 + if conversation_instance.conversation_info and conversation_instance.chat_stream: # 确保 conversation_info 和 chat_stream 都存在 + person_id_tuple = await get_person_id( + private_name=conversation_instance.private_name, + chat_stream=conversation_instance.chat_stream, + ) + if person_id_tuple: # 确保元组不为空 + conversation_instance.conversation_info.person_id = person_id_tuple[0] # 第一个元素是 person_id + private_platform_str = person_id_tuple[1] + private_user_id_str = person_id_tuple[2] + logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) 获取到 person_id: {conversation_instance.conversation_info.person_id} for {private_platform_str}:{private_user_id_str}") + else: + logger.warning(f"[私聊][{conversation_instance.private_name}] (Initializer) 未能从 get_person_id 获取到 person_id 相关信息。") + + + # 5. 启动需要后台运行的组件 + logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 启动 ChatObserver...") + if conversation_instance.chat_observer: # 确保存在 + conversation_instance.chat_observer.start() + + if conversation_instance.idle_conversation_starter: + logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 启动 IdleConversationStarter...") + conversation_instance.idle_conversation_starter.start() + logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) 空闲对话检测器已启动") + + if conversation_instance.mood_mng and hasattr(conversation_instance.mood_mng, 'start_mood_update') and \ + not conversation_instance.mood_mng._running: # type: ignore + conversation_instance.mood_mng.start_mood_update(update_interval=global_config.mood_update_interval) # type: ignore + logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) MoodManager 已启动后台更新,间隔: {global_config.mood_update_interval} 秒。") + elif conversation_instance.mood_mng and conversation_instance.mood_mng._running: # type: ignore + logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) MoodManager 已在运行中。") + else: + logger.warning(f"[私聊][{conversation_instance.private_name}] (Initializer) MoodManager 未能启动,相关功能可能受限。") + + if conversation_instance.conversation_info and conversation_instance.conversation_info.person_id and \ + conversation_instance.relationship_translator and conversation_instance.person_info_mng: # 确保都存在 + try: + numeric_relationship_value = await conversation_instance.person_info_mng.get_value( + conversation_instance.conversation_info.person_id, "relationship_value" + ) + if not isinstance(numeric_relationship_value, (int, float)): + from bson.decimal128 import Decimal128 + if isinstance(numeric_relationship_value, Decimal128): + numeric_relationship_value = float(numeric_relationship_value.to_decimal()) + else: + numeric_relationship_value = 0.0 + conversation_instance.conversation_info.relationship_text = await conversation_instance.relationship_translator.translate_relationship_value_to_text(numeric_relationship_value) + logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化时加载关系文本: {conversation_instance.conversation_info.relationship_text}") + except Exception as e_init_rel: + logger.error(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化时加载关系文本出错: {e_init_rel}") + conversation_instance.conversation_info.relationship_text = "你们的关系是:普通。" + + if conversation_instance.conversation_info and conversation_instance.mood_mng: # 确保都存在 + try: + conversation_instance.conversation_info.current_emotion_text = conversation_instance.mood_mng.get_prompt() # type: ignore + logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化时加载情绪文本: {conversation_instance.conversation_info.current_emotion_text}") + except Exception as e_init_emo: + logger.error(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化时加载情绪文本出错: {e_init_emo}") + # 保留 ConversationInfo 中的默认值 + + # 6. 标记初始化成功并设置运行状态 (这些标志由PFCManager控制和检查) + # conversation_instance._initialized = True -> 由 manager 设置 + # conversation_instance.should_continue = True -> 由 manager 设置 + conversation_instance.state = ConversationState.ANALYZING # 设置初始状态为分析 + + logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) 对话实例 {conversation_instance.stream_id} 核心组件初始化完成。") + + except Exception as e: + logger.error(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化对话实例核心组件失败: {e}") + logger.error(f"[私聊][{conversation_instance.private_name}] (Initializer) {traceback.format_exc()}") + # conversation_instance.should_continue = False # 由 manager 处理 + # conversation_instance._initialized = False # 由 manager 处理 + # 外部(PFCManager)会捕获这个异常并处理 should_continue 和 _initialized 标志 + # 以及调用 conversation_instance.stop() + raise # 将异常重新抛出,通知 PFCManager 初始化失败 + # finally: + # conversation_instance._initializing_flag_from_manager = False # 清除标志 \ No newline at end of file diff --git a/src/plugins/PFC/conversation_loop.py b/src/plugins/PFC/conversation_loop.py new file mode 100644 index 00000000..dff5caf3 --- /dev/null +++ b/src/plugins/PFC/conversation_loop.py @@ -0,0 +1,236 @@ +import time +import asyncio +import datetime +import traceback +from typing import Dict, Any, Optional, Set, List, TYPE_CHECKING +from dateutil import tz + +from src.common.logger_manager import get_logger +from src.config.config import global_config +from .pfc_types import ConversationState # 需要导入 ConversationState +from . import actions # 需要导入 actions 模块 + +if TYPE_CHECKING: + from .conversation import Conversation + +logger = get_logger("pfc_loop") + +# 时区配置 (从 conversation.py 移过来,或者考虑放到更全局的配置模块) +configured_tz = getattr(global_config, 'TIME_ZONE', 'Asia/Shanghai') +TIME_ZONE = tz.gettz(configured_tz) +if TIME_ZONE is None: + logger.error(f"配置的时区 '{configured_tz}' 无效,将使用默认时区 'Asia/Shanghai'") + TIME_ZONE = tz.gettz('Asia/Shanghai') + + +async def run_conversation_loop(conversation_instance: 'Conversation'): + """ + 核心的规划与行动循环 (PFC Loop)。 + 之前是 Conversation 类中的 _plan_and_action_loop 方法。 + """ + logger.info(f"[私聊][{conversation_instance.private_name}] 进入 run_conversation_loop 循环。") + + if not conversation_instance._initialized: + logger.error(f"[私聊][{conversation_instance.private_name}] 尝试在未初始化状态下运行规划循环,退出。") + return + + force_reflect_and_act = False # 用于强制使用反思 prompt 的标志 + + while conversation_instance.should_continue: + loop_iter_start_time = time.time() + logger.debug(f"[私聊][{conversation_instance.private_name}] 开始新一轮循环迭代 ({loop_iter_start_time:.2f})") + + # 更新当前时间 + try: + global TIME_ZONE # 引用全局 TIME_ZONE + if TIME_ZONE is None: # 如果还未加载成功 + configured_tz_loop = getattr(global_config, 'TIME_ZONE', 'Asia/Shanghai') + TIME_ZONE = tz.gettz(configured_tz_loop) + if TIME_ZONE is None: + logger.error(f"循环中: 配置的时区 '{configured_tz_loop}' 无效,将使用 'Asia/Shanghai'") + TIME_ZONE = tz.gettz('Asia/Shanghai') + + current_time_dt = datetime.datetime.now(TIME_ZONE) + if conversation_instance.observation_info: + time_str = current_time_dt.strftime("%Y-%m-%d %H:%M:%S %Z%z") + conversation_instance.observation_info.current_time_str = time_str + logger.debug(f"[私聊][{conversation_instance.private_name}] 更新 ObservationInfo 当前时间: {time_str}") + else: + logger.warning(f"[私聊][{conversation_instance.private_name}] ObservationInfo 未初始化,无法更新当前时间。") + except Exception as time_update_err: + logger.error(f"[私聊][{conversation_instance.private_name}] 更新 ObservationInfo 当前时间时出错: {time_update_err}") + + # 处理忽略状态 + if conversation_instance.ignore_until_timestamp and loop_iter_start_time < conversation_instance.ignore_until_timestamp: + if conversation_instance.idle_conversation_starter and conversation_instance.idle_conversation_starter._running: + conversation_instance.idle_conversation_starter.stop() + logger.debug(f"[私聊][{conversation_instance.private_name}] 对话被暂时忽略,暂停空闲对话检测") + sleep_duration = min(30, conversation_instance.ignore_until_timestamp - loop_iter_start_time) + await asyncio.sleep(sleep_duration) + continue + elif conversation_instance.ignore_until_timestamp and loop_iter_start_time >= conversation_instance.ignore_until_timestamp: + logger.info(f"[私聊][{conversation_instance.private_name}] 忽略时间已到 {conversation_instance.stream_id},准备结束对话。") + conversation_instance.ignore_until_timestamp = None + await conversation_instance.stop() # 调用 Conversation 实例的 stop 方法 + continue + else: + if conversation_instance.idle_conversation_starter and not conversation_instance.idle_conversation_starter._running: + conversation_instance.idle_conversation_starter.start() + logger.debug(f"[私聊][{conversation_instance.private_name}] 恢复空闲对话检测") + + # 核心规划与行动逻辑 + try: + # 更新关系和情绪文本 (在每次循环开始时进行) + if conversation_instance.conversation_info and conversation_instance._initialized: + # 更新关系 + if conversation_instance.conversation_info.person_id and conversation_instance.relationship_translator and conversation_instance.person_info_mng: + try: + numeric_relationship_value = await conversation_instance.person_info_mng.get_value( + conversation_instance.conversation_info.person_id, "relationship_value" + ) + if not isinstance(numeric_relationship_value, (int, float)): + from bson.decimal128 import Decimal128 + if isinstance(numeric_relationship_value, Decimal128): + numeric_relationship_value = float(numeric_relationship_value.to_decimal()) + else: + numeric_relationship_value = 0.0 + conversation_instance.conversation_info.relationship_text = await conversation_instance.relationship_translator.translate_relationship_value_to_text(numeric_relationship_value) + except Exception as e_rel: + logger.error(f"[私聊][{conversation_instance.private_name}] (Loop) 更新关系文本时出错: {e_rel}") + conversation_instance.conversation_info.relationship_text = "你们的关系是:普通。" + # 更新情绪 + if conversation_instance.mood_mng: + conversation_instance.conversation_info.current_emotion_text = conversation_instance.mood_mng.get_prompt() # type: ignore + + # 检查核心组件 + if not all([conversation_instance.action_planner, conversation_instance.observation_info, conversation_instance.conversation_info]): + logger.error(f"[私聊][{conversation_instance.private_name}] 核心组件未初始化,无法继续规划循环。将等待5秒后重试...") + await asyncio.sleep(5) + continue + + # 规划 + planning_start_time = time.time() + logger.debug(f"[私聊][{conversation_instance.private_name}] --- (Loop) 开始规划 ({planning_start_time:.2f}) ---") + if conversation_instance.conversation_info: + conversation_instance.conversation_info.other_new_messages_during_planning_count = 0 + + action, reason = await conversation_instance.action_planner.plan( + conversation_instance.observation_info, + conversation_instance.conversation_info, + conversation_instance.conversation_info.last_successful_reply_action if conversation_instance.conversation_info else None, + use_reflect_prompt=force_reflect_and_act + ) + force_reflect_and_act = False + logger.debug( + f"[私聊][{conversation_instance.private_name}] (Loop) ActionPlanner.plan 完成,初步规划动作: {action}" + ) + + # 检查中断 + current_unprocessed_messages = getattr(conversation_instance.observation_info, "unprocessed_messages", []) + new_messages_during_planning: List[Dict[str, Any]] = [] + other_new_messages_during_planning: List[Dict[str, Any]] = [] + + for msg in current_unprocessed_messages: + msg_time = msg.get("time") + sender_id_info = msg.get("user_info", {}) + sender_id = str(sender_id_info.get("user_id")) if sender_id_info else None + if msg_time and msg_time >= planning_start_time: + new_messages_during_planning.append(msg) + if sender_id != conversation_instance.bot_qq_str: + other_new_messages_during_planning.append(msg) + + new_msg_count = len(new_messages_during_planning) + other_new_msg_count = len(other_new_messages_during_planning) + + if conversation_instance.conversation_info and other_new_msg_count > 0: + conversation_instance.conversation_info.current_instance_message_count += other_new_msg_count + # 触发关系和情绪更新(如果需要) + if conversation_instance.relationship_updater and conversation_instance.observation_info and conversation_instance.chat_observer: + await conversation_instance.relationship_updater.update_relationship_incremental( + conversation_info=conversation_instance.conversation_info, + observation_info=conversation_instance.observation_info, + chat_observer_for_history=conversation_instance.chat_observer + ) + if conversation_instance.emotion_updater and other_new_messages_during_planning and conversation_instance.observation_info and conversation_instance.chat_observer: + last_user_msg = other_new_messages_during_planning[-1] + last_user_msg_text = last_user_msg.get("processed_plain_text", "用户发了新消息") + sender_name_for_event = getattr(conversation_instance.observation_info, 'sender_name', '对方') + event_desc = f"用户【{sender_name_for_event}】发送了新消息: '{last_user_msg_text[:30]}...'" + await conversation_instance.emotion_updater.update_emotion_based_on_context( + conversation_info=conversation_instance.conversation_info, + observation_info=conversation_instance.observation_info, + chat_observer_for_history=conversation_instance.chat_observer, + event_description=event_desc + ) + + should_interrupt: bool = False + interrupt_reason: str = "" + if action in ["wait", "listening"] and new_msg_count > 0: + should_interrupt = True + interrupt_reason = f"规划 {action} 期间收到 {new_msg_count} 条新消息" + elif other_new_msg_count > 2: # Threshold for other actions + should_interrupt = True + interrupt_reason = f"规划 {action} 期间收到 {other_new_msg_count} 条来自他人的新消息" + + if should_interrupt: + logger.info(f"[私聊][{conversation_instance.private_name}] (Loop) 中断 '{action}',原因: {interrupt_reason}。重新规划...") + cancel_record = { "action": action, "plan_reason": reason, "status": "cancelled_due_to_new_messages", "time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "final_reason": interrupt_reason, } + if conversation_instance.conversation_info: + if not hasattr(conversation_instance.conversation_info, "done_action") or conversation_instance.conversation_info.done_action is None: conversation_instance.conversation_info.done_action = [] + conversation_instance.conversation_info.done_action.append(cancel_record) + conversation_instance.conversation_info.last_successful_reply_action = None + conversation_instance.state = ConversationState.ANALYZING + await asyncio.sleep(0.1) + continue + + # 执行动作 (调用 actions 模块的函数) + logger.debug(f"[私聊][{conversation_instance.private_name}] (Loop) 未中断,调用 actions.handle_action 执行动作 '{action}'...") + if conversation_instance.conversation_info: + conversation_instance.conversation_info.other_new_messages_during_planning_count = other_new_msg_count + + await actions.handle_action(conversation_instance, action, reason, conversation_instance.observation_info, conversation_instance.conversation_info) + logger.debug(f"[私聊][{conversation_instance.private_name}] (Loop) actions.handle_action 完成。") + + # 检查是否需要反思 + last_action_record = {} + if conversation_instance.conversation_info and conversation_instance.conversation_info.done_action: + last_action_record = conversation_instance.conversation_info.done_action[-1] + if last_action_record.get("action") == "send_new_message" and last_action_record.get("status") == "done_no_reply": + logger.info(f"[私聊][{conversation_instance.private_name}] (Loop) 检测到需反思,设置标志。") + force_reflect_and_act = True + + # 检查结束条件 + goal_ended: bool = False + if conversation_instance.conversation_info and hasattr(conversation_instance.conversation_info, "goal_list") and conversation_instance.conversation_info.goal_list: + last_goal_item = conversation_instance.conversation_info.goal_list[-1] + current_goal = last_goal_item.get("goal") if isinstance(last_goal_item, dict) else (last_goal_item if isinstance(last_goal_item, str) else None) + if current_goal == "结束对话": goal_ended = True + + last_action_record_for_end_check = {} + if conversation_instance.conversation_info and conversation_instance.conversation_info.done_action: + last_action_record_for_end_check = conversation_instance.conversation_info.done_action[-1] + action_ended: bool = ( last_action_record_for_end_check.get("action") in ["end_conversation", "say_goodbye"] and last_action_record_for_end_check.get("status") == "done" ) + + if goal_ended or action_ended: + logger.info( f"[私聊][{conversation_instance.private_name}] (Loop) 检测到结束条件,停止循环。" ) + await conversation_instance.stop() # 调用 Conversation 的 stop + continue # 虽然会 break,但 continue 更明确 + + except asyncio.CancelledError: + logger.info(f"[私聊][{conversation_instance.private_name}] (Loop) PFC 主循环任务被取消。") + await conversation_instance.stop() # 调用 Conversation 的 stop + break + except Exception as loop_err: + logger.error(f"[私聊][{conversation_instance.private_name}] (Loop) PFC 主循环出错: {loop_err}") + logger.error(f"[私聊][{conversation_instance.private_name}] (Loop) {traceback.format_exc()}") + conversation_instance.state = ConversationState.ERROR + await asyncio.sleep(5) + + # 控制循环频率 + loop_duration = time.time() - loop_iter_start_time + min_loop_interval = 0.1 + logger.debug(f"[私聊][{conversation_instance.private_name}] (Loop) 循环迭代耗时: {loop_duration:.3f} 秒。") + if loop_duration < min_loop_interval: + await asyncio.sleep(min_loop_interval - loop_duration) + + logger.info(f"[私聊][{conversation_instance.private_name}] (Loop) PFC 循环已退出 for stream_id: {conversation_instance.stream_id}") \ No newline at end of file diff --git a/src/plugins/PFC/pfc_manager.py b/src/plugins/PFC/pfc_manager.py index dadf31f9..891e1906 100644 --- a/src/plugins/PFC/pfc_manager.py +++ b/src/plugins/PFC/pfc_manager.py @@ -1,10 +1,13 @@ import time -import asyncio # 引入 asyncio +import asyncio import traceback from typing import Dict, Optional from src.common.logger import get_module_logger from .conversation import Conversation +from .conversation_initializer import initialize_core_components +# >>> 新增导入 <<< +from .pfc_types import ConversationState # 导入 ConversationState logger = get_module_logger("pfc_manager") @@ -12,16 +15,12 @@ logger = get_module_logger("pfc_manager") class PFCManager: """PFC对话管理器,负责管理所有对话实例""" - # 单例模式 _instance = None - - # 会话实例管理 _instances: Dict[str, Conversation] = {} - _initializing: Dict[str, bool] = {} # 用于防止并发初始化同一个 stream_id + _initializing: Dict[str, bool] = {} # 用于防止并发初始化同一个 stream_id @classmethod def get_instance(cls) -> "PFCManager": - """获取管理器单例""" if cls._instance is None: cls._instance = PFCManager() return cls._instance @@ -29,115 +28,107 @@ class PFCManager: async def get_or_create_conversation(self, stream_id: str, private_name: str) -> Optional[Conversation]: """获取或创建对话实例,并确保其启动""" - # 检查是否正在初始化 (防止并发问题) if self._initializing.get(stream_id, False): logger.debug(f"[私聊][{private_name}] 会话实例正在初始化中,请稍候: {stream_id}") - # 可以选择等待一小段时间或直接返回 None - await asyncio.sleep(0.5) # 短暂等待,让初始化有机会完成 - # 再次检查实例是否存在 + await asyncio.sleep(0.5) if stream_id in self._instances and self._instances[stream_id]._initialized: logger.debug(f"[私聊][{private_name}] 初始化已完成,返回现有实例: {stream_id}") return self._instances[stream_id] else: logger.warning(f"[私聊][{private_name}] 等待后实例仍未初始化完成或不存在。") - return None # 避免返回未完成的实例 + return None - # 检查是否已有活动实例 if stream_id in self._instances: instance = self._instances[stream_id] - # 检查忽略状态 if ( hasattr(instance, "ignore_until_timestamp") and instance.ignore_until_timestamp and time.time() < instance.ignore_until_timestamp ): logger.debug(f"[私聊][{private_name}] 会话实例当前处于忽略状态: {stream_id}") - return None # 处于忽略状态,不返回实例 + return None - # 检查是否已初始化且应继续运行 if instance._initialized and instance.should_continue: logger.debug(f"[私聊][{private_name}] 使用现有活动会话实例: {stream_id}") return instance else: - # 如果实例存在但未初始化或不应继续,清理旧实例 logger.warning(f"[私聊][{private_name}] 发现无效或已停止的旧实例,清理并重新创建: {stream_id}") await self._cleanup_conversation(instance) - # 从字典中移除,确保下面能创建新的 if stream_id in self._instances: del self._instances[stream_id] - if stream_id in self._initializing: + if stream_id in self._initializing: # 确保也从这里移除 del self._initializing[stream_id] - # --- 创建并初始化新实例 --- + conversation_instance: Optional[Conversation] = None try: logger.info(f"[私聊][{private_name}] 创建新的对话实例: {stream_id}") - self._initializing[stream_id] = True # 标记开始初始化 + self._initializing[stream_id] = True - # 创建实例 conversation_instance = Conversation(stream_id, private_name) - self._instances[stream_id] = conversation_instance # 立即存入字典 + self._instances[stream_id] = conversation_instance - # **启动实例初始化** - # _initialize_conversation 会调用 conversation._initialize() - await self._initialize_conversation(conversation_instance) + # 调用初始化包装器 + await self._initialize_conversation_wrapper(conversation_instance) - # --- 关键修复:在初始化成功后调用 start() --- + # 检查初始化结果并启动 if conversation_instance._initialized and conversation_instance.should_continue: logger.info(f"[私聊][{private_name}] 初始化成功,调用 conversation.start() 启动主循环...") - await conversation_instance.start() # 确保调用 start 方法 + await conversation_instance.start() # start 方法内部会创建 loop 任务 else: - # 如果 _initialize_conversation 内部初始化失败 logger.error(f"[私聊][{private_name}] 初始化未成功完成,无法启动实例 {stream_id}。") - # 清理可能部分创建的实例 await self._cleanup_conversation(conversation_instance) - if stream_id in self._instances: + if stream_id in self._instances: # 再次检查以防万一 del self._instances[stream_id] - conversation_instance = None # 返回 None 表示失败 + conversation_instance = None except Exception as e: logger.error(f"[私聊][{private_name}] 创建或启动会话实例时发生严重错误: {stream_id}, 错误: {e}") logger.error(traceback.format_exc()) - # 确保清理 if conversation_instance: await self._cleanup_conversation(conversation_instance) if stream_id in self._instances: del self._instances[stream_id] - conversation_instance = None # 返回 None - + conversation_instance = None finally: - # 确保初始化标记被清除 - if stream_id in self._initializing: - self._initializing[stream_id] = False + if stream_id in self._initializing: # 确保在 finally 中也检查 + self._initializing[stream_id] = False # 清除初始化标记 return conversation_instance - async def _initialize_conversation(self, conversation: Conversation): - """(内部方法) 初始化会话实例的核心逻辑""" + async def _initialize_conversation_wrapper(self, conversation: Conversation): + """ + (内部方法) 初始化会话实例的核心逻辑包装器。 + """ stream_id = conversation.stream_id private_name = conversation.private_name try: - logger.info(f"[私聊][{private_name}] 管理器开始调用 conversation._initialize(): {stream_id}") - await conversation._initialize() # 调用实例自身的初始化方法 - # 注意:初始化成功与否由 conversation._initialized 和 conversation.should_continue 标志决定 - if conversation._initialized: + logger.info(f"[私聊][{private_name}] Manager 开始调用 initialize_core_components(): {stream_id}") + await initialize_core_components(conversation) + + # 检查初始化函数执行后的状态 + if conversation.state != ConversationState.INIT and conversation.state != ConversationState.ERROR: + conversation._initialized = True + conversation.should_continue = True logger.info( - f"[私聊][{private_name}] conversation._initialize() 调用完成,实例标记为已初始化: {stream_id}" + f"[私聊][{private_name}] initialize_core_components() 调用完成,实例标记为已初始化且可继续: {stream_id}" ) else: + conversation._initialized = False + conversation.should_continue = False logger.warning( - f"[私聊][{private_name}] conversation._initialize() 调用完成,但实例未成功标记为已初始化: {stream_id}" + f"[私聊][{private_name}] initialize_core_components() 调用完成,但实例状态为 {conversation.state.name},标记为未初始化或不可继续: {stream_id}" ) except Exception as e: - # _initialize 内部应该处理自己的异常,但这里也捕获以防万一 logger.error( - f"[私聊][{private_name}] 调用 conversation._initialize() 时发生未捕获错误: {stream_id}, 错误: {e}" + f"[私聊][{private_name}] 调用 initialize_core_components() 时发生未捕获错误: {stream_id}, 错误: {e}" ) logger.error(traceback.format_exc()) - # 确保实例状态反映失败 conversation._initialized = False conversation.should_continue = False + # >>> 修改:在捕获到异常时设置 ERROR 状态 <<< + conversation.state = ConversationState.ERROR async def _cleanup_conversation(self, conversation: Conversation): """清理会话实例的资源""" @@ -147,17 +138,10 @@ class PFCManager: private_name = conversation.private_name logger.info(f"[私聊][{private_name}] 开始清理会话实例资源: {stream_id}") try: - # 调用 conversation 的 stop 方法来停止其内部组件 if hasattr(conversation, "stop") and callable(conversation.stop): - await conversation.stop() # stop 方法应处理内部组件的停止 + await conversation.stop() else: - logger.warning(f"[私聊][{private_name}] Conversation 对象缺少 stop 方法,可能无法完全清理资源。") - # 尝试手动停止已知组件 (作为后备) - if hasattr(conversation, "idle_conversation_starter") and conversation.idle_conversation_starter: - conversation.idle_conversation_starter.stop() - if hasattr(conversation, "observation_info") and conversation.observation_info: - conversation.observation_info.unbind_from_chat_observer() - # ChatObserver 是单例,不在此处停止 + logger.warning(f"[私聊][{private_name}] Conversation 对象缺少 stop 方法。") logger.info(f"[私聊][{private_name}] 会话实例 {stream_id} 资源已清理") except Exception as e: @@ -168,15 +152,14 @@ class PFCManager: """获取已存在的会话实例 (只读)""" instance = self._instances.get(stream_id) if instance and instance._initialized and instance.should_continue: - # 检查忽略状态 if ( hasattr(instance, "ignore_until_timestamp") and instance.ignore_until_timestamp and time.time() < instance.ignore_until_timestamp ): - return None # 忽略期间不返回 + return None return instance - return None # 不存在或无效则返回 None + return None async def remove_conversation(self, stream_id: str): """移除并清理会话实例""" @@ -184,15 +167,13 @@ class PFCManager: instance_to_remove = self._instances[stream_id] logger.info(f"[管理器] 准备移除并清理会话实例: {stream_id}") try: - # 先从字典中移除引用,防止新的请求获取到正在清理的实例 del self._instances[stream_id] if stream_id in self._initializing: del self._initializing[stream_id] - # 清理资源 await self._cleanup_conversation(instance_to_remove) logger.info(f"[管理器] 会话实例 {stream_id} 已成功移除并清理") except Exception as e: logger.error(f"[管理器] 移除或清理会话实例 {stream_id} 时失败: {e}") logger.error(traceback.format_exc()) else: - logger.warning(f"[管理器] 尝试移除不存在的会话实例: {stream_id}") + logger.warning(f"[管理器] 尝试移除不存在的会话实例: {stream_id}") \ No newline at end of file From dab75f5e1f97c4844b59f1c58d4a6aa32a023a1d Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Thu, 8 May 2025 01:35:04 +0800 Subject: [PATCH 39/63] fix --- src/plugins/PFC/pfc_relationship.py | 18 +-- src/plugins/PFC/pfc_utils.py | 221 +++++++++++++++++----------- 2 files changed, 143 insertions(+), 96 deletions(-) diff --git a/src/plugins/PFC/pfc_relationship.py b/src/plugins/PFC/pfc_relationship.py index 9457a000..4dfee52c 100644 --- a/src/plugins/PFC/pfc_relationship.py +++ b/src/plugins/PFC/pfc_relationship.py @@ -98,16 +98,13 @@ class PfcRelationshipUpdater: current_relationship_value = await self.person_info_mng.get_value(conversation_info.person_id, "relationship_value") current_relationship_value = self.relationship_mng.ensure_float(current_relationship_value, conversation_info.person_id) - sender_name_for_prompt = getattr(observation_info, 'sender_name', '对方') - if not sender_name_for_prompt: sender_name_for_prompt = '对方' - - relationship_prompt = f"""你是{self.bot_name}。你正在与{sender_name_for_prompt}私聊。 + relationship_prompt = f"""你是{self.bot_name}。你正在与{self.private_name}私聊。 你们当前的关系值大约是 {current_relationship_value:.0f} (范围通常在-1000到1000,越高越代表关系越好)。 以下是你们最近的对话内容: --- {readable_history_for_llm} --- -请基于以上对话,判断你与{sender_name_for_prompt}的关系值应该如何“谨慎地”调整。 +请基于以上对话,判断你与{self.private_name}的关系值应该如何“谨慎地”调整。 请输出一个JSON对象,包含一个 "adjustment" 字段,其值为一个介于 -{self.REL_INCREMENTAL_MAX_CHANGE} 和 +{self.REL_INCREMENTAL_MAX_CHANGE} 之间的整数,代表关系值的变化。 例如:{{ "adjustment": 3 }}。如果对话内容不明确或难以判断,请倾向于输出较小的调整值(如0, 1, -1)。""" @@ -133,7 +130,7 @@ class PfcRelationshipUpdater: new_relationship_value = max(-1000.0, min(1000.0, current_relationship_value + adjustment_val)) await self.person_info_mng.update_one_field(conversation_info.person_id, "relationship_value", new_relationship_value) - logger.info(f"[私聊][{self.private_name}] 增量关系值更新:与【{sender_name_for_prompt}】的关系值从 {current_relationship_value:.2f} 调整了 {adjustment_val:.2f},变为 {new_relationship_value:.2f}") + logger.info(f"[私聊][{self.private_name}] 增量关系值更新:与【{self.private_name}】的关系值从 {current_relationship_value:.2f} 调整了 {adjustment_val:.2f},变为 {new_relationship_value:.2f}") if conversation_info.person_id: conversation_info.relationship_text = await self.relationship_mng.build_relationship_info(conversation_info.person_id, is_id=True) @@ -170,16 +167,13 @@ class PfcRelationshipUpdater: current_relationship_value = await self.person_info_mng.get_value(conversation_info.person_id, "relationship_value") current_relationship_value = self.relationship_mng.ensure_float(current_relationship_value, conversation_info.person_id) - sender_name_for_prompt = getattr(observation_info, 'sender_name', '对方') - if not sender_name_for_prompt: sender_name_for_prompt = '对方' - - relationship_prompt = f"""你是{self.bot_name}。你与{sender_name_for_prompt}的私聊刚刚结束。 + relationship_prompt = f"""你是{self.bot_name}。你与{self.private_name}的私聊刚刚结束。 你们当前的关系值大约是 {current_relationship_value:.0f} (范围通常在-1000到1000,越高越好)。 以下是你们本次私聊最后部分的对话内容: --- {readable_history_for_llm} --- -请基于以上对话的整体情况,判断你与【{sender_name_for_prompt}】的关系值应该如何进行一次总结性的调整。 +请基于以上对话的整体情况,判断你与【{self.private_name}】的关系值应该如何进行一次总结性的调整。 请输出一个JSON对象,包含一个 "final_adjustment" 字段,其值为一个整数,代表关系值的变化量(例如,可以是 -{self.REL_FINAL_MAX_CHANGE} 到 +{self.REL_FINAL_MAX_CHANGE} 之间的一个值)。 请大胆评估,但也要合理。""" @@ -205,7 +199,7 @@ class PfcRelationshipUpdater: new_relationship_value = max(-1000.0, min(1000.0, current_relationship_value + adjustment_val)) await self.person_info_mng.update_one_field(conversation_info.person_id, "relationship_value", new_relationship_value) - logger.info(f"[私聊][{self.private_name}] 最终关系值更新:与【{sender_name_for_prompt}】的关系值从 {current_relationship_value:.2f} 调整了 {adjustment_val:.2f},最终为 {new_relationship_value:.2f}") + logger.info(f"[私聊][{self.private_name}] 最终关系值更新:与【{self.private_name}】的关系值从 {current_relationship_value:.2f} 调整了 {adjustment_val:.2f},最终为 {new_relationship_value:.2f}") if conversation_info.person_id: # 虽然通常结束了,但更新一下无妨 conversation_info.relationship_text = await self.relationship_mng.build_relationship_info(conversation_info.person_id, is_id=True) diff --git a/src/plugins/PFC/pfc_utils.py b/src/plugins/PFC/pfc_utils.py index 920566ac..2f267739 100644 --- a/src/plugins/PFC/pfc_utils.py +++ b/src/plugins/PFC/pfc_utils.py @@ -100,104 +100,159 @@ def get_items_from_json( Returns: Tuple[bool, Union[Dict[str, Any], List[Dict[str, Any]]]]: (是否成功, 提取的字段字典或字典列表) """ - content = content.strip() - result = {} + cleaned_content = content.strip() + result: Union[Dict[str, Any], List[Dict[str, Any]]] = {} # 初始化类型 + # 匹配 ```json ... ``` 或 ``` ... ``` + markdown_match = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", cleaned_content, re.IGNORECASE) + if markdown_match: + cleaned_content = markdown_match.group(1).strip() + logger.debug(f"[私聊][{private_name}] 已去除 Markdown 标记,剩余内容: {cleaned_content[:100]}...") + # --- 新增结束 --- + # 设置默认值 + default_result: Dict[str, Any] = {} # 用于单对象时的默认值 if default_values: - result.update(default_values) + default_result.update(default_values) + result = default_result.copy() # 先用默认值初始化 # 首先尝试解析为JSON数组 if allow_array: try: - # 尝试找到文本中的JSON数组 - array_pattern = r"\[[\s\S]*\]" - array_match = re.search(array_pattern, content) - if array_match: - array_content = array_match.group() - json_array = json.loads(array_content) + # 尝试直接解析清理后的内容为列表 + json_array = json.loads(cleaned_content) - # 确认是数组类型 - if isinstance(json_array, list): - # 验证数组中的每个项目是否包含所有必需字段 - valid_items = [] - for item in json_array: - if not isinstance(item, dict): - continue + if isinstance(json_array, list): + valid_items_list: List[Dict[str, Any]] = [] + for item in json_array: + if not isinstance(item, dict): + logger.warning(f"[私聊][{private_name}] JSON数组中的元素不是字典: {item}") + continue - # 检查是否有所有必需字段 - if all(field in item for field in items): - # 验证字段类型 - if required_types: - type_valid = True - for field, expected_type in required_types.items(): - if field in item and not isinstance(item[field], expected_type): - type_valid = False - break + current_item_result = default_result.copy() # 每个元素都用默认值初始化 + valid_item = True - if not type_valid: - continue + # 提取并验证字段 + for field in items: + if field in item: + current_item_result[field] = item[field] + elif field not in default_result: # 如果字段不存在且没有默认值 + logger.warning(f"[私聊][{private_name}] JSON数组元素缺少必要字段 '{field}': {item}") + valid_item = False + break # 这个元素无效 - # 验证字符串字段不为空 - string_valid = True - for field in items: - if isinstance(item[field], str) and not item[field].strip(): - string_valid = False - break + if not valid_item: continue - if not string_valid: - continue + # 验证类型 + if required_types: + for field, expected_type in required_types.items(): + # 检查 current_item_result 中是否存在该字段 (可能来自 item 或 default_values) + if field in current_item_result and not isinstance(current_item_result[field], expected_type): + logger.warning(f"[私聊][{private_name}] JSON数组元素字段 '{field}' 类型错误 (应为 {expected_type.__name__}, 实际为 {type(current_item_result[field]).__name__}): {item}") + valid_item = False + break - valid_items.append(item) + if not valid_item: continue + + # 验证字符串不为空 (只检查 items 中要求的字段) + for field in items: + if field in current_item_result and isinstance(current_item_result[field], str) and not current_item_result[field].strip(): + logger.warning(f"[私聊][{private_name}] JSON数组元素字段 '{field}' 不能为空字符串: {item}") + valid_item = False + break + + if valid_item: + valid_items_list.append(current_item_result) # 只添加完全有效的项 + + if valid_items_list: # 只有当列表不为空时才认为是成功 + logger.debug(f"[私聊][{private_name}] 成功解析JSON数组,包含 {len(valid_items_list)} 个有效项目。") + return True, valid_items_list + else: + # 如果列表为空(可能所有项都无效),则继续尝试解析为单个对象 + logger.debug(f"[私聊][{private_name}] 解析为JSON数组,但未找到有效项目,尝试解析单个JSON对象。") + # result 重置回单个对象的默认值 + result = default_result.copy() - if valid_items: - return True, valid_items except json.JSONDecodeError: - logger.debug(f"[私聊][{private_name}]JSON数组解析失败,尝试解析单个JSON对象") + logger.debug(f"[私聊][{private_name}] JSON数组直接解析失败,尝试解析单个JSON对象") + # result 重置回单个对象的默认值 + result = default_result.copy() except Exception as e: - logger.debug(f"[私聊][{private_name}]尝试解析JSON数组时出错: {str(e)}") + logger.error(f"[私聊][{private_name}] 尝试解析JSON数组时发生未知错误: {str(e)}") + # result 重置回单个对象的默认值 + result = default_result.copy() - # 尝试解析JSON对象 + + # 尝试解析为单个JSON对象 try: - json_data = json.loads(content) + # 尝试直接解析清理后的内容 + json_data = json.loads(cleaned_content) + if not isinstance(json_data, dict): + logger.error(f"[私聊][{private_name}] 解析为单个对象,但结果不是字典类型: {type(json_data)}") + return False, default_result # 返回失败和默认值 + except json.JSONDecodeError: - # 如果直接解析失败,尝试查找和提取JSON部分 - json_pattern = r"\{[^{}]*\}" - json_match = re.search(json_pattern, content) + # 如果直接解析失败,尝试用正则表达式查找 JSON 对象部分 (作为后备) + # 这个正则比较简单,可能无法处理嵌套或复杂的 JSON + json_pattern = r"\{[\s\S]*?\}" # 使用非贪婪匹配 + json_match = re.search(json_pattern, cleaned_content) if json_match: try: - json_data = json.loads(json_match.group()) + potential_json_str = json_match.group() + json_data = json.loads(potential_json_str) + if not isinstance(json_data, dict): + logger.error(f"[私聊][{private_name}] 正则提取后解析,但结果不是字典类型: {type(json_data)}") + return False, default_result + logger.debug(f"[私聊][{private_name}] 通过正则提取并成功解析JSON对象。") except json.JSONDecodeError: - logger.error(f"[私聊][{private_name}]提取的JSON内容解析失败") - return False, result + logger.error(f"[私聊][{private_name}] 正则提取的部分 '{potential_json_str[:100]}...' 无法解析为JSON。") + return False, default_result else: - logger.error(f"[私聊][{private_name}]无法在返回内容中找到有效的JSON") - return False, result + logger.error(f"[私聊][{private_name}] 无法在返回内容中找到有效的JSON对象部分。原始内容: {cleaned_content[:100]}...") + return False, default_result - # 提取字段 + + # 提取并验证字段 (适用于单个JSON对象) + # 确保 result 是字典类型用于更新 + if not isinstance(result, dict): + result = default_result.copy() # 如果之前是列表,重置为字典 + + valid_single_object = True for item in items: if item in json_data: result[item] = json_data[item] + elif item not in default_result: # 如果字段不存在且没有默认值 + logger.error(f"[私聊][{private_name}] JSON对象缺少必要字段 '{item}'。JSON内容: {json_data}") + valid_single_object = False + break # 这个对象无效 - # 验证必需字段 - if not all(item in result for item in items): - logger.error(f"[私聊][{private_name}]JSON缺少必要字段,实际内容: {json_data}") - return False, result + if not valid_single_object: + return False, default_result - # 验证字段类型 + # 验证类型 if required_types: for field, expected_type in required_types.items(): if field in result and not isinstance(result[field], expected_type): - logger.error(f"[私聊][{private_name}]{field} 必须是 {expected_type.__name__} 类型") - return False, result + logger.error(f"[私聊][{private_name}] JSON对象字段 '{field}' 类型错误 (应为 {expected_type.__name__}, 实际为 {type(result[field]).__name__})") + valid_single_object = False + break - # 验证字符串字段不为空 + if not valid_single_object: + return False, default_result + + # 验证字符串不为空 (只检查 items 中要求的字段) for field in items: - if isinstance(result[field], str) and not result[field].strip(): - logger.error(f"[私聊][{private_name}]{field} 不能为空") - return False, result + if field in result and isinstance(result[field], str) and not result[field].strip(): + logger.error(f"[私聊][{private_name}] JSON对象字段 '{field}' 不能为空字符串") + valid_single_object = False + break + + if valid_single_object: + logger.debug(f"[私聊][{private_name}] 成功解析并验证了单个JSON对象。") + return True, result # 返回提取并验证后的字典 + else: + return False, default_result # 验证失败 - return True, result async def get_person_id(private_name: str, chat_stream: ChatStream): private_user_id_str: Optional[str] = None @@ -208,35 +263,33 @@ async def get_person_id(private_name: str, chat_stream: ChatStream): private_user_id_str = str(chat_stream.user_info.user_id) private_platform_str = chat_stream.user_info.platform logger.info(f"[私聊][{private_name}] 从 ChatStream 获取到私聊对象信息: ID={private_user_id_str}, Platform={private_platform_str}, Name={private_nickname_str}") - elif chat_stream.group_info is None and private_name: # 私聊场景,且 private_name 可能有用 - # 这是一个备选方案,如果 private_name 直接是 user_id - # 你需要确认 private_name 的确切含义和格式 <- private_name 就是对方昵称,格式是 str - # logger.warning(f"[私聊][{self.private_name}] 尝试使用 private_name ('{self.private_name}') 作为 user_id,平台默认为 'qq'") - # private_user_id_str = self.private_name - # private_platform_str = "qq" # 假设平台是qq - # private_nickname_str = self.private_name # 昵称也暂时用 private_name - pass # 暂时不启用此逻辑,依赖 observation_info 或 chat_stream.user_info + elif chat_stream.group_info is None and private_name: + pass if private_user_id_str and private_platform_str: try: - # 将 user_id 转换为整数类型,因为 person_info_manager.get_person_id 需要 int private_user_id_int = int(private_user_id_str) - person_id = person_info_manager.get_person_id( - private_platform_str, - private_user_id_int - # 使用转换后的整数ID - ) - # 确保用户在数据库中存在,如果不存在则创建 - # get_or_create_person 内部处理 person_id 的生成,所以我们直接传 platform 和 user_id - await person_info_manager.get_or_create_person( + # person_id = person_info_manager.get_person_id( # get_person_id 可能只查询,不创建 + # private_platform_str, + # private_user_id_int + # ) + # 使用 get_or_create_person 确保用户存在 + person_id = await person_info_manager.get_or_create_person( platform=private_platform_str, - user_id=private_user_id_int, # 使用转换后的整数ID - nickname=private_name, + user_id=private_user_id_int, + nickname=private_name, # 使用传入的 private_name 作为昵称 ) - return person_id, private_platform_str, private_user_id_str + if person_id is None: # 如果 get_or_create_person 返回 None,说明创建失败 + logger.error(f"[私聊][{private_name}] get_or_create_person 未能获取或创建 person_id。") + return None # 返回 None 表示失败 + + return person_id, private_platform_str, private_user_id_str # 返回获取或创建的 person_id except ValueError: logger.error(f"[私聊][{private_name}] 无法将 private_user_id_str ('{private_user_id_str}') 转换为整数。") + return None # 返回 None 表示失败 except Exception as e_pid: logger.error(f"[私聊][{private_name}] 获取或创建 person_id 时出错: {e_pid}") + return None # 返回 None 表示失败 else: - logger.warning(f"[私聊][{private_name}] 未能确定私聊对象的 user_id 或 platform,无法获取 person_id。将在收到消息后尝试。") \ No newline at end of file + logger.warning(f"[私聊][{private_name}] 未能确定私聊对象的 user_id 或 platform,无法获取 person_id。将在收到消息后尝试。") + return None # 返回 None 表示失败 \ No newline at end of file From e5fc903302b637b1dad45322d8dd2412e086652b Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Thu, 8 May 2025 03:11:51 +0800 Subject: [PATCH 40/63] =?UTF-8?q?=E5=A4=9A=E6=A0=B7=E5=8C=96=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E9=A3=8E=E6=A0=BC=EF=BC=88=E7=AC=AC=E4=B8=80=E7=89=88?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/reply_generator.py | 109 ++++++++++++++++++----------- 1 file changed, 67 insertions(+), 42 deletions(-) diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index f278a9bd..87250be2 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -1,3 +1,4 @@ +import random from .pfc_utils import retrieve_contextual_info # 可能用于旧知识库提取主题 (如果需要回退到旧方法) @@ -18,6 +19,35 @@ from src.plugins.utils.chat_message_builder import build_readable_messages logger = get_logger("reply_generator") +PROMPT_GER_VARIATIONS = [ + ("不用输出或提及提及对方的网名或绰号", 0.50), + ("如果当前对话比较轻松,可以尝试用轻松幽默或者略带调侃的语气回应,但要注意分寸", 0.8), + ("避免使用过于正式或书面化的词语,多用生活化的口语表达", 0.8), + ("如果对方的发言比较跳跃或难以理解,可以尝试用猜测或确认的语气回应", 0.8), + ("如果感觉对话有点干巴,可以尝试引入一些轻松的相关小话题或者自己的小想法,但不要偏离太远", 0.8), + ("注意观察对方的情绪(如果能从文字中判断),并作出适当的回应,比如安慰、鼓励或表示理解", 0.8), + ("", 0.10) +] + +REPLY_STYLE1_VARIATIONS = [ + ("整体风格可以平和、简短", 0.3), + ("回复可以非常简洁,有时甚至用单个词、短语或者一个反问就能表达清楚", 0.10), + ("尝试使用更自然的口语连接词,例如:然后/所以呢/不过嘛/倒是", 0.05), + ("在表达观点时,可以说得主观一些,例如:我觉得.../我个人感觉.../要我说...", 0.10), + ("**请省略主语,简短**", 0.4), + ("回复得认真一些", 0.05), +] + +REPLY_STYLE2_VARIATIONS = [ + ("结尾可以使用语气助词,例如:呀/噢/诶/哈/啦,让语气更生动", 0.10), + ("不要输出任何语气词", 0.10), + ("在适当的时候,可以用一些感叹词来表达情绪或态度,例如:哇/啊?/啧啧/哎呀", 0.05), + ("可以模糊化表达,例如:'我记得...'", 0.10), + ("对于一些无聊或者不想深入的话题,可以敷衍一下,例如:/哦这样啊/还行吧/随便啦", 0.10), + ("尽量用简单句和短句", 0.25), + ("不要输出任何标点符号,简短", 0.30), +] + # --- 定义 Prompt 模板 --- # Prompt for direct_reply (首次回复) @@ -51,8 +81,8 @@ PROMPT_DIRECT_REPLY = """ 5. 自然、得体,结合聊天记录逻辑合理,没有重复表达同质内容,也没有复读你之前的发言 请注意把握聊天内容,不要回复的太有条理,可以有个性。请分清"你"和对方说的话,不要把"你"说的话当做对方说的话,这是你自己说的话。 -可以回复得自然随意自然一些,就像真人一样,注意把握聊天内容,整体风格可以平和、简短,不要刻意突出自身学科背景,不要说你说过的话,可以简短,多简短都可以,但是避免冗长。 -请你注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出回复内容。 +可以回复得自然随意自然一些,就像真人一样,注意把握聊天内容,{reply_style1},不要刻意突出自身学科背景,不要说你说过的话,{reply_style2}。 +{prompt_ger},请你注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出回复内容。 不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。 请直接输出回复内容,不需要任何额外格式。""" @@ -89,7 +119,8 @@ PROMPT_SEND_NEW_MESSAGE = """ 5. 跟之前你发的消息自然的衔接,逻辑合理,没有重复表达同质内容或部分重叠内容,也没有复读你之前的发言 请注意把握聊天内容,不用太有条理,可以有个性。请分清"你"和对方说的话,不要把"你"说的话当做对方说的话,这是你自己说的话。 -这条消息可以自然随意自然一些,就像真人一样,注意把握聊天内容,整体风格可以平和、简短,不要刻意突出自身学科背景,不要说你说过的话,可以简短,多简短都可以,但是避免冗长。 +这条消息可以自然随意自然一些,就像真人一样,注意把握聊天内容,{reply_style1},不要刻意突出自身学科背景,不要说你说过的话,{reply_style2}。 +{prompt_ger}。 如果你决定继续发消息不合适,也可以不发送。 请严格按照以下JSON格式输出你的选择和消息内容,不要包含任何其他说明或非JSON文本: @@ -159,6 +190,10 @@ class ReplyGenerator: logger.debug( f"[私聊][{self.private_name}]开始生成回复 (动作类型: {action_type}):当前目标: {conversation_info.goal_list}" ) + + chosen_prompt_ger = random.choices([style[0] for style in PROMPT_GER_VARIATIONS], weights=[style[1] for style in PROMPT_GER_VARIATIONS], k=1)[0] + chosen_reply_style1 = random.choices([style[0] for style in REPLY_STYLE1_VARIATIONS], weights=[style[1] for style in REPLY_STYLE1_VARIATIONS], k=1)[0] + chosen_reply_style2 = random.choices([style[0] for style in REPLY_STYLE2_VARIATIONS], weights=[style[1] for style in REPLY_STYLE2_VARIATIONS], k=1)[0] # --- 构建通用 Prompt 参数 --- goals_str = "" @@ -269,42 +304,34 @@ class ReplyGenerator: if observation_info and hasattr(observation_info, 'current_time_str') and observation_info.current_time_str: current_time_value = observation_info.current_time_str - if action_type == "say_goodbye": - prompt = prompt_template.format( - persona_text=persona_text, - chat_history_text=chat_history_text, - current_time_str=current_time_value, - sender_name=sender_name_str, - relationship_text=relationship_text_str, - current_emotion_text=current_emotion_text_str - ) - elif action_type == "send_new_message": # PROMPT_SEND_NEW_MESSAGE 增加了 spam_warning_info - prompt = prompt_template.format( - persona_text=persona_text, - goals_str=goals_str, - chat_history_text=chat_history_text, - retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。", - retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。", - last_rejection_info=last_rejection_info_str, - current_time_str=current_time_value, - sender_name=sender_name_str, - relationship_text=relationship_text_str, - current_emotion_text=current_emotion_text_str, - spam_warning_info=spam_warning_message # 添加 spam_warning_info - ) - else: # PROMPT_DIRECT_REPLY (没有 spam_warning_info) - prompt = prompt_template.format( - persona_text=persona_text, - goals_str=goals_str, - chat_history_text=chat_history_text, - retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。", - retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。", - last_rejection_info=last_rejection_info_str, - current_time_str=current_time_value, - sender_name=sender_name_str, - relationship_text=relationship_text_str, - current_emotion_text=current_emotion_text_str - ) + format_params = { + "persona_text": persona_text, + "goals_str": goals_str, + "chat_history_text": chat_history_text, + "retrieved_memory_str": retrieved_memory_str if retrieved_memory_str else "无相关记忆。", + "retrieved_knowledge_str": retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。", + "last_rejection_info": last_rejection_info_str, + "current_time_str": current_time_value, + "sender_name": sender_name_str, + "relationship_text": relationship_text_str, + "current_emotion_text": current_emotion_text_str, + "prompt_ger": chosen_prompt_ger, + "reply_style1": chosen_reply_style1, + "reply_style2": chosen_reply_style2, + } + + if action_type == "send_new_message": + format_params["spam_warning_info"] = spam_warning_message + elif action_type == "say_goodbye": + farewell_params = { + key: format_params[key] for key in [ + "persona_text", "chat_history_text", "current_time_str", + "sender_name", "relationship_text", "current_emotion_text", + ] if key in format_params + } + prompt = prompt_template.format(**farewell_params) + else: # direct_reply + prompt = prompt_template.format(**format_params) except KeyError as e: logger.error( f"[私聊][{self.private_name}]格式化 Prompt 时出错,缺少键: {e}。请检查 Prompt 模板和传递的参数。" @@ -334,6 +361,4 @@ class ReplyGenerator: "txt": "LLM生成回复时出错" }}""".strip() else: - return "抱歉,我现在有点混乱,让我重新思考一下..." - - # check_reply 方法在 ReplyGenerator 中不再需要 \ No newline at end of file + return "抱歉,我现在有点混乱,让我重新思考一下..." \ No newline at end of file From d88a2787eacf971f4b2e97311e04a0b32c7fed9d Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Thu, 8 May 2025 03:30:52 +0800 Subject: [PATCH 41/63] fix --- src/plugins/PFC/reply_generator.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index 87250be2..2c12f7d2 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -304,34 +304,39 @@ class ReplyGenerator: if observation_info and hasattr(observation_info, 'current_time_str') and observation_info.current_time_str: current_time_value = observation_info.current_time_str - format_params = { + base_format_params = { "persona_text": persona_text, "goals_str": goals_str, "chat_history_text": chat_history_text, - "retrieved_memory_str": retrieved_memory_str if retrieved_memory_str else "无相关记忆。", - "retrieved_knowledge_str": retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。", + "retrieved_memory_str": retrieved_memory_str if retrieved_memory_str else "无相关记忆。", # 确保已定义 + "retrieved_knowledge_str": retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。", # 确保已定义 "last_rejection_info": last_rejection_info_str, "current_time_str": current_time_value, - "sender_name": sender_name_str, - "relationship_text": relationship_text_str, - "current_emotion_text": current_emotion_text_str, - "prompt_ger": chosen_prompt_ger, + "sender_name": sender_name_str, + "relationship_text": relationship_text_str, + "current_emotion_text": current_emotion_text_str, "reply_style1": chosen_reply_style1, "reply_style2": chosen_reply_style2, + "prompt_ger": chosen_prompt_ger, } if action_type == "send_new_message": - format_params["spam_warning_info"] = spam_warning_message + current_format_params = base_format_params.copy() + current_format_params["spam_warning_info"] = spam_warning_message + prompt = prompt_template.format(**current_format_params) elif action_type == "say_goodbye": farewell_params = { - key: format_params[key] for key in [ + k: v for k, v in base_format_params.items() if k in [ "persona_text", "chat_history_text", "current_time_str", "sender_name", "relationship_text", "current_emotion_text", - ] if key in format_params + ] } + prompt = prompt_template.format(**farewell_params) else: # direct_reply - prompt = prompt_template.format(**format_params) + current_format_params = base_format_params.copy() + prompt = prompt_template.format(**current_format_params) + except KeyError as e: logger.error( f"[私聊][{self.private_name}]格式化 Prompt 时出错,缺少键: {e}。请检查 Prompt 模板和传递的参数。" From 589045ba80dc866490bb74038e80c185c70802ea Mon Sep 17 00:00:00 2001 From: Bakadax Date: Thu, 8 May 2025 09:00:07 +0800 Subject: [PATCH 42/63] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=BC=A9=E8=BF=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/actions.py | 24 ++++++------- src/plugins/PFC/conversation.py | 2 +- src/plugins/PFC/conversation_initializer.py | 15 ++++---- src/plugins/PFC/conversation_loop.py | 2 +- src/plugins/PFC/pfc_relationship.py | 2 +- src/plugins/PFC/pfc_utils.py | 38 ++++++++++----------- src/plugins/PFC/reply_checker.py | 2 +- src/plugins/PFC/reply_generator.py | 4 +-- 8 files changed, 43 insertions(+), 46 deletions(-) diff --git a/src/plugins/PFC/actions.py b/src/plugins/PFC/actions.py index 386cd5f6..bda88516 100644 --- a/src/plugins/PFC/actions.py +++ b/src/plugins/PFC/actions.py @@ -74,10 +74,10 @@ async def handle_action( logger.error(f"[私聊][{conversation_instance.private_name}] ObservationInfo 为空,无法处理动作 '{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({ + 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 在这里是必需的 @@ -180,9 +180,9 @@ async def handle_action( # 检查生成的内容是否有效 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"或空 + 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": # 特殊情况日志 @@ -586,7 +586,7 @@ async def handle_action( 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( + 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 存在 @@ -634,7 +634,7 @@ async def handle_action( # 1. 重置临时存储的计数值 if conversation_info: # 确保 conversation_info 存在 - conversation_info.other_new_messages_during_planning_count = 0 + conversation_info.other_new_messages_during_planning_count = 0 # 2. 更新动作历史记录的最终状态和原因 # 优化:如果动作成功但状态仍是默认的 recall,则更新为 done @@ -659,7 +659,7 @@ async def handle_action( elif action in ["rethink_goal", "end_conversation", "block_and_ignore", "say_goodbye"]: final_reason = f"成功执行 {action}" elif action in ["direct_reply", "send_new_message"]: # 正常发送成功的case - final_reason = "成功发送" + final_reason = "成功发送" else: final_reason = f"动作 {action} 成功完成" # 如果已经是 "done" 或 "done_no_reply",则保留它们和它们对应的 final_reason @@ -683,7 +683,7 @@ async def handle_action( # 更新 done_action 中的记录 # 防御性检查,确保 conversation_info, done_action 存在,并且索引有效 if conversation_info and hasattr(conversation_info, 'done_action') and \ - conversation_info.done_action and action_index < len(conversation_info.done_action): + conversation_info.done_action and action_index < len(conversation_info.done_action): conversation_info.done_action[action_index].update( { "status": final_status, @@ -699,8 +699,8 @@ async def handle_action( log_final_reason = final_reason if final_reason else "无明确原因" # 为成功发送的动作添加发送内容摘要 if final_status == "done" and action_successful and \ - action in ["direct_reply", "send_new_message"] and \ - hasattr(conversation_instance, 'generated_reply') and conversation_instance.generated_reply: - log_final_reason += f" (发送内容: '{conversation_instance.generated_reply[:30]}...')" + action in ["direct_reply", "send_new_message"] and \ + hasattr(conversation_instance, 'generated_reply') and conversation_instance.generated_reply: + log_final_reason += f" (发送内容: '{conversation_instance.generated_reply[:30]}...')" logger.info(f"[私聊][{conversation_instance.private_name}] 动作 '{action}' 处理完成。最终状态: {final_status}, 原因: {log_final_reason}") \ No newline at end of file diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 74212dc7..2c87370c 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -126,7 +126,7 @@ class Conversation: # 最终关系评估 if self._initialized and self.relationship_updater and self.conversation_info and \ - self.observation_info and self.chat_observer: + self.observation_info and self.chat_observer: try: logger.info(f"[私聊][{self.private_name}] 准备执行最终关系评估...") await self.relationship_updater.update_relationship_final( diff --git a/src/plugins/PFC/conversation_initializer.py b/src/plugins/PFC/conversation_initializer.py index 2e348be9..1a4f8747 100644 --- a/src/plugins/PFC/conversation_initializer.py +++ b/src/plugins/PFC/conversation_initializer.py @@ -8,9 +8,6 @@ from src.plugins.utils.chat_message_builder import build_readable_messages, get_ from maim_message import UserInfo from src.plugins.chat.chat_stream import chat_manager from src.config.config import global_config -from ..person_info.person_info import person_info_manager -from ..person_info.relationship_manager import relationship_manager -from ..moods.moods import MoodManager # 导入 PFC 内部组件和类型 from .pfc_types import ConversationState @@ -226,7 +223,7 @@ async def initialize_core_components(conversation_instance: 'Conversation'): logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) 空闲对话检测器已启动") if conversation_instance.mood_mng and hasattr(conversation_instance.mood_mng, 'start_mood_update') and \ - not conversation_instance.mood_mng._running: # type: ignore + not conversation_instance.mood_mng._running: # type: ignore conversation_instance.mood_mng.start_mood_update(update_interval=global_config.mood_update_interval) # type: ignore logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) MoodManager 已启动后台更新,间隔: {global_config.mood_update_interval} 秒。") elif conversation_instance.mood_mng and conversation_instance.mood_mng._running: # type: ignore @@ -235,7 +232,7 @@ async def initialize_core_components(conversation_instance: 'Conversation'): logger.warning(f"[私聊][{conversation_instance.private_name}] (Initializer) MoodManager 未能启动,相关功能可能受限。") if conversation_instance.conversation_info and conversation_instance.conversation_info.person_id and \ - conversation_instance.relationship_translator and conversation_instance.person_info_mng: # 确保都存在 + conversation_instance.relationship_translator and conversation_instance.person_info_mng: # 确保都存在 try: numeric_relationship_value = await conversation_instance.person_info_mng.get_value( conversation_instance.conversation_info.person_id, "relationship_value" @@ -254,11 +251,11 @@ async def initialize_core_components(conversation_instance: 'Conversation'): if conversation_instance.conversation_info and conversation_instance.mood_mng: # 确保都存在 try: - conversation_instance.conversation_info.current_emotion_text = conversation_instance.mood_mng.get_prompt() # type: ignore - logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化时加载情绪文本: {conversation_instance.conversation_info.current_emotion_text}") + conversation_instance.conversation_info.current_emotion_text = conversation_instance.mood_mng.get_prompt() # type: ignore + logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化时加载情绪文本: {conversation_instance.conversation_info.current_emotion_text}") except Exception as e_init_emo: - logger.error(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化时加载情绪文本出错: {e_init_emo}") - # 保留 ConversationInfo 中的默认值 + logger.error(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化时加载情绪文本出错: {e_init_emo}") + # 保留 ConversationInfo 中的默认值 # 6. 标记初始化成功并设置运行状态 (这些标志由PFCManager控制和检查) # conversation_instance._initialized = True -> 由 manager 设置 diff --git a/src/plugins/PFC/conversation_loop.py b/src/plugins/PFC/conversation_loop.py index dff5caf3..53de59fb 100644 --- a/src/plugins/PFC/conversation_loop.py +++ b/src/plugins/PFC/conversation_loop.py @@ -208,7 +208,7 @@ async def run_conversation_loop(conversation_instance: 'Conversation'): last_action_record_for_end_check = {} if conversation_instance.conversation_info and conversation_instance.conversation_info.done_action: - last_action_record_for_end_check = conversation_instance.conversation_info.done_action[-1] + last_action_record_for_end_check = conversation_instance.conversation_info.done_action[-1] action_ended: bool = ( last_action_record_for_end_check.get("action") in ["end_conversation", "say_goodbye"] and last_action_record_for_end_check.get("status") == "done" ) if goal_ended or action_ended: diff --git a/src/plugins/PFC/pfc_relationship.py b/src/plugins/PFC/pfc_relationship.py index 4dfee52c..be2abf48 100644 --- a/src/plugins/PFC/pfc_relationship.py +++ b/src/plugins/PFC/pfc_relationship.py @@ -202,7 +202,7 @@ class PfcRelationshipUpdater: logger.info(f"[私聊][{self.private_name}] 最终关系值更新:与【{self.private_name}】的关系值从 {current_relationship_value:.2f} 调整了 {adjustment_val:.2f},最终为 {new_relationship_value:.2f}") if conversation_info.person_id: # 虽然通常结束了,但更新一下无妨 - conversation_info.relationship_text = await self.relationship_mng.build_relationship_info(conversation_info.person_id, is_id=True) + conversation_info.relationship_text = await self.relationship_mng.build_relationship_info(conversation_info.person_id, is_id=True) class PfcRepationshipTranslator: diff --git a/src/plugins/PFC/pfc_utils.py b/src/plugins/PFC/pfc_utils.py index 2f267739..2a16439b 100644 --- a/src/plugins/PFC/pfc_utils.py +++ b/src/plugins/PFC/pfc_utils.py @@ -137,9 +137,9 @@ def get_items_from_json( if field in item: current_item_result[field] = item[field] elif field not in default_result: # 如果字段不存在且没有默认值 - logger.warning(f"[私聊][{private_name}] JSON数组元素缺少必要字段 '{field}': {item}") - valid_item = False - break # 这个元素无效 + logger.warning(f"[私聊][{private_name}] JSON数组元素缺少必要字段 '{field}': {item}") + valid_item = False + break # 这个元素无效 if not valid_item: continue @@ -156,10 +156,10 @@ def get_items_from_json( # 验证字符串不为空 (只检查 items 中要求的字段) for field in items: - if field in current_item_result and isinstance(current_item_result[field], str) and not current_item_result[field].strip(): - logger.warning(f"[私聊][{private_name}] JSON数组元素字段 '{field}' 不能为空字符串: {item}") - valid_item = False - break + if field in current_item_result and isinstance(current_item_result[field], str) and not current_item_result[field].strip(): + logger.warning(f"[私聊][{private_name}] JSON数组元素字段 '{field}' 不能为空字符串: {item}") + valid_item = False + break if valid_item: valid_items_list.append(current_item_result) # 只添加完全有效的项 @@ -188,8 +188,8 @@ def get_items_from_json( # 尝试直接解析清理后的内容 json_data = json.loads(cleaned_content) if not isinstance(json_data, dict): - logger.error(f"[私聊][{private_name}] 解析为单个对象,但结果不是字典类型: {type(json_data)}") - return False, default_result # 返回失败和默认值 + logger.error(f"[私聊][{private_name}] 解析为单个对象,但结果不是字典类型: {type(json_data)}") + return False, default_result # 返回失败和默认值 except json.JSONDecodeError: # 如果直接解析失败,尝试用正则表达式查找 JSON 对象部分 (作为后备) @@ -201,8 +201,8 @@ def get_items_from_json( potential_json_str = json_match.group() json_data = json.loads(potential_json_str) if not isinstance(json_data, dict): - logger.error(f"[私聊][{private_name}] 正则提取后解析,但结果不是字典类型: {type(json_data)}") - return False, default_result + logger.error(f"[私聊][{private_name}] 正则提取后解析,但结果不是字典类型: {type(json_data)}") + return False, default_result logger.debug(f"[私聊][{private_name}] 通过正则提取并成功解析JSON对象。") except json.JSONDecodeError: logger.error(f"[私聊][{private_name}] 正则提取的部分 '{potential_json_str[:100]}...' 无法解析为JSON。") @@ -222,9 +222,9 @@ def get_items_from_json( if item in json_data: result[item] = json_data[item] elif item not in default_result: # 如果字段不存在且没有默认值 - logger.error(f"[私聊][{private_name}] JSON对象缺少必要字段 '{item}'。JSON内容: {json_data}") - valid_single_object = False - break # 这个对象无效 + logger.error(f"[私聊][{private_name}] JSON对象缺少必要字段 '{item}'。JSON内容: {json_data}") + valid_single_object = False + break # 这个对象无效 if not valid_single_object: return False, default_result @@ -248,10 +248,10 @@ def get_items_from_json( break if valid_single_object: - logger.debug(f"[私聊][{private_name}] 成功解析并验证了单个JSON对象。") - return True, result # 返回提取并验证后的字典 + logger.debug(f"[私聊][{private_name}] 成功解析并验证了单个JSON对象。") + return True, result # 返回提取并验证后的字典 else: - return False, default_result # 验证失败 + return False, default_result # 验证失败 async def get_person_id(private_name: str, chat_stream: ChatStream): @@ -280,8 +280,8 @@ async def get_person_id(private_name: str, chat_stream: ChatStream): nickname=private_name, # 使用传入的 private_name 作为昵称 ) if person_id is None: # 如果 get_or_create_person 返回 None,说明创建失败 - logger.error(f"[私聊][{private_name}] get_or_create_person 未能获取或创建 person_id。") - return None # 返回 None 表示失败 + logger.error(f"[私聊][{private_name}] get_or_create_person 未能获取或创建 person_id。") + return None # 返回 None 表示失败 return person_id, private_platform_str, private_user_id_str # 返回获取或创建的 person_id except ValueError: diff --git a/src/plugins/PFC/reply_checker.py b/src/plugins/PFC/reply_checker.py index d86e64ac..cb95eded 100644 --- a/src/plugins/PFC/reply_checker.py +++ b/src/plugins/PFC/reply_checker.py @@ -79,7 +79,7 @@ class ReplyChecker: # <--- 新增详细对比日志 --- END ---> if not match_found: # <--- 根据标记判断 - logger.debug(f"[私聊][{self.private_name}] ReplyChecker: 未找到重复。") # <--- 新增日志 + logger.debug(f"[私聊][{self.private_name}] ReplyChecker: 未找到重复。") # <--- 新增日志 return (True, "消息内容未与机器人历史发言重复。", False) except Exception as e: diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index 2c12f7d2..dfb6d879 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -362,8 +362,8 @@ class ReplyGenerator: if action_type == "send_new_message": # 返回一个表示错误的JSON,让调用方知道出错了但仍能解析 return """{{ - "send": "no", - "txt": "LLM生成回复时出错" + "send": "no", + "txt": "LLM生成回复时出错" }}""".strip() else: return "抱歉,我现在有点混乱,让我重新思考一下..." \ No newline at end of file From 646ca52526c21967af937a7d1bbfa9e177a1b788 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Thu, 8 May 2025 09:11:21 +0800 Subject: [PATCH 43/63] ruff --- src/plugins/PFC/action_planner.py | 5 +- src/plugins/PFC/actions.py | 2 +- src/plugins/PFC/conversation.py | 6 +- src/plugins/PFC/conversation_initializer.py | 1 - src/plugins/PFC/conversation_loop.py | 8 ++- src/plugins/PFC/pfc.py | 66 +-------------------- src/plugins/PFC/pfc_emotion.py | 3 +- src/plugins/PFC/pfc_utils.py | 6 +- src/plugins/PFC/reply_checker.py | 8 +-- src/plugins/PFC/reply_generator.py | 13 ++-- 10 files changed, 23 insertions(+), 95 deletions(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index e59adadb..3b7dcff6 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -228,7 +228,8 @@ class ActionPlanner: chat_history_text = await self._build_chat_history_text(observation_info) # 获取 sender_name, relationship_text, current_emotion_text sender_name_str = getattr(observation_info, 'sender_name', '对方') # 从 observation_info 获取 - if not sender_name_str: sender_name_str = '对方' # 再次确保有默认值 + if not sender_name_str: + sender_name_str = '对方' # 再次确保有默认值 relationship_text_str = getattr(conversation_info, 'relationship_text', '你们还不熟悉。') current_emotion_text_str = getattr(conversation_info, 'current_emotion_text', '心情平静。') @@ -507,7 +508,7 @@ class ActionPlanner: logger.debug(f"[私聊][{self.private_name}] 向 LLM 追加了 {other_unread_count} 条未读消息。") else: chat_history_text += ( - f"\n--- 以上均为已读消息,未读消息均已处理完毕 ---\n" + "\n--- 以上均为已读消息,未读消息均已处理完毕 ---\n" ) except AttributeError as e: logger.warning(f"[私聊][{self.private_name}] 构建聊天记录文本时属性错误: {e}") diff --git a/src/plugins/PFC/actions.py b/src/plugins/PFC/actions.py index bda88516..82ee4536 100644 --- a/src/plugins/PFC/actions.py +++ b/src/plugins/PFC/actions.py @@ -3,7 +3,7 @@ import asyncio import datetime import traceback import json -from typing import Dict, Any, Optional, Set, TYPE_CHECKING +from typing import Optional, Set, TYPE_CHECKING from src.common.logger_manager import get_logger from src.config.config import global_config diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 2c87370c..457e933c 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -1,13 +1,9 @@ import time import asyncio -import datetime import traceback -import json -from typing import Dict, Any, Optional, Set, List -from dateutil import tz +from typing import Dict, Any, Optional from src.common.logger_manager import get_logger -from src.plugins.utils.chat_message_builder import build_readable_messages # 移除了 get_raw_msg_before_timestamp_with_chat from maim_message import UserInfo from src.plugins.chat.chat_stream import chat_manager, ChatStream from ..chat.message import Message # 假设 Message 类型被 _convert_to_message 使用 diff --git a/src/plugins/PFC/conversation_initializer.py b/src/plugins/PFC/conversation_initializer.py index 1a4f8747..3f7dd394 100644 --- a/src/plugins/PFC/conversation_initializer.py +++ b/src/plugins/PFC/conversation_initializer.py @@ -1,5 +1,4 @@ import time -import asyncio import traceback from typing import TYPE_CHECKING diff --git a/src/plugins/PFC/conversation_loop.py b/src/plugins/PFC/conversation_loop.py index 53de59fb..c2178ef0 100644 --- a/src/plugins/PFC/conversation_loop.py +++ b/src/plugins/PFC/conversation_loop.py @@ -2,7 +2,7 @@ import time import asyncio import datetime import traceback -from typing import Dict, Any, Optional, Set, List, TYPE_CHECKING +from typing import Dict, Any, List, TYPE_CHECKING from dateutil import tz from src.common.logger_manager import get_logger @@ -176,7 +176,8 @@ async def run_conversation_loop(conversation_instance: 'Conversation'): logger.info(f"[私聊][{conversation_instance.private_name}] (Loop) 中断 '{action}',原因: {interrupt_reason}。重新规划...") cancel_record = { "action": action, "plan_reason": reason, "status": "cancelled_due_to_new_messages", "time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "final_reason": interrupt_reason, } if conversation_instance.conversation_info: - if not hasattr(conversation_instance.conversation_info, "done_action") or conversation_instance.conversation_info.done_action is None: conversation_instance.conversation_info.done_action = [] + if not hasattr(conversation_instance.conversation_info, "done_action") or conversation_instance.conversation_info.done_action is None: + conversation_instance.conversation_info.done_action = [] conversation_instance.conversation_info.done_action.append(cancel_record) conversation_instance.conversation_info.last_successful_reply_action = None conversation_instance.state = ConversationState.ANALYZING @@ -204,7 +205,8 @@ async def run_conversation_loop(conversation_instance: 'Conversation'): if conversation_instance.conversation_info and hasattr(conversation_instance.conversation_info, "goal_list") and conversation_instance.conversation_info.goal_list: last_goal_item = conversation_instance.conversation_info.goal_list[-1] current_goal = last_goal_item.get("goal") if isinstance(last_goal_item, dict) else (last_goal_item if isinstance(last_goal_item, str) else None) - if current_goal == "结束对话": goal_ended = True + if current_goal == "结束对话": + goal_ended = True last_action_record_for_end_check = {} if conversation_instance.conversation_info and conversation_instance.conversation_info.done_action: diff --git a/src/plugins/PFC/pfc.py b/src/plugins/PFC/pfc.py index 69f4d7eb..d0f3e7e6 100644 --- a/src/plugins/PFC/pfc.py +++ b/src/plugins/PFC/pfc.py @@ -100,7 +100,7 @@ class GoalAnalyzer: chat_history_text += f"\n--- 以下是 {observation_info.new_messages_count} 条新消息 ---\n{new_messages_str}" else: chat_history_text += ( - f"\n--- 以上均为已读消息,未读消息均已处理完毕 ---\n" + "\n--- 以上均为已读消息,未读消息均已处理完毕 ---\n" ) # await observation_info.clear_unprocessed_messages() @@ -283,66 +283,4 @@ class GoalAnalyzer: except Exception as e: logger.error(f"[私聊][{self.private_name}]分析对话状态时出错: {str(e)}") - return False, False, f"分析出错: {str(e)}" - - -# 先注释掉,万一以后出问题了还能开回来((( -# class DirectMessageSender: -# """直接发送消息到平台的发送器""" - -# def __init__(self, private_name: str): -# self.logger = get_module_logger("direct_sender") -# self.storage = MessageStorage() -# self.private_name = private_name - -# async def send_via_ws(self, message: MessageSending) -> None: -# try: -# await global_api.send_message(message) -# except Exception as e: -# raise ValueError(f"未找到平台:{message.message_info.platform} 的url配置,请检查配置文件") from e - -# async def send_message( -# self, -# chat_stream: ChatStream, -# content: str, -# reply_to_message: Optional[Message] = None, -# ) -> None: -# """直接发送消息到平台 - -# Args: -# chat_stream: 聊天流 -# content: 消息内容 -# reply_to_message: 要回复的消息 -# """ -# # 构建消息对象 -# message_segment = Seg(type="text", data=content) -# bot_user_info = UserInfo( -# user_id=global_config.BOT_QQ, -# user_nickname=global_config.BOT_NICKNAME, -# platform=chat_stream.platform, -# ) - -# message = MessageSending( -# message_id=f"dm{round(time.time(), 2)}", -# chat_stream=chat_stream, -# bot_user_info=bot_user_info, -# sender_info=reply_to_message.message_info.user_info if reply_to_message else None, -# message_segment=message_segment, -# reply=reply_to_message, -# is_head=True, -# is_emoji=False, -# thinking_start_time=time.time(), -# ) - -# # 处理消息 -# await message.process() - -# _message_json = message.to_dict() - -# # 发送消息 -# try: -# await self.send_via_ws(message) -# await self.storage.store_message(message, chat_stream) -# logger.success(f"[私聊][{self.private_name}]PFC消息已发送: {content}") -# except Exception as e: -# logger.error(f"[私聊][{self.private_name}]PFC消息发送失败: {str(e)}") + return False, False, f"分析出错: {str(e)}" \ No newline at end of file diff --git a/src/plugins/PFC/pfc_emotion.py b/src/plugins/PFC/pfc_emotion.py index 35b526a2..f1018bfd 100644 --- a/src/plugins/PFC/pfc_emotion.py +++ b/src/plugins/PFC/pfc_emotion.py @@ -67,7 +67,8 @@ class PfcEmotionUpdater: current_mood_text_from_manager = self.mood_mng.current_mood.text # 从 MoodManager 获取当前情绪文本 sender_name_for_prompt = getattr(observation_info, 'sender_name', '对方') - if not sender_name_for_prompt: sender_name_for_prompt = '对方' + if not sender_name_for_prompt: + sender_name_for_prompt = '对方' relationship_text_for_prompt = getattr(conversation_info, 'relationship_text', '关系一般。') # 从 ConversationInfo 获取关系文本 emotion_prompt = f"""你是{self.bot_name}。你现在的心情是【{current_mood_text_from_manager}】。 diff --git a/src/plugins/PFC/pfc_utils.py b/src/plugins/PFC/pfc_utils.py index 2a16439b..fb571062 100644 --- a/src/plugins/PFC/pfc_utils.py +++ b/src/plugins/PFC/pfc_utils.py @@ -141,7 +141,8 @@ def get_items_from_json( valid_item = False break # 这个元素无效 - if not valid_item: continue + if not valid_item: + continue # 验证类型 if required_types: @@ -152,7 +153,8 @@ def get_items_from_json( valid_item = False break - if not valid_item: continue + if not valid_item: + continue # 验证字符串不为空 (只检查 items 中要求的字段) for field in items: diff --git a/src/plugins/PFC/reply_checker.py b/src/plugins/PFC/reply_checker.py index cb95eded..47578502 100644 --- a/src/plugins/PFC/reply_checker.py +++ b/src/plugins/PFC/reply_checker.py @@ -1,13 +1,7 @@ - -import json from typing import Tuple, List, Dict, Any from src.common.logger import get_module_logger -# LLMRequest 和 global_config 不再需要直接在此文件中使用(除非 ReplyChecker 以后有其他功能) -# from ..models.utils_model import LLMRequest # <--- 移除 -# from ...config.config import global_config # <--- 移除,但下面会用到 bot_id -from ...config.config import global_config # 为了获取 BOT_QQ +from src.config.config import global_config # 为了获取 BOT_QQ from .chat_observer import ChatObserver -from maim_message import UserInfo # 保持,可能用于未来扩展,但当前逻辑不直接使用 logger = get_module_logger("reply_checker") diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index dfb6d879..1de3e937 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -1,12 +1,6 @@ import random from .pfc_utils import retrieve_contextual_info -# 可能用于旧知识库提取主题 (如果需要回退到旧方法) -# import jieba # 如果报错说找不到 jieba,可能需要安装: pip install jieba -# import re # 正则表达式库,通常 Python 自带 -from typing import Tuple, List, Dict, Any - -# from src.common.logger import get_module_logger from src.common.logger_manager import get_logger from ..models.utils_model import LLMRequest from ...config.config import global_config @@ -227,15 +221,16 @@ class ReplyGenerator: chat_history_text = "还没有聊天记录。" else: chat_history_text += ( - f"\n--- 以上均为已读消息,未读消息均已处理完毕 ---\n" + "\n--- 以上均为已读消息,未读消息均已处理完毕 ---\n" ) sender_name_str = getattr(observation_info, 'sender_name', '对方') - if not sender_name_str: sender_name_str = '对方' + if not sender_name_str: + sender_name_str = '对方' relationship_text_str = getattr(conversation_info, 'relationship_text', '你们还不熟悉。') current_emotion_text_str = getattr(conversation_info, 'current_emotion_text', '心情平静。') - + persona_text = f"你的名字是{self.name},{self.personality_info}。" retrieval_context = chat_history_text retrieved_memory_str, retrieved_knowledge_str = await retrieve_contextual_info( From d2dc9f86e6197e28ad07b324e55c0dc52ff6aaf2 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Thu, 8 May 2025 09:15:15 +0800 Subject: [PATCH 44/63] ruff --- src/plugins/PFC/conversation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 457e933c..e95f21d6 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -28,7 +28,6 @@ from .idle_conversation_starter import IdleConversationStarter from .pfc_KnowledgeFetcher import KnowledgeFetcher from .waiter import Waiter from .reply_checker import ReplyChecker -from . import actions # >>> 新增导入 <<< from .conversation_loop import run_conversation_loop # 导入新的循环函数 From de761cebea123372e623b8aed397909980581316 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 8 May 2025 01:15:37 +0000 Subject: [PATCH 45/63] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=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/config/config.py | 8 +- src/plugins/PFC/action_planner.py | 44 ++- src/plugins/PFC/actions.py | 417 ++++++++++++-------- src/plugins/PFC/chat_observer.py | 8 +- src/plugins/PFC/conversation.py | 45 ++- src/plugins/PFC/conversation_info.py | 18 +- src/plugins/PFC/conversation_initializer.py | 161 +++++--- src/plugins/PFC/conversation_loop.py | 217 ++++++---- src/plugins/PFC/observation_info.py | 23 +- src/plugins/PFC/pfc.py | 6 +- src/plugins/PFC/pfc_emotion.py | 65 +-- src/plugins/PFC/pfc_manager.py | 18 +- src/plugins/PFC/pfc_processor.py | 14 +- src/plugins/PFC/pfc_relationship.py | 150 ++++--- src/plugins/PFC/pfc_utils.py | 79 ++-- src/plugins/PFC/reply_checker.py | 42 +- src/plugins/PFC/reply_generator.py | 82 ++-- 17 files changed, 857 insertions(+), 540 deletions(-) diff --git a/src/config/config.py b/src/config/config.py index 0242d52a..b0d20ab6 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -273,7 +273,7 @@ class BotConfig: # enable_think_flow: bool = False # 是否启用思考流程 talk_allowed_private = set() enable_pfc_chatting: bool = False # 是否启用PFC聊天 - enable_pfc_reply_checker: bool = True # 是否开启PFC回复检查 + enable_pfc_reply_checker: bool = True # 是否开启PFC回复检查 # idle_conversation enable_idle_conversation: bool = False # 是否启用 pfc 主动发言(未完善) @@ -666,8 +666,10 @@ class BotConfig: config.talk_allowed_private = set(str(user) for user in experimental_config.get("talk_allowed_private", [])) if config.INNER_VERSION in SpecifierSet(">=1.1.0"): config.enable_pfc_chatting = experimental_config.get("pfc_chatting", config.enable_pfc_chatting) - if config.INNER_VERSION in SpecifierSet(">=1.1.0"): - config.enable_pfc_reply_checker = experimental_config.get("enable_pfc_reply_checker", config.enable_pfc_reply_checker) + if config.INNER_VERSION in SpecifierSet(">=1.1.0"): + config.enable_pfc_reply_checker = experimental_config.get( + "enable_pfc_reply_checker", config.enable_pfc_reply_checker + ) logger.info(f"PFC Reply Checker 状态: {'启用' if config.enable_pfc_reply_checker else '关闭'}") def idle_conversation(parent: dict): diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 3b7dcff6..3d7743a7 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -167,6 +167,7 @@ block_and_ignore: 更加极端的结束对话方式,直接结束对话并在 注意:请严格按照JSON格式输出,不要包含任何其他内容。""" + class ActionPlanner: """行动规划器""" @@ -204,7 +205,7 @@ class ActionPlanner: observation_info: ObservationInfo, conversation_info: ConversationInfo, last_successful_reply_action: Optional[str], - use_reflect_prompt: bool = False # 新增参数,用于指示是否使用PROMPT_REFLECT_AND_ACT + use_reflect_prompt: bool = False, # 新增参数,用于指示是否使用PROMPT_REFLECT_AND_ACT ) -> Tuple[str, str]: """ 规划下一步行动。 @@ -227,13 +228,12 @@ class ActionPlanner: goals_str = self._build_goals_string(conversation_info) chat_history_text = await self._build_chat_history_text(observation_info) # 获取 sender_name, relationship_text, current_emotion_text - sender_name_str = getattr(observation_info, 'sender_name', '对方') # 从 observation_info 获取 + sender_name_str = getattr(observation_info, "sender_name", "对方") # 从 observation_info 获取 if not sender_name_str: - sender_name_str = '对方' # 再次确保有默认值 - - relationship_text_str = getattr(conversation_info, 'relationship_text', '你们还不熟悉。') - current_emotion_text_str = getattr(conversation_info, 'current_emotion_text', '心情平静。') + sender_name_str = "对方" # 再次确保有默认值 + relationship_text_str = getattr(conversation_info, "relationship_text", "你们还不熟悉。") + current_emotion_text_str = getattr(conversation_info, "current_emotion_text", "心情平静。") persona_text = f"你的名字是{self.name},{self.personality_info}。" action_history_summary, last_action_context = self._build_action_history_context(conversation_info) @@ -250,14 +250,16 @@ class ActionPlanner: # --- 2. 选择并格式化 Prompt --- try: - if use_reflect_prompt: # 新增的判断 + if use_reflect_prompt: # 新增的判断 prompt_template = PROMPT_REFLECT_AND_ACT log_msg = "使用 PROMPT_REFLECT_AND_ACT (反思决策)" # 对于 PROMPT_REFLECT_AND_ACT,它不包含 send_new_message 选项,所以 spam_warning_message 中的相关提示可以调整或省略 # 但为了保持占位符填充的一致性,我们仍然计算它 spam_warning_message = "" - if conversation_info.my_message_count > 5: # 这里的 my_message_count 仍有意义,表示之前连续发送了多少 - spam_warning_message = f"⚠️【警告】**你之前已连续发送{str(conversation_info.my_message_count)}条消息!请谨慎决策。**" + if conversation_info.my_message_count > 5: # 这里的 my_message_count 仍有意义,表示之前连续发送了多少 + spam_warning_message = ( + f"⚠️【警告】**你之前已连续发送{str(conversation_info.my_message_count)}条消息!请谨慎决策。**" + ) elif conversation_info.my_message_count > 2: spam_warning_message = f"💬【提示】**你之前已连续发送{str(conversation_info.my_message_count)}条消息。请注意保持对话平衡。**" @@ -273,12 +275,12 @@ class ActionPlanner: else: prompt_template = PROMPT_INITIAL_REPLY log_msg = "使用 PROMPT_INITIAL_REPLY (首次/非连续回复决策)" - spam_warning_message = "" # 初始回复时通常不需要刷屏警告 + spam_warning_message = "" # 初始回复时通常不需要刷屏警告 logger.debug(f"[私聊][{self.private_name}] {log_msg}") current_time_value = "获取时间失败" - if observation_info and hasattr(observation_info, 'current_time_str') and observation_info.current_time_str: + if observation_info and hasattr(observation_info, "current_time_str") and observation_info.current_time_str: current_time_value = observation_info.current_time_str if spam_warning_message: @@ -298,7 +300,7 @@ class ActionPlanner: spam_warning_info=spam_warning_message, sender_name=sender_name_str, relationship_text=relationship_text_str, - current_emotion_text=current_emotion_text_str + current_emotion_text=current_emotion_text_str, ) logger.debug(f"[私聊][{self.private_name}] 发送到LLM的最终提示词:\n------\n{prompt}\n------") except KeyError as fmt_key_err: @@ -338,10 +340,14 @@ class ActionPlanner: if initial_action == "end_conversation": try: time_str_for_end_decision = "获取时间失败" - if observation_info and hasattr(observation_info, 'current_time_str') and observation_info.current_time_str: + if ( + observation_info + and hasattr(observation_info, "current_time_str") + and observation_info.current_time_str + ): time_str_for_end_decision = observation_info.current_time_str final_action, final_reason = await self._handle_end_conversation_decision( - persona_text, chat_history_text, initial_reason,time_str_for_end_decision + persona_text, chat_history_text, initial_reason, time_str_for_end_decision ) except Exception as end_dec_err: logger.error(f"[私聊][{self.private_name}] 处理结束对话决策时出错: {end_dec_err}") @@ -366,7 +372,7 @@ class ActionPlanner: "block_and_ignore", "say_goodbye", ] - valid_actions_reflect = [ # PROMPT_REFLECT_AND_ACT 的动作 + valid_actions_reflect = [ # PROMPT_REFLECT_AND_ACT 的动作 "wait", "listening", "rethink_goal", @@ -507,9 +513,7 @@ class ActionPlanner: ) logger.debug(f"[私聊][{self.private_name}] 向 LLM 追加了 {other_unread_count} 条未读消息。") else: - chat_history_text += ( - "\n--- 以上均为已读消息,未读消息均已处理完毕 ---\n" - ) + chat_history_text += "\n--- 以上均为已读消息,未读消息均已处理完毕 ---\n" except AttributeError as e: logger.warning(f"[私聊][{self.private_name}] 构建聊天记录文本时属性错误: {e}") chat_history_text = "[获取聊天记录时出错]\n" @@ -571,7 +575,9 @@ class ActionPlanner: ) -> Tuple[str, str]: """处理结束对话前的告别决策""" logger.info(f"[私聊][{self.private_name}] 初步规划结束对话,进入告别决策...") - end_decision_prompt = PROMPT_END_DECISION.format(persona_text=persona_text, chat_history_text=chat_history_text,current_time_str=current_time_str) + end_decision_prompt = PROMPT_END_DECISION.format( + persona_text=persona_text, chat_history_text=chat_history_text, current_time_str=current_time_str + ) logger.debug(f"[私聊][{self.private_name}] 发送到LLM的结束决策提示词:\n------\n{end_decision_prompt}\n------") llm_start_time = time.time() end_content, _ = await self.llm.generate_response_async(end_decision_prompt) diff --git a/src/plugins/PFC/actions.py b/src/plugins/PFC/actions.py index 82ee4536..ea72df82 100644 --- a/src/plugins/PFC/actions.py +++ b/src/plugins/PFC/actions.py @@ -17,7 +17,8 @@ if TYPE_CHECKING: logger = get_logger("pfc_actions") -async def _send_reply_internal(conversation_instance: 'Conversation') -> bool: + +async def _send_reply_internal(conversation_instance: "Conversation") -> bool: """ 内部辅助函数,用于发送 conversation_instance.generated_reply 中的内容。 这之前是 Conversation 类中的 _send_reply 方法。 @@ -43,7 +44,7 @@ async def _send_reply_internal(conversation_instance: 'Conversation') -> bool: reply_to_message=None, # 私聊通常不需要引用回复 ) # 自身发言数量累计 +1 - if conversation_instance.conversation_info: # 确保 conversation_info 存在 + if conversation_instance.conversation_info: # 确保 conversation_info 存在 conversation_instance.conversation_info.my_message_count += 1 # 发送成功后,将状态设置回分析,准备下一轮规划 conversation_instance.state = ConversationState.ANALYZING @@ -55,10 +56,13 @@ async def _send_reply_internal(conversation_instance: 'Conversation') -> bool: conversation_instance.state = ConversationState.ERROR # 发送失败标记错误状态 return False # 返回失败 + async def handle_action( - conversation_instance: 'Conversation', action: str, reason: str, + conversation_instance: "Conversation", + action: str, + reason: str, observation_info: Optional[ObservationInfo], - conversation_info: Optional[ConversationInfo] + conversation_info: Optional[ConversationInfo], ): """ 处理由 ActionPlanner 规划出的具体行动。 @@ -73,19 +77,20 @@ async def handle_action( if not observation_info: logger.error(f"[私聊][{conversation_instance.private_name}] ObservationInfo 为空,无法处理动作 '{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", - }) + 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 return - if not conversation_info: # conversation_info 在这里是必需的 + if not conversation_info: # conversation_info 在这里是必需的 logger.error(f"[私聊][{conversation_instance.private_name}] ConversationInfo 为空,无法处理动作 '{action}'。") conversation_instance.state = ConversationState.ERROR return - logger.info(f"[私聊][{conversation_instance.private_name}] 开始处理动作: {action}, 原因: {reason}") action_start_time = time.time() # 记录动作开始时间 @@ -98,7 +103,7 @@ async def handle_action( "final_reason": None, # 最终结果的原因,将在 finally 中设置 } # 安全地添加到历史记录列表 - if not hasattr(conversation_info, "done_action") or conversation_info.done_action is None: # 防御性检查 + 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) # 获取当前记录在列表中的索引,方便后续更新状态 @@ -108,15 +113,14 @@ async def handle_action( action_successful: bool = 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 - + should_send_reply: bool = True # 默认需要发送 (对于 direct_reply) + is_send_decision_from_rg: bool = False # 标记 send_new_message 的决策是否来自 ReplyGenerator try: # --- 根据不同的 action 类型执行相应的逻辑 --- @@ -126,7 +130,7 @@ async def handle_action( 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} 次)..." @@ -135,16 +139,16 @@ async def handle_action( 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 # 默认情况下,处理原始输出 + text_to_process = raw_llm_output # 默认情况下,处理原始输出 if action == "send_new_message": - is_send_decision_from_rg = True # 标记这是 send_new_message 的决策过程 + is_send_decision_from_rg = True # 标记这是 send_new_message 的决策过程 parsed_json = None try: # 尝试解析JSON @@ -155,9 +159,9 @@ async def handle_action( 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: # 如果成功解析 + text_to_process = "no" # 或者一个特定的错误标记 + + if parsed_json: # 如果成功解析 send_decision = parsed_json.get("send", "no").lower() generated_text_from_json = parsed_json.get("txt", "no") @@ -165,12 +169,12 @@ async def handle_action( 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" + else: # send_decision is "no" should_send_reply = False - text_to_process = "no" # 保持和 prompt 中一致,txt 为 "no" + text_to_process = "no" # 保持和 prompt 中一致,txt 为 "no" logger.info(f"{log_prefix} ReplyGenerator 决定不发送消息。") # 既然RG决定不发送,就直接跳出重试循环 - break + break # 如果 ReplyGenerator 在 send_new_message 动作中决定不发送,则跳出重试循环 if action == "send_new_message" and not should_send_reply: @@ -179,23 +183,28 @@ async def handle_action( 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"或空 - + 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": # 特殊情况日志 + if action == "send_new_message" and generated_content_for_check_or_send == "no": # 特殊情况日志 warning_msg += " (ReplyGenerator决定发送但文本为'no')" - + logger.warning(warning_msg + ",将进行下一次尝试 (如果适用)。") - check_reason = "生成内容无效或选择不发送" # 统一原因 + 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 # 直接进入下一次循环尝试 - + + await asyncio.sleep(0.5) # 暂停一下 + continue # 直接进入下一次循环尝试 + # --- 内容检查 --- conversation_instance.state = ConversationState.CHECKING if not conversation_instance.reply_checker: @@ -203,132 +212,152 @@ async def handle_action( # 准备检查器所需参数 current_goal_str = "" - if conversation_info.goal_list: # 确保 goal_list 存在且不为空 + 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_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( + ( + 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, # 可以是截断的文本 + 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, # 传递当前重试次数 + retry_count=current_retry_for_checker, # 传递当前重试次数 ) logger.info( f"{log_prefix} ReplyChecker 结果: 合适={is_suitable}, 原因='{check_reason}', 需重规划={need_replan_from_checker}" ) - else: # 如果配置关闭 + else: # 如果配置关闭 is_suitable = True check_reason = "ReplyChecker 已通过配置关闭" need_replan_from_checker = False logger.info(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} 回复因自身重复被拒绝: {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: # 其他不合适原因,但无需重规划,且可重试 + 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: # 需要重规划,或达到最大次数 + await asyncio.sleep(0.5) # 暂停一下 + continue # 进入下一次重试 + else: # 需要重规划,或达到最大次数 logger.warning(f"{log_prefix} 回复不合适且(需要重规划或已达最大次数)。原因: {check_reason}") - break # 结束循环,将在循环外部处理 - else: # is_suitable is True + break # 结束循环,将在循环外部处理 + else: # is_suitable is True # 找到了合适的回复 - conversation_info.last_reply_rejection_reason = None # 清除之前的拒绝原因 + conversation_info.last_reply_rejection_reason = None # 清除之前的拒绝原因 conversation_info.last_rejected_reply_content = None - break # 成功,跳出循环 - + 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" # 一个新的状态,表示动作完成但无回复 + logger.info( + f"[私聊][{conversation_instance.private_name}] 动作 '{action}': ReplyGenerator 决定不发送消息。" + ) + final_status = "done_no_reply" # 一个新的状态,表示动作完成但无回复 final_reason = "回复生成器决定不发送消息" - action_successful = True # 动作本身(决策)是成功的 + action_successful = True # 动作本身(决策)是成功的 # 清除追问状态,因为没有实际发送 conversation_info.last_successful_reply_action = None - conversation_info.my_message_count = 0 # 重置连续发言计数 + conversation_info.my_message_count = 0 # 重置连续发言计数 # 后续的 plan 循环会检测到这个 "done_no_reply" 状态并使用反思 prompt - elif is_suitable: # 适用于 direct_reply 或 (send_new_message 且 RG决定发送并通过检查) + elif is_suitable: # 适用于 direct_reply 或 (send_new_message 且 RG决定发送并通过检查) logger.info(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 # 使用检查通过的内容 + 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() # 记录发送完成时间 + 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_status = "done" # 明确设置 final_status final_reason = "成功发送" # 明确设置 final_reason logger.info(f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 成功发送回复.") - + # --- 新增:将机器人发送的消息添加到 ObservationInfo 的 chat_history --- - if observation_info and conversation_instance.bot_qq_str: # 确保 observation_info 和 bot_qq_str 存在 + 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 + "message_id": f"bot_sent_{send_end_time}", # 生成一个唯一ID "time": send_end_time, - "user_info": { # 构造机器人的 UserInfo + "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" + "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, # 简单处理 + "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}] 机器人发送的消息已添加到 chat_history。当前历史数: {observation_info.chat_history_count}") - + logger.debug( + f"[私聊][{conversation_instance.private_name}] 机器人发送的消息已添加到 chat_history。当前历史数: {observation_info.chat_history_count}" + ) + # 可选:如果 chat_history 过长,进行修剪 (例如,保留最近N条) - max_history_len = getattr(global_config, 'pfc_max_chat_history_for_checker', 50) # 例如,可配置 + 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) # 更新计数 + 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条 + 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 + 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}") + logger.error( + f"[私聊][{conversation_instance.private_name}] 更新 chat_history_str 时出错: {e_build_hist}" + ) observation_info.chat_history_str = "[构建聊天记录出错]" # --- 新增结束 --- @@ -342,17 +371,17 @@ async def handle_action( 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 - + 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 # 只清理发送前的 + 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}" @@ -373,59 +402,65 @@ async def handle_action( ) conversation_info.last_successful_reply_action = None # conversation_info.my_message_count 不在此处重置,因为它刚发了一条 - elif action == "direct_reply" or action == "send_new_message": # 成功发送后 + elif action == "direct_reply" or action == "send_new_message": # 成功发送后 logger.info( f"[私聊][{conversation_instance.private_name}] 成功执行 '{action}', 下一轮【允许】使用追问逻辑。" ) conversation_info.last_successful_reply_action = action - + # 更新实例消息计数和关系/情绪 - if conversation_info: # 再次确认 + if conversation_info: # 再次确认 conversation_info.current_instance_message_count += 1 - logger.debug(f"[私聊][{conversation_instance.private_name}] 实例消息计数(机器人发送后)增加到: {conversation_info.current_instance_message_count}") - - if conversation_instance.relationship_updater: # 确保存在 + logger.debug( + f"[私聊][{conversation_instance.private_name}] 实例消息计数(机器人发送后)增加到: {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 存在 + chat_observer_for_history=conversation_instance.chat_observer, # 确保 chat_observer 存在 ) - - sent_reply_summary = conversation_instance.generated_reply[:50] if conversation_instance.generated_reply else "空回复" + + 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: # 确保存在 + 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 + chat_observer_for_history=conversation_instance.chat_observer, # 确保 chat_observer 存在 + event_description=event_for_emotion_update, ) - else: # 发送失败 + else: # 发送失败 logger.error(f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 发送回复失败。") - final_status = "recall" # 标记为 recall 或 error + final_status = "recall" # 标记为 recall 或 error final_reason = "发送回复时失败" - action_successful = False # 确保 action_successful 为 False + action_successful = False # 确保 action_successful 为 False # 发送失败,重置追问状态和计数 conversation_info.last_successful_reply_action = None - conversation_info.my_message_count = 0 + conversation_info.my_message_count = 0 - elif need_replan_from_checker: # 如果检查器要求重规划 + elif need_replan_from_checker: # 如果检查器要求重规划 logger.warning( f"[私聊][{conversation_instance.private_name}] 动作 '{action}' 因 ReplyChecker 要求而被取消,将重新规划。原因: {check_reason}" ) - final_status = "recall" # 标记为 recall + 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) + else: # 达到最大尝试次数仍未找到合适回复 (is_suitable is False and not need_replan_from_checker) logger.warning( f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 达到最大尝试次数 ({max_reply_attempts}),未能生成/检查通过合适的回复。最终原因: {check_reason}" ) - final_status = "recall" # 标记为 recall + final_status = "recall" # 标记为 recall final_reason = f"尝试{max_reply_attempts}次后失败: {check_reason}" - action_successful = False # 确保 action_successful 为 False + action_successful = False # 确保 action_successful 为 False # 重置追问状态 conversation_info.last_successful_reply_action = None # my_message_count 保持不变 @@ -437,17 +472,23 @@ async def handle_action( raise RuntimeError("ReplyGenerator 未初始化") # 生成告别语 generated_content = await conversation_instance.reply_generator.generate( - observation_info, conversation_info, action_type=action # action_type='say_goodbye' + observation_info, + conversation_info, + action_type=action, # action_type='say_goodbye' + ) + logger.info( + f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 生成内容: '{generated_content[:100]}...'" ) - 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}': 生成内容为空或为错误提示,取消发送。") + logger.warning( + f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 生成内容为空或为错误提示,取消发送。" + ) final_reason = "生成内容无效" # 即使生成失败,也按计划结束对话 final_status = "done" # 标记为 done,因为目的是结束 - conversation_instance.should_continue = False # 停止对话 + conversation_instance.should_continue = False # 停止对话 logger.info(f"[私聊][{conversation_instance.private_name}] 告别语生成失败,仍按计划结束对话。") else: # 发送告别语 @@ -457,7 +498,7 @@ async def handle_action( f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 记录发送前时间戳: {timestamp_before_sending:.2f}" ) conversation_instance.state = ConversationState.SENDING - send_success = await _send_reply_internal(conversation_instance) # 调用重构后的发送函数 + send_success = await _send_reply_internal(conversation_instance) # 调用重构后的发送函数 send_end_time = time.time() if send_success: @@ -478,36 +519,42 @@ async def handle_action( if ( msg_id and msg_time - and sender_id != conversation_instance.bot_qq_str # 不是自己的消息 - and msg_time < timestamp_before_sending # 发送前 + 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 存在 + 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 "空回复" + 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: # 确保存在 + 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 + 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_status = "recall" # 或 "error" final_reason = "发送告别语失败" # 发送失败不能结束对话,让其自然流转或由其他逻辑结束 - conversation_instance.should_continue = True # 保持 should_continue + conversation_instance.should_continue = True # 保持 should_continue # 3. 处理重新思考目标动作 elif action == "rethink_goal": @@ -518,12 +565,14 @@ async def handle_action( 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都存在 + 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 + chat_observer_for_history=conversation_instance.chat_observer, # 确保 chat_observer 存在 + event_description=event_for_emotion_update, ) # 4. 处理倾听动作 @@ -533,20 +582,22 @@ async def handle_action( raise RuntimeError("Waiter 未初始化") logger.info(f"[私聊][{conversation_instance.private_name}] 动作 'listening': 进入倾听状态...") # 调用 Waiter 的倾听等待方法,内部会处理超时 - await conversation_instance.waiter.wait_listening(conversation_info) # 直接传递 conversation_info + 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: # 确保都存在 + 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 + 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': 收到最终结束指令,停止对话...") + logger.info( + f"[私聊][{conversation_instance.private_name}] 动作 'end_conversation': 收到最终结束指令,停止对话..." + ) action_successful = True # 标记成功 conversation_instance.should_continue = False # 设置标志以退出循环 @@ -561,12 +612,12 @@ async def handle_action( conversation_instance.state = ConversationState.IGNORED # 设置忽略状态 action_successful = True # 标记成功 event_for_emotion_update = "当前对话让你感到不适,你决定暂时不再理会对方" - if conversation_instance.emotion_updater and conversation_info and observation_info: # 确保都存在 + 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 + chat_observer_for_history=conversation_instance.chat_observer, # 确保 chat_observer 存在 + event_description=event_for_emotion_update, ) # 7. 处理等待动作 @@ -577,20 +628,20 @@ async def handle_action( logger.info(f"[私聊][{conversation_instance.private_name}] 动作 'wait': 进入等待状态...") # 调用 Waiter 的常规等待方法,内部处理超时 # wait 方法返回是否超时 (True=超时, False=未超时/被新消息中断) - timeout_occurred = await conversation_instance.waiter.wait(conversation_info) # 直接传递 conversation_info + timeout_occurred = await conversation_instance.waiter.wait(conversation_info) # 直接传递 conversation_info action_successful = True # wait 动作本身执行即视为成功 event_for_emotion_update = "" - if timeout_occurred: # 假设 timeout_occurred 能正确反映是否超时 + 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: # 确保都存在 + + 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 + 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 动作完成,无需在此清理消息。") @@ -615,7 +666,7 @@ async def handle_action( final_status = "cancelled" final_reason = "动作处理被取消" # 取消时也重置追问状态 - if conversation_info : # 确保 conversation_info 存在 + if conversation_info: # 确保 conversation_info 存在 conversation_info.last_successful_reply_action = None raise # 重新抛出 CancelledError,让上层知道任务被取消 except Exception as handle_err: @@ -626,14 +677,14 @@ async def handle_action( final_reason = f"处理动作时出错: {handle_err}" conversation_instance.state = ConversationState.ERROR # 设置对话状态为错误 # 出错时重置追问状态 - if conversation_info: # 确保 conversation_info 存在 + if conversation_info: # 确保 conversation_info 存在 conversation_info.last_successful_reply_action = None finally: # --- 无论成功与否,都执行 --- # 1. 重置临时存储的计数值 - if conversation_info: # 确保 conversation_info 存在 + if conversation_info: # 确保 conversation_info 存在 conversation_info.other_new_messages_during_planning_count = 0 # 2. 更新动作历史记录的最终状态和原因 @@ -642,15 +693,21 @@ async def handle_action( # 如果动作标记为成功,但 final_status 仍然是初始的 "recall" 或者 "start" # (因为可能在try块中成功执行了但没有显式更新 final_status 为 "done") # 或者是 "done_no_reply" 这种特殊的成功状态 - if final_status in ["recall", "start"] and action != "send_new_message": # send_new_message + no_reply 是特殊成功 + if ( + final_status in ["recall", "start"] and action != "send_new_message" + ): # send_new_message + no_reply 是特殊成功 final_status = "done" - if not final_reason or final_reason == "动作未成功执行": # 避免覆盖已有的具体成功原因 + if not final_reason or final_reason == "动作未成功执行": # 避免覆盖已有的具体成功原因 # 为不同类型的成功动作提供更具体的默认成功原因 if action == "wait": # 检查 conversation_info.goal_list 是否存在且不为空 timeout_occurred = ( - any("分钟," in g.get("goal", "") for g in conversation_info.goal_list if isinstance(g, dict)) - if conversation_info and conversation_info.goal_list + any( + "分钟," in g.get("goal", "") + for g in conversation_info.goal_list + if isinstance(g, dict) + ) + if conversation_info and conversation_info.goal_list else False ) final_reason = "等待完成" + (" (超时)" if timeout_occurred else " (收到新消息或中断)") @@ -658,32 +715,38 @@ async def handle_action( final_reason = "进入倾听状态" elif action in ["rethink_goal", "end_conversation", "block_and_ignore", "say_goodbye"]: final_reason = f"成功执行 {action}" - elif action in ["direct_reply", "send_new_message"]: # 正常发送成功的case + elif action in ["direct_reply", "send_new_message"]: # 正常发送成功的case final_reason = "成功发送" else: final_reason = f"动作 {action} 成功完成" # 如果已经是 "done" 或 "done_no_reply",则保留它们和它们对应的 final_reason - - else: # action_successful is False + + else: # action_successful is False # 如果动作标记为失败,且 final_status 还是 "recall" (初始值) 或 "start" if final_status in ["recall", "start"]: # 尝试从 conversation_info 中获取更具体的失败原因(例如 checker 的原因) # 这个 specific_rejection_reason 是在 try 块中被设置的 - specific_rejection_reason = getattr(conversation_info, 'last_reply_rejection_reason', None) - rejected_content = getattr(conversation_info, 'last_rejected_reply_content', None) + specific_rejection_reason = getattr(conversation_info, "last_reply_rejection_reason", None) + rejected_content = getattr(conversation_info, "last_rejected_reply_content", None) - if specific_rejection_reason: # 如果有更具体的原因 + if specific_rejection_reason: # 如果有更具体的原因 final_reason = f"执行失败: {specific_rejection_reason}" - if rejected_content and specific_rejection_reason == "机器人尝试发送重复消息": # 对复读提供更清晰的日志 + if ( + rejected_content and specific_rejection_reason == "机器人尝试发送重复消息" + ): # 对复读提供更清晰的日志 final_reason += f" (内容: '{rejected_content[:30]}...')" - elif not final_reason or final_reason == "动作未成功执行": # 如果没有更具体的原因,且当前原因还是默认的 + elif not final_reason or final_reason == "动作未成功执行": # 如果没有更具体的原因,且当前原因还是默认的 final_reason = f"动作 {action} 执行失败或被意外中止" # 如果 final_status 已经是 "error" 或 "cancelled",则保留它们和它们对应的 final_reason # 更新 done_action 中的记录 # 防御性检查,确保 conversation_info, done_action 存在,并且索引有效 - if conversation_info and hasattr(conversation_info, 'done_action') and \ - conversation_info.done_action and action_index < len(conversation_info.done_action): + if ( + conversation_info + and hasattr(conversation_info, "done_action") + and conversation_info.done_action + and action_index < len(conversation_info.done_action) + ): conversation_info.done_action[action_index].update( { "status": final_status, @@ -693,14 +756,22 @@ async def handle_action( } ) else: - logger.error(f"[私聊][{conversation_instance.private_name}] 无法更新动作历史记录,索引 {action_index} 无效或列表为空。") - + logger.error( + f"[私聊][{conversation_instance.private_name}] 无法更新动作历史记录,索引 {action_index} 无效或列表为空。" + ) + # 最终日志输出 log_final_reason = final_reason if final_reason else "无明确原因" # 为成功发送的动作添加发送内容摘要 - if final_status == "done" and action_successful and \ - action in ["direct_reply", "send_new_message"] and \ - hasattr(conversation_instance, 'generated_reply') and conversation_instance.generated_reply: - log_final_reason += f" (发送内容: '{conversation_instance.generated_reply[:30]}...')" + if ( + final_status == "done" + and action_successful + and action in ["direct_reply", "send_new_message"] + and hasattr(conversation_instance, "generated_reply") + and conversation_instance.generated_reply + ): + log_final_reason += f" (发送内容: '{conversation_instance.generated_reply[:30]}...')" - logger.info(f"[私聊][{conversation_instance.private_name}] 动作 '{action}' 处理完成。最终状态: {final_status}, 原因: {log_final_reason}") \ No newline at end of file + logger.info( + f"[私聊][{conversation_instance.private_name}] 动作 '{action}' 处理完成。最终状态: {final_status}, 原因: {log_final_reason}" + ) diff --git a/src/plugins/PFC/chat_observer.py b/src/plugins/PFC/chat_observer.py index f4f43fea..c8fd280f 100644 --- a/src/plugins/PFC/chat_observer.py +++ b/src/plugins/PFC/chat_observer.py @@ -101,17 +101,19 @@ class ChatObserver: message: 消息数据 """ try: - if isinstance(message, dict): # 确保是字典才添加 + if isinstance(message, dict): # 确保是字典才添加 self.message_cache.append(message) # 可选:限制 message_cache 的大小,例如只保留最近 N 条 # 你可以根据你的需求调整 MAX_CACHE_SIZE # 对于情绪判断,可能不需要太长的历史,例如 5-10 条可能就足够了 # 但 ChatObserver 的 get_cached_messages 也可能被其他地方用于获取更长的历史 # 所以这里的 MAX_CACHE_SIZE 需要权衡,或者让调用者自己决定 limit - MAX_CACHE_SIZE = 30 # 例如,保留最近30条作为通用缓存 + MAX_CACHE_SIZE = 30 # 例如,保留最近30条作为通用缓存 if len(self.message_cache) > MAX_CACHE_SIZE: self.message_cache = self.message_cache[-MAX_CACHE_SIZE:] - logger.debug(f"[私聊][{self.private_name}] 消息已添加到 ChatObserver 缓存,当前缓存大小: {len(self.message_cache)}") + logger.debug( + f"[私聊][{self.private_name}] 消息已添加到 ChatObserver 缓存,当前缓存大小: {len(self.message_cache)}" + ) else: logger.warning(f"[私聊][{self.private_name}] 尝试向 message_cache 添加非字典类型消息: {type(message)}") diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index e95f21d6..4a3c9e02 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -6,7 +6,7 @@ from typing import Dict, Any, Optional from src.common.logger_manager import get_logger from maim_message import UserInfo from src.plugins.chat.chat_stream import chat_manager, ChatStream -from ..chat.message import Message # 假设 Message 类型被 _convert_to_message 使用 +from ..chat.message import Message # 假设 Message 类型被 _convert_to_message 使用 from src.config.config import global_config from ..person_info.person_info import person_info_manager from ..person_info.relationship_manager import relationship_manager @@ -30,9 +30,10 @@ from .waiter import Waiter from .reply_checker import ReplyChecker # >>> 新增导入 <<< -from .conversation_loop import run_conversation_loop # 导入新的循环函数 +from .conversation_loop import run_conversation_loop # 导入新的循环函数 from rich.traceback import install + install(extra_lines=3) logger = get_logger("pfc_conversation") @@ -44,6 +45,7 @@ logger = get_logger("pfc_conversation") # logger.error(f"配置的时区 '{configured_tz}' 无效,将使用默认时区 'Asia/Shanghai'") # TIME_ZONE = tz.gettz('Asia/Shanghai') + class Conversation: """ 对话类,负责管理单个私聊对话的状态和核心逻辑流程。 @@ -57,7 +59,7 @@ class Conversation: self.stream_id: str = stream_id self.private_name: str = private_name self.state: ConversationState = ConversationState.INIT - self.should_continue: bool = False # Manager 会在初始化后设置 + self.should_continue: bool = False # Manager 会在初始化后设置 self.ignore_until_timestamp: Optional[float] = None self.generated_reply: str = "" self.chat_stream: Optional[ChatStream] = None @@ -81,7 +83,7 @@ class Conversation: self.conversation_info: Optional[ConversationInfo] = None self.reply_checker: Optional[ReplyChecker] = None - self._initialized: bool = False # Manager 会在初始化成功后设为 True + self._initialized: bool = False # Manager 会在初始化成功后设为 True self.bot_qq_str: Optional[str] = str(global_config.BOT_QQ) if global_config.BOT_QQ else None if not self.bot_qq_str: @@ -98,9 +100,7 @@ class Conversation: return if not self.should_continue: - logger.warning( - f"[私聊][{self.private_name}] 对话实例已被 Manager 标记为不应继续,无法启动规划循环。" - ) + logger.warning(f"[私聊][{self.private_name}] 对话实例已被 Manager 标记为不应继续,无法启动规划循环。") return logger.info(f"[私聊][{self.private_name}] 对话系统启动,准备创建规划循环任务...") @@ -120,14 +120,19 @@ class Conversation: self.should_continue = False # 最终关系评估 - if self._initialized and self.relationship_updater and self.conversation_info and \ - self.observation_info and self.chat_observer: + if ( + self._initialized + and self.relationship_updater + and self.conversation_info + and self.observation_info + and self.chat_observer + ): try: logger.info(f"[私聊][{self.private_name}] 准备执行最终关系评估...") await self.relationship_updater.update_relationship_final( conversation_info=self.conversation_info, observation_info=self.observation_info, - chat_observer_for_history=self.chat_observer + chat_observer_for_history=self.chat_observer, ) logger.info(f"[私聊][{self.private_name}] 最终关系评估已调用。") except Exception as e_final_rel: @@ -141,11 +146,11 @@ class Conversation: self.idle_conversation_starter.stop() if self.observation_info and self.chat_observer: self.observation_info.unbind_from_chat_observer() - if self.mood_mng and hasattr(self.mood_mng, 'stop_mood_update') and self.mood_mng._running: # type: ignore - self.mood_mng.stop_mood_update() # type: ignore + if self.mood_mng and hasattr(self.mood_mng, "stop_mood_update") and self.mood_mng._running: # type: ignore + self.mood_mng.stop_mood_update() # type: ignore logger.info(f"[私聊][{self.private_name}] MoodManager 后台更新已停止。") - self._initialized = False # 标记为未初始化 + self._initialized = False # 标记为未初始化 logger.info(f"[私聊][{self.private_name}] 对话实例 {self.stream_id} 已停止。") # _plan_and_action_loop 方法已被移除 @@ -156,7 +161,9 @@ class Conversation: try: chat_stream_to_use = self.chat_stream or chat_manager.get_stream(self.stream_id) if not chat_stream_to_use: - logger.error(f"[私聊][{self.private_name}] 无法确定 ChatStream for stream_id {self.stream_id},无法转换消息。") + logger.error( + f"[私聊][{self.private_name}] 无法确定 ChatStream for stream_id {self.stream_id},无法转换消息。" + ) return None user_info_dict = msg_dict.get("user_info", {}) @@ -165,9 +172,13 @@ class Conversation: try: user_info = UserInfo.from_dict(user_info_dict) except Exception as e: - logger.warning(f"[私聊][{self.private_name}] 从字典创建 UserInfo 时出错: {e}, dict: {user_info_dict}") + logger.warning( + f"[私聊][{self.private_name}] 从字典创建 UserInfo 时出错: {e}, dict: {user_info_dict}" + ) if not user_info: - logger.warning(f"[私聊][{self.private_name}] 消息缺少有效的 UserInfo,无法转换。 msg_id: {msg_dict.get('message_id')}") + logger.warning( + f"[私聊][{self.private_name}] 消息缺少有效的 UserInfo,无法转换。 msg_id: {msg_dict.get('message_id')}" + ) return None return Message( @@ -181,4 +192,4 @@ class Conversation: except Exception as e: logger.error(f"[私聊][{self.private_name}] 转换消息时出错: {e}") logger.error(f"[私聊][{self.private_name}] {traceback.format_exc()}") - return None \ No newline at end of file + return None diff --git a/src/plugins/PFC/conversation_info.py b/src/plugins/PFC/conversation_info.py index 9cfa7ff9..0e7a5138 100644 --- a/src/plugins/PFC/conversation_info.py +++ b/src/plugins/PFC/conversation_info.py @@ -3,18 +3,18 @@ from typing import Optional, List, Dict, Any class ConversationInfo: def __init__(self): - self.done_action: List[Dict[str, Any]] = [] # 建议明确类型 - self.goal_list: List[Dict[str, Any]] = [] # 建议明确类型 - self.knowledge_list: List[Any] = [] # 建议明确类型 - self.memory_list: List[Any] = [] # 建议明确类型 + self.done_action: List[Dict[str, Any]] = [] # 建议明确类型 + self.goal_list: List[Dict[str, Any]] = [] # 建议明确类型 + self.knowledge_list: List[Any] = [] # 建议明确类型 + self.memory_list: List[Any] = [] # 建议明确类型 self.last_successful_reply_action: Optional[str] = None self.last_reply_rejection_reason: Optional[str] = None # 用于存储上次回复被拒原因 self.last_rejected_reply_content: Optional[str] = None # 用于存储上次被拒的回复内容 self.my_message_count: int = 0 # 用于存储连续发送了多少条消息 # --- 新增字段 --- - self.person_id: Optional[str] = None # 私聊对象的唯一ID - self.relationship_text: Optional[str] = "你们还不熟悉。" # 与当前对话者的关系描述文本 - self.current_emotion_text: Optional[str] = "心情平静。" # 机器人当前的情绪描述文本 - self.current_instance_message_count: int = 0 # 当前私聊实例中的消息计数 - # --- 新增字段结束 --- \ No newline at end of file + self.person_id: Optional[str] = None # 私聊对象的唯一ID + self.relationship_text: Optional[str] = "你们还不熟悉。" # 与当前对话者的关系描述文本 + self.current_emotion_text: Optional[str] = "心情平静。" # 机器人当前的情绪描述文本 + self.current_instance_message_count: int = 0 # 当前私聊实例中的消息计数 + # --- 新增字段结束 --- diff --git a/src/plugins/PFC/conversation_initializer.py b/src/plugins/PFC/conversation_initializer.py index 3f7dd394..472c1291 100644 --- a/src/plugins/PFC/conversation_initializer.py +++ b/src/plugins/PFC/conversation_initializer.py @@ -18,7 +18,7 @@ from .observation_info import ObservationInfo from .conversation_info import ConversationInfo from .reply_generator import ReplyGenerator from .idle_conversation_starter import IdleConversationStarter -from .pfc_KnowledgeFetcher import KnowledgeFetcher # 修正大小写 +from .pfc_KnowledgeFetcher import KnowledgeFetcher # 修正大小写 from .waiter import Waiter from .pfc_utils import get_person_id from .reply_checker import ReplyChecker @@ -31,17 +31,20 @@ if TYPE_CHECKING: logger = get_logger("pfc_initializer") -async def load_initial_history(conversation_instance: 'Conversation'): + +async def load_initial_history(conversation_instance: "Conversation"): """ 加载并处理初始的聊天记录。 之前是 Conversation 类中的 _load_initial_history 方法。 """ - if not conversation_instance.observation_info: # 确保 ObservationInfo 已初始化 + if not conversation_instance.observation_info: # 确保 ObservationInfo 已初始化 logger.warning(f"[私聊][{conversation_instance.private_name}] ObservationInfo 未初始化,无法加载历史记录。") return try: - logger.info(f"[私聊][{conversation_instance.private_name}] 为 {conversation_instance.stream_id} 加载初始聊天记录...") + logger.info( + f"[私聊][{conversation_instance.private_name}] 为 {conversation_instance.stream_id} 加载初始聊天记录..." + ) # 从聊天核心获取原始消息列表 initial_messages = get_raw_msg_before_timestamp_with_chat( chat_id=conversation_instance.stream_id, @@ -69,7 +72,9 @@ async def load_initial_history(conversation_instance: 'Conversation'): str(last_user_info.user_id) if last_user_info else None ) except Exception as e: - logger.warning(f"[私聊][{conversation_instance.private_name}] 解析最后一条消息的用户信息时出错: {e}") + logger.warning( + f"[私聊][{conversation_instance.private_name}] 解析最后一条消息的用户信息时出错: {e}" + ) conversation_instance.observation_info.last_message_sender = None else: # 如果 user_info 不是字典,也标记为未知 @@ -91,8 +96,13 @@ async def load_initial_history(conversation_instance: 'Conversation'): # 更新 ChatObserver 和 IdleStarter 的时间戳 if conversation_instance.chat_observer: # 更新观察者的最后消息时间,避免重复处理这些初始消息 - conversation_instance.chat_observer.last_message_time = conversation_instance.observation_info.last_message_time - if conversation_instance.idle_conversation_starter and conversation_instance.observation_info.last_message_time: + conversation_instance.chat_observer.last_message_time = ( + conversation_instance.observation_info.last_message_time + ) + if ( + conversation_instance.idle_conversation_starter + and conversation_instance.observation_info.last_message_time + ): # 更新空闲计时器的起始时间 await conversation_instance.idle_conversation_starter.update_last_message_time( conversation_instance.observation_info.last_message_time @@ -114,7 +124,7 @@ async def load_initial_history(conversation_instance: 'Conversation'): conversation_instance.observation_info.chat_history_str = "[加载聊天记录出错]" -async def initialize_core_components(conversation_instance: 'Conversation'): +async def initialize_core_components(conversation_instance: "Conversation"): """ 异步初始化对话实例及其所有依赖的核心组件。 之前是 Conversation 类中的 _initialize 方法。 @@ -125,31 +135,39 @@ async def initialize_core_components(conversation_instance: 'Conversation'): # return # conversation_instance._initializing_flag_from_manager = True # 标记开始初始化 - logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) 开始初始化对话实例核心组件: {conversation_instance.stream_id}") + logger.info( + f"[私聊][{conversation_instance.private_name}] (Initializer) 开始初始化对话实例核心组件: {conversation_instance.stream_id}" + ) try: # 1. 初始化核心功能组件 logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 ActionPlanner...") - conversation_instance.action_planner = ActionPlanner(conversation_instance.stream_id, conversation_instance.private_name) + conversation_instance.action_planner = ActionPlanner( + conversation_instance.stream_id, conversation_instance.private_name + ) conversation_instance.relationship_updater = PfcRelationshipUpdater( - private_name=conversation_instance.private_name, - bot_name=global_config.BOT_NICKNAME + private_name=conversation_instance.private_name, bot_name=global_config.BOT_NICKNAME + ) + conversation_instance.relationship_translator = PfcRepationshipTranslator( + private_name=conversation_instance.private_name ) - conversation_instance.relationship_translator = PfcRepationshipTranslator(private_name=conversation_instance.private_name) logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) PfcRelationship 初始化完成。") conversation_instance.emotion_updater = PfcEmotionUpdater( - private_name=conversation_instance.private_name, - bot_name=global_config.BOT_NICKNAME + private_name=conversation_instance.private_name, bot_name=global_config.BOT_NICKNAME ) logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) PfcEmotion 初始化完成。") logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 GoalAnalyzer...") - conversation_instance.goal_analyzer = GoalAnalyzer(conversation_instance.stream_id, conversation_instance.private_name) + conversation_instance.goal_analyzer = GoalAnalyzer( + conversation_instance.stream_id, conversation_instance.private_name + ) logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 ReplyGenerator...") - conversation_instance.reply_generator = ReplyGenerator(conversation_instance.stream_id, conversation_instance.private_name) + conversation_instance.reply_generator = ReplyGenerator( + conversation_instance.stream_id, conversation_instance.private_name + ) logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 KnowledgeFetcher...") conversation_instance.knowledge_fetcher = KnowledgeFetcher(conversation_instance.private_name) @@ -161,7 +179,9 @@ async def initialize_core_components(conversation_instance: 'Conversation'): conversation_instance.direct_sender = DirectMessageSender(conversation_instance.private_name) logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 ReplyChecker...") - conversation_instance.reply_checker = ReplyChecker(conversation_instance.stream_id, conversation_instance.private_name) + conversation_instance.reply_checker = ReplyChecker( + conversation_instance.stream_id, conversation_instance.private_name + ) # 获取关联的 ChatStream logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 获取 ChatStream...") @@ -173,47 +193,60 @@ async def initialize_core_components(conversation_instance: 'Conversation'): raise ValueError(f"无法获取 stream_id {conversation_instance.stream_id} 的 ChatStream") logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 IdleConversationStarter...") - conversation_instance.idle_conversation_starter = IdleConversationStarter(conversation_instance.stream_id, conversation_instance.private_name) + conversation_instance.idle_conversation_starter = IdleConversationStarter( + conversation_instance.stream_id, conversation_instance.private_name + ) # 2. 初始化信息存储和观察组件 logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 获取 ChatObserver 实例...") - conversation_instance.chat_observer = ChatObserver.get_instance(conversation_instance.stream_id, conversation_instance.private_name) + conversation_instance.chat_observer = ChatObserver.get_instance( + conversation_instance.stream_id, conversation_instance.private_name + ) logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 ObservationInfo...") conversation_instance.observation_info = ObservationInfo(conversation_instance.private_name) - if not conversation_instance.observation_info.bot_id: # 确保 ObservationInfo 知道机器人的 ID - logger.warning(f"[私聊][{conversation_instance.private_name}] (Initializer) ObservationInfo 未能自动获取 bot_id,尝试手动设置。") + if not conversation_instance.observation_info.bot_id: # 确保 ObservationInfo 知道机器人的 ID + logger.warning( + f"[私聊][{conversation_instance.private_name}] (Initializer) ObservationInfo 未能自动获取 bot_id,尝试手动设置。" + ) conversation_instance.observation_info.bot_id = conversation_instance.bot_qq_str logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 ConversationInfo...") conversation_instance.conversation_info = ConversationInfo() # 3. 绑定观察者和信息处理器 - logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 绑定 ObservationInfo 到 ChatObserver...") - if conversation_instance.observation_info and conversation_instance.chat_observer: # 确保二者都存在 + logger.debug( + f"[私聊][{conversation_instance.private_name}] (Initializer) 绑定 ObservationInfo 到 ChatObserver..." + ) + if conversation_instance.observation_info and conversation_instance.chat_observer: # 确保二者都存在 conversation_instance.observation_info.bind_to_chat_observer(conversation_instance.chat_observer) # 4. 加载初始聊天记录 (调用本文件内的函数) await load_initial_history(conversation_instance) # 4.1 加载用户数据 - if conversation_instance.conversation_info and conversation_instance.chat_stream: # 确保 conversation_info 和 chat_stream 都存在 + if ( + conversation_instance.conversation_info and conversation_instance.chat_stream + ): # 确保 conversation_info 和 chat_stream 都存在 person_id_tuple = await get_person_id( private_name=conversation_instance.private_name, chat_stream=conversation_instance.chat_stream, ) - if person_id_tuple: # 确保元组不为空 - conversation_instance.conversation_info.person_id = person_id_tuple[0] # 第一个元素是 person_id + if person_id_tuple: # 确保元组不为空 + conversation_instance.conversation_info.person_id = person_id_tuple[0] # 第一个元素是 person_id private_platform_str = person_id_tuple[1] private_user_id_str = person_id_tuple[2] - logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) 获取到 person_id: {conversation_instance.conversation_info.person_id} for {private_platform_str}:{private_user_id_str}") + logger.info( + f"[私聊][{conversation_instance.private_name}] (Initializer) 获取到 person_id: {conversation_instance.conversation_info.person_id} for {private_platform_str}:{private_user_id_str}" + ) else: - logger.warning(f"[私聊][{conversation_instance.private_name}] (Initializer) 未能从 get_person_id 获取到 person_id 相关信息。") - + logger.warning( + f"[私聊][{conversation_instance.private_name}] (Initializer) 未能从 get_person_id 获取到 person_id 相关信息。" + ) # 5. 启动需要后台运行的组件 logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 启动 ChatObserver...") - if conversation_instance.chat_observer: # 确保存在 + if conversation_instance.chat_observer: # 确保存在 conversation_instance.chat_observer.start() if conversation_instance.idle_conversation_starter: @@ -221,39 +254,65 @@ async def initialize_core_components(conversation_instance: 'Conversation'): conversation_instance.idle_conversation_starter.start() logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) 空闲对话检测器已启动") - if conversation_instance.mood_mng and hasattr(conversation_instance.mood_mng, 'start_mood_update') and \ - not conversation_instance.mood_mng._running: # type: ignore - conversation_instance.mood_mng.start_mood_update(update_interval=global_config.mood_update_interval) # type: ignore - logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) MoodManager 已启动后台更新,间隔: {global_config.mood_update_interval} 秒。") - elif conversation_instance.mood_mng and conversation_instance.mood_mng._running: # type: ignore + if ( + conversation_instance.mood_mng + and hasattr(conversation_instance.mood_mng, "start_mood_update") + and not conversation_instance.mood_mng._running + ): # type: ignore + conversation_instance.mood_mng.start_mood_update(update_interval=global_config.mood_update_interval) # type: ignore + logger.info( + f"[私聊][{conversation_instance.private_name}] (Initializer) MoodManager 已启动后台更新,间隔: {global_config.mood_update_interval} 秒。" + ) + elif conversation_instance.mood_mng and conversation_instance.mood_mng._running: # type: ignore logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) MoodManager 已在运行中。") else: - logger.warning(f"[私聊][{conversation_instance.private_name}] (Initializer) MoodManager 未能启动,相关功能可能受限。") + logger.warning( + f"[私聊][{conversation_instance.private_name}] (Initializer) MoodManager 未能启动,相关功能可能受限。" + ) - if conversation_instance.conversation_info and conversation_instance.conversation_info.person_id and \ - conversation_instance.relationship_translator and conversation_instance.person_info_mng: # 确保都存在 + if ( + conversation_instance.conversation_info + and conversation_instance.conversation_info.person_id + and conversation_instance.relationship_translator + and conversation_instance.person_info_mng + ): # 确保都存在 try: numeric_relationship_value = await conversation_instance.person_info_mng.get_value( conversation_instance.conversation_info.person_id, "relationship_value" ) if not isinstance(numeric_relationship_value, (int, float)): from bson.decimal128 import Decimal128 + if isinstance(numeric_relationship_value, Decimal128): numeric_relationship_value = float(numeric_relationship_value.to_decimal()) else: numeric_relationship_value = 0.0 - conversation_instance.conversation_info.relationship_text = await conversation_instance.relationship_translator.translate_relationship_value_to_text(numeric_relationship_value) - logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化时加载关系文本: {conversation_instance.conversation_info.relationship_text}") + conversation_instance.conversation_info.relationship_text = ( + await conversation_instance.relationship_translator.translate_relationship_value_to_text( + numeric_relationship_value + ) + ) + logger.info( + f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化时加载关系文本: {conversation_instance.conversation_info.relationship_text}" + ) except Exception as e_init_rel: - logger.error(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化时加载关系文本出错: {e_init_rel}") + logger.error( + f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化时加载关系文本出错: {e_init_rel}" + ) conversation_instance.conversation_info.relationship_text = "你们的关系是:普通。" - - if conversation_instance.conversation_info and conversation_instance.mood_mng: # 确保都存在 + + if conversation_instance.conversation_info and conversation_instance.mood_mng: # 确保都存在 try: - conversation_instance.conversation_info.current_emotion_text = conversation_instance.mood_mng.get_prompt() # type: ignore - logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化时加载情绪文本: {conversation_instance.conversation_info.current_emotion_text}") + conversation_instance.conversation_info.current_emotion_text = ( + conversation_instance.mood_mng.get_prompt() + ) # type: ignore + logger.info( + f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化时加载情绪文本: {conversation_instance.conversation_info.current_emotion_text}" + ) except Exception as e_init_emo: - logger.error(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化时加载情绪文本出错: {e_init_emo}") + logger.error( + f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化时加载情绪文本出错: {e_init_emo}" + ) # 保留 ConversationInfo 中的默认值 # 6. 标记初始化成功并设置运行状态 (这些标志由PFCManager控制和检查) @@ -261,7 +320,9 @@ async def initialize_core_components(conversation_instance: 'Conversation'): # conversation_instance.should_continue = True -> 由 manager 设置 conversation_instance.state = ConversationState.ANALYZING # 设置初始状态为分析 - logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) 对话实例 {conversation_instance.stream_id} 核心组件初始化完成。") + logger.info( + f"[私聊][{conversation_instance.private_name}] (Initializer) 对话实例 {conversation_instance.stream_id} 核心组件初始化完成。" + ) except Exception as e: logger.error(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化对话实例核心组件失败: {e}") @@ -270,6 +331,6 @@ async def initialize_core_components(conversation_instance: 'Conversation'): # conversation_instance._initialized = False # 由 manager 处理 # 外部(PFCManager)会捕获这个异常并处理 should_continue 和 _initialized 标志 # 以及调用 conversation_instance.stop() - raise # 将异常重新抛出,通知 PFCManager 初始化失败 + raise # 将异常重新抛出,通知 PFCManager 初始化失败 # finally: - # conversation_instance._initializing_flag_from_manager = False # 清除标志 \ No newline at end of file + # conversation_instance._initializing_flag_from_manager = False # 清除标志 diff --git a/src/plugins/PFC/conversation_loop.py b/src/plugins/PFC/conversation_loop.py index c2178ef0..80508cf0 100644 --- a/src/plugins/PFC/conversation_loop.py +++ b/src/plugins/PFC/conversation_loop.py @@ -7,8 +7,8 @@ from dateutil import tz from src.common.logger_manager import get_logger from src.config.config import global_config -from .pfc_types import ConversationState # 需要导入 ConversationState -from . import actions # 需要导入 actions 模块 +from .pfc_types import ConversationState # 需要导入 ConversationState +from . import actions # 需要导入 actions 模块 if TYPE_CHECKING: from .conversation import Conversation @@ -16,14 +16,14 @@ if TYPE_CHECKING: logger = get_logger("pfc_loop") # 时区配置 (从 conversation.py 移过来,或者考虑放到更全局的配置模块) -configured_tz = getattr(global_config, 'TIME_ZONE', 'Asia/Shanghai') +configured_tz = getattr(global_config, "TIME_ZONE", "Asia/Shanghai") TIME_ZONE = tz.gettz(configured_tz) if TIME_ZONE is None: logger.error(f"配置的时区 '{configured_tz}' 无效,将使用默认时区 'Asia/Shanghai'") - TIME_ZONE = tz.gettz('Asia/Shanghai') + TIME_ZONE = tz.gettz("Asia/Shanghai") -async def run_conversation_loop(conversation_instance: 'Conversation'): +async def run_conversation_loop(conversation_instance: "Conversation"): """ 核心的规划与行动循环 (PFC Loop)。 之前是 Conversation 类中的 _plan_and_action_loop 方法。 @@ -33,92 +33,133 @@ async def run_conversation_loop(conversation_instance: 'Conversation'): if not conversation_instance._initialized: logger.error(f"[私聊][{conversation_instance.private_name}] 尝试在未初始化状态下运行规划循环,退出。") return - - force_reflect_and_act = False # 用于强制使用反思 prompt 的标志 - + + force_reflect_and_act = False # 用于强制使用反思 prompt 的标志 + while conversation_instance.should_continue: loop_iter_start_time = time.time() logger.debug(f"[私聊][{conversation_instance.private_name}] 开始新一轮循环迭代 ({loop_iter_start_time:.2f})") - + # 更新当前时间 try: - global TIME_ZONE # 引用全局 TIME_ZONE - if TIME_ZONE is None: # 如果还未加载成功 - configured_tz_loop = getattr(global_config, 'TIME_ZONE', 'Asia/Shanghai') + global TIME_ZONE # 引用全局 TIME_ZONE + if TIME_ZONE is None: # 如果还未加载成功 + configured_tz_loop = getattr(global_config, "TIME_ZONE", "Asia/Shanghai") TIME_ZONE = tz.gettz(configured_tz_loop) if TIME_ZONE is None: logger.error(f"循环中: 配置的时区 '{configured_tz_loop}' 无效,将使用 'Asia/Shanghai'") - TIME_ZONE = tz.gettz('Asia/Shanghai') - + TIME_ZONE = tz.gettz("Asia/Shanghai") + current_time_dt = datetime.datetime.now(TIME_ZONE) - if conversation_instance.observation_info: - time_str = current_time_dt.strftime("%Y-%m-%d %H:%M:%S %Z%z") + if conversation_instance.observation_info: + time_str = current_time_dt.strftime("%Y-%m-%d %H:%M:%S %Z%z") conversation_instance.observation_info.current_time_str = time_str logger.debug(f"[私聊][{conversation_instance.private_name}] 更新 ObservationInfo 当前时间: {time_str}") else: - logger.warning(f"[私聊][{conversation_instance.private_name}] ObservationInfo 未初始化,无法更新当前时间。") + logger.warning( + f"[私聊][{conversation_instance.private_name}] ObservationInfo 未初始化,无法更新当前时间。" + ) except Exception as time_update_err: - logger.error(f"[私聊][{conversation_instance.private_name}] 更新 ObservationInfo 当前时间时出错: {time_update_err}") + logger.error( + f"[私聊][{conversation_instance.private_name}] 更新 ObservationInfo 当前时间时出错: {time_update_err}" + ) # 处理忽略状态 - if conversation_instance.ignore_until_timestamp and loop_iter_start_time < conversation_instance.ignore_until_timestamp: - if conversation_instance.idle_conversation_starter and conversation_instance.idle_conversation_starter._running: + if ( + conversation_instance.ignore_until_timestamp + and loop_iter_start_time < conversation_instance.ignore_until_timestamp + ): + if ( + conversation_instance.idle_conversation_starter + and conversation_instance.idle_conversation_starter._running + ): conversation_instance.idle_conversation_starter.stop() logger.debug(f"[私聊][{conversation_instance.private_name}] 对话被暂时忽略,暂停空闲对话检测") sleep_duration = min(30, conversation_instance.ignore_until_timestamp - loop_iter_start_time) await asyncio.sleep(sleep_duration) - continue - elif conversation_instance.ignore_until_timestamp and loop_iter_start_time >= conversation_instance.ignore_until_timestamp: - logger.info(f"[私聊][{conversation_instance.private_name}] 忽略时间已到 {conversation_instance.stream_id},准备结束对话。") - conversation_instance.ignore_until_timestamp = None - await conversation_instance.stop() # 调用 Conversation 实例的 stop 方法 - continue + continue + elif ( + conversation_instance.ignore_until_timestamp + and loop_iter_start_time >= conversation_instance.ignore_until_timestamp + ): + logger.info( + f"[私聊][{conversation_instance.private_name}] 忽略时间已到 {conversation_instance.stream_id},准备结束对话。" + ) + conversation_instance.ignore_until_timestamp = None + await conversation_instance.stop() # 调用 Conversation 实例的 stop 方法 + continue else: - if conversation_instance.idle_conversation_starter and not conversation_instance.idle_conversation_starter._running: + if ( + conversation_instance.idle_conversation_starter + and not conversation_instance.idle_conversation_starter._running + ): conversation_instance.idle_conversation_starter.start() logger.debug(f"[私聊][{conversation_instance.private_name}] 恢复空闲对话检测") # 核心规划与行动逻辑 try: # 更新关系和情绪文本 (在每次循环开始时进行) - if conversation_instance.conversation_info and conversation_instance._initialized: + if conversation_instance.conversation_info and conversation_instance._initialized: # 更新关系 - if conversation_instance.conversation_info.person_id and conversation_instance.relationship_translator and conversation_instance.person_info_mng: + if ( + conversation_instance.conversation_info.person_id + and conversation_instance.relationship_translator + and conversation_instance.person_info_mng + ): try: numeric_relationship_value = await conversation_instance.person_info_mng.get_value( conversation_instance.conversation_info.person_id, "relationship_value" ) if not isinstance(numeric_relationship_value, (int, float)): from bson.decimal128 import Decimal128 + if isinstance(numeric_relationship_value, Decimal128): numeric_relationship_value = float(numeric_relationship_value.to_decimal()) - else: + else: numeric_relationship_value = 0.0 - conversation_instance.conversation_info.relationship_text = await conversation_instance.relationship_translator.translate_relationship_value_to_text(numeric_relationship_value) + conversation_instance.conversation_info.relationship_text = ( + await conversation_instance.relationship_translator.translate_relationship_value_to_text( + numeric_relationship_value + ) + ) except Exception as e_rel: logger.error(f"[私聊][{conversation_instance.private_name}] (Loop) 更新关系文本时出错: {e_rel}") - conversation_instance.conversation_info.relationship_text = "你们的关系是:普通。" + conversation_instance.conversation_info.relationship_text = "你们的关系是:普通。" # 更新情绪 if conversation_instance.mood_mng: - conversation_instance.conversation_info.current_emotion_text = conversation_instance.mood_mng.get_prompt() # type: ignore - + conversation_instance.conversation_info.current_emotion_text = ( + conversation_instance.mood_mng.get_prompt() + ) # type: ignore + # 检查核心组件 - if not all([conversation_instance.action_planner, conversation_instance.observation_info, conversation_instance.conversation_info]): - logger.error(f"[私聊][{conversation_instance.private_name}] 核心组件未初始化,无法继续规划循环。将等待5秒后重试...") + if not all( + [ + conversation_instance.action_planner, + conversation_instance.observation_info, + conversation_instance.conversation_info, + ] + ): + logger.error( + f"[私聊][{conversation_instance.private_name}] 核心组件未初始化,无法继续规划循环。将等待5秒后重试..." + ) await asyncio.sleep(5) continue # 规划 planning_start_time = time.time() - logger.debug(f"[私聊][{conversation_instance.private_name}] --- (Loop) 开始规划 ({planning_start_time:.2f}) ---") + logger.debug( + f"[私聊][{conversation_instance.private_name}] --- (Loop) 开始规划 ({planning_start_time:.2f}) ---" + ) if conversation_instance.conversation_info: conversation_instance.conversation_info.other_new_messages_during_planning_count = 0 action, reason = await conversation_instance.action_planner.plan( conversation_instance.observation_info, - conversation_instance.conversation_info, - conversation_instance.conversation_info.last_successful_reply_action if conversation_instance.conversation_info else None, - use_reflect_prompt=force_reflect_and_act + conversation_instance.conversation_info, + conversation_instance.conversation_info.last_successful_reply_action + if conversation_instance.conversation_info + else None, + use_reflect_prompt=force_reflect_and_act, ) force_reflect_and_act = False logger.debug( @@ -138,29 +179,38 @@ async def run_conversation_loop(conversation_instance: 'Conversation'): new_messages_during_planning.append(msg) if sender_id != conversation_instance.bot_qq_str: other_new_messages_during_planning.append(msg) - + new_msg_count = len(new_messages_during_planning) other_new_msg_count = len(other_new_messages_during_planning) - - if conversation_instance.conversation_info and other_new_msg_count > 0: + + if conversation_instance.conversation_info and other_new_msg_count > 0: conversation_instance.conversation_info.current_instance_message_count += other_new_msg_count # 触发关系和情绪更新(如果需要) - if conversation_instance.relationship_updater and conversation_instance.observation_info and conversation_instance.chat_observer: + if ( + conversation_instance.relationship_updater + and conversation_instance.observation_info + and conversation_instance.chat_observer + ): await conversation_instance.relationship_updater.update_relationship_incremental( conversation_info=conversation_instance.conversation_info, observation_info=conversation_instance.observation_info, - chat_observer_for_history=conversation_instance.chat_observer + chat_observer_for_history=conversation_instance.chat_observer, ) - if conversation_instance.emotion_updater and other_new_messages_during_planning and conversation_instance.observation_info and conversation_instance.chat_observer: + if ( + conversation_instance.emotion_updater + and other_new_messages_during_planning + and conversation_instance.observation_info + and conversation_instance.chat_observer + ): last_user_msg = other_new_messages_during_planning[-1] last_user_msg_text = last_user_msg.get("processed_plain_text", "用户发了新消息") - sender_name_for_event = getattr(conversation_instance.observation_info, 'sender_name', '对方') + sender_name_for_event = getattr(conversation_instance.observation_info, "sender_name", "对方") event_desc = f"用户【{sender_name_for_event}】发送了新消息: '{last_user_msg_text[:30]}...'" await conversation_instance.emotion_updater.update_emotion_based_on_context( conversation_info=conversation_instance.conversation_info, observation_info=conversation_instance.observation_info, chat_observer_for_history=conversation_instance.chat_observer, - event_description=event_desc + event_description=event_desc, ) should_interrupt: bool = False @@ -168,15 +218,26 @@ async def run_conversation_loop(conversation_instance: 'Conversation'): if action in ["wait", "listening"] and new_msg_count > 0: should_interrupt = True interrupt_reason = f"规划 {action} 期间收到 {new_msg_count} 条新消息" - elif other_new_msg_count > 2: # Threshold for other actions + elif other_new_msg_count > 2: # Threshold for other actions should_interrupt = True interrupt_reason = f"规划 {action} 期间收到 {other_new_msg_count} 条来自他人的新消息" if should_interrupt: - logger.info(f"[私聊][{conversation_instance.private_name}] (Loop) 中断 '{action}',原因: {interrupt_reason}。重新规划...") - cancel_record = { "action": action, "plan_reason": reason, "status": "cancelled_due_to_new_messages", "time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "final_reason": interrupt_reason, } + logger.info( + f"[私聊][{conversation_instance.private_name}] (Loop) 中断 '{action}',原因: {interrupt_reason}。重新规划..." + ) + cancel_record = { + "action": action, + "plan_reason": reason, + "status": "cancelled_due_to_new_messages", + "time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "final_reason": interrupt_reason, + } if conversation_instance.conversation_info: - if not hasattr(conversation_instance.conversation_info, "done_action") or conversation_instance.conversation_info.done_action is None: + if ( + not hasattr(conversation_instance.conversation_info, "done_action") + or conversation_instance.conversation_info.done_action is None + ): conversation_instance.conversation_info.done_action = [] conversation_instance.conversation_info.done_action.append(cancel_record) conversation_instance.conversation_info.last_successful_reply_action = None @@ -185,43 +246,65 @@ async def run_conversation_loop(conversation_instance: 'Conversation'): continue # 执行动作 (调用 actions 模块的函数) - logger.debug(f"[私聊][{conversation_instance.private_name}] (Loop) 未中断,调用 actions.handle_action 执行动作 '{action}'...") + logger.debug( + f"[私聊][{conversation_instance.private_name}] (Loop) 未中断,调用 actions.handle_action 执行动作 '{action}'..." + ) if conversation_instance.conversation_info: conversation_instance.conversation_info.other_new_messages_during_planning_count = other_new_msg_count - - await actions.handle_action(conversation_instance, action, reason, conversation_instance.observation_info, conversation_instance.conversation_info) + + await actions.handle_action( + conversation_instance, + action, + reason, + conversation_instance.observation_info, + conversation_instance.conversation_info, + ) logger.debug(f"[私聊][{conversation_instance.private_name}] (Loop) actions.handle_action 完成。") # 检查是否需要反思 last_action_record = {} if conversation_instance.conversation_info and conversation_instance.conversation_info.done_action: last_action_record = conversation_instance.conversation_info.done_action[-1] - if last_action_record.get("action") == "send_new_message" and last_action_record.get("status") == "done_no_reply": + if ( + last_action_record.get("action") == "send_new_message" + and last_action_record.get("status") == "done_no_reply" + ): logger.info(f"[私聊][{conversation_instance.private_name}] (Loop) 检测到需反思,设置标志。") force_reflect_and_act = True - + # 检查结束条件 goal_ended: bool = False - if conversation_instance.conversation_info and hasattr(conversation_instance.conversation_info, "goal_list") and conversation_instance.conversation_info.goal_list: + if ( + conversation_instance.conversation_info + and hasattr(conversation_instance.conversation_info, "goal_list") + and conversation_instance.conversation_info.goal_list + ): last_goal_item = conversation_instance.conversation_info.goal_list[-1] - current_goal = last_goal_item.get("goal") if isinstance(last_goal_item, dict) else (last_goal_item if isinstance(last_goal_item, str) else None) + current_goal = ( + last_goal_item.get("goal") + if isinstance(last_goal_item, dict) + else (last_goal_item if isinstance(last_goal_item, str) else None) + ) if current_goal == "结束对话": goal_ended = True last_action_record_for_end_check = {} if conversation_instance.conversation_info and conversation_instance.conversation_info.done_action: last_action_record_for_end_check = conversation_instance.conversation_info.done_action[-1] - action_ended: bool = ( last_action_record_for_end_check.get("action") in ["end_conversation", "say_goodbye"] and last_action_record_for_end_check.get("status") == "done" ) + action_ended: bool = ( + last_action_record_for_end_check.get("action") in ["end_conversation", "say_goodbye"] + and last_action_record_for_end_check.get("status") == "done" + ) if goal_ended or action_ended: - logger.info( f"[私聊][{conversation_instance.private_name}] (Loop) 检测到结束条件,停止循环。" ) - await conversation_instance.stop() # 调用 Conversation 的 stop - continue # 虽然会 break,但 continue 更明确 + logger.info(f"[私聊][{conversation_instance.private_name}] (Loop) 检测到结束条件,停止循环。") + await conversation_instance.stop() # 调用 Conversation 的 stop + continue # 虽然会 break,但 continue 更明确 except asyncio.CancelledError: logger.info(f"[私聊][{conversation_instance.private_name}] (Loop) PFC 主循环任务被取消。") - await conversation_instance.stop() # 调用 Conversation 的 stop - break + await conversation_instance.stop() # 调用 Conversation 的 stop + break except Exception as loop_err: logger.error(f"[私聊][{conversation_instance.private_name}] (Loop) PFC 主循环出错: {loop_err}") logger.error(f"[私聊][{conversation_instance.private_name}] (Loop) {traceback.format_exc()}") @@ -235,4 +318,6 @@ async def run_conversation_loop(conversation_instance: 'Conversation'): if loop_duration < min_loop_interval: await asyncio.sleep(min_loop_interval - loop_duration) - logger.info(f"[私聊][{conversation_instance.private_name}] (Loop) PFC 循环已退出 for stream_id: {conversation_instance.stream_id}") \ No newline at end of file + logger.info( + f"[私聊][{conversation_instance.private_name}] (Loop) PFC 循环已退出 for stream_id: {conversation_instance.stream_id}" + ) diff --git a/src/plugins/PFC/observation_info.py b/src/plugins/PFC/observation_info.py index 4f5ea75f..2d2a0f27 100644 --- a/src/plugins/PFC/observation_info.py +++ b/src/plugins/PFC/observation_info.py @@ -6,13 +6,14 @@ from maim_message import UserInfo from src.common.logger import get_module_logger from src.plugins.utils.chat_message_builder import build_readable_messages from src.config.config import global_config + # 确保导入路径正确 from .chat_observer import ChatObserver from .chat_states import NotificationHandler, NotificationType, Notification logger = get_module_logger("observation_info") -TIME_ZONE = tz.gettz(global_config.TIME_ZONE if global_config else 'Asia/Shanghai') # 使用配置的时区,提供默认值 +TIME_ZONE = tz.gettz(global_config.TIME_ZONE if global_config else "Asia/Shanghai") # 使用配置的时区,提供默认值 class ObservationInfoHandler(NotificationHandler): @@ -115,10 +116,9 @@ class ObservationInfo: # 新增:发信人信息 self.sender_name: Optional[str] = None - self.sender_user_id: Optional[str] = None # 存储为字符串 + self.sender_user_id: Optional[str] = None # 存储为字符串 self.sender_platform: Optional[str] = None - # 聊天记录相关 self.chat_history: List[Dict[str, Any]] = [] # 存储已处理的消息历史 self.chat_history_str: str = "还没有聊天记录。" # 用于生成 Prompt 的历史记录字符串 @@ -146,7 +146,7 @@ class ObservationInfo: # 其他状态 self.is_typing: bool = False # 是否正在输入 (未来可能用到) self.changed: bool = False # 状态是否有变化 (用于优化) - + # 用于存储格式化的当前时间 self.current_time_str: Optional[str] = None @@ -226,16 +226,18 @@ class ObservationInfo: if not message_time or not message_id: logger.warning(f"[私聊][{self.private_name}] 收到的消息缺少 time 或 message_id: {message}") return - + # --- 新增/修改:提取并存储发信人详细信息 --- current_message_sender_id: Optional[str] = None if user_info: try: - self.sender_user_id = str(user_info.user_id) # 确保是字符串 - self.sender_name = user_info.user_nickname # 或者 user_info.card 如果私聊时card更准 + self.sender_user_id = str(user_info.user_id) # 确保是字符串 + self.sender_name = user_info.user_nickname # 或者 user_info.card 如果私聊时card更准 self.sender_platform = user_info.platform - current_message_sender_id = self.sender_user_id # 用于后续逻辑 - logger.debug(f"[私聊][{self.private_name}] 更新发信人信息: ID={self.sender_user_id}, Name={self.sender_name}, Platform={self.sender_platform}") + current_message_sender_id = self.sender_user_id # 用于后续逻辑 + logger.debug( + f"[私聊][{self.private_name}] 更新发信人信息: ID={self.sender_user_id}, Name={self.sender_name}, Platform={self.sender_platform}" + ) except AttributeError as e: logger.error(f"[私聊][{self.private_name}] 从 UserInfo 对象提取信息时出错: {e}, UserInfo: {user_info}") # 如果提取失败,将这些新字段设为 None,避免使用旧数据 @@ -250,13 +252,12 @@ class ObservationInfo: self.sender_platform = None # --- 新增/修改结束 --- - # 更新最后消息时间(所有消息) if message_time > (self.last_message_time or 0): self.last_message_time = message_time self.last_message_id = message_id self.last_message_content = processed_text - self.last_message_sender = current_message_sender_id # 使用新获取的 current_message_sender_id + self.last_message_sender = current_message_sender_id # 使用新获取的 current_message_sender_id # 更新说话者特定时间 if sender_id_str: diff --git a/src/plugins/PFC/pfc.py b/src/plugins/PFC/pfc.py index d0f3e7e6..77a8f4b7 100644 --- a/src/plugins/PFC/pfc.py +++ b/src/plugins/PFC/pfc.py @@ -99,9 +99,7 @@ class GoalAnalyzer: ) chat_history_text += f"\n--- 以下是 {observation_info.new_messages_count} 条新消息 ---\n{new_messages_str}" else: - chat_history_text += ( - "\n--- 以上均为已读消息,未读消息均已处理完毕 ---\n" - ) + chat_history_text += "\n--- 以上均为已读消息,未读消息均已处理完毕 ---\n" # await observation_info.clear_unprocessed_messages() persona_text = f"你的名字是{self.name},{self.personality_info}。" @@ -283,4 +281,4 @@ class GoalAnalyzer: except Exception as e: logger.error(f"[私聊][{self.private_name}]分析对话状态时出错: {str(e)}") - return False, False, f"分析出错: {str(e)}" \ No newline at end of file + return False, False, f"分析出错: {str(e)}" diff --git a/src/plugins/PFC/pfc_emotion.py b/src/plugins/PFC/pfc_emotion.py index f1018bfd..d88344f8 100644 --- a/src/plugins/PFC/pfc_emotion.py +++ b/src/plugins/PFC/pfc_emotion.py @@ -3,14 +3,15 @@ from typing import List, Dict, Any from src.plugins.PFC.chat_observer import ChatObserver from src.common.logger_manager import get_logger from src.plugins.models.utils_model import LLMRequest -from src.plugins.moods.moods import MoodManager # MoodManager 本身是单例 +from src.plugins.moods.moods import MoodManager # MoodManager 本身是单例 from src.plugins.utils.chat_message_builder import build_readable_messages from src.plugins.PFC.observation_info import ObservationInfo from src.plugins.PFC.conversation_info import ConversationInfo -from src.config.config import global_config # 导入全局配置 +from src.config.config import global_config # 导入全局配置 logger = get_logger("pfc_emotion") + class PfcEmotionUpdater: def __init__(self, private_name: str, bot_name: str): """ @@ -18,31 +19,33 @@ class PfcEmotionUpdater: """ self.private_name = private_name self.bot_name = bot_name - self.mood_mng = MoodManager.get_instance() # 获取 MoodManager 单例 + self.mood_mng = MoodManager.get_instance() # 获取 MoodManager 单例 # LLM 实例 (根据 global_config.llm_summary 配置) - llm_config_summary = getattr(global_config, 'llm_summary', None) + llm_config_summary = getattr(global_config, "llm_summary", None) if llm_config_summary and isinstance(llm_config_summary, dict): logger.info(f"[私聊][{self.private_name}] 使用 llm_summary 配置初始化情绪判断LLM。") self.llm = LLMRequest( model=llm_config_summary, - temperature=llm_config_summary.get("temperature", 0.5), # temperature 来自其自身配置或默认0.7,这里用0.5 - max_tokens=llm_config_summary.get("max_tokens", 256), # 情绪词输出不需要很多token - request_type="pfc_emotion_evaluation" + temperature=llm_config_summary.get( + "temperature", 0.5 + ), # temperature 来自其自身配置或默认0.7,这里用0.5 + max_tokens=llm_config_summary.get("max_tokens", 256), # 情绪词输出不需要很多token + request_type="pfc_emotion_evaluation", ) else: logger.error(f"[私聊][{self.private_name}] 未找到 llm_summary 配置或配置无效!情绪判断功能将受限。") - self.llm = None # LLM 未初始化 + self.llm = None # LLM 未初始化 - self.EMOTION_UPDATE_INTENSITY = getattr(global_config, 'pfc_emotion_update_intensity', 0.6) - self.EMOTION_HISTORY_COUNT = getattr(global_config, 'pfc_emotion_history_count', 5) + self.EMOTION_UPDATE_INTENSITY = getattr(global_config, "pfc_emotion_update_intensity", 0.6) + self.EMOTION_HISTORY_COUNT = getattr(global_config, "pfc_emotion_history_count", 5) async def update_emotion_based_on_context( self, conversation_info: ConversationInfo, observation_info: ObservationInfo, - chat_observer_for_history: ChatObserver, # ChatObserver 实例 - event_description: str + chat_observer_for_history: ChatObserver, # ChatObserver 实例 + event_description: str, ) -> None: if not self.llm: logger.error(f"[私聊][{self.private_name}] LLM未初始化,无法进行情绪更新。") @@ -57,19 +60,23 @@ class PfcEmotionUpdater: recent_messages_for_emotion: List[Dict[str, Any]] = [] if chat_observer_for_history: - recent_messages_for_emotion = chat_observer_for_history.get_cached_messages(limit=self.EMOTION_HISTORY_COUNT) + recent_messages_for_emotion = chat_observer_for_history.get_cached_messages( + limit=self.EMOTION_HISTORY_COUNT + ) elif observation_info.chat_history: - recent_messages_for_emotion = observation_info.chat_history[-self.EMOTION_HISTORY_COUNT:] - + recent_messages_for_emotion = observation_info.chat_history[-self.EMOTION_HISTORY_COUNT :] + readable_recent_history = await build_readable_messages( recent_messages_for_emotion, replace_bot_name=True, merge_messages=True, timestamp_mode="none" ) - current_mood_text_from_manager = self.mood_mng.current_mood.text # 从 MoodManager 获取当前情绪文本 - sender_name_for_prompt = getattr(observation_info, 'sender_name', '对方') + current_mood_text_from_manager = self.mood_mng.current_mood.text # 从 MoodManager 获取当前情绪文本 + sender_name_for_prompt = getattr(observation_info, "sender_name", "对方") if not sender_name_for_prompt: - sender_name_for_prompt = '对方' - relationship_text_for_prompt = getattr(conversation_info, 'relationship_text', '关系一般。') # 从 ConversationInfo 获取关系文本 + sender_name_for_prompt = "对方" + relationship_text_for_prompt = getattr( + conversation_info, "relationship_text", "关系一般。" + ) # 从 ConversationInfo 获取关系文本 emotion_prompt = f"""你是{self.bot_name}。你现在的心情是【{current_mood_text_from_manager}】。 你正在和用户【{sender_name_for_prompt}】私聊,你们的关系是:【{relationship_text_for_prompt}】。 @@ -86,19 +93,27 @@ class PfcEmotionUpdater: try: logger.debug(f"[私聊][{self.private_name}] 情绪判断Prompt:\n{emotion_prompt}") content, _ = await self.llm.generate_response_async(emotion_prompt) - detected_emotion_word = content.strip().replace("\"", "").replace("'", "") + detected_emotion_word = content.strip().replace('"', "").replace("'", "") logger.debug(f"[私聊][{self.private_name}] 情绪判断LLM原始返回: '{detected_emotion_word}'") - if detected_emotion_word and detected_emotion_word != "无变化" and detected_emotion_word in self.mood_mng.emotion_map: + if ( + detected_emotion_word + and detected_emotion_word != "无变化" + and detected_emotion_word in self.mood_mng.emotion_map + ): self.mood_mng.update_mood_from_emotion(detected_emotion_word, intensity=self.EMOTION_UPDATE_INTENSITY) - logger.info(f"[私聊][{self.private_name}] 基于事件 '{event_description}',情绪已更新为倾向于 '{detected_emotion_word}'。当前心情: {self.mood_mng.current_mood.text}") + logger.info( + f"[私聊][{self.private_name}] 基于事件 '{event_description}',情绪已更新为倾向于 '{detected_emotion_word}'。当前心情: {self.mood_mng.current_mood.text}" + ) elif detected_emotion_word == "无变化": logger.info(f"[私聊][{self.private_name}] 基于事件 '{event_description}',LLM判断情绪无显著变化。") else: - logger.warning(f"[私聊][{self.private_name}] LLM返回了未知的情绪词 '{detected_emotion_word}' 或未返回有效词,情绪未主动更新。") + logger.warning( + f"[私聊][{self.private_name}] LLM返回了未知的情绪词 '{detected_emotion_word}' 或未返回有效词,情绪未主动更新。" + ) except Exception as e: logger.error(f"[私聊][{self.private_name}] 情绪判断LLM调用或处理失败: {e}") # 无论LLM判断如何,都更新conversation_info中的情绪文本以供Prompt使用 - if conversation_info and self.mood_mng: # 确保conversation_info有效 - conversation_info.current_emotion_text = self.mood_mng.get_prompt() \ No newline at end of file + if conversation_info and self.mood_mng: # 确保conversation_info有效 + conversation_info.current_emotion_text = self.mood_mng.get_prompt() diff --git a/src/plugins/PFC/pfc_manager.py b/src/plugins/PFC/pfc_manager.py index 891e1906..ce6f2566 100644 --- a/src/plugins/PFC/pfc_manager.py +++ b/src/plugins/PFC/pfc_manager.py @@ -6,8 +6,9 @@ from typing import Dict, Optional from src.common.logger import get_module_logger from .conversation import Conversation from .conversation_initializer import initialize_core_components + # >>> 新增导入 <<< -from .pfc_types import ConversationState # 导入 ConversationState +from .pfc_types import ConversationState # 导入 ConversationState logger = get_module_logger("pfc_manager") @@ -17,7 +18,7 @@ class PFCManager: _instance = None _instances: Dict[str, Conversation] = {} - _initializing: Dict[str, bool] = {} # 用于防止并发初始化同一个 stream_id + _initializing: Dict[str, bool] = {} # 用于防止并发初始化同一个 stream_id @classmethod def get_instance(cls) -> "PFCManager": @@ -56,10 +57,9 @@ class PFCManager: await self._cleanup_conversation(instance) if stream_id in self._instances: del self._instances[stream_id] - if stream_id in self._initializing: # 确保也从这里移除 + if stream_id in self._initializing: # 确保也从这里移除 del self._initializing[stream_id] - conversation_instance: Optional[Conversation] = None try: logger.info(f"[私聊][{private_name}] 创建新的对话实例: {stream_id}") @@ -74,11 +74,11 @@ class PFCManager: # 检查初始化结果并启动 if conversation_instance._initialized and conversation_instance.should_continue: logger.info(f"[私聊][{private_name}] 初始化成功,调用 conversation.start() 启动主循环...") - await conversation_instance.start() # start 方法内部会创建 loop 任务 + await conversation_instance.start() # start 方法内部会创建 loop 任务 else: logger.error(f"[私聊][{private_name}] 初始化未成功完成,无法启动实例 {stream_id}。") await self._cleanup_conversation(conversation_instance) - if stream_id in self._instances: # 再次检查以防万一 + if stream_id in self._instances: # 再次检查以防万一 del self._instances[stream_id] conversation_instance = None @@ -91,8 +91,8 @@ class PFCManager: del self._instances[stream_id] conversation_instance = None finally: - if stream_id in self._initializing: # 确保在 finally 中也检查 - self._initializing[stream_id] = False # 清除初始化标记 + if stream_id in self._initializing: # 确保在 finally 中也检查 + self._initializing[stream_id] = False # 清除初始化标记 return conversation_instance @@ -176,4 +176,4 @@ class PFCManager: logger.error(f"[管理器] 移除或清理会话实例 {stream_id} 时失败: {e}") logger.error(traceback.format_exc()) else: - logger.warning(f"[管理器] 尝试移除不存在的会话实例: {stream_id}") \ No newline at end of file + logger.warning(f"[管理器] 尝试移除不存在的会话实例: {stream_id}") diff --git a/src/plugins/PFC/pfc_processor.py b/src/plugins/PFC/pfc_processor.py index 523e2261..428db544 100644 --- a/src/plugins/PFC/pfc_processor.py +++ b/src/plugins/PFC/pfc_processor.py @@ -13,6 +13,7 @@ from datetime import datetime logger = get_logger("pfc_processor") + async def _handle_error(error: Exception, context: str, message: Optional[MessageRecv] = None) -> None: """统一的错误处理函数 @@ -26,8 +27,9 @@ async def _handle_error(error: Exception, context: str, message: Optional[Messag if message and hasattr(message, "raw_message"): logger.error(f"相关消息原始内容: {message.raw_message}") + class PFCProcessor: - """ PFC 处理器,负责处理接收到的信息并计数""" + """PFC 处理器,负责处理接收到的信息并计数""" def __init__(self): """初始化 PFC 处理器,创建消息存储实例""" @@ -105,9 +107,7 @@ class PFCProcessor: """检查消息中是否包含过滤词""" for word in global_config.ban_words: if word in text: - logger.info( - f"[私聊]{userinfo.user_nickname}:{text}" - ) + logger.info(f"[私聊]{userinfo.user_nickname}:{text}") logger.info(f"[过滤词识别]消息中含有{word},filtered") return True return False @@ -117,9 +117,7 @@ class PFCProcessor: """检查消息是否匹配过滤正则表达式""" for pattern in global_config.ban_msgs_regex: if pattern.search(text): - logger.info( - f"[私聊]{userinfo.user_nickname}:{text}" - ) + logger.info(f"[私聊]{userinfo.user_nickname}:{text}") logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered") return True - return False \ No newline at end of file + return False diff --git a/src/plugins/PFC/pfc_relationship.py b/src/plugins/PFC/pfc_relationship.py index be2abf48..aa0586e3 100644 --- a/src/plugins/PFC/pfc_relationship.py +++ b/src/plugins/PFC/pfc_relationship.py @@ -3,16 +3,19 @@ from src.plugins.PFC.chat_observer import ChatObserver from src.common.logger_manager import get_logger from src.plugins.models.utils_model import LLMRequest from src.plugins.person_info.person_info import person_info_manager -from src.plugins.person_info.relationship_manager import relationship_manager # 主要用其 ensure_float 和 build_relationship_info +from src.plugins.person_info.relationship_manager import ( + relationship_manager, +) # 主要用其 ensure_float 和 build_relationship_info from src.plugins.utils.chat_message_builder import build_readable_messages from src.plugins.PFC.observation_info import ObservationInfo from src.plugins.PFC.conversation_info import ConversationInfo from src.plugins.PFC.pfc_utils import get_items_from_json -from src.config.config import global_config # 导入全局配置 (向上两级到 src/, 再到 config) +from src.config.config import global_config # 导入全局配置 (向上两级到 src/, 再到 config) logger = get_logger("pfc_relationship") + class PfcRelationshipUpdater: def __init__(self, private_name: str, bot_name: str): """ @@ -25,48 +28,50 @@ class PfcRelationshipUpdater: self.private_name = private_name self.bot_name = bot_name self.person_info_mng = person_info_manager - self.relationship_mng = relationship_manager # 复用其实例方法 + self.relationship_mng = relationship_manager # 复用其实例方法 # LLM 实例 (为关系评估创建一个新的) # 尝试读取 llm_PFC_relationship_eval 配置,如果不存在则回退 - llm_config_rel_eval = getattr(global_config, 'llm_PFC_relationship_eval', None) + llm_config_rel_eval = getattr(global_config, "llm_PFC_relationship_eval", None) if llm_config_rel_eval and isinstance(llm_config_rel_eval, dict): logger.info(f"[私聊][{self.private_name}] 使用 llm_PFC_relationship_eval 配置初始化关系评估LLM。") self.llm = LLMRequest( model=llm_config_rel_eval, - temperature=llm_config_rel_eval.get("temp", 0.5), # 判断任务通常用较低温度 + temperature=llm_config_rel_eval.get("temp", 0.5), # 判断任务通常用较低温度 max_tokens=llm_config_rel_eval.get("max_tokens", 512), - request_type="pfc_relationship_evaluation" + request_type="pfc_relationship_evaluation", ) else: - logger.warning(f"[私聊][{self.private_name}] 未找到 llm_PFC_relationship_eval 配置或配置无效,将回退使用 llm_PFC_action_planner 的配置。") - llm_config_action_planner = getattr(global_config, 'llm_PFC_action_planner', None) + logger.warning( + f"[私聊][{self.private_name}] 未找到 llm_PFC_relationship_eval 配置或配置无效,将回退使用 llm_PFC_action_planner 的配置。" + ) + llm_config_action_planner = getattr(global_config, "llm_PFC_action_planner", None) if llm_config_action_planner and isinstance(llm_config_action_planner, dict): self.llm = LLMRequest( - model=llm_config_action_planner, # 使用 action_planner 的模型配置 - temperature=llm_config_action_planner.get("temp", 0.5), # 但温度可以尝试低一些 + model=llm_config_action_planner, # 使用 action_planner 的模型配置 + temperature=llm_config_action_planner.get("temp", 0.5), # 但温度可以尝试低一些 max_tokens=llm_config_action_planner.get("max_tokens", 512), - request_type="pfc_relationship_evaluation_fallback" + request_type="pfc_relationship_evaluation_fallback", ) - else: # 极端情况,连 action_planner 的配置都没有 + else: # 极端情况,连 action_planner 的配置都没有 logger.error(f"[私聊][{self.private_name}] 无法找到任何有效的LLM配置用于关系评估!关系更新功能将受限。") - self.llm = None # LLM 未初始化 + self.llm = None # LLM 未初始化 # 从 global_config 读取参数,若无则使用默认值 - self.REL_INCREMENTAL_INTERVAL = getattr(global_config, 'pfc_relationship_incremental_interval', 10) - self.REL_INCREMENTAL_MSG_COUNT = getattr(global_config, 'pfc_relationship_incremental_msg_count', 10) - self.REL_INCREMENTAL_DEFAULT_CHANGE = getattr(global_config, 'pfc_relationship_incremental_default_change', 1.0) - self.REL_INCREMENTAL_MAX_CHANGE = getattr(global_config, 'pfc_relationship_incremental_max_change', 5.0) + self.REL_INCREMENTAL_INTERVAL = getattr(global_config, "pfc_relationship_incremental_interval", 10) + self.REL_INCREMENTAL_MSG_COUNT = getattr(global_config, "pfc_relationship_incremental_msg_count", 10) + self.REL_INCREMENTAL_DEFAULT_CHANGE = getattr(global_config, "pfc_relationship_incremental_default_change", 1.0) + self.REL_INCREMENTAL_MAX_CHANGE = getattr(global_config, "pfc_relationship_incremental_max_change", 5.0) - self.REL_FINAL_MSG_COUNT = getattr(global_config, 'pfc_relationship_final_msg_count', 30) - self.REL_FINAL_DEFAULT_CHANGE = getattr(global_config, 'pfc_relationship_final_default_change', 5.0) - self.REL_FINAL_MAX_CHANGE = getattr(global_config, 'pfc_relationship_final_max_change', 50.0) + self.REL_FINAL_MSG_COUNT = getattr(global_config, "pfc_relationship_final_msg_count", 30) + self.REL_FINAL_DEFAULT_CHANGE = getattr(global_config, "pfc_relationship_final_default_change", 5.0) + self.REL_FINAL_MAX_CHANGE = getattr(global_config, "pfc_relationship_final_max_change", 50.0) async def update_relationship_incremental( self, conversation_info: ConversationInfo, observation_info: ObservationInfo, - chat_observer_for_history: ChatObserver # ChatObserver 实例 + chat_observer_for_history: ChatObserver, # ChatObserver 实例 ) -> None: if not self.llm: logger.error(f"[私聊][{self.private_name}] LLM未初始化,无法进行增量关系更新。") @@ -75,18 +80,22 @@ class PfcRelationshipUpdater: logger.debug(f"[私聊][{self.private_name}] 增量关系更新:缺少必要信息。") return - if not (conversation_info.current_instance_message_count % self.REL_INCREMENTAL_INTERVAL == 0 \ - and conversation_info.current_instance_message_count > 0): + if not ( + conversation_info.current_instance_message_count % self.REL_INCREMENTAL_INTERVAL == 0 + and conversation_info.current_instance_message_count > 0 + ): return - logger.info(f"[私聊][{self.private_name}] 达到增量关系更新阈值 ({conversation_info.current_instance_message_count}条消息),开始评估...") + logger.info( + f"[私聊][{self.private_name}] 达到增量关系更新阈值 ({conversation_info.current_instance_message_count}条消息),开始评估..." + ) messages_for_eval: List[Dict[str, Any]] = [] if chat_observer_for_history: messages_for_eval = chat_observer_for_history.get_cached_messages(limit=self.REL_INCREMENTAL_MSG_COUNT) elif observation_info.chat_history: - messages_for_eval = observation_info.chat_history[-self.REL_INCREMENTAL_MSG_COUNT:] - + messages_for_eval = observation_info.chat_history[-self.REL_INCREMENTAL_MSG_COUNT :] + if not messages_for_eval: logger.warning(f"[私聊][{self.private_name}] 增量关系更新:没有足够的消息进行评估。") return @@ -95,8 +104,12 @@ class PfcRelationshipUpdater: messages_for_eval, replace_bot_name=True, merge_messages=False, timestamp_mode="relative" ) - current_relationship_value = await self.person_info_mng.get_value(conversation_info.person_id, "relationship_value") - current_relationship_value = self.relationship_mng.ensure_float(current_relationship_value, conversation_info.person_id) + current_relationship_value = await self.person_info_mng.get_value( + conversation_info.person_id, "relationship_value" + ) + current_relationship_value = self.relationship_mng.ensure_float( + current_relationship_value, conversation_info.person_id + ) relationship_prompt = f"""你是{self.bot_name}。你正在与{self.private_name}私聊。 你们当前的关系值大约是 {current_relationship_value:.0f} (范围通常在-1000到1000,越高越代表关系越好)。 @@ -115,9 +128,11 @@ class PfcRelationshipUpdater: logger.debug(f"[私聊][{self.private_name}] 增量关系评估LLM原始返回: {content}") success, result = get_items_from_json( - content, self.private_name, "adjustment", + content, + self.private_name, + "adjustment", default_values={"adjustment": self.REL_INCREMENTAL_DEFAULT_CHANGE}, - required_types={"adjustment": (int, float)} + required_types={"adjustment": (int, float)}, ) raw_adjustment = result.get("adjustment", self.REL_INCREMENTAL_DEFAULT_CHANGE) if not isinstance(raw_adjustment, (int, float)): @@ -129,17 +144,23 @@ class PfcRelationshipUpdater: logger.error(f"[私聊][{self.private_name}] 增量关系评估LLM调用或解析失败: {e}") new_relationship_value = max(-1000.0, min(1000.0, current_relationship_value + adjustment_val)) - await self.person_info_mng.update_one_field(conversation_info.person_id, "relationship_value", new_relationship_value) - logger.info(f"[私聊][{self.private_name}] 增量关系值更新:与【{self.private_name}】的关系值从 {current_relationship_value:.2f} 调整了 {adjustment_val:.2f},变为 {new_relationship_value:.2f}") + await self.person_info_mng.update_one_field( + conversation_info.person_id, "relationship_value", new_relationship_value + ) + logger.info( + f"[私聊][{self.private_name}] 增量关系值更新:与【{self.private_name}】的关系值从 {current_relationship_value:.2f} 调整了 {adjustment_val:.2f},变为 {new_relationship_value:.2f}" + ) if conversation_info.person_id: - conversation_info.relationship_text = await self.relationship_mng.build_relationship_info(conversation_info.person_id, is_id=True) + conversation_info.relationship_text = await self.relationship_mng.build_relationship_info( + conversation_info.person_id, is_id=True + ) async def update_relationship_final( self, conversation_info: ConversationInfo, observation_info: ObservationInfo, - chat_observer_for_history: ChatObserver + chat_observer_for_history: ChatObserver, ) -> None: if not self.llm: logger.error(f"[私聊][{self.private_name}] LLM未初始化,无法进行最终关系更新。") @@ -147,14 +168,14 @@ class PfcRelationshipUpdater: if not conversation_info or not conversation_info.person_id or not observation_info: logger.debug(f"[私聊][{self.private_name}] 最终关系更新:缺少必要信息。") return - + logger.info(f"[私聊][{self.private_name}] 私聊结束,开始最终关系评估...") messages_for_eval: List[Dict[str, Any]] = [] if chat_observer_for_history: messages_for_eval = chat_observer_for_history.get_cached_messages(limit=self.REL_FINAL_MSG_COUNT) elif observation_info.chat_history: - messages_for_eval = observation_info.chat_history[-self.REL_FINAL_MSG_COUNT:] + messages_for_eval = observation_info.chat_history[-self.REL_FINAL_MSG_COUNT :] if not messages_for_eval: logger.warning(f"[私聊][{self.private_name}] 最终关系更新:没有足够的消息进行评估。") @@ -164,8 +185,12 @@ class PfcRelationshipUpdater: messages_for_eval, replace_bot_name=True, merge_messages=False, timestamp_mode="relative" ) - current_relationship_value = await self.person_info_mng.get_value(conversation_info.person_id, "relationship_value") - current_relationship_value = self.relationship_mng.ensure_float(current_relationship_value, conversation_info.person_id) + current_relationship_value = await self.person_info_mng.get_value( + conversation_info.person_id, "relationship_value" + ) + current_relationship_value = self.relationship_mng.ensure_float( + current_relationship_value, conversation_info.person_id + ) relationship_prompt = f"""你是{self.bot_name}。你与{self.private_name}的私聊刚刚结束。 你们当前的关系值大约是 {current_relationship_value:.0f} (范围通常在-1000到1000,越高越好)。 @@ -184,9 +209,11 @@ class PfcRelationshipUpdater: logger.debug(f"[私聊][{self.private_name}] 最终关系评估LLM原始返回: {content}") success, result = get_items_from_json( - content, self.private_name, "final_adjustment", + content, + self.private_name, + "final_adjustment", default_values={"final_adjustment": self.REL_FINAL_DEFAULT_CHANGE}, - required_types={"final_adjustment": (int, float)} + required_types={"final_adjustment": (int, float)}, ) raw_adjustment = result.get("final_adjustment", self.REL_FINAL_DEFAULT_CHANGE) if not isinstance(raw_adjustment, (int, float)): @@ -198,11 +225,17 @@ class PfcRelationshipUpdater: logger.error(f"[私聊][{self.private_name}] 最终关系评估LLM调用或解析失败: {e}") new_relationship_value = max(-1000.0, min(1000.0, current_relationship_value + adjustment_val)) - await self.person_info_mng.update_one_field(conversation_info.person_id, "relationship_value", new_relationship_value) - logger.info(f"[私聊][{self.private_name}] 最终关系值更新:与【{self.private_name}】的关系值从 {current_relationship_value:.2f} 调整了 {adjustment_val:.2f},最终为 {new_relationship_value:.2f}") - - if conversation_info.person_id: # 虽然通常结束了,但更新一下无妨 - conversation_info.relationship_text = await self.relationship_mng.build_relationship_info(conversation_info.person_id, is_id=True) + await self.person_info_mng.update_one_field( + conversation_info.person_id, "relationship_value", new_relationship_value + ) + logger.info( + f"[私聊][{self.private_name}] 最终关系值更新:与【{self.private_name}】的关系值从 {current_relationship_value:.2f} 调整了 {adjustment_val:.2f},最终为 {new_relationship_value:.2f}" + ) + + if conversation_info.person_id: # 虽然通常结束了,但更新一下无妨 + conversation_info.relationship_text = await self.relationship_mng.build_relationship_info( + conversation_info.person_id, is_id=True + ) class PfcRepationshipTranslator: @@ -210,6 +243,7 @@ class PfcRepationshipTranslator: 因为对于PFC的planner来说 其暗示了选择回复 所以新建代码文件来适配PFC的决策层面""" + def __init__(self, private_name: str): self.private_name = private_name @@ -220,22 +254,22 @@ class PfcRepationshipTranslator: level_num = self._calculate_relationship_level_num(relationship_value, self.private_name) relationship_descriptions = [ - "厌恶", # level_num 0 - "冷漠", # level_num 1 - "初识", # level_num 2 - "友好", # level_num 3 - "喜欢", # level_num 4 - "暧昧" # level_num 5 + "厌恶", # level_num 0 + "冷漠", # level_num 1 + "初识", # level_num 2 + "友好", # level_num 3 + "喜欢", # level_num 4 + "暧昧", # level_num 5 ] if 0 <= level_num < len(relationship_descriptions): description = relationship_descriptions[level_num] else: - description = "普通" # 默认或错误情况 + description = "普通" # 默认或错误情况 logger.warning(f"[私聊][{self.private_name}] 计算出的 level_num ({level_num}) 无效,关系描述默认为 '普通'") return f"你们的关系是:{description}。" - + @staticmethod def _calculate_relationship_level_num(relationship_value: float, private_name: str) -> int: """ @@ -243,7 +277,9 @@ class PfcRepationshipTranslator: 这里的阈值应与 relationship_manager.py 中的保持一致 """ if not isinstance(relationship_value, (int, float)): - logger.warning(f"[私聊][{private_name}] 传入的 relationship_value '{relationship_value}' 不是有效的数值类型,默认为0。") + logger.warning( + f"[私聊][{private_name}] 传入的 relationship_value '{relationship_value}' 不是有效的数值类型,默认为0。" + ) relationship_value = 0.0 if -1000 <= relationship_value < -227: @@ -264,7 +300,7 @@ class PfcRepationshipTranslator: level_num = 5 elif relationship_value < -1000: level_num = 0 - else: # 理论上不会到这里,除非前面的条件逻辑有误 + else: # 理论上不会到这里,除非前面的条件逻辑有误 logger.warning(f"[私聊][{private_name}] 关系值 {relationship_value} 未落入任何预设范围,默认为普通。") - level_num = 2 - return level_num \ No newline at end of file + level_num = 2 + return level_num diff --git a/src/plugins/PFC/pfc_utils.py b/src/plugins/PFC/pfc_utils.py index fb571062..47430af6 100644 --- a/src/plugins/PFC/pfc_utils.py +++ b/src/plugins/PFC/pfc_utils.py @@ -101,7 +101,7 @@ def get_items_from_json( Tuple[bool, Union[Dict[str, Any], List[Dict[str, Any]]]]: (是否成功, 提取的字段字典或字典列表) """ cleaned_content = content.strip() - result: Union[Dict[str, Any], List[Dict[str, Any]]] = {} # 初始化类型 + result: Union[Dict[str, Any], List[Dict[str, Any]]] = {} # 初始化类型 # 匹配 ```json ... ``` 或 ``` ... ``` markdown_match = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", cleaned_content, re.IGNORECASE) if markdown_match: @@ -109,12 +109,11 @@ def get_items_from_json( logger.debug(f"[私聊][{private_name}] 已去除 Markdown 标记,剩余内容: {cleaned_content[:100]}...") # --- 新增结束 --- - # 设置默认值 - default_result: Dict[str, Any] = {} # 用于单对象时的默认值 + default_result: Dict[str, Any] = {} # 用于单对象时的默认值 if default_values: default_result.update(default_values) - result = default_result.copy() # 先用默认值初始化 + result = default_result.copy() # 先用默认值初始化 # 首先尝试解析为JSON数组 if allow_array: @@ -129,17 +128,17 @@ def get_items_from_json( logger.warning(f"[私聊][{private_name}] JSON数组中的元素不是字典: {item}") continue - current_item_result = default_result.copy() # 每个元素都用默认值初始化 + current_item_result = default_result.copy() # 每个元素都用默认值初始化 valid_item = True # 提取并验证字段 for field in items: if field in item: current_item_result[field] = item[field] - elif field not in default_result: # 如果字段不存在且没有默认值 + elif field not in default_result: # 如果字段不存在且没有默认值 logger.warning(f"[私聊][{private_name}] JSON数组元素缺少必要字段 '{field}': {item}") valid_item = False - break # 这个元素无效 + break # 这个元素无效 if not valid_item: continue @@ -148,25 +147,33 @@ def get_items_from_json( if required_types: for field, expected_type in required_types.items(): # 检查 current_item_result 中是否存在该字段 (可能来自 item 或 default_values) - if field in current_item_result and not isinstance(current_item_result[field], expected_type): - logger.warning(f"[私聊][{private_name}] JSON数组元素字段 '{field}' 类型错误 (应为 {expected_type.__name__}, 实际为 {type(current_item_result[field]).__name__}): {item}") + if field in current_item_result and not isinstance( + current_item_result[field], expected_type + ): + logger.warning( + f"[私聊][{private_name}] JSON数组元素字段 '{field}' 类型错误 (应为 {expected_type.__name__}, 实际为 {type(current_item_result[field]).__name__}): {item}" + ) valid_item = False break if not valid_item: continue - + # 验证字符串不为空 (只检查 items 中要求的字段) for field in items: - if field in current_item_result and isinstance(current_item_result[field], str) and not current_item_result[field].strip(): + if ( + field in current_item_result + and isinstance(current_item_result[field], str) + and not current_item_result[field].strip() + ): logger.warning(f"[私聊][{private_name}] JSON数组元素字段 '{field}' 不能为空字符串: {item}") valid_item = False break if valid_item: - valid_items_list.append(current_item_result) # 只添加完全有效的项 + valid_items_list.append(current_item_result) # 只添加完全有效的项 - if valid_items_list: # 只有当列表不为空时才认为是成功 + if valid_items_list: # 只有当列表不为空时才认为是成功 logger.debug(f"[私聊][{private_name}] 成功解析JSON数组,包含 {len(valid_items_list)} 个有效项目。") return True, valid_items_list else: @@ -184,19 +191,18 @@ def get_items_from_json( # result 重置回单个对象的默认值 result = default_result.copy() - # 尝试解析为单个JSON对象 try: # 尝试直接解析清理后的内容 json_data = json.loads(cleaned_content) if not isinstance(json_data, dict): logger.error(f"[私聊][{private_name}] 解析为单个对象,但结果不是字典类型: {type(json_data)}") - return False, default_result # 返回失败和默认值 + return False, default_result # 返回失败和默认值 except json.JSONDecodeError: # 如果直接解析失败,尝试用正则表达式查找 JSON 对象部分 (作为后备) # 这个正则比较简单,可能无法处理嵌套或复杂的 JSON - json_pattern = r"\{[\s\S]*?\}" # 使用非贪婪匹配 + json_pattern = r"\{[\s\S]*?\}" # 使用非贪婪匹配 json_match = re.search(json_pattern, cleaned_content) if json_match: try: @@ -210,23 +216,24 @@ def get_items_from_json( logger.error(f"[私聊][{private_name}] 正则提取的部分 '{potential_json_str[:100]}...' 无法解析为JSON。") return False, default_result else: - logger.error(f"[私聊][{private_name}] 无法在返回内容中找到有效的JSON对象部分。原始内容: {cleaned_content[:100]}...") + logger.error( + f"[私聊][{private_name}] 无法在返回内容中找到有效的JSON对象部分。原始内容: {cleaned_content[:100]}..." + ) return False, default_result - # 提取并验证字段 (适用于单个JSON对象) # 确保 result 是字典类型用于更新 if not isinstance(result, dict): - result = default_result.copy() # 如果之前是列表,重置为字典 + result = default_result.copy() # 如果之前是列表,重置为字典 valid_single_object = True for item in items: if item in json_data: result[item] = json_data[item] - elif item not in default_result: # 如果字段不存在且没有默认值 + elif item not in default_result: # 如果字段不存在且没有默认值 logger.error(f"[私聊][{private_name}] JSON对象缺少必要字段 '{item}'。JSON内容: {json_data}") valid_single_object = False - break # 这个对象无效 + break # 这个对象无效 if not valid_single_object: return False, default_result @@ -235,7 +242,9 @@ def get_items_from_json( if required_types: for field, expected_type in required_types.items(): if field in result and not isinstance(result[field], expected_type): - logger.error(f"[私聊][{private_name}] JSON对象字段 '{field}' 类型错误 (应为 {expected_type.__name__}, 实际为 {type(result[field]).__name__})") + logger.error( + f"[私聊][{private_name}] JSON对象字段 '{field}' 类型错误 (应为 {expected_type.__name__}, 实际为 {type(result[field]).__name__})" + ) valid_single_object = False break @@ -251,9 +260,9 @@ def get_items_from_json( if valid_single_object: logger.debug(f"[私聊][{private_name}] 成功解析并验证了单个JSON对象。") - return True, result # 返回提取并验证后的字典 + return True, result # 返回提取并验证后的字典 else: - return False, default_result # 验证失败 + return False, default_result # 验证失败 async def get_person_id(private_name: str, chat_stream: ChatStream): @@ -264,7 +273,9 @@ async def get_person_id(private_name: str, chat_stream: ChatStream): if chat_stream.user_info: private_user_id_str = str(chat_stream.user_info.user_id) private_platform_str = chat_stream.user_info.platform - logger.info(f"[私聊][{private_name}] 从 ChatStream 获取到私聊对象信息: ID={private_user_id_str}, Platform={private_platform_str}, Name={private_nickname_str}") + logger.info( + f"[私聊][{private_name}] 从 ChatStream 获取到私聊对象信息: ID={private_user_id_str}, Platform={private_platform_str}, Name={private_nickname_str}" + ) elif chat_stream.group_info is None and private_name: pass @@ -279,19 +290,21 @@ async def get_person_id(private_name: str, chat_stream: ChatStream): person_id = await person_info_manager.get_or_create_person( platform=private_platform_str, user_id=private_user_id_int, - nickname=private_name, # 使用传入的 private_name 作为昵称 + nickname=private_name, # 使用传入的 private_name 作为昵称 ) - if person_id is None: # 如果 get_or_create_person 返回 None,说明创建失败 + if person_id is None: # 如果 get_or_create_person 返回 None,说明创建失败 logger.error(f"[私聊][{private_name}] get_or_create_person 未能获取或创建 person_id。") - return None # 返回 None 表示失败 + return None # 返回 None 表示失败 - return person_id, private_platform_str, private_user_id_str # 返回获取或创建的 person_id + return person_id, private_platform_str, private_user_id_str # 返回获取或创建的 person_id except ValueError: logger.error(f"[私聊][{private_name}] 无法将 private_user_id_str ('{private_user_id_str}') 转换为整数。") - return None # 返回 None 表示失败 + return None # 返回 None 表示失败 except Exception as e_pid: logger.error(f"[私聊][{private_name}] 获取或创建 person_id 时出错: {e_pid}") - return None # 返回 None 表示失败 + return None # 返回 None 表示失败 else: - logger.warning(f"[私聊][{private_name}] 未能确定私聊对象的 user_id 或 platform,无法获取 person_id。将在收到消息后尝试。") - return None # 返回 None 表示失败 \ No newline at end of file + logger.warning( + f"[私聊][{private_name}] 未能确定私聊对象的 user_id 或 platform,无法获取 person_id。将在收到消息后尝试。" + ) + return None # 返回 None 表示失败 diff --git a/src/plugins/PFC/reply_checker.py b/src/plugins/PFC/reply_checker.py index 47578502..aa00cd96 100644 --- a/src/plugins/PFC/reply_checker.py +++ b/src/plugins/PFC/reply_checker.py @@ -1,6 +1,6 @@ from typing import Tuple, List, Dict, Any from src.common.logger import get_module_logger -from src.config.config import global_config # 为了获取 BOT_QQ +from src.config.config import global_config # 为了获取 BOT_QQ from .chat_observer import ChatObserver logger = get_module_logger("reply_checker") @@ -15,10 +15,16 @@ class ReplyChecker: self.private_name = private_name self.chat_observer = ChatObserver.get_instance(stream_id, private_name) # self.max_retries = 3 # 这个 max_retries 属性在当前设计下不再由 checker 控制,而是由 conversation.py 控制 - self.bot_qq_str = str(global_config.BOT_QQ) # 获取机器人QQ号用于识别自身消息 + self.bot_qq_str = str(global_config.BOT_QQ) # 获取机器人QQ号用于识别自身消息 async def check( - self, reply: str, goal: str, chat_history: List[Dict[str, Any]], chat_history_text: str, current_time_str: str, retry_count: int = 0 + self, + reply: str, + goal: str, + chat_history: List[Dict[str, Any]], + chat_history_text: str, + current_time_str: str, + retry_count: int = 0, ) -> Tuple[bool, str, bool]: """检查生成的回复是否与机器人之前的发言完全一致(长度大于4) @@ -37,15 +43,14 @@ class ReplyChecker: """ if not self.bot_qq_str: logger.error(f"[私聊][{self.private_name}] ReplyChecker: BOT_QQ 未配置,无法检查机器人自身消息。") - return True, "BOT_QQ未配置,跳过重复检查。", False # 无法检查则默认通过 + return True, "BOT_QQ未配置,跳过重复检查。", False # 无法检查则默认通过 if len(reply) <= 4: return True, "消息长度小于等于4字符,跳过重复检查。", False - try: - match_found = False # <--- 用于调试 - for i, msg_dict in enumerate(chat_history): # <--- 添加索引用于日志 + match_found = False # <--- 用于调试 + for i, msg_dict in enumerate(chat_history): # <--- 添加索引用于日志 if not isinstance(msg_dict, dict): continue @@ -54,7 +59,7 @@ class ReplyChecker: continue sender_id = str(user_info_data.get("user_id")) - + if sender_id == self.bot_qq_str: historical_message_text = msg_dict.get("processed_plain_text", "") # <--- 新增详细对比日志 --- START ---> @@ -62,23 +67,20 @@ class ReplyChecker: f"[私聊][{self.private_name}] ReplyChecker: 历史记录 #{i} (机器人): '{historical_message_text}' (长度 {len(historical_message_text)})" ) if reply == historical_message_text: - logger.warning( - f"[私聊][{self.private_name}] ReplyChecker: !!! 精确匹配成功 !!!" - ) - logger.warning( - f"[私聊][{self.private_name}] ReplyChecker 检测到机器人自身重复消息: '{reply}'" - ) - match_found = True # <--- 标记找到 - return (False, "机器人尝试发送重复消息", False) + logger.warning(f"[私聊][{self.private_name}] ReplyChecker: !!! 精确匹配成功 !!!") + logger.warning(f"[私聊][{self.private_name}] ReplyChecker 检测到机器人自身重复消息: '{reply}'") + match_found = True # <--- 标记找到 + return (False, "机器人尝试发送重复消息", False) # <--- 新增详细对比日志 --- END ---> - - if not match_found: # <--- 根据标记判断 - logger.debug(f"[私聊][{self.private_name}] ReplyChecker: 未找到重复。") # <--- 新增日志 + + if not match_found: # <--- 根据标记判断 + logger.debug(f"[私聊][{self.private_name}] ReplyChecker: 未找到重复。") # <--- 新增日志 return (True, "消息内容未与机器人历史发言重复。", False) except Exception as e: import traceback + logger.error(f"[私聊][{self.private_name}] ReplyChecker 检查重复时出错: 类型={type(e)}, 值={e}") logger.error(f"[私聊][{self.private_name}]{traceback.format_exc()}") # 发生未知错误时,为安全起见,默认通过,并记录原因 - return (True, f"检查重复时发生内部错误: {str(e)}", False) \ No newline at end of file + return (True, f"检查重复时发生内部错误: {str(e)}", False) diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index 1de3e937..79942b09 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -19,13 +19,13 @@ PROMPT_GER_VARIATIONS = [ ("避免使用过于正式或书面化的词语,多用生活化的口语表达", 0.8), ("如果对方的发言比较跳跃或难以理解,可以尝试用猜测或确认的语气回应", 0.8), ("如果感觉对话有点干巴,可以尝试引入一些轻松的相关小话题或者自己的小想法,但不要偏离太远", 0.8), - ("注意观察对方的情绪(如果能从文字中判断),并作出适当的回应,比如安慰、鼓励或表示理解", 0.8), - ("", 0.10) + ("注意观察对方的情绪(如果能从文字中判断),并作出适当的回应,比如安慰、鼓励或表示理解", 0.8), + ("", 0.10), ] REPLY_STYLE1_VARIATIONS = [ ("整体风格可以平和、简短", 0.3), - ("回复可以非常简洁,有时甚至用单个词、短语或者一个反问就能表达清楚", 0.10), + ("回复可以非常简洁,有时甚至用单个词、短语或者一个反问就能表达清楚", 0.10), ("尝试使用更自然的口语连接词,例如:然后/所以呢/不过嘛/倒是", 0.05), ("在表达观点时,可以说得主观一些,例如:我觉得.../我个人感觉.../要我说...", 0.10), ("**请省略主语,简短**", 0.4), @@ -37,7 +37,7 @@ REPLY_STYLE2_VARIATIONS = [ ("不要输出任何语气词", 0.10), ("在适当的时候,可以用一些感叹词来表达情绪或态度,例如:哇/啊?/啧啧/哎呀", 0.05), ("可以模糊化表达,例如:'我记得...'", 0.10), - ("对于一些无聊或者不想深入的话题,可以敷衍一下,例如:/哦这样啊/还行吧/随便啦", 0.10), + ("对于一些无聊或者不想深入的话题,可以敷衍一下,例如:/哦这样啊/还行吧/随便啦", 0.10), ("尽量用简单句和短句", 0.25), ("不要输出任何标点符号,简短", 0.30), ] @@ -156,7 +156,7 @@ class ReplyGenerator: self.llm = LLMRequest( model=global_config.llm_PFC_chat, temperature=global_config.llm_PFC_chat["temp"], - max_tokens=300, # 对于JSON输出,这个可能需要适当调整,但一般回复短,JSON结构也简单 + max_tokens=300, # 对于JSON输出,这个可能需要适当调整,但一般回复短,JSON结构也简单 request_type="reply_generation", ) self.personality_info = Individuality.get_instance().get_prompt(x_person=2, level=3) @@ -184,10 +184,20 @@ class ReplyGenerator: logger.debug( f"[私聊][{self.private_name}]开始生成回复 (动作类型: {action_type}):当前目标: {conversation_info.goal_list}" ) - - chosen_prompt_ger = random.choices([style[0] for style in PROMPT_GER_VARIATIONS], weights=[style[1] for style in PROMPT_GER_VARIATIONS], k=1)[0] - chosen_reply_style1 = random.choices([style[0] for style in REPLY_STYLE1_VARIATIONS], weights=[style[1] for style in REPLY_STYLE1_VARIATIONS], k=1)[0] - chosen_reply_style2 = random.choices([style[0] for style in REPLY_STYLE2_VARIATIONS], weights=[style[1] for style in REPLY_STYLE2_VARIATIONS], k=1)[0] + + chosen_prompt_ger = random.choices( + [style[0] for style in PROMPT_GER_VARIATIONS], weights=[style[1] for style in PROMPT_GER_VARIATIONS], k=1 + )[0] + chosen_reply_style1 = random.choices( + [style[0] for style in REPLY_STYLE1_VARIATIONS], + weights=[style[1] for style in REPLY_STYLE1_VARIATIONS], + k=1, + )[0] + chosen_reply_style2 = random.choices( + [style[0] for style in REPLY_STYLE2_VARIATIONS], + weights=[style[1] for style in REPLY_STYLE2_VARIATIONS], + k=1, + )[0] # --- 构建通用 Prompt 参数 --- goals_str = "" @@ -220,16 +230,14 @@ class ReplyGenerator: elif not chat_history_text: chat_history_text = "还没有聊天记录。" else: - chat_history_text += ( - "\n--- 以上均为已读消息,未读消息均已处理完毕 ---\n" - ) + chat_history_text += "\n--- 以上均为已读消息,未读消息均已处理完毕 ---\n" - sender_name_str = getattr(observation_info, 'sender_name', '对方') - if not sender_name_str: - sender_name_str = '对方' + sender_name_str = getattr(observation_info, "sender_name", "对方") + if not sender_name_str: + sender_name_str = "对方" - relationship_text_str = getattr(conversation_info, 'relationship_text', '你们还不熟悉。') - current_emotion_text_str = getattr(conversation_info, 'current_emotion_text', '心情平静。') + relationship_text_str = getattr(conversation_info, "relationship_text", "你们还不熟悉。") + current_emotion_text_str = getattr(conversation_info, "current_emotion_text", "心情平静。") persona_text = f"你的名字是{self.name},{self.personality_info}。" retrieval_context = chat_history_text @@ -270,10 +278,10 @@ class ReplyGenerator: f" 内容: {last_content}\n" f" 原因: {last_reason}" ) - + # 新增:构建刷屏警告信息 for PROMPT_SEND_NEW_MESSAGE spam_warning_message = "" - if action_type == "send_new_message": # 只在 send_new_message 时构建刷屏警告 + if action_type == "send_new_message": # 只在 send_new_message 时构建刷屏警告 if conversation_info.my_message_count > 5: spam_warning_message = f"⚠️【警告】**你已连续发送{str(conversation_info.my_message_count)}条消息!请谨慎考虑是否继续发送!以免刷屏对造成对方困扰!**" elif conversation_info.my_message_count > 2: @@ -281,7 +289,6 @@ class ReplyGenerator: if spam_warning_message: spam_warning_message = f"\n{spam_warning_message}\n" - # --- 选择 Prompt --- if action_type == "send_new_message": prompt_template = PROMPT_SEND_NEW_MESSAGE @@ -289,22 +296,24 @@ class ReplyGenerator: elif action_type == "say_goodbye": prompt_template = PROMPT_FAREWELL logger.info(f"[私聊][{self.private_name}]使用 PROMPT_FAREWELL (告别语生成)") - else: + else: prompt_template = PROMPT_DIRECT_REPLY logger.info(f"[私聊][{self.private_name}]使用 PROMPT_DIRECT_REPLY (首次/非连续回复生成)") # --- 格式化最终的 Prompt --- try: current_time_value = "获取时间失败" - if observation_info and hasattr(observation_info, 'current_time_str') and observation_info.current_time_str: + if observation_info and hasattr(observation_info, "current_time_str") and observation_info.current_time_str: current_time_value = observation_info.current_time_str - + base_format_params = { "persona_text": persona_text, "goals_str": goals_str, "chat_history_text": chat_history_text, - "retrieved_memory_str": retrieved_memory_str if retrieved_memory_str else "无相关记忆。", # 确保已定义 - "retrieved_knowledge_str": retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。", # 确保已定义 + "retrieved_memory_str": retrieved_memory_str if retrieved_memory_str else "无相关记忆。", # 确保已定义 + "retrieved_knowledge_str": retrieved_knowledge_str + if retrieved_knowledge_str + else "无相关知识。", # 确保已定义 "last_rejection_info": last_rejection_info_str, "current_time_str": current_time_value, "sender_name": sender_name_str, @@ -318,17 +327,24 @@ class ReplyGenerator: if action_type == "send_new_message": current_format_params = base_format_params.copy() current_format_params["spam_warning_info"] = spam_warning_message - prompt = prompt_template.format(**current_format_params) + prompt = prompt_template.format(**current_format_params) elif action_type == "say_goodbye": farewell_params = { - k: v for k, v in base_format_params.items() if k in [ - "persona_text", "chat_history_text", "current_time_str", - "sender_name", "relationship_text", "current_emotion_text", + k: v + for k, v in base_format_params.items() + if k + in [ + "persona_text", + "chat_history_text", + "current_time_str", + "sender_name", + "relationship_text", + "current_emotion_text", ] } - + prompt = prompt_template.format(**farewell_params) - else: # direct_reply + else: # direct_reply current_format_params = base_format_params.copy() prompt = prompt_template.format(**current_format_params) @@ -336,7 +352,7 @@ class ReplyGenerator: logger.error( f"[私聊][{self.private_name}]格式化 Prompt 时出错,缺少键: {e}。请检查 Prompt 模板和传递的参数。" ) - return "抱歉,准备回复时出了点问题,请检查一下我的代码..." # 对于JSON期望的场景,这里可能也需要返回一个固定的错误JSON + return "抱歉,准备回复时出了点问题,请检查一下我的代码..." # 对于JSON期望的场景,这里可能也需要返回一个固定的错误JSON except Exception as fmt_err: logger.error(f"[私聊][{self.private_name}]格式化 Prompt 时发生未知错误: {fmt_err}") return "抱歉,准备回复时出了点内部错误,请检查一下我的代码..." @@ -361,4 +377,4 @@ class ReplyGenerator: "txt": "LLM生成回复时出错" }}""".strip() else: - return "抱歉,我现在有点混乱,让我重新思考一下..." \ No newline at end of file + return "抱歉,我现在有点混乱,让我重新思考一下..." From e5b2e2a5ee5f779ad6d5c768a83c0262373596b2 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Thu, 8 May 2025 12:26:53 +0800 Subject: [PATCH 46/63] =?UTF-8?q?pfc=20=E5=85=B3=E7=B3=BB=E5=80=BC?= =?UTF-8?q?=E8=AE=A1=E7=AE=97=E5=8A=A0=E5=85=A5=E9=9D=9E=E7=BA=BF=E6=80=A7?= =?UTF-8?q?=E6=8D=A2=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/pfc_relationship.py | 23 +++++++++++++--------- src/plugins/PFC/pfc_utils.py | 30 +++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/plugins/PFC/pfc_relationship.py b/src/plugins/PFC/pfc_relationship.py index aa0586e3..fb6e96aa 100644 --- a/src/plugins/PFC/pfc_relationship.py +++ b/src/plugins/PFC/pfc_relationship.py @@ -9,7 +9,7 @@ from src.plugins.person_info.relationship_manager import ( from src.plugins.utils.chat_message_builder import build_readable_messages from src.plugins.PFC.observation_info import ObservationInfo from src.plugins.PFC.conversation_info import ConversationInfo -from src.plugins.PFC.pfc_utils import get_items_from_json +from src.plugins.PFC.pfc_utils import get_items_from_json, adjust_relationship_value_nonlinear from src.config.config import global_config # 导入全局配置 (向上两级到 src/, 再到 config) @@ -121,7 +121,7 @@ class PfcRelationshipUpdater: 请输出一个JSON对象,包含一个 "adjustment" 字段,其值为一个介于 -{self.REL_INCREMENTAL_MAX_CHANGE} 和 +{self.REL_INCREMENTAL_MAX_CHANGE} 之间的整数,代表关系值的变化。 例如:{{ "adjustment": 3 }}。如果对话内容不明确或难以判断,请倾向于输出较小的调整值(如0, 1, -1)。""" - adjustment_val = self.REL_INCREMENTAL_DEFAULT_CHANGE + raw_adjustment_val = self.REL_INCREMENTAL_DEFAULT_CHANGE try: logger.debug(f"[私聊][{self.private_name}] 增量关系评估Prompt:\n{relationship_prompt}") content, _ = await self.llm.generate_response_async(relationship_prompt) @@ -136,13 +136,15 @@ class PfcRelationshipUpdater: ) raw_adjustment = result.get("adjustment", self.REL_INCREMENTAL_DEFAULT_CHANGE) if not isinstance(raw_adjustment, (int, float)): - adjustment_val = self.REL_INCREMENTAL_DEFAULT_CHANGE + raw_adjustment_val = self.REL_INCREMENTAL_DEFAULT_CHANGE else: - adjustment_val = float(raw_adjustment) - adjustment_val = max(-self.REL_INCREMENTAL_MAX_CHANGE, min(self.REL_INCREMENTAL_MAX_CHANGE, adjustment_val)) + raw_adjustment_val = float(raw_adjustment) + raw_adjustment_val = max(-self.REL_INCREMENTAL_MAX_CHANGE, min(self.REL_INCREMENTAL_MAX_CHANGE, raw_adjustment_val)) except Exception as e: logger.error(f"[私聊][{self.private_name}] 增量关系评估LLM调用或解析失败: {e}") + adjustment_val = await adjust_relationship_value_nonlinear(current_relationship_value, raw_adjustment_val) + new_relationship_value = max(-1000.0, min(1000.0, current_relationship_value + adjustment_val)) await self.person_info_mng.update_one_field( conversation_info.person_id, "relationship_value", new_relationship_value @@ -202,7 +204,7 @@ class PfcRelationshipUpdater: 请输出一个JSON对象,包含一个 "final_adjustment" 字段,其值为一个整数,代表关系值的变化量(例如,可以是 -{self.REL_FINAL_MAX_CHANGE} 到 +{self.REL_FINAL_MAX_CHANGE} 之间的一个值)。 请大胆评估,但也要合理。""" - adjustment_val = self.REL_FINAL_DEFAULT_CHANGE + raw_adjustment_val = self.REL_FINAL_DEFAULT_CHANGE try: logger.debug(f"[私聊][{self.private_name}] 最终关系评估Prompt:\n{relationship_prompt}") content, _ = await self.llm.generate_response_async(relationship_prompt) @@ -217,13 +219,16 @@ class PfcRelationshipUpdater: ) raw_adjustment = result.get("final_adjustment", self.REL_FINAL_DEFAULT_CHANGE) if not isinstance(raw_adjustment, (int, float)): - adjustment_val = self.REL_FINAL_DEFAULT_CHANGE + raw_adjustment_val = self.REL_FINAL_DEFAULT_CHANGE else: - adjustment_val = float(raw_adjustment) - adjustment_val = max(-self.REL_FINAL_MAX_CHANGE, min(self.REL_FINAL_MAX_CHANGE, adjustment_val)) + raw_adjustment_val = float(raw_adjustment) + raw_adjustment_val = max(-self.REL_INCREMENTAL_MAX_CHANGE, min(self.REL_INCREMENTAL_MAX_CHANGE, raw_adjustment_val)) except Exception as e: logger.error(f"[私聊][{self.private_name}] 最终关系评估LLM调用或解析失败: {e}") + + adjustment_val = await adjust_relationship_value_nonlinear(current_relationship_value, raw_adjustment_val) + new_relationship_value = max(-1000.0, min(1000.0, current_relationship_value + adjustment_val)) await self.person_info_mng.update_one_field( conversation_info.person_id, "relationship_value", new_relationship_value diff --git a/src/plugins/PFC/pfc_utils.py b/src/plugins/PFC/pfc_utils.py index 47430af6..7d0f6695 100644 --- a/src/plugins/PFC/pfc_utils.py +++ b/src/plugins/PFC/pfc_utils.py @@ -7,6 +7,7 @@ from src.plugins.memory_system.Hippocampus import HippocampusManager from src.plugins.heartFC_chat.heartflow_prompt_builder import prompt_builder # 确认 prompt_builder 的导入路径 from src.plugins.chat.chat_stream import ChatStream from ..person_info.person_info import person_info_manager +import math logger = get_logger("pfc_utils") @@ -308,3 +309,32 @@ async def get_person_id(private_name: str, chat_stream: ChatStream): f"[私聊][{private_name}] 未能确定私聊对象的 user_id 或 platform,无法获取 person_id。将在收到消息后尝试。" ) return None # 返回 None 表示失败 + +async def adjust_relationship_value_nonlinear(self, old_value: float, raw_adjustment: float) -> float: + # 限制 old_value 范围 + old_value = max(-1000, min(1000, old_value)) + value = raw_adjustment + + if old_value >= 0: + if value >= 0: + value = value * math.cos(math.pi * old_value / 2000) + if old_value > 500: + rdict = person_info_manager.get_specific_value_list("relationship_value", lambda x: x > 700) + high_value_count = len(rdict) + if old_value > 700: + value *= 3 / (high_value_count + 2) + else: + value *= 3 / (high_value_count + 3) + elif value < 0: + value = value * math.exp(old_value / 2000) + else: + value = 0 + else: + if value >= 0: + value = value * math.exp(old_value / 2000) + elif value < 0: + value = value * math.cos(math.pi * old_value / 2000) + else: + value = 0 + + return value From 2ab60068f856b66c2b6c0cd02ace4fd64864948a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 8 May 2025 04:27:16 +0000 Subject: [PATCH 47/63] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=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/plugins/PFC/pfc_relationship.py | 9 ++++++--- src/plugins/PFC/pfc_utils.py | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/plugins/PFC/pfc_relationship.py b/src/plugins/PFC/pfc_relationship.py index fb6e96aa..3ebc0c1b 100644 --- a/src/plugins/PFC/pfc_relationship.py +++ b/src/plugins/PFC/pfc_relationship.py @@ -139,7 +139,9 @@ class PfcRelationshipUpdater: raw_adjustment_val = self.REL_INCREMENTAL_DEFAULT_CHANGE else: raw_adjustment_val = float(raw_adjustment) - raw_adjustment_val = max(-self.REL_INCREMENTAL_MAX_CHANGE, min(self.REL_INCREMENTAL_MAX_CHANGE, raw_adjustment_val)) + raw_adjustment_val = max( + -self.REL_INCREMENTAL_MAX_CHANGE, min(self.REL_INCREMENTAL_MAX_CHANGE, raw_adjustment_val) + ) except Exception as e: logger.error(f"[私聊][{self.private_name}] 增量关系评估LLM调用或解析失败: {e}") @@ -222,11 +224,12 @@ class PfcRelationshipUpdater: raw_adjustment_val = self.REL_FINAL_DEFAULT_CHANGE else: raw_adjustment_val = float(raw_adjustment) - raw_adjustment_val = max(-self.REL_INCREMENTAL_MAX_CHANGE, min(self.REL_INCREMENTAL_MAX_CHANGE, raw_adjustment_val)) + raw_adjustment_val = max( + -self.REL_INCREMENTAL_MAX_CHANGE, min(self.REL_INCREMENTAL_MAX_CHANGE, raw_adjustment_val) + ) except Exception as e: logger.error(f"[私聊][{self.private_name}] 最终关系评估LLM调用或解析失败: {e}") - adjustment_val = await adjust_relationship_value_nonlinear(current_relationship_value, raw_adjustment_val) new_relationship_value = max(-1000.0, min(1000.0, current_relationship_value + adjustment_val)) diff --git a/src/plugins/PFC/pfc_utils.py b/src/plugins/PFC/pfc_utils.py index 7d0f6695..e4cc39e2 100644 --- a/src/plugins/PFC/pfc_utils.py +++ b/src/plugins/PFC/pfc_utils.py @@ -310,6 +310,7 @@ async def get_person_id(private_name: str, chat_stream: ChatStream): ) return None # 返回 None 表示失败 + async def adjust_relationship_value_nonlinear(self, old_value: float, raw_adjustment: float) -> float: # 限制 old_value 范围 old_value = max(-1000, min(1000, old_value)) From 997b4e4380459807fe75abb77308d1debed88d85 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Thu, 8 May 2025 12:35:45 +0800 Subject: [PATCH 48/63] =?UTF-8?q?=E4=BF=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/pfc_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/PFC/pfc_utils.py b/src/plugins/PFC/pfc_utils.py index 7d0f6695..96aeaa7f 100644 --- a/src/plugins/PFC/pfc_utils.py +++ b/src/plugins/PFC/pfc_utils.py @@ -310,7 +310,7 @@ async def get_person_id(private_name: str, chat_stream: ChatStream): ) return None # 返回 None 表示失败 -async def adjust_relationship_value_nonlinear(self, old_value: float, raw_adjustment: float) -> float: +async def adjust_relationship_value_nonlinear(old_value: float, raw_adjustment: float) -> float: # 限制 old_value 范围 old_value = max(-1000, min(1000, old_value)) value = raw_adjustment From 5f2fac4a6d3244e8e589c9888c602c46eeecef74 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Thu, 8 May 2025 13:13:30 +0800 Subject: [PATCH 49/63] =?UTF-8?q?=E5=8F=98=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/pfc_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/PFC/pfc_utils.py b/src/plugins/PFC/pfc_utils.py index 30333d7d..6047aa81 100644 --- a/src/plugins/PFC/pfc_utils.py +++ b/src/plugins/PFC/pfc_utils.py @@ -320,7 +320,7 @@ async def adjust_relationship_value_nonlinear(old_value: float, raw_adjustment: if value >= 0: value = value * math.cos(math.pi * old_value / 2000) if old_value > 500: - rdict = person_info_manager.get_specific_value_list("relationship_value", lambda x: x > 700) + rdict = await person_info_manager.get_specific_value_list("relationship_value", lambda x: x > 700) high_value_count = len(rdict) if old_value > 700: value *= 3 / (high_value_count + 2) From 5b1e5aa50e265a65af04e84e6399f4b789230fe2 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 8 May 2025 18:54:49 +0800 Subject: [PATCH 50/63] Update logger.py --- src/common/logger.py | 50 -------------------------------------------- 1 file changed, 50 deletions(-) diff --git a/src/common/logger.py b/src/common/logger.py index 318d9b37..f7d6fb28 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -6,56 +6,6 @@ from types import ModuleType from pathlib import Path from dotenv import load_dotenv -""" -日志颜色说明: - -1. 主程序(Main) -浅黄色标题 | 浅黄色消息 - -2. 海马体(Memory) -浅黄色标题 | 浅黄色消息 - -3. PFC(前额叶皮质) -浅绿色标题 | 浅绿色消息 - -4. 心情(Mood) -品红色标题 | 品红色消息 - -5. 工具使用(Tool) -品红色标题 | 品红色消息 - -6. 关系(Relation) -浅品红色标题 | 浅品红色消息 - -7. 配置(Config) -浅青色标题 | 浅青色消息 - -8. 麦麦大脑袋 -浅绿色标题 | 浅绿色消息 - -9. 在干嘛 -青色标题 | 青色消息 - -10. 麦麦组织语言 -浅绿色标题 | 浅绿色消息 - -11. 见闻(Chat) -浅蓝色标题 | 绿色消息 - -12. 表情包(Emoji) -橙色标题 | 橙色消息 fg #FFD700 - -13. 子心流 - -13. 其他模块 -模块名标题 | 对应颜色消息 - - -注意: -1. 级别颜色遵循loguru默认配置 -2. 可通过环境变量修改日志级别 -""" - # 加载 .env 文件 env_path = Path(__file__).resolve().parent.parent.parent / ".env" From f9c28217f7d58c61c0170a45e64c0dad05860eec Mon Sep 17 00:00:00 2001 From: Plutor-05 Date: Thu, 8 May 2025 19:11:35 +0800 Subject: [PATCH 51/63] =?UTF-8?q?feat(PFC):=20=E9=87=8D=E6=9E=84=E9=97=B2?= =?UTF-8?q?=E7=BD=AE=E5=AF=B9=E8=AF=9D=E6=A8=A1=E5=9D=97=EF=BC=8C=E8=BF=81?= =?UTF-8?q?=E7=A7=BBidle=5Fconversation=5Fstarter=E5=88=B0PFC=5Fidle?= =?UTF-8?q?=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + src/plugins/PFC/PFC_idle/__init__.py | 20 + src/plugins/PFC/PFC_idle/idle_chat.py | 550 ++++++++++++++++++ src/plugins/PFC/PFC_idle/idle_chat_manager.py | 182 ++++++ src/plugins/PFC/PFC_idle/idle_conversation.py | 506 ++++++++++++++++ .../idle_conversation_starter.py | 54 +- src/plugins/PFC/actions.py | 16 +- src/plugins/PFC/chat_observer.py | 13 + src/plugins/PFC/conversation.py | 10 +- src/plugins/PFC/conversation_initializer.py | 22 +- src/plugins/PFC/conversation_loop.py | 19 +- 11 files changed, 1346 insertions(+), 48 deletions(-) create mode 100644 src/plugins/PFC/PFC_idle/__init__.py create mode 100644 src/plugins/PFC/PFC_idle/idle_chat.py create mode 100644 src/plugins/PFC/PFC_idle/idle_chat_manager.py create mode 100644 src/plugins/PFC/PFC_idle/idle_conversation.py rename src/plugins/PFC/{ => PFC_idle}/idle_conversation_starter.py (90%) diff --git a/.gitignore b/.gitignore index 9e1b9681..67565521 100644 --- a/.gitignore +++ b/.gitignore @@ -300,3 +300,5 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk +__pycache__/ +*.pyc diff --git a/src/plugins/PFC/PFC_idle/__init__.py b/src/plugins/PFC/PFC_idle/__init__.py new file mode 100644 index 00000000..a06c1834 --- /dev/null +++ b/src/plugins/PFC/PFC_idle/__init__.py @@ -0,0 +1,20 @@ +""" +PFC_idle 包 - 用于空闲时主动聊天的功能模块 + +该包包含以下主要组件: +- IdleChat: 根据关系和活跃度进行智能主动聊天 +- IdleChatManager: 管理多个聊天实例的空闲状态 +- IdleConversation: 处理与空闲聊天相关的功能,与主Conversation类解耦 +""" + +from .idle_chat import IdleChat +from .idle_chat_manager import IdleChatManager +from .idle_conversation import IdleConversation, get_idle_conversation_instance, initialize_idle_conversation + +__all__ = [ + 'IdleChat', + 'IdleChatManager', + 'IdleConversation', + 'get_idle_conversation_instance', + 'initialize_idle_conversation' +] \ No newline at end of file diff --git a/src/plugins/PFC/PFC_idle/idle_chat.py b/src/plugins/PFC/PFC_idle/idle_chat.py new file mode 100644 index 00000000..38c9e772 --- /dev/null +++ b/src/plugins/PFC/PFC_idle/idle_chat.py @@ -0,0 +1,550 @@ +from typing import Optional, Dict, List, Set +import asyncio +import time +import random +import traceback +from datetime import datetime +from src.common.logger_manager import get_logger +from src.config.config import global_config +from src.plugins.models.utils_model import LLMRequest +from src.plugins.utils.prompt_builder import Prompt, global_prompt_manager +from src.plugins.person_info.person_info import person_info_manager +from src.plugins.utils.chat_message_builder import build_readable_messages +from ...schedule.schedule_generator import bot_schedule +from ....config.config import global_config +from ..chat_observer import ChatObserver +from ..message_sender import DirectMessageSender +from src.plugins.chat.chat_stream import ChatStream +from maim_message import UserInfo +from ..pfc_relationship import PfcRepationshipTranslator +from rich.traceback import install + +install(extra_lines=3) + +logger = get_logger("pfc_idle_chat") + +class IdleChat: + """主动聊天组件(测试中) + + 在以下条件都满足时触发主动聊天: + 1. 当前没有任何活跃的对话实例 + 2. 在指定的活动时间内(7:00-23:00) + 3. 根据关系值动态调整触发概率 + 4. 上次触发后已经过了足够的冷却时间 + """ + + # 单例模式实现 + _instances: Dict[str, 'IdleChat'] = {} + + # 全局共享状态,用于跟踪未回复的用户 + _pending_replies: Dict[str, float] = {} # 用户名 -> 发送时间 + _tried_users: Set[str] = set() # 已尝试过的用户集合 + _global_lock = asyncio.Lock() # 保护共享状态的全局锁 + + @classmethod + def get_instance(cls, stream_id: str, private_name: str) -> 'IdleChat': + """获取IdleChat实例(单例模式) + + Args: + stream_id: 聊天流ID + private_name: 私聊用户名称 + + Returns: + IdleChat: IdleChat实例 + """ + key = f"{private_name}:{stream_id}" + if key not in cls._instances: + cls._instances[key] = cls(stream_id, private_name) + # 创建实例时自动启动检测 + cls._instances[key].start() + logger.info(f"[私聊][{private_name}]创建新的IdleChat实例并启动") + return cls._instances[key] + + @classmethod + async def register_user_response(cls, private_name: str) -> None: + """注册用户已回复 + + 当用户回复消息时调用此方法,将用户从待回复列表中移除 + + Args: + private_name: 私聊用户名称 + """ + async with cls._global_lock: + if private_name in cls._pending_replies: + del cls._pending_replies[private_name] + logger.info(f"[私聊][{private_name}]已回复主动聊天消息,从待回复列表中移除") + + @classmethod + async def get_next_available_user(cls) -> Optional[str]: + """获取下一个可用于主动聊天的用户 + + 优先选择未尝试过的用户,其次是已尝试但超时未回复的用户 + + Returns: + Optional[str]: 下一个可用的用户名,如果没有则返回None + """ + async with cls._global_lock: + current_time = time.time() + timeout_threshold = 7200 # 2小时未回复视为超时 + + # 清理超时未回复的用户 + for user, send_time in list(cls._pending_replies.items()): + if current_time - send_time > timeout_threshold: + logger.info(f"[私聊][{user}]超过{timeout_threshold}秒未回复,标记为超时") + del cls._pending_replies[user] + + # 获取所有实例中的用户 + all_users = set() + for key in cls._instances: + user = key.split(':', 1)[0] + all_users.add(user) + + # 优先选择未尝试过的用户 + untried_users = all_users - cls._tried_users + if untried_users: + next_user = random.choice(list(untried_users)) + cls._tried_users.add(next_user) + return next_user + + # 如果所有用户都已尝试过,重置尝试集合,从头开始 + if len(cls._tried_users) >= len(all_users): + cls._tried_users.clear() + logger.info(f"[私聊]所有用户都已尝试过,重置尝试列表") + # 随机选择一个不在待回复列表中的用户 + available_users = all_users - set(cls._pending_replies.keys()) + if available_users: + next_user = random.choice(list(available_users)) + cls._tried_users.add(next_user) + return next_user + + return None + + def __init__(self, stream_id: str, private_name: str): + """初始化主动聊天组件 + + Args: + stream_id: 聊天流ID + private_name: 私聊用户名称 + """ + self.stream_id = stream_id + self.private_name = private_name + self.chat_observer = ChatObserver.get_instance(stream_id, private_name) + self.message_sender = DirectMessageSender(private_name) + + # 添加异步锁,保护对共享变量的访问 + self._lock: asyncio.Lock = asyncio.Lock() + + # LLM请求对象,用于生成主动对话内容 + self.llm = LLMRequest( + model=global_config.llm_normal, + temperature=0.5, + max_tokens=500, + request_type="idle_chat" + ) + + # 工作状态 + self.active_instances_count: int = 0 + self.last_trigger_time: float = time.time() - 1500 # 初始化时减少等待时间 + self._running: bool = False + self._task: Optional[asyncio.Task] = None + + # 配置参数 - 从global_config加载 + self.min_cooldown = getattr(global_config,"MIN_IDLE_TIME", 7200) # 最短冷却时间(默认2小时)建议修改长一点,你也不希望你的bot一直骚扰你吧 + self.max_cooldown = getattr(global_config, "MAX_IDLE_TIME", 14400) # 最长冷却时间(默认4小时) + self.min_idle_time = getattr(global_config, "MIN_IDLE_TIME", 3600) + self.check_interval = getattr(global_config, "IDLE_CHECK_INTERVAL", 600) # 检查间隔(默认10分钟) + self.active_hours_start = 6 # 活动开始时间 + self.active_hours_end = 24 # 活动结束时间 + + # 关系值相关 + self.base_trigger_probability = 0.3 # 基础触发概率 + self.relationship_factor = 0.0003 # 关系值影响因子 + + def start(self) -> None: + """启动主动聊天检测""" + # 检查是否启用了主动聊天功能 + if not getattr(global_config, "ENABLE_IDLE_CONVERSATION", False): + logger.info(f"[私聊][{self.private_name}]主动聊天功能已禁用(配置ENABLE_IDLE_CONVERSATION=False)") + return + + if self._running: + logger.debug(f"[私聊][{self.private_name}]主动聊天功能已在运行中") + return + + self._running = True + self._task = asyncio.create_task(self._check_idle_loop()) + logger.info(f"[私聊][{self.private_name}]启动主动聊天检测") + + def stop(self) -> None: + """停止主动聊天检测 + """ + if not self._running: + return + + self._running = False + if self._task: + self._task.cancel() + self._task = None + logger.info(f"[私聊][{self.private_name}]停止主动聊天检测") + + async def increment_active_instances(self) -> None: + """增加活跃实例计数 + + 当创建新的对话实例时调用此方法 + """ + async with self._lock: + self.active_instances_count += 1 + logger.debug(f"[私聊][{self.private_name}]活跃实例数+1,当前:{self.active_instances_count}") + + async def decrement_active_instances(self) -> None: + """减少活跃实例计数 + + 当对话实例结束时调用此方法 + """ + async with self._lock: + self.active_instances_count = max(0, self.active_instances_count - 1) + logger.debug(f"[私聊][{self.private_name}]活跃实例数-1,当前:{self.active_instances_count}") + + async def update_last_message_time(self, message_time: Optional[float] = None) -> None: + """更新最后一条消息的时间 + + Args: + message_time: 消息时间戳,如果为None则使用当前时间 + """ + async with self._lock: + self.last_trigger_time = message_time or time.time() + logger.debug(f"[私聊][{self.private_name}]更新最后消息时间: {self.last_trigger_time:.2f}") + + # 当用户发送消息时,也应该注册响应 + await self.__class__.register_user_response(self.private_name) + + def _is_active_hours(self) -> bool: + """检查是否在活动时间内""" + current_hour = datetime.now().hour + return self.active_hours_start <= current_hour < self.active_hours_end + + async def _should_trigger(self) -> bool: + """检查是否应该触发主动聊天""" + async with self._lock: + # 确保计数不会出错,重置为0如果发现是负数 + if self.active_instances_count < 0: + logger.warning(f"[私聊][{self.private_name}]检测到活跃实例数为负数,重置为0") + self.active_instances_count = 0 + + # 检查是否有活跃实例 + if self.active_instances_count > 0: + logger.debug(f"[私聊][{self.private_name}]存在活跃实例({self.active_instances_count}),不触发主动聊天") + return False + + # 检查是否在活动时间内 + if not self._is_active_hours(): + logger.debug(f"[私聊][{self.private_name}]不在活动时间内,不触发主动聊天") + return False + + # 检查冷却时间 + current_time = time.time() + time_since_last_trigger = current_time - self.last_trigger_time + if time_since_last_trigger < self.min_cooldown: + time_left = self.min_cooldown - time_since_last_trigger + logger.debug(f"[私聊][{self.private_name}]冷却时间未到(已过{time_since_last_trigger:.0f}秒/需要{self.min_cooldown}秒),还需等待{time_left:.0f}秒,不触发主动聊天") + return False + + # 强制触发检查 - 如果超过最大冷却时间,增加触发概率 + force_trigger = False + if time_since_last_trigger > self.max_cooldown * 2: # 如果超过最大冷却时间的两倍 + force_probability = min(0.6, self.base_trigger_probability * 2) # 增加概率但不超过0.6 + random_force = random.random() + force_trigger = random_force < force_probability + if force_trigger: + logger.info(f"[私聊][{self.private_name}]超过最大冷却时间({time_since_last_trigger:.0f}秒),强制触发主动聊天") + return True + + # 获取关系值 + relationship_value = 0 + try: + # 导入relationship_manager以使用ensure_float方法 + from src.plugins.person_info.relationship_manager import relationship_manager + + # 尝试获取person_id + person_id = None + try: + # 先尝试通过昵称获取person_id + platform = "qq" # 默认平台 + person_id = person_info_manager.get_person_id(platform, self.private_name) + + # 如果通过昵称获取失败,尝试通过stream_id解析 + if not person_id: + parts = self.stream_id.split('_') + if len(parts) >= 2 and parts[0] == "private": + user_id = parts[1] + platform = parts[2] if len(parts) >= 3 else "qq" + try: + person_id = person_info_manager.get_person_id(platform, int(user_id)) + except ValueError: + # 如果user_id不是整数,尝试作为字符串使用 + person_id = person_info_manager.get_person_id(platform, user_id) + except Exception as e2: + logger.warning(f"[私聊][{self.private_name}]尝试获取person_id失败: {str(e2)}") + + # 获取关系值 + if person_id: + raw_value = await person_info_manager.get_value(person_id, "relationship_value") + relationship_value = relationship_manager.ensure_float(raw_value, person_id) + logger.debug(f"[私聊][{self.private_name}]成功获取关系值: {relationship_value}") + else: + logger.warning(f"[私聊][{self.private_name}]无法获取person_id,使用默认关系值0") + + # 使用PfcRepationshipTranslator获取关系描述 + relationship_translator = PfcRepationshipTranslator(self.private_name) + relationship_level = relationship_translator._calculate_relationship_level_num(relationship_value, self.private_name) + + # 基于关系等级调整触发概率 + # 关系越好,主动聊天概率越高 + level_probability_factors = [0.05, 0.1, 0.2, 0.3, 0.4, 0.5] # 每个等级对应的基础概率因子 + base_probability = level_probability_factors[relationship_level] + + # 基础概率因子 + trigger_probability = base_probability + trigger_probability = max(0.05, min(0.6, trigger_probability)) # 限制在0.05-0.6之间 + + # 最大冷却时间调整 - 随着冷却时间增加,逐渐增加触发概率 + if time_since_last_trigger > self.max_cooldown: + # 计算额外概率 - 每超过最大冷却时间的10%,增加1%的概率,最多增加30% + extra_time_factor = min(0.3, (time_since_last_trigger - self.max_cooldown) / (self.max_cooldown * 10)) + trigger_probability += extra_time_factor + logger.debug(f"[私聊][{self.private_name}]超过标准冷却时间,额外增加概率: +{extra_time_factor:.2f}") + + # 随机判断是否触发 + random_value = random.random() + should_trigger = random_value < trigger_probability + logger.debug(f"[私聊][{self.private_name}]触发概率计算: 基础({base_probability:.2f}) + 关系值({relationship_value})影响 = {trigger_probability:.2f},随机值={random_value:.2f}, 结果={should_trigger}") + + # 如果决定触发,记录详细日志 + if should_trigger: + logger.info(f"[私聊][{self.private_name}]决定触发主动聊天: 触发概率={trigger_probability:.2f}, 距上次已过{time_since_last_trigger:.0f}秒") + + return should_trigger + + except Exception as e: + logger.error(f"[私聊][{self.private_name}]获取关系值失败: {str(e)}") + logger.error(traceback.format_exc()) + + # 即使获取关系值失败,仍有一个基础的几率触发 + # 这确保即使数据库有问题,主动聊天功能仍然可用 + base_fallback_probability = 0.1 # 较低的基础几率 + random_fallback = random.random() + fallback_trigger = random_fallback < base_fallback_probability + if fallback_trigger: + logger.info(f"[私聊][{self.private_name}]获取关系值失败,使用后备触发机制: 概率={base_fallback_probability:.2f}, 决定={fallback_trigger}") + return fallback_trigger + + async def _check_idle_loop(self) -> None: + """检查空闲状态的循环""" + try: + while self._running: + # 检查是否启用了主动聊天功能 + if not getattr(global_config, "ENABLE_IDLE_CONVERSATION", False): + # 如果禁用了功能,等待一段时间后再次检查配置 + await asyncio.sleep(60) # 每分钟检查一次配置变更 + continue + + # 检查当前用户是否应该触发主动聊天 + should_trigger = await self._should_trigger() + + # 如果当前用户不触发,检查是否有其他用户已经超时未回复 + if not should_trigger: + async with self.__class__._global_lock: + current_time = time.time() + pending_timeout = 1800 # 30分钟未回复检查 + + # 检查此用户是否在等待回复列表中 + if self.private_name in self.__class__._pending_replies: + logger.debug(f"[私聊][{self.private_name}]当前用户在等待回复列表中,不进行额外检查") + else: + # 查找所有超过30分钟未回复的用户 + timed_out_users = [] + for user, send_time in self.__class__._pending_replies.items(): + if current_time - send_time > pending_timeout: + timed_out_users.append(user) + + # 如果有超时未回复的用户,尝试找下一个用户 + if timed_out_users: + logger.info(f"[私聊]发现{len(timed_out_users)}个用户超过{pending_timeout}秒未回复") + next_user = await self.__class__.get_next_available_user() + + if next_user and next_user != self.private_name: + logger.info(f"[私聊]选择下一个用户[{next_user}]进行主动聊天") + # 查找该用户的实例并触发聊天 + for key, instance in self.__class__._instances.items(): + if key.startswith(f"{next_user}:"): + logger.info(f"[私聊]为用户[{next_user}]触发主动聊天") + # 触发该实例的主动聊天 + asyncio.create_task(instance._initiate_chat()) + break + + # 如果当前用户应该触发主动聊天 + if should_trigger: + try: + await self._initiate_chat() + # 更新上次触发时间 + async with self._lock: + self.last_trigger_time = time.time() + + # 将此用户添加到等待回复列表中 + async with self.__class__._global_lock: + self.__class__._pending_replies[self.private_name] = time.time() + self.__class__._tried_users.add(self.private_name) + logger.info(f"[私聊][{self.private_name}]已添加到等待回复列表中") + except Exception as e: + logger.error(f"[私聊][{self.private_name}]执行主动聊天过程出错: {str(e)}") + logger.error(traceback.format_exc()) + + # 等待下一次检查 + check_interval = self.check_interval # 使用配置的检查间隔 + logger.debug(f"[私聊][{self.private_name}]等待{check_interval}秒后进行下一次主动聊天检查") + await asyncio.sleep(check_interval) + + except asyncio.CancelledError: + logger.debug(f"[私聊][{self.private_name}]主动聊天检测任务被取消") + except Exception as e: + logger.error(f"[私聊][{self.private_name}]主动聊天检测出错: {str(e)}") + logger.error(traceback.format_exc()) + # 尝试重新启动检测循环 + if self._running: + logger.info(f"[私聊][{self.private_name}]尝试重新启动主动聊天检测") + self._task = asyncio.create_task(self._check_idle_loop()) + + async def _get_chat_stream(self) -> Optional[ChatStream]: + """获取聊天流实例""" + try: + # 尝试从全局聊天管理器获取现有的聊天流 + from src.plugins.chat.chat_stream import chat_manager + existing_chat_stream = chat_manager.get_stream(self.stream_id) + if existing_chat_stream: + logger.debug(f"[私聊][{self.private_name}]从chat_manager找到现有聊天流") + return existing_chat_stream + + # 如果没有现有聊天流,则创建新的 + logger.debug(f"[私聊][{self.private_name}]未找到现有聊天流,创建新聊天流") + # 创建用户信息对象 + user_info = UserInfo( + user_id=self.private_name, # 使用私聊用户的ID + user_nickname=self.private_name, # 使用私聊用户的名称 + platform="qq" + ) + # 创建聊天流 + new_stream = ChatStream(self.stream_id, "qq", user_info) + # 将新创建的聊天流添加到管理器中 + chat_manager.register_stream(new_stream) + logger.debug(f"[私聊][{self.private_name}]成功创建并注册新聊天流") + return new_stream + except Exception as e: + logger.error(f"[私聊][{self.private_name}]创建/获取聊天流失败: {str(e)}") + logger.error(traceback.format_exc()) + return None + + async def _initiate_chat(self) -> None: + """生成并发送主动聊天消息""" + try: + # 获取聊天历史记录 + messages = self.chat_observer.get_cached_messages(limit=12) + chat_history_text = await build_readable_messages( + messages, + replace_bot_name=True, + merge_messages=False, + timestamp_mode="relative", + read_mark=0.0 + ) + + # 获取关系信息 + from src.plugins.person_info.relationship_manager import relationship_manager + + # 获取关系值 + relationship_value = 0 + try: + platform = "qq" + person_id = person_info_manager.get_person_id(platform, self.private_name) + if person_id: + raw_value = await person_info_manager.get_value(person_id, "relationship_value") + relationship_value = relationship_manager.ensure_float(raw_value, person_id) + except Exception as e: + logger.warning(f"[私聊][{self.private_name}]获取关系值失败,使用默认值: {e}") + + # 使用PfcRepationshipTranslator获取关系描述 + relationship_translator = PfcRepationshipTranslator(self.private_name) + full_relationship_text = await relationship_translator.translate_relationship_value_to_text(relationship_value) + + # 提取纯关系描述(去掉"你们的关系是:"前缀) + relationship_description = "普通" # 默认值 + if ":" in full_relationship_text: + relationship_description = full_relationship_text.split(":")[1].replace("。", "") + + if global_config.ENABLE_SCHEDULE_GEN: + schedule_prompt = await global_prompt_manager.format_prompt( + "schedule_prompt", schedule_info=bot_schedule.get_current_num_task(num=1, time_info=False) + ) + else: + schedule_prompt = "" + + # 构建提示词 + current_time = datetime.now().strftime("%H:%M") + prompt = f"""你是{global_config.BOT_NICKNAME}。 + 你正在与用户{self.private_name}进行QQ私聊,你们的关系是{relationship_description} + 现在时间{current_time} + 这是你的日程{schedule_prompt} + 你想要主动发起对话。 + 请基于以下之前的对话历史,生成一条自然、友好、符合关系程度的主动对话消息。 + 这条消息应能够引起用户的兴趣,重新开始对话。 + 最近的对话历史(并不是现在的对话): + {chat_history_text} + 请你严格根据对话历史决定是告诉对方你正在做的事情,还是询问对方正在做的事情 + 请直接输出一条消息,不要有任何额外的解释或引导文字 + 消息内容尽量简短 + """ + + # 生成回复 + logger.debug(f"[私聊][{self.private_name}]开始生成主动聊天内容") + try: + content, _ = await asyncio.wait_for( + self.llm.generate_response_async(prompt), + timeout=30 + ) + logger.debug(f"[私聊][{self.private_name}]成功生成主动聊天内容: {content}") + except asyncio.TimeoutError: + logger.error(f"[私聊][{self.private_name}]生成主动聊天内容超时") + return + except Exception as llm_err: + logger.error(f"[私聊][{self.private_name}]生成主动聊天内容失败: {str(llm_err)}") + logger.error(traceback.format_exc()) + return + + # 清理结果 + content = content.strip() + content = content.strip("\"'") + + if not content: + logger.error(f"[私聊][{self.private_name}]生成的主动聊天内容为空") + return + + # 获取聊天流 + chat_stream = await self._get_chat_stream() + if not chat_stream: + logger.error(f"[私聊][{self.private_name}]无法获取有效的聊天流,取消发送主动消息") + return + + # 发送消息 + try: + logger.debug(f"[私聊][{self.private_name}]准备发送主动聊天消息: {content}") + await self.message_sender.send_message( + chat_stream=chat_stream, + content=content, + reply_to_message=None + ) + logger.info(f"[私聊][{self.private_name}]成功主动发起聊天: {content}") + except Exception as e: + logger.error(f"[私聊][{self.private_name}]发送主动聊天消息失败: {str(e)}") + logger.error(traceback.format_exc()) + + except Exception as e: + logger.error(f"[私聊][{self.private_name}]主动发起聊天过程中发生未预期的错误: {str(e)}") + logger.error(traceback.format_exc()) \ No newline at end of file diff --git a/src/plugins/PFC/PFC_idle/idle_chat_manager.py b/src/plugins/PFC/PFC_idle/idle_chat_manager.py new file mode 100644 index 00000000..66ec37b6 --- /dev/null +++ b/src/plugins/PFC/PFC_idle/idle_chat_manager.py @@ -0,0 +1,182 @@ +from typing import Dict, Optional +import asyncio +from src.common.logger_manager import get_logger +from .idle_chat import IdleChat +import traceback + +logger = get_logger("pfc_idle_chat_manager") + +class IdleChatManager: + """空闲聊天管理器 + + 用于管理所有私聊用户的空闲聊天实例。 + 采用单例模式,确保全局只有一个管理器实例。 + """ + + _instance: Optional["IdleChatManager"] = None + _lock: asyncio.Lock = asyncio.Lock() + + def __init__(self): + """初始化空闲聊天管理器""" + self._idle_chats: Dict[str, IdleChat] = {} # stream_id -> IdleChat + self._active_conversations_count: Dict[str, int] = {} # stream_id -> count + + @classmethod + def get_instance(cls) -> "IdleChatManager": + """获取管理器单例 (同步版本) + + Returns: + IdleChatManager: 管理器实例 + """ + if not cls._instance: + # 在同步环境中创建实例 + cls._instance = cls() + return cls._instance + + @classmethod + async def get_instance_async(cls) -> "IdleChatManager": + """获取管理器单例 (异步版本) + + Returns: + IdleChatManager: 管理器实例 + """ + if not cls._instance: + async with cls._lock: + if not cls._instance: + cls._instance = cls() + return cls._instance + + async def get_or_create_idle_chat(self, stream_id: str, private_name: str) -> IdleChat: + """获取或创建空闲聊天实例 + + Args: + stream_id: 聊天流ID + private_name: 私聊用户名称 + + Returns: + IdleChat: 空闲聊天实例 + """ + if stream_id not in self._idle_chats: + idle_chat = IdleChat(stream_id, private_name) + self._idle_chats[stream_id] = idle_chat + # 初始化活跃对话计数 + if stream_id not in self._active_conversations_count: + self._active_conversations_count[stream_id] = 0 + idle_chat.start() # 启动空闲检测 + logger.info(f"[私聊][{private_name}]创建并启动新的空闲聊天实例") + return self._idle_chats[stream_id] + + async def remove_idle_chat(self, stream_id: str) -> None: + """移除空闲聊天实例 + + Args: + stream_id: 聊天流ID + """ + if stream_id in self._idle_chats: + idle_chat = self._idle_chats[stream_id] + idle_chat.stop() # 停止空闲检测 + del self._idle_chats[stream_id] + if stream_id in self._active_conversations_count: + del self._active_conversations_count[stream_id] + logger.info(f"[私聊][{idle_chat.private_name}]移除空闲聊天实例") + + async def notify_conversation_start(self, stream_id: str) -> None: + """通知对话开始 + + Args: + stream_id: 聊天流ID + """ + try: + if stream_id not in self._idle_chats: + logger.warning(f"对话开始通知: {stream_id} 没有对应的IdleChat实例,将创建一个") + # 从stream_id尝试提取private_name + private_name = stream_id + if stream_id.startswith("private_"): + parts = stream_id.split("_") + if len(parts) >= 2: + private_name = parts[1] # 取第二部分作为名称 + await self.get_or_create_idle_chat(stream_id, private_name) + + if stream_id not in self._active_conversations_count: + self._active_conversations_count[stream_id] = 0 + + # 增加计数前记录当前值,用于日志 + old_count = self._active_conversations_count[stream_id] + self._active_conversations_count[stream_id] += 1 + new_count = self._active_conversations_count[stream_id] + + # 确保IdleChat实例存在 + idle_chat = self._idle_chats.get(stream_id) + if idle_chat: + await idle_chat.increment_active_instances() + logger.debug(f"对话开始通知: {stream_id}, 计数从{old_count}增加到{new_count}") + else: + logger.error(f"对话开始通知: {stream_id}, 计数增加但IdleChat不存在! 计数:{old_count}->{new_count}") + except Exception as e: + logger.error(f"对话开始通知处理失败: {stream_id}, 错误: {e}") + logger.error(traceback.format_exc()) + + async def notify_conversation_end(self, stream_id: str) -> None: + """通知对话结束 + + Args: + stream_id: 聊天流ID + """ + try: + # 记录当前计数用于日志 + old_count = self._active_conversations_count.get(stream_id, 0) + + # 安全减少计数,避免负数 + if stream_id in self._active_conversations_count and self._active_conversations_count[stream_id] > 0: + self._active_conversations_count[stream_id] -= 1 + else: + # 如果计数已经为0或不存在,设置为0 + self._active_conversations_count[stream_id] = 0 + + new_count = self._active_conversations_count.get(stream_id, 0) + + # 确保IdleChat实例存在 + idle_chat = self._idle_chats.get(stream_id) + if idle_chat: + await idle_chat.decrement_active_instances() + logger.debug(f"对话结束通知: {stream_id}, 计数从{old_count}减少到{new_count}") + else: + logger.warning(f"对话结束通知: {stream_id}, 计数减少但IdleChat不存在! 计数:{old_count}->{new_count}") + + # 检查是否所有对话都结束了,帮助调试 + all_counts = sum(self._active_conversations_count.values()) + if all_counts == 0: + logger.info(f"所有对话实例都已结束,当前总活跃计数为0") + except Exception as e: + logger.error(f"对话结束通知处理失败: {stream_id}, 错误: {e}") + logger.error(traceback.format_exc()) + + def get_idle_chat(self, stream_id: str) -> Optional[IdleChat]: + """获取空闲聊天实例 + + Args: + stream_id: 聊天流ID + + Returns: + Optional[IdleChat]: 空闲聊天实例,如果不存在则返回None + """ + return self._idle_chats.get(stream_id) + + def get_active_conversations_count(self, stream_id: str) -> int: + """获取指定流的活跃对话计数 + + Args: + stream_id: 聊天流ID + + Returns: + int: 活跃对话计数 + """ + return self._active_conversations_count.get(stream_id, 0) + + def get_all_active_conversations_count(self) -> int: + """获取所有活跃对话总计数 + + Returns: + int: 活跃对话总计数 + """ + return sum(self._active_conversations_count.values()) \ No newline at end of file diff --git a/src/plugins/PFC/PFC_idle/idle_conversation.py b/src/plugins/PFC/PFC_idle/idle_conversation.py new file mode 100644 index 00000000..3aae1853 --- /dev/null +++ b/src/plugins/PFC/PFC_idle/idle_conversation.py @@ -0,0 +1,506 @@ +import traceback +import logging +import asyncio +from typing import Optional, Dict +from src.common.logger_manager import get_logger +import time + +logger = get_logger("pfc_idle_conversation") + +class IdleConversation: + """ + 处理Idle聊天相关的功能,将这些功能从主Conversation类中分离出来, + 以减少代码量并方便维护。 + """ + + def __init__(self): + """初始化IdleConversation实例""" + self._idle_chat_manager = None + self._running = False + self._active_streams: Dict[str, bool] = {} # 跟踪活跃的流 + self._monitor_task = None # 用于后台监控的任务 + self._lock = asyncio.Lock() # 用于线程安全操作 + self._initialization_in_progress = False # 防止并发初始化 + + async def initialize(self): + """初始化Idle聊天管理器""" + # 防止并发初始化 + if self._initialization_in_progress: + logger.debug("IdleConversation正在初始化中,等待完成") + return False + + if self._idle_chat_manager is not None: + logger.debug("IdleConversation已初始化,无需重复操作") + return True + + # 标记开始初始化 + self._initialization_in_progress = True + + try: + # 从PFCManager获取IdleChatManager实例 + from ..pfc_manager import PFCManager + pfc_manager = PFCManager.get_instance() + self._idle_chat_manager = pfc_manager.get_idle_chat_manager() + logger.debug("IdleConversation初始化完成,已获取IdleChatManager实例") + return True + except Exception as e: + logger.error(f"初始化IdleConversation时出错: {e}") + logger.error(traceback.format_exc()) + return False + finally: + # 无论成功或失败,都清除初始化标志 + self._initialization_in_progress = False + + async def start(self): + """启动IdleConversation,创建后台监控任务""" + if self._running: + logger.debug("IdleConversation已经在运行") + return False + + if not self._idle_chat_manager: + success = await self.initialize() + if not success: + logger.error("无法启动IdleConversation:初始化失败") + return False + + try: + self._running = True + # 创建后台监控任务,使用try-except块来捕获可能的异常 + try: + loop = asyncio.get_running_loop() + if loop.is_running(): + self._monitor_task = asyncio.create_task(self._monitor_loop()) + logger.info("IdleConversation启动成功,后台监控任务已创建") + else: + logger.warning("事件循环不活跃,跳过监控任务创建") + except RuntimeError: + # 如果没有活跃的事件循环,记录警告但继续执行 + logger.warning("没有活跃的事件循环,IdleConversation将不会启动监控任务") + # 尽管没有监控任务,但仍然将running设为True表示IdleConversation已启动 + + return True + except Exception as e: + self._running = False + logger.error(f"启动IdleConversation失败: {e}") + logger.error(traceback.format_exc()) + return False + + async def stop(self): + """停止IdleConversation的后台任务""" + if not self._running: + return + + self._running = False + if self._monitor_task and not self._monitor_task.done(): + try: + self._monitor_task.cancel() + try: + await asyncio.wait_for(self._monitor_task, timeout=2.0) + except asyncio.TimeoutError: + logger.warning("停止IdleConversation监控任务超时") + except asyncio.CancelledError: + pass # 正常取消 + except Exception as e: + logger.error(f"停止IdleConversation监控任务时出错: {e}") + logger.error(traceback.format_exc()) + + self._monitor_task = None + logger.info("IdleConversation已停止") + + async def _monitor_loop(self): + """后台监控循环,定期检查活跃的会话并执行必要的操作""" + try: + while self._running: + try: + # 同步活跃流计数到IdleChatManager + if self._idle_chat_manager: + await self._sync_active_streams_to_manager() + + # 这里可以添加定期检查逻辑,如查询空闲状态等 + active_count = len(self._active_streams) + logger.debug(f"IdleConversation监控中,当前活跃流数量: {active_count}") + + except Exception as e: + logger.error(f"IdleConversation监控循环出错: {e}") + logger.error(traceback.format_exc()) + + # 每30秒执行一次监控 + await asyncio.sleep(30) + except asyncio.CancelledError: + logger.info("IdleConversation监控任务已取消") + except Exception as e: + logger.error(f"IdleConversation监控任务异常退出: {e}") + logger.error(traceback.format_exc()) + self._running = False + + async def _sync_active_streams_to_manager(self): + """同步活跃流计数到IdleChatManager和IdleChat""" + try: + if not self._idle_chat_manager: + return + + # 获取当前的活跃流列表 + async with self._lock: + active_streams = list(self._active_streams.keys()) + + # 对每个活跃流,确保IdleChatManager和IdleChat中的计数是正确的 + for stream_id in active_streams: + # 获取当前IdleChatManager中的计数 + manager_count = self._idle_chat_manager.get_active_conversations_count(stream_id) + + # 由于我们的活跃流字典只记录是否活跃(值为True),所以计数应该是1 + if manager_count != 1: + # 修正IdleChatManager中的计数 + old_count = manager_count + self._idle_chat_manager._active_conversations_count[stream_id] = 1 + logger.warning(f"同步调整IdleChatManager中的计数: stream_id={stream_id}, {old_count}->1") + + # 同时修正IdleChat中的计数 + idle_chat = self._idle_chat_manager.get_idle_chat(stream_id) + if idle_chat: + if getattr(idle_chat, "active_instances_count", 0) != 1: + old_count = getattr(idle_chat, "active_instances_count", 0) + idle_chat.active_instances_count = 1 + logger.warning(f"同步调整IdleChat中的计数: stream_id={stream_id}, {old_count}->1") + + # 检查IdleChatManager中有没有多余的计数(conversation中已不存在但manager中还有) + for stream_id, count in list(self._idle_chat_manager._active_conversations_count.items()): + if count > 0 and stream_id not in active_streams: + # 重置为0 + self._idle_chat_manager._active_conversations_count[stream_id] = 0 + logger.warning(f"重置IdleChatManager中的多余计数: stream_id={stream_id}, {count}->0") + + # 同时修正IdleChat中的计数 + idle_chat = self._idle_chat_manager.get_idle_chat(stream_id) + if idle_chat and getattr(idle_chat, "active_instances_count", 0) > 0: + old_count = getattr(idle_chat, "active_instances_count", 0) + idle_chat.active_instances_count = 0 + logger.warning(f"同步重置IdleChat中的计数: stream_id={stream_id}, {old_count}->0") + + # 日志记录同步结果 + total_active = len(active_streams) + total_manager = sum(self._idle_chat_manager._active_conversations_count.values()) + logger.debug(f"同步后的计数: IdleConversation活跃流={total_active}, IdleChatManager总计数={total_manager}") + + except Exception as e: + logger.error(f"同步活跃流计数失败: {e}") + logger.error(traceback.format_exc()) + + async def get_or_create_idle_chat(self, stream_id: str, private_name: str): + """ + 获取或创建IdleChat实例 + + Args: + stream_id: 聊天流ID + private_name: 私聊对象名称,用于日志 + + Returns: + bool: 操作是否成功 + """ + # 确保IdleConversation已启动 + if not self._running: + await self.start() + + if not self._idle_chat_manager: + # 如果尚未初始化,尝试初始化 + success = await self.initialize() + if not success: + logger.warning(f"[私聊][{private_name}] 获取或创建IdleChat失败:IdleChatManager未初始化") + return False + + try: + # 创建IdleChat实例 + idle_chat = await self._idle_chat_manager.get_or_create_idle_chat(stream_id, private_name) + logger.debug(f"[私聊][{private_name}] 已创建或获取IdleChat实例") + return True + except Exception as e: + logger.warning(f"[私聊][{private_name}] 创建或获取IdleChat实例失败: {e}") + logger.warning(traceback.format_exc()) + return False + + async def notify_conversation_start(self, stream_id: str, private_name: str) -> bool: + """ + 通知空闲聊天管理器对话开始 + + Args: + stream_id: 聊天流ID + private_name: 私聊对象名称,用于日志 + + Returns: + bool: 通知是否成功 + """ + try: + # 确保IdleConversation已启动 + if not self._running: + success = await self.start() + if not success: + logger.warning(f"[私聊][{private_name}] 启动IdleConversation失败,无法通知对话开始") + return False + + if not self._idle_chat_manager: + # 如果尚未初始化,尝试初始化 + success = await self.initialize() + if not success: + logger.warning(f"[私聊][{private_name}] 通知对话开始失败:IdleChatManager未初始化") + return False + + try: + # 确保IdleChat实例已创建 - 这是关键步骤,要先创建IdleChat + await self.get_or_create_idle_chat(stream_id, private_name) + + # 先记录活跃状态 - 这是权威源 + async with self._lock: + self._active_streams[stream_id] = True + + # 然后同步到IdleChatManager + if self._idle_chat_manager: + await self._idle_chat_manager.notify_conversation_start(stream_id) + logger.info(f"[私聊][{private_name}] 已通知空闲聊天管理器对话开始") + else: + logger.warning(f"[私聊][{private_name}] IdleChatManager不存在,但已记录活跃状态") + + # 立即进行一次同步,确保数据一致性 + await self._sync_active_streams_to_manager() + + return True + except Exception as e: + logger.warning(f"[私聊][{private_name}] 通知空闲聊天管理器对话开始失败: {e}") + logger.warning(traceback.format_exc()) + # 即使通知失败,也应记录活跃状态 + async with self._lock: + self._active_streams[stream_id] = True + return False + except Exception as outer_e: + logger.error(f"[私聊][{private_name}] 处理对话开始通知时发生严重错误: {outer_e}") + logger.error(traceback.format_exc()) + return False + + async def notify_conversation_end(self, stream_id: str, private_name: str) -> bool: + """ + 通知空闲聊天管理器对话结束 + + Args: + stream_id: 聊天流ID + private_name: 私聊对象名称,用于日志 + + Returns: + bool: 通知是否成功 + """ + try: + # 先从自身的活跃流中移除 - 这是权威源 + was_active = False + async with self._lock: + if stream_id in self._active_streams: + del self._active_streams[stream_id] + was_active = True + logger.debug(f"[私聊][{private_name}] 已从活跃流中移除 {stream_id}") + + if not self._idle_chat_manager: + # 如果尚未初始化,尝试初始化 + success = await self.initialize() + if not success: + logger.warning(f"[私聊][{private_name}] 通知对话结束失败:IdleChatManager未初始化") + return False + + try: + # 然后同步到IdleChatManager + if self._idle_chat_manager: + # 无论如何都尝试通知 + await self._idle_chat_manager.notify_conversation_end(stream_id) + + # 立即进行一次同步,确保数据一致性 + await self._sync_active_streams_to_manager() + + logger.info(f"[私聊][{private_name}] 已通知空闲聊天管理器对话结束") + + # 检查当前活跃流数量 + active_count = len(self._active_streams) + if active_count == 0: + logger.info(f"[私聊][{private_name}] 当前无活跃流,可能会触发主动聊天") + + # 额外调用:如果实例存在且只有在确实移除了活跃流的情况下才触发检查 + if was_active: + idle_chat = self._idle_chat_manager.get_idle_chat(stream_id) + if idle_chat: + # 直接触发IdleChat检查,而不是等待下一个循环 + logger.info(f"[私聊][{private_name}] 对话结束,手动触发一次主动聊天检查") + asyncio.create_task(self._trigger_idle_chat_check(idle_chat, stream_id, private_name)) + + return True + else: + logger.warning(f"[私聊][{private_name}] IdleChatManager不存在,但已更新活跃状态") + return False + except Exception as e: + logger.warning(f"[私聊][{private_name}] 通知空闲聊天管理器对话结束失败: {e}") + logger.warning(traceback.format_exc()) + return False + except Exception as outer_e: + logger.error(f"[私聊][{private_name}] 处理对话结束通知时发生严重错误: {outer_e}") + logger.error(traceback.format_exc()) + return False + + async def _trigger_idle_chat_check(self, idle_chat, stream_id: str, private_name: str): + """在对话结束后,手动触发一次IdleChat的检查""" + try: + # 确保活跃计数与IdleConversation一致 + async with self._lock: + is_active_in_conversation = stream_id in self._active_streams + + # 强制使IdleChat的计数与IdleConversation一致 + if is_active_in_conversation: + # 如果在IdleConversation中是活跃的,IdleChat的计数应该是1 + if idle_chat.active_instances_count != 1: + old_count = idle_chat.active_instances_count + idle_chat.active_instances_count = 1 + logger.warning(f"[私聊][{private_name}] 修正IdleChat计数: {old_count}->1") + else: + # 如果在IdleConversation中不是活跃的,IdleChat的计数应该是0 + if idle_chat.active_instances_count != 0: + old_count = idle_chat.active_instances_count + idle_chat.active_instances_count = 0 + logger.warning(f"[私聊][{private_name}] 修正IdleChat计数: {old_count}->0") + + # 等待1秒,让任何正在进行的处理完成 + await asyncio.sleep(1) + + # 只有当stream不再活跃时才触发检查 + if not is_active_in_conversation: + # 尝试触发一次检查 + if hasattr(idle_chat, "_should_trigger"): + should_trigger = await idle_chat._should_trigger() + logger.info(f"[私聊][{private_name}] 手动触发主动聊天检查结果: {should_trigger}") + + # 如果应该触发,直接调用_initiate_chat + if should_trigger and hasattr(idle_chat, "_initiate_chat"): + logger.info(f"[私聊][{private_name}] 手动触发主动聊天") + await idle_chat._initiate_chat() + # 更新最后触发时间 + idle_chat.last_trigger_time = time.time() + else: + logger.warning(f"[私聊][{private_name}] IdleChat没有_should_trigger方法,无法触发检查") + except Exception as e: + logger.error(f"[私聊][{private_name}] 手动触发主动聊天检查时出错: {e}") + logger.error(traceback.format_exc()) + + def is_stream_active(self, stream_id: str) -> bool: + """检查指定的stream是否活跃""" + return stream_id in self._active_streams + + def get_active_streams_count(self) -> int: + """获取当前活跃的stream数量""" + return len(self._active_streams) + + @property + def is_running(self) -> bool: + """检查IdleConversation是否正在运行""" + return self._running + + @property + def idle_chat_manager(self): + """获取IdleChatManager实例""" + return self._idle_chat_manager + +# 创建单例实例 +_instance: Optional[IdleConversation] = None +_instance_lock = asyncio.Lock() +_initialization_in_progress = False # 防止并发初始化 + +async def initialize_idle_conversation() -> IdleConversation: + """初始化并启动IdleConversation单例实例""" + global _initialization_in_progress + + # 防止并发初始化 + if _initialization_in_progress: + logger.debug("IdleConversation全局初始化正在进行中,等待完成") + return get_idle_conversation_instance() + + # 标记正在初始化 + _initialization_in_progress = True + + try: + instance = get_idle_conversation_instance() + + # 如果实例已经在运行,避免重复初始化 + if getattr(instance, '_running', False): + logger.debug("IdleConversation已在运行状态,无需重新初始化") + _initialization_in_progress = False + return instance + + # 初始化实例 + success = await instance.initialize() + if not success: + logger.error("IdleConversation初始化失败") + _initialization_in_progress = False + return instance + + # 启动实例 + success = await instance.start() + if not success: + logger.error("IdleConversation启动失败") + else: + # 启动成功,进行初始检查 + logger.info("IdleConversation启动成功,执行初始化后检查") + # 这里可以添加一些启动后的检查,如果需要 + + # 创建一个异步任务,定期检查系统状态 + asyncio.create_task(periodic_system_check(instance)) + + return instance + except Exception as e: + logger.error(f"初始化并启动IdleConversation时出错: {e}") + logger.error(traceback.format_exc()) + # 重置标志,允许下次再试 + _initialization_in_progress = False + return get_idle_conversation_instance() # 返回实例,即使初始化失败 + finally: + # 清除初始化标志 + _initialization_in_progress = False + +async def periodic_system_check(instance: IdleConversation): + """定期检查系统状态,确保主动聊天功能正常工作""" + try: + # 等待10秒,让系统完全启动 + await asyncio.sleep(10) + + while getattr(instance, '_running', False): + try: + # 检查活跃流数量 + active_streams_count = len(getattr(instance, '_active_streams', {})) + + # 如果IdleChatManager存在,检查其中的活跃对话计数 + idle_chat_manager = getattr(instance, '_idle_chat_manager', None) + if idle_chat_manager and hasattr(idle_chat_manager, 'get_all_active_conversations_count'): + manager_count = idle_chat_manager.get_all_active_conversations_count() + + # 如果两者不一致,记录警告 + if active_streams_count != manager_count: + logger.warning(f"检测到计数不一致: IdleConversation记录的活跃流数量({active_streams_count}) 与 IdleChatManager记录的活跃对话数({manager_count})不匹配") + + # 如果IdleChatManager记录的计数为0但自己的记录不为0,进行修正 + if manager_count == 0 and active_streams_count > 0: + logger.warning(f"检测到可能的计数错误,尝试修正:清空IdleConversation的活跃流记录") + async with instance._lock: + instance._active_streams.clear() + + # 检查计数如果为0,帮助日志输出 + if active_streams_count == 0: + logger.debug("当前没有活跃的对话流,应该可以触发主动聊天") + + except Exception as check_err: + logger.error(f"执行系统检查时出错: {check_err}") + logger.error(traceback.format_exc()) + + # 每60秒检查一次 + await asyncio.sleep(60) + except asyncio.CancelledError: + logger.debug("系统检查任务被取消") + except Exception as e: + logger.error(f"系统检查任务异常退出: {e}") + logger.error(traceback.format_exc()) + +def get_idle_conversation_instance() -> IdleConversation: + """获取IdleConversation的单例实例""" + global _instance + if _instance is None: + _instance = IdleConversation() + return _instance \ No newline at end of file diff --git a/src/plugins/PFC/idle_conversation_starter.py b/src/plugins/PFC/PFC_idle/idle_conversation_starter.py similarity index 90% rename from src/plugins/PFC/idle_conversation_starter.py rename to src/plugins/PFC/PFC_idle/idle_conversation_starter.py index b161edd0..1ace9dbd 100644 --- a/src/plugins/PFC/idle_conversation_starter.py +++ b/src/plugins/PFC/PFC_idle/idle_conversation_starter.py @@ -1,22 +1,33 @@ -from typing import TYPE_CHECKING, Optional -import asyncio import time +import asyncio import random -from src.common.logger import get_module_logger -from ..models.utils_model import LLMRequest +import traceback +from typing import TYPE_CHECKING, Optional +from datetime import datetime + +from src.common.logger_manager import get_logger +from src.plugins.models.utils_model import LLMRequest from src.config.config import global_config -from .chat_observer import ChatObserver -from .message_sender import DirectMessageSender -from ..chat.chat_stream import ChatStream -from maim_message import UserInfo +from src.plugins.chat.chat_stream import chat_manager, ChatStream from src.individuality.individuality import Individuality from src.plugins.utils.chat_message_builder import build_readable_messages +from maim_message import UserInfo +from ..chat_observer import ChatObserver +from ..message_sender import DirectMessageSender + +# 导入富文本回溯,用于更好的错误展示 +from rich.traceback import install + +# 使用TYPE_CHECKING避免循环导入 if TYPE_CHECKING: - from .conversation import Conversation + from ..conversation import Conversation + from ..pfc_manager import PFCManager -logger = get_module_logger("pfc_idle") +install(extra_lines=3) +# 获取当前模块的日志记录器 +logger = get_logger("idle_conversation_starter") class IdleConversationStarter: """长时间无对话主动发起对话的组件 @@ -125,6 +136,7 @@ class IdleConversationStarter: except Exception as e: logger.error(f"[私聊][{self.private_name}]重新加载配置时出错: {str(e)}") + logger.error(traceback.format_exc()) async def _check_idle_loop(self) -> None: """检查空闲状态的循环 @@ -167,6 +179,7 @@ class IdleConversationStarter: logger.debug(f"[私聊][{self.private_name}]空闲对话检测任务被取消") except Exception as e: logger.error(f"[私聊][{self.private_name}]空闲对话检测出错: {str(e)}") + logger.error(traceback.format_exc()) # 尝试重新启动检测循环 if self._running: logger.info(f"[私聊][{self.private_name}]尝试重新启动空闲对话检测") @@ -175,7 +188,7 @@ class IdleConversationStarter: async def _initiate_conversation(self) -> None: """生成并发送主动对话内容 - 获取聊天历史记录,使用LLM生成合适的开场白,然后发送消息。 + 获取聊天历史记录,使用LLM生成合适的开场白(大概),然后发送消息。 """ try: # 获取聊天历史记录,用于生成更合适的开场白 @@ -212,6 +225,7 @@ class IdleConversationStarter: return except Exception as llm_err: logger.error(f"[私聊][{self.private_name}]生成主动对话内容失败: {str(llm_err)}") + logger.error(traceback.format_exc()) return # 清理结果 @@ -225,9 +239,10 @@ class IdleConversationStarter: # 统一错误处理,从这里开始所有操作都在同一个try-except块中 logger.debug(f"[私聊][{self.private_name}]成功生成主动对话内容: {content},准备发送") - from .pfc_manager import PFCManager - - # 获取当前实例 + # 在函数内部导入PFCManager,避免循环导入 + from ..pfc_manager import PFCManager + + # 获取当前实例 - 注意这是同步方法,不需要await pfc_manager = PFCManager.get_instance() # 结束当前对话实例(如果存在) @@ -239,6 +254,7 @@ class IdleConversationStarter: await pfc_manager.remove_conversation(self.stream_id) except Exception as e: logger.warning(f"[私聊][{self.private_name}]结束当前对话实例时出错: {str(e)},继续创建新实例") + logger.warning(traceback.format_exc()) # 创建新的对话实例 logger.info(f"[私聊][{self.private_name}]创建新的对话实例以发送主动消息") @@ -247,6 +263,7 @@ class IdleConversationStarter: new_conversation = await pfc_manager.get_or_create_conversation(self.stream_id, self.private_name) except Exception as e: logger.error(f"[私聊][{self.private_name}]创建新对话实例失败: {str(e)}") + logger.error(traceback.format_exc()) return # 确保新对话实例已初始化完成 @@ -269,14 +286,17 @@ class IdleConversationStarter: new_conversation.chat_observer.trigger_update() except Exception as e: logger.warning(f"[私聊][{self.private_name}]触发聊天观察者更新失败: {str(e)}") + logger.warning(traceback.format_exc()) - logger.success(f"[私聊][{self.private_name}]成功主动发起对话: {content}") + logger.info(f"[私聊][{self.private_name}]成功主动发起对话: {content}") except Exception as e: logger.error(f"[私聊][{self.private_name}]发送主动对话消息失败: {str(e)}") + logger.error(traceback.format_exc()) except Exception as e: # 顶级异常处理,确保任何未捕获的异常都不会导致整个进程崩溃 logger.error(f"[私聊][{self.private_name}]主动发起对话过程中发生未预期的错误: {str(e)}") + logger.error(traceback.format_exc()) async def _get_chat_stream(self, conversation: Optional["Conversation"] = None) -> Optional[ChatStream]: """获取可用的聊天流 @@ -313,8 +333,6 @@ class IdleConversationStarter: return conversation.chat_stream # 2. 尝试从聊天管理器获取 - from src.plugins.chat.chat_stream import chat_manager - try: logger.info(f"[私聊][{self.private_name}]尝试从chat_manager获取聊天流") chat_stream = chat_manager.get_stream(self.stream_id) @@ -322,6 +340,7 @@ class IdleConversationStarter: return chat_stream except Exception as e: logger.warning(f"[私聊][{self.private_name}]从chat_manager获取聊天流失败: {str(e)}") + logger.warning(traceback.format_exc()) # 3. 创建新的聊天流 try: @@ -332,4 +351,5 @@ class IdleConversationStarter: return ChatStream(self.stream_id, "qq", user_info) except Exception as e: logger.error(f"[私聊][{self.private_name}]创建新聊天流失败: {str(e)}") + logger.error(traceback.format_exc()) return None diff --git a/src/plugins/PFC/actions.py b/src/plugins/PFC/actions.py index ea72df82..7b15cdfb 100644 --- a/src/plugins/PFC/actions.py +++ b/src/plugins/PFC/actions.py @@ -98,7 +98,7 @@ async def handle_action( current_action_record = { "action": action, "plan_reason": reason, # 记录规划时的原因 - "status": "start", # 初始状态为“开始” + "status": "start", # 初始状态为"开始" "time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # 记录开始时间 "final_reason": None, # 最终结果的原因,将在 finally 中设置 } @@ -361,9 +361,10 @@ async def handle_action( observation_info.chat_history_str = "[构建聊天记录出错]" # --- 新增结束 --- - # 更新空闲对话启动器的时间 - if conversation_instance.idle_conversation_starter: - await conversation_instance.idle_conversation_starter.update_last_message_time(send_end_time) + # 更新 idle_conversation_starter 的最后消息时间 + # (避免在发送消息后很快触发主动聊天) + 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", []) @@ -505,9 +506,10 @@ async def handle_action( action_successful = True # 标记成功 # final_status 和 final_reason 会在 finally 中设置 logger.info(f"[私聊][{conversation_instance.private_name}] 成功发送告别语,即将停止对话实例。") - # 更新空闲计时器 - if conversation_instance.idle_conversation_starter: - await conversation_instance.idle_conversation_starter.update_last_message_time(send_end_time) + # 更新 idle_conversation_starter 的最后消息时间 + # (避免在发送消息后很快触发主动聊天) + 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() diff --git a/src/plugins/PFC/chat_observer.py b/src/plugins/PFC/chat_observer.py index c8fd280f..4fede3d2 100644 --- a/src/plugins/PFC/chat_observer.py +++ b/src/plugins/PFC/chat_observer.py @@ -114,6 +114,19 @@ class ChatObserver: logger.debug( f"[私聊][{self.private_name}] 消息已添加到 ChatObserver 缓存,当前缓存大小: {len(self.message_cache)}" ) + + # 检查是否用户发送的消息(而非机器人自己) + try: + from .PFC_idle.idle_chat import IdleChat + + # 获取消息的发送者 + user_info = message.get("user_info", {}) + if user_info and str(user_info.get("user_id")) != str(global_config.BOT_QQ): + # 用户发送了消息,通知IdleChat + asyncio.create_task(IdleChat.register_user_response(self.private_name)) + logger.debug(f"[私聊][{self.private_name}] 检测到用户消息,已通知IdleChat更新用户响应状态") + except Exception as e_idle: + logger.warning(f"[私聊][{self.private_name}] 通知IdleChat用户响应状态失败: {e_idle}") else: logger.warning(f"[私聊][{self.private_name}] 尝试向 message_cache 添加非字典类型消息: {type(message)}") diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 4a3c9e02..d74f5096 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -24,7 +24,7 @@ from .action_planner import ActionPlanner from .observation_info import ObservationInfo from .conversation_info import ConversationInfo from .reply_generator import ReplyGenerator -from .idle_conversation_starter import IdleConversationStarter +from .PFC_idle.idle_chat import IdleChat from .pfc_KnowledgeFetcher import KnowledgeFetcher from .waiter import Waiter from .reply_checker import ReplyChecker @@ -77,7 +77,7 @@ class Conversation: self.knowledge_fetcher: Optional[KnowledgeFetcher] = None self.waiter: Optional[Waiter] = None self.direct_sender: Optional[DirectMessageSender] = None - self.idle_conversation_starter: Optional[IdleConversationStarter] = None + self.idle_chat: Optional[IdleChat] = None self.chat_observer: Optional[ChatObserver] = None self.observation_info: Optional[ObservationInfo] = None self.conversation_info: Optional[ConversationInfo] = None @@ -142,8 +142,10 @@ class Conversation: logger.warning(f"[私聊][{self.private_name}] 跳过最终关系评估,实例未完全初始化或缺少组件。") # 停止其他组件 - if self.idle_conversation_starter: - self.idle_conversation_starter.stop() + if self.idle_chat: + # 减少活跃实例计数,而不是停止IdleChat + await self.idle_chat.decrement_active_instances() + logger.info(f"[私聊][{self.private_name}] 已减少IdleChat活跃实例计数") if self.observation_info and self.chat_observer: self.observation_info.unbind_from_chat_observer() if self.mood_mng and hasattr(self.mood_mng, "stop_mood_update") and self.mood_mng._running: # type: ignore diff --git a/src/plugins/PFC/conversation_initializer.py b/src/plugins/PFC/conversation_initializer.py index 472c1291..ec7e47a5 100644 --- a/src/plugins/PFC/conversation_initializer.py +++ b/src/plugins/PFC/conversation_initializer.py @@ -17,7 +17,7 @@ from .action_planner import ActionPlanner from .observation_info import ObservationInfo from .conversation_info import ConversationInfo from .reply_generator import ReplyGenerator -from .idle_conversation_starter import IdleConversationStarter +from .PFC_idle.idle_chat import IdleChat from .pfc_KnowledgeFetcher import KnowledgeFetcher # 修正大小写 from .waiter import Waiter from .pfc_utils import get_person_id @@ -93,18 +93,18 @@ async def load_initial_history(conversation_instance: "Conversation"): read_mark=0.0, # read_mark 可能需要根据实际情况调整 ) - # 更新 ChatObserver 和 IdleStarter 的时间戳 + # 更新 ChatObserver 和 IdleChat 的时间戳 if conversation_instance.chat_observer: # 更新观察者的最后消息时间,避免重复处理这些初始消息 conversation_instance.chat_observer.last_message_time = ( conversation_instance.observation_info.last_message_time ) if ( - conversation_instance.idle_conversation_starter + conversation_instance.idle_chat and conversation_instance.observation_info.last_message_time ): # 更新空闲计时器的起始时间 - await conversation_instance.idle_conversation_starter.update_last_message_time( + await conversation_instance.idle_chat.update_last_message_time( conversation_instance.observation_info.last_message_time ) @@ -192,10 +192,12 @@ async def initialize_core_components(conversation_instance: "Conversation"): ) raise ValueError(f"无法获取 stream_id {conversation_instance.stream_id} 的 ChatStream") - logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 IdleConversationStarter...") - conversation_instance.idle_conversation_starter = IdleConversationStarter( + logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 IdleChat...") + conversation_instance.idle_chat = IdleChat.get_instance( conversation_instance.stream_id, conversation_instance.private_name ) + await conversation_instance.idle_chat.increment_active_instances() + logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) IdleChat实例已获取并增加活跃计数") # 2. 初始化信息存储和观察组件 logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 获取 ChatObserver 实例...") @@ -249,10 +251,10 @@ async def initialize_core_components(conversation_instance: "Conversation"): if conversation_instance.chat_observer: # 确保存在 conversation_instance.chat_observer.start() - if conversation_instance.idle_conversation_starter: - logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 启动 IdleConversationStarter...") - conversation_instance.idle_conversation_starter.start() - logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) 空闲对话检测器已启动") + if conversation_instance.idle_chat: + logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 启动 IdleChat...") + # 不需要再次启动,只需确保已初始化 + logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) IdleChat实例已初始化") if ( conversation_instance.mood_mng diff --git a/src/plugins/PFC/conversation_loop.py b/src/plugins/PFC/conversation_loop.py index 80508cf0..c4575735 100644 --- a/src/plugins/PFC/conversation_loop.py +++ b/src/plugins/PFC/conversation_loop.py @@ -70,11 +70,13 @@ async def run_conversation_loop(conversation_instance: "Conversation"): and loop_iter_start_time < conversation_instance.ignore_until_timestamp ): if ( - conversation_instance.idle_conversation_starter - and conversation_instance.idle_conversation_starter._running + conversation_instance.idle_chat + and conversation_instance.idle_chat._running ): - conversation_instance.idle_conversation_starter.stop() - logger.debug(f"[私聊][{conversation_instance.private_name}] 对话被暂时忽略,暂停空闲对话检测") + # 不直接停止服务,改为暂时忽略此用户 + # 虽然我们仍然可以通过active_instances_count来决定是否触发主动聊天 + # 但为了安全起见,我们只记录一个日志 + logger.debug(f"[私聊][{conversation_instance.private_name}] 对话被暂时忽略,暂停对该用户的主动聊天") sleep_duration = min(30, conversation_instance.ignore_until_timestamp - loop_iter_start_time) await asyncio.sleep(sleep_duration) continue @@ -89,12 +91,9 @@ async def run_conversation_loop(conversation_instance: "Conversation"): await conversation_instance.stop() # 调用 Conversation 实例的 stop 方法 continue else: - if ( - conversation_instance.idle_conversation_starter - and not conversation_instance.idle_conversation_starter._running - ): - conversation_instance.idle_conversation_starter.start() - logger.debug(f"[私聊][{conversation_instance.private_name}] 恢复空闲对话检测") + # 忽略状态结束,这里不需要任何特殊处理 + # IdleChat会通过active_instances_count自动决定是否触发 + pass # 核心规划与行动逻辑 try: From 38adeadc475887fffc843cde837e3f853d0783e5 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Thu, 8 May 2025 19:32:36 +0800 Subject: [PATCH 52/63] ruff --- src/plugins/PFC/PFC_idle/idle_chat.py | 7 +++---- src/plugins/PFC/PFC_idle/idle_chat_manager.py | 2 +- src/plugins/PFC/PFC_idle/idle_conversation.py | 5 ++--- src/plugins/PFC/PFC_idle/idle_conversation_starter.py | 2 -- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/plugins/PFC/PFC_idle/idle_chat.py b/src/plugins/PFC/PFC_idle/idle_chat.py index 38c9e772..6a01e4c4 100644 --- a/src/plugins/PFC/PFC_idle/idle_chat.py +++ b/src/plugins/PFC/PFC_idle/idle_chat.py @@ -1,4 +1,4 @@ -from typing import Optional, Dict, List, Set +from typing import Optional, Dict, Set import asyncio import time import random @@ -7,11 +7,10 @@ from datetime import datetime from src.common.logger_manager import get_logger from src.config.config import global_config from src.plugins.models.utils_model import LLMRequest -from src.plugins.utils.prompt_builder import Prompt, global_prompt_manager +from src.plugins.utils.prompt_builder import global_prompt_manager from src.plugins.person_info.person_info import person_info_manager from src.plugins.utils.chat_message_builder import build_readable_messages from ...schedule.schedule_generator import bot_schedule -from ....config.config import global_config from ..chat_observer import ChatObserver from ..message_sender import DirectMessageSender from src.plugins.chat.chat_stream import ChatStream @@ -109,7 +108,7 @@ class IdleChat: # 如果所有用户都已尝试过,重置尝试集合,从头开始 if len(cls._tried_users) >= len(all_users): cls._tried_users.clear() - logger.info(f"[私聊]所有用户都已尝试过,重置尝试列表") + logger.info("[私聊]所有用户都已尝试过,重置尝试列表") # 随机选择一个不在待回复列表中的用户 available_users = all_users - set(cls._pending_replies.keys()) if available_users: diff --git a/src/plugins/PFC/PFC_idle/idle_chat_manager.py b/src/plugins/PFC/PFC_idle/idle_chat_manager.py index 66ec37b6..1218c109 100644 --- a/src/plugins/PFC/PFC_idle/idle_chat_manager.py +++ b/src/plugins/PFC/PFC_idle/idle_chat_manager.py @@ -146,7 +146,7 @@ class IdleChatManager: # 检查是否所有对话都结束了,帮助调试 all_counts = sum(self._active_conversations_count.values()) if all_counts == 0: - logger.info(f"所有对话实例都已结束,当前总活跃计数为0") + logger.info("所有对话实例都已结束,当前总活跃计数为0") except Exception as e: logger.error(f"对话结束通知处理失败: {stream_id}, 错误: {e}") logger.error(traceback.format_exc()) diff --git a/src/plugins/PFC/PFC_idle/idle_conversation.py b/src/plugins/PFC/PFC_idle/idle_conversation.py index 3aae1853..68e5a240 100644 --- a/src/plugins/PFC/PFC_idle/idle_conversation.py +++ b/src/plugins/PFC/PFC_idle/idle_conversation.py @@ -1,5 +1,4 @@ import traceback -import logging import asyncio from typing import Optional, Dict from src.common.logger_manager import get_logger @@ -210,7 +209,7 @@ class IdleConversation: try: # 创建IdleChat实例 - idle_chat = await self._idle_chat_manager.get_or_create_idle_chat(stream_id, private_name) + _idle_chat = await self._idle_chat_manager.get_or_create_idle_chat(stream_id, private_name) logger.debug(f"[私聊][{private_name}] 已创建或获取IdleChat实例") return True except Exception as e: @@ -478,7 +477,7 @@ async def periodic_system_check(instance: IdleConversation): # 如果IdleChatManager记录的计数为0但自己的记录不为0,进行修正 if manager_count == 0 and active_streams_count > 0: - logger.warning(f"检测到可能的计数错误,尝试修正:清空IdleConversation的活跃流记录") + logger.warning("检测到可能的计数错误,尝试修正:清空IdleConversation的活跃流记录") async with instance._lock: instance._active_streams.clear() diff --git a/src/plugins/PFC/PFC_idle/idle_conversation_starter.py b/src/plugins/PFC/PFC_idle/idle_conversation_starter.py index 1ace9dbd..e6782ab1 100644 --- a/src/plugins/PFC/PFC_idle/idle_conversation_starter.py +++ b/src/plugins/PFC/PFC_idle/idle_conversation_starter.py @@ -3,7 +3,6 @@ import asyncio import random import traceback from typing import TYPE_CHECKING, Optional -from datetime import datetime from src.common.logger_manager import get_logger from src.plugins.models.utils_model import LLMRequest @@ -22,7 +21,6 @@ from rich.traceback import install # 使用TYPE_CHECKING避免循环导入 if TYPE_CHECKING: from ..conversation import Conversation - from ..pfc_manager import PFCManager install(extra_lines=3) From 19d1da1ec805a52f66a932194b18076c4fa84b9b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 8 May 2025 11:32:55 +0000 Subject: [PATCH 53/63] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=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/plugins/PFC/PFC_idle/__init__.py | 12 +- src/plugins/PFC/PFC_idle/idle_chat.py | 243 +++++++++--------- src/plugins/PFC/PFC_idle/idle_chat_manager.py | 41 +-- src/plugins/PFC/PFC_idle/idle_conversation.py | 154 +++++------ .../PFC/PFC_idle/idle_conversation_starter.py | 3 +- src/plugins/PFC/chat_observer.py | 4 +- src/plugins/PFC/conversation_initializer.py | 5 +- src/plugins/PFC/conversation_loop.py | 5 +- 8 files changed, 237 insertions(+), 230 deletions(-) diff --git a/src/plugins/PFC/PFC_idle/__init__.py b/src/plugins/PFC/PFC_idle/__init__.py index a06c1834..77d90210 100644 --- a/src/plugins/PFC/PFC_idle/__init__.py +++ b/src/plugins/PFC/PFC_idle/__init__.py @@ -12,9 +12,9 @@ from .idle_chat_manager import IdleChatManager from .idle_conversation import IdleConversation, get_idle_conversation_instance, initialize_idle_conversation __all__ = [ - 'IdleChat', - 'IdleChatManager', - 'IdleConversation', - 'get_idle_conversation_instance', - 'initialize_idle_conversation' -] \ No newline at end of file + "IdleChat", + "IdleChatManager", + "IdleConversation", + "get_idle_conversation_instance", + "initialize_idle_conversation", +] diff --git a/src/plugins/PFC/PFC_idle/idle_chat.py b/src/plugins/PFC/PFC_idle/idle_chat.py index 6a01e4c4..2962d52c 100644 --- a/src/plugins/PFC/PFC_idle/idle_chat.py +++ b/src/plugins/PFC/PFC_idle/idle_chat.py @@ -22,32 +22,33 @@ install(extra_lines=3) logger = get_logger("pfc_idle_chat") + class IdleChat: """主动聊天组件(测试中) - + 在以下条件都满足时触发主动聊天: 1. 当前没有任何活跃的对话实例 2. 在指定的活动时间内(7:00-23:00) 3. 根据关系值动态调整触发概率 4. 上次触发后已经过了足够的冷却时间 """ - + # 单例模式实现 - _instances: Dict[str, 'IdleChat'] = {} - + _instances: Dict[str, "IdleChat"] = {} + # 全局共享状态,用于跟踪未回复的用户 _pending_replies: Dict[str, float] = {} # 用户名 -> 发送时间 _tried_users: Set[str] = set() # 已尝试过的用户集合 _global_lock = asyncio.Lock() # 保护共享状态的全局锁 - + @classmethod - def get_instance(cls, stream_id: str, private_name: str) -> 'IdleChat': + def get_instance(cls, stream_id: str, private_name: str) -> "IdleChat": """获取IdleChat实例(单例模式) - + Args: stream_id: 聊天流ID private_name: 私聊用户名称 - + Returns: IdleChat: IdleChat实例 """ @@ -58,13 +59,13 @@ class IdleChat: cls._instances[key].start() logger.info(f"[私聊][{private_name}]创建新的IdleChat实例并启动") return cls._instances[key] - + @classmethod async def register_user_response(cls, private_name: str) -> None: """注册用户已回复 - + 当用户回复消息时调用此方法,将用户从待回复列表中移除 - + Args: private_name: 私聊用户名称 """ @@ -72,39 +73,39 @@ class IdleChat: if private_name in cls._pending_replies: del cls._pending_replies[private_name] logger.info(f"[私聊][{private_name}]已回复主动聊天消息,从待回复列表中移除") - + @classmethod async def get_next_available_user(cls) -> Optional[str]: """获取下一个可用于主动聊天的用户 - + 优先选择未尝试过的用户,其次是已尝试但超时未回复的用户 - + Returns: Optional[str]: 下一个可用的用户名,如果没有则返回None """ async with cls._global_lock: current_time = time.time() timeout_threshold = 7200 # 2小时未回复视为超时 - + # 清理超时未回复的用户 for user, send_time in list(cls._pending_replies.items()): if current_time - send_time > timeout_threshold: logger.info(f"[私聊][{user}]超过{timeout_threshold}秒未回复,标记为超时") del cls._pending_replies[user] - + # 获取所有实例中的用户 all_users = set() for key in cls._instances: - user = key.split(':', 1)[0] + user = key.split(":", 1)[0] all_users.add(user) - + # 优先选择未尝试过的用户 untried_users = all_users - cls._tried_users if untried_users: next_user = random.choice(list(untried_users)) cls._tried_users.add(next_user) return next_user - + # 如果所有用户都已尝试过,重置尝试集合,从头开始 if len(cls._tried_users) >= len(all_users): cls._tried_users.clear() @@ -115,9 +116,9 @@ class IdleChat: next_user = random.choice(list(available_users)) cls._tried_users.add(next_user) return next_user - + return None - + def __init__(self, stream_id: str, private_name: str): """初始化主动聊天组件 @@ -129,99 +130,95 @@ class IdleChat: self.private_name = private_name self.chat_observer = ChatObserver.get_instance(stream_id, private_name) self.message_sender = DirectMessageSender(private_name) - + # 添加异步锁,保护对共享变量的访问 self._lock: asyncio.Lock = asyncio.Lock() - + # LLM请求对象,用于生成主动对话内容 - self.llm = LLMRequest( - model=global_config.llm_normal, - temperature=0.5, - max_tokens=500, - request_type="idle_chat" - ) - + self.llm = LLMRequest(model=global_config.llm_normal, temperature=0.5, max_tokens=500, request_type="idle_chat") + # 工作状态 self.active_instances_count: int = 0 self.last_trigger_time: float = time.time() - 1500 # 初始化时减少等待时间 self._running: bool = False self._task: Optional[asyncio.Task] = None - + # 配置参数 - 从global_config加载 - self.min_cooldown = getattr(global_config,"MIN_IDLE_TIME", 7200) # 最短冷却时间(默认2小时)建议修改长一点,你也不希望你的bot一直骚扰你吧 + self.min_cooldown = getattr( + global_config, "MIN_IDLE_TIME", 7200 + ) # 最短冷却时间(默认2小时)建议修改长一点,你也不希望你的bot一直骚扰你吧 self.max_cooldown = getattr(global_config, "MAX_IDLE_TIME", 14400) # 最长冷却时间(默认4小时) - self.min_idle_time = getattr(global_config, "MIN_IDLE_TIME", 3600) + self.min_idle_time = getattr(global_config, "MIN_IDLE_TIME", 3600) self.check_interval = getattr(global_config, "IDLE_CHECK_INTERVAL", 600) # 检查间隔(默认10分钟) self.active_hours_start = 6 # 活动开始时间 - self.active_hours_end = 24 # 活动结束时间 - + self.active_hours_end = 24 # 活动结束时间 + # 关系值相关 self.base_trigger_probability = 0.3 # 基础触发概率 - self.relationship_factor = 0.0003 # 关系值影响因子 - + self.relationship_factor = 0.0003 # 关系值影响因子 + def start(self) -> None: """启动主动聊天检测""" # 检查是否启用了主动聊天功能 if not getattr(global_config, "ENABLE_IDLE_CONVERSATION", False): logger.info(f"[私聊][{self.private_name}]主动聊天功能已禁用(配置ENABLE_IDLE_CONVERSATION=False)") return - + if self._running: logger.debug(f"[私聊][{self.private_name}]主动聊天功能已在运行中") return - + self._running = True self._task = asyncio.create_task(self._check_idle_loop()) logger.info(f"[私聊][{self.private_name}]启动主动聊天检测") - + def stop(self) -> None: - """停止主动聊天检测 - """ + """停止主动聊天检测""" if not self._running: return - + self._running = False if self._task: self._task.cancel() self._task = None logger.info(f"[私聊][{self.private_name}]停止主动聊天检测") - + async def increment_active_instances(self) -> None: """增加活跃实例计数 - + 当创建新的对话实例时调用此方法 """ async with self._lock: self.active_instances_count += 1 logger.debug(f"[私聊][{self.private_name}]活跃实例数+1,当前:{self.active_instances_count}") - + async def decrement_active_instances(self) -> None: """减少活跃实例计数 - + 当对话实例结束时调用此方法 """ async with self._lock: self.active_instances_count = max(0, self.active_instances_count - 1) logger.debug(f"[私聊][{self.private_name}]活跃实例数-1,当前:{self.active_instances_count}") - + async def update_last_message_time(self, message_time: Optional[float] = None) -> None: """更新最后一条消息的时间 - + Args: message_time: 消息时间戳,如果为None则使用当前时间 """ async with self._lock: self.last_trigger_time = message_time or time.time() logger.debug(f"[私聊][{self.private_name}]更新最后消息时间: {self.last_trigger_time:.2f}") - + # 当用户发送消息时,也应该注册响应 await self.__class__.register_user_response(self.private_name) - + def _is_active_hours(self) -> bool: """检查是否在活动时间内""" current_hour = datetime.now().hour return self.active_hours_start <= current_hour < self.active_hours_end - + async def _should_trigger(self) -> bool: """检查是否应该触发主动聊天""" async with self._lock: @@ -229,25 +226,27 @@ class IdleChat: if self.active_instances_count < 0: logger.warning(f"[私聊][{self.private_name}]检测到活跃实例数为负数,重置为0") self.active_instances_count = 0 - + # 检查是否有活跃实例 if self.active_instances_count > 0: logger.debug(f"[私聊][{self.private_name}]存在活跃实例({self.active_instances_count}),不触发主动聊天") return False - + # 检查是否在活动时间内 if not self._is_active_hours(): logger.debug(f"[私聊][{self.private_name}]不在活动时间内,不触发主动聊天") return False - + # 检查冷却时间 current_time = time.time() time_since_last_trigger = current_time - self.last_trigger_time if time_since_last_trigger < self.min_cooldown: time_left = self.min_cooldown - time_since_last_trigger - logger.debug(f"[私聊][{self.private_name}]冷却时间未到(已过{time_since_last_trigger:.0f}秒/需要{self.min_cooldown}秒),还需等待{time_left:.0f}秒,不触发主动聊天") + logger.debug( + f"[私聊][{self.private_name}]冷却时间未到(已过{time_since_last_trigger:.0f}秒/需要{self.min_cooldown}秒),还需等待{time_left:.0f}秒,不触发主动聊天" + ) return False - + # 强制触发检查 - 如果超过最大冷却时间,增加触发概率 force_trigger = False if time_since_last_trigger > self.max_cooldown * 2: # 如果超过最大冷却时间的两倍 @@ -255,9 +254,11 @@ class IdleChat: random_force = random.random() force_trigger = random_force < force_probability if force_trigger: - logger.info(f"[私聊][{self.private_name}]超过最大冷却时间({time_since_last_trigger:.0f}秒),强制触发主动聊天") + logger.info( + f"[私聊][{self.private_name}]超过最大冷却时间({time_since_last_trigger:.0f}秒),强制触发主动聊天" + ) return True - + # 获取关系值 relationship_value = 0 try: @@ -270,10 +271,10 @@ class IdleChat: # 先尝试通过昵称获取person_id platform = "qq" # 默认平台 person_id = person_info_manager.get_person_id(platform, self.private_name) - + # 如果通过昵称获取失败,尝试通过stream_id解析 if not person_id: - parts = self.stream_id.split('_') + parts = self.stream_id.split("_") if len(parts) >= 2 and parts[0] == "private": user_id = parts[1] platform = parts[2] if len(parts) >= 3 else "qq" @@ -284,7 +285,7 @@ class IdleChat: person_id = person_info_manager.get_person_id(platform, user_id) except Exception as e2: logger.warning(f"[私聊][{self.private_name}]尝试获取person_id失败: {str(e2)}") - + # 获取关系值 if person_id: raw_value = await person_info_manager.get_value(person_id, "relationship_value") @@ -292,51 +293,61 @@ class IdleChat: logger.debug(f"[私聊][{self.private_name}]成功获取关系值: {relationship_value}") else: logger.warning(f"[私聊][{self.private_name}]无法获取person_id,使用默认关系值0") - + # 使用PfcRepationshipTranslator获取关系描述 relationship_translator = PfcRepationshipTranslator(self.private_name) - relationship_level = relationship_translator._calculate_relationship_level_num(relationship_value, self.private_name) - + relationship_level = relationship_translator._calculate_relationship_level_num( + relationship_value, self.private_name + ) + # 基于关系等级调整触发概率 # 关系越好,主动聊天概率越高 level_probability_factors = [0.05, 0.1, 0.2, 0.3, 0.4, 0.5] # 每个等级对应的基础概率因子 base_probability = level_probability_factors[relationship_level] - + # 基础概率因子 trigger_probability = base_probability trigger_probability = max(0.05, min(0.6, trigger_probability)) # 限制在0.05-0.6之间 - + # 最大冷却时间调整 - 随着冷却时间增加,逐渐增加触发概率 if time_since_last_trigger > self.max_cooldown: # 计算额外概率 - 每超过最大冷却时间的10%,增加1%的概率,最多增加30% - extra_time_factor = min(0.3, (time_since_last_trigger - self.max_cooldown) / (self.max_cooldown * 10)) + extra_time_factor = min( + 0.3, (time_since_last_trigger - self.max_cooldown) / (self.max_cooldown * 10) + ) trigger_probability += extra_time_factor logger.debug(f"[私聊][{self.private_name}]超过标准冷却时间,额外增加概率: +{extra_time_factor:.2f}") - + # 随机判断是否触发 random_value = random.random() should_trigger = random_value < trigger_probability - logger.debug(f"[私聊][{self.private_name}]触发概率计算: 基础({base_probability:.2f}) + 关系值({relationship_value})影响 = {trigger_probability:.2f},随机值={random_value:.2f}, 结果={should_trigger}") - + logger.debug( + f"[私聊][{self.private_name}]触发概率计算: 基础({base_probability:.2f}) + 关系值({relationship_value})影响 = {trigger_probability:.2f},随机值={random_value:.2f}, 结果={should_trigger}" + ) + # 如果决定触发,记录详细日志 if should_trigger: - logger.info(f"[私聊][{self.private_name}]决定触发主动聊天: 触发概率={trigger_probability:.2f}, 距上次已过{time_since_last_trigger:.0f}秒") - + logger.info( + f"[私聊][{self.private_name}]决定触发主动聊天: 触发概率={trigger_probability:.2f}, 距上次已过{time_since_last_trigger:.0f}秒" + ) + return should_trigger - + except Exception as e: logger.error(f"[私聊][{self.private_name}]获取关系值失败: {str(e)}") logger.error(traceback.format_exc()) - + # 即使获取关系值失败,仍有一个基础的几率触发 # 这确保即使数据库有问题,主动聊天功能仍然可用 base_fallback_probability = 0.1 # 较低的基础几率 random_fallback = random.random() fallback_trigger = random_fallback < base_fallback_probability if fallback_trigger: - logger.info(f"[私聊][{self.private_name}]获取关系值失败,使用后备触发机制: 概率={base_fallback_probability:.2f}, 决定={fallback_trigger}") + logger.info( + f"[私聊][{self.private_name}]获取关系值失败,使用后备触发机制: 概率={base_fallback_probability:.2f}, 决定={fallback_trigger}" + ) return fallback_trigger - + async def _check_idle_loop(self) -> None: """检查空闲状态的循环""" try: @@ -346,16 +357,16 @@ class IdleChat: # 如果禁用了功能,等待一段时间后再次检查配置 await asyncio.sleep(60) # 每分钟检查一次配置变更 continue - + # 检查当前用户是否应该触发主动聊天 should_trigger = await self._should_trigger() - + # 如果当前用户不触发,检查是否有其他用户已经超时未回复 if not should_trigger: async with self.__class__._global_lock: current_time = time.time() pending_timeout = 1800 # 30分钟未回复检查 - + # 检查此用户是否在等待回复列表中 if self.private_name in self.__class__._pending_replies: logger.debug(f"[私聊][{self.private_name}]当前用户在等待回复列表中,不进行额外检查") @@ -365,12 +376,12 @@ class IdleChat: for user, send_time in self.__class__._pending_replies.items(): if current_time - send_time > pending_timeout: timed_out_users.append(user) - + # 如果有超时未回复的用户,尝试找下一个用户 if timed_out_users: logger.info(f"[私聊]发现{len(timed_out_users)}个用户超过{pending_timeout}秒未回复") next_user = await self.__class__.get_next_available_user() - + if next_user and next_user != self.private_name: logger.info(f"[私聊]选择下一个用户[{next_user}]进行主动聊天") # 查找该用户的实例并触发聊天 @@ -380,7 +391,7 @@ class IdleChat: # 触发该实例的主动聊天 asyncio.create_task(instance._initiate_chat()) break - + # 如果当前用户应该触发主动聊天 if should_trigger: try: @@ -388,7 +399,7 @@ class IdleChat: # 更新上次触发时间 async with self._lock: self.last_trigger_time = time.time() - + # 将此用户添加到等待回复列表中 async with self.__class__._global_lock: self.__class__._pending_replies[self.private_name] = time.time() @@ -397,12 +408,12 @@ class IdleChat: except Exception as e: logger.error(f"[私聊][{self.private_name}]执行主动聊天过程出错: {str(e)}") logger.error(traceback.format_exc()) - + # 等待下一次检查 check_interval = self.check_interval # 使用配置的检查间隔 logger.debug(f"[私聊][{self.private_name}]等待{check_interval}秒后进行下一次主动聊天检查") await asyncio.sleep(check_interval) - + except asyncio.CancelledError: logger.debug(f"[私聊][{self.private_name}]主动聊天检测任务被取消") except Exception as e: @@ -412,24 +423,25 @@ class IdleChat: if self._running: logger.info(f"[私聊][{self.private_name}]尝试重新启动主动聊天检测") self._task = asyncio.create_task(self._check_idle_loop()) - + async def _get_chat_stream(self) -> Optional[ChatStream]: """获取聊天流实例""" try: # 尝试从全局聊天管理器获取现有的聊天流 from src.plugins.chat.chat_stream import chat_manager + existing_chat_stream = chat_manager.get_stream(self.stream_id) if existing_chat_stream: logger.debug(f"[私聊][{self.private_name}]从chat_manager找到现有聊天流") return existing_chat_stream - + # 如果没有现有聊天流,则创建新的 logger.debug(f"[私聊][{self.private_name}]未找到现有聊天流,创建新聊天流") # 创建用户信息对象 user_info = UserInfo( user_id=self.private_name, # 使用私聊用户的ID user_nickname=self.private_name, # 使用私聊用户的名称 - platform="qq" + platform="qq", ) # 创建聊天流 new_stream = ChatStream(self.stream_id, "qq", user_info) @@ -441,23 +453,19 @@ class IdleChat: logger.error(f"[私聊][{self.private_name}]创建/获取聊天流失败: {str(e)}") logger.error(traceback.format_exc()) return None - + async def _initiate_chat(self) -> None: """生成并发送主动聊天消息""" try: # 获取聊天历史记录 messages = self.chat_observer.get_cached_messages(limit=12) chat_history_text = await build_readable_messages( - messages, - replace_bot_name=True, - merge_messages=False, - timestamp_mode="relative", - read_mark=0.0 + messages, replace_bot_name=True, merge_messages=False, timestamp_mode="relative", read_mark=0.0 ) - + # 获取关系信息 from src.plugins.person_info.relationship_manager import relationship_manager - + # 获取关系值 relationship_value = 0 try: @@ -468,23 +476,25 @@ class IdleChat: relationship_value = relationship_manager.ensure_float(raw_value, person_id) except Exception as e: logger.warning(f"[私聊][{self.private_name}]获取关系值失败,使用默认值: {e}") - + # 使用PfcRepationshipTranslator获取关系描述 relationship_translator = PfcRepationshipTranslator(self.private_name) - full_relationship_text = await relationship_translator.translate_relationship_value_to_text(relationship_value) - + full_relationship_text = await relationship_translator.translate_relationship_value_to_text( + relationship_value + ) + # 提取纯关系描述(去掉"你们的关系是:"前缀) relationship_description = "普通" # 默认值 if ":" in full_relationship_text: relationship_description = full_relationship_text.split(":")[1].replace("。", "") - + if global_config.ENABLE_SCHEDULE_GEN: schedule_prompt = await global_prompt_manager.format_prompt( - "schedule_prompt", schedule_info=bot_schedule.get_current_num_task(num=1, time_info=False) - ) + "schedule_prompt", schedule_info=bot_schedule.get_current_num_task(num=1, time_info=False) + ) else: schedule_prompt = "" - + # 构建提示词 current_time = datetime.now().strftime("%H:%M") prompt = f"""你是{global_config.BOT_NICKNAME}。 @@ -500,14 +510,11 @@ class IdleChat: 请直接输出一条消息,不要有任何额外的解释或引导文字 消息内容尽量简短 """ - + # 生成回复 logger.debug(f"[私聊][{self.private_name}]开始生成主动聊天内容") try: - content, _ = await asyncio.wait_for( - self.llm.generate_response_async(prompt), - timeout=30 - ) + content, _ = await asyncio.wait_for(self.llm.generate_response_async(prompt), timeout=30) logger.debug(f"[私聊][{self.private_name}]成功生成主动聊天内容: {content}") except asyncio.TimeoutError: logger.error(f"[私聊][{self.private_name}]生成主动聊天内容超时") @@ -516,34 +523,30 @@ class IdleChat: logger.error(f"[私聊][{self.private_name}]生成主动聊天内容失败: {str(llm_err)}") logger.error(traceback.format_exc()) return - + # 清理结果 content = content.strip() content = content.strip("\"'") - + if not content: logger.error(f"[私聊][{self.private_name}]生成的主动聊天内容为空") return - + # 获取聊天流 chat_stream = await self._get_chat_stream() if not chat_stream: logger.error(f"[私聊][{self.private_name}]无法获取有效的聊天流,取消发送主动消息") return - + # 发送消息 try: 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, content=content, reply_to_message=None) logger.info(f"[私聊][{self.private_name}]成功主动发起聊天: {content}") except Exception as e: logger.error(f"[私聊][{self.private_name}]发送主动聊天消息失败: {str(e)}") logger.error(traceback.format_exc()) - + except Exception as e: logger.error(f"[私聊][{self.private_name}]主动发起聊天过程中发生未预期的错误: {str(e)}") - logger.error(traceback.format_exc()) \ No newline at end of file + logger.error(traceback.format_exc()) diff --git a/src/plugins/PFC/PFC_idle/idle_chat_manager.py b/src/plugins/PFC/PFC_idle/idle_chat_manager.py index 1218c109..a8708331 100644 --- a/src/plugins/PFC/PFC_idle/idle_chat_manager.py +++ b/src/plugins/PFC/PFC_idle/idle_chat_manager.py @@ -6,21 +6,22 @@ import traceback logger = get_logger("pfc_idle_chat_manager") + class IdleChatManager: """空闲聊天管理器 - + 用于管理所有私聊用户的空闲聊天实例。 采用单例模式,确保全局只有一个管理器实例。 """ - + _instance: Optional["IdleChatManager"] = None _lock: asyncio.Lock = asyncio.Lock() - + def __init__(self): """初始化空闲聊天管理器""" self._idle_chats: Dict[str, IdleChat] = {} # stream_id -> IdleChat self._active_conversations_count: Dict[str, int] = {} # stream_id -> count - + @classmethod def get_instance(cls) -> "IdleChatManager": """获取管理器单例 (同步版本) @@ -32,7 +33,7 @@ class IdleChatManager: # 在同步环境中创建实例 cls._instance = cls() return cls._instance - + @classmethod async def get_instance_async(cls) -> "IdleChatManager": """获取管理器单例 (异步版本) @@ -45,7 +46,7 @@ class IdleChatManager: if not cls._instance: cls._instance = cls() return cls._instance - + async def get_or_create_idle_chat(self, stream_id: str, private_name: str) -> IdleChat: """获取或创建空闲聊天实例 @@ -65,7 +66,7 @@ class IdleChatManager: idle_chat.start() # 启动空闲检测 logger.info(f"[私聊][{private_name}]创建并启动新的空闲聊天实例") return self._idle_chats[stream_id] - + async def remove_idle_chat(self, stream_id: str) -> None: """移除空闲聊天实例 @@ -79,7 +80,7 @@ class IdleChatManager: if stream_id in self._active_conversations_count: del self._active_conversations_count[stream_id] logger.info(f"[私聊][{idle_chat.private_name}]移除空闲聊天实例") - + async def notify_conversation_start(self, stream_id: str) -> None: """通知对话开始 @@ -96,15 +97,15 @@ class IdleChatManager: if len(parts) >= 2: private_name = parts[1] # 取第二部分作为名称 await self.get_or_create_idle_chat(stream_id, private_name) - + if stream_id not in self._active_conversations_count: self._active_conversations_count[stream_id] = 0 - + # 增加计数前记录当前值,用于日志 old_count = self._active_conversations_count[stream_id] self._active_conversations_count[stream_id] += 1 new_count = self._active_conversations_count[stream_id] - + # 确保IdleChat实例存在 idle_chat = self._idle_chats.get(stream_id) if idle_chat: @@ -115,7 +116,7 @@ class IdleChatManager: except Exception as e: logger.error(f"对话开始通知处理失败: {stream_id}, 错误: {e}") logger.error(traceback.format_exc()) - + async def notify_conversation_end(self, stream_id: str) -> None: """通知对话结束 @@ -125,16 +126,16 @@ class IdleChatManager: try: # 记录当前计数用于日志 old_count = self._active_conversations_count.get(stream_id, 0) - + # 安全减少计数,避免负数 if stream_id in self._active_conversations_count and self._active_conversations_count[stream_id] > 0: self._active_conversations_count[stream_id] -= 1 else: # 如果计数已经为0或不存在,设置为0 self._active_conversations_count[stream_id] = 0 - + new_count = self._active_conversations_count.get(stream_id, 0) - + # 确保IdleChat实例存在 idle_chat = self._idle_chats.get(stream_id) if idle_chat: @@ -142,7 +143,7 @@ class IdleChatManager: logger.debug(f"对话结束通知: {stream_id}, 计数从{old_count}减少到{new_count}") else: logger.warning(f"对话结束通知: {stream_id}, 计数减少但IdleChat不存在! 计数:{old_count}->{new_count}") - + # 检查是否所有对话都结束了,帮助调试 all_counts = sum(self._active_conversations_count.values()) if all_counts == 0: @@ -150,7 +151,7 @@ class IdleChatManager: except Exception as e: logger.error(f"对话结束通知处理失败: {stream_id}, 错误: {e}") logger.error(traceback.format_exc()) - + def get_idle_chat(self, stream_id: str) -> Optional[IdleChat]: """获取空闲聊天实例 @@ -161,7 +162,7 @@ class IdleChatManager: Optional[IdleChat]: 空闲聊天实例,如果不存在则返回None """ return self._idle_chats.get(stream_id) - + def get_active_conversations_count(self, stream_id: str) -> int: """获取指定流的活跃对话计数 @@ -172,11 +173,11 @@ class IdleChatManager: int: 活跃对话计数 """ return self._active_conversations_count.get(stream_id, 0) - + def get_all_active_conversations_count(self) -> int: """获取所有活跃对话总计数 Returns: int: 活跃对话总计数 """ - return sum(self._active_conversations_count.values()) \ No newline at end of file + return sum(self._active_conversations_count.values()) diff --git a/src/plugins/PFC/PFC_idle/idle_conversation.py b/src/plugins/PFC/PFC_idle/idle_conversation.py index 68e5a240..90036d13 100644 --- a/src/plugins/PFC/PFC_idle/idle_conversation.py +++ b/src/plugins/PFC/PFC_idle/idle_conversation.py @@ -6,6 +6,7 @@ import time logger = get_logger("pfc_idle_conversation") + class IdleConversation: """ 处理Idle聊天相关的功能,将这些功能从主Conversation类中分离出来, @@ -27,17 +28,18 @@ class IdleConversation: if self._initialization_in_progress: logger.debug("IdleConversation正在初始化中,等待完成") return False - + if self._idle_chat_manager is not None: logger.debug("IdleConversation已初始化,无需重复操作") return True - + # 标记开始初始化 self._initialization_in_progress = True - + try: # 从PFCManager获取IdleChatManager实例 from ..pfc_manager import PFCManager + pfc_manager = PFCManager.get_instance() self._idle_chat_manager = pfc_manager.get_idle_chat_manager() logger.debug("IdleConversation初始化完成,已获取IdleChatManager实例") @@ -49,19 +51,19 @@ class IdleConversation: finally: # 无论成功或失败,都清除初始化标志 self._initialization_in_progress = False - + async def start(self): """启动IdleConversation,创建后台监控任务""" if self._running: logger.debug("IdleConversation已经在运行") return False - + if not self._idle_chat_manager: success = await self.initialize() if not success: logger.error("无法启动IdleConversation:初始化失败") return False - + try: self._running = True # 创建后台监控任务,使用try-except块来捕获可能的异常 @@ -76,19 +78,19 @@ class IdleConversation: # 如果没有活跃的事件循环,记录警告但继续执行 logger.warning("没有活跃的事件循环,IdleConversation将不会启动监控任务") # 尽管没有监控任务,但仍然将running设为True表示IdleConversation已启动 - + return True except Exception as e: self._running = False logger.error(f"启动IdleConversation失败: {e}") logger.error(traceback.format_exc()) return False - + async def stop(self): """停止IdleConversation的后台任务""" if not self._running: return - + self._running = False if self._monitor_task and not self._monitor_task.done(): try: @@ -102,10 +104,10 @@ class IdleConversation: except Exception as e: logger.error(f"停止IdleConversation监控任务时出错: {e}") logger.error(traceback.format_exc()) - + self._monitor_task = None logger.info("IdleConversation已停止") - + async def _monitor_loop(self): """后台监控循环,定期检查活跃的会话并执行必要的操作""" try: @@ -114,15 +116,15 @@ class IdleConversation: # 同步活跃流计数到IdleChatManager if self._idle_chat_manager: await self._sync_active_streams_to_manager() - + # 这里可以添加定期检查逻辑,如查询空闲状态等 active_count = len(self._active_streams) logger.debug(f"IdleConversation监控中,当前活跃流数量: {active_count}") - + except Exception as e: logger.error(f"IdleConversation监控循环出错: {e}") logger.error(traceback.format_exc()) - + # 每30秒执行一次监控 await asyncio.sleep(30) except asyncio.CancelledError: @@ -131,29 +133,29 @@ class IdleConversation: logger.error(f"IdleConversation监控任务异常退出: {e}") logger.error(traceback.format_exc()) self._running = False - + async def _sync_active_streams_to_manager(self): """同步活跃流计数到IdleChatManager和IdleChat""" try: if not self._idle_chat_manager: return - + # 获取当前的活跃流列表 async with self._lock: active_streams = list(self._active_streams.keys()) - + # 对每个活跃流,确保IdleChatManager和IdleChat中的计数是正确的 for stream_id in active_streams: # 获取当前IdleChatManager中的计数 manager_count = self._idle_chat_manager.get_active_conversations_count(stream_id) - + # 由于我们的活跃流字典只记录是否活跃(值为True),所以计数应该是1 if manager_count != 1: # 修正IdleChatManager中的计数 old_count = manager_count self._idle_chat_manager._active_conversations_count[stream_id] = 1 logger.warning(f"同步调整IdleChatManager中的计数: stream_id={stream_id}, {old_count}->1") - + # 同时修正IdleChat中的计数 idle_chat = self._idle_chat_manager.get_idle_chat(stream_id) if idle_chat: @@ -161,26 +163,26 @@ class IdleConversation: old_count = getattr(idle_chat, "active_instances_count", 0) idle_chat.active_instances_count = 1 logger.warning(f"同步调整IdleChat中的计数: stream_id={stream_id}, {old_count}->1") - + # 检查IdleChatManager中有没有多余的计数(conversation中已不存在但manager中还有) for stream_id, count in list(self._idle_chat_manager._active_conversations_count.items()): if count > 0 and stream_id not in active_streams: # 重置为0 self._idle_chat_manager._active_conversations_count[stream_id] = 0 logger.warning(f"重置IdleChatManager中的多余计数: stream_id={stream_id}, {count}->0") - + # 同时修正IdleChat中的计数 idle_chat = self._idle_chat_manager.get_idle_chat(stream_id) if idle_chat and getattr(idle_chat, "active_instances_count", 0) > 0: old_count = getattr(idle_chat, "active_instances_count", 0) idle_chat.active_instances_count = 0 logger.warning(f"同步重置IdleChat中的计数: stream_id={stream_id}, {old_count}->0") - + # 日志记录同步结果 total_active = len(active_streams) total_manager = sum(self._idle_chat_manager._active_conversations_count.values()) logger.debug(f"同步后的计数: IdleConversation活跃流={total_active}, IdleChatManager总计数={total_manager}") - + except Exception as e: logger.error(f"同步活跃流计数失败: {e}") logger.error(traceback.format_exc()) @@ -188,25 +190,25 @@ class IdleConversation: async def get_or_create_idle_chat(self, stream_id: str, private_name: str): """ 获取或创建IdleChat实例 - + Args: stream_id: 聊天流ID private_name: 私聊对象名称,用于日志 - + Returns: bool: 操作是否成功 """ # 确保IdleConversation已启动 if not self._running: await self.start() - + if not self._idle_chat_manager: # 如果尚未初始化,尝试初始化 success = await self.initialize() if not success: logger.warning(f"[私聊][{private_name}] 获取或创建IdleChat失败:IdleChatManager未初始化") return False - + try: # 创建IdleChat实例 _idle_chat = await self._idle_chat_manager.get_or_create_idle_chat(stream_id, private_name) @@ -220,11 +222,11 @@ class IdleConversation: async def notify_conversation_start(self, stream_id: str, private_name: str) -> bool: """ 通知空闲聊天管理器对话开始 - + Args: stream_id: 聊天流ID private_name: 私聊对象名称,用于日志 - + Returns: bool: 通知是否成功 """ @@ -235,32 +237,32 @@ class IdleConversation: if not success: logger.warning(f"[私聊][{private_name}] 启动IdleConversation失败,无法通知对话开始") return False - + if not self._idle_chat_manager: # 如果尚未初始化,尝试初始化 success = await self.initialize() if not success: logger.warning(f"[私聊][{private_name}] 通知对话开始失败:IdleChatManager未初始化") return False - + try: # 确保IdleChat实例已创建 - 这是关键步骤,要先创建IdleChat await self.get_or_create_idle_chat(stream_id, private_name) - + # 先记录活跃状态 - 这是权威源 async with self._lock: self._active_streams[stream_id] = True - + # 然后同步到IdleChatManager if self._idle_chat_manager: await self._idle_chat_manager.notify_conversation_start(stream_id) logger.info(f"[私聊][{private_name}] 已通知空闲聊天管理器对话开始") else: logger.warning(f"[私聊][{private_name}] IdleChatManager不存在,但已记录活跃状态") - + # 立即进行一次同步,确保数据一致性 await self._sync_active_streams_to_manager() - + return True except Exception as e: logger.warning(f"[私聊][{private_name}] 通知空闲聊天管理器对话开始失败: {e}") @@ -277,11 +279,11 @@ class IdleConversation: async def notify_conversation_end(self, stream_id: str, private_name: str) -> bool: """ 通知空闲聊天管理器对话结束 - + Args: stream_id: 聊天流ID private_name: 私聊对象名称,用于日志 - + Returns: bool: 通知是否成功 """ @@ -293,30 +295,30 @@ class IdleConversation: del self._active_streams[stream_id] was_active = True logger.debug(f"[私聊][{private_name}] 已从活跃流中移除 {stream_id}") - + if not self._idle_chat_manager: # 如果尚未初始化,尝试初始化 success = await self.initialize() if not success: logger.warning(f"[私聊][{private_name}] 通知对话结束失败:IdleChatManager未初始化") return False - + try: # 然后同步到IdleChatManager if self._idle_chat_manager: # 无论如何都尝试通知 await self._idle_chat_manager.notify_conversation_end(stream_id) - + # 立即进行一次同步,确保数据一致性 await self._sync_active_streams_to_manager() - + logger.info(f"[私聊][{private_name}] 已通知空闲聊天管理器对话结束") - + # 检查当前活跃流数量 active_count = len(self._active_streams) if active_count == 0: logger.info(f"[私聊][{private_name}] 当前无活跃流,可能会触发主动聊天") - + # 额外调用:如果实例存在且只有在确实移除了活跃流的情况下才触发检查 if was_active: idle_chat = self._idle_chat_manager.get_idle_chat(stream_id) @@ -324,7 +326,7 @@ class IdleConversation: # 直接触发IdleChat检查,而不是等待下一个循环 logger.info(f"[私聊][{private_name}] 对话结束,手动触发一次主动聊天检查") asyncio.create_task(self._trigger_idle_chat_check(idle_chat, stream_id, private_name)) - + return True else: logger.warning(f"[私聊][{private_name}] IdleChatManager不存在,但已更新活跃状态") @@ -337,14 +339,14 @@ class IdleConversation: logger.error(f"[私聊][{private_name}] 处理对话结束通知时发生严重错误: {outer_e}") logger.error(traceback.format_exc()) return False - + async def _trigger_idle_chat_check(self, idle_chat, stream_id: str, private_name: str): """在对话结束后,手动触发一次IdleChat的检查""" try: # 确保活跃计数与IdleConversation一致 async with self._lock: is_active_in_conversation = stream_id in self._active_streams - + # 强制使IdleChat的计数与IdleConversation一致 if is_active_in_conversation: # 如果在IdleConversation中是活跃的,IdleChat的计数应该是1 @@ -358,17 +360,17 @@ class IdleConversation: old_count = idle_chat.active_instances_count idle_chat.active_instances_count = 0 logger.warning(f"[私聊][{private_name}] 修正IdleChat计数: {old_count}->0") - + # 等待1秒,让任何正在进行的处理完成 await asyncio.sleep(1) - + # 只有当stream不再活跃时才触发检查 if not is_active_in_conversation: # 尝试触发一次检查 if hasattr(idle_chat, "_should_trigger"): should_trigger = await idle_chat._should_trigger() logger.info(f"[私聊][{private_name}] 手动触发主动聊天检查结果: {should_trigger}") - + # 如果应该触发,直接调用_initiate_chat if should_trigger and hasattr(idle_chat, "_initiate_chat"): logger.info(f"[私聊][{private_name}] 手动触发主动聊天") @@ -384,11 +386,11 @@ class IdleConversation: def is_stream_active(self, stream_id: str) -> bool: """检查指定的stream是否活跃""" return stream_id in self._active_streams - + def get_active_streams_count(self) -> int: """获取当前活跃的stream数量""" return len(self._active_streams) - + @property def is_running(self) -> bool: """检查IdleConversation是否正在运行""" @@ -399,39 +401,41 @@ class IdleConversation: """获取IdleChatManager实例""" return self._idle_chat_manager + # 创建单例实例 _instance: Optional[IdleConversation] = None _instance_lock = asyncio.Lock() _initialization_in_progress = False # 防止并发初始化 + async def initialize_idle_conversation() -> IdleConversation: """初始化并启动IdleConversation单例实例""" global _initialization_in_progress - + # 防止并发初始化 if _initialization_in_progress: logger.debug("IdleConversation全局初始化正在进行中,等待完成") return get_idle_conversation_instance() - + # 标记正在初始化 _initialization_in_progress = True - + try: instance = get_idle_conversation_instance() - + # 如果实例已经在运行,避免重复初始化 - if getattr(instance, '_running', False): + if getattr(instance, "_running", False): logger.debug("IdleConversation已在运行状态,无需重新初始化") _initialization_in_progress = False return instance - + # 初始化实例 success = await instance.initialize() if not success: logger.error("IdleConversation初始化失败") _initialization_in_progress = False return instance - + # 启动实例 success = await instance.start() if not success: @@ -440,10 +444,10 @@ async def initialize_idle_conversation() -> IdleConversation: # 启动成功,进行初始检查 logger.info("IdleConversation启动成功,执行初始化后检查") # 这里可以添加一些启动后的检查,如果需要 - + # 创建一个异步任务,定期检查系统状态 asyncio.create_task(periodic_system_check(instance)) - + return instance except Exception as e: logger.error(f"初始化并启动IdleConversation时出错: {e}") @@ -455,40 +459,43 @@ async def initialize_idle_conversation() -> IdleConversation: # 清除初始化标志 _initialization_in_progress = False + async def periodic_system_check(instance: IdleConversation): """定期检查系统状态,确保主动聊天功能正常工作""" try: # 等待10秒,让系统完全启动 await asyncio.sleep(10) - - while getattr(instance, '_running', False): + + while getattr(instance, "_running", False): try: # 检查活跃流数量 - active_streams_count = len(getattr(instance, '_active_streams', {})) - + active_streams_count = len(getattr(instance, "_active_streams", {})) + # 如果IdleChatManager存在,检查其中的活跃对话计数 - idle_chat_manager = getattr(instance, '_idle_chat_manager', None) - if idle_chat_manager and hasattr(idle_chat_manager, 'get_all_active_conversations_count'): + idle_chat_manager = getattr(instance, "_idle_chat_manager", None) + if idle_chat_manager and hasattr(idle_chat_manager, "get_all_active_conversations_count"): manager_count = idle_chat_manager.get_all_active_conversations_count() - + # 如果两者不一致,记录警告 if active_streams_count != manager_count: - logger.warning(f"检测到计数不一致: IdleConversation记录的活跃流数量({active_streams_count}) 与 IdleChatManager记录的活跃对话数({manager_count})不匹配") - + logger.warning( + f"检测到计数不一致: IdleConversation记录的活跃流数量({active_streams_count}) 与 IdleChatManager记录的活跃对话数({manager_count})不匹配" + ) + # 如果IdleChatManager记录的计数为0但自己的记录不为0,进行修正 if manager_count == 0 and active_streams_count > 0: logger.warning("检测到可能的计数错误,尝试修正:清空IdleConversation的活跃流记录") async with instance._lock: instance._active_streams.clear() - + # 检查计数如果为0,帮助日志输出 if active_streams_count == 0: logger.debug("当前没有活跃的对话流,应该可以触发主动聊天") - + except Exception as check_err: logger.error(f"执行系统检查时出错: {check_err}") logger.error(traceback.format_exc()) - + # 每60秒检查一次 await asyncio.sleep(60) except asyncio.CancelledError: @@ -497,9 +504,10 @@ async def periodic_system_check(instance: IdleConversation): logger.error(f"系统检查任务异常退出: {e}") logger.error(traceback.format_exc()) + def get_idle_conversation_instance() -> IdleConversation: """获取IdleConversation的单例实例""" global _instance if _instance is None: _instance = IdleConversation() - return _instance \ No newline at end of file + return _instance diff --git a/src/plugins/PFC/PFC_idle/idle_conversation_starter.py b/src/plugins/PFC/PFC_idle/idle_conversation_starter.py index e6782ab1..83a58219 100644 --- a/src/plugins/PFC/PFC_idle/idle_conversation_starter.py +++ b/src/plugins/PFC/PFC_idle/idle_conversation_starter.py @@ -27,6 +27,7 @@ install(extra_lines=3) # 获取当前模块的日志记录器 logger = get_logger("idle_conversation_starter") + class IdleConversationStarter: """长时间无对话主动发起对话的组件 @@ -239,7 +240,7 @@ class IdleConversationStarter: # 在函数内部导入PFCManager,避免循环导入 from ..pfc_manager import PFCManager - + # 获取当前实例 - 注意这是同步方法,不需要await pfc_manager = PFCManager.get_instance() diff --git a/src/plugins/PFC/chat_observer.py b/src/plugins/PFC/chat_observer.py index 4fede3d2..0a4352e1 100644 --- a/src/plugins/PFC/chat_observer.py +++ b/src/plugins/PFC/chat_observer.py @@ -114,11 +114,11 @@ class ChatObserver: logger.debug( f"[私聊][{self.private_name}] 消息已添加到 ChatObserver 缓存,当前缓存大小: {len(self.message_cache)}" ) - + # 检查是否用户发送的消息(而非机器人自己) try: from .PFC_idle.idle_chat import IdleChat - + # 获取消息的发送者 user_info = message.get("user_info", {}) if user_info and str(user_info.get("user_id")) != str(global_config.BOT_QQ): diff --git a/src/plugins/PFC/conversation_initializer.py b/src/plugins/PFC/conversation_initializer.py index ec7e47a5..f5303341 100644 --- a/src/plugins/PFC/conversation_initializer.py +++ b/src/plugins/PFC/conversation_initializer.py @@ -99,10 +99,7 @@ async def load_initial_history(conversation_instance: "Conversation"): conversation_instance.chat_observer.last_message_time = ( conversation_instance.observation_info.last_message_time ) - if ( - conversation_instance.idle_chat - and conversation_instance.observation_info.last_message_time - ): + if conversation_instance.idle_chat and conversation_instance.observation_info.last_message_time: # 更新空闲计时器的起始时间 await conversation_instance.idle_chat.update_last_message_time( conversation_instance.observation_info.last_message_time diff --git a/src/plugins/PFC/conversation_loop.py b/src/plugins/PFC/conversation_loop.py index c4575735..4b65e99f 100644 --- a/src/plugins/PFC/conversation_loop.py +++ b/src/plugins/PFC/conversation_loop.py @@ -69,10 +69,7 @@ async def run_conversation_loop(conversation_instance: "Conversation"): conversation_instance.ignore_until_timestamp and loop_iter_start_time < conversation_instance.ignore_until_timestamp ): - if ( - conversation_instance.idle_chat - and conversation_instance.idle_chat._running - ): + if conversation_instance.idle_chat and conversation_instance.idle_chat._running: # 不直接停止服务,改为暂时忽略此用户 # 虽然我们仍然可以通过active_instances_count来决定是否触发主动聊天 # 但为了安全起见,我们只记录一个日志 From fd64e8516d7965ae3a11640680ad4ef9ab7f28b7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 8 May 2025 11:33:01 +0000 Subject: [PATCH 54/63] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=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/plugins/PFC/PFC_idle/__init__.py | 12 +- src/plugins/PFC/PFC_idle/idle_chat.py | 243 +++++++++--------- src/plugins/PFC/PFC_idle/idle_chat_manager.py | 41 +-- src/plugins/PFC/PFC_idle/idle_conversation.py | 154 +++++------ .../PFC/PFC_idle/idle_conversation_starter.py | 3 +- src/plugins/PFC/chat_observer.py | 4 +- src/plugins/PFC/conversation_initializer.py | 5 +- src/plugins/PFC/conversation_loop.py | 5 +- 8 files changed, 237 insertions(+), 230 deletions(-) diff --git a/src/plugins/PFC/PFC_idle/__init__.py b/src/plugins/PFC/PFC_idle/__init__.py index a06c1834..77d90210 100644 --- a/src/plugins/PFC/PFC_idle/__init__.py +++ b/src/plugins/PFC/PFC_idle/__init__.py @@ -12,9 +12,9 @@ from .idle_chat_manager import IdleChatManager from .idle_conversation import IdleConversation, get_idle_conversation_instance, initialize_idle_conversation __all__ = [ - 'IdleChat', - 'IdleChatManager', - 'IdleConversation', - 'get_idle_conversation_instance', - 'initialize_idle_conversation' -] \ No newline at end of file + "IdleChat", + "IdleChatManager", + "IdleConversation", + "get_idle_conversation_instance", + "initialize_idle_conversation", +] diff --git a/src/plugins/PFC/PFC_idle/idle_chat.py b/src/plugins/PFC/PFC_idle/idle_chat.py index 6a01e4c4..2962d52c 100644 --- a/src/plugins/PFC/PFC_idle/idle_chat.py +++ b/src/plugins/PFC/PFC_idle/idle_chat.py @@ -22,32 +22,33 @@ install(extra_lines=3) logger = get_logger("pfc_idle_chat") + class IdleChat: """主动聊天组件(测试中) - + 在以下条件都满足时触发主动聊天: 1. 当前没有任何活跃的对话实例 2. 在指定的活动时间内(7:00-23:00) 3. 根据关系值动态调整触发概率 4. 上次触发后已经过了足够的冷却时间 """ - + # 单例模式实现 - _instances: Dict[str, 'IdleChat'] = {} - + _instances: Dict[str, "IdleChat"] = {} + # 全局共享状态,用于跟踪未回复的用户 _pending_replies: Dict[str, float] = {} # 用户名 -> 发送时间 _tried_users: Set[str] = set() # 已尝试过的用户集合 _global_lock = asyncio.Lock() # 保护共享状态的全局锁 - + @classmethod - def get_instance(cls, stream_id: str, private_name: str) -> 'IdleChat': + def get_instance(cls, stream_id: str, private_name: str) -> "IdleChat": """获取IdleChat实例(单例模式) - + Args: stream_id: 聊天流ID private_name: 私聊用户名称 - + Returns: IdleChat: IdleChat实例 """ @@ -58,13 +59,13 @@ class IdleChat: cls._instances[key].start() logger.info(f"[私聊][{private_name}]创建新的IdleChat实例并启动") return cls._instances[key] - + @classmethod async def register_user_response(cls, private_name: str) -> None: """注册用户已回复 - + 当用户回复消息时调用此方法,将用户从待回复列表中移除 - + Args: private_name: 私聊用户名称 """ @@ -72,39 +73,39 @@ class IdleChat: if private_name in cls._pending_replies: del cls._pending_replies[private_name] logger.info(f"[私聊][{private_name}]已回复主动聊天消息,从待回复列表中移除") - + @classmethod async def get_next_available_user(cls) -> Optional[str]: """获取下一个可用于主动聊天的用户 - + 优先选择未尝试过的用户,其次是已尝试但超时未回复的用户 - + Returns: Optional[str]: 下一个可用的用户名,如果没有则返回None """ async with cls._global_lock: current_time = time.time() timeout_threshold = 7200 # 2小时未回复视为超时 - + # 清理超时未回复的用户 for user, send_time in list(cls._pending_replies.items()): if current_time - send_time > timeout_threshold: logger.info(f"[私聊][{user}]超过{timeout_threshold}秒未回复,标记为超时") del cls._pending_replies[user] - + # 获取所有实例中的用户 all_users = set() for key in cls._instances: - user = key.split(':', 1)[0] + user = key.split(":", 1)[0] all_users.add(user) - + # 优先选择未尝试过的用户 untried_users = all_users - cls._tried_users if untried_users: next_user = random.choice(list(untried_users)) cls._tried_users.add(next_user) return next_user - + # 如果所有用户都已尝试过,重置尝试集合,从头开始 if len(cls._tried_users) >= len(all_users): cls._tried_users.clear() @@ -115,9 +116,9 @@ class IdleChat: next_user = random.choice(list(available_users)) cls._tried_users.add(next_user) return next_user - + return None - + def __init__(self, stream_id: str, private_name: str): """初始化主动聊天组件 @@ -129,99 +130,95 @@ class IdleChat: self.private_name = private_name self.chat_observer = ChatObserver.get_instance(stream_id, private_name) self.message_sender = DirectMessageSender(private_name) - + # 添加异步锁,保护对共享变量的访问 self._lock: asyncio.Lock = asyncio.Lock() - + # LLM请求对象,用于生成主动对话内容 - self.llm = LLMRequest( - model=global_config.llm_normal, - temperature=0.5, - max_tokens=500, - request_type="idle_chat" - ) - + self.llm = LLMRequest(model=global_config.llm_normal, temperature=0.5, max_tokens=500, request_type="idle_chat") + # 工作状态 self.active_instances_count: int = 0 self.last_trigger_time: float = time.time() - 1500 # 初始化时减少等待时间 self._running: bool = False self._task: Optional[asyncio.Task] = None - + # 配置参数 - 从global_config加载 - self.min_cooldown = getattr(global_config,"MIN_IDLE_TIME", 7200) # 最短冷却时间(默认2小时)建议修改长一点,你也不希望你的bot一直骚扰你吧 + self.min_cooldown = getattr( + global_config, "MIN_IDLE_TIME", 7200 + ) # 最短冷却时间(默认2小时)建议修改长一点,你也不希望你的bot一直骚扰你吧 self.max_cooldown = getattr(global_config, "MAX_IDLE_TIME", 14400) # 最长冷却时间(默认4小时) - self.min_idle_time = getattr(global_config, "MIN_IDLE_TIME", 3600) + self.min_idle_time = getattr(global_config, "MIN_IDLE_TIME", 3600) self.check_interval = getattr(global_config, "IDLE_CHECK_INTERVAL", 600) # 检查间隔(默认10分钟) self.active_hours_start = 6 # 活动开始时间 - self.active_hours_end = 24 # 活动结束时间 - + self.active_hours_end = 24 # 活动结束时间 + # 关系值相关 self.base_trigger_probability = 0.3 # 基础触发概率 - self.relationship_factor = 0.0003 # 关系值影响因子 - + self.relationship_factor = 0.0003 # 关系值影响因子 + def start(self) -> None: """启动主动聊天检测""" # 检查是否启用了主动聊天功能 if not getattr(global_config, "ENABLE_IDLE_CONVERSATION", False): logger.info(f"[私聊][{self.private_name}]主动聊天功能已禁用(配置ENABLE_IDLE_CONVERSATION=False)") return - + if self._running: logger.debug(f"[私聊][{self.private_name}]主动聊天功能已在运行中") return - + self._running = True self._task = asyncio.create_task(self._check_idle_loop()) logger.info(f"[私聊][{self.private_name}]启动主动聊天检测") - + def stop(self) -> None: - """停止主动聊天检测 - """ + """停止主动聊天检测""" if not self._running: return - + self._running = False if self._task: self._task.cancel() self._task = None logger.info(f"[私聊][{self.private_name}]停止主动聊天检测") - + async def increment_active_instances(self) -> None: """增加活跃实例计数 - + 当创建新的对话实例时调用此方法 """ async with self._lock: self.active_instances_count += 1 logger.debug(f"[私聊][{self.private_name}]活跃实例数+1,当前:{self.active_instances_count}") - + async def decrement_active_instances(self) -> None: """减少活跃实例计数 - + 当对话实例结束时调用此方法 """ async with self._lock: self.active_instances_count = max(0, self.active_instances_count - 1) logger.debug(f"[私聊][{self.private_name}]活跃实例数-1,当前:{self.active_instances_count}") - + async def update_last_message_time(self, message_time: Optional[float] = None) -> None: """更新最后一条消息的时间 - + Args: message_time: 消息时间戳,如果为None则使用当前时间 """ async with self._lock: self.last_trigger_time = message_time or time.time() logger.debug(f"[私聊][{self.private_name}]更新最后消息时间: {self.last_trigger_time:.2f}") - + # 当用户发送消息时,也应该注册响应 await self.__class__.register_user_response(self.private_name) - + def _is_active_hours(self) -> bool: """检查是否在活动时间内""" current_hour = datetime.now().hour return self.active_hours_start <= current_hour < self.active_hours_end - + async def _should_trigger(self) -> bool: """检查是否应该触发主动聊天""" async with self._lock: @@ -229,25 +226,27 @@ class IdleChat: if self.active_instances_count < 0: logger.warning(f"[私聊][{self.private_name}]检测到活跃实例数为负数,重置为0") self.active_instances_count = 0 - + # 检查是否有活跃实例 if self.active_instances_count > 0: logger.debug(f"[私聊][{self.private_name}]存在活跃实例({self.active_instances_count}),不触发主动聊天") return False - + # 检查是否在活动时间内 if not self._is_active_hours(): logger.debug(f"[私聊][{self.private_name}]不在活动时间内,不触发主动聊天") return False - + # 检查冷却时间 current_time = time.time() time_since_last_trigger = current_time - self.last_trigger_time if time_since_last_trigger < self.min_cooldown: time_left = self.min_cooldown - time_since_last_trigger - logger.debug(f"[私聊][{self.private_name}]冷却时间未到(已过{time_since_last_trigger:.0f}秒/需要{self.min_cooldown}秒),还需等待{time_left:.0f}秒,不触发主动聊天") + logger.debug( + f"[私聊][{self.private_name}]冷却时间未到(已过{time_since_last_trigger:.0f}秒/需要{self.min_cooldown}秒),还需等待{time_left:.0f}秒,不触发主动聊天" + ) return False - + # 强制触发检查 - 如果超过最大冷却时间,增加触发概率 force_trigger = False if time_since_last_trigger > self.max_cooldown * 2: # 如果超过最大冷却时间的两倍 @@ -255,9 +254,11 @@ class IdleChat: random_force = random.random() force_trigger = random_force < force_probability if force_trigger: - logger.info(f"[私聊][{self.private_name}]超过最大冷却时间({time_since_last_trigger:.0f}秒),强制触发主动聊天") + logger.info( + f"[私聊][{self.private_name}]超过最大冷却时间({time_since_last_trigger:.0f}秒),强制触发主动聊天" + ) return True - + # 获取关系值 relationship_value = 0 try: @@ -270,10 +271,10 @@ class IdleChat: # 先尝试通过昵称获取person_id platform = "qq" # 默认平台 person_id = person_info_manager.get_person_id(platform, self.private_name) - + # 如果通过昵称获取失败,尝试通过stream_id解析 if not person_id: - parts = self.stream_id.split('_') + parts = self.stream_id.split("_") if len(parts) >= 2 and parts[0] == "private": user_id = parts[1] platform = parts[2] if len(parts) >= 3 else "qq" @@ -284,7 +285,7 @@ class IdleChat: person_id = person_info_manager.get_person_id(platform, user_id) except Exception as e2: logger.warning(f"[私聊][{self.private_name}]尝试获取person_id失败: {str(e2)}") - + # 获取关系值 if person_id: raw_value = await person_info_manager.get_value(person_id, "relationship_value") @@ -292,51 +293,61 @@ class IdleChat: logger.debug(f"[私聊][{self.private_name}]成功获取关系值: {relationship_value}") else: logger.warning(f"[私聊][{self.private_name}]无法获取person_id,使用默认关系值0") - + # 使用PfcRepationshipTranslator获取关系描述 relationship_translator = PfcRepationshipTranslator(self.private_name) - relationship_level = relationship_translator._calculate_relationship_level_num(relationship_value, self.private_name) - + relationship_level = relationship_translator._calculate_relationship_level_num( + relationship_value, self.private_name + ) + # 基于关系等级调整触发概率 # 关系越好,主动聊天概率越高 level_probability_factors = [0.05, 0.1, 0.2, 0.3, 0.4, 0.5] # 每个等级对应的基础概率因子 base_probability = level_probability_factors[relationship_level] - + # 基础概率因子 trigger_probability = base_probability trigger_probability = max(0.05, min(0.6, trigger_probability)) # 限制在0.05-0.6之间 - + # 最大冷却时间调整 - 随着冷却时间增加,逐渐增加触发概率 if time_since_last_trigger > self.max_cooldown: # 计算额外概率 - 每超过最大冷却时间的10%,增加1%的概率,最多增加30% - extra_time_factor = min(0.3, (time_since_last_trigger - self.max_cooldown) / (self.max_cooldown * 10)) + extra_time_factor = min( + 0.3, (time_since_last_trigger - self.max_cooldown) / (self.max_cooldown * 10) + ) trigger_probability += extra_time_factor logger.debug(f"[私聊][{self.private_name}]超过标准冷却时间,额外增加概率: +{extra_time_factor:.2f}") - + # 随机判断是否触发 random_value = random.random() should_trigger = random_value < trigger_probability - logger.debug(f"[私聊][{self.private_name}]触发概率计算: 基础({base_probability:.2f}) + 关系值({relationship_value})影响 = {trigger_probability:.2f},随机值={random_value:.2f}, 结果={should_trigger}") - + logger.debug( + f"[私聊][{self.private_name}]触发概率计算: 基础({base_probability:.2f}) + 关系值({relationship_value})影响 = {trigger_probability:.2f},随机值={random_value:.2f}, 结果={should_trigger}" + ) + # 如果决定触发,记录详细日志 if should_trigger: - logger.info(f"[私聊][{self.private_name}]决定触发主动聊天: 触发概率={trigger_probability:.2f}, 距上次已过{time_since_last_trigger:.0f}秒") - + logger.info( + f"[私聊][{self.private_name}]决定触发主动聊天: 触发概率={trigger_probability:.2f}, 距上次已过{time_since_last_trigger:.0f}秒" + ) + return should_trigger - + except Exception as e: logger.error(f"[私聊][{self.private_name}]获取关系值失败: {str(e)}") logger.error(traceback.format_exc()) - + # 即使获取关系值失败,仍有一个基础的几率触发 # 这确保即使数据库有问题,主动聊天功能仍然可用 base_fallback_probability = 0.1 # 较低的基础几率 random_fallback = random.random() fallback_trigger = random_fallback < base_fallback_probability if fallback_trigger: - logger.info(f"[私聊][{self.private_name}]获取关系值失败,使用后备触发机制: 概率={base_fallback_probability:.2f}, 决定={fallback_trigger}") + logger.info( + f"[私聊][{self.private_name}]获取关系值失败,使用后备触发机制: 概率={base_fallback_probability:.2f}, 决定={fallback_trigger}" + ) return fallback_trigger - + async def _check_idle_loop(self) -> None: """检查空闲状态的循环""" try: @@ -346,16 +357,16 @@ class IdleChat: # 如果禁用了功能,等待一段时间后再次检查配置 await asyncio.sleep(60) # 每分钟检查一次配置变更 continue - + # 检查当前用户是否应该触发主动聊天 should_trigger = await self._should_trigger() - + # 如果当前用户不触发,检查是否有其他用户已经超时未回复 if not should_trigger: async with self.__class__._global_lock: current_time = time.time() pending_timeout = 1800 # 30分钟未回复检查 - + # 检查此用户是否在等待回复列表中 if self.private_name in self.__class__._pending_replies: logger.debug(f"[私聊][{self.private_name}]当前用户在等待回复列表中,不进行额外检查") @@ -365,12 +376,12 @@ class IdleChat: for user, send_time in self.__class__._pending_replies.items(): if current_time - send_time > pending_timeout: timed_out_users.append(user) - + # 如果有超时未回复的用户,尝试找下一个用户 if timed_out_users: logger.info(f"[私聊]发现{len(timed_out_users)}个用户超过{pending_timeout}秒未回复") next_user = await self.__class__.get_next_available_user() - + if next_user and next_user != self.private_name: logger.info(f"[私聊]选择下一个用户[{next_user}]进行主动聊天") # 查找该用户的实例并触发聊天 @@ -380,7 +391,7 @@ class IdleChat: # 触发该实例的主动聊天 asyncio.create_task(instance._initiate_chat()) break - + # 如果当前用户应该触发主动聊天 if should_trigger: try: @@ -388,7 +399,7 @@ class IdleChat: # 更新上次触发时间 async with self._lock: self.last_trigger_time = time.time() - + # 将此用户添加到等待回复列表中 async with self.__class__._global_lock: self.__class__._pending_replies[self.private_name] = time.time() @@ -397,12 +408,12 @@ class IdleChat: except Exception as e: logger.error(f"[私聊][{self.private_name}]执行主动聊天过程出错: {str(e)}") logger.error(traceback.format_exc()) - + # 等待下一次检查 check_interval = self.check_interval # 使用配置的检查间隔 logger.debug(f"[私聊][{self.private_name}]等待{check_interval}秒后进行下一次主动聊天检查") await asyncio.sleep(check_interval) - + except asyncio.CancelledError: logger.debug(f"[私聊][{self.private_name}]主动聊天检测任务被取消") except Exception as e: @@ -412,24 +423,25 @@ class IdleChat: if self._running: logger.info(f"[私聊][{self.private_name}]尝试重新启动主动聊天检测") self._task = asyncio.create_task(self._check_idle_loop()) - + async def _get_chat_stream(self) -> Optional[ChatStream]: """获取聊天流实例""" try: # 尝试从全局聊天管理器获取现有的聊天流 from src.plugins.chat.chat_stream import chat_manager + existing_chat_stream = chat_manager.get_stream(self.stream_id) if existing_chat_stream: logger.debug(f"[私聊][{self.private_name}]从chat_manager找到现有聊天流") return existing_chat_stream - + # 如果没有现有聊天流,则创建新的 logger.debug(f"[私聊][{self.private_name}]未找到现有聊天流,创建新聊天流") # 创建用户信息对象 user_info = UserInfo( user_id=self.private_name, # 使用私聊用户的ID user_nickname=self.private_name, # 使用私聊用户的名称 - platform="qq" + platform="qq", ) # 创建聊天流 new_stream = ChatStream(self.stream_id, "qq", user_info) @@ -441,23 +453,19 @@ class IdleChat: logger.error(f"[私聊][{self.private_name}]创建/获取聊天流失败: {str(e)}") logger.error(traceback.format_exc()) return None - + async def _initiate_chat(self) -> None: """生成并发送主动聊天消息""" try: # 获取聊天历史记录 messages = self.chat_observer.get_cached_messages(limit=12) chat_history_text = await build_readable_messages( - messages, - replace_bot_name=True, - merge_messages=False, - timestamp_mode="relative", - read_mark=0.0 + messages, replace_bot_name=True, merge_messages=False, timestamp_mode="relative", read_mark=0.0 ) - + # 获取关系信息 from src.plugins.person_info.relationship_manager import relationship_manager - + # 获取关系值 relationship_value = 0 try: @@ -468,23 +476,25 @@ class IdleChat: relationship_value = relationship_manager.ensure_float(raw_value, person_id) except Exception as e: logger.warning(f"[私聊][{self.private_name}]获取关系值失败,使用默认值: {e}") - + # 使用PfcRepationshipTranslator获取关系描述 relationship_translator = PfcRepationshipTranslator(self.private_name) - full_relationship_text = await relationship_translator.translate_relationship_value_to_text(relationship_value) - + full_relationship_text = await relationship_translator.translate_relationship_value_to_text( + relationship_value + ) + # 提取纯关系描述(去掉"你们的关系是:"前缀) relationship_description = "普通" # 默认值 if ":" in full_relationship_text: relationship_description = full_relationship_text.split(":")[1].replace("。", "") - + if global_config.ENABLE_SCHEDULE_GEN: schedule_prompt = await global_prompt_manager.format_prompt( - "schedule_prompt", schedule_info=bot_schedule.get_current_num_task(num=1, time_info=False) - ) + "schedule_prompt", schedule_info=bot_schedule.get_current_num_task(num=1, time_info=False) + ) else: schedule_prompt = "" - + # 构建提示词 current_time = datetime.now().strftime("%H:%M") prompt = f"""你是{global_config.BOT_NICKNAME}。 @@ -500,14 +510,11 @@ class IdleChat: 请直接输出一条消息,不要有任何额外的解释或引导文字 消息内容尽量简短 """ - + # 生成回复 logger.debug(f"[私聊][{self.private_name}]开始生成主动聊天内容") try: - content, _ = await asyncio.wait_for( - self.llm.generate_response_async(prompt), - timeout=30 - ) + content, _ = await asyncio.wait_for(self.llm.generate_response_async(prompt), timeout=30) logger.debug(f"[私聊][{self.private_name}]成功生成主动聊天内容: {content}") except asyncio.TimeoutError: logger.error(f"[私聊][{self.private_name}]生成主动聊天内容超时") @@ -516,34 +523,30 @@ class IdleChat: logger.error(f"[私聊][{self.private_name}]生成主动聊天内容失败: {str(llm_err)}") logger.error(traceback.format_exc()) return - + # 清理结果 content = content.strip() content = content.strip("\"'") - + if not content: logger.error(f"[私聊][{self.private_name}]生成的主动聊天内容为空") return - + # 获取聊天流 chat_stream = await self._get_chat_stream() if not chat_stream: logger.error(f"[私聊][{self.private_name}]无法获取有效的聊天流,取消发送主动消息") return - + # 发送消息 try: 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, content=content, reply_to_message=None) logger.info(f"[私聊][{self.private_name}]成功主动发起聊天: {content}") except Exception as e: logger.error(f"[私聊][{self.private_name}]发送主动聊天消息失败: {str(e)}") logger.error(traceback.format_exc()) - + except Exception as e: logger.error(f"[私聊][{self.private_name}]主动发起聊天过程中发生未预期的错误: {str(e)}") - logger.error(traceback.format_exc()) \ No newline at end of file + logger.error(traceback.format_exc()) diff --git a/src/plugins/PFC/PFC_idle/idle_chat_manager.py b/src/plugins/PFC/PFC_idle/idle_chat_manager.py index 1218c109..a8708331 100644 --- a/src/plugins/PFC/PFC_idle/idle_chat_manager.py +++ b/src/plugins/PFC/PFC_idle/idle_chat_manager.py @@ -6,21 +6,22 @@ import traceback logger = get_logger("pfc_idle_chat_manager") + class IdleChatManager: """空闲聊天管理器 - + 用于管理所有私聊用户的空闲聊天实例。 采用单例模式,确保全局只有一个管理器实例。 """ - + _instance: Optional["IdleChatManager"] = None _lock: asyncio.Lock = asyncio.Lock() - + def __init__(self): """初始化空闲聊天管理器""" self._idle_chats: Dict[str, IdleChat] = {} # stream_id -> IdleChat self._active_conversations_count: Dict[str, int] = {} # stream_id -> count - + @classmethod def get_instance(cls) -> "IdleChatManager": """获取管理器单例 (同步版本) @@ -32,7 +33,7 @@ class IdleChatManager: # 在同步环境中创建实例 cls._instance = cls() return cls._instance - + @classmethod async def get_instance_async(cls) -> "IdleChatManager": """获取管理器单例 (异步版本) @@ -45,7 +46,7 @@ class IdleChatManager: if not cls._instance: cls._instance = cls() return cls._instance - + async def get_or_create_idle_chat(self, stream_id: str, private_name: str) -> IdleChat: """获取或创建空闲聊天实例 @@ -65,7 +66,7 @@ class IdleChatManager: idle_chat.start() # 启动空闲检测 logger.info(f"[私聊][{private_name}]创建并启动新的空闲聊天实例") return self._idle_chats[stream_id] - + async def remove_idle_chat(self, stream_id: str) -> None: """移除空闲聊天实例 @@ -79,7 +80,7 @@ class IdleChatManager: if stream_id in self._active_conversations_count: del self._active_conversations_count[stream_id] logger.info(f"[私聊][{idle_chat.private_name}]移除空闲聊天实例") - + async def notify_conversation_start(self, stream_id: str) -> None: """通知对话开始 @@ -96,15 +97,15 @@ class IdleChatManager: if len(parts) >= 2: private_name = parts[1] # 取第二部分作为名称 await self.get_or_create_idle_chat(stream_id, private_name) - + if stream_id not in self._active_conversations_count: self._active_conversations_count[stream_id] = 0 - + # 增加计数前记录当前值,用于日志 old_count = self._active_conversations_count[stream_id] self._active_conversations_count[stream_id] += 1 new_count = self._active_conversations_count[stream_id] - + # 确保IdleChat实例存在 idle_chat = self._idle_chats.get(stream_id) if idle_chat: @@ -115,7 +116,7 @@ class IdleChatManager: except Exception as e: logger.error(f"对话开始通知处理失败: {stream_id}, 错误: {e}") logger.error(traceback.format_exc()) - + async def notify_conversation_end(self, stream_id: str) -> None: """通知对话结束 @@ -125,16 +126,16 @@ class IdleChatManager: try: # 记录当前计数用于日志 old_count = self._active_conversations_count.get(stream_id, 0) - + # 安全减少计数,避免负数 if stream_id in self._active_conversations_count and self._active_conversations_count[stream_id] > 0: self._active_conversations_count[stream_id] -= 1 else: # 如果计数已经为0或不存在,设置为0 self._active_conversations_count[stream_id] = 0 - + new_count = self._active_conversations_count.get(stream_id, 0) - + # 确保IdleChat实例存在 idle_chat = self._idle_chats.get(stream_id) if idle_chat: @@ -142,7 +143,7 @@ class IdleChatManager: logger.debug(f"对话结束通知: {stream_id}, 计数从{old_count}减少到{new_count}") else: logger.warning(f"对话结束通知: {stream_id}, 计数减少但IdleChat不存在! 计数:{old_count}->{new_count}") - + # 检查是否所有对话都结束了,帮助调试 all_counts = sum(self._active_conversations_count.values()) if all_counts == 0: @@ -150,7 +151,7 @@ class IdleChatManager: except Exception as e: logger.error(f"对话结束通知处理失败: {stream_id}, 错误: {e}") logger.error(traceback.format_exc()) - + def get_idle_chat(self, stream_id: str) -> Optional[IdleChat]: """获取空闲聊天实例 @@ -161,7 +162,7 @@ class IdleChatManager: Optional[IdleChat]: 空闲聊天实例,如果不存在则返回None """ return self._idle_chats.get(stream_id) - + def get_active_conversations_count(self, stream_id: str) -> int: """获取指定流的活跃对话计数 @@ -172,11 +173,11 @@ class IdleChatManager: int: 活跃对话计数 """ return self._active_conversations_count.get(stream_id, 0) - + def get_all_active_conversations_count(self) -> int: """获取所有活跃对话总计数 Returns: int: 活跃对话总计数 """ - return sum(self._active_conversations_count.values()) \ No newline at end of file + return sum(self._active_conversations_count.values()) diff --git a/src/plugins/PFC/PFC_idle/idle_conversation.py b/src/plugins/PFC/PFC_idle/idle_conversation.py index 68e5a240..90036d13 100644 --- a/src/plugins/PFC/PFC_idle/idle_conversation.py +++ b/src/plugins/PFC/PFC_idle/idle_conversation.py @@ -6,6 +6,7 @@ import time logger = get_logger("pfc_idle_conversation") + class IdleConversation: """ 处理Idle聊天相关的功能,将这些功能从主Conversation类中分离出来, @@ -27,17 +28,18 @@ class IdleConversation: if self._initialization_in_progress: logger.debug("IdleConversation正在初始化中,等待完成") return False - + if self._idle_chat_manager is not None: logger.debug("IdleConversation已初始化,无需重复操作") return True - + # 标记开始初始化 self._initialization_in_progress = True - + try: # 从PFCManager获取IdleChatManager实例 from ..pfc_manager import PFCManager + pfc_manager = PFCManager.get_instance() self._idle_chat_manager = pfc_manager.get_idle_chat_manager() logger.debug("IdleConversation初始化完成,已获取IdleChatManager实例") @@ -49,19 +51,19 @@ class IdleConversation: finally: # 无论成功或失败,都清除初始化标志 self._initialization_in_progress = False - + async def start(self): """启动IdleConversation,创建后台监控任务""" if self._running: logger.debug("IdleConversation已经在运行") return False - + if not self._idle_chat_manager: success = await self.initialize() if not success: logger.error("无法启动IdleConversation:初始化失败") return False - + try: self._running = True # 创建后台监控任务,使用try-except块来捕获可能的异常 @@ -76,19 +78,19 @@ class IdleConversation: # 如果没有活跃的事件循环,记录警告但继续执行 logger.warning("没有活跃的事件循环,IdleConversation将不会启动监控任务") # 尽管没有监控任务,但仍然将running设为True表示IdleConversation已启动 - + return True except Exception as e: self._running = False logger.error(f"启动IdleConversation失败: {e}") logger.error(traceback.format_exc()) return False - + async def stop(self): """停止IdleConversation的后台任务""" if not self._running: return - + self._running = False if self._monitor_task and not self._monitor_task.done(): try: @@ -102,10 +104,10 @@ class IdleConversation: except Exception as e: logger.error(f"停止IdleConversation监控任务时出错: {e}") logger.error(traceback.format_exc()) - + self._monitor_task = None logger.info("IdleConversation已停止") - + async def _monitor_loop(self): """后台监控循环,定期检查活跃的会话并执行必要的操作""" try: @@ -114,15 +116,15 @@ class IdleConversation: # 同步活跃流计数到IdleChatManager if self._idle_chat_manager: await self._sync_active_streams_to_manager() - + # 这里可以添加定期检查逻辑,如查询空闲状态等 active_count = len(self._active_streams) logger.debug(f"IdleConversation监控中,当前活跃流数量: {active_count}") - + except Exception as e: logger.error(f"IdleConversation监控循环出错: {e}") logger.error(traceback.format_exc()) - + # 每30秒执行一次监控 await asyncio.sleep(30) except asyncio.CancelledError: @@ -131,29 +133,29 @@ class IdleConversation: logger.error(f"IdleConversation监控任务异常退出: {e}") logger.error(traceback.format_exc()) self._running = False - + async def _sync_active_streams_to_manager(self): """同步活跃流计数到IdleChatManager和IdleChat""" try: if not self._idle_chat_manager: return - + # 获取当前的活跃流列表 async with self._lock: active_streams = list(self._active_streams.keys()) - + # 对每个活跃流,确保IdleChatManager和IdleChat中的计数是正确的 for stream_id in active_streams: # 获取当前IdleChatManager中的计数 manager_count = self._idle_chat_manager.get_active_conversations_count(stream_id) - + # 由于我们的活跃流字典只记录是否活跃(值为True),所以计数应该是1 if manager_count != 1: # 修正IdleChatManager中的计数 old_count = manager_count self._idle_chat_manager._active_conversations_count[stream_id] = 1 logger.warning(f"同步调整IdleChatManager中的计数: stream_id={stream_id}, {old_count}->1") - + # 同时修正IdleChat中的计数 idle_chat = self._idle_chat_manager.get_idle_chat(stream_id) if idle_chat: @@ -161,26 +163,26 @@ class IdleConversation: old_count = getattr(idle_chat, "active_instances_count", 0) idle_chat.active_instances_count = 1 logger.warning(f"同步调整IdleChat中的计数: stream_id={stream_id}, {old_count}->1") - + # 检查IdleChatManager中有没有多余的计数(conversation中已不存在但manager中还有) for stream_id, count in list(self._idle_chat_manager._active_conversations_count.items()): if count > 0 and stream_id not in active_streams: # 重置为0 self._idle_chat_manager._active_conversations_count[stream_id] = 0 logger.warning(f"重置IdleChatManager中的多余计数: stream_id={stream_id}, {count}->0") - + # 同时修正IdleChat中的计数 idle_chat = self._idle_chat_manager.get_idle_chat(stream_id) if idle_chat and getattr(idle_chat, "active_instances_count", 0) > 0: old_count = getattr(idle_chat, "active_instances_count", 0) idle_chat.active_instances_count = 0 logger.warning(f"同步重置IdleChat中的计数: stream_id={stream_id}, {old_count}->0") - + # 日志记录同步结果 total_active = len(active_streams) total_manager = sum(self._idle_chat_manager._active_conversations_count.values()) logger.debug(f"同步后的计数: IdleConversation活跃流={total_active}, IdleChatManager总计数={total_manager}") - + except Exception as e: logger.error(f"同步活跃流计数失败: {e}") logger.error(traceback.format_exc()) @@ -188,25 +190,25 @@ class IdleConversation: async def get_or_create_idle_chat(self, stream_id: str, private_name: str): """ 获取或创建IdleChat实例 - + Args: stream_id: 聊天流ID private_name: 私聊对象名称,用于日志 - + Returns: bool: 操作是否成功 """ # 确保IdleConversation已启动 if not self._running: await self.start() - + if not self._idle_chat_manager: # 如果尚未初始化,尝试初始化 success = await self.initialize() if not success: logger.warning(f"[私聊][{private_name}] 获取或创建IdleChat失败:IdleChatManager未初始化") return False - + try: # 创建IdleChat实例 _idle_chat = await self._idle_chat_manager.get_or_create_idle_chat(stream_id, private_name) @@ -220,11 +222,11 @@ class IdleConversation: async def notify_conversation_start(self, stream_id: str, private_name: str) -> bool: """ 通知空闲聊天管理器对话开始 - + Args: stream_id: 聊天流ID private_name: 私聊对象名称,用于日志 - + Returns: bool: 通知是否成功 """ @@ -235,32 +237,32 @@ class IdleConversation: if not success: logger.warning(f"[私聊][{private_name}] 启动IdleConversation失败,无法通知对话开始") return False - + if not self._idle_chat_manager: # 如果尚未初始化,尝试初始化 success = await self.initialize() if not success: logger.warning(f"[私聊][{private_name}] 通知对话开始失败:IdleChatManager未初始化") return False - + try: # 确保IdleChat实例已创建 - 这是关键步骤,要先创建IdleChat await self.get_or_create_idle_chat(stream_id, private_name) - + # 先记录活跃状态 - 这是权威源 async with self._lock: self._active_streams[stream_id] = True - + # 然后同步到IdleChatManager if self._idle_chat_manager: await self._idle_chat_manager.notify_conversation_start(stream_id) logger.info(f"[私聊][{private_name}] 已通知空闲聊天管理器对话开始") else: logger.warning(f"[私聊][{private_name}] IdleChatManager不存在,但已记录活跃状态") - + # 立即进行一次同步,确保数据一致性 await self._sync_active_streams_to_manager() - + return True except Exception as e: logger.warning(f"[私聊][{private_name}] 通知空闲聊天管理器对话开始失败: {e}") @@ -277,11 +279,11 @@ class IdleConversation: async def notify_conversation_end(self, stream_id: str, private_name: str) -> bool: """ 通知空闲聊天管理器对话结束 - + Args: stream_id: 聊天流ID private_name: 私聊对象名称,用于日志 - + Returns: bool: 通知是否成功 """ @@ -293,30 +295,30 @@ class IdleConversation: del self._active_streams[stream_id] was_active = True logger.debug(f"[私聊][{private_name}] 已从活跃流中移除 {stream_id}") - + if not self._idle_chat_manager: # 如果尚未初始化,尝试初始化 success = await self.initialize() if not success: logger.warning(f"[私聊][{private_name}] 通知对话结束失败:IdleChatManager未初始化") return False - + try: # 然后同步到IdleChatManager if self._idle_chat_manager: # 无论如何都尝试通知 await self._idle_chat_manager.notify_conversation_end(stream_id) - + # 立即进行一次同步,确保数据一致性 await self._sync_active_streams_to_manager() - + logger.info(f"[私聊][{private_name}] 已通知空闲聊天管理器对话结束") - + # 检查当前活跃流数量 active_count = len(self._active_streams) if active_count == 0: logger.info(f"[私聊][{private_name}] 当前无活跃流,可能会触发主动聊天") - + # 额外调用:如果实例存在且只有在确实移除了活跃流的情况下才触发检查 if was_active: idle_chat = self._idle_chat_manager.get_idle_chat(stream_id) @@ -324,7 +326,7 @@ class IdleConversation: # 直接触发IdleChat检查,而不是等待下一个循环 logger.info(f"[私聊][{private_name}] 对话结束,手动触发一次主动聊天检查") asyncio.create_task(self._trigger_idle_chat_check(idle_chat, stream_id, private_name)) - + return True else: logger.warning(f"[私聊][{private_name}] IdleChatManager不存在,但已更新活跃状态") @@ -337,14 +339,14 @@ class IdleConversation: logger.error(f"[私聊][{private_name}] 处理对话结束通知时发生严重错误: {outer_e}") logger.error(traceback.format_exc()) return False - + async def _trigger_idle_chat_check(self, idle_chat, stream_id: str, private_name: str): """在对话结束后,手动触发一次IdleChat的检查""" try: # 确保活跃计数与IdleConversation一致 async with self._lock: is_active_in_conversation = stream_id in self._active_streams - + # 强制使IdleChat的计数与IdleConversation一致 if is_active_in_conversation: # 如果在IdleConversation中是活跃的,IdleChat的计数应该是1 @@ -358,17 +360,17 @@ class IdleConversation: old_count = idle_chat.active_instances_count idle_chat.active_instances_count = 0 logger.warning(f"[私聊][{private_name}] 修正IdleChat计数: {old_count}->0") - + # 等待1秒,让任何正在进行的处理完成 await asyncio.sleep(1) - + # 只有当stream不再活跃时才触发检查 if not is_active_in_conversation: # 尝试触发一次检查 if hasattr(idle_chat, "_should_trigger"): should_trigger = await idle_chat._should_trigger() logger.info(f"[私聊][{private_name}] 手动触发主动聊天检查结果: {should_trigger}") - + # 如果应该触发,直接调用_initiate_chat if should_trigger and hasattr(idle_chat, "_initiate_chat"): logger.info(f"[私聊][{private_name}] 手动触发主动聊天") @@ -384,11 +386,11 @@ class IdleConversation: def is_stream_active(self, stream_id: str) -> bool: """检查指定的stream是否活跃""" return stream_id in self._active_streams - + def get_active_streams_count(self) -> int: """获取当前活跃的stream数量""" return len(self._active_streams) - + @property def is_running(self) -> bool: """检查IdleConversation是否正在运行""" @@ -399,39 +401,41 @@ class IdleConversation: """获取IdleChatManager实例""" return self._idle_chat_manager + # 创建单例实例 _instance: Optional[IdleConversation] = None _instance_lock = asyncio.Lock() _initialization_in_progress = False # 防止并发初始化 + async def initialize_idle_conversation() -> IdleConversation: """初始化并启动IdleConversation单例实例""" global _initialization_in_progress - + # 防止并发初始化 if _initialization_in_progress: logger.debug("IdleConversation全局初始化正在进行中,等待完成") return get_idle_conversation_instance() - + # 标记正在初始化 _initialization_in_progress = True - + try: instance = get_idle_conversation_instance() - + # 如果实例已经在运行,避免重复初始化 - if getattr(instance, '_running', False): + if getattr(instance, "_running", False): logger.debug("IdleConversation已在运行状态,无需重新初始化") _initialization_in_progress = False return instance - + # 初始化实例 success = await instance.initialize() if not success: logger.error("IdleConversation初始化失败") _initialization_in_progress = False return instance - + # 启动实例 success = await instance.start() if not success: @@ -440,10 +444,10 @@ async def initialize_idle_conversation() -> IdleConversation: # 启动成功,进行初始检查 logger.info("IdleConversation启动成功,执行初始化后检查") # 这里可以添加一些启动后的检查,如果需要 - + # 创建一个异步任务,定期检查系统状态 asyncio.create_task(periodic_system_check(instance)) - + return instance except Exception as e: logger.error(f"初始化并启动IdleConversation时出错: {e}") @@ -455,40 +459,43 @@ async def initialize_idle_conversation() -> IdleConversation: # 清除初始化标志 _initialization_in_progress = False + async def periodic_system_check(instance: IdleConversation): """定期检查系统状态,确保主动聊天功能正常工作""" try: # 等待10秒,让系统完全启动 await asyncio.sleep(10) - - while getattr(instance, '_running', False): + + while getattr(instance, "_running", False): try: # 检查活跃流数量 - active_streams_count = len(getattr(instance, '_active_streams', {})) - + active_streams_count = len(getattr(instance, "_active_streams", {})) + # 如果IdleChatManager存在,检查其中的活跃对话计数 - idle_chat_manager = getattr(instance, '_idle_chat_manager', None) - if idle_chat_manager and hasattr(idle_chat_manager, 'get_all_active_conversations_count'): + idle_chat_manager = getattr(instance, "_idle_chat_manager", None) + if idle_chat_manager and hasattr(idle_chat_manager, "get_all_active_conversations_count"): manager_count = idle_chat_manager.get_all_active_conversations_count() - + # 如果两者不一致,记录警告 if active_streams_count != manager_count: - logger.warning(f"检测到计数不一致: IdleConversation记录的活跃流数量({active_streams_count}) 与 IdleChatManager记录的活跃对话数({manager_count})不匹配") - + logger.warning( + f"检测到计数不一致: IdleConversation记录的活跃流数量({active_streams_count}) 与 IdleChatManager记录的活跃对话数({manager_count})不匹配" + ) + # 如果IdleChatManager记录的计数为0但自己的记录不为0,进行修正 if manager_count == 0 and active_streams_count > 0: logger.warning("检测到可能的计数错误,尝试修正:清空IdleConversation的活跃流记录") async with instance._lock: instance._active_streams.clear() - + # 检查计数如果为0,帮助日志输出 if active_streams_count == 0: logger.debug("当前没有活跃的对话流,应该可以触发主动聊天") - + except Exception as check_err: logger.error(f"执行系统检查时出错: {check_err}") logger.error(traceback.format_exc()) - + # 每60秒检查一次 await asyncio.sleep(60) except asyncio.CancelledError: @@ -497,9 +504,10 @@ async def periodic_system_check(instance: IdleConversation): logger.error(f"系统检查任务异常退出: {e}") logger.error(traceback.format_exc()) + def get_idle_conversation_instance() -> IdleConversation: """获取IdleConversation的单例实例""" global _instance if _instance is None: _instance = IdleConversation() - return _instance \ No newline at end of file + return _instance diff --git a/src/plugins/PFC/PFC_idle/idle_conversation_starter.py b/src/plugins/PFC/PFC_idle/idle_conversation_starter.py index e6782ab1..83a58219 100644 --- a/src/plugins/PFC/PFC_idle/idle_conversation_starter.py +++ b/src/plugins/PFC/PFC_idle/idle_conversation_starter.py @@ -27,6 +27,7 @@ install(extra_lines=3) # 获取当前模块的日志记录器 logger = get_logger("idle_conversation_starter") + class IdleConversationStarter: """长时间无对话主动发起对话的组件 @@ -239,7 +240,7 @@ class IdleConversationStarter: # 在函数内部导入PFCManager,避免循环导入 from ..pfc_manager import PFCManager - + # 获取当前实例 - 注意这是同步方法,不需要await pfc_manager = PFCManager.get_instance() diff --git a/src/plugins/PFC/chat_observer.py b/src/plugins/PFC/chat_observer.py index 4fede3d2..0a4352e1 100644 --- a/src/plugins/PFC/chat_observer.py +++ b/src/plugins/PFC/chat_observer.py @@ -114,11 +114,11 @@ class ChatObserver: logger.debug( f"[私聊][{self.private_name}] 消息已添加到 ChatObserver 缓存,当前缓存大小: {len(self.message_cache)}" ) - + # 检查是否用户发送的消息(而非机器人自己) try: from .PFC_idle.idle_chat import IdleChat - + # 获取消息的发送者 user_info = message.get("user_info", {}) if user_info and str(user_info.get("user_id")) != str(global_config.BOT_QQ): diff --git a/src/plugins/PFC/conversation_initializer.py b/src/plugins/PFC/conversation_initializer.py index ec7e47a5..f5303341 100644 --- a/src/plugins/PFC/conversation_initializer.py +++ b/src/plugins/PFC/conversation_initializer.py @@ -99,10 +99,7 @@ async def load_initial_history(conversation_instance: "Conversation"): conversation_instance.chat_observer.last_message_time = ( conversation_instance.observation_info.last_message_time ) - if ( - conversation_instance.idle_chat - and conversation_instance.observation_info.last_message_time - ): + if conversation_instance.idle_chat and conversation_instance.observation_info.last_message_time: # 更新空闲计时器的起始时间 await conversation_instance.idle_chat.update_last_message_time( conversation_instance.observation_info.last_message_time diff --git a/src/plugins/PFC/conversation_loop.py b/src/plugins/PFC/conversation_loop.py index c4575735..4b65e99f 100644 --- a/src/plugins/PFC/conversation_loop.py +++ b/src/plugins/PFC/conversation_loop.py @@ -69,10 +69,7 @@ async def run_conversation_loop(conversation_instance: "Conversation"): conversation_instance.ignore_until_timestamp and loop_iter_start_time < conversation_instance.ignore_until_timestamp ): - if ( - conversation_instance.idle_chat - and conversation_instance.idle_chat._running - ): + if conversation_instance.idle_chat and conversation_instance.idle_chat._running: # 不直接停止服务,改为暂时忽略此用户 # 虽然我们仍然可以通过active_instances_count来决定是否触发主动聊天 # 但为了安全起见,我们只记录一个日志 From b2dd8717419e77142e007794a79825ecfb7aaf6d Mon Sep 17 00:00:00 2001 From: Bakadax Date: Thu, 8 May 2025 19:48:29 +0800 Subject: [PATCH 55/63] =?UTF-8?q?config=20=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/config.py | 9 ++++----- template/bot_config_template.toml | 11 ++--------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/config/config.py b/src/config/config.py index b0d20ab6..da5b3c2e 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -276,7 +276,7 @@ class BotConfig: enable_pfc_reply_checker: bool = True # 是否开启PFC回复检查 # idle_conversation - enable_idle_conversation: bool = False # 是否启用 pfc 主动发言(未完善) + enable_idle_conversation: bool = False # 是否启用 pfc 主动发言 idle_check_interval: int = 10 # 检查间隔,10分钟检查一次 min_idle_time: int = 7200 # 最短无活动时间,2小时 (7200秒) max_idle_time: int = 18000 # 最长无活动时间,5小时 (18000秒) @@ -493,7 +493,6 @@ class BotConfig: "llm_heartflow", "llm_PFC_action_planner", "llm_PFC_chat", - "llm_PFC_reply_checker", "llm_PFC_relationship_eval", ] @@ -666,7 +665,7 @@ class BotConfig: config.talk_allowed_private = set(str(user) for user in experimental_config.get("talk_allowed_private", [])) if config.INNER_VERSION in SpecifierSet(">=1.1.0"): config.enable_pfc_chatting = experimental_config.get("pfc_chatting", config.enable_pfc_chatting) - if config.INNER_VERSION in SpecifierSet(">=1.1.0"): + if config.INNER_VERSION in SpecifierSet(">=1.6.2"): config.enable_pfc_reply_checker = experimental_config.get( "enable_pfc_reply_checker", config.enable_pfc_reply_checker ) @@ -674,7 +673,7 @@ class BotConfig: def idle_conversation(parent: dict): idle_conversation_config = parent["idle_conversation"] - if config.INNER_VERSION in SpecifierSet(">=1.6.3"): + if config.INNER_VERSION in SpecifierSet(">=1.6.2"): config.enable_idle_conversation = idle_conversation_config.get( "enable_idle_conversation", config.enable_idle_conversation ) @@ -717,7 +716,7 @@ class BotConfig: "chat": {"func": chat, "support": ">=1.6.0", "necessary": False}, "normal_chat": {"func": normal_chat, "support": ">=1.6.0", "necessary": False}, "focus_chat": {"func": focus_chat, "support": ">=1.6.0", "necessary": False}, - "idle_conversation": {"func": idle_conversation, "support": ">=1.6.3", "necessary": False}, + "idle_conversation": {"func": idle_conversation, "support": ">=1.6.2", "necessary": False}, } # 原地修改,将 字符串版本表达式 转换成 版本对象 diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 95282ed5..0415e75d 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "1.6.5" +version = "1.6.2" #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #如果你想要修改配置文件,请在修改后将version的值进行变更 @@ -191,7 +191,7 @@ pfc_chatting = false # 是否启用PFC聊天,该功能仅作用于私聊,与 enable_pfc_reply_checker = true # 是否启用 PFC 的回复检查器 [idle_conversation] -enable_idle_conversation = false +enable_idle_conversation = false # 是否启用 pfc 主动发言 idle_check_interval = 10 # 检查间隔,10分钟检查一次 min_idle_time = 7200 # 最短无活动时间,2小时 (7200秒) max_idle_time = 18000 # 最长无活动时间,5小时 (18000秒) @@ -291,13 +291,6 @@ temp = 0.3 pri_in = 2 pri_out = 8 -#PFC检查模型 -[model.llm_PFC_reply_checker] -name = "Pro/deepseek-ai/DeepSeek-V3" -provider = "SILICONFLOW" -pri_in = 2 -pri_out = 8 - # PFC 关系评估LLM [model.llm_PFC_relationship_eval] name = "Pro/deepseek-ai/DeepSeek-V3" # 或者其他你认为适合判断任务的模型 From 70afc68ba69245cd017ad90524637cd13b3e7eb2 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Thu, 8 May 2025 19:58:10 +0800 Subject: [PATCH 56/63] =?UTF-8?q?config=20template=20=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/bot_config_template.toml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 0415e75d..55b22cad 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -295,10 +295,9 @@ pri_out = 8 [model.llm_PFC_relationship_eval] name = "Pro/deepseek-ai/DeepSeek-V3" # 或者其他你认为适合判断任务的模型 provider = "SILICONFLOW" -temp = 0.4 # 判断任务建议温度稍低 -max_tokens = 512 # 根据Prompt长度和期望输出来调整 -pri_in = 2 # 价格信息(可选) -pri_out = 8 # 价格信息(可选) +temp = 0.4 +pri_in = 2 +pri_out = 8 #以下模型暂时没有使用!! #以下模型暂时没有使用!! From c45120161df4f29c25d7728b3418df1eab584ae2 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Thu, 8 May 2025 20:30:12 +0800 Subject: [PATCH 57/63] =?UTF-8?q?=E5=B0=86=E9=83=A8=E5=88=86=E8=BE=93?= =?UTF-8?q?=E5=87=BA=E8=BD=AC=E4=B8=BAdebug=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/actions.py | 6 +++--- src/plugins/PFC/conversation.py | 6 +++--- src/plugins/PFC/conversation_initializer.py | 22 ++++++++++----------- src/plugins/PFC/observation_info.py | 4 ++-- src/plugins/PFC/pfc_emotion.py | 2 +- src/plugins/PFC/pfc_utils.py | 2 +- src/plugins/PFC/reply_generator.py | 4 ++-- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/plugins/PFC/actions.py b/src/plugins/PFC/actions.py index 7b15cdfb..e4a2cb77 100644 --- a/src/plugins/PFC/actions.py +++ b/src/plugins/PFC/actions.py @@ -246,7 +246,7 @@ async def handle_action( is_suitable = True check_reason = "ReplyChecker 已通过配置关闭" need_replan_from_checker = False - logger.info(f"{log_prefix} [配置关闭] ReplyChecker 已跳过,默认回复为合适。") + logger.debug(f"{log_prefix} [配置关闭] ReplyChecker 已跳过,默认回复为合适。") # 处理检查结果 if not is_suitable: @@ -398,13 +398,13 @@ async def handle_action( # 如果是 direct_reply 且规划期间有他人新消息,则下次不追问 if other_new_msg_count_during_planning > 0 and action == "direct_reply": - logger.info( + 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.info( + logger.debug( f"[私聊][{conversation_instance.private_name}] 成功执行 '{action}', 下一轮【允许】使用追问逻辑。" ) conversation_info.last_successful_reply_action = action diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index d74f5096..da613317 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -134,7 +134,7 @@ class Conversation: observation_info=self.observation_info, chat_observer_for_history=self.chat_observer, ) - logger.info(f"[私聊][{self.private_name}] 最终关系评估已调用。") + logger.debug(f"[私聊][{self.private_name}] 最终关系评估已调用。") except Exception as e_final_rel: logger.error(f"[私聊][{self.private_name}] 调用最终关系评估时出错: {e_final_rel}") logger.error(traceback.format_exc()) @@ -145,12 +145,12 @@ class Conversation: if self.idle_chat: # 减少活跃实例计数,而不是停止IdleChat await self.idle_chat.decrement_active_instances() - logger.info(f"[私聊][{self.private_name}] 已减少IdleChat活跃实例计数") + logger.debug(f"[私聊][{self.private_name}] 已减少IdleChat活跃实例计数") if self.observation_info and self.chat_observer: self.observation_info.unbind_from_chat_observer() if self.mood_mng and hasattr(self.mood_mng, "stop_mood_update") and self.mood_mng._running: # type: ignore self.mood_mng.stop_mood_update() # type: ignore - logger.info(f"[私聊][{self.private_name}] MoodManager 后台更新已停止。") + logger.debug(f"[私聊][{self.private_name}] MoodManager 后台更新已停止。") self._initialized = False # 标记为未初始化 logger.info(f"[私聊][{self.private_name}] 对话实例 {self.stream_id} 已停止。") diff --git a/src/plugins/PFC/conversation_initializer.py b/src/plugins/PFC/conversation_initializer.py index f5303341..932f74f5 100644 --- a/src/plugins/PFC/conversation_initializer.py +++ b/src/plugins/PFC/conversation_initializer.py @@ -42,7 +42,7 @@ async def load_initial_history(conversation_instance: "Conversation"): return try: - logger.info( + logger.debug( f"[私聊][{conversation_instance.private_name}] 为 {conversation_instance.stream_id} 加载初始聊天记录..." ) # 从聊天核心获取原始消息列表 @@ -132,7 +132,7 @@ async def initialize_core_components(conversation_instance: "Conversation"): # return # conversation_instance._initializing_flag_from_manager = True # 标记开始初始化 - logger.info( + logger.debug( f"[私聊][{conversation_instance.private_name}] (Initializer) 开始初始化对话实例核心组件: {conversation_instance.stream_id}" ) @@ -149,12 +149,12 @@ async def initialize_core_components(conversation_instance: "Conversation"): conversation_instance.relationship_translator = PfcRepationshipTranslator( private_name=conversation_instance.private_name ) - logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) PfcRelationship 初始化完成。") + logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) PfcRelationship 初始化完成。") conversation_instance.emotion_updater = PfcEmotionUpdater( private_name=conversation_instance.private_name, bot_name=global_config.BOT_NICKNAME ) - logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) PfcEmotion 初始化完成。") + logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) PfcEmotion 初始化完成。") logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化 GoalAnalyzer...") conversation_instance.goal_analyzer = GoalAnalyzer( @@ -194,7 +194,7 @@ async def initialize_core_components(conversation_instance: "Conversation"): conversation_instance.stream_id, conversation_instance.private_name ) await conversation_instance.idle_chat.increment_active_instances() - logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) IdleChat实例已获取并增加活跃计数") + logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) IdleChat实例已获取并增加活跃计数") # 2. 初始化信息存储和观察组件 logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 获取 ChatObserver 实例...") @@ -235,7 +235,7 @@ async def initialize_core_components(conversation_instance: "Conversation"): conversation_instance.conversation_info.person_id = person_id_tuple[0] # 第一个元素是 person_id private_platform_str = person_id_tuple[1] private_user_id_str = person_id_tuple[2] - logger.info( + logger.debug( f"[私聊][{conversation_instance.private_name}] (Initializer) 获取到 person_id: {conversation_instance.conversation_info.person_id} for {private_platform_str}:{private_user_id_str}" ) else: @@ -251,7 +251,7 @@ async def initialize_core_components(conversation_instance: "Conversation"): if conversation_instance.idle_chat: logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) 启动 IdleChat...") # 不需要再次启动,只需确保已初始化 - logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) IdleChat实例已初始化") + logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) IdleChat实例已初始化") if ( conversation_instance.mood_mng @@ -259,11 +259,11 @@ async def initialize_core_components(conversation_instance: "Conversation"): and not conversation_instance.mood_mng._running ): # type: ignore conversation_instance.mood_mng.start_mood_update(update_interval=global_config.mood_update_interval) # type: ignore - logger.info( + logger.debug( f"[私聊][{conversation_instance.private_name}] (Initializer) MoodManager 已启动后台更新,间隔: {global_config.mood_update_interval} 秒。" ) elif conversation_instance.mood_mng and conversation_instance.mood_mng._running: # type: ignore - logger.info(f"[私聊][{conversation_instance.private_name}] (Initializer) MoodManager 已在运行中。") + logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) MoodManager 已在运行中。") else: logger.warning( f"[私聊][{conversation_instance.private_name}] (Initializer) MoodManager 未能启动,相关功能可能受限。" @@ -291,7 +291,7 @@ async def initialize_core_components(conversation_instance: "Conversation"): numeric_relationship_value ) ) - logger.info( + logger.debug( f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化时加载关系文本: {conversation_instance.conversation_info.relationship_text}" ) except Exception as e_init_rel: @@ -305,7 +305,7 @@ async def initialize_core_components(conversation_instance: "Conversation"): conversation_instance.conversation_info.current_emotion_text = ( conversation_instance.mood_mng.get_prompt() ) # type: ignore - logger.info( + logger.debug( f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化时加载情绪文本: {conversation_instance.conversation_info.current_emotion_text}" ) except Exception as e_init_emo: diff --git a/src/plugins/PFC/observation_info.py b/src/plugins/PFC/observation_info.py index 2d2a0f27..7b3d6279 100644 --- a/src/plugins/PFC/observation_info.py +++ b/src/plugins/PFC/observation_info.py @@ -297,7 +297,7 @@ class ObservationInfo: if new_count < original_count: self.new_messages_count = new_count - logger.info( + logger.debug( f"[私聊][{self.private_name}] 移除了未处理的消息 (ID: {message_id_to_delete}), 当前未处理数: {self.new_messages_count}" ) self.update_changed() @@ -384,7 +384,7 @@ class ObservationInfo: self.new_messages_count = len(self.unprocessed_messages) self.chat_history_count = len(self.chat_history) - logger.info( + logger.debug( f"[私聊][{self.private_name}] 已清理 {cleared_count} 条消息 (IDs: {message_ids_to_clear}),剩余未处理 {self.new_messages_count} 条,当前历史记录 {self.chat_history_count} 条。" ) diff --git a/src/plugins/PFC/pfc_emotion.py b/src/plugins/PFC/pfc_emotion.py index d88344f8..5237d7b2 100644 --- a/src/plugins/PFC/pfc_emotion.py +++ b/src/plugins/PFC/pfc_emotion.py @@ -24,7 +24,7 @@ class PfcEmotionUpdater: # LLM 实例 (根据 global_config.llm_summary 配置) llm_config_summary = getattr(global_config, "llm_summary", None) if llm_config_summary and isinstance(llm_config_summary, dict): - logger.info(f"[私聊][{self.private_name}] 使用 llm_summary 配置初始化情绪判断LLM。") + logger.debug(f"[私聊][{self.private_name}] 使用 llm_summary 配置初始化情绪判断LLM。") self.llm = LLMRequest( model=llm_config_summary, temperature=llm_config_summary.get( diff --git a/src/plugins/PFC/pfc_utils.py b/src/plugins/PFC/pfc_utils.py index 6047aa81..38ead120 100644 --- a/src/plugins/PFC/pfc_utils.py +++ b/src/plugins/PFC/pfc_utils.py @@ -274,7 +274,7 @@ async def get_person_id(private_name: str, chat_stream: ChatStream): if chat_stream.user_info: private_user_id_str = str(chat_stream.user_info.user_id) private_platform_str = chat_stream.user_info.platform - logger.info( + logger.debug( f"[私聊][{private_name}] 从 ChatStream 获取到私聊对象信息: ID={private_user_id_str}, Platform={private_platform_str}, Name={private_nickname_str}" ) elif chat_stream.group_info is None and private_name: diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index 79942b09..5f3b3591 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -260,7 +260,7 @@ class ReplyGenerator: f"请根据此提示调整你的新回复,确保内容新颖,不要重复你已经说过的话。\n" f"------\n" ) - logger.info( + logger.debug( f"[私聊][{self.private_name}] (ReplyGenerator) 检测到自身复读,将加入特定警告到 Prompt:\n" f" 内容: {last_content}" ) @@ -273,7 +273,7 @@ class ReplyGenerator: f"请根据【消息内容】和【失败原因】调整你的新回复,避免重复之前的错误。\n" f"------\n" ) - logger.info( + logger.debug( f"[私聊][{self.private_name}] (ReplyGenerator) 检测到上次回复失败信息,将加入 Prompt:\n" f" 内容: {last_content}\n" f" 原因: {last_reason}" From a9c2b7956531da323f42e27a4ba391f3250af7b8 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Thu, 8 May 2025 21:07:39 +0800 Subject: [PATCH 58/63] =?UTF-8?q?logger=20=E8=BE=93=E5=87=BA=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E4=B8=BA=20bot=5Fnickname?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/actions.py | 4 ++-- src/plugins/PFC/reply_checker.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugins/PFC/actions.py b/src/plugins/PFC/actions.py index e4a2cb77..80ea2d1d 100644 --- a/src/plugins/PFC/actions.py +++ b/src/plugins/PFC/actions.py @@ -334,7 +334,7 @@ async def handle_action( 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}] 机器人发送的消息已添加到 chat_history。当前历史数: {observation_info.chat_history_count}" + f"[私聊][{conversation_instance.private_name}] {global_config.BOT_NICKNAME}发送的消息已添加到 chat_history。当前历史数: {observation_info.chat_history_count}" ) # 可选:如果 chat_history 过长,进行修剪 (例如,保留最近N条) @@ -413,7 +413,7 @@ async def handle_action( if conversation_info: # 再次确认 conversation_info.current_instance_message_count += 1 logger.debug( - f"[私聊][{conversation_instance.private_name}] 实例消息计数(机器人发送后)增加到: {conversation_info.current_instance_message_count}" + f"[私聊][{conversation_instance.private_name}] 实例消息计数({global_config.BOT_NICKNAME}发送后)增加到: {conversation_info.current_instance_message_count}" ) if conversation_instance.relationship_updater: # 确保存在 diff --git a/src/plugins/PFC/reply_checker.py b/src/plugins/PFC/reply_checker.py index aa00cd96..c155212f 100644 --- a/src/plugins/PFC/reply_checker.py +++ b/src/plugins/PFC/reply_checker.py @@ -42,7 +42,7 @@ class ReplyChecker: 对于非重复消息: (True, "消息内容未与机器人历史发言重复。", False) """ if not self.bot_qq_str: - logger.error(f"[私聊][{self.private_name}] ReplyChecker: BOT_QQ 未配置,无法检查机器人自身消息。") + logger.error(f"[私聊][{self.private_name}] ReplyChecker: BOT_QQ 未配置,无法检查{global_config.BOT_NICKNAME}自身消息。") return True, "BOT_QQ未配置,跳过重复检查。", False # 无法检查则默认通过 if len(reply) <= 4: @@ -64,11 +64,11 @@ class ReplyChecker: historical_message_text = msg_dict.get("processed_plain_text", "") # <--- 新增详细对比日志 --- START ---> logger.debug( - f"[私聊][{self.private_name}] ReplyChecker: 历史记录 #{i} (机器人): '{historical_message_text}' (长度 {len(historical_message_text)})" + f"[私聊][{self.private_name}] ReplyChecker: 历史记录 #{i} ({global_config.BOT_NICKNAME}): '{historical_message_text}' (长度 {len(historical_message_text)})" ) if reply == historical_message_text: logger.warning(f"[私聊][{self.private_name}] ReplyChecker: !!! 精确匹配成功 !!!") - logger.warning(f"[私聊][{self.private_name}] ReplyChecker 检测到机器人自身重复消息: '{reply}'") + logger.warning(f"[私聊][{self.private_name}] ReplyChecker 检测到{global_config.BOT_NICKNAME}自身重复消息: '{reply}'") match_found = True # <--- 标记找到 return (False, "机器人尝试发送重复消息", False) # <--- 新增详细对比日志 --- END ---> From 118501b0cfb044a7c65f1530e0ee66163cfa3afc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 8 May 2025 13:07:57 +0000 Subject: [PATCH 59/63] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=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/plugins/PFC/reply_checker.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/plugins/PFC/reply_checker.py b/src/plugins/PFC/reply_checker.py index c155212f..4f4030e6 100644 --- a/src/plugins/PFC/reply_checker.py +++ b/src/plugins/PFC/reply_checker.py @@ -42,7 +42,9 @@ class ReplyChecker: 对于非重复消息: (True, "消息内容未与机器人历史发言重复。", False) """ if not self.bot_qq_str: - logger.error(f"[私聊][{self.private_name}] ReplyChecker: BOT_QQ 未配置,无法检查{global_config.BOT_NICKNAME}自身消息。") + logger.error( + f"[私聊][{self.private_name}] ReplyChecker: BOT_QQ 未配置,无法检查{global_config.BOT_NICKNAME}自身消息。" + ) return True, "BOT_QQ未配置,跳过重复检查。", False # 无法检查则默认通过 if len(reply) <= 4: @@ -68,7 +70,9 @@ class ReplyChecker: ) if reply == historical_message_text: logger.warning(f"[私聊][{self.private_name}] ReplyChecker: !!! 精确匹配成功 !!!") - logger.warning(f"[私聊][{self.private_name}] ReplyChecker 检测到{global_config.BOT_NICKNAME}自身重复消息: '{reply}'") + logger.warning( + f"[私聊][{self.private_name}] ReplyChecker 检测到{global_config.BOT_NICKNAME}自身重复消息: '{reply}'" + ) match_found = True # <--- 标记找到 return (False, "机器人尝试发送重复消息", False) # <--- 新增详细对比日志 --- END ---> From be0cf8c61d7a99acea171ef5c195303e5871af04 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 8 May 2025 13:08:06 +0000 Subject: [PATCH 60/63] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=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/plugins/PFC/reply_checker.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/plugins/PFC/reply_checker.py b/src/plugins/PFC/reply_checker.py index c155212f..4f4030e6 100644 --- a/src/plugins/PFC/reply_checker.py +++ b/src/plugins/PFC/reply_checker.py @@ -42,7 +42,9 @@ class ReplyChecker: 对于非重复消息: (True, "消息内容未与机器人历史发言重复。", False) """ if not self.bot_qq_str: - logger.error(f"[私聊][{self.private_name}] ReplyChecker: BOT_QQ 未配置,无法检查{global_config.BOT_NICKNAME}自身消息。") + logger.error( + f"[私聊][{self.private_name}] ReplyChecker: BOT_QQ 未配置,无法检查{global_config.BOT_NICKNAME}自身消息。" + ) return True, "BOT_QQ未配置,跳过重复检查。", False # 无法检查则默认通过 if len(reply) <= 4: @@ -68,7 +70,9 @@ class ReplyChecker: ) if reply == historical_message_text: logger.warning(f"[私聊][{self.private_name}] ReplyChecker: !!! 精确匹配成功 !!!") - logger.warning(f"[私聊][{self.private_name}] ReplyChecker 检测到{global_config.BOT_NICKNAME}自身重复消息: '{reply}'") + logger.warning( + f"[私聊][{self.private_name}] ReplyChecker 检测到{global_config.BOT_NICKNAME}自身重复消息: '{reply}'" + ) match_found = True # <--- 标记找到 return (False, "机器人尝试发送重复消息", False) # <--- 新增详细对比日志 --- END ---> From 949447fd21a126ecc7a6a32e9a30968ac09627f2 Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Thu, 8 May 2025 21:31:10 +0800 Subject: [PATCH 61/63] =?UTF-8?q?=E5=8E=BB=E9=99=A4planner=E7=9A=84?= =?UTF-8?q?=E4=BA=BA=E8=AE=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/action_planner.py | 110 ++++++++++++------------------ 1 file changed, 45 insertions(+), 65 deletions(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 3d7743a7..6d78fec1 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -3,7 +3,7 @@ import traceback from typing import Tuple, Optional, Dict, Any, List from src.common.logger_manager import get_logger -from src.individuality.individuality import Individuality +# from src.individuality.individuality import Individuality from src.plugins.utils.chat_message_builder import build_readable_messages from ..models.utils_model import LLMRequest from ...config.config import global_config @@ -22,26 +22,21 @@ logger = get_logger("pfc_action_planner") # Prompt(1): 首次回复或非连续回复时的决策 Prompt PROMPT_INITIAL_REPLY = """ 当前时间:{current_time_str} -{persona_text} -现在你正在和{sender_name}在QQ上私聊 -你和对方的关系是:{relationship_text} -你现在的心情是:{current_emotion_text} -请根据以下【所有信息】审慎且灵活的决策下一步行动,可以回复,可以倾听,可以调取知识,甚至可以屏蔽对方: +现在{persona_text}正在与{sender_name}在qq上私聊 +他们的关系是:{relationship_text} +{persona_text}现在的心情是是:{current_emotion_text} +你现在需要操控{persona_text},根据以下【所有信息】灵活,合理的决策{persona_text}的下一步行动,需要符合正常人的社交流程,可以回复,可以倾听,甚至可以屏蔽对方: 【当前对话目标】 {goals_str} 【最近行动历史概要】 {action_history_summary} -【你想起来的相关知识】 -{retrieved_knowledge_str} 【上一次行动的详细情况和结果】 {last_action_context} 【时间和超时提示】 {time_since_last_bot_message_info}{timeout_context} 【最近的对话记录】(包括你已成功发送的消息 和 新收到的消息) {chat_history_text} -【你的回忆】 -{retrieved_memory_str} {spam_warning_info} @@ -56,7 +51,7 @@ block_and_ignore: 更加极端的结束对话方式,直接结束对话并在 请以JSON格式输出你的决策: {{ "action": "选择的行动类型 (必须是上面列表中的一个)", - "reason": "选择该行动的详细原因 (必须有解释你是如何根据“上一次行动结果”、“对话记录”和自身设定人设做出合理判断的)" + "reason": "选择该行动的原因 " }} 注意:请严格按照JSON格式输出,不要包含任何其他内容。""" @@ -64,26 +59,21 @@ block_and_ignore: 更加极端的结束对话方式,直接结束对话并在 # Prompt(2): 上一次成功回复后,决定继续发言时的决策 Prompt PROMPT_FOLLOW_UP = """ 当前时间:{current_time_str} -{persona_text} -现在你正在和{sender_name}在QQ上私聊,**并且刚刚你已经回复了对方** -你与对方的关系是:{relationship_text} -你现在的心情是:{current_emotion_text} -请根据以下【所有信息】审慎且灵活的决策下一步行动,可以继续发送新消息,可以等待,可以倾听,可以调取知识,甚至可以屏蔽对方: +现在{persona_text}正在与{sender_name}在qq上私聊,**并且刚刚{persona_text}已经回复了对方** +他们的关系是:{relationship_text} +{persona_text}现在的心情是是:{current_emotion_text} +你现在需要操控{persona_text},根据以下【所有信息】灵活,合理的决策{persona_text}的下一步行动,需要符合正常人的社交流程,可以发送新消息,可以等待,可以倾听,可以结束对话,甚至可以屏蔽对方: 【当前对话目标】 {goals_str} 【最近行动历史概要】 {action_history_summary} -【你想起来的相关知识】 -{retrieved_knowledge_str} 【上一次行动的详细情况和结果】 {last_action_context} 【时间和超时提示】 {time_since_last_bot_message_info}{timeout_context} 【最近的对话记录】(包括你已成功发送的消息 和 新收到的消息) {chat_history_text} -【你的回忆】 -{retrieved_memory_str} {spam_warning_info} @@ -99,7 +89,7 @@ block_and_ignore: 更加极端的结束对话方式,直接结束对话并在 请以JSON格式输出你的决策: {{ "action": "选择的行动类型 (必须是上面列表中的一个)", - "reason": "选择该行动的详细原因 (必须有解释你是如何根据“上一次行动结果”、“对话记录”和自身设定人设做出合理判断的。)" + "reason": "选择该行动的原因" }} 注意:请严格按照JSON格式输出,不要包含任何其他内容。""" @@ -107,19 +97,22 @@ block_and_ignore: 更加极端的结束对话方式,直接结束对话并在 # 新增:Prompt(3): 决定是否在结束对话前发送告别语 PROMPT_END_DECISION = """ 当前时间:{current_time_str} -{persona_text}。刚刚你决定结束一场 QQ 私聊。 +现在{persona_text}与{sender_name}刚刚结束了一场qq私聊 +他们的关系是:{relationship_text} +你现在需要操控{persona_text},根据以下【所有信息】灵活,合理的决策{persona_text}的下一步行动,需要符合正常人的社交流程: -【你们之前的聊天记录】 + +【他们之前的聊天记录】 {chat_history_text} -你觉得你们的对话已经完整结束了吗?有时候,在对话自然结束后再说点什么可能会有点奇怪,但有时也可能需要一条简短的消息来圆满结束。 -如果觉得确实有必要再发一条简短、自然、符合你人设的告别消息(比如 "好,下次再聊~" 或 "嗯,先这样吧"),就输出 "yes"。 +你觉得他们的对话已经完整结束了吗?有时候,在对话自然结束后再说点什么可能会有点奇怪,但有时也可能需要一条简短的消息来圆满结束。 +如果觉得确实有必要再发一条简短、自然的告别消息(比如 "好,下次再聊~" 或 "嗯,先这样吧"),就输出 "yes"。 如果觉得当前状态下直接结束对话更好,没有必要再发消息,就输出 "no"。 请以 JSON 格式输出你的选择: {{ "say_bye": "yes/no", - "reason": "选择 yes 或 no 的原因和内心想法 (简要说明)" + "reason": "选择 yes 或 no 的原因和 (简要说明)" }} 注意:请严格按照 JSON 格式输出,不要包含任何其他内容。""" @@ -127,27 +120,21 @@ PROMPT_END_DECISION = """ # Prompt(4): 当 reply_generator 决定不发送消息后的反思决策 Prompt PROMPT_REFLECT_AND_ACT = """ 当前时间:{current_time_str} -{persona_text} -现在你正在和{sender_name}在QQ上私聊 -你与对方的关系是:{relationship_text} -你现在的心情是:{current_emotion_text} -刚刚你本来想发一条新消息,但是想了想,你决定不发了。 -请根据以下【所有信息】审慎且灵活的决策下一步行动,可以等待,可以倾听,可以结束对话,甚至可以屏蔽对方: +现在{persona_text}正在与{sender_name}在qq上私聊,刚刚{persona_text}打算发一条新消息,想了想还是不发了 +他们的关系是:{relationship_text} +{persona_text}现在的心情是是:{current_emotion_text} +你现在需要操控{persona_text},根据以下【所有信息】灵活,合理的决策{persona_text}的下一步行动,需要符合正常人的社交流程,可以等待,可以倾听,可以结束对话,甚至可以屏蔽对方: 【当前对话目标】 {goals_str} 【最近行动历史概要】 {action_history_summary} -【你想起来的相关知识】 -{retrieved_knowledge_str} 【上一次行动的详细情况和结果】 {last_action_context} 【时间和超时提示】 {time_since_last_bot_message_info}{timeout_context} 【最近的对话记录】(包括你已成功发送的消息 和 新收到的消息) {chat_history_text} -【你的回忆】 -{retrieved_memory_str} {spam_warning_info} @@ -162,12 +149,11 @@ block_and_ignore: 更加极端的结束对话方式,直接结束对话并在 请以JSON格式输出你的决策: {{ "action": "选择的行动类型 (必须是上面列表中的一个)", - "reason": "选择该行动的详细原因 (必须有解释你是如何根据“上一次行动结果”、“对话记录”和自身设定人设做出合理判断的。)" + "reason": "选择该行动的原因" }} 注意:请严格按照JSON格式输出,不要包含任何其他内容。""" - class ActionPlanner: """行动规划器""" @@ -195,7 +181,7 @@ class ActionPlanner: raise # 获取个性化信息和机器人名称 - self.personality_info = Individuality.get_instance().get_prompt(x_person=2, level=3) + # self.personality_info = Individuality.get_instance().get_prompt(x_person=2, level=3) self.name = global_config.BOT_NICKNAME # 获取 ChatObserver 实例 (单例模式) self.chat_observer = ChatObserver.get_instance(stream_id, private_name) @@ -205,7 +191,7 @@ class ActionPlanner: observation_info: ObservationInfo, conversation_info: ConversationInfo, last_successful_reply_action: Optional[str], - use_reflect_prompt: bool = False, # 新增参数,用于指示是否使用PROMPT_REFLECT_AND_ACT + use_reflect_prompt: bool = False # 新增参数,用于指示是否使用PROMPT_REFLECT_AND_ACT ) -> Tuple[str, str]: """ 规划下一步行动。 @@ -228,14 +214,14 @@ class ActionPlanner: goals_str = self._build_goals_string(conversation_info) chat_history_text = await self._build_chat_history_text(observation_info) # 获取 sender_name, relationship_text, current_emotion_text - sender_name_str = getattr(observation_info, "sender_name", "对方") # 从 observation_info 获取 - if not sender_name_str: - sender_name_str = "对方" # 再次确保有默认值 + sender_name_str = getattr(observation_info, 'sender_name', '对方') # 从 observation_info 获取 + if not sender_name_str: sender_name_str = '对方' # 再次确保有默认值 - relationship_text_str = getattr(conversation_info, "relationship_text", "你们还不熟悉。") - current_emotion_text_str = getattr(conversation_info, "current_emotion_text", "心情平静。") + relationship_text_str = getattr(conversation_info, 'relationship_text', '你们还不熟悉。') + current_emotion_text_str = getattr(conversation_info, 'current_emotion_text', '心情平静。') - persona_text = f"你的名字是{self.name},{self.personality_info}。" + + persona_text = f"{self.name}。" action_history_summary, last_action_context = self._build_action_history_context(conversation_info) retrieved_memory_str, retrieved_knowledge_str = await retrieve_contextual_info( chat_history_text, self.private_name @@ -250,16 +236,14 @@ class ActionPlanner: # --- 2. 选择并格式化 Prompt --- try: - if use_reflect_prompt: # 新增的判断 + if use_reflect_prompt: # 新增的判断 prompt_template = PROMPT_REFLECT_AND_ACT log_msg = "使用 PROMPT_REFLECT_AND_ACT (反思决策)" # 对于 PROMPT_REFLECT_AND_ACT,它不包含 send_new_message 选项,所以 spam_warning_message 中的相关提示可以调整或省略 # 但为了保持占位符填充的一致性,我们仍然计算它 spam_warning_message = "" - if conversation_info.my_message_count > 5: # 这里的 my_message_count 仍有意义,表示之前连续发送了多少 - spam_warning_message = ( - f"⚠️【警告】**你之前已连续发送{str(conversation_info.my_message_count)}条消息!请谨慎决策。**" - ) + if conversation_info.my_message_count > 5: # 这里的 my_message_count 仍有意义,表示之前连续发送了多少 + spam_warning_message = f"⚠️【警告】**你之前已连续发送{str(conversation_info.my_message_count)}条消息!请谨慎决策。**" elif conversation_info.my_message_count > 2: spam_warning_message = f"💬【提示】**你之前已连续发送{str(conversation_info.my_message_count)}条消息。请注意保持对话平衡。**" @@ -275,12 +259,12 @@ class ActionPlanner: else: prompt_template = PROMPT_INITIAL_REPLY log_msg = "使用 PROMPT_INITIAL_REPLY (首次/非连续回复决策)" - spam_warning_message = "" # 初始回复时通常不需要刷屏警告 + spam_warning_message = "" # 初始回复时通常不需要刷屏警告 logger.debug(f"[私聊][{self.private_name}] {log_msg}") current_time_value = "获取时间失败" - if observation_info and hasattr(observation_info, "current_time_str") and observation_info.current_time_str: + if observation_info and hasattr(observation_info, 'current_time_str') and observation_info.current_time_str: current_time_value = observation_info.current_time_str if spam_warning_message: @@ -300,7 +284,7 @@ class ActionPlanner: spam_warning_info=spam_warning_message, sender_name=sender_name_str, relationship_text=relationship_text_str, - current_emotion_text=current_emotion_text_str, + current_emotion_text=current_emotion_text_str ) logger.debug(f"[私聊][{self.private_name}] 发送到LLM的最终提示词:\n------\n{prompt}\n------") except KeyError as fmt_key_err: @@ -340,14 +324,10 @@ class ActionPlanner: if initial_action == "end_conversation": try: time_str_for_end_decision = "获取时间失败" - if ( - observation_info - and hasattr(observation_info, "current_time_str") - and observation_info.current_time_str - ): + if observation_info and hasattr(observation_info, 'current_time_str') and observation_info.current_time_str: time_str_for_end_decision = observation_info.current_time_str final_action, final_reason = await self._handle_end_conversation_decision( - persona_text, chat_history_text, initial_reason, time_str_for_end_decision + persona_text, chat_history_text, initial_reason,time_str_for_end_decision ) except Exception as end_dec_err: logger.error(f"[私聊][{self.private_name}] 处理结束对话决策时出错: {end_dec_err}") @@ -372,7 +352,7 @@ class ActionPlanner: "block_and_ignore", "say_goodbye", ] - valid_actions_reflect = [ # PROMPT_REFLECT_AND_ACT 的动作 + valid_actions_reflect = [ # PROMPT_REFLECT_AND_ACT 的动作 "wait", "listening", "rethink_goal", @@ -513,7 +493,9 @@ class ActionPlanner: ) logger.debug(f"[私聊][{self.private_name}] 向 LLM 追加了 {other_unread_count} 条未读消息。") else: - chat_history_text += "\n--- 以上均为已读消息,未读消息均已处理完毕 ---\n" + chat_history_text += ( + f"\n--- 以上均为已读消息,未读消息均已处理完毕 ---\n" + ) except AttributeError as e: logger.warning(f"[私聊][{self.private_name}] 构建聊天记录文本时属性错误: {e}") chat_history_text = "[获取聊天记录时出错]\n" @@ -575,9 +557,7 @@ class ActionPlanner: ) -> Tuple[str, str]: """处理结束对话前的告别决策""" logger.info(f"[私聊][{self.private_name}] 初步规划结束对话,进入告别决策...") - end_decision_prompt = PROMPT_END_DECISION.format( - persona_text=persona_text, chat_history_text=chat_history_text, current_time_str=current_time_str - ) + end_decision_prompt = PROMPT_END_DECISION.format(persona_text=persona_text, chat_history_text=chat_history_text,current_time_str=current_time_str) logger.debug(f"[私聊][{self.private_name}] 发送到LLM的结束决策提示词:\n------\n{end_decision_prompt}\n------") llm_start_time = time.time() end_content, _ = await self.llm.generate_response_async(end_decision_prompt) From 01236be285fb523845c73f7c86aa5fe4271447a2 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Thu, 8 May 2025 21:34:26 +0800 Subject: [PATCH 62/63] =?UTF-8?q?=E7=BB=93=E6=9D=9F=E7=8E=AF=E8=8A=82nonet?= =?UTF-8?q?ype=20bug=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/action_planner.py | 38 +++++++++++++++++++------------ 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 6d78fec1..905ee68e 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -6,10 +6,10 @@ from src.common.logger_manager import get_logger # from src.individuality.individuality import Individuality from src.plugins.utils.chat_message_builder import build_readable_messages from ..models.utils_model import LLMRequest -from ...config.config import global_config +from src.config.config import global_config # 确保导入路径正确 -from .pfc_utils import get_items_from_json, retrieve_contextual_info +from .pfc_utils import get_items_from_json from .chat_observer import ChatObserver from .observation_info import ObservationInfo from .conversation_info import ConversationInfo @@ -191,7 +191,7 @@ class ActionPlanner: observation_info: ObservationInfo, conversation_info: ConversationInfo, last_successful_reply_action: Optional[str], - use_reflect_prompt: bool = False # 新增参数,用于指示是否使用PROMPT_REFLECT_AND_ACT + use_reflect_prompt: bool = False, # 新增参数,用于指示是否使用PROMPT_REFLECT_AND_ACT ) -> Tuple[str, str]: """ 规划下一步行动。 @@ -223,12 +223,12 @@ class ActionPlanner: persona_text = f"{self.name}。" action_history_summary, last_action_context = self._build_action_history_context(conversation_info) - retrieved_memory_str, retrieved_knowledge_str = await retrieve_contextual_info( - chat_history_text, self.private_name - ) - logger.info( - f"[私聊][{self.private_name}] (ActionPlanner) 检索完成。记忆: {'有' if '回忆起' in retrieved_memory_str else '无'} / 知识: {'有' if retrieved_knowledge_str and '无相关知识' not in retrieved_knowledge_str and '出错' not in retrieved_knowledge_str else '无'}" - ) + # retrieved_memory_str, retrieved_knowledge_str = await retrieve_contextual_info( + # chat_history_text, self.private_name + # ) + # logger.info( + # f"[私聊][{self.private_name}] (ActionPlanner) 检索完成。记忆: {'有' if '回忆起' in retrieved_memory_str else '无'} / 知识: {'有' if retrieved_knowledge_str and '无相关知识' not in retrieved_knowledge_str and '出错' not in retrieved_knowledge_str else '无'}" + # ) except Exception as prep_err: logger.error(f"[私聊][{self.private_name}] 准备 Prompt 输入时出错: {prep_err}") logger.error(traceback.format_exc()) @@ -278,8 +278,8 @@ class ActionPlanner: time_since_last_bot_message_info=time_since_last_bot_message_info, timeout_context=timeout_context, chat_history_text=chat_history_text if chat_history_text.strip() else "还没有聊天记录。", - retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。", - retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。", + # retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。", + # retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。", current_time_str=current_time_value, spam_warning_info=spam_warning_message, sender_name=sender_name_str, @@ -324,10 +324,18 @@ class ActionPlanner: if initial_action == "end_conversation": try: time_str_for_end_decision = "获取时间失败" - if observation_info and hasattr(observation_info, 'current_time_str') and observation_info.current_time_str: + if ( + observation_info + and hasattr(observation_info, "current_time_str") + and observation_info.current_time_str + ): time_str_for_end_decision = observation_info.current_time_str final_action, final_reason = await self._handle_end_conversation_decision( - persona_text, chat_history_text, initial_reason,time_str_for_end_decision + persona_text, + chat_history_text, initial_reason, + time_str_for_end_decision, + sender_name_str=sender_name_str, + relationship_text_str=relationship_text_str ) except Exception as end_dec_err: logger.error(f"[私聊][{self.private_name}] 处理结束对话决策时出错: {end_dec_err}") @@ -553,11 +561,11 @@ class ActionPlanner: # --- Helper method for handling end_conversation decision --- async def _handle_end_conversation_decision( - self, persona_text: str, chat_history_text: str, initial_reason: str, current_time_str: str + self, persona_text: str, chat_history_text: str, initial_reason: str, current_time_str: str, sender_name_str: str, relationship_text_str: str ) -> Tuple[str, str]: """处理结束对话前的告别决策""" logger.info(f"[私聊][{self.private_name}] 初步规划结束对话,进入告别决策...") - end_decision_prompt = PROMPT_END_DECISION.format(persona_text=persona_text, chat_history_text=chat_history_text,current_time_str=current_time_str) + end_decision_prompt = PROMPT_END_DECISION.format(persona_text=persona_text, chat_history_text=chat_history_text,current_time_str=current_time_str,sender_name = sender_name_str, relationship_text = relationship_text_str) logger.debug(f"[私聊][{self.private_name}] 发送到LLM的结束决策提示词:\n------\n{end_decision_prompt}\n------") llm_start_time = time.time() end_content, _ = await self.llm.generate_response_async(end_decision_prompt) From a48d91739a20743fe4a0df8d34b51c72b80cc532 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Thu, 8 May 2025 21:42:40 +0800 Subject: [PATCH 63/63] =?UTF-8?q?=E6=9D=80=E6=8E=89=E5=A4=9A=E4=BD=99logge?= =?UTF-8?q?rinfo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/actions.py | 4 ++-- src/plugins/PFC/conversation_loop.py | 2 +- src/plugins/PFC/pfc_emotion.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugins/PFC/actions.py b/src/plugins/PFC/actions.py index 80ea2d1d..8d8a9e0f 100644 --- a/src/plugins/PFC/actions.py +++ b/src/plugins/PFC/actions.py @@ -295,7 +295,7 @@ async def handle_action( # 后续的 plan 循环会检测到这个 "done_no_reply" 状态并使用反思 prompt elif is_suitable: # 适用于 direct_reply 或 (send_new_message 且 RG决定发送并通过检查) - logger.info(f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 找到合适的回复,准备发送。") + 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 # 使用检查通过的内容 @@ -311,7 +311,7 @@ async def handle_action( action_successful = True final_status = "done" # 明确设置 final_status final_reason = "成功发送" # 明确设置 final_reason - logger.info(f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 成功发送回复.") + logger.debug(f"[私聊][{conversation_instance.private_name}] 动作 '{action}': 成功发送回复.") # --- 新增:将机器人发送的消息添加到 ObservationInfo 的 chat_history --- if ( diff --git a/src/plugins/PFC/conversation_loop.py b/src/plugins/PFC/conversation_loop.py index 4b65e99f..d0cd287d 100644 --- a/src/plugins/PFC/conversation_loop.py +++ b/src/plugins/PFC/conversation_loop.py @@ -28,7 +28,7 @@ async def run_conversation_loop(conversation_instance: "Conversation"): 核心的规划与行动循环 (PFC Loop)。 之前是 Conversation 类中的 _plan_and_action_loop 方法。 """ - logger.info(f"[私聊][{conversation_instance.private_name}] 进入 run_conversation_loop 循环。") + logger.debug(f"[私聊][{conversation_instance.private_name}] 进入 run_conversation_loop 循环。") if not conversation_instance._initialized: logger.error(f"[私聊][{conversation_instance.private_name}] 尝试在未初始化状态下运行规划循环,退出。") diff --git a/src/plugins/PFC/pfc_emotion.py b/src/plugins/PFC/pfc_emotion.py index 5237d7b2..d2496520 100644 --- a/src/plugins/PFC/pfc_emotion.py +++ b/src/plugins/PFC/pfc_emotion.py @@ -102,11 +102,11 @@ class PfcEmotionUpdater: and detected_emotion_word in self.mood_mng.emotion_map ): self.mood_mng.update_mood_from_emotion(detected_emotion_word, intensity=self.EMOTION_UPDATE_INTENSITY) - logger.info( + logger.debug( f"[私聊][{self.private_name}] 基于事件 '{event_description}',情绪已更新为倾向于 '{detected_emotion_word}'。当前心情: {self.mood_mng.current_mood.text}" ) elif detected_emotion_word == "无变化": - logger.info(f"[私聊][{self.private_name}] 基于事件 '{event_description}',LLM判断情绪无显著变化。") + logger.debug(f"[私聊][{self.private_name}] 基于事件 '{event_description}',LLM判断情绪无显著变化。") else: logger.warning( f"[私聊][{self.private_name}] LLM返回了未知的情绪词 '{detected_emotion_word}' 或未返回有效词,情绪未主动更新。"