diff --git a/src/chat/focus_chat/expressors/default_expressor.py b/src/chat/focus_chat/expressors/default_expressor.py index 28b80929e..783077b23 100644 --- a/src/chat/focus_chat/expressors/default_expressor.py +++ b/src/chat/focus_chat/expressors/default_expressor.py @@ -1,6 +1,5 @@ -import time import traceback -from typing import List, Optional, Dict, Any +from typing import List, Optional, Dict, Any, Tuple from src.chat.message_receive.message import MessageRecv, MessageThinking, MessageSending from src.chat.message_receive.message import Seg # Local import needed after move from src.chat.message_receive.message import UserInfo @@ -18,6 +17,7 @@ from src.chat.utils.info_catcher import info_catcher_manager from src.manager.mood_manager import mood_manager from src.chat.heart_flow.utils_chat import get_chat_type_and_target_info from src.chat.message_receive.chat_stream import ChatStream +from src.chat.focus_chat.hfc_utils import parse_thinking_id_to_timestamp logger = get_logger("expressor") @@ -41,7 +41,7 @@ class DefaultExpressor: async def initialize(self): self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.chat_id) - async def _create_thinking_message(self, anchor_message: Optional[MessageRecv]) -> Optional[str]: + async def _create_thinking_message(self, anchor_message: Optional[MessageRecv], thinking_id: str): """创建思考消息 (尝试锚定到 anchor_message)""" if not anchor_message or not anchor_message.chat_stream: logger.error(f"{self.log_prefix} 无法创建思考消息,缺少有效的锚点消息或聊天流。") @@ -49,6 +49,7 @@ class DefaultExpressor: chat = anchor_message.chat_stream messageinfo = anchor_message.message_info + thinking_time_point = parse_thinking_id_to_timestamp(thinking_id) bot_user_info = UserInfo( user_id=global_config.BOT_QQ, user_nickname=global_config.BOT_NICKNAME, @@ -58,9 +59,6 @@ class DefaultExpressor: # logger.debug(f"创建思考消息chat:{chat}") # logger.debug(f"创建思考消息bot_user_info:{bot_user_info}") # logger.debug(f"创建思考消息messageinfo:{messageinfo}") - - thinking_time_point = round(time.time(), 2) - thinking_id = "mt" + str(thinking_time_point) thinking_message = MessageThinking( message_id=thinking_id, chat_stream=chat, @@ -69,9 +67,8 @@ class DefaultExpressor: thinking_start_time=thinking_time_point, ) logger.debug(f"创建思考消息thinking_message:{thinking_message}") - # Access MessageManager directly (using heart_fc_sender) + await self.heart_fc_sender.register_thinking(thinking_message) - return thinking_id async def deal_reply( self, @@ -79,11 +76,10 @@ class DefaultExpressor: action_data: Dict[str, Any], reasoning: str, anchor_message: MessageRecv, + thinking_id: str, ) -> tuple[bool, Optional[List[str]]]: # 创建思考消息 - thinking_id = await self._create_thinking_message(anchor_message) - if not thinking_id: - raise Exception("无法创建思考消息") + await self._create_thinking_message(anchor_message, thinking_id) reply = None # 初始化 reply,防止未定义 try: @@ -102,8 +98,14 @@ class DefaultExpressor: action_data=action_data, ) + with Timer("选择表情", cycle_timers): + emoji_keyword = action_data.get("emojis", []) + emoji_base64 = await self._choose_emoji(emoji_keyword) + if emoji_base64: + reply.append(("emoji", emoji_base64)) + if reply: - with Timer("发送文本消息", cycle_timers): + with Timer("发送消息", cycle_timers): await self._send_response_messages( anchor_message=anchor_message, thinking_id=thinking_id, @@ -113,12 +115,6 @@ class DefaultExpressor: else: logger.warning(f"{self.log_prefix} 文本回复生成失败") - # 处理表情部分 - emoji_keyword = action_data.get("emojis", []) - if emoji_keyword: - await self._handle_emoji(anchor_message, [], emoji_keyword) - has_sent_something = True - if not has_sent_something: logger.warning(f"{self.log_prefix} 回复动作未包含任何有效内容") @@ -195,40 +191,46 @@ class DefaultExpressor: logger.info(f"想要表达:{in_mind_reply}") logger.info(f"理由:{reason}") logger.info(f"生成回复: {content}\n") + info_catcher.catch_after_llm_generated( prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=model_name ) except Exception as llm_e: # 精简报错信息 - logger.error(f"{self.log_prefix}[Replier-{thinking_id}] LLM 生成失败: {llm_e}") + logger.error(f"{self.log_prefix}LLM 生成失败: {llm_e}") return None # LLM 调用失败则无法生成回复 - # 5. 处理 LLM 响应 - if not content: - logger.warning(f"{self.log_prefix}[Replier-{thinking_id}] LLM 生成了空内容。") - return None - processed_response = process_llm_response(content) + # 5. 处理 LLM 响应 + if not content: + logger.warning(f"{self.log_prefix}LLM 生成了空内容。") + return None if not processed_response: - logger.warning(f"{self.log_prefix}[Replier-{thinking_id}] 处理后的回复为空。") + logger.warning(f"{self.log_prefix}处理后的回复为空。") return None - return processed_response + reply_set = [] + for str in processed_response: + reply_seg = ("text", str) + reply_set.append(reply_seg) + + return reply_set except Exception as e: - logger.error(f"{self.log_prefix}[Replier-{thinking_id}] 回复生成意外失败: {e}") + logger.error(f"{self.log_prefix}回复生成意外失败: {e}") traceback.print_exc() return None # --- 发送器 (Sender) --- # async def _send_response_messages( - self, anchor_message: Optional[MessageRecv], response_set: List[str], thinking_id: str + self, anchor_message: Optional[MessageRecv], response_set: List[Tuple[str, str]], thinking_id: str ) -> Optional[MessageSending]: """发送回复消息 (尝试锚定到 anchor_message),使用 HeartFCSender""" chat = self.chat_stream + chat_id = self.chat_id if chat is None: logger.error(f"{self.log_prefix} 无法发送回复,chat_stream 为空。") return None @@ -236,98 +238,104 @@ class DefaultExpressor: logger.error(f"{self.log_prefix} 无法发送回复,anchor_message 为空。") return None - chat_id = self.chat_id stream_name = chat_manager.get_stream_name(chat_id) or chat_id # 获取流名称用于日志 # 检查思考过程是否仍在进行,并获取开始时间 thinking_start_time = await self.heart_fc_sender.get_thinking_start_time(chat_id, thinking_id) if thinking_start_time is None: - logger.warning(f"[{stream_name}] {thinking_id} 思考过程未找到或已结束,无法发送回复。") + logger.error(f"[{stream_name}]思考过程未找到或已结束,无法发送回复。") return None mark_head = False first_bot_msg: Optional[MessageSending] = None reply_message_ids = [] # 记录实际发送的消息ID - bot_user_info = UserInfo( - user_id=global_config.BOT_QQ, - user_nickname=global_config.BOT_NICKNAME, - platform=chat.platform, - ) for i, msg_text in enumerate(response_set): # 为每个消息片段生成唯一ID + type = msg_text[0] + data = msg_text[1] + part_message_id = f"{thinking_id}_{i}" - message_segment = Seg(type="text", data=msg_text) - bot_message = MessageSending( - message_id=part_message_id, # 使用片段的唯一ID - chat_stream=chat, - bot_user_info=bot_user_info, - sender_info=anchor_message.message_info.user_info, + message_segment = Seg(type=type, data=data) + + if type == "emoji": + is_emoji = True + else: + is_emoji = False + reply_to = not mark_head + + bot_message = self._build_single_sending_message( + anchor_message=anchor_message, + message_id=part_message_id, message_segment=message_segment, - reply=anchor_message, # 回复原始锚点 - is_head=not mark_head, - is_emoji=False, - thinking_start_time=thinking_start_time, # 传递原始思考开始时间 + reply_to=reply_to, + is_emoji=is_emoji, + thinking_id=thinking_id, ) + try: if not mark_head: mark_head = True first_bot_msg = bot_message # 保存第一个成功发送的消息对象 - await self.heart_fc_sender.type_and_send_message(bot_message, typing=False) + typing = False else: - await self.heart_fc_sender.type_and_send_message(bot_message, typing=True) + typing = True + + await self.heart_fc_sender.send_message(bot_message, has_thinking=True, typing=typing) reply_message_ids.append(part_message_id) # 记录我们生成的ID except Exception as e: - logger.error( - f"{self.log_prefix}[Sender-{thinking_id}] 发送回复片段 {i} ({part_message_id}) 时失败: {e}" - ) + logger.error(f"{self.log_prefix}发送回复片段 {i} ({part_message_id}) 时失败: {e}") # 这里可以选择是继续发送下一个片段还是中止 # 在尝试发送完所有片段后,完成原始的 thinking_id 状态 try: await self.heart_fc_sender.complete_thinking(chat_id, thinking_id) except Exception as e: - logger.error(f"{self.log_prefix}[Sender-{thinking_id}] 完成思考状态 {thinking_id} 时出错: {e}") + logger.error(f"{self.log_prefix}完成思考状态 {thinking_id} 时出错: {e}") return first_bot_msg # 返回第一个成功发送的消息对象 - async def _handle_emoji(self, anchor_message: Optional[MessageRecv], response_set: List[str], send_emoji: str = ""): - """处理表情包 (尝试锚定到 anchor_message),使用 HeartFCSender""" - if not anchor_message or not anchor_message.chat_stream: - logger.error(f"{self.log_prefix} 无法处理表情包,缺少有效的锚点消息或聊天流。") - return - - chat = anchor_message.chat_stream - + async def _choose_emoji(self, send_emoji: str): + """ + 选择表情,根据send_emoji文本选择表情,返回表情base64 + """ emoji_raw = await emoji_manager.get_emoji_for_text(send_emoji) - if emoji_raw: - emoji_path, description = emoji_raw + emoji_path, _description = emoji_raw + emoji_base64 = image_path_to_base64(emoji_path) + return emoji_base64 - emoji_cq = image_path_to_base64(emoji_path) - thinking_time_point = round(time.time(), 2) # 用于唯一ID - message_segment = Seg(type="emoji", data=emoji_cq) - bot_user_info = UserInfo( - user_id=global_config.BOT_QQ, - user_nickname=global_config.BOT_NICKNAME, - platform=anchor_message.message_info.platform, - ) - bot_message = MessageSending( - message_id="me" + str(thinking_time_point), # 表情消息的唯一ID - chat_stream=chat, - bot_user_info=bot_user_info, - sender_info=anchor_message.message_info.user_info, - message_segment=message_segment, - reply=anchor_message, # 回复原始锚点 - is_head=False, # 表情通常不是头部消息 - is_emoji=True, - # 不需要 thinking_start_time - ) + async def _build_single_sending_message( + self, + anchor_message: MessageRecv, + message_id: str, + message_segment: Seg, + reply_to: bool, + is_emoji: bool, + thinking_id: str, + ) -> MessageSending: + """构建单个发送消息""" - try: - await self.heart_fc_sender.send_and_store(bot_message) - except Exception as e: - logger.error(f"{self.log_prefix} 发送表情包 {bot_message.message_info.message_id} 时失败: {e}") + thinking_start_time = await self.heart_fc_sender.get_thinking_start_time(self.chat_id, thinking_id) + bot_user_info = UserInfo( + user_id=global_config.BOT_QQ, + user_nickname=global_config.BOT_NICKNAME, + platform=self.chat_stream.platform, + ) + + bot_message = MessageSending( + message_id=message_id, # 使用片段的唯一ID + chat_stream=self.chat_stream, + bot_user_info=bot_user_info, + sender_info=anchor_message.message_info.user_info, + message_segment=message_segment, + reply=anchor_message, # 回复原始锚点 + is_head=reply_to, + is_emoji=is_emoji, + thinking_start_time=thinking_start_time, # 传递原始思考开始时间 + ) + + return bot_message diff --git a/src/chat/focus_chat/expressors/exprssion_learner.py b/src/chat/focus_chat/expressors/exprssion_learner.py index 2d688b71b..57908402e 100644 --- a/src/chat/focus_chat/expressors/exprssion_learner.py +++ b/src/chat/focus_chat/expressors/exprssion_learner.py @@ -58,9 +58,9 @@ def init_prompt() -> None: {chat_str} 请从上面这段群聊中概括除了人名为"麦麦"之外的人的语法和句法特点,只考虑纯文字,不要考虑表情包和图片 -不要总结【图片】,【动画表情】,[图片],[动画表情],不总结 表情符号 +不要总结【图片】,【动画表情】,[图片],[动画表情],不总结 表情符号 at @ 回复 和[回复] 不要涉及具体的人名,只考虑语法和句法特点, -语法和句法特点要包括,句子长短(具体字数),如何分局,有何种语病,如何拆分句子。 +语法和句法特点要包括,句子长短(具体字数),有何种语病,如何拆分句子。 总结成如下格式的规律,总结的内容要简洁,不浮夸: 当"xxx"时,可以"xxx" diff --git a/src/chat/focus_chat/heartFC_chat.py b/src/chat/focus_chat/heartFC_chat.py index 641a45b47..d0c10d6b4 100644 --- a/src/chat/focus_chat/heartFC_chat.py +++ b/src/chat/focus_chat/heartFC_chat.py @@ -30,7 +30,7 @@ from src.chat.heart_flow.observation.hfcloop_observation import HFCloopObservati from src.chat.heart_flow.observation.working_observation import WorkingObservation from src.chat.focus_chat.info_processors.tool_processor import ToolProcessor from src.chat.focus_chat.expressors.default_expressor import DefaultExpressor -from src.chat.focus_chat.hfc_utils import _create_empty_anchor_message +from src.chat.focus_chat.hfc_utils import create_empty_anchor_message, parse_thinking_id_to_timestamp from src.chat.focus_chat.memory_activator import MemoryActivator install(extra_lines=3) @@ -203,9 +203,9 @@ class HeartFChatting: self._cycle_counter = 0 self._cycle_history: Deque[CycleDetail] = deque(maxlen=10) # 保留最近10个循环的信息 self._current_cycle: Optional[CycleDetail] = None - self._lian_xu_bu_hui_fu_ci_shu: int = 0 # <--- 新增:连续不回复计数器 + self.total_no_reply_count: int = 0 # <--- 新增:连续不回复计数器 self._shutting_down: bool = False # <--- 新增:关闭标志位 - self._lian_xu_deng_dai_shi_jian: float = 0.0 # <--- 新增:累计等待时间 + self.total_waiting_time: float = 0.0 # <--- 新增:累计等待时间 async def _initialize(self) -> bool: """ @@ -325,11 +325,12 @@ class HeartFChatting: await asyncio.sleep(0.1) # 短暂等待避免空转 continue - # 记录规划开始时间点 - planner_start_db_time = time.time() + # thinking_id 是思考过程的ID,用于标记每一轮思考 + thinking_id = "tid" + str(round(time.time(), 2)) # 主循环:思考->决策->执行 - action_taken, thinking_id = await self._think_plan_execute_loop(cycle_timers, planner_start_db_time) + + action_taken = await self._think_plan_execute_loop(cycle_timers, thinking_id) # 更新循环信息 self._current_cycle.set_thinking_id(thinking_id) @@ -391,9 +392,8 @@ class HeartFChatting: if acquired and self._processing_lock.locked(): self._processing_lock.release() - async def _think_plan_execute_loop(self, cycle_timers: dict, planner_start_db_time: float) -> tuple[bool, str]: + async def _think_plan_execute_loop(self, cycle_timers: dict, thinking_id: str) -> tuple[bool, str]: try: - await asyncio.sleep(1) with Timer("观察", cycle_timers): await self.observations[0].observe() await self.memory_observation.observe() @@ -515,7 +515,7 @@ class HeartFChatting: self.hfcloop_observation.add_loop_info(self._current_cycle) - return await self._handle_action(action, reasoning, action_data, cycle_timers, planner_start_db_time) + return await self._handle_action(action, reasoning, action_data, cycle_timers, thinking_id) except Exception as e: logger.error(f"{self.log_prefix} 并行+串行处理失败: {e}") @@ -523,7 +523,12 @@ class HeartFChatting: return False, "" async def _handle_action( - self, action: str, reasoning: str, action_data: dict, cycle_timers: dict, planner_start_db_time: float + self, + action: str, + reasoning: str, + action_data: dict, + cycle_timers: dict, + thinking_id: str, ) -> tuple[bool, str]: """ 处理规划动作 @@ -550,17 +555,17 @@ class HeartFChatting: try: if action == "reply": - return await handler(reasoning, action_data, cycle_timers) + return await handler(reasoning, action_data, cycle_timers, thinking_id) else: # no_reply - return await handler(reasoning, planner_start_db_time, cycle_timers), "" + return await handler(reasoning, cycle_timers, thinking_id) except Exception as e: logger.error(f"{self.log_prefix} 处理{action}时出错: {e}") # 出错时也重置计数器 - self._lian_xu_bu_hui_fu_ci_shu = 0 - self._lian_xu_deng_dai_shi_jian = 0.0 + self.total_no_reply_count = 0 + self.total_waiting_time = 0.0 return False, "" - async def _handle_no_reply(self, reasoning: str, planner_start_db_time: float, cycle_timers: dict) -> bool: + async def _handle_no_reply(self, reasoning: str, cycle_timers: dict, thinking_id: str) -> bool: """ 处理不回复的情况 @@ -584,53 +589,50 @@ class HeartFChatting: try: with Timer("等待新消息", cycle_timers): # 等待新消息、超时或关闭信号,并获取结果 - await self._wait_for_new_message(observation, planner_start_db_time, self.log_prefix) + await self._wait_for_new_message(observation, thinking_id, self.log_prefix) # 从计时器获取实际等待时间 current_waiting = cycle_timers.get("等待新消息", 0.0) if not self._shutting_down: - self._lian_xu_bu_hui_fu_ci_shu += 1 - self._lian_xu_deng_dai_shi_jian += current_waiting # 累加等待时间 + self.total_no_reply_count += 1 + self.total_waiting_time += current_waiting # 累加等待时间 logger.debug( - f"{self.log_prefix} 连续不回复计数增加: {self._lian_xu_bu_hui_fu_ci_shu}/{CONSECUTIVE_NO_REPLY_THRESHOLD}, " - f"本次等待: {current_waiting:.2f}秒, 累计等待: {self._lian_xu_deng_dai_shi_jian:.2f}秒" + f"{self.log_prefix} 连续不回复计数增加: {self.total_no_reply_count}/{CONSECUTIVE_NO_REPLY_THRESHOLD}, " + f"本次等待: {current_waiting:.2f}秒, 累计等待: {self.total_waiting_time:.2f}秒" ) # 检查是否同时达到次数和时间阈值 time_threshold = 0.66 * WAITING_TIME_THRESHOLD * CONSECUTIVE_NO_REPLY_THRESHOLD if ( - self._lian_xu_bu_hui_fu_ci_shu >= CONSECUTIVE_NO_REPLY_THRESHOLD - and self._lian_xu_deng_dai_shi_jian >= time_threshold + self.total_no_reply_count >= CONSECUTIVE_NO_REPLY_THRESHOLD + and self.total_waiting_time >= time_threshold ): logger.info( - f"{self.log_prefix} 连续不回复达到阈值 ({self._lian_xu_bu_hui_fu_ci_shu}次) " - f"且累计等待时间达到 {self._lian_xu_deng_dai_shi_jian:.2f}秒 (阈值 {time_threshold}秒)," + f"{self.log_prefix} 连续不回复达到阈值 ({self.total_no_reply_count}次) " + f"且累计等待时间达到 {self.total_waiting_time:.2f}秒 (阈值 {time_threshold}秒)," f"调用回调请求状态转换" ) # 调用回调。注意:这里不重置计数器和时间,依赖回调函数成功改变状态来隐式重置上下文。 await self.on_consecutive_no_reply_callback() - elif self._lian_xu_bu_hui_fu_ci_shu >= CONSECUTIVE_NO_REPLY_THRESHOLD: + elif self.total_no_reply_count >= CONSECUTIVE_NO_REPLY_THRESHOLD: # 仅次数达到阈值,但时间未达到 logger.debug( - f"{self.log_prefix} 连续不回复次数达到阈值 ({self._lian_xu_bu_hui_fu_ci_shu}次) " - f"但累计等待时间 {self._lian_xu_deng_dai_shi_jian:.2f}秒 未达到时间阈值 ({time_threshold}秒),暂不调用回调" + f"{self.log_prefix} 连续不回复次数达到阈值 ({self.total_no_reply_count}次) " + f"但累计等待时间 {self.total_waiting_time:.2f}秒 未达到时间阈值 ({time_threshold}秒),暂不调用回调" ) # else: 次数和时间都未达到阈值,不做处理 - return True + return True, thinking_id except asyncio.CancelledError: - # 如果在等待过程中任务被取消(可能是因为 shutdown) logger.info(f"{self.log_prefix} 处理 'no_reply' 时等待被中断 (CancelledError)") - # 让异常向上传播,由 _hfc_loop 的异常处理逻辑接管 raise except Exception as e: # 捕获调用管理器或其他地方可能发生的错误 logger.error(f"{self.log_prefix} 处理 'no_reply' 时发生错误: {e}") logger.error(traceback.format_exc()) - # 发生意外错误时,可以选择是否重置计数器,这里选择不重置 - return False # 表示动作未成功 + return False, thinking_id - async def _wait_for_new_message(self, observation, planner_start_db_time: float, log_prefix: str) -> bool: + async def _wait_for_new_message(self, observation: ChattingObservation, thinking_id: str, log_prefix: str) -> bool: """ 等待新消息 或 检测到关闭信号 @@ -650,8 +652,10 @@ class HeartFChatting: return False # 表示因为关闭而退出 # ----------------------------------- + thinking_id_timestamp = parse_thinking_id_to_timestamp(thinking_id) + # 检查新消息 - if await observation.has_new_messages_since(planner_start_db_time): + if await observation.has_new_messages_since(thinking_id_timestamp): logger.info(f"{log_prefix} 检测到新消息") return True @@ -925,38 +929,42 @@ class HeartFChatting: "llm_error": llm_error, # 返回错误状态 } - async def _handle_reply(self, reasoning: str, reply_data: dict, cycle_timers: dict) -> tuple[bool, str]: + async def _handle_reply( + self, reasoning: str, reply_data: dict, cycle_timers: dict, thinking_id: str + ) -> tuple[bool, str]: """ 处理统一的回复动作 - 可包含文本和表情,顺序任意 reply_data格式: { - "text": ["你好啊", "今天天气真不错"], # 文本内容列表(可选) - "emojis": ["微笑", "阳光"] # 表情关键词列表(可选) + "text": "你好啊" # 文本内容列表(可选) + "target": "锚定消息", # 锚定消息的文本内容 + "emojis": "微笑" # 表情关键词列表(可选) } """ # 重置连续不回复计数器 - self._lian_xu_bu_hui_fu_ci_shu = 0 - self._lian_xu_deng_dai_shi_jian = 0.0 + self.total_no_reply_count = 0 + self.total_waiting_time = 0.0 - # 获取锚定消息 + # 从聊天观察获取锚定消息 observations: ChattingObservation = self.observations[0] anchor_message = observations.serch_message_by_text(reply_data["target"]) # 如果没有找到锚点消息,创建一个占位符 if not anchor_message: logger.info(f"{self.log_prefix} 未找到锚点消息,创建占位符") - anchor_message = await _create_empty_anchor_message( + anchor_message = await create_empty_anchor_message( self.chat_stream.platform, self.chat_stream.group_info, self.chat_stream ) - if not anchor_message: - logger.error(f"{self.log_prefix} 创建占位符失败,无法继续处理回复") - return False, "" else: anchor_message.update_chat_stream(self.chat_stream) success, reply_set = await self.expressor.deal_reply( - cycle_timers=cycle_timers, action_data=reply_data, anchor_message=anchor_message, reasoning=reasoning + cycle_timers=cycle_timers, + action_data=reply_data, + anchor_message=anchor_message, + reasoning=reasoning, + thinking_id=thinking_id, ) reply_text = "" diff --git a/src/chat/focus_chat/heartFC_sender.py b/src/chat/focus_chat/heartFC_sender.py index 3443e096c..846ad1fe8 100644 --- a/src/chat/focus_chat/heartFC_sender.py +++ b/src/chat/focus_chat/heartFC_sender.py @@ -7,6 +7,7 @@ from src.chat.utils.utils import truncate_message from src.common.logger_manager import get_logger from src.chat.utils.utils import calculate_typing_time from rich.traceback import install +import traceback install(extra_lines=3) @@ -16,17 +17,16 @@ logger = get_logger("sender") async def send_message(message: MessageSending) -> None: """合并后的消息发送函数,包含WS发送和日志记录""" - message_preview = truncate_message(message.processed_plain_text) + message_preview = truncate_message(message.processed_plain_text, max_length=40) try: # 直接调用API发送消息 await global_api.send_message(message) - logger.success(f"发送消息 '{message_preview}' 成功") + logger.success(f"已将消息 '{message_preview}' 发往平台'{message.message_info.platform}'") except Exception as e: - logger.error(f"发送消息 '{message_preview}' 失败: {str(e)}") - if not message.message_info.platform: - raise ValueError(f"未找到平台:{message.message_info.platform} 的url配置,请检查配置文件") from e + logger.error(f"发送消息 '{message_preview}' 发往平台'{message.message_info.platform}' 失败: {str(e)}") + traceback.print_exc() raise e # 重新抛出其他异常 @@ -66,21 +66,24 @@ class HeartFCSender: del self.thinking_messages[chat_id] logger.debug(f"[{chat_id}] Removed empty thinking message container.") - def is_thinking(self, chat_id: str, message_id: str) -> bool: - """检查指定的消息 ID 是否当前正处于思考状态。""" - return chat_id in self.thinking_messages and message_id in self.thinking_messages[chat_id] - async def get_thinking_start_time(self, chat_id: str, message_id: str) -> Optional[float]: """获取已注册思考消息的开始时间。""" async with self._thinking_lock: thinking_message = self.thinking_messages.get(chat_id, {}).get(message_id) return thinking_message.thinking_start_time if thinking_message else None - async def type_and_send_message(self, message: MessageSending, typing=False): + async def send_message(self, message: MessageSending, has_thinking=False, typing=False): """ - 立即处理、发送并存储单个 MessageSending 消息。 - 调用此方法前,应先调用 register_thinking 注册对应的思考消息。 - 此方法执行后会调用 complete_thinking 清理思考状态。 + 处理、发送并存储一条消息。 + + 参数: + message: MessageSending 对象,待发送的消息。 + has_thinking: 是否管理思考状态,表情包无思考状态(如需调用 register_thinking/complete_thinking)。 + typing: 是否模拟打字等待(根据 has_thinking 控制等待时长)。 + + 用法: + - has_thinking=True 时,自动处理思考消息的时间和清理。 + - typing=True 时,发送前会有打字等待。 """ if not message.chat_stream: logger.error("消息缺少 chat_stream,无法发送") @@ -93,27 +96,29 @@ class HeartFCSender: message_id = message.message_info.message_id try: - _ = message.update_thinking_time() + if has_thinking: + _ = message.update_thinking_time() - # --- 条件应用 set_reply 逻辑 --- - if ( - message.is_head - and not message.is_private_message() - and message.reply.processed_plain_text != "[System Trigger Context]" - ): - logger.debug(f"[{chat_id}] 应用 set_reply 逻辑: {message.processed_plain_text[:20]}...") - message.set_reply(message.reply) - # --- 结束条件 set_reply --- + # --- 条件应用 set_reply 逻辑 --- + if ( + message.is_head + and not message.is_private_message() + and message.reply.processed_plain_text != "[System Trigger Context]" + ): + logger.debug(f"[{chat_id}] 应用 set_reply 逻辑: {message.processed_plain_text[:20]}...") await message.process() if typing: - typing_time = calculate_typing_time( - input_string=message.processed_plain_text, - thinking_start_time=message.thinking_start_time, - is_emoji=message.is_emoji, - ) - await asyncio.sleep(typing_time) + if has_thinking: + typing_time = calculate_typing_time( + input_string=message.processed_plain_text, + thinking_start_time=message.thinking_start_time, + is_emoji=message.is_emoji, + ) + await asyncio.sleep(typing_time) + else: + await asyncio.sleep(0.5) await send_message(message) await self.storage.store_message(message, message.chat_stream) @@ -123,30 +128,3 @@ class HeartFCSender: raise e finally: await self.complete_thinking(chat_id, message_id) - - async def send_and_store(self, message: MessageSending): - """处理、发送并存储单个消息,不涉及思考状态管理。""" - if not message.chat_stream: - logger.error(f"[{message.message_info.platform or 'UnknownPlatform'}] 消息缺少 chat_stream,无法发送") - return - if not message.message_info or not message.message_info.message_id: - logger.error( - f"[{message.chat_stream.stream_id if message.chat_stream else 'UnknownStream'}] 消息缺少 message_info 或 message_id,无法发送" - ) - return - - chat_id = message.chat_stream.stream_id - message_id = message.message_info.message_id # 获取消息ID用于日志 - - try: - await message.process() - - await asyncio.sleep(0.5) - - await send_message(message) # 使用现有的发送方法 - await self.storage.store_message(message, message.chat_stream) # 使用现有的存储方法 - - except Exception as e: - logger.error(f"[{chat_id}] 处理或存储消息 {message_id} 时出错: {e}") - # 重新抛出异常,让调用者知道失败了 - raise e diff --git a/src/chat/focus_chat/heartflow_processor.py b/src/chat/focus_chat/heartflow_processor.py index 0b7500a3a..bbfa4ce46 100644 --- a/src/chat/focus_chat/heartflow_processor.py +++ b/src/chat/focus_chat/heartflow_processor.py @@ -9,7 +9,8 @@ from maim_message import Seg from src.chat.heart_flow.heartflow import heartflow from src.common.logger_manager import get_logger from ..message_receive.chat_stream import chat_manager -from ..message_receive.message_buffer import message_buffer + +# from ..message_receive.message_buffer import message_buffer from ..utils.timer_calculator import Timer from src.chat.person_info.relationship_manager import relationship_manager from typing import Optional, Tuple, Dict, Any @@ -169,7 +170,7 @@ class HeartFCProcessor: messageinfo = message.message_info # 2. 消息缓冲与流程序化 - await message_buffer.start_caching_messages(message) + # await message_buffer.start_caching_messages(message) chat = await chat_manager.get_or_create_stream( platform=messageinfo.platform, @@ -188,16 +189,16 @@ class HeartFCProcessor: return # 4. 缓冲检查 - buffer_result = await message_buffer.query_buffer_result(message) - if not buffer_result: - msg_type = _get_message_type(message) - type_messages = { - "text": f"触发缓冲,消息:{message.processed_plain_text}", - "image": "触发缓冲,表情包/图片等待中", - "seglist": "触发缓冲,消息列表等待中", - } - logger.debug(type_messages.get(msg_type, "触发未知类型缓冲")) - return + # buffer_result = await message_buffer.query_buffer_result(message) + # if not buffer_result: + # msg_type = _get_message_type(message) + # type_messages = { + # "text": f"触发缓冲,消息:{message.processed_plain_text}", + # "image": "触发缓冲,表情包/图片等待中", + # "seglist": "触发缓冲,消息列表等待中", + # } + # logger.debug(type_messages.get(msg_type, "触发未知类型缓冲")) + # return # 5. 消息存储 await self.storage.store_message(message, chat) @@ -210,7 +211,7 @@ class HeartFCProcessor: # 7. 日志记录 mes_name = chat.group_info.group_name if chat.group_info else "私聊" - current_time = time.strftime("%H点%M分%S秒", time.localtime(message.message_info.time)) + current_time = time.strftime("%H:%M:%S", time.localtime(message.message_info.time)) logger.info( f"[{current_time}][{mes_name}]" f"{userinfo.user_nickname}:" diff --git a/src/chat/focus_chat/heartflow_prompt_builder.py b/src/chat/focus_chat/heartflow_prompt_builder.py index 404fb3e69..9d39ae83d 100644 --- a/src/chat/focus_chat/heartflow_prompt_builder.py +++ b/src/chat/focus_chat/heartflow_prompt_builder.py @@ -32,11 +32,11 @@ def init_prompt(): 以上是聊天内容,你需要了解聊天记录中的内容 {chat_target} -你的名字是{bot_name},{prompt_personality},在这聊天中,"{target_message}"引起了你的注意,你想表达:{in_mind_reply},原因是:{reason}。你现在要思考怎么回复 +你的名字是{bot_name},{prompt_personality},在这聊天中,"{target_message}"引起了你的注意,对这句话,你想表达:{in_mind_reply},原因是:{reason}。你现在要思考怎么回复 你需要使用合适的语法和句法,参考聊天内容,组织一条日常且口语化的回复。 请你根据情景使用以下句法: {grammar_habbits} -回复尽量简短一些。可以参考贴吧,知乎和微博的回复风格,你可以完全重组回复,保留最基本的表达含义就好,但注意回复要简短。 +回复尽量简短一些。可以参考贴吧,知乎和微博的回复风格,你可以完全重组回复,保留最基本的表达含义就好,但注意回复要简短,但重组后保持语意通顺。 回复不要浮夸,不要用夸张修辞,平淡一些。不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 ),只输出一条回复就好。 现在,你说: """, diff --git a/src/chat/focus_chat/hfc_utils.py b/src/chat/focus_chat/hfc_utils.py index 5b29a803e..36907c4c0 100644 --- a/src/chat/focus_chat/hfc_utils.py +++ b/src/chat/focus_chat/hfc_utils.py @@ -1,5 +1,4 @@ import time -import traceback from typing import Optional from src.chat.message_receive.message import MessageRecv, BaseMessageInfo from src.chat.message_receive.chat_stream import ChatStream @@ -10,7 +9,7 @@ import json logger = get_logger(__name__) -async def _create_empty_anchor_message( +async def create_empty_anchor_message( platform: str, group_info: dict, chat_stream: ChatStream ) -> Optional[MessageRecv]: """ @@ -18,31 +17,36 @@ async def _create_empty_anchor_message( 如果重构失败或观察为空,则创建一个占位符。 """ - try: - placeholder_id = f"mid_pf_{int(time.time() * 1000)}" - placeholder_user = UserInfo(user_id="system_trigger", user_nickname="System Trigger", platform=platform) - placeholder_msg_info = BaseMessageInfo( - message_id=placeholder_id, - platform=platform, - group_info=group_info, - user_info=placeholder_user, - time=time.time(), - ) - placeholder_msg_dict = { - "message_info": placeholder_msg_info.to_dict(), - "processed_plain_text": "[System Trigger Context]", - "raw_message": "", - "time": placeholder_msg_info.time, - } - anchor_message = MessageRecv(placeholder_msg_dict) - anchor_message.update_chat_stream(chat_stream) - logger.debug(f"创建占位符锚点消息: ID={anchor_message.message_info.message_id}") - return anchor_message + placeholder_id = f"mid_pf_{int(time.time() * 1000)}" + placeholder_user = UserInfo(user_id="system_trigger", user_nickname="System Trigger", platform=platform) + placeholder_msg_info = BaseMessageInfo( + message_id=placeholder_id, + platform=platform, + group_info=group_info, + user_info=placeholder_user, + time=time.time(), + ) + placeholder_msg_dict = { + "message_info": placeholder_msg_info.to_dict(), + "processed_plain_text": "[System Trigger Context]", + "raw_message": "", + "time": placeholder_msg_info.time, + } + anchor_message = MessageRecv(placeholder_msg_dict) + anchor_message.update_chat_stream(chat_stream) - except Exception as e: - logger.error(f"Error getting/creating anchor message: {e}") - logger.error(traceback.format_exc()) - return None + return anchor_message + + +def parse_thinking_id_to_timestamp(thinking_id: str) -> float: + """ + 将形如 'tid' 的 thinking_id 解析回 float 时间戳 + 例如: 'tid1718251234.56' -> 1718251234.56 + """ + if not thinking_id.startswith("tid"): + raise ValueError("thinking_id 格式不正确") + ts_str = thinking_id[3:] + return float(ts_str) def get_keywords_from_json(json_str: str) -> list[str]: diff --git a/src/chat/person_info/relationship_manager.py b/src/chat/person_info/relationship_manager.py index 61743b160..c8a443857 100644 --- a/src/chat/person_info/relationship_manager.py +++ b/src/chat/person_info/relationship_manager.py @@ -94,13 +94,23 @@ class RelationshipManager: return False @staticmethod - async def first_knowing_some_one(platform, user_id, user_nickname, user_cardname, user_avatar): + async def first_knowing_some_one( + platform: str, user_id: str, user_nickname: str, user_cardname: str, user_avatar: str + ): """判断是否认识某人""" person_id = person_info_manager.get_person_id(platform, user_id) - await person_info_manager.update_one_field(person_id, "nickname", user_nickname) - # await person_info_manager.update_one_field(person_id, "user_cardname", user_cardname) - # await person_info_manager.update_one_field(person_id, "user_avatar", user_avatar) - await person_info_manager.qv_person_name(person_id, user_nickname, user_cardname, user_avatar) + data = { + "platform": platform, + "user_id": user_id, + "nickname": user_nickname, + "konw_time": int(time.time()), + } + await person_info_manager.update_one_field( + person_id=person_id, field_name="nickname", value=user_nickname, data=data + ) + await person_info_manager.qv_person_name( + person_id=person_id, user_nickname=user_nickname, user_cardname=user_cardname, user_avatar=user_avatar + ) async def calculate_update_relationship_value(self, user_info: UserInfo, platform: str, label: str, stance: str): """计算并变更关系值 diff --git a/src/config/config.py b/src/config/config.py index bbd3b9236..b186f3b83 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -622,12 +622,6 @@ class BotConfig: # config.ban_user_id = set(groups_config.get("ban_user_id", [])) config.ban_user_id = set(str(user) for user in groups_config.get("ban_user_id", [])) - def platforms(parent: dict): - platforms_config = parent["platforms"] - if platforms_config and isinstance(platforms_config, dict): - for k in platforms_config.keys(): - config.api_urls[k] = platforms_config[k] - def experimental(parent: dict): experimental_config = parent["experimental"] config.enable_friend_chat = experimental_config.get("enable_friend_chat", config.enable_friend_chat) @@ -662,7 +656,6 @@ class BotConfig: "remote": {"func": remote, "support": ">=0.0.10", "necessary": False}, "keywords_reaction": {"func": keywords_reaction, "support": ">=0.0.2", "necessary": False}, "chinese_typo": {"func": chinese_typo, "support": ">=0.0.3", "necessary": False}, - "platforms": {"func": platforms, "support": ">=1.0.0"}, "response_splitter": {"func": response_splitter, "support": ">=0.0.11", "necessary": False}, "experimental": {"func": experimental, "support": ">=0.0.11", "necessary": False}, "chat": {"func": chat, "support": ">=1.6.0", "necessary": False}, diff --git a/src/chat/focus_chat/heartFC_chatting_logic.md b/src/heartFC_chatting_logic.md similarity index 100% rename from src/chat/focus_chat/heartFC_chatting_logic.md rename to src/heartFC_chatting_logic.md diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 81bafe9c6..931afe2ed 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -59,7 +59,7 @@ gender = "男" # 性别 appearance = "用几句话描述外貌特征" # 外貌特征 该选项还在调试中,暂时未生效 [platforms] # 必填项目,填写每个平台适配器提供的链接 -nonebot-qq="http://127.0.0.1:18002/api/message" +qq="http://127.0.0.1:18002/api/message" [chat] #麦麦的聊天通用设置 allow_focus_mode = false # 是否允许专注聊天状态