diff --git a/src/config/config.py b/src/config/config.py index 033d57f56..9b4386e7d 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -22,7 +22,7 @@ logger = get_logger("config") # 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码 is_test = False mai_version_main = "0.6.3" -mai_version_fix = "fix-1" +mai_version_fix = "fix-2" if mai_version_fix: if is_test: diff --git a/src/heart_flow/background_tasks.py b/src/heart_flow/background_tasks.py index 1b64c205c..99816540e 100644 --- a/src/heart_flow/background_tasks.py +++ b/src/heart_flow/background_tasks.py @@ -19,6 +19,8 @@ INTEREST_EVAL_INTERVAL_SECONDS = 5 NORMAL_CHAT_TIMEOUT_CHECK_INTERVAL_SECONDS = 60 # 新增状态评估间隔 HF_JUDGE_STATE_UPDATE_INTERVAL_SECONDS = 60 +# 新增私聊激活检查间隔 +PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS = 5 # 与兴趣评估类似,设为5秒 CLEANUP_INTERVAL_SECONDS = 1200 STATE_UPDATE_INTERVAL_SECONDS = 60 @@ -71,9 +73,10 @@ class BackgroundTaskManager: self._state_update_task: Optional[asyncio.Task] = None self._cleanup_task: Optional[asyncio.Task] = None self._logging_task: Optional[asyncio.Task] = None - self._normal_chat_timeout_check_task: Optional[asyncio.Task] = None # Nyaa~ 添加聊天超时检查任务的引用 - self._hf_judge_state_update_task: Optional[asyncio.Task] = None # Nyaa~ 添加状态评估任务的引用 - self._into_focus_task: Optional[asyncio.Task] = None # Nyaa~ 添加兴趣评估任务的引用 + self._normal_chat_timeout_check_task: Optional[asyncio.Task] = None + self._hf_judge_state_update_task: Optional[asyncio.Task] = None + self._into_focus_task: Optional[asyncio.Task] = None + self._private_chat_activation_task: Optional[asyncio.Task] = None # 新增私聊激活任务引用 self._tasks: List[Optional[asyncio.Task]] = [] # Keep track of all tasks async def start_tasks(self): @@ -124,6 +127,14 @@ class BackgroundTaskManager: f"专注评估任务已启动 间隔:{INTEREST_EVAL_INTERVAL_SECONDS}s", "_into_focus_task", ), + # 新增私聊激活任务配置 + ( + # Use lambda to pass the interval to the runner function + lambda: self._run_private_chat_activation_cycle(PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS), + "debug", + f"私聊激活检查任务已启动 间隔:{PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS}s", + "_private_chat_activation_task", + ), ] # 统一启动所有任务 @@ -277,3 +288,11 @@ class BackgroundTaskManager: interval=INTEREST_EVAL_INTERVAL_SECONDS, task_func=self._perform_into_focus_work, ) + + # 新增私聊激活任务运行器 + async def _run_private_chat_activation_cycle(self, interval: int): + await _run_periodic_loop( + task_name="Private Chat Activation Check", + interval=interval, + task_func=self.subheartflow_manager.sbhf_absent_private_into_focus + ) diff --git a/src/heart_flow/observation.py b/src/heart_flow/observation.py index e34f37d32..06dcdc313 100644 --- a/src/heart_flow/observation.py +++ b/src/heart_flow/observation.py @@ -12,9 +12,31 @@ from src.plugins.utils.chat_message_builder import ( num_new_messages_since, get_person_id_list, ) +from src.plugins.utils.prompt_builder import Prompt, global_prompt_manager +from src.plugins.chat.chat_stream import chat_manager +from typing import Optional +from src.plugins.person_info.person_info import person_info_manager +# Import the new utility function +from .utils_chat import get_chat_type_and_target_info logger = get_logger("observation") +# --- Define Prompt Templates for Chat Summary --- +Prompt( + """这是qq群聊的聊天记录,请总结以下聊天记录的主题: +{chat_logs} +请用一句话概括,包括人物、事件和主要信息,不要分点。""", + "chat_summary_group_prompt" # Template for group chat +) + +Prompt( + """这是你和{chat_target}的私聊记录,请总结以下聊天记录的主题: +{chat_logs} +请用一句话概括,包括事件,时间,和主要信息,不要分点。""", + "chat_summary_private_prompt" # Template for private chat +) +# --- End Prompt Template Definition --- + # 所有观察的基类 class Observation: @@ -34,28 +56,37 @@ class ChattingObservation(Observation): super().__init__("chat", chat_id) self.chat_id = chat_id + # --- Initialize attributes (defaults) --- + self.is_group_chat: bool = False + self.chat_target_info: Optional[dict] = None + # --- End Initialization --- + + # --- Other attributes initialized in __init__ --- self.talking_message = [] self.talking_message_str = "" self.talking_message_str_truncate = "" - self.name = global_config.BOT_NICKNAME self.nick_name = global_config.BOT_ALIAS_NAMES - self.max_now_obs_len = global_config.observation_context_size self.overlap_len = global_config.compressed_length self.mid_memorys = [] self.max_mid_memory_len = global_config.compress_length_limit self.mid_memory_info = "" - self.person_list = [] - self.llm_summary = LLMRequest( model=global_config.llm_observation, temperature=0.7, max_tokens=300, request_type="chat_observation" ) + async def initialize(self): + # --- Use utility function to determine chat type and fetch info --- + self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.chat_id) + logger.debug(f"ChattingObservation {self.chat_id} initialized: is_group={self.is_group_chat}, target_info={self.chat_target_info}") + # --- End using utility function --- + + # Fetch initial messages (existing logic) initial_messages = get_raw_msg_before_timestamp_with_chat(self.chat_id, self.last_observe_time, 10) - self.talking_message = initial_messages # 将这些消息设为初始上下文 + self.talking_message = initial_messages self.talking_message_str = await build_readable_messages(self.talking_message) # 进行一次观察 返回观察结果observe_info @@ -109,18 +140,49 @@ class ChattingObservation(Observation): messages=oldest_messages, timestamp_mode="normal", read_mark=0 ) - # 调用 LLM 总结主题 - prompt = ( - f"请总结以下聊天记录的主题:\n{oldest_messages_str}\n用一句话概括包括人物事件和主要信息,不要分点:" - ) - summary = "没有主题的闲聊" # 默认值 + # --- Build prompt using template --- + prompt = None # Initialize prompt as None try: - summary_result, _ = await self.llm_summary.generate_response_async(prompt) - if summary_result: # 确保结果不为空 - summary = summary_result + # 构建 Prompt - 根据 is_group_chat 选择模板 + if self.is_group_chat: + prompt_template_name = "chat_summary_group_prompt" + prompt = await global_prompt_manager.format_prompt( + prompt_template_name, + chat_logs=oldest_messages_str + ) + else: + # For private chat, add chat_target to the prompt variables + prompt_template_name = "chat_summary_private_prompt" + # Determine the target name for the prompt + chat_target_name = "对方" # Default fallback + if self.chat_target_info: + # Prioritize person_name, then nickname + chat_target_name = self.chat_target_info.get('person_name') or self.chat_target_info.get('user_nickname') or chat_target_name + + # Format the private chat prompt + prompt = await global_prompt_manager.format_prompt( + prompt_template_name, + # Assuming the private prompt template uses {chat_target} + chat_target=chat_target_name, + chat_logs=oldest_messages_str + ) except Exception as e: - logger.error(f"总结主题失败 for chat {self.chat_id}: {e}") - # 保留默认总结 "没有主题的闲聊" + logger.error(f"构建总结 Prompt 失败 for chat {self.chat_id}: {e}") + # prompt remains None + + summary = "没有主题的闲聊" # 默认值 + + if prompt: # Check if prompt was built successfully + try: + summary_result, _, _ = await self.llm_summary.generate_response(prompt) + if summary_result: # 确保结果不为空 + summary = summary_result + except Exception as e: + logger.error(f"总结主题失败 for chat {self.chat_id}: {e}") + # 保留默认总结 "没有主题的闲聊" + else: + logger.warning(f"因 Prompt 构建失败,跳过 LLM 总结 for chat {self.chat_id}") + mid_memory = { "id": str(int(datetime.now().timestamp())), diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index 8d07e6b52..8e00c263a 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -13,6 +13,8 @@ from src.plugins.heartFC_chat.normal_chat import NormalChat from src.heart_flow.mai_state_manager import MaiStateInfo from src.heart_flow.chat_state_info import ChatState, ChatStateInfo from src.heart_flow.sub_mind import SubMind +from src.plugins.person_info.person_info import person_info_manager +from .utils_chat import get_chat_type_and_target_info # 定义常量 (从 interest.py 移动过来) @@ -238,6 +240,11 @@ class SubHeartflow: self.chat_state_last_time: float = 0 self.history_chat_state: List[Tuple[ChatState, float]] = [] + # --- Initialize attributes --- + self.is_group_chat: bool = False + self.chat_target_info: Optional[dict] = None + # --- End Initialization --- + # 兴趣检测器 self.interest_chatting: InterestChatting = InterestChatting() @@ -260,11 +267,20 @@ class SubHeartflow: subheartflow_id=self.subheartflow_id, chat_state=self.chat_state, observations=self.observations ) - # 日志前缀 - self.log_prefix = chat_manager.get_stream_name(self.subheartflow_id) or self.subheartflow_id + # 日志前缀 - Moved determination to initialize + self.log_prefix = str(subheartflow_id) # Initial default prefix async def initialize(self): - """异步初始化方法,创建兴趣流""" + """异步初始化方法,创建兴趣流并确定聊天类型""" + + # --- Use utility function to determine chat type and fetch info --- + self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.chat_id) + # Update log prefix after getting info (potential stream name) + self.log_prefix = chat_manager.get_stream_name(self.subheartflow_id) or self.subheartflow_id # Keep this line or adjust if utils provides name + logger.debug(f"SubHeartflow {self.chat_id} initialized: is_group={self.is_group_chat}, target_info={self.chat_target_info}") + # --- End using utility function --- + + # Initialize interest system (existing logic) await self.interest_chatting.initialize() logger.debug(f"{self.log_prefix} InterestChatting 实例已初始化。") @@ -286,26 +302,33 @@ class SubHeartflow: async def _start_normal_chat(self) -> bool: """ - 启动 NormalChat 实例, - 进入 CHAT 状态时使用 - - 确保 HeartFChatting 已停止 + 启动 NormalChat 实例,并进行异步初始化。 + 进入 CHAT 状态时使用。 + 确保 HeartFChatting 已停止。 """ await self._stop_heart_fc_chat() # 确保 专注聊天已停止 log_prefix = self.log_prefix try: - # 获取聊天流并创建 NormalChat 实例 + # 获取聊天流并创建 NormalChat 实例 (同步部分) chat_stream = chat_manager.get_stream(self.chat_id) + if not chat_stream: + logger.error(f"{log_prefix} 无法获取 chat_stream,无法启动 NormalChat。") + return False + self.normal_chat_instance = NormalChat(chat_stream=chat_stream, interest_dict=self.get_interest_dict()) + + # 进行异步初始化 + await self.normal_chat_instance.initialize() + # 启动聊天任务 logger.info(f"{log_prefix} 开始普通聊天,随便水群...") - await self.normal_chat_instance.start_chat() # <--- 修正:调用 start_chat + await self.normal_chat_instance.start_chat() # start_chat now ensures init is called again if needed return True except Exception as e: - logger.error(f"{log_prefix} 启动 NormalChat 时出错: {e}") + logger.error(f"{log_prefix} 启动 NormalChat 或其初始化时出错: {e}") logger.error(traceback.format_exc()) - self.normal_chat_instance = None # 启动失败,清理实例 + self.normal_chat_instance = None # 启动/初始化失败,清理实例 return False async def _stop_heart_fc_chat(self): diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index 30119cca3..25e5d3ccf 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -335,27 +335,35 @@ class SubHeartflowManager: async def sbhf_absent_into_chat(self): """ - 随机选一个 ABSENT 状态的子心流,评估是否应转换为 CHAT 状态。 + 随机选一个 ABSENT 状态的 *群聊* 子心流,评估是否应转换为 CHAT 状态。 每次调用最多转换一个。 + 私聊会被忽略。 """ current_mai_state = self.mai_state_info.get_current_state() chat_limit = current_mai_state.get_normal_chat_max_num() async with self._lock: - # 1. 筛选出所有 ABSENT 状态的子心流 - absent_subflows = [ - hf for hf in self.subheartflows.values() if hf.chat_state.chat_status == ChatState.ABSENT + # 1. 筛选出所有 ABSENT 状态的 *群聊* 子心流 + absent_group_subflows = [ + hf for hf in self.subheartflows.values() + if hf.chat_state.chat_status == ChatState.ABSENT and hf.is_group_chat ] - if not absent_subflows: - logger.debug("没有摸鱼的子心流可以评估。") # 日志太频繁,注释掉 + if not absent_group_subflows: + # logger.debug("没有摸鱼的群聊子心流可以评估。") # 日志太频繁 return # 没有目标,直接返回 # 2. 随机选一个幸运儿 - sub_hf_to_evaluate = random.choice(absent_subflows) + sub_hf_to_evaluate = random.choice(absent_group_subflows) flow_id = sub_hf_to_evaluate.subheartflow_id stream_name = chat_manager.get_stream_name(flow_id) or flow_id log_prefix = f"[{stream_name}]" + + # --- Private chat check (redundant due to filter above, but safe) --- + # if not sub_hf_to_evaluate.is_group_chat: + # logger.debug(f"{log_prefix} 是私聊,跳过 CHAT 状态评估。") + # return + # --- End check --- # 3. 检查 CHAT 上限 current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT) @@ -658,8 +666,10 @@ class SubHeartflowManager: # --- 新增:处理来自 HeartFChatting 的状态转换请求 --- # async def sbhf_focus_into_absent(self, subflow_id: Any): """ - 接收来自 HeartFChatting 的请求,将特定子心流的状态转换为 ABSENT。 + 接收来自 HeartFChatting 的请求,将特定子心流的状态转换为 ABSENT 或 CHAT。 通常在连续多次 "no_reply" 后被调用。 + 对于私聊,总是转换为 ABSENT。 + 对于群聊,随机决定转换为 ABSENT 或 CHAT (如果 CHAT 未达上限)。 Args: subflow_id: 需要转换状态的子心流 ID。 @@ -667,50 +677,44 @@ class SubHeartflowManager: async with self._lock: subflow = self.subheartflows.get(subflow_id) if not subflow: - logger.warning(f"[状态转换请求] 尝试转换不存在的子心流 {subflow_id} 到 ABSENT") + logger.warning(f"[状态转换请求] 尝试转换不存在的子心流 {subflow_id} 到 ABSENT/CHAT") return stream_name = chat_manager.get_stream_name(subflow_id) or subflow_id current_state = subflow.chat_state.chat_status - # 仅当子心流处于 FOCUSED 状态时才进行转换 - # 因为 HeartFChatting 只在 FOCUSED 状态下运行 if current_state == ChatState.FOCUSED: - target_state = ChatState.ABSENT # 默认目标状态 - log_reason = "默认转换" + target_state = ChatState.ABSENT # Default target + log_reason = "默认转换 (私聊或群聊)" - # 决定是去 ABSENT 还是 CHAT - if random.random() < 0.5: - target_state = ChatState.ABSENT - log_reason = "随机选择 ABSENT" - logger.debug(f"[状态转换请求] {stream_name} ({current_state.value}) 随机决定进入 ABSENT") - else: - # 尝试进入 CHAT,先检查限制 - current_mai_state = self.mai_state_info.get_current_state() - chat_limit = current_mai_state.get_normal_chat_max_num() - # 使用不上锁的版本,因为我们已经在锁内 - current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT) - - if current_chat_count < chat_limit: - target_state = ChatState.CHAT - log_reason = f"随机选择 CHAT (当前 {current_chat_count}/{chat_limit})" - logger.debug( - f"[状态转换请求] {stream_name} ({current_state.value}) 随机决定进入 CHAT,未达上限 ({current_chat_count}/{chat_limit})" - ) - else: + # --- Modify logic based on chat type --- # + if subflow.is_group_chat: + # Group chat: Decide between ABSENT or CHAT + if random.random() < 0.5: # 50% chance to try CHAT + current_mai_state = self.mai_state_info.get_current_state() + chat_limit = current_mai_state.get_normal_chat_max_num() + current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT) + + if current_chat_count < chat_limit: + target_state = ChatState.CHAT + log_reason = f"群聊随机选择 CHAT (当前 {current_chat_count}/{chat_limit})" + else: + target_state = ChatState.ABSENT # Fallback to ABSENT if CHAT limit reached + log_reason = f"群聊随机选择 CHAT 但已达上限 ({current_chat_count}/{chat_limit}),转为 ABSENT" + else: # 50% chance to go directly to ABSENT target_state = ChatState.ABSENT - log_reason = f"随机选择 CHAT 但已达上限 ({current_chat_count}/{chat_limit}),转为 ABSENT" - logger.debug( - f"[状态转换请求] {stream_name} ({current_state.value}) 随机决定进入 CHAT,但已达上限 ({current_chat_count}/{chat_limit}),改为进入 ABSENT" - ) + log_reason = "群聊随机选择 ABSENT" + else: + # Private chat: Always go to ABSENT + target_state = ChatState.ABSENT + log_reason = "私聊退出 FOCUSED,转为 ABSENT" + # --- End modification --- # - # 开始转换 logger.info( f"[状态转换请求] 接收到请求,将 {stream_name} (当前: {current_state.value}) 尝试转换为 {target_state.value} ({log_reason})" ) try: await subflow.change_chat_state(target_state) - # 检查最终状态 final_state = subflow.chat_state.chat_status if final_state == target_state: logger.debug(f"[状态转换请求] {stream_name} 状态已成功转换为 {final_state.value}") @@ -728,5 +732,98 @@ class SubHeartflowManager: logger.warning( f"[状态转换请求] 收到对 {stream_name} 的请求,但其状态为 {current_state.value} (非 FOCUSED),不执行转换" ) + # --- 结束新增 --- # + + # --- 新增:处理私聊从 ABSENT 直接到 FOCUSED 的逻辑 --- # + async def sbhf_absent_private_into_focus(self): + """检查 ABSENT 状态的私聊子心流是否有新活动,若有且未达 FOCUSED 上限,则直接转换为 FOCUSED。""" + log_prefix_task = "[私聊激活检查]" + transitioned_count = 0 + checked_count = 0 + + # --- 获取当前状态和 FOCUSED 上限 --- # + current_mai_state = self.mai_state_info.get_current_state() + focused_limit = current_mai_state.get_focused_chat_max_num() + + # --- 检查是否允许 FOCUS 模式 --- # + if not global_config.allow_focus_mode: + # Log less frequently to avoid spam + # if int(time.time()) % 60 == 0: + # logger.debug(f"{log_prefix_task} 配置不允许进入 FOCUSED 状态") + return + + if focused_limit <= 0: + # logger.debug(f"{log_prefix_task} 当前状态 ({current_mai_state.value}) 不允许 FOCUSED 子心流") + return + + async with self._lock: + # --- 获取当前 FOCUSED 计数 (不上锁版本) --- # + current_focused_count = self.count_subflows_by_state_nolock(ChatState.FOCUSED) + + # --- 筛选出所有 ABSENT 状态的私聊子心流 --- # + eligible_subflows = [ + hf for hf in self.subheartflows.values() + if hf.chat_state.chat_status == ChatState.ABSENT and not hf.is_group_chat + ] + checked_count = len(eligible_subflows) + + if not eligible_subflows: + # logger.debug(f"{log_prefix_task} 没有 ABSENT 状态的私聊子心流可以评估。") + return + + # --- 遍历评估每个符合条件的私聊 --- # + for sub_hf in eligible_subflows: + # --- 再次检查 FOCUSED 上限,因为可能有多个同时激活 --- # + if current_focused_count >= focused_limit: + logger.debug(f"{log_prefix_task} 已达专注上限 ({current_focused_count}/{focused_limit}),停止检查后续私聊。") + break # 已满,无需再检查其他私聊 + + flow_id = sub_hf.subheartflow_id + stream_name = chat_manager.get_stream_name(flow_id) or flow_id + log_prefix = f"[{stream_name}]({log_prefix_task})" + + try: + # --- 检查是否有新活动 --- # + observation = sub_hf._get_primary_observation() # 获取主要观察者 + is_active = False + if observation: + # 检查自上次状态变为 ABSENT 后是否有新消息 + # 使用 chat_state_changed_time 可能更精确 + # 加一点点缓冲时间(例如 1 秒)以防时间戳完全相等 + timestamp_to_check = sub_hf.chat_state_changed_time - 1 + has_new = await observation.has_new_messages_since(timestamp_to_check) + if has_new: + is_active = True + logger.debug(f"{log_prefix} 检测到新消息,标记为活跃。") + # 可选:检查兴趣度是否大于0 (如果需要) + # interest_level = await sub_hf.interest_chatting.get_interest() + # if interest_level > 0: + # is_active = True + # logger.debug(f"{log_prefix} 检测到兴趣度 > 0 ({interest_level:.2f}),标记为活跃。") + else: + logger.warning(f"{log_prefix} 无法获取主要观察者来检查活动状态。") + + # --- 如果活跃且未达上限,则尝试转换 --- # + if is_active: + logger.info(f"{log_prefix} 检测到活跃且未达专注上限 ({current_focused_count}/{focused_limit}),尝试转换为 FOCUSED。") + await sub_hf.change_chat_state(ChatState.FOCUSED) + # 确认转换成功 + if sub_hf.chat_state.chat_status == ChatState.FOCUSED: + transitioned_count += 1 + current_focused_count += 1 # 更新计数器以供本轮后续检查 + logger.info(f"{log_prefix} 成功进入 FOCUSED 状态。") + else: + logger.warning(f"{log_prefix} 尝试进入 FOCUSED 状态失败。当前状态: {sub_hf.chat_state.chat_status.value}") + # else: # 不活跃,无需操作 + # logger.debug(f"{log_prefix} 未检测到新活动,保持 ABSENT。") + + except Exception as e: + logger.error(f"{log_prefix} 检查私聊活动或转换状态时出错: {e}", exc_info=True) + + # --- 循环结束后记录总结日志 --- # + if transitioned_count > 0: + logger.debug(f"{log_prefix_task} 完成,共检查 {checked_count} 个私聊,{transitioned_count} 个转换为 FOCUSED。") # --- 结束新增 --- # + + # --- 结束新增:处理来自 HeartFChatting 的状态转换请求 --- # diff --git a/src/heart_flow/utils_chat.py b/src/heart_flow/utils_chat.py new file mode 100644 index 000000000..5ad664b14 --- /dev/null +++ b/src/heart_flow/utils_chat.py @@ -0,0 +1,76 @@ +import asyncio +from typing import Optional, Tuple, Dict +from src.common.logger_manager import get_logger +from src.plugins.chat.chat_stream import chat_manager +from src.plugins.person_info.person_info import person_info_manager + +logger = get_logger("heartflow_utils") + +async def get_chat_type_and_target_info(chat_id: str) -> Tuple[bool, Optional[Dict]]: + """ + 获取聊天类型(是否群聊)和私聊对象信息。 + + Args: + chat_id: 聊天流ID + + Returns: + Tuple[bool, Optional[Dict]]: + - bool: 是否为群聊 (True 是群聊, False 是私聊或未知) + - Optional[Dict]: 如果是私聊,包含对方信息的字典;否则为 None。 + 字典包含: platform, user_id, user_nickname, person_id, person_name + """ + is_group_chat = False # Default to private/unknown + chat_target_info = None + + try: + chat_stream = await asyncio.to_thread(chat_manager.get_stream, chat_id) # Use to_thread if get_stream is sync + # If get_stream is already async, just use: chat_stream = await chat_manager.get_stream(chat_id) + + if chat_stream: + if chat_stream.group_info: + is_group_chat = True + chat_target_info = None # Explicitly None for group chat + elif chat_stream.user_info: # It's a private chat + is_group_chat = False + user_info = chat_stream.user_info + platform = chat_stream.platform + user_id = user_info.user_id + + # Initialize target_info with basic info + target_info = { + 'platform': platform, + 'user_id': user_id, + 'user_nickname': user_info.user_nickname, + 'person_id': None, + 'person_name': None + } + + # Try to fetch person info (assuming person_info_manager methods are sync) + try: + # Use asyncio.to_thread for potentially blocking sync calls + person_id = await asyncio.to_thread(person_info_manager.get_person_id, platform, user_id) + person_name = None + if person_id: + person_name = await asyncio.to_thread(person_info_manager.get_value, person_id, "person_name") + + # If person_info_manager methods are async, await them directly: + # person_id = await person_info_manager.get_person_id(platform, user_id) + # person_name = None + # if person_id: + # person_name = await person_info_manager.get_value(person_id, "person_name") + + target_info['person_id'] = person_id + target_info['person_name'] = person_name + except Exception as person_e: + logger.warning(f"获取 person_id 或 person_name 时出错 for {platform}:{user_id} in utils: {person_e}") + + chat_target_info = target_info + else: + logger.warning(f"无法获取 chat_stream for {chat_id} in utils") + # Keep defaults: is_group_chat=False, chat_target_info=None + + except Exception as e: + logger.error(f"获取聊天类型和目标信息时出错 for {chat_id}: {e}", exc_info=True) + # Keep defaults on error + + return is_group_chat, chat_target_info \ No newline at end of file diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index c2077fadf..f0a47f1df 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -27,6 +27,7 @@ from src.plugins.chat.utils import process_llm_response from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager from src.plugins.moods.moods import MoodManager from src.individuality.individuality import Individuality +from src.heart_flow.utils_chat import get_chat_type_and_target_info WAITING_TIME_THRESHOLD = 300 # 等待新消息时间阈值,单位秒 @@ -194,7 +195,12 @@ class HeartFChatting: self.on_consecutive_no_reply_callback = on_consecutive_no_reply_callback # 日志前缀 - self.log_prefix: str = f"[{chat_manager.get_stream_name(chat_id) or chat_id}]" + self.log_prefix: str = str(chat_id) # Initial default, will be updated + + # --- Initialize attributes (defaults) --- + self.is_group_chat: bool = False + self.chat_target_info: Optional[dict] = None + # --- End Initialization --- # 动作管理器 self.action_manager = ActionManager() @@ -234,22 +240,34 @@ class HeartFChatting: async def _initialize(self) -> bool: """ - 懒初始化以使用提供的标识符解析chat_stream。 - 确保实例已准备好处理触发器。 + 懒初始化,解析chat_stream, 获取聊天类型和目标信息。 """ if self._initialized: return True - - self.chat_stream = chat_manager.get_stream(self.stream_id) - if not self.chat_stream: - logger.error(f"{self.log_prefix} 获取ChatStream失败。") - return False - - # 更新日志前缀(以防流名称发生变化) - self.log_prefix = f"[{chat_manager.get_stream_name(self.stream_id) or self.stream_id}]" + + # --- Use utility function to determine chat type and fetch info --- + # Note: get_chat_type_and_target_info handles getting the chat_stream internally + self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.stream_id) + + # Update log prefix based on potential stream name (if needed, or get it from chat_stream if util doesn't return it) + # Assuming get_chat_type_and_target_info focuses only on type/target + # We still need the chat_stream object itself for other operations + try: + self.chat_stream = await asyncio.to_thread(chat_manager.get_stream, self.stream_id) + if not self.chat_stream: + logger.error(f"[HFC:{self.stream_id}] 获取ChatStream失败 during _initialize, though util func might have succeeded earlier.") + return False # Cannot proceed without chat_stream object + # Update log prefix using the fetched stream object + self.log_prefix = f"[{chat_manager.get_stream_name(self.stream_id) or self.stream_id}]" + except Exception as e: + logger.error(f"[HFC:{self.stream_id}] 获取ChatStream时出错 in _initialize: {e}") + return False + + logger.debug(f"{self.log_prefix} HeartFChatting initialized: is_group={self.is_group_chat}, target_info={self.chat_target_info}") + # --- End using utility function --- self._initialized = True - logger.debug(f"{self.log_prefix}麦麦感觉到了,可以开始认真水群 ") + logger.debug(f"{self.log_prefix} 麦麦感觉到了,可以开始认真水群 ") return True async def start(self): diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index c6cdf2510..42d6d1748 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -79,7 +79,7 @@ def init_prompt(): - 避免重复或评价自己的发言 - 不要和自己聊天 -【决策任务】 +决策任务 {action_options_text} 你必须从上面列出的可用行动中选择一个,并说明原因。 @@ -90,20 +90,6 @@ JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query": "reasoning": "string", // 做出此决定的详细理由和思考过程,说明你如何应用了回复原则 "emoji_query": "string" // 可选。如果行动是 'emoji_reply',必须提供表情主题(填写表情包的适用场合);如果行动是 'text_reply' 且你想附带表情,也在此提供表情主题,否则留空字符串 ""。遵循回复原则,不要滥用。 }} - -例如: -{{ - "action": "text_reply", - "reasoning": "用户提到了我,且问题比较具体,适合用文本回复。考虑到内容,可以带上一个微笑表情。", - "emoji_query": "微笑" -}} -或 -{{ - "action": "no_reply", - "reasoning": "我已经连续回复了两次,而且这个话题我不太感兴趣,根据回复原则,选择不回复,等待其他人发言。", - "emoji_query": "" -}} - 请输出你的决策 JSON: """, # 使用三引号避免内部引号问题 "planner_prompt", # 保持名称不变,替换内容 diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py index 9ed63c2df..3668e1874 100644 --- a/src/plugins/heartFC_chat/normal_chat.py +++ b/src/plugins/heartFC_chat/normal_chat.py @@ -19,6 +19,7 @@ from src.plugins.chat.chat_stream import ChatStream, chat_manager from src.plugins.person_info.relationship_manager import relationship_manager from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager from src.plugins.utils.timer_calculator import Timer +from src.heart_flow.utils_chat import get_chat_type_and_target_info logger = get_logger("chat") @@ -26,28 +27,46 @@ logger = get_logger("chat") class NormalChat: def __init__(self, chat_stream: ChatStream, interest_dict: dict): - """ - 初始化 NormalChat 实例,针对特定的 ChatStream。 - - Args: - chat_stream (ChatStream): 此 NormalChat 实例关联的聊天流对象。 - """ + """初始化 NormalChat 实例。只进行同步操作。""" + # Basic info from chat_stream (sync) self.chat_stream = chat_stream self.stream_id = chat_stream.stream_id - self.stream_name = chat_manager.get_stream_name(self.stream_id) or self.stream_id + # Get initial stream name, might be updated in initialize + self.stream_name = chat_manager.get_stream_name(self.stream_id) or self.stream_id + # Interest dict self.interest_dict = interest_dict + # --- Initialize attributes (defaults) --- + self.is_group_chat: bool = False + self.chat_target_info: Optional[dict] = None + # --- End Initialization --- + + # Other sync initializations self.gpt = NormalChatGenerator() - self.mood_manager = MoodManager.get_instance() # MoodManager 保持单例 - # 存储此实例的兴趣监控任务 + self.mood_manager = MoodManager.get_instance() self.start_time = time.time() - self.last_speak_time = 0 - self._chat_task: Optional[asyncio.Task] = None - logger.info(f"[{self.stream_name}] NormalChat 实例初始化完成。") + self._initialized = False # Track initialization status + + # logger.info(f"[{self.stream_name}] NormalChat 实例 __init__ 完成 (同步部分)。") + # Avoid logging here as stream_name might not be final + + async def initialize(self): + """异步初始化,获取聊天类型和目标信息。""" + if self._initialized: + return + + # --- Use utility function to determine chat type and fetch info --- + self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.stream_id) + # Update stream_name again after potential async call in util func + self.stream_name = chat_manager.get_stream_name(self.stream_id) or self.stream_id + logger.debug(f"[{self.stream_name}] NormalChat initialized: is_group={self.is_group_chat}, target_info={self.chat_target_info}") + # --- End using utility function --- + self._initialized = True + logger.info(f"[{self.stream_name}] NormalChat 实例 initialize 完成 (异步部分)。") # 改为实例方法 async def _create_thinking_message(self, message: MessageRecv) -> str: @@ -416,22 +435,18 @@ class NormalChat: # 改为实例方法, 移除 chat 参数 async def start_chat(self): - """为此 NormalChat 实例关联的 ChatStream 启动聊天任务(如果尚未运行), - 并在后台处理一次初始的高兴趣消息。""" # 文言文注释示例:启聊之始,若有遗珠,当于暗处拂拭,勿碍正途。 + """先进行异步初始化,然后启动聊天任务。""" + if not self._initialized: + await self.initialize() # Ensure initialized before starting tasks + if self._chat_task is None or self._chat_task.done(): - # --- 修改:使用 create_task 启动初始消息处理 --- - logger.info(f"[{self.stream_name}] 开始后台处理初始兴趣消息...") - # 创建一个任务来处理初始消息,不阻塞当前流程 - _initial_process_task = asyncio.create_task(self._process_initial_interest_messages()) - # 可以考虑给这个任务也添加完成回调来记录日志或处理错误 - # initial_process_task.add_done_callback(...) - # --- 修改结束 --- - - # 启动后台轮询任务 (这部分不变) - logger.info(f"[{self.stream_name}] 启动后台兴趣消息轮询任务...") - polling_task = asyncio.create_task(self._reply_interested_message()) # 注意变量名区分 + logger.info(f"[{self.stream_name}] 开始后台处理初始兴趣消息和轮询任务...") + # Process initial messages first + await self._process_initial_interest_messages() + # Then start polling task + polling_task = asyncio.create_task(self._reply_interested_message()) polling_task.add_done_callback(lambda t: self._handle_task_completion(t)) - self._chat_task = polling_task # self._chat_task 仍然指向主要的轮询任务 + self._chat_task = polling_task else: logger.info(f"[{self.stream_name}] 聊天轮询任务已在运行中。")