From 6db8dc01d5b962380a550b70e13c57eaaebeb3b1 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 1 May 2025 01:21:48 +0800 Subject: [PATCH 01/18] =?UTF-8?q?feat=EF=BC=9A=E9=87=87=E7=94=A8=E7=9B=B4?= =?UTF-8?q?=E6=8E=A5=E8=BE=93=E5=87=BA=E6=B3=95=E6=9E=84=E5=BB=BA=E5=8A=A8?= =?UTF-8?q?=E4=BD=9Cplanner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/logger.py | 4 +- src/heart_flow/mai_state_manager.py | 7 +- src/heart_flow/sub_heartflow.py | 2 +- src/plugins/heartFC_chat/heartFC_chat.py | 327 ++++++++---------- .../heartFC_chat/heartflow_prompt_builder.py | 47 ++- 5 files changed, 192 insertions(+), 195 deletions(-) diff --git a/src/common/logger.py b/src/common/logger.py index b5317d58..12a9c135 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -634,12 +634,14 @@ HFC_STYLE_CONFIG = { }, "simple": { "console_format": ( - "{time:MM-DD HH:mm} | 专注聊天 | {message}" + "{time:MM-DD HH:mm} | 专注聊天 | {message}" ), "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 专注聊天 | {message}", }, } + + CONFIRM_STYLE_CONFIG = { "console_format": "{message}", # noqa: E501 "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | EULA与PRIVACY确认 | {message}", diff --git a/src/heart_flow/mai_state_manager.py b/src/heart_flow/mai_state_manager.py index cd739344..20f8f8ad 100644 --- a/src/heart_flow/mai_state_manager.py +++ b/src/heart_flow/mai_state_manager.py @@ -10,8 +10,11 @@ logger = get_logger("mai_state") # -- 状态相关的可配置参数 (可以从 glocal_config 加载) -- -# enable_unlimited_hfc_chat = True # 调试用:无限专注聊天 -enable_unlimited_hfc_chat = False +# The line `enable_unlimited_hfc_chat = False` is setting a configuration parameter that controls +# whether a specific debugging feature is enabled or not. When `enable_unlimited_hfc_chat` is set to +# `False`, it means that the debugging feature for unlimited focused chatting is disabled. +enable_unlimited_hfc_chat = True # 调试用:无限专注聊天 +# enable_unlimited_hfc_chat = False prevent_offline_state = True # 目前默认不启用OFFLINE状态 diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index 4a48e977..291800cf 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -18,7 +18,7 @@ from src.heart_flow.sub_mind import SubMind # 定义常量 (从 interest.py 移动过来) MAX_INTEREST = 15.0 -logger = get_logger("subheartflow") +logger = get_logger("sub_heartflow") PROBABILITY_INCREASE_RATE_PER_SECOND = 0.1 PROBABILITY_DECREASE_RATE_PER_SECOND = 0.1 diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index bfdf2d6a..5b1e3151 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -2,6 +2,7 @@ import asyncio import time import traceback import random # <--- 添加导入 +import json # <--- 确保导入 json from typing import List, Optional, Dict, Any, Deque, Callable, Coroutine from collections import deque from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending @@ -37,7 +38,7 @@ EMOJI_SEND_PRO = 0.3 # 设置一个概率,比如 30% 才真的发 CONSECUTIVE_NO_REPLY_THRESHOLD = 3 # 连续不回复的阈值 -logger = get_logger("HFC") # Logger Name Changed +logger = get_logger("hfc") # Logger Name Changed # 默认动作定义 @@ -119,35 +120,6 @@ class ActionManager: """重置为默认动作集""" self._available_actions = DEFAULT_ACTIONS.copy() - def get_planner_tool_definition(self) -> List[Dict[str, Any]]: - """获取当前动作集对应的规划器工具定义""" - return [ - { - "type": "function", - "function": { - "name": "decide_reply_action", - "description": "根据当前聊天内容和上下文,决定机器人是否应该回复以及如何回复。", - "parameters": { - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": list(self._available_actions.keys()), - "description": "决定采取的行动:" - + ", ".join([f"'{k}'({v})" for k, v in self._available_actions.items()]), - }, - "reasoning": {"type": "string", "description": "做出此决定的简要理由。"}, - "emoji_query": { - "type": "string", - "description": "如果行动是'emoji_reply',指定表情的主题或概念。如果行动是'text_reply'且希望在文本后追加表情,也在此指定表情主题。", - }, - }, - "required": ["action", "reasoning"], - }, - }, - } - ] - # 在文件开头添加自定义异常类 class HeartFCError(Exception): @@ -222,7 +194,6 @@ class HeartFChatting: max_tokens=256, request_type="response_heartflow", ) - self.tool_user = ToolUser() self.heart_fc_sender = HeartFCSender() # LLM规划器配置 @@ -784,225 +755,208 @@ class HeartFChatting: async def _planner(self, current_mind: str, cycle_timers: dict, is_re_planned: bool = False) -> Dict[str, Any]: """ 规划器 (Planner): 使用LLM根据上下文决定是否和如何回复。 + 重构为:让LLM返回结构化JSON文本,然后在代码中解析。 参数: current_mind: 子思维的当前思考结果 cycle_timers: 计时器字典 - is_re_planned: 是否为重新规划 + is_re_planned: 是否为重新规划 (此重构中暂时简化,不处理 is_re_planned 的特殊逻辑) """ - logger.info(f"{self.log_prefix}[Planner] 开始{'重新' if is_re_planned else ''}执行规划器") + logger.info(f"{self.log_prefix}[Planner] 开始执行规划器 (JSON解析模式)") - # --- 新增:检查历史动作并调整可用动作 --- - lian_xu_wen_ben_hui_fu = 0 # 连续文本回复次数 actions_to_remove_temporarily = [] - probability_roll = random.random() # 在循环外掷骰子一次,用于概率判断 - - # 反向遍历最近的循环历史 + # --- 检查历史动作并决定临时移除动作 (逻辑保持不变) --- + lian_xu_wen_ben_hui_fu = 0 + probability_roll = random.random() for cycle in reversed(self._cycle_history): - # 只关心实际执行了动作的循环 if cycle.action_taken: if cycle.action_type == "text_reply": lian_xu_wen_ben_hui_fu += 1 else: - break # 遇到非文本回复,中断计数 - # 检查最近的3个循环即可,避免检查过多历史 (如果历史很长) + break if len(self._cycle_history) > 0 and cycle.cycle_id <= self._cycle_history[0].cycle_id + ( len(self._cycle_history) - 4 ): break - logger.debug(f"{self.log_prefix}[Planner] 检测到连续文本回复次数: {lian_xu_wen_ben_hui_fu}") - # 根据连续次数决定临时移除哪些动作 if lian_xu_wen_ben_hui_fu >= 3: logger.info(f"{self.log_prefix}[Planner] 连续回复 >= 3 次,强制移除 text_reply 和 emoji_reply") actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"]) elif lian_xu_wen_ben_hui_fu == 2: - if probability_roll < 0.8: # 80% 概率 + if probability_roll < 0.8: logger.info(f"{self.log_prefix}[Planner] 连续回复 2 次,80% 概率移除 text_reply 和 emoji_reply (触发)") actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"]) else: - logger.info( - f"{self.log_prefix}[Planner] 连续回复 2 次,80% 概率移除 text_reply 和 emoji_reply (未触发)" - ) + logger.info(f"{self.log_prefix}[Planner] 连续回复 2 次,80% 概率移除 text_reply 和 emoji_reply (未触发)") elif lian_xu_wen_ben_hui_fu == 1: - if probability_roll < 0.4: # 40% 概率 + if probability_roll < 0.4: logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次,40% 概率移除 text_reply (触发)") actions_to_remove_temporarily.append("text_reply") else: logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次,40% 概率移除 text_reply (未触发)") - # 如果 lian_xu_wen_ben_hui_fu == 0,则不移除任何动作 - # --- 结束:检查历史动作 --- + # --- 结束检查历史动作 --- # 获取观察信息 observation = self.observations[0] - if is_re_planned: - await observation.observe() + # if is_re_planned: # 暂时简化,不处理重新规划 + # await observation.observe() observed_messages = observation.talking_message observed_messages_str = observation.talking_message_str_truncate - # --- 使用 LLM 进行决策 --- # - reasoning = "默认决策或获取决策失败" - llm_error = False # LLM错误标志 - arguments = None # 初始化参数变量 - emoji_query = "" # <--- 在这里初始化 emoji_query + # --- 使用 LLM 进行决策 (JSON 输出模式) --- # + action = "no_reply" # 默认动作 + reasoning = "规划器初始化默认" + emoji_query = "" + llm_error = False # LLM 请求或解析错误标志 + + # 获取我们将传递给 prompt 构建器和用于验证的当前可用动作 + current_available_actions = self.action_manager.get_available_actions() try: - # --- 新增:应用临时动作移除 --- + # --- 应用临时动作移除 --- if actions_to_remove_temporarily: self.action_manager.temporarily_remove_actions(actions_to_remove_temporarily) + # 更新 current_available_actions 以反映移除后的状态 + current_available_actions = self.action_manager.get_available_actions() logger.debug( - f"{self.log_prefix}[Planner] 临时移除的动作: {actions_to_remove_temporarily}, 当前可用: {list(self.action_manager.get_available_actions().keys())}" + f"{self.log_prefix}[Planner] 临时移除的动作: {actions_to_remove_temporarily}, 当前可用: {list(current_available_actions.keys())}" ) - # --- 构建提示词 --- - replan_prompt_str = "" - if is_re_planned: - replan_prompt_str = await self._build_replan_prompt( - self._current_cycle.action_type, self._current_cycle.reasoning - ) + # --- 构建提示词 (调用修改后的 _build_planner_prompt) --- + # replan_prompt_str = "" # 暂时简化 + # if is_re_planned: + # replan_prompt_str = await self._build_replan_prompt( + # self._current_cycle.action_type, self._current_cycle.reasoning + # ) prompt = await self._build_planner_prompt( - observed_messages_str, current_mind, self.sub_mind.structured_info, replan_prompt_str + observed_messages_str, + current_mind, + self.sub_mind.structured_info, + "", # replan_prompt_str, + current_available_actions # <--- 传入当前可用动作 ) - # --- 调用 LLM --- + # --- 调用 LLM (普通文本生成) --- + llm_content = None try: - planner_tools = self.action_manager.get_planner_tool_definition() - logger.debug(f"{self.log_prefix}[Planner] 本次使用的工具定义: {planner_tools}") # 记录本次使用的工具 - _response_text, _reasoning_content, tool_calls = await self.planner_llm.generate_response_tool_async( - prompt=prompt, - tools=planner_tools, - ) - logger.debug(f"{self.log_prefix}[Planner] 原始人 LLM响应: {_response_text}") + # 假设 LLMRequest 有 generate_response 方法返回 (content, reasoning, model_name) + # 我们只需要 content + # !! 注意:这里假设 self.planner_llm 有 generate_response 方法 + # !! 如果你的 LLMRequest 类使用的是其他方法名,请相应修改 + llm_content, _, _ = await self.planner_llm.generate_response(prompt=prompt) + logger.debug(f"{self.log_prefix}[Planner] LLM 原始 JSON 响应 (预期): {llm_content}") except Exception as req_e: - logger.error(f"{self.log_prefix}[Planner] LLM请求执行失败: {req_e}") - action = "error" - reasoning = f"LLM请求失败: {req_e}" + logger.error(f"{self.log_prefix}[Planner] LLM 请求执行失败: {req_e}") + reasoning = f"LLM 请求失败: {req_e}" llm_error = True - # 直接返回错误结果 - return { - "action": action, - "reasoning": reasoning, - "emoji_query": "", - "current_mind": current_mind, - "observed_messages": observed_messages, - "llm_error": llm_error, - } + # 直接使用默认动作返回错误结果 + action = "no_reply" # 明确设置为默认值 + emoji_query = "" # 明确设置为空 + # 不再立即返回,而是继续执行 finally 块以恢复动作 + # return { ... } - # 默认错误状态 - action = "error" - reasoning = "处理工具调用时出错" - llm_error = True + # --- 解析 LLM 返回的 JSON (仅当 LLM 请求未出错时进行) --- + if not llm_error and llm_content: + try: + # 尝试去除可能的 markdown 代码块标记 + cleaned_content = llm_content.strip().removeprefix("```json").removeprefix("```").removesuffix("```").strip() + if not cleaned_content: + raise json.JSONDecodeError("Cleaned content is empty", cleaned_content, 0) + parsed_json = json.loads(cleaned_content) - # 1. 验证工具调用 - success, valid_tool_calls, error_msg = process_llm_tool_calls( - tool_calls, log_prefix=f"{self.log_prefix}[Planner] " - ) + # 提取决策,提供默认值 + extracted_action = parsed_json.get("action", "no_reply") + extracted_reasoning = parsed_json.get("reasoning", "LLM未提供理由") + extracted_emoji_query = parsed_json.get("emoji_query", "") - if success and valid_tool_calls: - # 2. 提取第一个调用并获取参数 - first_tool_call = valid_tool_calls[0] - tool_name = first_tool_call.get("function", {}).get("name") - arguments = extract_tool_call_arguments(first_tool_call, None) - - # 3. 检查名称和参数 - expected_tool_name = "decide_reply_action" - if tool_name == expected_tool_name and arguments is not None: - # 4. 成功,提取决策 - extracted_action = arguments.get("action", "no_reply") - # 验证动作 - if extracted_action not in self.action_manager.get_available_actions(): - # 如果LLM返回了一个此时不该用的动作(因为被临时移除了) - # 或者完全无效的动作 + # 验证动作是否在当前可用列表中 + # !! 使用调用 prompt 时实际可用的动作列表进行验证 + if extracted_action not in current_available_actions: logger.warning( - f"{self.log_prefix}[Planner] LLM返回了当前不可用或无效的动作: {extracted_action},将强制使用 'no_reply'" + f"{self.log_prefix}[Planner] LLM 返回了当前不可用或无效的动作: '{extracted_action}' (可用: {list(current_available_actions.keys())}),将强制使用 'no_reply'" ) action = "no_reply" - reasoning = f"LLM返回了当前不可用的动作: {extracted_action}" + reasoning = f"LLM 返回了当前不可用的动作 '{extracted_action}' (可用: {list(current_available_actions.keys())})。原始理由: {extracted_reasoning}" emoji_query = "" - llm_error = False # 视为逻辑修正而非 LLM 错误 - # --- 检查 'no_reply' 是否也恰好被移除了 (极端情况) --- - if "no_reply" not in self.action_manager.get_available_actions(): - logger.error( - f"{self.log_prefix}[Planner] 严重错误:'no_reply' 动作也不可用!无法执行任何动作。" - ) - action = "error" # 回退到错误状态 - reasoning = "无法执行任何有效动作,包括 no_reply" - llm_error = True + # 检查 no_reply 是否也恰好被移除了 (极端情况) + if "no_reply" not in current_available_actions: + logger.error(f"{self.log_prefix}[Planner] 严重错误:'no_reply' 动作也不可用!无法执行任何动作。") + action = "error" # 回退到错误状态 + reasoning = "无法执行任何有效动作,包括 no_reply" + llm_error = True # 标记为严重错误 + else: + llm_error = False # 视为逻辑修正而非 LLM 错误 else: - # 动作有效且可用,使用提取的值 + # 动作有效且可用 action = extracted_action - reasoning = arguments.get("reasoning", "未提供理由") - emoji_query = arguments.get("emoji_query", "") - llm_error = False # 成功处理 - # 记录决策结果 + reasoning = extracted_reasoning + emoji_query = extracted_emoji_query + llm_error = False # 解析成功 logger.debug( - f"{self.log_prefix}[要做什么]\nPrompt:\n{prompt}\n\n决策结果: {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'" + f"{self.log_prefix}[要做什么]\nPrompt:\n{prompt}\n\n决策结果 (来自JSON): {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'" ) - elif tool_name != expected_tool_name: - reasoning = f"LLM返回了非预期的工具: {tool_name}" - logger.warning(f"{self.log_prefix}[Planner] {reasoning}") - else: # arguments is None - reasoning = f"无法提取工具 {tool_name} 的参数" - logger.warning(f"{self.log_prefix}[Planner] {reasoning}") - elif not success: - reasoning = f"验证工具调用失败: {error_msg}" - logger.warning(f"{self.log_prefix}[Planner] {reasoning}") - else: # not valid_tool_calls - # 如果没有有效的工具调用,我们需要检查 'no_reply' 是否是当前唯一可用的动作 - available_actions = list(self.action_manager.get_available_actions().keys()) - if available_actions == ["no_reply"]: - logger.info( - f"{self.log_prefix}[Planner] LLM未返回工具调用,但当前唯一可用动作是 'no_reply',将执行 'no_reply'" - ) - action = "no_reply" - reasoning = "LLM未返回工具调用,且当前仅 'no_reply' 可用" - emoji_query = "" - llm_error = False # 视为逻辑选择而非错误 - else: - reasoning = "LLM未返回有效的工具调用" - logger.warning(f"{self.log_prefix}[Planner] {reasoning}") - # llm_error 保持为 True - # 如果 llm_error 仍然是 True,说明在处理过程中有错误发生 - except Exception as llm_e: - logger.error(f"{self.log_prefix}[Planner] Planner LLM处理过程中发生意外错误: {llm_e}") + except json.JSONDecodeError as json_e: + logger.warning(f"{self.log_prefix}[Planner] 解析LLM响应JSON失败: {json_e}. LLM原始输出: '{llm_content}'") + reasoning = f"解析LLM响应JSON失败: {json_e}. 将使用默认动作 'no_reply'." + action = "no_reply" # 解析失败则默认不回复 + emoji_query = "" + llm_error = True # 标记解析错误 + except Exception as parse_e: + logger.error(f"{self.log_prefix}[Planner] 处理LLM响应时发生意外错误: {parse_e}") + reasoning = f"处理LLM响应时发生意外错误: {parse_e}. 将使用默认动作 'no_reply'." + action = "no_reply" + emoji_query = "" + llm_error = True + elif not llm_error and not llm_content: + # LLM 请求成功但返回空内容 + logger.warning(f"{self.log_prefix}[Planner] LLM 返回了空内容。") + reasoning = "LLM 返回了空内容,使用默认动作 'no_reply'." + action = "no_reply" + emoji_query = "" + llm_error = True # 标记为空响应错误 + + # 如果 llm_error 在此阶段为 True,意味着请求成功但解析失败或返回空 + # 如果 llm_error 在请求阶段就为 True,则跳过了此解析块 + + except Exception as outer_e: + logger.error(f"{self.log_prefix}[Planner] Planner 处理过程中发生意外错误: {outer_e}") logger.error(traceback.format_exc()) - action = "error" - reasoning = f"Planner内部处理错误: {llm_e}" + action = "error" # 发生未知错误,标记为 error 动作 + reasoning = f"Planner 内部处理错误: {outer_e}" + emoji_query = "" llm_error = True - # --- 新增:确保动作恢复 --- finally: - if actions_to_remove_temporarily: # 只有当确实移除了动作时才需要恢复 + # --- 确保动作恢复 --- + # 检查 self._original_actions_backup 是否有值来判断是否需要恢复 + if self.action_manager._original_actions_backup is not None: self.action_manager.restore_actions() logger.debug( f"{self.log_prefix}[Planner] 恢复了原始动作集, 当前可用: {list(self.action_manager.get_available_actions().keys())}" ) - # --- 结束:确保动作恢复 --- - - # --- 新增:概率性忽略文本回复附带的表情(正确的位置)--- + # --- 结束确保动作恢复 --- + # --- 概率性忽略文本回复附带的表情 (逻辑保持不变) --- if action == "text_reply" and emoji_query: - logger.debug(f"{self.log_prefix}[Planner] 大模型想让麦麦发文字时带表情: '{emoji_query}'") - # 掷骰子看看要不要听它的 + logger.debug(f"{self.log_prefix}[Planner] 大模型建议文字回复带表情: '{emoji_query}'") if random.random() > EMOJI_SEND_PRO: logger.info( f"{self.log_prefix}[Planner] 但是麦麦这次不想加表情 ({1 - EMOJI_SEND_PRO:.0%}),忽略表情 '{emoji_query}'" ) - emoji_query = "" # 把表情请求清空,就不发了 + emoji_query = "" # 清空表情请求 else: logger.info(f"{self.log_prefix}[Planner] 好吧,加上表情 '{emoji_query}'") - # --- 结束:概率性忽略 --- - - # --- 结束 LLM 决策 --- # + # --- 结束概率性忽略 --- + # 返回结果字典 return { "action": action, "reasoning": reasoning, "emoji_query": emoji_query, "current_mind": current_mind, "observed_messages": observed_messages, - "llm_error": llm_error, + "llm_error": llm_error, # 返回错误状态 } async def _get_anchor_message(self) -> Optional[MessageRecv]: @@ -1146,8 +1100,9 @@ class HeartFChatting: current_mind: Optional[str], structured_info: Dict[str, Any], replan_prompt: str, + current_available_actions: Dict[str, str], ) -> str: - """构建 Planner LLM 的提示词""" + """构建 Planner LLM 的提示词 (获取模板并填充数据)""" try: # 准备结构化信息块 structured_info_block = "" @@ -1163,12 +1118,13 @@ class HeartFChatting: else: chat_content_block = "当前没有观察到新的聊天内容。\n" - # 准备当前思维块 + # 准备当前思维块 (修改以匹配模板) current_mind_block = "" if current_mind: - current_mind_block = f"{current_mind}" + # 模板中占位符是 {current_mind_block},它期望包含"你的内心想法:"的前缀 + current_mind_block = f"你的内心想法:\n{current_mind}" else: - current_mind_block = "[没有特别的想法]" + current_mind_block = "你的内心想法:\n[没有特别的想法]" # 准备循环信息块 (分析最近的活动循环) recent_active_cycles = [] @@ -1208,23 +1164,40 @@ class HeartFChatting: # 包装提示块,增加可读性,即使没有连续回复也给个标记 if cycle_info_block: + # 模板中占位符是 {cycle_info_block},它期望包含"【近期回复历史】"的前缀 cycle_info_block = f"\n【近期回复历史】\n{cycle_info_block}\n" else: # 如果最近的活动循环不是文本回复,或者没有活动循环 cycle_info_block = "\n【近期回复历史】\n(最近没有连续文本回复)\n" individuality = Individuality.get_instance() + # 模板中占位符是 {prompt_personality} prompt_personality = individuality.get_prompt(x_person=2, level=2) - # 获取提示词模板并填充数据 - prompt = (await global_prompt_manager.get_prompt_async("planner_prompt")).format( + # --- 构建可用动作描述 (用于填充模板中的 {action_options_text}) --- + action_options_text = "当前你可以选择的行动有:\n" + action_keys = list(current_available_actions.keys()) + for name in action_keys: + desc = current_available_actions[name] + action_options_text += f"- '{name}': {desc}\n" + + # --- 选择一个示例动作键 (用于填充模板中的 {example_action}) --- + example_action_key = action_keys[0] if action_keys else "no_reply" + + # --- 获取提示词模板 --- + planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt") + + # --- 填充模板 --- + prompt = planner_prompt_template.format( bot_name=global_config.BOT_NICKNAME, prompt_personality=prompt_personality, structured_info_block=structured_info_block, chat_content_block=chat_content_block, - current_mind_block=current_mind_block, - replan=replan_prompt, + current_mind_block=current_mind_block, + replan="", # 暂时留空 replan 信息 cycle_info_block=cycle_info_block, + action_options_text=action_options_text, # 传入可用动作描述 + example_action=example_action_key # 传入示例动作键 ) return prompt @@ -1232,7 +1205,7 @@ class HeartFChatting: except Exception as e: logger.error(f"{self.log_prefix}[Planner] 构建提示词时出错: {e}") logger.error(traceback.format_exc()) - return "" + return "[构建 Planner Prompt 时出错]" # 返回错误提示,避免空字符串 # --- 回复器 (Replier) 的定义 --- # async def _replier_work( @@ -1273,7 +1246,7 @@ class HeartFChatting: try: with Timer("LLM生成", {}): # 内部计时器,可选保留 content, reasoning_content, model_name = await self.model_normal.generate_response(prompt) - logger.info(f"{self.log_prefix}[Replier-{thinking_id}]\\nPrompt:\\n{prompt}\\n生成回复: {content}\\n") + # logger.info(f"{self.log_prefix}[Replier-{thinking_id}]\\nPrompt:\\n{prompt}\\n生成回复: {content}\\n") # 捕捉 LLM 输出信息 info_catcher.catch_after_llm_generated( prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=model_name diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 69bae041..7cb847e0 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -7,13 +7,14 @@ from src.plugins.utils.chat_message_builder import build_readable_messages, get_ from src.plugins.person_info.relationship_manager import relationship_manager from src.plugins.chat.utils import get_embedding import time -from typing import Union, Optional +from typing import Union, Optional, Dict, Any from ...common.database import db from ..chat.utils import get_recent_group_speaker from ..moods.moods import MoodManager from ..memory_system.Hippocampus import HippocampusManager from ..schedule.schedule_generator import bot_schedule from ..knowledge.knowledge_lib import qa_manager +import traceback logger = get_logger("prompt") @@ -47,17 +48,15 @@ def init_prompt(): "info_from_tools", ) - # Planner提示词 - 优化版 + # Planner提示词 - 修改为要求 JSON 输出 Prompt( - """你的名字是{bot_name},{prompt_personality},你现在正在一个群聊中。需要基于以下信息决定如何参与对话: + '''你的名字是{bot_name},{prompt_personality},你现在正在一个群聊中。需要基于以下信息决定如何参与对话: {structured_info_block} {chat_content_block} -你的内心想法: {current_mind_block} -{replan} {cycle_info_block} -请综合分析聊天内容和你看到的新消息,参考内心想法,使用'decide_reply_action'工具做出决策。决策时请注意: +请综合分析聊天内容和你看到的新消息,参考内心想法,并根据以下原则和可用动作做出决策。 【回复原则】 1. 不回复(no_reply)适用: @@ -81,14 +80,34 @@ def init_prompt(): - 避免重复或评价自己的发言 - 不要和自己聊天 -【必须遵守】 -- 遵守回复原则 -- 必须调用工具并包含action和reasoning -- 你可以选择文字回复(text_reply),纯表情回复(emoji_reply),不回复(no_reply) -- 并不是所有选择都可用 -- 选择text_reply或emoji_reply时必须提供emoji_query -- 保持回复自然,符合日常聊天习惯""", - "planner_prompt", +【决策任务】 +{action_options_text} + +你必须从上面列出的可用行动中选择一个,并说明原因。 +你的决策必须以严格的 JSON 格式输出,且仅包含 JSON 内容,不要有任何其他文字或解释。 +JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query": +{{ + "action": "string", // 必须是上面提供的可用行动之一 (例如: '{example_action}') + "reasoning": "string", // 做出此决定的详细理由和思考过程,说明你如何应用了回复原则 + "emoji_query": "string" // 可选。如果行动是 'emoji_reply',必须提供表情主题;如果行动是 'text_reply' 且你想附带表情,也在此提供表情主题,否则留空字符串 ""。遵循回复原则,不要滥用。 +}} + +例如: +{{ + "action": "text_reply", + "reasoning": "用户提到了我,且问题比较具体,适合用文本回复。考虑到内容,可以带上一个微笑表情。", + "emoji_query": "微笑" +}} +或 +{{ + "action": "no_reply", + "reasoning": "我已经连续回复了两次,而且这个话题我不太感兴趣,根据回复原则,选择不回复,等待其他人发言。", + "emoji_query": "" +}} + +请输出你的决策 JSON: +''', # 使用三引号避免内部引号问题 + "planner_prompt", # 保持名称不变,替换内容 ) Prompt( From 0b62802c2c479c4e2e57753b4f2c7f04cca2750c Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 1 May 2025 01:40:12 +0800 Subject: [PATCH 02/18] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E5=A4=8D=E4=BA=86?= =?UTF-8?q?=E8=A1=A8=E6=83=85=E5=8C=85=E6=B3=A8=E5=86=8C=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/emoji_system/emoji_manager.py | 490 +++++++++++++++------- 1 file changed, 333 insertions(+), 157 deletions(-) diff --git a/src/plugins/emoji_system/emoji_manager.py b/src/plugins/emoji_system/emoji_manager.py index 72169bc4..62b2712c 100644 --- a/src/plugins/emoji_system/emoji_manager.py +++ b/src/plugins/emoji_system/emoji_manager.py @@ -34,9 +34,12 @@ MAX_EMOJI_FOR_PROMPT = 20 # 最大允许的表情包描述数量于图片替换 class MaiEmoji: """定义一个表情包""" - def __init__(self, filename: str, path: str): - self.path = path # 存储目录路径 - self.filename = filename + def __init__(self, full_path: str): + if not full_path: + raise ValueError("full_path cannot be empty") + self.full_path = full_path # 文件的完整路径 (包括文件名) + self.path = os.path.dirname(full_path) # 文件所在的目录路径 + self.filename = os.path.basename(full_path) # 文件名 self.embedding = [] self.hash = "" # 初始为空,在创建实例时会计算 self.description = "" @@ -48,35 +51,58 @@ class MaiEmoji: self.format = "" async def initialize_hash_format(self): - """从文件创建表情包实例 - - 参数: - file_path: 文件的完整路径 - - 返回: - MaiEmoji: 创建的表情包实例,如果失败则返回None - """ + """从文件创建表情包实例, 计算哈希值和格式""" + image_base64 = None + image_bytes = None try: - file_path = os.path.join(self.path, self.filename) - if not os.path.exists(file_path): - logger.error(f"[错误] 表情包文件不存在: {file_path}") + # 使用 full_path 检查文件是否存在 + if not os.path.exists(self.full_path): + logger.error(f"[初始化错误] 表情包文件不存在: {self.full_path}") + self.is_deleted = True return None - image_base64 = image_path_to_base64(file_path) + # 使用 full_path 读取文件 + logger.debug(f"[初始化] 正在读取文件: {self.full_path}") + image_base64 = image_path_to_base64(self.full_path) if image_base64 is None: - logger.error(f"[错误] 无法读取图片: {file_path}") + logger.error(f"[初始化错误] 无法读取或转换Base64: {self.full_path}") + self.is_deleted = True return None + logger.debug(f"[初始化] 文件读取成功 (Base64预览: {image_base64[:50]}...)") # 计算哈希值 + logger.debug(f"[初始化] 正在解码Base64并计算哈希: {self.filename}") image_bytes = base64.b64decode(image_base64) self.hash = hashlib.md5(image_bytes).hexdigest() + logger.debug(f"[初始化] 哈希计算成功: {self.hash}") # 获取图片格式 - self.format = Image.open(io.BytesIO(image_bytes)).format.lower() + logger.debug(f"[初始化] 正在使用Pillow获取格式: {self.filename}") + try: + with Image.open(io.BytesIO(image_bytes)) as img: + self.format = img.format.lower() + logger.debug(f"[初始化] 格式获取成功: {self.format}") + except Exception as pil_error: + logger.error(f"[初始化错误] Pillow无法处理图片 ({self.filename}): {pil_error}") + logger.error(traceback.format_exc()) + self.is_deleted = True + return None + # 如果所有步骤成功,返回 True + return True + + except FileNotFoundError: + logger.error(f"[初始化错误] 文件在处理过程中丢失: {self.full_path}") + self.is_deleted = True + return None + except base64.binascii.Error as b64_error: + logger.error(f"[初始化错误] Base64解码失败 ({self.filename}): {b64_error}") + self.is_deleted = True + return None except Exception as e: - logger.error(f"[错误] 初始化表情包失败: {str(e)}") + logger.error(f"[初始化错误] 初始化表情包时发生未预期错误 ({self.filename}): {str(e)}") logger.error(traceback.format_exc()) + self.is_deleted = True return None async def register_to_db(self): @@ -87,44 +113,47 @@ class MaiEmoji: """ try: # 确保目标目录存在 - os.makedirs(EMOJI_REGISTED_DIR, exist_ok=True) - # 源路径是当前实例的完整路径 - source_path = os.path.join(self.path, self.filename) - # 目标路径 - destination_path = os.path.join(EMOJI_REGISTED_DIR, self.filename) + # 源路径是当前实例的完整路径 self.full_path + source_full_path = self.full_path + # 目标完整路径 + destination_full_path = os.path.join(EMOJI_REGISTED_DIR, self.filename) # 检查源文件是否存在 - if not os.path.exists(source_path): - logger.error(f"[错误] 源文件不存在: {source_path}") + if not os.path.exists(source_full_path): + logger.error(f"[错误] 源文件不存在: {source_full_path}") return False # --- 文件移动 --- try: # 如果目标文件已存在,先删除 (确保移动成功) - if os.path.exists(destination_path): - os.remove(destination_path) + if os.path.exists(destination_full_path): + os.remove(destination_full_path) - os.rename(source_path, destination_path) - logger.debug(f"[移动] 文件从 {source_path} 移动到 {destination_path}") - # 更新实例的路径属性为新目录 + os.rename(source_full_path, destination_full_path) + logger.debug(f"[移动] 文件从 {source_full_path} 移动到 {destination_full_path}") + # 更新实例的路径属性为新路径 + self.full_path = destination_full_path self.path = EMOJI_REGISTED_DIR + # self.filename 保持不变 except Exception as move_error: logger.error(f"[错误] 移动文件失败: {str(move_error)}") - return False # 文件移动失败,不继续 + # 如果移动失败,尝试将实例状态恢复?暂时不处理,仅返回失败 + return False # --- 数据库操作 --- try: # 准备数据库记录 for emoji collection emoji_record = { "filename": self.filename, - "path": os.path.join(self.path, self.filename), # 使用更新后的路径 + "path": self.path, # 存储目录路径 + "full_path": self.full_path, # 存储完整文件路径 "embedding": self.embedding, "description": self.description, - "emotion": self.emotion, # 添加情感标签字段 + "emotion": self.emotion, "hash": self.hash, "format": self.format, - "timestamp": int(self.register_time), # 使用实例的注册时间 + "timestamp": int(self.register_time), "usage_count": self.usage_count, "last_used_time": self.last_used_time, } @@ -132,17 +161,24 @@ class MaiEmoji: # 使用upsert确保记录存在或被更新 db["emoji"].update_one({"hash": self.hash}, {"$set": emoji_record}, upsert=True) - logger.success(f"[注册] 表情包信息保存到数据库: {self.emotion}") + logger.success(f"[注册] 表情包信息保存到数据库: {self.filename} ({self.emotion})") return True except Exception as db_error: - logger.error(f"[错误] 保存数据库失败: {str(db_error)}") - # 考虑是否需要将文件移回?为了简化,暂时只记录错误 + logger.error(f"[错误] 保存数据库失败 ({self.filename}): {str(db_error)}") + # 数据库保存失败,是否需要将文件移回?为了简化,暂时只记录错误 + # 可以考虑在这里尝试删除已移动的文件,避免残留 + try: + if os.path.exists(self.full_path): # full_path 此时是目标路径 + os.remove(self.full_path) + logger.warning(f"[回滚] 已删除移动失败后残留的文件: {self.full_path}") + except Exception as remove_error: + logger.error(f"[错误] 回滚删除文件失败: {remove_error}") return False except Exception as e: - logger.error(f"[错误] 注册表情包失败: {str(e)}") + logger.error(f"[错误] 注册表情包失败 ({self.filename}): {str(e)}") logger.error(traceback.format_exc()) return False @@ -156,30 +192,34 @@ class MaiEmoji: """ try: # 1. 删除文件 - if os.path.exists(os.path.join(self.path, self.filename)): + file_to_delete = self.full_path + if os.path.exists(file_to_delete): try: - os.remove(os.path.join(self.path, self.filename)) - logger.debug(f"[删除] 文件: {os.path.join(self.path, self.filename)}") + os.remove(file_to_delete) + logger.debug(f"[删除] 文件: {file_to_delete}") except Exception as e: - logger.error(f"[错误] 删除文件失败 {os.path.join(self.path, self.filename)}: {str(e)}") - # 继续执行,即使文件删除失败也尝试删除数据库记录 + logger.error(f"[错误] 删除文件失败 {file_to_delete}: {str(e)}") + # 文件删除失败,但仍然尝试删除数据库记录 # 2. 删除数据库记录 result = db.emoji.delete_one({"hash": self.hash}) deleted_in_db = result.deleted_count > 0 if deleted_in_db: - logger.info(f"[删除] 表情包 {self.filename} 无对应文件,已删除") - + logger.info(f"[删除] 表情包数据库记录 {self.filename} (Hash: {self.hash})") # 3. 标记对象已被删除 self.is_deleted = True return True else: - logger.error(f"[错误] 删除表情包记录失败: {self.hash}") + # 如果数据库记录删除失败,但文件可能已删除,记录一个警告 + if not os.path.exists(file_to_delete): + logger.warning(f"[警告] 表情包文件 {file_to_delete} 已删除,但数据库记录删除失败 (Hash: {self.hash})") + else: + logger.error(f"[错误] 删除表情包数据库记录失败: {self.hash}") return False except Exception as e: - logger.error(f"[错误] 删除表情包失败: {str(e)}") + logger.error(f"[错误] 删除表情包失败 ({self.filename}): {str(e)}") return False @@ -209,6 +249,7 @@ class EmojiManager: def _ensure_emoji_dir(self): """确保表情存储目录存在""" os.makedirs(EMOJI_DIR, exist_ok=True) + os.makedirs(EMOJI_REGISTED_DIR, exist_ok=True) def initialize(self): """初始化数据库连接和表情目录""" @@ -265,22 +306,27 @@ class EmojiManager: Args: text_emotion: 输入的情感描述文本 Returns: - Optional[Tuple[str, str]]: (表情包文件路径, 表情包描述),如果没有找到则返回None + Optional[Tuple[str, str]]: (表情包完整文件路径, 表情包描述),如果没有找到则返回None """ try: self._ensure_db() _time_start = time.time() - # 获取所有表情包 + # 获取所有表情包 (从内存缓存中获取) all_emojis = self.emoji_objects if not all_emojis: - logger.warning("数据库中没有任何表情包") + logger.warning("内存中没有任何表情包对象") + # 可以考虑再查一次数据库?或者依赖定期任务更新 return None # 计算每个表情包与输入文本的最大情感相似度 emoji_similarities = [] for emoji in all_emojis: + # 跳过已标记为删除的对象 + if emoji.is_deleted: + continue + emotions = emoji.emotion if not emotions: continue @@ -321,9 +367,10 @@ class EmojiManager: _time_end = time.time() logger.info( # 使用匹配到的 emotion 记录日志喵~ - f"为[{text_emotion}]找到表情包: {matched_emotion},({similarity:.4f})" + f"为[{text_emotion}]找到表情包: {matched_emotion} ({selected_emoji.filename}), Similarity: {similarity:.4f}" ) - return selected_emoji.path, f"[ {selected_emoji.description} ]" + # 返回完整文件路径和描述 + return selected_emoji.full_path, f"[ {selected_emoji.description} ]" except Exception as e: logger.error(f"[错误] 获取表情包失败: {str(e)}") @@ -371,40 +418,50 @@ class EmojiManager: self.emoji_num = total_count removed_count = 0 # 使用列表复制进行遍历,因为我们会在遍历过程中修改列表 - for emoji in self.emoji_objects[:]: + objects_to_remove = [] + for emoji in self.emoji_objects: try: + # 跳过已经标记为删除的,避免重复处理 + if emoji.is_deleted: + objects_to_remove.append(emoji) # 收集起来一次性移除 + continue + # 检查文件是否存在 - if not os.path.exists(emoji.path): - logger.warning(f"[检查] 表情包文件已被删除: {emoji.path}") + if not os.path.exists(emoji.full_path): + logger.warning(f"[检查] 表情包文件丢失: {emoji.full_path}") # 执行表情包对象的删除方法 - await emoji.delete() - # 从列表中移除该对象 - self.emoji_objects.remove(emoji) + await emoji.delete() # delete 方法现在会标记 is_deleted + objects_to_remove.append(emoji) # 标记删除后,也收集起来移除 # 更新计数 self.emoji_num -= 1 removed_count += 1 continue - if emoji.description == None: - logger.warning(f"[检查] 表情包文件已被删除: {emoji.path}") - # 执行表情包对象的删除方法 + # 检查描述是否为空 (如果为空也视为无效) + if not emoji.description: + logger.warning(f"[检查] 表情包描述为空,视为无效: {emoji.filename}") await emoji.delete() - # 从列表中移除该对象 - self.emoji_objects.remove(emoji) - # 更新计数 + objects_to_remove.append(emoji) self.emoji_num -= 1 removed_count += 1 continue except Exception as item_error: - logger.error(f"[错误] 处理表情包记录时出错: {str(item_error)}") + logger.error(f"[错误] 处理表情包记录时出错 ({emoji.filename}): {str(item_error)}") + # 即使出错,也尝试继续检查下一个 continue + # 从 self.emoji_objects 中移除标记的对象 + if objects_to_remove: + self.emoji_objects = [e for e in self.emoji_objects if e not in objects_to_remove] + + # 清理 EMOJI_REGISTED_DIR 目录中未被追踪的文件 await self.clean_unused_emojis(EMOJI_REGISTED_DIR, self.emoji_objects) + # 输出清理结果 if removed_count > 0: - logger.success(f"[清理] 已清理 {removed_count} 个失效的表情包记录") - logger.info(f"[统计] 清理前: {total_count} | 清理后: {len(self.emoji_objects)}") + logger.success(f"[清理] 已清理 {removed_count} 个失效/文件丢失的表情包记录") + logger.info(f"[统计] 清理前记录数: {total_count} | 清理后有效记录数: {len(self.emoji_objects)}") else: logger.info(f"[检查] 已检查 {total_count} 个表情包记录,全部完好") @@ -467,45 +524,72 @@ class EmojiManager: await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60) async def get_all_emoji_from_db(self): - """获取所有表情包并初始化为MaiEmoji类对象 - - 参数: - hash: 可选,如果提供则只返回指定哈希值的表情包 - - 返回: - list[MaiEmoji]: 表情包对象列表 - """ + """获取所有表情包并初始化为MaiEmoji类对象,更新 self.emoji_objects""" try: self._ensure_db() + logger.info("[数据库] 开始加载所有表情包记录...") - # 获取所有表情包 all_emoji_data = list(db.emoji.find()) - - # 将数据库记录转换为MaiEmoji对象 emoji_objects = [] + load_errors = 0 + for emoji_data in all_emoji_data: - emoji = MaiEmoji( - filename=emoji_data.get("filename", ""), - path=emoji_data.get("path", ""), - ) + full_path = emoji_data.get("full_path") + if not full_path: + logger.warning(f"[加载错误] 数据库记录缺少 'full_path' 字段: {emoji_data.get('_id')}") + load_errors += 1 + continue # 跳过缺少 full_path 的记录 - # 设置额外属性 - emoji.hash = emoji_data.get("hash", "") - emoji.usage_count = emoji_data.get("usage_count", 0) - emoji.last_used_time = emoji_data.get("last_used_time", emoji_data.get("timestamp", time.time())) - emoji.register_time = emoji_data.get("timestamp", time.time()) - emoji.description = emoji_data.get("description", "") - emoji.emotion = emoji_data.get("emotion", []) # 添加情感标签的加载 - emoji_objects.append(emoji) + try: + # 使用 full_path 初始化 MaiEmoji 对象 + emoji = MaiEmoji(full_path=full_path) - # 存储到EmojiManager中 + # 设置从数据库加载的属性 + emoji.hash = emoji_data.get("hash", "") + # 如果 hash 为空,也跳过?取决于业务逻辑 + if not emoji.hash: + logger.warning(f"[加载错误] 数据库记录缺少 'hash' 字段: {full_path}") + load_errors += 1 + continue + + emoji.description = emoji_data.get("description", "") + emoji.emotion = emoji_data.get("emotion", []) + emoji.usage_count = emoji_data.get("usage_count", 0) + # 优先使用 last_used_time,否则用 timestamp,最后用当前时间 + last_used = emoji_data.get("last_used_time") + timestamp = emoji_data.get("timestamp") + emoji.last_used_time = last_used if last_used is not None else (timestamp if timestamp is not None else time.time()) + emoji.register_time = timestamp if timestamp is not None else time.time() + emoji.format = emoji_data.get("format", "") # 加载格式 + + # 不需要再手动设置 path 和 filename,__init__ 会自动处理 + + emoji_objects.append(emoji) + + except ValueError as ve: #捕获 __init__ 可能的错误 + logger.error(f"[加载错误] 初始化 MaiEmoji 失败 ({full_path}): {ve}") + load_errors += 1 + except Exception as e: + logger.error(f"[加载错误] 处理数据库记录时出错 ({full_path}): {str(e)}") + load_errors += 1 + + + # 更新内存中的列表和数量 self.emoji_objects = emoji_objects + self.emoji_num = len(emoji_objects) + + logger.success(f"[数据库] 加载完成: 共加载 {self.emoji_num} 个表情包记录。") + if load_errors > 0: + logger.warning(f"[数据库] 加载过程中出现 {load_errors} 个错误。") + except Exception as e: - logger.error(f"[错误] 获取所有表情包对象失败: {str(e)}") + logger.error(f"[错误] 从数据库加载所有表情包对象失败: {str(e)}") + self.emoji_objects = [] # 加载失败则清空列表 + self.emoji_num = 0 async def get_emoji_from_db(self, hash=None): - """获取所有表情包并初始化为MaiEmoji类对象 + """获取指定哈希值的表情包并初始化为MaiEmoji类对象列表 (主要用于调试或特定查找) 参数: hash: 可选,如果提供则只返回指定哈希值的表情包 @@ -516,50 +600,71 @@ class EmojiManager: try: self._ensure_db() - # 准备查询条件 query = {} if hash: query = {"hash": hash} + else: + logger.warning("[查询] 未提供 hash,将尝试加载所有表情包,建议使用 get_all_emoji_from_db 更新管理器状态。") - # 获取所有表情包 - all_emoji_data = list(db.emoji.find(query)) - # 将数据库记录转换为MaiEmoji对象 + emoji_data_list = list(db.emoji.find(query)) emoji_objects = [] - for emoji_data in all_emoji_data: - emoji = MaiEmoji( - filename=emoji_data.get("filename", ""), - path=emoji_data.get("path", ""), - ) + load_errors = 0 - # 设置额外属性 - emoji.usage_count = emoji_data.get("usage_count", 0) - emoji.last_used_time = emoji_data.get("last_used_time", emoji_data.get("timestamp", time.time())) - emoji.register_time = emoji_data.get("timestamp", time.time()) - emoji.description = emoji_data.get("description", "") - emoji.emotion = emoji_data.get("emotion", []) # 添加情感标签的加载 + for emoji_data in emoji_data_list: + full_path = emoji_data.get("full_path") + if not full_path: + logger.warning(f"[加载错误] 数据库记录缺少 'full_path' 字段: {emoji_data.get('_id')}") + load_errors += 1 + continue - emoji_objects.append(emoji) + try: + emoji = MaiEmoji(full_path=full_path) + emoji.hash = emoji_data.get("hash", "") + if not emoji.hash: + logger.warning(f"[加载错误] 数据库记录缺少 'hash' 字段: {full_path}") + load_errors += 1 + continue - # 存储到EmojiManager中 - self.emoji_objects = emoji_objects + emoji.description = emoji_data.get("description", "") + emoji.emotion = emoji_data.get("emotion", []) + emoji.usage_count = emoji_data.get("usage_count", 0) + last_used = emoji_data.get("last_used_time") + timestamp = emoji_data.get("timestamp") + emoji.last_used_time = last_used if last_used is not None else (timestamp if timestamp is not None else time.time()) + emoji.register_time = timestamp if timestamp is not None else time.time() + emoji.format = emoji_data.get("format", "") + emoji_objects.append(emoji) + except ValueError as ve: + logger.error(f"[加载错误] 初始化 MaiEmoji 失败 ({full_path}): {ve}") + load_errors += 1 + except Exception as e: + logger.error(f"[加载错误] 处理数据库记录时出错 ({full_path}): {str(e)}") + load_errors += 1 + + + if load_errors > 0: + logger.warning(f"[查询] 加载过程中出现 {load_errors} 个错误。") return emoji_objects except Exception as e: - logger.error(f"[错误] 获取所有表情包对象失败: {str(e)}") + logger.error(f"[错误] 从数据库获取表情包对象失败: {str(e)}") return [] - async def get_emoji_from_manager(self, hash) -> MaiEmoji: - """从EmojiManager中获取表情包 + async def get_emoji_from_manager(self, hash) -> Optional[MaiEmoji]: + """从内存中的 emoji_objects 列表获取表情包 参数: - hash:如果提供则只返回指定哈希值的表情包 + hash: 要查找的表情包哈希值 + 返回: + MaiEmoji 或 None: 如果找到则返回 MaiEmoji 对象,否则返回 None """ for emoji in self.emoji_objects: - if emoji.hash == hash: + # 确保对象未被标记为删除且哈希值匹配 + if not emoji.is_deleted and emoji.hash == hash: return emoji - return None + return None # 如果循环结束还没找到,则返回 None async def delete_emoji(self, emoji_hash: str) -> bool: """根据哈希值删除表情包 @@ -779,51 +884,111 @@ class EmojiManager: Returns: bool: 注册是否成功 """ + file_full_path = os.path.join(EMOJI_DIR, filename) + if not os.path.exists(file_full_path): + logger.error(f"[注册失败] 文件不存在: {file_full_path}") + return False + try: - # 使用MaiEmoji类创建表情包实例 - new_emoji = MaiEmoji(filename, EMOJI_DIR) - await new_emoji.initialize_hash_format() - emoji_base64 = image_path_to_base64(os.path.join(EMOJI_DIR, filename)) - description, emotions = await self.build_emoji_description(emoji_base64) - if description == "" or description == None: + # 1. 创建 MaiEmoji 实例并初始化哈希和格式 + new_emoji = MaiEmoji(full_path=file_full_path) + init_result = await new_emoji.initialize_hash_format() + if init_result is None or new_emoji.is_deleted: # 初始化失败或文件读取错误 + logger.error(f"[注册失败] 初始化哈希和格式失败: {filename}") + # 是否需要删除源文件?看业务需求,暂时不删 return False - new_emoji.description = description - new_emoji.emotion = emotions - # 检查是否已经注册过 - # 对比内存中是否存在相同哈希值的表情包 + # 2. 检查哈希是否已存在 (在内存中检查) if await self.get_emoji_from_manager(new_emoji.hash): - logger.warning(f"[警告] 表情包已存在: {filename}") + logger.warning(f"[注册跳过] 表情包已存在 (Hash: {new_emoji.hash}): {filename}") + # 删除重复的源文件 + try: + os.remove(file_full_path) + logger.info(f"[清理] 删除重复的待注册文件: {filename}") + except Exception as e: + logger.error(f"[错误] 删除重复文件失败: {str(e)}") + return False # 返回 False 表示未注册新表情 + + # 3. 构建描述和情感 + try: + emoji_base64 = image_path_to_base64(file_full_path) + if emoji_base64 is None: # 再次检查读取 + logger.error(f"[注册失败] 无法读取图片以生成描述: {filename}") + return False + description, emotions = await self.build_emoji_description(emoji_base64) + if not description: # 检查描述是否成功生成或审核通过 + logger.warning(f"[注册失败] 未能生成有效描述或审核未通过: {filename}") + # 删除未能生成描述的文件 + try: + os.remove(file_full_path) + logger.info(f"[清理] 删除描述生成失败的文件: {filename}") + except Exception as e: + logger.error(f"[错误] 删除描述生成失败文件时出错: {str(e)}") + return False + new_emoji.description = description + new_emoji.emotion = emotions + except Exception as build_desc_error: + logger.error(f"[注册失败] 生成描述/情感时出错 ({filename}): {build_desc_error}") + # 同样考虑删除文件 + try: + os.remove(file_full_path) + logger.info(f"[清理] 删除描述生成异常的文件: {filename}") + except Exception as e: + logger.error(f"[错误] 删除描述生成异常文件时出错: {str(e)}") return False + # 4. 检查容量并决定是否替换或直接注册 if self.emoji_num >= self.emoji_num_max: - logger.warning(f"表情包数量已达到上限({self.emoji_num}/{self.emoji_num_max})") + logger.warning(f"表情包数量已达到上限({self.emoji_num}/{self.emoji_num_max}),尝试替换...") replaced = await self.replace_a_emoji(new_emoji) if not replaced: - logger.error("[错误] 替换表情包失败,无法完成注册") + logger.error("[注册失败] 替换表情包失败,无法完成注册") + # 替换失败,删除新表情包文件 + try: + os.remove(file_full_path) # new_emoji 的 full_path 此时还是源路径 + logger.info(f"[清理] 删除替换失败的新表情文件: {filename}") + except Exception as e: + logger.error(f"[错误] 删除替换失败文件时出错: {str(e)}") return False + # 替换成功时,replace_a_emoji 内部已处理 new_emoji 的注册和添加到列表 return True else: - # 修复:等待异步注册完成 - register_success = await new_emoji.register_to_db() + # 直接注册 + register_success = await new_emoji.register_to_db() # 此方法会移动文件并更新 DB if register_success: + # 注册成功后,添加到内存列表 self.emoji_objects.append(new_emoji) self.emoji_num += 1 - logger.success(f"[成功] 注册: {filename}") + logger.success(f"[成功] 注册新表情包: {filename} (当前: {self.emoji_num}/{self.emoji_num_max})") return True else: - logger.error(f"[错误] 注册表情包到数据库失败: {filename}") + logger.error(f"[注册失败] 保存表情包到数据库/移动文件失败: {filename}") + # register_to_db 失败时,内部会尝试清理移动后的文件,源文件可能还在 + # 是否需要删除源文件? + if os.path.exists(file_full_path): + try: + os.remove(file_full_path) + logger.info(f"[清理] 删除注册失败的源文件: {filename}") + except Exception as e: + logger.error(f"[错误] 删除注册失败源文件时出错: {str(e)}") return False except Exception as e: - logger.error(f"[错误] 注册表情包失败: {str(e)}") + logger.error(f"[错误] 注册表情包时发生未预期错误 ({filename}): {str(e)}") logger.error(traceback.format_exc()) + # 尝试删除源文件以避免循环处理 + if os.path.exists(file_full_path): + try: + os.remove(file_full_path) + logger.info(f"[清理] 删除处理异常的源文件: {filename}") + except Exception as remove_error: + logger.error(f"[错误] 删除异常处理文件时出错: {remove_error}") return False async def clear_temp_emoji(self): - """每天清理临时表情包 + """清理临时表情包 清理/data/emoji和/data/image目录下的所有文件 - 当目录中文件数超过50时,会全部删除 + 当目录中文件数超过100时,会全部删除 """ logger.info("[清理] 开始清理缓存...") @@ -833,7 +998,7 @@ class EmojiManager: if os.path.exists(emoji_dir): files = os.listdir(emoji_dir) # 如果文件数超过50就全部删除 - if len(files) > 50: + if len(files) > 100: for filename in files: file_path = os.path.join(emoji_dir, filename) if os.path.isfile(file_path): @@ -845,7 +1010,7 @@ class EmojiManager: if os.path.exists(image_dir): files = os.listdir(image_dir) # 如果文件数超过50就全部删除 - if len(files) > 50: + if len(files) > 100: for filename in files: file_path = os.path.join(image_dir, filename) if os.path.isfile(file_path): @@ -855,29 +1020,40 @@ class EmojiManager: logger.success("[清理] 完成") async def clean_unused_emojis(self, emoji_dir, emoji_objects): - """清理未使用的表情包文件 - 遍历指定文件夹中的所有文件,删除未在emoji_objects列表中的文件 - """ - # 首先检查目录是否存在喵~ + """清理指定目录中未被 emoji_objects 追踪的表情包文件""" if not os.path.exists(emoji_dir): - logger.warning(f"[清理] 表情包目录不存在,跳过清理: {emoji_dir}") + logger.warning(f"[清理] 目标目录不存在,跳过清理: {emoji_dir}") return - # 获取所有表情包路径 - emoji_paths = {emoji.path for emoji in emoji_objects} + try: + # 获取内存中所有有效表情包的完整路径集合 + tracked_full_paths = {emoji.full_path for emoji in emoji_objects if not emoji.is_deleted} + cleaned_count = 0 - # 遍历文件夹中的所有文件 - for file_name in os.listdir(emoji_dir): - file_path = os.path.join(emoji_dir, file_name) + # 遍历指定目录中的所有文件 + for file_name in os.listdir(emoji_dir): + file_full_path = os.path.join(emoji_dir, file_name) - # 检查文件是否在表情包路径列表中 - if file_path not in emoji_paths: - try: - # 删除未在表情包列表中的文件 - os.remove(file_path) - logger.info(f"[清理] 删除未使用的表情包文件: {file_path}") - except Exception as e: - logger.error(f"[错误] 删除文件时出错: {str(e)}") + # 确保处理的是文件而不是子目录 + if not os.path.isfile(file_full_path): + continue + + # 如果文件不在被追踪的集合中,则删除 + if file_full_path not in tracked_full_paths: + try: + os.remove(file_full_path) + logger.info(f"[清理] 删除未追踪的表情包文件: {file_full_path}") + cleaned_count += 1 + except Exception as e: + logger.error(f"[错误] 删除文件时出错 ({file_full_path}): {str(e)}") + + if cleaned_count > 0: + logger.success(f"[清理] 在目录 {emoji_dir} 中清理了 {cleaned_count} 个未追踪的文件。") + else: + logger.info(f"[清理] 目录 {emoji_dir} 中没有发现未追踪的文件。") + + except Exception as e: + logger.error(f"[错误] 清理未使用表情包文件时出错 ({emoji_dir}): {str(e)}") # 创建全局单例 From ccbdc6ffe00acc92607c397f2f36c13ab1bedb5b Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 1 May 2025 01:41:30 +0800 Subject: [PATCH 03/18] frrr --- src/common/logger.py | 5 +- src/plugins/emoji_system/emoji_manager.py | 148 +++++++++--------- src/plugins/heartFC_chat/heartFC_chat.py | 76 ++++----- .../heartFC_chat/heartflow_prompt_builder.py | 7 +- 4 files changed, 121 insertions(+), 115 deletions(-) diff --git a/src/common/logger.py b/src/common/logger.py index 12a9c135..6c95935e 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -633,15 +633,12 @@ HFC_STYLE_CONFIG = { "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 专注聊天 | {message}", }, "simple": { - "console_format": ( - "{time:MM-DD HH:mm} | 专注聊天 | {message}" - ), + "console_format": ("{time:MM-DD HH:mm} | 专注聊天 | {message}"), "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 专注聊天 | {message}", }, } - CONFIRM_STYLE_CONFIG = { "console_format": "{message}", # noqa: E501 "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | EULA与PRIVACY确认 | {message}", diff --git a/src/plugins/emoji_system/emoji_manager.py b/src/plugins/emoji_system/emoji_manager.py index 62b2712c..5f377678 100644 --- a/src/plugins/emoji_system/emoji_manager.py +++ b/src/plugins/emoji_system/emoji_manager.py @@ -37,9 +37,9 @@ class MaiEmoji: def __init__(self, full_path: str): if not full_path: raise ValueError("full_path cannot be empty") - self.full_path = full_path # 文件的完整路径 (包括文件名) - self.path = os.path.dirname(full_path) # 文件所在的目录路径 - self.filename = os.path.basename(full_path) # 文件名 + self.full_path = full_path # 文件的完整路径 (包括文件名) + self.path = os.path.dirname(full_path) # 文件所在的目录路径 + self.filename = os.path.basename(full_path) # 文件名 self.embedding = [] self.hash = "" # 初始为空,在创建实例时会计算 self.description = "" @@ -92,13 +92,13 @@ class MaiEmoji: return True except FileNotFoundError: - logger.error(f"[初始化错误] 文件在处理过程中丢失: {self.full_path}") - self.is_deleted = True - return None + logger.error(f"[初始化错误] 文件在处理过程中丢失: {self.full_path}") + self.is_deleted = True + return None except base64.binascii.Error as b64_error: - logger.error(f"[初始化错误] Base64解码失败 ({self.filename}): {b64_error}") - self.is_deleted = True - return None + logger.error(f"[初始化错误] Base64解码失败 ({self.filename}): {b64_error}") + self.is_deleted = True + return None except Exception as e: logger.error(f"[初始化错误] 初始化表情包时发生未预期错误 ({self.filename}): {str(e)}") logger.error(traceback.format_exc()) @@ -146,8 +146,8 @@ class MaiEmoji: # 准备数据库记录 for emoji collection emoji_record = { "filename": self.filename, - "path": self.path, # 存储目录路径 - "full_path": self.full_path, # 存储完整文件路径 + "path": self.path, # 存储目录路径 + "full_path": self.full_path, # 存储完整文件路径 "embedding": self.embedding, "description": self.description, "emotion": self.emotion, @@ -170,11 +170,11 @@ class MaiEmoji: # 数据库保存失败,是否需要将文件移回?为了简化,暂时只记录错误 # 可以考虑在这里尝试删除已移动的文件,避免残留 try: - if os.path.exists(self.full_path): # full_path 此时是目标路径 - os.remove(self.full_path) - logger.warning(f"[回滚] 已删除移动失败后残留的文件: {self.full_path}") + if os.path.exists(self.full_path): # full_path 此时是目标路径 + os.remove(self.full_path) + logger.warning(f"[回滚] 已删除移动失败后残留的文件: {self.full_path}") except Exception as remove_error: - logger.error(f"[错误] 回滚删除文件失败: {remove_error}") + logger.error(f"[错误] 回滚删除文件失败: {remove_error}") return False except Exception as e: @@ -213,9 +213,11 @@ class MaiEmoji: else: # 如果数据库记录删除失败,但文件可能已删除,记录一个警告 if not os.path.exists(file_to_delete): - logger.warning(f"[警告] 表情包文件 {file_to_delete} 已删除,但数据库记录删除失败 (Hash: {self.hash})") + logger.warning( + f"[警告] 表情包文件 {file_to_delete} 已删除,但数据库记录删除失败 (Hash: {self.hash})" + ) else: - logger.error(f"[错误] 删除表情包数据库记录失败: {self.hash}") + logger.error(f"[错误] 删除表情包数据库记录失败: {self.hash}") return False except Exception as e: @@ -323,7 +325,7 @@ class EmojiManager: # 计算每个表情包与输入文本的最大情感相似度 emoji_similarities = [] for emoji in all_emojis: - # 跳过已标记为删除的对象 + # 跳过已标记为删除的对象 if emoji.is_deleted: continue @@ -421,17 +423,17 @@ class EmojiManager: objects_to_remove = [] for emoji in self.emoji_objects: try: - # 跳过已经标记为删除的,避免重复处理 + # 跳过已经标记为删除的,避免重复处理 if emoji.is_deleted: - objects_to_remove.append(emoji) # 收集起来一次性移除 + objects_to_remove.append(emoji) # 收集起来一次性移除 continue # 检查文件是否存在 if not os.path.exists(emoji.full_path): logger.warning(f"[检查] 表情包文件丢失: {emoji.full_path}") # 执行表情包对象的删除方法 - await emoji.delete() # delete 方法现在会标记 is_deleted - objects_to_remove.append(emoji) # 标记删除后,也收集起来移除 + await emoji.delete() # delete 方法现在会标记 is_deleted + objects_to_remove.append(emoji) # 标记删除后,也收集起来移除 # 更新计数 self.emoji_num -= 1 removed_count += 1 @@ -453,7 +455,7 @@ class EmojiManager: # 从 self.emoji_objects 中移除标记的对象 if objects_to_remove: - self.emoji_objects = [e for e in self.emoji_objects if e not in objects_to_remove] + self.emoji_objects = [e for e in self.emoji_objects if e not in objects_to_remove] # 清理 EMOJI_REGISTED_DIR 目录中未被追踪的文件 await self.clean_unused_emojis(EMOJI_REGISTED_DIR, self.emoji_objects) @@ -538,7 +540,7 @@ class EmojiManager: if not full_path: logger.warning(f"[加载错误] 数据库记录缺少 'full_path' 字段: {emoji_data.get('_id')}") load_errors += 1 - continue # 跳过缺少 full_path 的记录 + continue # 跳过缺少 full_path 的记录 try: # 使用 full_path 初始化 MaiEmoji 对象 @@ -548,9 +550,9 @@ class EmojiManager: emoji.hash = emoji_data.get("hash", "") # 如果 hash 为空,也跳过?取决于业务逻辑 if not emoji.hash: - logger.warning(f"[加载错误] 数据库记录缺少 'hash' 字段: {full_path}") - load_errors += 1 - continue + logger.warning(f"[加载错误] 数据库记录缺少 'hash' 字段: {full_path}") + load_errors += 1 + continue emoji.description = emoji_data.get("description", "") emoji.emotion = emoji_data.get("emotion", []) @@ -558,21 +560,22 @@ class EmojiManager: # 优先使用 last_used_time,否则用 timestamp,最后用当前时间 last_used = emoji_data.get("last_used_time") timestamp = emoji_data.get("timestamp") - emoji.last_used_time = last_used if last_used is not None else (timestamp if timestamp is not None else time.time()) + emoji.last_used_time = ( + last_used if last_used is not None else (timestamp if timestamp is not None else time.time()) + ) emoji.register_time = timestamp if timestamp is not None else time.time() - emoji.format = emoji_data.get("format", "") # 加载格式 + emoji.format = emoji_data.get("format", "") # 加载格式 # 不需要再手动设置 path 和 filename,__init__ 会自动处理 emoji_objects.append(emoji) - except ValueError as ve: #捕获 __init__ 可能的错误 - logger.error(f"[加载错误] 初始化 MaiEmoji 失败 ({full_path}): {ve}") - load_errors += 1 + except ValueError as ve: # 捕获 __init__ 可能的错误 + logger.error(f"[加载错误] 初始化 MaiEmoji 失败 ({full_path}): {ve}") + load_errors += 1 except Exception as e: - logger.error(f"[加载错误] 处理数据库记录时出错 ({full_path}): {str(e)}") - load_errors += 1 - + logger.error(f"[加载错误] 处理数据库记录时出错 ({full_path}): {str(e)}") + load_errors += 1 # 更新内存中的列表和数量 self.emoji_objects = emoji_objects @@ -580,12 +583,11 @@ class EmojiManager: logger.success(f"[数据库] 加载完成: 共加载 {self.emoji_num} 个表情包记录。") if load_errors > 0: - logger.warning(f"[数据库] 加载过程中出现 {load_errors} 个错误。") - + logger.warning(f"[数据库] 加载过程中出现 {load_errors} 个错误。") except Exception as e: logger.error(f"[错误] 从数据库加载所有表情包对象失败: {str(e)}") - self.emoji_objects = [] # 加载失败则清空列表 + self.emoji_objects = [] # 加载失败则清空列表 self.emoji_num = 0 async def get_emoji_from_db(self, hash=None): @@ -604,8 +606,9 @@ class EmojiManager: if hash: query = {"hash": hash} else: - logger.warning("[查询] 未提供 hash,将尝试加载所有表情包,建议使用 get_all_emoji_from_db 更新管理器状态。") - + logger.warning( + "[查询] 未提供 hash,将尝试加载所有表情包,建议使用 get_all_emoji_from_db 更新管理器状态。" + ) emoji_data_list = list(db.emoji.find(query)) emoji_objects = [] @@ -622,29 +625,30 @@ class EmojiManager: emoji = MaiEmoji(full_path=full_path) emoji.hash = emoji_data.get("hash", "") if not emoji.hash: - logger.warning(f"[加载错误] 数据库记录缺少 'hash' 字段: {full_path}") - load_errors += 1 - continue + logger.warning(f"[加载错误] 数据库记录缺少 'hash' 字段: {full_path}") + load_errors += 1 + continue emoji.description = emoji_data.get("description", "") emoji.emotion = emoji_data.get("emotion", []) emoji.usage_count = emoji_data.get("usage_count", 0) last_used = emoji_data.get("last_used_time") timestamp = emoji_data.get("timestamp") - emoji.last_used_time = last_used if last_used is not None else (timestamp if timestamp is not None else time.time()) + emoji.last_used_time = ( + last_used if last_used is not None else (timestamp if timestamp is not None else time.time()) + ) emoji.register_time = timestamp if timestamp is not None else time.time() emoji.format = emoji_data.get("format", "") emoji_objects.append(emoji) except ValueError as ve: - logger.error(f"[加载错误] 初始化 MaiEmoji 失败 ({full_path}): {ve}") - load_errors += 1 + logger.error(f"[加载错误] 初始化 MaiEmoji 失败 ({full_path}): {ve}") + load_errors += 1 except Exception as e: - logger.error(f"[加载错误] 处理数据库记录时出错 ({full_path}): {str(e)}") - load_errors += 1 - + logger.error(f"[加载错误] 处理数据库记录时出错 ({full_path}): {str(e)}") + load_errors += 1 if load_errors > 0: - logger.warning(f"[查询] 加载过程中出现 {load_errors} 个错误。") + logger.warning(f"[查询] 加载过程中出现 {load_errors} 个错误。") return emoji_objects @@ -661,10 +665,10 @@ class EmojiManager: MaiEmoji 或 None: 如果找到则返回 MaiEmoji 对象,否则返回 None """ for emoji in self.emoji_objects: - # 确保对象未被标记为删除且哈希值匹配 + # 确保对象未被标记为删除且哈希值匹配 if not emoji.is_deleted and emoji.hash == hash: return emoji - return None # 如果循环结束还没找到,则返回 None + return None # 如果循环结束还没找到,则返回 None async def delete_emoji(self, emoji_hash: str) -> bool: """根据哈希值删除表情包 @@ -886,14 +890,14 @@ class EmojiManager: """ file_full_path = os.path.join(EMOJI_DIR, filename) if not os.path.exists(file_full_path): - logger.error(f"[注册失败] 文件不存在: {file_full_path}") - return False + logger.error(f"[注册失败] 文件不存在: {file_full_path}") + return False try: # 1. 创建 MaiEmoji 实例并初始化哈希和格式 new_emoji = MaiEmoji(full_path=file_full_path) init_result = await new_emoji.initialize_hash_format() - if init_result is None or new_emoji.is_deleted: # 初始化失败或文件读取错误 + if init_result is None or new_emoji.is_deleted: # 初始化失败或文件读取错误 logger.error(f"[注册失败] 初始化哈希和格式失败: {filename}") # 是否需要删除源文件?看业务需求,暂时不删 return False @@ -901,22 +905,22 @@ class EmojiManager: # 2. 检查哈希是否已存在 (在内存中检查) if await self.get_emoji_from_manager(new_emoji.hash): logger.warning(f"[注册跳过] 表情包已存在 (Hash: {new_emoji.hash}): {filename}") - # 删除重复的源文件 + # 删除重复的源文件 try: os.remove(file_full_path) logger.info(f"[清理] 删除重复的待注册文件: {filename}") except Exception as e: logger.error(f"[错误] 删除重复文件失败: {str(e)}") - return False # 返回 False 表示未注册新表情 + return False # 返回 False 表示未注册新表情 # 3. 构建描述和情感 try: emoji_base64 = image_path_to_base64(file_full_path) - if emoji_base64 is None: # 再次检查读取 - logger.error(f"[注册失败] 无法读取图片以生成描述: {filename}") - return False + if emoji_base64 is None: # 再次检查读取 + logger.error(f"[注册失败] 无法读取图片以生成描述: {filename}") + return False description, emotions = await self.build_emoji_description(emoji_base64) - if not description: # 检查描述是否成功生成或审核通过 + if not description: # 检查描述是否成功生成或审核通过 logger.warning(f"[注册失败] 未能生成有效描述或审核未通过: {filename}") # 删除未能生成描述的文件 try: @@ -943,9 +947,9 @@ class EmojiManager: replaced = await self.replace_a_emoji(new_emoji) if not replaced: logger.error("[注册失败] 替换表情包失败,无法完成注册") - # 替换失败,删除新表情包文件 + # 替换失败,删除新表情包文件 try: - os.remove(file_full_path) # new_emoji 的 full_path 此时还是源路径 + os.remove(file_full_path) # new_emoji 的 full_path 此时还是源路径 logger.info(f"[清理] 删除替换失败的新表情文件: {filename}") except Exception as e: logger.error(f"[错误] 删除替换失败文件时出错: {str(e)}") @@ -954,7 +958,7 @@ class EmojiManager: return True else: # 直接注册 - register_success = await new_emoji.register_to_db() # 此方法会移动文件并更新 DB + register_success = await new_emoji.register_to_db() # 此方法会移动文件并更新 DB if register_success: # 注册成功后,添加到内存列表 self.emoji_objects.append(new_emoji) @@ -963,20 +967,20 @@ class EmojiManager: return True else: logger.error(f"[注册失败] 保存表情包到数据库/移动文件失败: {filename}") - # register_to_db 失败时,内部会尝试清理移动后的文件,源文件可能还在 - # 是否需要删除源文件? + # register_to_db 失败时,内部会尝试清理移动后的文件,源文件可能还在 + # 是否需要删除源文件? if os.path.exists(file_full_path): - try: - os.remove(file_full_path) - logger.info(f"[清理] 删除注册失败的源文件: {filename}") - except Exception as e: - logger.error(f"[错误] 删除注册失败源文件时出错: {str(e)}") + try: + os.remove(file_full_path) + logger.info(f"[清理] 删除注册失败的源文件: {filename}") + except Exception as e: + logger.error(f"[错误] 删除注册失败源文件时出错: {str(e)}") return False except Exception as e: logger.error(f"[错误] 注册表情包时发生未预期错误 ({filename}): {str(e)}") logger.error(traceback.format_exc()) - # 尝试删除源文件以避免循环处理 + # 尝试删除源文件以避免循环处理 if os.path.exists(file_full_path): try: os.remove(file_full_path) diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 5b1e3151..3dc648ec 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -15,9 +15,7 @@ from src.plugins.models.utils_model import LLMRequest from src.config.config import global_config from src.plugins.chat.utils_image import image_path_to_base64 # Local import needed after move from src.plugins.utils.timer_calculator import Timer # <--- Import Timer -from src.do_tool.tool_use import ToolUser from src.plugins.emoji_system.emoji_manager import emoji_manager -from src.plugins.utils.json_utils import process_llm_tool_calls, extract_tool_call_arguments from src.heart_flow.sub_mind import SubMind from src.heart_flow.observation import Observation from src.plugins.heartFC_chat.heartflow_prompt_builder import global_prompt_manager, prompt_builder @@ -788,7 +786,9 @@ class HeartFChatting: logger.info(f"{self.log_prefix}[Planner] 连续回复 2 次,80% 概率移除 text_reply 和 emoji_reply (触发)") actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"]) else: - logger.info(f"{self.log_prefix}[Planner] 连续回复 2 次,80% 概率移除 text_reply 和 emoji_reply (未触发)") + logger.info( + f"{self.log_prefix}[Planner] 连续回复 2 次,80% 概率移除 text_reply 和 emoji_reply (未触发)" + ) elif lian_xu_wen_ben_hui_fu == 1: if probability_roll < 0.4: logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次,40% 概率移除 text_reply (触发)") @@ -805,10 +805,10 @@ class HeartFChatting: observed_messages_str = observation.talking_message_str_truncate # --- 使用 LLM 进行决策 (JSON 输出模式) --- # - action = "no_reply" # 默认动作 + action = "no_reply" # 默认动作 reasoning = "规划器初始化默认" emoji_query = "" - llm_error = False # LLM 请求或解析错误标志 + llm_error = False # LLM 请求或解析错误标志 # 获取我们将传递给 prompt 构建器和用于验证的当前可用动作 current_available_actions = self.action_manager.get_available_actions() @@ -833,8 +833,8 @@ class HeartFChatting: observed_messages_str, current_mind, self.sub_mind.structured_info, - "", # replan_prompt_str, - current_available_actions # <--- 传入当前可用动作 + "", # replan_prompt_str, + current_available_actions, # <--- 传入当前可用动作 ) # --- 调用 LLM (普通文本生成) --- @@ -851,8 +851,8 @@ class HeartFChatting: reasoning = f"LLM 请求失败: {req_e}" llm_error = True # 直接使用默认动作返回错误结果 - action = "no_reply" # 明确设置为默认值 - emoji_query = "" # 明确设置为空 + action = "no_reply" # 明确设置为默认值 + emoji_query = "" # 明确设置为空 # 不再立即返回,而是继续执行 finally 块以恢复动作 # return { ... } @@ -860,9 +860,11 @@ class HeartFChatting: if not llm_error and llm_content: try: # 尝试去除可能的 markdown 代码块标记 - cleaned_content = llm_content.strip().removeprefix("```json").removeprefix("```").removesuffix("```").strip() + cleaned_content = ( + llm_content.strip().removeprefix("```json").removeprefix("```").removesuffix("```").strip() + ) if not cleaned_content: - raise json.JSONDecodeError("Cleaned content is empty", cleaned_content, 0) + raise json.JSONDecodeError("Cleaned content is empty", cleaned_content, 0) parsed_json = json.loads(cleaned_content) # 提取决策,提供默认值 @@ -881,28 +883,32 @@ class HeartFChatting: emoji_query = "" # 检查 no_reply 是否也恰好被移除了 (极端情况) if "no_reply" not in current_available_actions: - logger.error(f"{self.log_prefix}[Planner] 严重错误:'no_reply' 动作也不可用!无法执行任何动作。") - action = "error" # 回退到错误状态 - reasoning = "无法执行任何有效动作,包括 no_reply" - llm_error = True # 标记为严重错误 + logger.error( + f"{self.log_prefix}[Planner] 严重错误:'no_reply' 动作也不可用!无法执行任何动作。" + ) + action = "error" # 回退到错误状态 + reasoning = "无法执行任何有效动作,包括 no_reply" + llm_error = True # 标记为严重错误 else: - llm_error = False # 视为逻辑修正而非 LLM 错误 + llm_error = False # 视为逻辑修正而非 LLM 错误 else: # 动作有效且可用 action = extracted_action reasoning = extracted_reasoning emoji_query = extracted_emoji_query - llm_error = False # 解析成功 + llm_error = False # 解析成功 logger.debug( - f"{self.log_prefix}[要做什么]\nPrompt:\n{prompt}\n\n决策结果 (来自JSON): {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'" + f"{self.log_prefix}[要做什么]\nPrompt:\n{prompt}\n\n决策结果 (来自JSON): {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'" ) except json.JSONDecodeError as json_e: - logger.warning(f"{self.log_prefix}[Planner] 解析LLM响应JSON失败: {json_e}. LLM原始输出: '{llm_content}'") + logger.warning( + f"{self.log_prefix}[Planner] 解析LLM响应JSON失败: {json_e}. LLM原始输出: '{llm_content}'" + ) reasoning = f"解析LLM响应JSON失败: {json_e}. 将使用默认动作 'no_reply'." - action = "no_reply" # 解析失败则默认不回复 + action = "no_reply" # 解析失败则默认不回复 emoji_query = "" - llm_error = True # 标记解析错误 + llm_error = True # 标记解析错误 except Exception as parse_e: logger.error(f"{self.log_prefix}[Planner] 处理LLM响应时发生意外错误: {parse_e}") reasoning = f"处理LLM响应时发生意外错误: {parse_e}. 将使用默认动作 'no_reply'." @@ -910,12 +916,12 @@ class HeartFChatting: emoji_query = "" llm_error = True elif not llm_error and not llm_content: - # LLM 请求成功但返回空内容 + # LLM 请求成功但返回空内容 logger.warning(f"{self.log_prefix}[Planner] LLM 返回了空内容。") reasoning = "LLM 返回了空内容,使用默认动作 'no_reply'." action = "no_reply" emoji_query = "" - llm_error = True # 标记为空响应错误 + llm_error = True # 标记为空响应错误 # 如果 llm_error 在此阶段为 True,意味着请求成功但解析失败或返回空 # 如果 llm_error 在请求阶段就为 True,则跳过了此解析块 @@ -923,7 +929,7 @@ class HeartFChatting: except Exception as outer_e: logger.error(f"{self.log_prefix}[Planner] Planner 处理过程中发生意外错误: {outer_e}") logger.error(traceback.format_exc()) - action = "error" # 发生未知错误,标记为 error 动作 + action = "error" # 发生未知错误,标记为 error 动作 reasoning = f"Planner 内部处理错误: {outer_e}" emoji_query = "" llm_error = True @@ -944,7 +950,7 @@ class HeartFChatting: logger.info( f"{self.log_prefix}[Planner] 但是麦麦这次不想加表情 ({1 - EMOJI_SEND_PRO:.0%}),忽略表情 '{emoji_query}'" ) - emoji_query = "" # 清空表情请求 + emoji_query = "" # 清空表情请求 else: logger.info(f"{self.log_prefix}[Planner] 好吧,加上表情 '{emoji_query}'") # --- 结束概率性忽略 --- @@ -956,7 +962,7 @@ class HeartFChatting: "emoji_query": emoji_query, "current_mind": current_mind, "observed_messages": observed_messages, - "llm_error": llm_error, # 返回错误状态 + "llm_error": llm_error, # 返回错误状态 } async def _get_anchor_message(self) -> Optional[MessageRecv]: @@ -1178,26 +1184,26 @@ class HeartFChatting: action_options_text = "当前你可以选择的行动有:\n" action_keys = list(current_available_actions.keys()) for name in action_keys: - desc = current_available_actions[name] - action_options_text += f"- '{name}': {desc}\n" + desc = current_available_actions[name] + action_options_text += f"- '{name}': {desc}\n" # --- 选择一个示例动作键 (用于填充模板中的 {example_action}) --- example_action_key = action_keys[0] if action_keys else "no_reply" - # --- 获取提示词模板 --- + # --- 获取提示词模板 --- planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt") - # --- 填充模板 --- + # --- 填充模板 --- prompt = planner_prompt_template.format( bot_name=global_config.BOT_NICKNAME, prompt_personality=prompt_personality, structured_info_block=structured_info_block, chat_content_block=chat_content_block, - current_mind_block=current_mind_block, - replan="", # 暂时留空 replan 信息 + current_mind_block=current_mind_block, + replan="", # 暂时留空 replan 信息 cycle_info_block=cycle_info_block, - action_options_text=action_options_text, # 传入可用动作描述 - example_action=example_action_key # 传入示例动作键 + action_options_text=action_options_text, # 传入可用动作描述 + example_action=example_action_key, # 传入示例动作键 ) return prompt @@ -1205,7 +1211,7 @@ class HeartFChatting: except Exception as e: logger.error(f"{self.log_prefix}[Planner] 构建提示词时出错: {e}") logger.error(traceback.format_exc()) - return "[构建 Planner Prompt 时出错]" # 返回错误提示,避免空字符串 + return "[构建 Planner Prompt 时出错]" # 返回错误提示,避免空字符串 # --- 回复器 (Replier) 的定义 --- # async def _replier_work( diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 7cb847e0..1e5d8d21 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -7,14 +7,13 @@ from src.plugins.utils.chat_message_builder import build_readable_messages, get_ from src.plugins.person_info.relationship_manager import relationship_manager from src.plugins.chat.utils import get_embedding import time -from typing import Union, Optional, Dict, Any +from typing import Union, Optional from ...common.database import db from ..chat.utils import get_recent_group_speaker from ..moods.moods import MoodManager from ..memory_system.Hippocampus import HippocampusManager from ..schedule.schedule_generator import bot_schedule from ..knowledge.knowledge_lib import qa_manager -import traceback logger = get_logger("prompt") @@ -50,7 +49,7 @@ def init_prompt(): # Planner提示词 - 修改为要求 JSON 输出 Prompt( - '''你的名字是{bot_name},{prompt_personality},你现在正在一个群聊中。需要基于以下信息决定如何参与对话: + """你的名字是{bot_name},{prompt_personality},你现在正在一个群聊中。需要基于以下信息决定如何参与对话: {structured_info_block} {chat_content_block} {current_mind_block} @@ -106,7 +105,7 @@ JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query": }} 请输出你的决策 JSON: -''', # 使用三引号避免内部引号问题 +""", # 使用三引号避免内部引号问题 "planner_prompt", # 保持名称不变,替换内容 ) From f363b7ca0021f085c8a48548df9d491305b910ec Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 1 May 2025 01:49:10 +0800 Subject: [PATCH 04/18] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E5=A4=8DLogger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/heart_flow/sub_heartflow.py | 6 +++--- src/plugins/emoji_system/emoji_manager.py | 4 ++-- src/plugins/heartFC_chat/heartFC_chat.py | 20 +++++++++++++++----- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index 291800cf..75918e1b 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -346,7 +346,7 @@ class SubHeartflow: return True # 已经在运行 # 如果实例不存在,则创建并启动 - logger.info(f"{log_prefix} 麦麦准备开始专注聊天 (创建新实例)...") + logger.info(f"{log_prefix} 麦麦准备开始专注聊天...") try: # 创建 HeartFChatting 实例,并传递 从构造函数传入的 回调函数 self.heart_fc_instance = HeartFChatting( @@ -359,7 +359,7 @@ class SubHeartflow: # 初始化并启动 HeartFChatting if await self.heart_fc_instance._initialize(): await self.heart_fc_instance.start() - logger.info(f"{log_prefix} 麦麦已成功进入专注聊天模式 (新实例已启动)。") + logger.debug(f"{log_prefix} 麦麦已成功进入专注聊天模式 (新实例已启动)。") return True else: logger.error(f"{log_prefix} HeartFChatting 初始化失败,无法进入专注模式。") @@ -397,7 +397,7 @@ class SubHeartflow: # 移除限额检查逻辑 logger.debug(f"{log_prefix} 准备进入或保持 专注聊天 状态") if await self._start_heart_fc_chat(): - logger.info(f"{log_prefix} 成功进入或保持 HeartFChatting 状态。") + logger.debug(f"{log_prefix} 成功进入或保持 HeartFChatting 状态。") state_changed = True else: logger.error(f"{log_prefix} 启动 HeartFChatting 失败,无法进入 FOCUSED 状态。") diff --git a/src/plugins/emoji_system/emoji_manager.py b/src/plugins/emoji_system/emoji_manager.py index 5f377678..d6da4ce3 100644 --- a/src/plugins/emoji_system/emoji_manager.py +++ b/src/plugins/emoji_system/emoji_manager.py @@ -1052,9 +1052,9 @@ class EmojiManager: logger.error(f"[错误] 删除文件时出错 ({file_full_path}): {str(e)}") if cleaned_count > 0: - logger.success(f"[清理] 在目录 {emoji_dir} 中清理了 {cleaned_count} 个未追踪的文件。") + logger.success(f"[清理] 在目录 {emoji_dir} 中清理了 {cleaned_count} 个破损表情包。") else: - logger.info(f"[清理] 目录 {emoji_dir} 中没有发现未追踪的文件。") + logger.info(f"[清理] 目录 {emoji_dir} 中没有需要清理的。") except Exception as e: logger.error(f"[错误] 清理未使用表情包文件时出错 ({emoji_dir}): {str(e)}") diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 3dc648ec..e4aa28ab 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -230,7 +230,7 @@ class HeartFChatting: self.log_prefix = f"[{chat_manager.get_stream_name(self.stream_id) or self.stream_id}]" self._initialized = True - logger.info(f"麦麦感觉到了,可以开始认真水群{self.log_prefix} ") + logger.debug(f"{self.log_prefix}麦麦感觉到了,可以开始认真水群 ") return True async def start(self): @@ -261,7 +261,7 @@ class HeartFChatting: pass # 忽略取消或超时错误 self._loop_task = None # 清理旧任务引用 - logger.info(f"{self.log_prefix} 启动认真水群(HFC)主循环...") + logger.debug(f"{self.log_prefix} 启动认真水群(HFC)主循环...") # 创建新的循环任务 self._loop_task = asyncio.create_task(self._hfc_loop()) # 添加完成回调 @@ -439,6 +439,16 @@ class HeartFChatting: # execute:执行 + # 在此处添加日志记录 + if action == "text_reply": + action_str = "回复" + elif action == "emoji_reply": + action_str = "回复表情" + else: + action_str = "不回复" + + logger.info(f"{self.log_prefix} 麦麦决定'{action_str}', 原因'{reasoning}'") + return await self._handle_action( action, reasoning, planner_result.get("emoji_query", ""), cycle_timers, planner_start_db_time ) @@ -760,7 +770,7 @@ class HeartFChatting: cycle_timers: 计时器字典 is_re_planned: 是否为重新规划 (此重构中暂时简化,不处理 is_re_planned 的特殊逻辑) """ - logger.info(f"{self.log_prefix}[Planner] 开始执行规划器 (JSON解析模式)") + logger.info(f"{self.log_prefix}开始想要做什么") actions_to_remove_temporarily = [] # --- 检查历史动作并决定临时移除动作 (逻辑保持不变) --- @@ -948,11 +958,11 @@ class HeartFChatting: logger.debug(f"{self.log_prefix}[Planner] 大模型建议文字回复带表情: '{emoji_query}'") if random.random() > EMOJI_SEND_PRO: logger.info( - f"{self.log_prefix}[Planner] 但是麦麦这次不想加表情 ({1 - EMOJI_SEND_PRO:.0%}),忽略表情 '{emoji_query}'" + f"{self.log_prefix}但是麦麦这次不想加表情 ({1 - EMOJI_SEND_PRO:.0%}),忽略表情 '{emoji_query}'" ) emoji_query = "" # 清空表情请求 else: - logger.info(f"{self.log_prefix}[Planner] 好吧,加上表情 '{emoji_query}'") + logger.info(f"{self.log_prefix}好吧,加上表情 '{emoji_query}'") # --- 结束概率性忽略 --- # 返回结果字典 From e3be745452ca868e85f73692764372e19e12f08c Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 1 May 2025 01:54:06 +0800 Subject: [PATCH 05/18] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E6=94=B9Looger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/heart_flow/sub_heartflow.py | 4 ++-- src/plugins/heartFC_chat/heartFC_chat.py | 4 ++-- src/plugins/heartFC_chat/heartflow_prompt_builder.py | 2 +- src/plugins/heartFC_chat/normal_chat.py | 6 +++--- src/plugins/heartFC_chat/normal_chat_generator.py | 6 ++++-- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index 75918e1b..8d07e6b5 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -511,12 +511,12 @@ class SubHeartflow: # 取消可能存在的旧后台任务 (self.task) if self.task and not self.task.done(): - logger.info(f"{self.log_prefix} 取消子心流主任务 (Shutdown)...") + logger.debug(f"{self.log_prefix} 取消子心流主任务 (Shutdown)...") self.task.cancel() try: await asyncio.wait_for(self.task, timeout=1.0) # 给点时间响应取消 except asyncio.CancelledError: - logger.info(f"{self.log_prefix} 子心流主任务已取消 (Shutdown)。") + logger.debug(f"{self.log_prefix} 子心流主任务已取消 (Shutdown)。") except asyncio.TimeoutError: logger.warning(f"{self.log_prefix} 等待子心流主任务取消超时 (Shutdown)。") except Exception as e: diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index e4aa28ab..b9c50b40 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -1001,8 +1001,8 @@ class HeartFChatting: } anchor_message = MessageRecv(placeholder_msg_dict) anchor_message.update_chat_stream(self.chat_stream) - logger.info( - f"{self.log_prefix} Created placeholder anchor message: ID={anchor_message.message_info.message_id}" + logger.debug( + f"{self.log_prefix} 创建占位符锚点消息: ID={anchor_message.message_info.message_id}" ) return anchor_message diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 1e5d8d21..66cf6af8 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -88,7 +88,7 @@ JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query": {{ "action": "string", // 必须是上面提供的可用行动之一 (例如: '{example_action}') "reasoning": "string", // 做出此决定的详细理由和思考过程,说明你如何应用了回复原则 - "emoji_query": "string" // 可选。如果行动是 'emoji_reply',必须提供表情主题;如果行动是 'text_reply' 且你想附带表情,也在此提供表情主题,否则留空字符串 ""。遵循回复原则,不要滥用。 + "emoji_query": "string" // 可选。如果行动是 'emoji_reply',必须提供表情主题(填写表情包的适用场合);如果行动是 'text_reply' 且你想附带表情,也在此提供表情主题,否则留空字符串 ""。遵循回复原则,不要滥用。 }} 例如: diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py index c159a329..60a6d7cb 100644 --- a/src/plugins/heartFC_chat/normal_chat.py +++ b/src/plugins/heartFC_chat/normal_chat.py @@ -443,7 +443,7 @@ class NormalChat: logger.error(f"[{self.stream_name}] 任务异常: {exc}") logger.error(traceback.format_exc()) except asyncio.CancelledError: - logger.info(f"[{self.stream_name}] 任务已取消") + logger.debug(f"[{self.stream_name}] 任务已取消") except Exception as e: logger.error(f"[{self.stream_name}] 回调处理错误: {e}") finally: @@ -456,12 +456,12 @@ class NormalChat: """停止当前实例的兴趣监控任务。""" if self._chat_task and not self._chat_task.done(): task = self._chat_task - logger.info(f"[{self.stream_name}] 尝试取消聊天任务。") + logger.debug(f"[{self.stream_name}] 尝试取消normal聊天任务。") task.cancel() try: await task # 等待任务响应取消 except asyncio.CancelledError: - logger.info(f"[{self.stream_name}] 聊天任务已成功取消。") + logger.info(f"[{self.stream_name}] 结束一般聊天模式。") except Exception as e: # 回调函数 _handle_task_completion 会处理异常日志 logger.warning(f"[{self.stream_name}] 等待监控任务取消时捕获到异常 (可能已在回调中记录): {e}") diff --git a/src/plugins/heartFC_chat/normal_chat_generator.py b/src/plugins/heartFC_chat/normal_chat_generator.py index 02baf94d..6c7abc7d 100644 --- a/src/plugins/heartFC_chat/normal_chat_generator.py +++ b/src/plugins/heartFC_chat/normal_chat_generator.py @@ -82,12 +82,14 @@ class NormalChatGenerator: sender_name=sender_name, chat_stream=message.chat_stream, ) - logger.info(f"构建prompt时间: {t_build_prompt.human_readable}") + logger.debug(f"构建prompt时间: {t_build_prompt.human_readable}") try: content, reasoning_content, self.current_model_name = await model.generate_response(prompt) - logger.info(f"prompt:{prompt}\n生成回复:{content}") + logger.debug(f"prompt:{prompt}\n生成回复:{content}") + + logger.info(f"对 {message.processed_plain_text} 的回复:{content}") info_catcher.catch_after_llm_generated( prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=self.current_model_name From 261f93530d119124ecea5e4233f2014e6156372b Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 1 May 2025 01:59:28 +0800 Subject: [PATCH 06/18] =?UTF-8?q?fix=EF=BC=9A=E5=85=B3=E9=97=AD=E5=BC=80?= =?UTF-8?q?=E6=8C=82=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/heart_flow/mai_state_manager.py | 4 ++-- template/bot_config_template.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/heart_flow/mai_state_manager.py b/src/heart_flow/mai_state_manager.py index 20f8f8ad..29277820 100644 --- a/src/heart_flow/mai_state_manager.py +++ b/src/heart_flow/mai_state_manager.py @@ -13,8 +13,8 @@ logger = get_logger("mai_state") # The line `enable_unlimited_hfc_chat = False` is setting a configuration parameter that controls # whether a specific debugging feature is enabled or not. When `enable_unlimited_hfc_chat` is set to # `False`, it means that the debugging feature for unlimited focused chatting is disabled. -enable_unlimited_hfc_chat = True # 调试用:无限专注聊天 -# enable_unlimited_hfc_chat = False +# enable_unlimited_hfc_chat = True # 调试用:无限专注聊天 +enable_unlimited_hfc_chat = False prevent_offline_state = True # 目前默认不启用OFFLINE状态 diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 23368cca..c924d35a 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -104,8 +104,8 @@ mentioned_bot_inevitable_reply = false # 提及 bot 必然回复 at_bot_inevitable_reply = false # @bot 必然回复 [focus_chat] #专注聊天 -reply_trigger_threshold = 3.5 # 专注聊天触发阈值,越低越容易进入专注聊天 -default_decay_rate_per_second = 0.98 # 默认衰减率,越大衰减越快,越高越难进入专注聊天 +reply_trigger_threshold = 3.6 # 专注聊天触发阈值,越低越容易进入专注聊天 +default_decay_rate_per_second = 0.95 # 默认衰减率,越大衰减越快,越高越难进入专注聊天 consecutive_no_reply_threshold = 3 # 连续不回复的阈值,越低越容易结束专注聊天 # 以下选项暂时无效 From 6718f81839f65281f83774ea8a2125a2906fa5c6 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 1 May 2025 02:03:50 +0800 Subject: [PATCH 07/18] Update normal_chat.py --- src/plugins/heartFC_chat/normal_chat.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py index 60a6d7cb..9ed63c2d 100644 --- a/src/plugins/heartFC_chat/normal_chat.py +++ b/src/plugins/heartFC_chat/normal_chat.py @@ -358,7 +358,9 @@ class NormalChat: processed_count = 0 # --- 修改:迭代前创建要处理的ID列表副本,防止迭代时修改 --- messages_to_process_initially = list(messages_to_reply) # 创建副本 - # --- 修改结束 --- + # --- 新增:限制最多处理两条消息 --- + messages_to_process_initially = messages_to_process_initially[:2] + # --- 新增结束 --- for item in messages_to_process_initially: # 使用副本迭代 msg_id, (message, interest_value, is_mentioned) = item # --- 修改:在处理前尝试 pop,防止竞争 --- From 54fe078e90c954a5c157ed39b5b9789c12c4059b Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 1 May 2025 02:05:34 +0800 Subject: [PATCH 08/18] Update config.py --- src/config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/config.py b/src/config/config.py index e6cf16d4..fbf558a3 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 = "" +mai_version_fix = "fix-1" if mai_version_fix: if is_test: From 2d062118bb9de7850fab52d6048ab68b16128ca4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 30 Apr 2025 18:05:45 +0000 Subject: [PATCH 09/18] =?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/heartFC_chat/heartFC_chat.py | 6 ++---- src/plugins/heartFC_chat/normal_chat_generator.py | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index b9c50b40..73d679e4 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -446,7 +446,7 @@ class HeartFChatting: action_str = "回复表情" else: action_str = "不回复" - + logger.info(f"{self.log_prefix} 麦麦决定'{action_str}', 原因'{reasoning}'") return await self._handle_action( @@ -1001,9 +1001,7 @@ class HeartFChatting: } anchor_message = MessageRecv(placeholder_msg_dict) anchor_message.update_chat_stream(self.chat_stream) - logger.debug( - f"{self.log_prefix} 创建占位符锚点消息: ID={anchor_message.message_info.message_id}" - ) + logger.debug(f"{self.log_prefix} 创建占位符锚点消息: ID={anchor_message.message_info.message_id}") return anchor_message except Exception as e: diff --git a/src/plugins/heartFC_chat/normal_chat_generator.py b/src/plugins/heartFC_chat/normal_chat_generator.py index 6c7abc7d..ea698bf2 100644 --- a/src/plugins/heartFC_chat/normal_chat_generator.py +++ b/src/plugins/heartFC_chat/normal_chat_generator.py @@ -88,7 +88,7 @@ class NormalChatGenerator: content, reasoning_content, self.current_model_name = await model.generate_response(prompt) logger.debug(f"prompt:{prompt}\n生成回复:{content}") - + logger.info(f"对 {message.processed_plain_text} 的回复:{content}") info_catcher.catch_after_llm_generated( From 93e872db6b7992cd4e582b38d605c0bd8de63258 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 1 May 2025 02:08:19 +0800 Subject: [PATCH 10/18] 123 --- src/plugins/heartFC_chat/heartFC_chat.py | 6 ++---- src/plugins/heartFC_chat/normal_chat_generator.py | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index b9c50b40..73d679e4 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -446,7 +446,7 @@ class HeartFChatting: action_str = "回复表情" else: action_str = "不回复" - + logger.info(f"{self.log_prefix} 麦麦决定'{action_str}', 原因'{reasoning}'") return await self._handle_action( @@ -1001,9 +1001,7 @@ class HeartFChatting: } anchor_message = MessageRecv(placeholder_msg_dict) anchor_message.update_chat_stream(self.chat_stream) - logger.debug( - f"{self.log_prefix} 创建占位符锚点消息: ID={anchor_message.message_info.message_id}" - ) + logger.debug(f"{self.log_prefix} 创建占位符锚点消息: ID={anchor_message.message_info.message_id}") return anchor_message except Exception as e: diff --git a/src/plugins/heartFC_chat/normal_chat_generator.py b/src/plugins/heartFC_chat/normal_chat_generator.py index 6c7abc7d..ea698bf2 100644 --- a/src/plugins/heartFC_chat/normal_chat_generator.py +++ b/src/plugins/heartFC_chat/normal_chat_generator.py @@ -88,7 +88,7 @@ class NormalChatGenerator: content, reasoning_content, self.current_model_name = await model.generate_response(prompt) logger.debug(f"prompt:{prompt}\n生成回复:{content}") - + logger.info(f"对 {message.processed_plain_text} 的回复:{content}") info_catcher.catch_after_llm_generated( From 00544c9b623f299a1273e9ce5e60b8f506ade585 Mon Sep 17 00:00:00 2001 From: tcmofashi Date: Thu, 1 May 2025 02:20:34 +0800 Subject: [PATCH 11/18] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=A7=81?= =?UTF-8?q?=E8=81=8Aprompt=E6=9E=84=E5=BB=BA=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/heartFC_chat/heartflow_prompt_builder.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index a0f266d6..429e9697 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -157,10 +157,13 @@ class PromptBuilder: current_mind_info, structured_info, chat_stream, + sender_name, ) return None - async def _build_prompt_focus(self, reason, current_mind_info, structured_info, chat_stream) -> tuple[str, str]: + async def _build_prompt_focus( + self, reason, current_mind_info, structured_info, chat_stream, sender_name + ) -> tuple[str, str]: individuality = Individuality.get_instance() prompt_personality = individuality.get_prompt(x_person=0, level=2) # 日程构建 @@ -240,6 +243,7 @@ class PromptBuilder: reason=reason, prompt_ger=prompt_ger, moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), + sender_name=sender_name, ) return prompt From 2f669c705543ec4efb36ad6d5490790ada710964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E7=8C=AB?= Date: Thu, 1 May 2025 05:58:18 +0900 Subject: [PATCH 12/18] QA: Update requirements and refactor message handling logic etc. --- bot.py | 1 - requirements.txt | Bin 732 -> 762 bytes scripts/interest_monitor_gui.py | 39 +- src/common/logger.py | 12 +- src/common/message_repository.py | 19 +- src/heart_flow/background_tasks.py | 65 ++-- src/heart_flow/interest_logger.py | 13 +- src/heart_flow/sub_mind.py | 1 + src/plugins/PFC/chat_observer.py | 4 + src/plugins/PFC/message_storage.py | 4 +- src/plugins/PFC/observation_info.py | 4 + src/plugins/PFC/pfc.py | 6 +- src/plugins/chat/bot.py | 4 +- src/plugins/chat/message_sender.py | 72 ++-- src/plugins/chat/utils.py | 2 +- src/plugins/emoji_system/emoji_manager.py | 349 ++++++++---------- src/plugins/heartFC_chat/heartFC_chat.py | 39 +- src/plugins/heartFC_chat/heartFC_sender.py | 39 +- .../heartFC_chat/heartflow_processor.py | 254 ++++++------- .../heartFC_chat/heartflow_prompt_builder.py | 181 ++++----- src/plugins/knowledge/src/qa_manager.py | 2 +- .../respon_info_catcher/info_catcher.py | 24 +- src/plugins/schedule/schedule_generator.py | 1 + src/plugins/utils/chat_message_builder.py | 6 +- src/plugins/willing/mode_custom.py | 18 + 25 files changed, 578 insertions(+), 581 deletions(-) diff --git a/bot.py b/bot.py index d547c360..770f5365 100644 --- a/bot.py +++ b/bot.py @@ -119,7 +119,6 @@ async def graceful_shutdown(): for task in tasks: task.cancel() await asyncio.gather(*tasks, return_exceptions=True) - except Exception as e: logger.error(f"麦麦关闭失败: {e}") diff --git a/requirements.txt b/requirements.txt index 06068d888e7575b1d9352a71d9b73111ebaa78b2..91ae096c192cc55966cf12380ae1df85b176f345 100644 GIT binary patch delta 38 ocmcb^`iphL9VWQ~hD3&BhHN02&XCEF2PC-|ih!(Sh72GJ0NG9m>;M1& delta 7 OcmeyxdWUtx9VP$|B?E5& diff --git a/scripts/interest_monitor_gui.py b/scripts/interest_monitor_gui.py index 1f03b969..0c44507c 100644 --- a/scripts/interest_monitor_gui.py +++ b/scripts/interest_monitor_gui.py @@ -28,8 +28,26 @@ matplotlib.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei"] matplotlib.rcParams["axes.unicode_minus"] = False # 解决负号'-'显示为方块的问题 +def get_random_color(): + """生成随机颜色用于区分线条""" + return "#{:06x}".format(random.randint(0, 0xFFFFFF)) + + +def format_timestamp(ts): + """辅助函数:格式化时间戳,处理 None 或无效值""" + if ts is None: + return "N/A" + try: + # 假设 ts 是 float 类型的时间戳 + dt_object = datetime.fromtimestamp(float(ts)) + return dt_object.strftime("%Y-%m-%d %H:%M:%S") + except (ValueError, TypeError): + return "Invalid Time" + + class InterestMonitorApp: def __init__(self, root): + self._main_mind_loaded = None self.root = root self.root.title(WINDOW_TITLE) self.root.geometry("1800x800") # 调整窗口大小以适应图表 @@ -173,10 +191,6 @@ class InterestMonitorApp: """当 Combobox 选择改变时调用,更新单个流的图表""" self.update_single_stream_plot() - def get_random_color(self): - """生成随机颜色用于区分线条""" - return "#{:06x}".format(random.randint(0, 0xFFFFFF)) - def load_main_mind_history(self): """只读取包含main_mind的日志行,维护历史想法队列""" if not os.path.exists(LOG_FILE_PATH): @@ -332,7 +346,7 @@ class InterestMonitorApp: new_probability_history[stream_id] = deque(maxlen=MAX_HISTORY_POINTS) # 创建概率 deque # 检查是否已有颜色,没有则分配 if stream_id not in self.stream_colors: - self.stream_colors[stream_id] = self.get_random_color() + self.stream_colors[stream_id] = get_random_color() # *** 存储此 stream_id 最新的显示名称 *** new_stream_display_names[stream_id] = group_name @@ -593,17 +607,6 @@ class InterestMonitorApp: # --- 新增:重新绘制画布 --- self.canvas_single.draw() - def format_timestamp(self, ts): - """辅助函数:格式化时间戳,处理 None 或无效值""" - if ts is None: - return "N/A" - try: - # 假设 ts 是 float 类型的时间戳 - dt_object = datetime.fromtimestamp(float(ts)) - return dt_object.strftime("%Y-%m-%d %H:%M:%S") - except (ValueError, TypeError): - return "Invalid Time" - def update_single_stream_details(self, stream_id): """更新单个流详情区域的标签内容""" if stream_id: @@ -616,8 +619,8 @@ class InterestMonitorApp: self.single_stream_sub_mind.set(f"想法: {sub_mind}") self.single_stream_chat_state.set(f"状态: {chat_state}") self.single_stream_threshold.set(f"阈值以上: {'是' if threshold else '否'}") - self.single_stream_last_active.set(f"最后活跃: {self.format_timestamp(last_active_ts)}") - self.single_stream_last_interaction.set(f"最后交互: {self.format_timestamp(last_interaction_ts)}") + self.single_stream_last_active.set(f"最后活跃: {format_timestamp(last_active_ts)}") + self.single_stream_last_interaction.set(f"最后交互: {format_timestamp(last_interaction_ts)}") else: # 如果没有选择流,则清空详情 self.single_stream_sub_mind.set("想法: N/A") diff --git a/src/common/logger.py b/src/common/logger.py index 6c95935e..a0d621d9 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -321,7 +321,7 @@ CHAT_STYLE_CONFIG = { "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 见闻 | {message}", }, "simple": { - "console_format": ("{time:MM-DD HH:mm} | 见闻 | {message}"), # noqa: E501 + "console_format": "{time:MM-DD HH:mm} | 见闻 | {message}", # noqa: E501 "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 见闻 | {message}", }, } @@ -353,7 +353,7 @@ SUB_HEARTFLOW_STYLE_CONFIG = { "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}", }, "simple": { - "console_format": ("{time:MM-DD HH:mm} | 麦麦水群 | {message}"), # noqa: E501 + "console_format": "{time:MM-DD HH:mm} | 麦麦水群 | {message}", # noqa: E501 "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群 | {message}", }, } @@ -369,7 +369,7 @@ SUB_HEARTFLOW_MIND_STYLE_CONFIG = { "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}", }, "simple": { - "console_format": ("{time:MM-DD HH:mm} | 麦麦小脑袋 | {message}"), # noqa: E501 + "console_format": "{time:MM-DD HH:mm} | 麦麦小脑袋 | {message}", # noqa: E501 "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}", }, } @@ -385,7 +385,7 @@ SUBHEARTFLOW_MANAGER_STYLE_CONFIG = { "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群[管理] | {message}", }, "simple": { - "console_format": ("{time:MM-DD HH:mm} | 麦麦水群[管理] | {message}"), # noqa: E501 + "console_format": "{time:MM-DD HH:mm} | 麦麦水群[管理] | {message}", # noqa: E501 "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群[管理] | {message}", }, } @@ -633,7 +633,7 @@ HFC_STYLE_CONFIG = { "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 专注聊天 | {message}", }, "simple": { - "console_format": ("{time:MM-DD HH:mm} | 专注聊天 | {message}"), + "console_format": "{time:MM-DD HH:mm} | 专注聊天 | {message}", "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 专注聊天 | {message}", }, } @@ -1031,7 +1031,7 @@ def add_custom_style_handler( # retention=current_config["retention"], # compression=current_config["compression"], # encoding="utf-8", - # filter=lambda record: record["extra"].get("module") == module_name + # message_filter=lambda record: record["extra"].get("module") == module_name # and record["extra"].get("custom_style") == style_name, # enqueue=True, # ) diff --git a/src/common/message_repository.py b/src/common/message_repository.py index fc7b7e54..72643f91 100644 --- a/src/common/message_repository.py +++ b/src/common/message_repository.py @@ -7,13 +7,16 @@ logger = get_module_logger(__name__) def find_messages( - filter: Dict[str, Any], sort: Optional[List[tuple[str, int]]] = None, limit: int = 0, limit_mode: str = "latest" + message_filter: Dict[str, Any], + sort: Optional[List[tuple[str, int]]] = None, + limit: int = 0, + limit_mode: str = "latest", ) -> List[Dict[str, Any]]: """ 根据提供的过滤器、排序和限制条件查找消息。 Args: - filter: MongoDB 查询过滤器。 + message_filter: MongoDB 查询过滤器。 sort: MongoDB 排序条件列表,例如 [('time', 1)]。仅在 limit 为 0 时生效。 limit: 返回的最大文档数,0表示不限制。 limit_mode: 当 limit > 0 时生效。 'earliest' 表示获取最早的记录, 'latest' 表示获取最新的记录(结果仍按时间正序排列)。默认为 'latest'。 @@ -22,7 +25,7 @@ def find_messages( 消息文档列表,如果出错则返回空列表。 """ try: - query = db.messages.find(filter) + query = db.messages.find(message_filter) results: List[Dict[str, Any]] = [] if limit > 0: @@ -46,28 +49,28 @@ def find_messages( return results except Exception as e: log_message = ( - f"查找消息失败 (filter={filter}, sort={sort}, limit={limit}, limit_mode={limit_mode}): {e}\n" + f"查找消息失败 (filter={message_filter}, sort={sort}, limit={limit}, limit_mode={limit_mode}): {e}\n" + traceback.format_exc() ) logger.error(log_message) return [] -def count_messages(filter: Dict[str, Any]) -> int: +def count_messages(message_filter: Dict[str, Any]) -> int: """ 根据提供的过滤器计算消息数量。 Args: - filter: MongoDB 查询过滤器。 + message_filter: MongoDB 查询过滤器。 Returns: 符合条件的消息数量,如果出错则返回 0。 """ try: - count = db.messages.count_documents(filter) + count = db.messages.count_documents(message_filter) return count except Exception as e: - log_message = f"计数消息失败 (filter={filter}): {e}\n" + traceback.format_exc() + log_message = f"计数消息失败 (message_filter={message_filter}): {e}\n" + traceback.format_exc() logger.error(log_message) return 0 diff --git a/src/heart_flow/background_tasks.py b/src/heart_flow/background_tasks.py index 56fee2a9..38360653 100644 --- a/src/heart_flow/background_tasks.py +++ b/src/heart_flow/background_tasks.py @@ -25,6 +25,33 @@ STATE_UPDATE_INTERVAL_SECONDS = 60 LOG_INTERVAL_SECONDS = 3 +async def _run_periodic_loop( + task_name: str, interval: int, task_func: Callable[..., Coroutine[Any, Any, None]], **kwargs +): + """周期性任务主循环""" + while True: + start_time = asyncio.get_event_loop().time() + # logger.debug(f"开始执行后台任务: {task_name}") + + try: + await task_func(**kwargs) # 执行实际任务 + except asyncio.CancelledError: + logger.info(f"任务 {task_name} 已取消") + break + except Exception as e: + logger.error(f"任务 {task_name} 执行出错: {e}") + logger.error(traceback.format_exc()) + + # 计算并执行间隔等待 + elapsed = asyncio.get_event_loop().time() - start_time + sleep_time = max(0, interval - elapsed) + # if sleep_time < 0.1: # 任务超时处理, DEBUG 时可能干扰断点 + # logger.warning(f"任务 {task_name} 超时执行 ({elapsed:.2f}s > {interval}s)") + await asyncio.sleep(sleep_time) + + logger.debug(f"任务循环结束: {task_name}") # 调整日志信息 + + class BackgroundTaskManager: """管理 Heartflow 的后台周期性任务。""" @@ -143,32 +170,6 @@ class BackgroundTaskManager: # 第三步:清空任务列表 self._tasks = [] # 重置任务列表 - async def _run_periodic_loop( - self, task_name: str, interval: int, task_func: Callable[..., Coroutine[Any, Any, None]], **kwargs - ): - """周期性任务主循环""" - while True: - start_time = asyncio.get_event_loop().time() - # logger.debug(f"开始执行后台任务: {task_name}") - - try: - await task_func(**kwargs) # 执行实际任务 - except asyncio.CancelledError: - logger.info(f"任务 {task_name} 已取消") - break - except Exception as e: - logger.error(f"任务 {task_name} 执行出错: {e}") - logger.error(traceback.format_exc()) - - # 计算并执行间隔等待 - elapsed = asyncio.get_event_loop().time() - start_time - sleep_time = max(0, interval - elapsed) - # if sleep_time < 0.1: # 任务超时处理, DEBUG 时可能干扰断点 - # logger.warning(f"任务 {task_name} 超时执行 ({elapsed:.2f}s > {interval}s)") - await asyncio.sleep(sleep_time) - - logger.debug(f"任务循环结束: {task_name}") # 调整日志信息 - async def _perform_state_update_work(self): """执行状态更新工作""" previous_status = self.mai_state_info.get_current_state() @@ -249,33 +250,33 @@ class BackgroundTaskManager: # --- Specific Task Runners --- # async def _run_state_update_cycle(self, interval: int): - await self._run_periodic_loop( + await _run_periodic_loop( task_name="State Update", interval=interval, task_func=self._perform_state_update_work ) async def _run_absent_into_chat(self, interval: int): - await self._run_periodic_loop( + await _run_periodic_loop( task_name="Into Chat", interval=interval, task_func=self._perform_absent_into_chat ) async def _run_normal_chat_timeout_check_cycle(self, interval: int): - await self._run_periodic_loop( + await _run_periodic_loop( task_name="Normal Chat Timeout Check", interval=interval, task_func=self._normal_chat_timeout_check_work ) async def _run_cleanup_cycle(self): - await self._run_periodic_loop( + await _run_periodic_loop( task_name="Subflow Cleanup", interval=CLEANUP_INTERVAL_SECONDS, task_func=self._perform_cleanup_work ) async def _run_logging_cycle(self): - await self._run_periodic_loop( + await _run_periodic_loop( task_name="State Logging", interval=LOG_INTERVAL_SECONDS, task_func=self._perform_logging_work ) # --- 新增兴趣评估任务运行器 --- async def _run_into_focus_cycle(self): - await self._run_periodic_loop( + await _run_periodic_loop( task_name="Into Focus", interval=INTEREST_EVAL_INTERVAL_SECONDS, task_func=self._perform_into_focus_work, diff --git a/src/heart_flow/interest_logger.py b/src/heart_flow/interest_logger.py index 04cdb6f4..06d3f1cb 100644 --- a/src/heart_flow/interest_logger.py +++ b/src/heart_flow/interest_logger.py @@ -23,6 +23,12 @@ LOG_DIRECTORY = "logs/interest" HISTORY_LOG_FILENAME = "interest_history.log" +def _ensure_log_directory(): + """确保日志目录存在。""" + os.makedirs(LOG_DIRECTORY, exist_ok=True) + logger.info(f"已确保日志目录 '{LOG_DIRECTORY}' 存在") + + class InterestLogger: """负责定期记录主心流和所有子心流的状态到日志文件。""" @@ -37,12 +43,7 @@ class InterestLogger: self.subheartflow_manager = subheartflow_manager self.heartflow = heartflow # 存储 Heartflow 实例 self._history_log_file_path = os.path.join(LOG_DIRECTORY, HISTORY_LOG_FILENAME) - self._ensure_log_directory() - - def _ensure_log_directory(self): - """确保日志目录存在。""" - os.makedirs(LOG_DIRECTORY, exist_ok=True) - logger.info(f"已确保日志目录 '{LOG_DIRECTORY}' 存在") + _ensure_log_directory() async def get_all_subflow_states(self) -> Dict[str, Dict]: """并发获取所有活跃子心流的当前完整状态。""" diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index fbf1be87..f1716d24 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -86,6 +86,7 @@ def calculate_replacement_probability(similarity: float) -> float: class SubMind: def __init__(self, subheartflow_id: str, chat_state: ChatStateInfo, observations: Observation): + self.last_active_time = None self.subheartflow_id = subheartflow_id self.llm_model = LLMRequest( diff --git a/src/plugins/PFC/chat_observer.py b/src/plugins/PFC/chat_observer.py index 102c9502..2822e111 100644 --- a/src/plugins/PFC/chat_observer.py +++ b/src/plugins/PFC/chat_observer.py @@ -37,6 +37,10 @@ class ChatObserver: Args: stream_id: 聊天流ID """ + self.last_check_time = None + self.last_check_time = None + self.last_bot_speak_time = None + self.last_user_speak_time = None if stream_id in self._instances: raise RuntimeError(f"ChatObserver for {stream_id} already exists. Use get_instance() instead.") diff --git a/src/plugins/PFC/message_storage.py b/src/plugins/PFC/message_storage.py index b57f5d2b..cd6a01e3 100644 --- a/src/plugins/PFC/message_storage.py +++ b/src/plugins/PFC/message_storage.py @@ -51,11 +51,9 @@ class MongoDBMessageStorage(MessageStorage): """MongoDB消息存储实现""" async def get_messages_after(self, chat_id: str, message_time: float) -> List[Dict[str, Any]]: - query = {"chat_id": chat_id} + query = {"chat_id": chat_id, "time": {"$gt": message_time}} # print(f"storage_check_message: {message_time}") - query["time"] = {"$gt": message_time} - return list(db.messages.find(query).sort("time", 1)) async def get_messages_before(self, chat_id: str, time_point: float, limit: int = 5) -> List[Dict[str, Any]]: diff --git a/src/plugins/PFC/observation_info.py b/src/plugins/PFC/observation_info.py index 35f39301..af7f537b 100644 --- a/src/plugins/PFC/observation_info.py +++ b/src/plugins/PFC/observation_info.py @@ -158,6 +158,10 @@ class ObservationInfo: # meta_plan_trigger: bool = False # --- 修改:移除 __post_init__ 的参数 --- + def __init__(self): + self.chat_observer = None + self.chat_observer = None + def __post_init__(self): """初始化后创建handler并进行必要的设置""" self.chat_observer: Optional[ChatObserver] = None # 添加类型提示 diff --git a/src/plugins/PFC/pfc.py b/src/plugins/PFC/pfc.py index d6f4c519..e12cb242 100644 --- a/src/plugins/PFC/pfc.py +++ b/src/plugins/PFC/pfc.py @@ -147,14 +147,14 @@ class GoalAnalyzer: # 返回第一个目标作为当前主要目标(如果有) if result: first_goal = result[0] - return (first_goal.get("goal", ""), "", first_goal.get("reasoning", "")) + return first_goal.get("goal", ""), "", first_goal.get("reasoning", "") else: # 单个目标的情况 conversation_info.goal_list.append(result) - return (goal, "", reasoning) + return goal, "", reasoning # 如果解析失败,返回默认值 - return ("", "", "") + return "", "", "" async def _update_goals(self, new_goal: str, method: str, reasoning: str): """更新目标列表 diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 8051d0a8..7ca1483c 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -1,3 +1,5 @@ +from typing import Dict, Any + from ..moods.moods import MoodManager # 导入情绪管理器 from ...config.config import global_config from .message import MessageRecv @@ -46,7 +48,7 @@ class ChatBot: except Exception as e: logger.error(f"创建PFC聊天失败: {e}") - async def message_process(self, message_data: str) -> None: + async def message_process(self, message_data: Dict[str, Any]) -> None: """处理转化后的统一格式消息 这个函数本质是预处理一些数据,根据配置信息和消息内容,预处理消息,并分发到合适的消息处理器中 heart_flow模式:使用思维流系统进行回复 diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index 493397bb..30d943d9 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -17,6 +17,40 @@ from src.common.logger_manager import get_logger logger = get_logger("sender") +async def send_via_ws(message: MessageSending) -> None: + """通过 WebSocket 发送消息""" + try: + await send_message(message) + except Exception as e: + logger.error(f"WS发送失败: {e}") + raise ValueError(f"未找到平台:{message.message_info.platform} 的url配置,请检查配置文件") from e + + +async def send_message( + message: MessageSending, +) -> None: + """发送消息(核心发送逻辑)""" + + # --- 添加计算打字和延迟的逻辑 (从 heartflow_message_sender 移动并调整) --- + typing_time = calculate_typing_time( + input_string=message.processed_plain_text, + thinking_start_time=message.thinking_start_time, + is_emoji=message.is_emoji, + ) + # logger.trace(f"{message.processed_plain_text},{typing_time},计算输入时间结束") # 减少日志 + await asyncio.sleep(typing_time) + # logger.trace(f"{message.processed_plain_text},{typing_time},等待输入时间结束") # 减少日志 + # --- 结束打字延迟 --- + + message_preview = truncate_message(message.processed_plain_text) + + try: + await send_via_ws(message) + logger.success(f"发送消息 '{message_preview}' 成功") # 调整日志格式 + except Exception as e: + logger.error(f"发送消息 '{message_preview}' 失败: {str(e)}") + + class MessageSender: """发送器 (不再是单例)""" @@ -29,39 +63,6 @@ class MessageSender: """设置当前bot实例""" pass - async def send_via_ws(self, message: MessageSending) -> None: - """通过 WebSocket 发送消息""" - try: - await global_api.send_message(message) - except Exception as e: - logger.error(f"WS发送失败: {e}") - raise ValueError(f"未找到平台:{message.message_info.platform} 的url配置,请检查配置文件") from e - - async def send_message( - self, - message: MessageSending, - ) -> None: - """发送消息(核心发送逻辑)""" - - # --- 添加计算打字和延迟的逻辑 (从 heartflow_message_sender 移动并调整) --- - typing_time = calculate_typing_time( - input_string=message.processed_plain_text, - thinking_start_time=message.thinking_start_time, - is_emoji=message.is_emoji, - ) - # logger.trace(f"{message.processed_plain_text},{typing_time},计算输入时间结束") # 减少日志 - await asyncio.sleep(typing_time) - # logger.trace(f"{message.processed_plain_text},{typing_time},等待输入时间结束") # 减少日志 - # --- 结束打字延迟 --- - - message_preview = truncate_message(message.processed_plain_text) - - try: - await self.send_via_ws(message) - logger.success(f"发送消息 '{message_preview}' 成功") # 调整日志格式 - except Exception as e: - logger.error(f"发送消息 '{message_preview}' 失败: {str(e)}") - class MessageContainer: """单个聊天流的发送/思考消息容器""" @@ -119,7 +120,7 @@ class MessageContainer: """移除指定的消息对象,如果消息存在则返回True,否则返回False""" try: _initial_len = len(self.messages) - # 使用列表推导式或 filter 创建新列表,排除要删除的元素 + # 使用列表推导式或 message_filter 创建新列表,排除要删除的元素 # self.messages = [msg for msg in self.messages if msg is not message_to_remove] # 或者直接 remove (如果确定对象唯一性) if message_to_remove in self.messages: @@ -146,6 +147,7 @@ class MessageManager: """管理所有聊天流的消息容器 (不再是单例)""" def __init__(self): + self._processor_task = None self.containers: Dict[str, MessageContainer] = {} self.storage = MessageStorage() # 添加 storage 实例 self._running = True # 处理器运行状态 @@ -226,7 +228,7 @@ class MessageManager: await message.process() # 预处理消息内容 # 使用全局 message_sender 实例 - await message_sender.send_message(message) + await send_message(message) await self.storage.store_message(message, message.chat_stream) # 移除消息要在发送 *之后* diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index 71980f48..16581f3a 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -263,7 +263,7 @@ def split_into_sentences_w_remove_punctuation(text: str) -> List[str]: if char in separators: # 检查分割条件:如果分隔符左右都是英文字母,则不分割 can_split = True - if i > 0 and i < len(text) - 1: + if 0 < i < len(text) - 1: prev_char = text[i - 1] next_char = text[i + 1] # if is_english_letter(prev_char) and is_english_letter(next_char) and char == ' ': # 原计划只对空格应用此规则,现应用于所有分隔符 diff --git a/src/plugins/emoji_system/emoji_manager.py b/src/plugins/emoji_system/emoji_manager.py index d6da4ce3..a75a4f90 100644 --- a/src/plugins/emoji_system/emoji_manager.py +++ b/src/plugins/emoji_system/emoji_manager.py @@ -16,7 +16,6 @@ from ..chat.utils_image import image_path_to_base64, image_manager from ..models.utils_model import LLMRequest from src.common.logger_manager import get_logger - logger = get_logger("emoji") BASE_DIR = os.path.join("data") @@ -24,7 +23,6 @@ EMOJI_DIR = os.path.join(BASE_DIR, "emoji") # 表情包存储目录 EMOJI_REGISTED_DIR = os.path.join(BASE_DIR, "emoji_registed") # 已注册的表情包注册目录 MAX_EMOJI_FOR_PROMPT = 20 # 最大允许的表情包描述数量于图片替换的 prompt 中 - """ 还没经过测试,有些地方数据库和内存数据同步可能不完全 @@ -225,6 +223,140 @@ class MaiEmoji: return False +def _emoji_objects_to_readable_list(emoji_objects): + """将表情包对象列表转换为可读的字符串列表 + + 参数: + emoji_objects: MaiEmoji对象列表 + + 返回: + list[str]: 可读的表情包信息字符串列表 + """ + emoji_info_list = [] + for i, emoji in enumerate(emoji_objects): + # 转换时间戳为可读时间 + time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(emoji.register_time)) + # 构建每个表情包的信息字符串 + emoji_info = f"编号: {i + 1}\n描述: {emoji.description}\n使用次数: {emoji.usage_count}\n添加时间: {time_str}\n" + emoji_info_list.append(emoji_info) + return emoji_info_list + + +def _to_emoji_objects(data): + emoji_objects = [] + load_errors = 0 + emoji_data_list = list(data) + + for emoji_data in emoji_data_list: + full_path = emoji_data.get("full_path") + if not full_path: + logger.warning(f"[加载错误] 数据库记录缺少 'full_path' 字段: {emoji_data.get('_id')}") + load_errors += 1 + continue # 跳过缺少 full_path 的记录 + + try: + # 使用 full_path 初始化 MaiEmoji 对象 + emoji = MaiEmoji(full_path=full_path) + + # 设置从数据库加载的属性 + emoji.hash = emoji_data.get("hash", "") + # 如果 hash 为空,也跳过?取决于业务逻辑 + if not emoji.hash: + logger.warning(f"[加载错误] 数据库记录缺少 'hash' 字段: {full_path}") + load_errors += 1 + continue + + emoji.description = emoji_data.get("description", "") + emoji.emotion = emoji_data.get("emotion", []) + emoji.usage_count = emoji_data.get("usage_count", 0) + # 优先使用 last_used_time,否则用 timestamp,最后用当前时间 + last_used = emoji_data.get("last_used_time") + timestamp = emoji_data.get("timestamp") + emoji.last_used_time = ( + last_used if last_used is not None else (timestamp if timestamp is not None else time.time()) + ) + emoji.register_time = timestamp if timestamp is not None else time.time() + emoji.format = emoji_data.get("format", "") # 加载格式 + + # 不需要再手动设置 path 和 filename,__init__ 会自动处理 + + emoji_objects.append(emoji) + + except ValueError as ve: # 捕获 __init__ 可能的错误 + logger.error(f"[加载错误] 初始化 MaiEmoji 失败 ({full_path}): {ve}") + load_errors += 1 + except Exception as e: + logger.error(f"[加载错误] 处理数据库记录时出错 ({full_path}): {str(e)}") + load_errors += 1 + return emoji_objects, load_errors + return emoji_objects, load_errors + + +def _ensure_emoji_dir(): + """确保表情存储目录存在""" + os.makedirs(EMOJI_DIR, exist_ok=True) + os.makedirs(EMOJI_REGISTED_DIR, exist_ok=True) + + +async def clear_temp_emoji(): + """清理临时表情包 + 清理/data/emoji和/data/image目录下的所有文件 + 当目录中文件数超过100时,会全部删除 + """ + + logger.info("[清理] 开始清理缓存...") + + for need_clear in (os.path.join(BASE_DIR, "emoji"), os.path.join(BASE_DIR, "image")): + if os.path.exists(need_clear): + files = os.listdir(need_clear) + # 如果文件数超过50就全部删除 + if len(files) > 100: + for filename in files: + file_path = os.path.join(need_clear, filename) + if os.path.isfile(file_path): + os.remove(file_path) + logger.debug(f"[清理] 删除: {filename}") + + logger.success("[清理] 完成") + + +async def clean_unused_emojis(emoji_dir, emoji_objects): + """清理指定目录中未被 emoji_objects 追踪的表情包文件""" + if not os.path.exists(emoji_dir): + logger.warning(f"[清理] 目标目录不存在,跳过清理: {emoji_dir}") + return + + try: + # 获取内存中所有有效表情包的完整路径集合 + tracked_full_paths = {emoji.full_path for emoji in emoji_objects if not emoji.is_deleted} + cleaned_count = 0 + + # 遍历指定目录中的所有文件 + for file_name in os.listdir(emoji_dir): + file_full_path = os.path.join(emoji_dir, file_name) + + # 确保处理的是文件而不是子目录 + if not os.path.isfile(file_full_path): + continue + + # 如果文件不在被追踪的集合中,则删除 + if file_full_path not in tracked_full_paths: + try: + os.remove(file_full_path) + logger.info(f"[清理] 删除未追踪的表情包文件: {file_full_path}") + cleaned_count += 1 + except Exception as e: + logger.error(f"[错误] 删除文件时出错 ({file_full_path}): {str(e)}") + + if cleaned_count > 0: + logger.success(f"[清理] 在目录 {emoji_dir} 中清理了 {cleaned_count} 个破损表情包。") + else: + logger.info(f"[清理] 目录 {emoji_dir} 中没有需要清理的。") + + except Exception as e: + logger.error(f"[错误] 清理未使用表情包文件时出错 ({emoji_dir}): {str(e)}") + + class EmojiManager: _instance = None @@ -235,6 +367,7 @@ class EmojiManager: return cls._instance def __init__(self): + self._initialized = None self._scan_task = None self.vlm = LLMRequest(model=global_config.vlm, temperature=0.3, max_tokens=1000, request_type="emoji") self.llm_emotion_judge = LLMRequest( @@ -248,23 +381,18 @@ class EmojiManager: logger.info("启动表情包管理器") - def _ensure_emoji_dir(self): - """确保表情存储目录存在""" - os.makedirs(EMOJI_DIR, exist_ok=True) - os.makedirs(EMOJI_REGISTED_DIR, exist_ok=True) - def initialize(self): """初始化数据库连接和表情目录""" if not self._initialized: try: self._ensure_emoji_collection() - self._ensure_emoji_dir() + _ensure_emoji_dir() self._initialized = True # 更新表情包数量 # 启动时执行一次完整性检查 # await self.check_emoji_file_integrity() - except Exception: - logger.exception("初始化表情管理器失败") + except Exception as e: + logger.exception(f"初始化表情管理器失败: {e}") def _ensure_db(self): """确保数据库已初始化""" @@ -291,12 +419,12 @@ class EmojiManager: db.emoji.create_index([("embedding", "2dsphere")]) db.emoji.create_index([("filename", 1)], unique=True) - def record_usage(self, hash: str): + def record_usage(self, emoji_hash: str): """记录表情使用次数""" try: - db.emoji.update_one({"hash": hash}, {"$inc": {"usage_count": 1}}) + db.emoji.update_one({"hash": emoji_hash}, {"$inc": {"usage_count": 1}}) for emoji in self.emoji_objects: - if emoji.hash == hash: + if emoji.hash == emoji_hash: emoji.usage_count += 1 break @@ -458,7 +586,7 @@ class EmojiManager: self.emoji_objects = [e for e in self.emoji_objects if e not in objects_to_remove] # 清理 EMOJI_REGISTED_DIR 目录中未被追踪的文件 - await self.clean_unused_emojis(EMOJI_REGISTED_DIR, self.emoji_objects) + await clean_unused_emojis(EMOJI_REGISTED_DIR, self.emoji_objects) # 输出清理结果 if removed_count > 0: @@ -477,7 +605,7 @@ class EmojiManager: while True: logger.info("[扫描] 开始检查表情包完整性...") await self.check_emoji_file_integrity() - await self.clear_temp_emoji() + await clear_temp_emoji() logger.info("[扫描] 开始扫描新表情包...") # 检查表情包目录是否存在 @@ -531,51 +659,7 @@ class EmojiManager: self._ensure_db() logger.info("[数据库] 开始加载所有表情包记录...") - all_emoji_data = list(db.emoji.find()) - emoji_objects = [] - load_errors = 0 - - for emoji_data in all_emoji_data: - full_path = emoji_data.get("full_path") - if not full_path: - logger.warning(f"[加载错误] 数据库记录缺少 'full_path' 字段: {emoji_data.get('_id')}") - load_errors += 1 - continue # 跳过缺少 full_path 的记录 - - try: - # 使用 full_path 初始化 MaiEmoji 对象 - emoji = MaiEmoji(full_path=full_path) - - # 设置从数据库加载的属性 - emoji.hash = emoji_data.get("hash", "") - # 如果 hash 为空,也跳过?取决于业务逻辑 - if not emoji.hash: - logger.warning(f"[加载错误] 数据库记录缺少 'hash' 字段: {full_path}") - load_errors += 1 - continue - - emoji.description = emoji_data.get("description", "") - emoji.emotion = emoji_data.get("emotion", []) - emoji.usage_count = emoji_data.get("usage_count", 0) - # 优先使用 last_used_time,否则用 timestamp,最后用当前时间 - last_used = emoji_data.get("last_used_time") - timestamp = emoji_data.get("timestamp") - emoji.last_used_time = ( - last_used if last_used is not None else (timestamp if timestamp is not None else time.time()) - ) - emoji.register_time = timestamp if timestamp is not None else time.time() - emoji.format = emoji_data.get("format", "") # 加载格式 - - # 不需要再手动设置 path 和 filename,__init__ 会自动处理 - - emoji_objects.append(emoji) - - except ValueError as ve: # 捕获 __init__ 可能的错误 - logger.error(f"[加载错误] 初始化 MaiEmoji 失败 ({full_path}): {ve}") - load_errors += 1 - except Exception as e: - logger.error(f"[加载错误] 处理数据库记录时出错 ({full_path}): {str(e)}") - load_errors += 1 + emoji_objects, load_errors = _to_emoji_objects(db.emoji.find()) # 更新内存中的列表和数量 self.emoji_objects = emoji_objects @@ -590,11 +674,11 @@ class EmojiManager: self.emoji_objects = [] # 加载失败则清空列表 self.emoji_num = 0 - async def get_emoji_from_db(self, hash=None): + async def get_emoji_from_db(self, emoji_hash=None): """获取指定哈希值的表情包并初始化为MaiEmoji类对象列表 (主要用于调试或特定查找) 参数: - hash: 可选,如果提供则只返回指定哈希值的表情包 + emoji_hash: 可选,如果提供则只返回指定哈希值的表情包 返回: list[MaiEmoji]: 表情包对象列表 @@ -603,49 +687,14 @@ class EmojiManager: self._ensure_db() query = {} - if hash: - query = {"hash": hash} + if emoji_hash: + query = {"hash": emoji_hash} else: logger.warning( "[查询] 未提供 hash,将尝试加载所有表情包,建议使用 get_all_emoji_from_db 更新管理器状态。" ) - emoji_data_list = list(db.emoji.find(query)) - emoji_objects = [] - load_errors = 0 - - for emoji_data in emoji_data_list: - full_path = emoji_data.get("full_path") - if not full_path: - logger.warning(f"[加载错误] 数据库记录缺少 'full_path' 字段: {emoji_data.get('_id')}") - load_errors += 1 - continue - - try: - emoji = MaiEmoji(full_path=full_path) - emoji.hash = emoji_data.get("hash", "") - if not emoji.hash: - logger.warning(f"[加载错误] 数据库记录缺少 'hash' 字段: {full_path}") - load_errors += 1 - continue - - emoji.description = emoji_data.get("description", "") - emoji.emotion = emoji_data.get("emotion", []) - emoji.usage_count = emoji_data.get("usage_count", 0) - last_used = emoji_data.get("last_used_time") - timestamp = emoji_data.get("timestamp") - emoji.last_used_time = ( - last_used if last_used is not None else (timestamp if timestamp is not None else time.time()) - ) - emoji.register_time = timestamp if timestamp is not None else time.time() - emoji.format = emoji_data.get("format", "") - emoji_objects.append(emoji) - except ValueError as ve: - logger.error(f"[加载错误] 初始化 MaiEmoji 失败 ({full_path}): {ve}") - load_errors += 1 - except Exception as e: - logger.error(f"[加载错误] 处理数据库记录时出错 ({full_path}): {str(e)}") - load_errors += 1 + emoji_objects, load_errors = _to_emoji_objects(db.emoji.find(query)) if load_errors > 0: logger.warning(f"[查询] 加载过程中出现 {load_errors} 个错误。") @@ -656,17 +705,17 @@ class EmojiManager: logger.error(f"[错误] 从数据库获取表情包对象失败: {str(e)}") return [] - async def get_emoji_from_manager(self, hash) -> Optional[MaiEmoji]: + async def get_emoji_from_manager(self, emoji_hash) -> Optional[MaiEmoji]: """从内存中的 emoji_objects 列表获取表情包 参数: - hash: 要查找的表情包哈希值 + emoji_hash: 要查找的表情包哈希值 返回: MaiEmoji 或 None: 如果找到则返回 MaiEmoji 对象,否则返回 None """ for emoji in self.emoji_objects: # 确保对象未被标记为删除且哈希值匹配 - if not emoji.is_deleted and emoji.hash == hash: + if not emoji.is_deleted and emoji.hash == emoji_hash: return emoji return None # 如果循环结束还没找到,则返回 None @@ -709,26 +758,6 @@ class EmojiManager: logger.error(traceback.format_exc()) return False - def _emoji_objects_to_readable_list(self, emoji_objects): - """将表情包对象列表转换为可读的字符串列表 - - 参数: - emoji_objects: MaiEmoji对象列表 - - 返回: - list[str]: 可读的表情包信息字符串列表 - """ - emoji_info_list = [] - for i, emoji in enumerate(emoji_objects): - # 转换时间戳为可读时间 - time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(emoji.register_time)) - # 构建每个表情包的信息字符串 - emoji_info = ( - f"编号: {i + 1}\n描述: {emoji.description}\n使用次数: {emoji.usage_count}\n添加时间: {time_str}\n" - ) - emoji_info_list.append(emoji_info) - return emoji_info_list - async def replace_a_emoji(self, new_emoji: MaiEmoji): """替换一个表情包 @@ -755,7 +784,7 @@ class EmojiManager: ) # 将表情包信息转换为可读的字符串 - emoji_info_list = self._emoji_objects_to_readable_list(selected_emojis) + emoji_info_list = _emoji_objects_to_readable_list(selected_emojis) # 构建提示词 prompt = ( @@ -853,7 +882,7 @@ class EmojiManager: ''' content, _ = await self.vlm.generate_response_for_image(prompt, image_base64, image_format) if content == "否": - return None, [] + return "", [] # 分析情感含义 emotion_prompt = f""" @@ -989,76 +1018,6 @@ class EmojiManager: logger.error(f"[错误] 删除异常处理文件时出错: {remove_error}") return False - async def clear_temp_emoji(self): - """清理临时表情包 - 清理/data/emoji和/data/image目录下的所有文件 - 当目录中文件数超过100时,会全部删除 - """ - - logger.info("[清理] 开始清理缓存...") - - # 清理emoji目录 - emoji_dir = os.path.join(BASE_DIR, "emoji") - if os.path.exists(emoji_dir): - files = os.listdir(emoji_dir) - # 如果文件数超过50就全部删除 - if len(files) > 100: - for filename in files: - file_path = os.path.join(emoji_dir, filename) - if os.path.isfile(file_path): - os.remove(file_path) - logger.debug(f"[清理] 删除: {filename}") - - # 清理image目录 - image_dir = os.path.join(BASE_DIR, "image") - if os.path.exists(image_dir): - files = os.listdir(image_dir) - # 如果文件数超过50就全部删除 - if len(files) > 100: - for filename in files: - file_path = os.path.join(image_dir, filename) - if os.path.isfile(file_path): - os.remove(file_path) - logger.debug(f"[清理] 删除图片: {filename}") - - logger.success("[清理] 完成") - - async def clean_unused_emojis(self, emoji_dir, emoji_objects): - """清理指定目录中未被 emoji_objects 追踪的表情包文件""" - if not os.path.exists(emoji_dir): - logger.warning(f"[清理] 目标目录不存在,跳过清理: {emoji_dir}") - return - - try: - # 获取内存中所有有效表情包的完整路径集合 - tracked_full_paths = {emoji.full_path for emoji in emoji_objects if not emoji.is_deleted} - cleaned_count = 0 - - # 遍历指定目录中的所有文件 - for file_name in os.listdir(emoji_dir): - file_full_path = os.path.join(emoji_dir, file_name) - - # 确保处理的是文件而不是子目录 - if not os.path.isfile(file_full_path): - continue - - # 如果文件不在被追踪的集合中,则删除 - if file_full_path not in tracked_full_paths: - try: - os.remove(file_full_path) - logger.info(f"[清理] 删除未追踪的表情包文件: {file_full_path}") - cleaned_count += 1 - except Exception as e: - logger.error(f"[错误] 删除文件时出错 ({file_full_path}): {str(e)}") - - if cleaned_count > 0: - logger.success(f"[清理] 在目录 {emoji_dir} 中清理了 {cleaned_count} 个破损表情包。") - else: - logger.info(f"[清理] 目录 {emoji_dir} 中没有需要清理的。") - - except Exception as e: - logger.error(f"[错误] 清理未使用表情包文件时出错 ({emoji_dir}): {str(e)}") - # 创建全局单例 emoji_manager = EmojiManager() diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 73d679e4..47d420dd 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -144,6 +144,25 @@ class SenderError(HeartFCError): pass +async def _handle_cycle_delay(action_taken_this_cycle: bool, cycle_start_time: float, log_prefix: str): + """处理循环延迟""" + cycle_duration = time.monotonic() - cycle_start_time + + try: + sleep_duration = 0.0 + if not action_taken_this_cycle and cycle_duration < 1: + sleep_duration = 1 - cycle_duration + elif cycle_duration < 0.2: + sleep_duration = 0.2 + + if sleep_duration > 0: + await asyncio.sleep(sleep_duration) + + except asyncio.CancelledError: + logger.info(f"{log_prefix} Sleep interrupted, loop likely cancelling.") + raise + + class HeartFChatting: """ 管理一个连续的Plan-Replier-Sender循环 @@ -327,7 +346,7 @@ class HeartFChatting: self._current_cycle.timers = cycle_timers # 防止循环过快消耗资源 - await self._handle_cycle_delay(action_taken, loop_cycle_start_time, self.log_prefix) + await _handle_cycle_delay(action_taken, loop_cycle_start_time, self.log_prefix) # 完成当前循环并保存历史 self._current_cycle.complete_cycle() @@ -715,24 +734,6 @@ class HeartFChatting: if not self._shutting_down: logger.debug(f"{log_prefix} 该次决策耗时: {'; '.join(timer_strings)}") - async def _handle_cycle_delay(self, action_taken_this_cycle: bool, cycle_start_time: float, log_prefix: str): - """处理循环延迟""" - cycle_duration = time.monotonic() - cycle_start_time - - try: - sleep_duration = 0.0 - if not action_taken_this_cycle and cycle_duration < 1: - sleep_duration = 1 - cycle_duration - elif cycle_duration < 0.2: - sleep_duration = 0.2 - - if sleep_duration > 0: - await asyncio.sleep(sleep_duration) - - except asyncio.CancelledError: - logger.info(f"{log_prefix} Sleep interrupted, loop likely cancelling.") - raise - async def _get_submind_thinking(self, cycle_timers: dict) -> str: """ 获取子思维的思考结果 diff --git a/src/plugins/heartFC_chat/heartFC_sender.py b/src/plugins/heartFC_chat/heartFC_sender.py index 9e65edcf..a4103667 100644 --- a/src/plugins/heartFC_chat/heartFC_sender.py +++ b/src/plugins/heartFC_chat/heartFC_sender.py @@ -12,6 +12,22 @@ from src.plugins.chat.utils import calculate_typing_time logger = get_logger("sender") +async def send_message(message: MessageSending) -> None: + """合并后的消息发送函数,包含WS发送和日志记录""" + message_preview = truncate_message(message.processed_plain_text) + + try: + # 直接调用API发送消息 + await send_message(message) + logger.success(f"发送消息 '{message_preview}' 成功") + + 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 + raise e # 重新抛出其他异常 + + class HeartFCSender: """管理消息的注册、即时处理、发送和存储,并跟踪思考状态。""" @@ -21,21 +37,6 @@ class HeartFCSender: self.thinking_messages: Dict[str, Dict[str, MessageThinking]] = {} self._thinking_lock = asyncio.Lock() # 保护 thinking_messages 的锁 - async def send_message(self, message: MessageSending) -> None: - """合并后的消息发送函数,包含WS发送和日志记录""" - message_preview = truncate_message(message.processed_plain_text) - - try: - # 直接调用API发送消息 - await global_api.send_message(message) - logger.success(f"发送消息 '{message_preview}' 成功") - - 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 - raise e # 重新抛出其他异常 - async def register_thinking(self, thinking_message: MessageThinking): """注册一个思考中的消息。""" if not thinking_message.chat_stream or not thinking_message.message_info.message_id: @@ -73,7 +74,7 @@ class HeartFCSender: 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, type=False): + async def type_and_send_message(self, message: MessageSending, typing=False): """ 立即处理、发送并存储单个 MessageSending 消息。 调用此方法前,应先调用 register_thinking 注册对应的思考消息。 @@ -100,7 +101,7 @@ class HeartFCSender: await message.process() - if type: + if typing: typing_time = calculate_typing_time( input_string=message.processed_plain_text, thinking_start_time=message.thinking_start_time, @@ -108,7 +109,7 @@ class HeartFCSender: ) await asyncio.sleep(typing_time) - await self.send_message(message) + await send_message(message) await self.storage.store_message(message, message.chat_stream) except Exception as e: @@ -136,7 +137,7 @@ class HeartFCSender: await asyncio.sleep(0.5) - await self.send_message(message) # 使用现有的发送方法 + await send_message(message) # 使用现有的发送方法 await self.storage.store_message(message, message.chat_stream) # 使用现有的存储方法 except Exception as e: diff --git a/src/plugins/heartFC_chat/heartflow_processor.py b/src/plugins/heartFC_chat/heartflow_processor.py index f7f3819c..5bd63b14 100644 --- a/src/plugins/heartFC_chat/heartflow_processor.py +++ b/src/plugins/heartFC_chat/heartflow_processor.py @@ -12,11 +12,134 @@ from ..chat.chat_stream import chat_manager from ..chat.message_buffer import message_buffer from ..utils.timer_calculator import Timer from src.plugins.person_info.relationship_manager import relationship_manager -from typing import Optional, Tuple +from typing import Optional, Tuple, Dict, Any logger = get_logger("chat") +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}") + + +async def _process_relationship(message: MessageRecv) -> None: + """处理用户关系逻辑 + + Args: + message: 消息对象,包含用户信息 + """ + platform = message.message_info.platform + user_id = message.message_info.user_info.user_id + nickname = message.message_info.user_info.user_nickname + cardname = message.message_info.user_info.user_cardname or nickname + + is_known = await relationship_manager.is_known_some_one(platform, user_id) + + if not is_known: + logger.info(f"首次认识用户: {nickname}") + await relationship_manager.first_knowing_some_one(platform, user_id, nickname, cardname, "") + elif not await relationship_manager.is_qved_name(platform, user_id): + logger.info(f"给用户({nickname},{cardname})取名: {nickname}") + await relationship_manager.first_knowing_some_one(platform, user_id, nickname, cardname, "") + + +async def _calculate_interest(message: MessageRecv) -> Tuple[float, bool]: + """计算消息的兴趣度 + + Args: + message: 待处理的消息对象 + + Returns: + Tuple[float, bool]: (兴趣度, 是否被提及) + """ + is_mentioned, _ = is_mentioned_bot_in_message(message) + interested_rate = 0.0 + + with Timer("记忆激活"): + interested_rate = await HippocampusManager.get_instance().get_activate_from_text( + message.processed_plain_text, + fast_retrieval=True, + ) + logger.trace(f"记忆激活率: {interested_rate:.2f}") + + if is_mentioned: + interest_increase_on_mention = 1 + interested_rate += interest_increase_on_mention + + return interested_rate, is_mentioned + + +def _get_message_type(message: MessageRecv) -> str: + """获取消息类型 + + Args: + message: 消息对象 + + Returns: + str: 消息类型 + """ + if message.message_segment.type != "seglist": + return message.message_segment.type + + if ( + isinstance(message.message_segment.data, list) + and all(isinstance(x, Seg) for x in message.message_segment.data) + and len(message.message_segment.data) == 1 + ): + return message.message_segment.data[0].type + + return "seglist" + + +def _check_ban_words(text: str, chat, userinfo) -> bool: + """检查消息是否包含过滤词 + + Args: + text: 待检查的文本 + chat: 聊天对象 + userinfo: 用户信息 + + Returns: + bool: 是否包含过滤词 + """ + for word in global_config.ban_words: + if word in text: + chat_name = chat.group_info.group_name if chat.group_info else "私聊" + logger.info(f"[{chat_name}]{userinfo.user_nickname}:{text}") + logger.info(f"[过滤词识别]消息中含有{word},filtered") + return True + return False + + +def _check_ban_regex(text: str, chat, userinfo) -> bool: + """检查消息是否匹配过滤正则表达式 + + Args: + text: 待检查的文本 + chat: 聊天对象 + userinfo: 用户信息 + + Returns: + bool: 是否匹配过滤正则 + """ + for pattern in global_config.ban_msgs_regex: + if pattern.search(text): + chat_name = chat.group_info.group_name if chat.group_info else "私聊" + logger.info(f"[{chat_name}]{userinfo.user_nickname}:{text}") + logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered") + return True + return False + + class HeartFCProcessor: """心流处理器,负责处理接收到的消息并计算兴趣度""" @@ -24,86 +147,7 @@ class HeartFCProcessor: """初始化心流处理器,创建消息存储实例""" self.storage = MessageStorage() - async def _handle_error(self, 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}") - - async def _process_relationship(self, message: MessageRecv) -> None: - """处理用户关系逻辑 - - Args: - message: 消息对象,包含用户信息 - """ - platform = message.message_info.platform - user_id = message.message_info.user_info.user_id - nickname = message.message_info.user_info.user_nickname - cardname = message.message_info.user_info.user_cardname or nickname - - is_known = await relationship_manager.is_known_some_one(platform, user_id) - - if not is_known: - logger.info(f"首次认识用户: {nickname}") - await relationship_manager.first_knowing_some_one(platform, user_id, nickname, cardname, "") - elif not await relationship_manager.is_qved_name(platform, user_id): - logger.info(f"给用户({nickname},{cardname})取名: {nickname}") - await relationship_manager.first_knowing_some_one(platform, user_id, nickname, cardname, "") - - async def _calculate_interest(self, message: MessageRecv) -> Tuple[float, bool]: - """计算消息的兴趣度 - - Args: - message: 待处理的消息对象 - - Returns: - Tuple[float, bool]: (兴趣度, 是否被提及) - """ - is_mentioned, _ = is_mentioned_bot_in_message(message) - interested_rate = 0.0 - - with Timer("记忆激活"): - interested_rate = await HippocampusManager.get_instance().get_activate_from_text( - message.processed_plain_text, - fast_retrieval=True, - ) - logger.trace(f"记忆激活率: {interested_rate:.2f}") - - if is_mentioned: - interest_increase_on_mention = 1 - interested_rate += interest_increase_on_mention - - return interested_rate, is_mentioned - - def _get_message_type(self, message: MessageRecv) -> str: - """获取消息类型 - - Args: - message: 消息对象 - - Returns: - str: 消息类型 - """ - if message.message_segment.type != "seglist": - return message.message_segment.type - - if ( - isinstance(message.message_segment.data, list) - and all(isinstance(x, Seg) for x in message.message_segment.data) - and len(message.message_segment.data) == 1 - ): - return message.message_segment.data[0].type - - return "seglist" - - async def process_message(self, message_data: str) -> None: + async def process_message(self, message_data: Dict[str, Any]) -> None: """处理接收到的原始消息数据 主要流程: @@ -138,7 +182,7 @@ class HeartFCProcessor: await message.process() # 3. 过滤检查 - if self._check_ban_words(message.processed_plain_text, chat, userinfo) or self._check_ban_regex( + if _check_ban_words(message.processed_plain_text, chat, userinfo) or _check_ban_regex( message.raw_message, chat, userinfo ): return @@ -146,7 +190,7 @@ class HeartFCProcessor: # 4. 缓冲检查 buffer_result = await message_buffer.query_buffer_result(message) if not buffer_result: - msg_type = self._get_message_type(message) + msg_type = _get_message_type(message) type_messages = { "text": f"触发缓冲,消息:{message.processed_plain_text}", "image": "触发缓冲,表情包/图片等待中", @@ -160,7 +204,7 @@ class HeartFCProcessor: logger.trace(f"存储成功: {message.processed_plain_text}") # 6. 兴趣度计算与更新 - interested_rate, is_mentioned = await self._calculate_interest(message) + interested_rate, is_mentioned = await _calculate_interest(message) await subheartflow.interest_chatting.increase_interest(value=interested_rate) subheartflow.interest_chatting.add_interest_dict(message, interested_rate, is_mentioned) @@ -175,45 +219,7 @@ class HeartFCProcessor: ) # 8. 关系处理 - await self._process_relationship(message) + await _process_relationship(message) except Exception as e: - await self._handle_error(e, "消息处理失败", message) - - def _check_ban_words(self, text: str, chat, userinfo) -> bool: - """检查消息是否包含过滤词 - - Args: - text: 待检查的文本 - chat: 聊天对象 - userinfo: 用户信息 - - Returns: - bool: 是否包含过滤词 - """ - for word in global_config.ban_words: - if word in text: - chat_name = chat.group_info.group_name if chat.group_info else "私聊" - logger.info(f"[{chat_name}]{userinfo.user_nickname}:{text}") - logger.info(f"[过滤词识别]消息中含有{word},filtered") - return True - return False - - def _check_ban_regex(self, text: str, chat, userinfo) -> bool: - """检查消息是否匹配过滤正则表达式 - - Args: - text: 待检查的文本 - chat: 聊天对象 - userinfo: 用户信息 - - Returns: - bool: 是否匹配过滤正则 - """ - for pattern in global_config.ban_msgs_regex: - if pattern.search(text): - chat_name = chat.group_info.group_name if chat.group_info else "私聊" - logger.info(f"[{chat_name}]{userinfo.user_nickname}:{text}") - logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered") - return True - return False + await _handle_error(e, "消息处理失败", message) diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 40819f01..9b83f265 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -151,6 +151,96 @@ JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query": Prompt("\n你有以下这些**知识**:\n{prompt_info}\n请你**记住上面的知识**,之后可能会用到。\n", "knowledge_prompt") +async def _build_prompt_focus( + reason, current_mind_info, structured_info, chat_stream, sender_name +) -> tuple[str, str]: + individuality = Individuality.get_instance() + prompt_personality = individuality.get_prompt(x_person=0, level=2) + # 日程构建 + # schedule_prompt = f'''你现在正在做的事情是:{bot_schedule.get_current_num_task(num = 1,time_info = False)}''' + + if chat_stream.group_info: + chat_in_group = True + else: + chat_in_group = False + + message_list_before_now = get_raw_msg_before_timestamp_with_chat( + chat_id=chat_stream.stream_id, + timestamp=time.time(), + limit=global_config.observation_context_size, + ) + + chat_talking_prompt = await build_readable_messages( + message_list_before_now, + replace_bot_name=True, + merge_messages=False, + timestamp_mode="normal", + read_mark=0.0, + truncate=True, + ) + + # 中文高手(新加的好玩功能) + prompt_ger = "" + if random.random() < 0.04: + prompt_ger += "你喜欢用倒装句" + if random.random() < 0.02: + prompt_ger += "你喜欢用反问句" + + reply_styles1 = [ + ("给出日常且口语化的回复,平淡一些", 0.4), # 40%概率 + ("给出非常简短的回复", 0.4), # 40%概率 + ("给出缺失主语的回复,简短", 0.15), # 15%概率 + ("给出带有语病的回复,朴实平淡", 0.05), # 5%概率 + ] + reply_style1_chosen = random.choices( + [style[0] for style in reply_styles1], weights=[style[1] for style in reply_styles1], k=1 + )[0] + + reply_styles2 = [ + ("不要回复的太有条理,可以有个性", 0.6), # 60%概率 + ("不要回复的太有条理,可以复读", 0.15), # 15%概率 + ("回复的认真一些", 0.2), # 20%概率 + ("可以回复单个表情符号", 0.05), # 5%概率 + ] + reply_style2_chosen = random.choices( + [style[0] for style in reply_styles2], weights=[style[1] for style in reply_styles2], k=1 + )[0] + + if structured_info: + structured_info_prompt = await global_prompt_manager.format_prompt( + "info_from_tools", structured_info=structured_info + ) + else: + structured_info_prompt = "" + + logger.debug("开始构建prompt") + + prompt = await global_prompt_manager.format_prompt( + "heart_flow_prompt", + info_from_tools=structured_info_prompt, + chat_target=await global_prompt_manager.get_prompt_async("chat_target_group1") + if chat_in_group + else await global_prompt_manager.get_prompt_async("chat_target_private1"), + chat_talking_prompt=chat_talking_prompt, + bot_name=global_config.BOT_NICKNAME, + prompt_personality=prompt_personality, + chat_target_2=await global_prompt_manager.get_prompt_async("chat_target_group2") + if chat_in_group + else await global_prompt_manager.get_prompt_async("chat_target_private2"), + current_mind_info=current_mind_info, + reply_style2=reply_style2_chosen, + reply_style1=reply_style1_chosen, + reason=reason, + prompt_ger=prompt_ger, + moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), + sender_name=sender_name, + ) + + logger.debug(f"focus_chat_prompt: \n{prompt}") + + return prompt + + class PromptBuilder: def __init__(self): self.prompt_built = "" @@ -170,7 +260,7 @@ class PromptBuilder: return await self._build_prompt_normal(chat_stream, message_txt, sender_name) elif build_mode == "focus": - return await self._build_prompt_focus( + return await _build_prompt_focus( reason, current_mind_info, structured_info, @@ -179,95 +269,6 @@ class PromptBuilder: ) return None - async def _build_prompt_focus( - self, reason, current_mind_info, structured_info, chat_stream, sender_name - ) -> tuple[str, str]: - individuality = Individuality.get_instance() - prompt_personality = individuality.get_prompt(x_person=0, level=2) - # 日程构建 - # schedule_prompt = f'''你现在正在做的事情是:{bot_schedule.get_current_num_task(num = 1,time_info = False)}''' - - if chat_stream.group_info: - chat_in_group = True - else: - chat_in_group = False - - message_list_before_now = get_raw_msg_before_timestamp_with_chat( - chat_id=chat_stream.stream_id, - timestamp=time.time(), - limit=global_config.observation_context_size, - ) - - chat_talking_prompt = await build_readable_messages( - message_list_before_now, - replace_bot_name=True, - merge_messages=False, - timestamp_mode="normal", - read_mark=0.0, - truncate=True, - ) - - # 中文高手(新加的好玩功能) - prompt_ger = "" - if random.random() < 0.04: - prompt_ger += "你喜欢用倒装句" - if random.random() < 0.02: - prompt_ger += "你喜欢用反问句" - - reply_styles1 = [ - ("给出日常且口语化的回复,平淡一些", 0.4), # 40%概率 - ("给出非常简短的回复", 0.4), # 40%概率 - ("给出缺失主语的回复,简短", 0.15), # 15%概率 - ("给出带有语病的回复,朴实平淡", 0.05), # 5%概率 - ] - reply_style1_chosen = random.choices( - [style[0] for style in reply_styles1], weights=[style[1] for style in reply_styles1], k=1 - )[0] - - reply_styles2 = [ - ("不要回复的太有条理,可以有个性", 0.6), # 60%概率 - ("不要回复的太有条理,可以复读", 0.15), # 15%概率 - ("回复的认真一些", 0.2), # 20%概率 - ("可以回复单个表情符号", 0.05), # 5%概率 - ] - reply_style2_chosen = random.choices( - [style[0] for style in reply_styles2], weights=[style[1] for style in reply_styles2], k=1 - )[0] - - if structured_info: - structured_info_prompt = await global_prompt_manager.format_prompt( - "info_from_tools", structured_info=structured_info - ) - else: - structured_info_prompt = "" - - logger.debug("开始构建prompt") - - prompt = await global_prompt_manager.format_prompt( - "heart_flow_prompt", - info_from_tools=structured_info_prompt, - chat_target=await global_prompt_manager.get_prompt_async("chat_target_group1") - if chat_in_group - else await global_prompt_manager.get_prompt_async("chat_target_private1"), - chat_talking_prompt=chat_talking_prompt, - bot_name=global_config.BOT_NICKNAME, - prompt_personality=prompt_personality, - chat_target_2=await global_prompt_manager.get_prompt_async("chat_target_group2") - if chat_in_group - else await global_prompt_manager.get_prompt_async("chat_target_private2"), - current_mind_info=current_mind_info, - reply_style2=reply_style2_chosen, - reply_style1=reply_style1_chosen, - reason=reason, - prompt_ger=prompt_ger, - moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), - sender_name=sender_name, - ) - - logger.debug(f"focus_chat_prompt: \n{prompt}") - - return prompt - async def _build_prompt_normal(self, chat_stream, message_txt: str, sender_name: str = "某人") -> tuple[str, str]: individuality = Individuality.get_instance() prompt_personality = individuality.get_prompt(x_person=2, level=2) diff --git a/src/plugins/knowledge/src/qa_manager.py b/src/plugins/knowledge/src/qa_manager.py index a09879a1..11067d0e 100644 --- a/src/plugins/knowledge/src/qa_manager.py +++ b/src/plugins/knowledge/src/qa_manager.py @@ -27,7 +27,7 @@ class QAManager: self.kg_manager = kg_manager self.llm_client_list = { "embedding": llm_client_embedding, - "filter": llm_client_filter, + "message_filter": llm_client_filter, "qa": llm_client_qa, } diff --git a/src/plugins/respon_info_catcher/info_catcher.py b/src/plugins/respon_info_catcher/info_catcher.py index 5cb67a16..08be4a76 100644 --- a/src/plugins/respon_info_catcher/info_catcher.py +++ b/src/plugins/respon_info_catcher/info_catcher.py @@ -185,32 +185,24 @@ class InfoCatcher: try: # 将消息对象转换为可序列化的字典喵~ - thinking_log_data = { - "chat_id": self.chat_id, - # "response_mode": self.response_mode, # 这个也删掉喵~ - "trigger_text": self.trigger_response_text, - "response_text": self.response_text, - "trigger_info": { + thinking_log_data = {"chat_id": self.chat_id, "trigger_text": self.trigger_response_text, + "response_text": self.response_text, "trigger_info": { "time": self.trigger_response_time, "message": self.message_to_dict(self.trigger_response_message), - }, - "response_info": { + }, "response_info": { "time": self.response_time, "message": self.response_messages, - }, - "timing_results": self.timing_results, - "chat_history": self.message_list_to_dict(self.chat_history), - "chat_history_in_thinking": self.message_list_to_dict(self.chat_history_in_thinking), - "chat_history_after_response": self.message_list_to_dict(self.chat_history_after_response), - } + }, "timing_results": self.timing_results, "chat_history": self.message_list_to_dict(self.chat_history), + "chat_history_in_thinking": self.message_list_to_dict(self.chat_history_in_thinking), + "chat_history_after_response": self.message_list_to_dict( + self.chat_history_after_response), "heartflow_data": self.heartflow_data, + "reasoning_data": self.reasoning_data} # 根据不同的响应模式添加相应的数据喵~ # 现在直接都加上去好了喵~ # if self.response_mode == "heart_flow": # thinking_log_data["mode_specific_data"] = self.heartflow_data # elif self.response_mode == "reasoning": # thinking_log_data["mode_specific_data"] = self.reasoning_data - thinking_log_data["heartflow_data"] = self.heartflow_data - thinking_log_data["reasoning_data"] = self.reasoning_data # 将数据插入到 thinking_log 集合中喵~ db.thinking_log.insert_one(thinking_log_data) diff --git a/src/plugins/schedule/schedule_generator.py b/src/plugins/schedule/schedule_generator.py index 761fcb7d..ee7bdee1 100644 --- a/src/plugins/schedule/schedule_generator.py +++ b/src/plugins/schedule/schedule_generator.py @@ -30,6 +30,7 @@ class ScheduleGenerator: def __init__(self): # 使用离线LLM模型 + self.enable_output = None self.llm_scheduler_all = LLMRequest( model=global_config.llm_reasoning, temperature=global_config.SCHEDULE_TEMPERATURE + 0.3, diff --git a/src/plugins/utils/chat_message_builder.py b/src/plugins/utils/chat_message_builder.py index a7eef443..75fca69c 100644 --- a/src/plugins/utils/chat_message_builder.py +++ b/src/plugins/utils/chat_message_builder.py @@ -123,7 +123,7 @@ def num_new_messages_since(chat_id: str, timestamp_start: float = 0.0, timestamp return 0 # 起始时间大于等于结束时间,没有新消息 filter_query = {"chat_id": chat_id, "time": {"$gt": timestamp_start, "$lt": _timestamp_end}} - return count_messages(filter=filter_query) + return count_messages(message_filter=filter_query) def num_new_messages_since_with_users( @@ -137,7 +137,7 @@ def num_new_messages_since_with_users( "time": {"$gt": timestamp_start, "$lt": timestamp_end}, "user_id": {"$in": person_ids}, } - return count_messages(filter=filter_query) + return count_messages(message_filter=filter_query) async def _build_readable_messages_internal( @@ -227,7 +227,7 @@ async def _build_readable_messages_internal( replace_content = "......(太长了)" truncated_content = content - if limit > 0 and original_len > limit: + if 0 < limit < original_len: truncated_content = f"{content[:limit]}{replace_content}" message_details.append((timestamp, name, truncated_content)) diff --git a/src/plugins/willing/mode_custom.py b/src/plugins/willing/mode_custom.py index c3a5c307..4b2e8f3c 100644 --- a/src/plugins/willing/mode_custom.py +++ b/src/plugins/willing/mode_custom.py @@ -2,5 +2,23 @@ from .willing_manager import BaseWillingManager class CustomWillingManager(BaseWillingManager): + async def async_task_starter(self) -> None: + pass + + async def before_generate_reply_handle(self, message_id: str): + pass + + async def after_generate_reply_handle(self, message_id: str): + pass + + async def not_reply_handle(self, message_id: str): + pass + + async def get_reply_probability(self, message_id: str): + pass + + async def bombing_buffer_message_handle(self, message_id: str): + pass + def __init__(self): super().__init__() From 3d001da30e9f69295b8031aeab9ba056772a3719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E7=8C=AB?= Date: Thu, 1 May 2025 06:07:59 +0900 Subject: [PATCH 13/18] QA: Refactor similarity calculation and improve state management logic --- src/heart_flow/mai_state_manager.py | 5 +- src/heart_flow/sub_mind.py | 4 +- src/heart_flow/subheartflow_manager.py | 70 ++++++++++---------- src/plugins/PFC/pfc.py | 41 ++++++------ src/plugins/chat/bot.py | 2 +- src/plugins/knowledge/src/prompt_template.py | 8 +-- src/plugins/models/utils_model.py | 47 ++++++------- 7 files changed, 91 insertions(+), 86 deletions(-) diff --git a/src/heart_flow/mai_state_manager.py b/src/heart_flow/mai_state_manager.py index 29277820..d289a94a 100644 --- a/src/heart_flow/mai_state_manager.py +++ b/src/heart_flow/mai_state_manager.py @@ -62,6 +62,7 @@ class MaiState(enum.Enum): return MAX_NORMAL_CHAT_NUM_NORMAL elif self == MaiState.FOCUSED_CHAT: return MAX_NORMAL_CHAT_NUM_FOCUSED + return None def get_focused_chat_max_num(self): # 调试用 @@ -76,6 +77,7 @@ class MaiState(enum.Enum): return MAX_FOCUSED_CHAT_NUM_NORMAL elif self == MaiState.FOCUSED_CHAT: return MAX_FOCUSED_CHAT_NUM_FOCUSED + return None class MaiStateInfo: @@ -135,7 +137,8 @@ class MaiStateManager: def __init__(self): pass - def check_and_decide_next_state(self, current_state_info: MaiStateInfo) -> Optional[MaiState]: + @staticmethod + def check_and_decide_next_state(current_state_info: MaiStateInfo) -> Optional[MaiState]: """ 根据当前状态和规则检查是否需要转换状态,并决定下一个状态。 diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index f1716d24..09d7b936 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -78,7 +78,7 @@ def calculate_replacement_probability(similarity: float) -> float: # p = 3.5 * s - 1.4 probability = 3.5 * similarity - 1.4 return max(0.0, probability) - elif 0.6 < similarity < 0.9: + else: # 0.6 < similarity < 0.9 # p = s + 0.1 probability = similarity + 0.1 return min(1.0, max(0.0, probability)) @@ -169,7 +169,7 @@ class SubMind: last_cycle = history_cycle[-1] if history_cycle else None # 上一次决策信息 - if last_cycle != None: + if last_cycle is not None: last_action = last_cycle.action_type last_reasoning = last_cycle.reasoning is_replan = last_cycle.replanned diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index 16f36dcc..30119cca 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -32,6 +32,40 @@ INACTIVE_THRESHOLD_SECONDS = 3600 # 子心流不活跃超时时间(秒) NORMAL_CHAT_TIMEOUT_SECONDS = 30 * 60 # 30分钟 +async def _try_set_subflow_absent_internal(subflow: "SubHeartflow", log_prefix: str) -> bool: + """ + 尝试将给定的子心流对象状态设置为 ABSENT (内部方法,不处理锁)。 + + Args: + subflow: 子心流对象。 + log_prefix: 用于日志记录的前缀 (例如 "[子心流管理]" 或 "[停用]")。 + + Returns: + bool: 如果状态成功变为 ABSENT 或原本就是 ABSENT,返回 True;否则返回 False。 + """ + flow_id = subflow.subheartflow_id + stream_name = chat_manager.get_stream_name(flow_id) or flow_id + + if subflow.chat_state.chat_status != ChatState.ABSENT: + logger.debug(f"{log_prefix} 设置 {stream_name} 状态为 ABSENT") + try: + await subflow.change_chat_state(ChatState.ABSENT) + # 再次检查以确认状态已更改 (change_chat_state 内部应确保) + if subflow.chat_state.chat_status == ChatState.ABSENT: + return True + else: + logger.warning( + f"{log_prefix} 调用 change_chat_state 后,{stream_name} 状态仍为 {subflow.chat_state.chat_status.value}" + ) + return False + except Exception as e: + logger.error(f"{log_prefix} 设置 {stream_name} 状态为 ABSENT 时失败: {e}", exc_info=True) + return False + else: + logger.debug(f"{log_prefix} {stream_name} 已是 ABSENT 状态") + return True # 已经是目标状态,视为成功 + + class SubHeartflowManager: """管理所有活跃的 SubHeartflow 实例。""" @@ -109,38 +143,6 @@ class SubHeartflowManager: return None # --- 新增:内部方法,用于尝试将单个子心流设置为 ABSENT --- - async def _try_set_subflow_absent_internal(self, subflow: "SubHeartflow", log_prefix: str) -> bool: - """ - 尝试将给定的子心流对象状态设置为 ABSENT (内部方法,不处理锁)。 - - Args: - subflow: 子心流对象。 - log_prefix: 用于日志记录的前缀 (例如 "[子心流管理]" 或 "[停用]")。 - - Returns: - bool: 如果状态成功变为 ABSENT 或原本就是 ABSENT,返回 True;否则返回 False。 - """ - flow_id = subflow.subheartflow_id - stream_name = chat_manager.get_stream_name(flow_id) or flow_id - - if subflow.chat_state.chat_status != ChatState.ABSENT: - logger.debug(f"{log_prefix} 设置 {stream_name} 状态为 ABSENT") - try: - await subflow.change_chat_state(ChatState.ABSENT) - # 再次检查以确认状态已更改 (change_chat_state 内部应确保) - if subflow.chat_state.chat_status == ChatState.ABSENT: - return True - else: - logger.warning( - f"{log_prefix} 调用 change_chat_state 后,{stream_name} 状态仍为 {subflow.chat_state.chat_status.value}" - ) - return False - except Exception as e: - logger.error(f"{log_prefix} 设置 {stream_name} 状态为 ABSENT 时失败: {e}", exc_info=True) - return False - else: - logger.debug(f"{log_prefix} {stream_name} 已是 ABSENT 状态") - return True # 已经是目标状态,视为成功 # --- 结束新增 --- @@ -154,7 +156,7 @@ class SubHeartflowManager: logger.info(f"{log_prefix} 正在停止 {stream_name}, 原因: {reason}") # 调用内部方法处理状态变更 - success = await self._try_set_subflow_absent_internal(subheartflow, log_prefix) + success = await _try_set_subflow_absent_internal(subheartflow, log_prefix) return success # 锁在此处自动释放 @@ -241,7 +243,7 @@ class SubHeartflowManager: # 记录原始状态,以便统计实际改变的数量 original_state_was_absent = subflow.chat_state.chat_status == ChatState.ABSENT - success = await self._try_set_subflow_absent_internal(subflow, log_prefix) + success = await _try_set_subflow_absent_internal(subflow, log_prefix) # 如果成功设置为 ABSENT 且原始状态不是 ABSENT,则计数 if success and not original_state_was_absent: diff --git a/src/plugins/PFC/pfc.py b/src/plugins/PFC/pfc.py index e12cb242..6cb1fe83 100644 --- a/src/plugins/PFC/pfc.py +++ b/src/plugins/PFC/pfc.py @@ -15,6 +15,26 @@ if TYPE_CHECKING: logger = get_module_logger("pfc") +def _calculate_similarity(goal1: str, goal2: str) -> float: + """简单计算两个目标之间的相似度 + + 这里使用一个简单的实现,实际可以使用更复杂的文本相似度算法 + + Args: + goal1: 第一个目标 + goal2: 第二个目标 + + Returns: + float: 相似度得分 (0-1) + """ + # 简单实现:检查重叠字数比例 + words1 = set(goal1) + words2 = set(goal2) + overlap = len(words1.intersection(words2)) + total = len(words1.union(words2)) + return overlap / total if total > 0 else 0 + + class GoalAnalyzer: """对话目标分析器""" @@ -166,7 +186,7 @@ class GoalAnalyzer: """ # 检查新目标是否与现有目标相似 for i, (existing_goal, _, _) in enumerate(self.goals): - if self._calculate_similarity(new_goal, existing_goal) > 0.7: # 相似度阈值 + if _calculate_similarity(new_goal, existing_goal) > 0.7: # 相似度阈值 # 更新现有目标 self.goals[i] = (new_goal, method, reasoning) # 将此目标移到列表前面(最主要的位置) @@ -180,25 +200,6 @@ class GoalAnalyzer: if len(self.goals) > self.max_goals: self.goals.pop() # 移除最老的目标 - def _calculate_similarity(self, goal1: str, goal2: str) -> float: - """简单计算两个目标之间的相似度 - - 这里使用一个简单的实现,实际可以使用更复杂的文本相似度算法 - - Args: - goal1: 第一个目标 - goal2: 第二个目标 - - Returns: - float: 相似度得分 (0-1) - """ - # 简单实现:检查重叠字数比例 - words1 = set(goal1) - words2 = set(goal2) - overlap = len(words1.intersection(words2)) - total = len(words1.union(words2)) - return overlap / total if total > 0 else 0 - async def get_all_goals(self) -> List[Tuple[str, str, str]]: """获取所有当前目标 diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 7ca1483c..89e171ba 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -84,7 +84,7 @@ class ChatBot: return # 群聊黑名单拦截 - if groupinfo != None and groupinfo.group_id not in global_config.talk_allowed_groups: + if groupinfo is not None and groupinfo.group_id not in global_config.talk_allowed_groups: logger.trace(f"群{groupinfo.group_id}被禁止回复") return diff --git a/src/plugins/knowledge/src/prompt_template.py b/src/plugins/knowledge/src/prompt_template.py index 18a5002e..14a36008 100644 --- a/src/plugins/knowledge/src/prompt_template.py +++ b/src/plugins/knowledge/src/prompt_template.py @@ -1,5 +1,3 @@ -from typing import List - from .llm_client import LLMMessage entity_extract_system_prompt = """你是一个性能优异的实体提取系统。请从段落中提取出所有实体,并以JSON列表的形式输出。 @@ -13,7 +11,7 @@ entity_extract_system_prompt = """你是一个性能优异的实体提取系统 """ -def build_entity_extract_context(paragraph: str) -> List[LLMMessage]: +def build_entity_extract_context(paragraph: str) -> list[LLMMessage]: messages = [ LLMMessage("system", entity_extract_system_prompt).to_dict(), LLMMessage("user", f"""段落:\n```\n{paragraph}```""").to_dict(), @@ -38,7 +36,7 @@ rdf_triple_extract_system_prompt = """你是一个性能优异的RDF(资源描 """ -def build_rdf_triple_extract_context(paragraph: str, entities: str) -> List[LLMMessage]: +def build_rdf_triple_extract_context(paragraph: str, entities: str) -> list[LLMMessage]: messages = [ LLMMessage("system", rdf_triple_extract_system_prompt).to_dict(), LLMMessage("user", f"""段落:\n```\n{paragraph}```\n\n实体列表:\n```\n{entities}```""").to_dict(), @@ -56,7 +54,7 @@ qa_system_prompt = """ """ -def build_qa_context(question: str, knowledge: list[(str, str, str)]) -> List[LLMMessage]: +def build_qa_context(question: str, knowledge: list[tuple[str, str, str]]) -> list[LLMMessage]: knowledge = "\n".join([f"{i + 1}. 相关性:{k[0]}\n{k[1]}" for i, k in enumerate(knowledge)]) messages = [ LLMMessage("system", qa_system_prompt).to_dict(), diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py index e421641c..6b9c0416 100644 --- a/src/plugins/models/utils_model.py +++ b/src/plugins/models/utils_model.py @@ -65,6 +65,28 @@ error_code_mapping = { } +async def _safely_record(request_content: Dict[str, Any], payload: Dict[str, Any]): + image_base64: str = request_content.get("image_base64") + image_format: str = request_content.get("image_format") + if ( + image_base64 + and payload + and isinstance(payload, dict) + and "messages" in payload + and len(payload["messages"]) > 0 + ): + if isinstance(payload["messages"][0], dict) and "content" in payload["messages"][0]: + content = payload["messages"][0]["content"] + if isinstance(content, list) and len(content) > 1 and "image_url" in content[1]: + payload["messages"][0]["content"][1]["image_url"]["url"] = ( + f"data:image/{image_format.lower() if image_format else 'jpeg'};base64," + f"{image_base64[:10]}...{image_base64[-10:]}" + ) + # if isinstance(content, str) and len(content) > 100: + # payload["messages"][0]["content"] = content[:100] + return payload + + class LLMRequest: # 定义需要转换的模型列表,作为类变量避免重复 MODELS_NEEDING_TRANSFORMATION = [ @@ -551,7 +573,7 @@ class LLMRequest: f"模型 {self.model_name} HTTP响应错误达到最大重试次数: 状态码: {exception.status}, 错误: {exception.message}" ) # 安全地检查和记录请求详情 - handled_payload = await self._safely_record(request_content, payload) + handled_payload = await _safely_record(request_content, payload) logger.critical(f"请求头: {await self._build_headers(no_key=True)} 请求体: {handled_payload}") raise RuntimeError( f"模型 {self.model_name} API请求失败: 状态码 {exception.status}, {exception.message}" @@ -565,31 +587,10 @@ class LLMRequest: else: logger.critical(f"模型 {self.model_name} 请求失败: {str(exception)}") # 安全地检查和记录请求详情 - handled_payload = await self._safely_record(request_content, payload) + handled_payload = await _safely_record(request_content, payload) logger.critical(f"请求头: {await self._build_headers(no_key=True)} 请求体: {handled_payload}") raise RuntimeError(f"模型 {self.model_name} API请求失败: {str(exception)}") - async def _safely_record(self, request_content: Dict[str, Any], payload: Dict[str, Any]): - image_base64: str = request_content.get("image_base64") - image_format: str = request_content.get("image_format") - if ( - image_base64 - and payload - and isinstance(payload, dict) - and "messages" in payload - and len(payload["messages"]) > 0 - ): - if isinstance(payload["messages"][0], dict) and "content" in payload["messages"][0]: - content = payload["messages"][0]["content"] - if isinstance(content, list) and len(content) > 1 and "image_url" in content[1]: - payload["messages"][0]["content"][1]["image_url"]["url"] = ( - f"data:image/{image_format.lower() if image_format else 'jpeg'};base64," - f"{image_base64[:10]}...{image_base64[-10:]}" - ) - # if isinstance(content, str) and len(content) > 100: - # payload["messages"][0]["content"] = content[:100] - return payload - async def _transform_parameters(self, params: dict) -> dict: """ 根据模型名称转换参数: From 263e8d196ad5ecda2312dba284f301da4616e134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E7=8C=AB?= Date: Thu, 1 May 2025 06:55:05 +0900 Subject: [PATCH 14/18] fix: Update type hints to use newer Python syntax MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace Dict, List, Optional with dict, list, < /dev/null | None syntax - Fix abstract method implementation in message.py - Improve type annotations and function return types - Remove unreachable code in get_current_task_tool.py - Refactor HTML elements to use style attributes 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 16 ++-- scripts/import_openie.py | 7 +- src/api/config_api.py | 28 +++---- src/common/logger.py | 6 +- src/common/message_repository.py | 8 +- src/config/config.py | 4 +- src/do_tool/not_used/change_mood.py | 6 +- src/do_tool/not_used/change_relationship.py | 2 +- src/do_tool/not_used/get_current_task.py | 9 ++- src/do_tool/not_used/mid_chat_mem.py | 6 +- src/do_tool/not_used/send_emoji.py | 2 +- src/do_tool/tool_can_use/README.md | 2 +- src/do_tool/tool_can_use/base_tool.py | 12 +-- .../tool_can_use/compare_numbers_tool.py | 7 +- src/do_tool/tool_can_use/get_knowledge.py | 7 +- src/do_tool/tool_can_use/get_memory.py | 1 - src/do_tool/tool_can_use/get_time_date.py | 1 - .../tool_can_use/lpmm_get_knowledge.py | 1 - src/do_tool/tool_use.py | 1 - src/individuality/scene.py | 12 +-- src/plugins/PFC/chat_observer.py | 1 + src/plugins/PFC/pfc_manager.py | 1 + src/plugins/PFC/pfc_utils.py | 1 + src/plugins/PFC/reply_checker.py | 2 + src/plugins/chat/message.py | 80 ++++++++----------- src/plugins/chat/message_sender.py | 16 ++-- src/plugins/chat/utils.py | 22 +++-- src/plugins/memory_system/Hippocampus.py | 4 +- src/plugins/schedule/schedule_generator.py | 3 +- 29 files changed, 125 insertions(+), 143 deletions(-) diff --git a/README.md b/README.md index 58cb82c7..f349e0ca 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 麦麦!MaiCore-MaiMBot (编辑中)
-
+
![Python Version](https://img.shields.io/badge/Python-3.10+-blue) ![License](https://img.shields.io/github/license/SengokuCola/MaiMBot?label=协议) @@ -12,7 +12,7 @@
-

+

Logo @@ -21,8 +21,8 @@ 画师:略nd -

MaiBot(麦麦)

-

+

MaiBot(麦麦)

+

一款专注于 群组聊天 的赛博网友
探索本项目的文档 » @@ -50,7 +50,7 @@ - 🧠 **持久记忆系统**:基于MongoDB的长期记忆存储 - 🔄 **动态人格系统**:自适应的性格特征 -

+
麦麦演示视频
@@ -97,9 +97,9 @@ - [四群](https://qm.qq.com/q/wlH5eT8OmQ) 729957033【已满】 -
-

📚 文档

-
+ +## 📚 文档 + ### (部分内容可能过时,请注意版本对应) diff --git a/scripts/import_openie.py b/scripts/import_openie.py index 26cbd8ce..595f22ec 100644 --- a/scripts/import_openie.py +++ b/scripts/import_openie.py @@ -8,7 +8,6 @@ import sys import os sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) -from typing import Dict, List from src.plugins.knowledge.src.lpmmconfig import PG_NAMESPACE, global_config from src.plugins.knowledge.src.embedding_store import EmbeddingManager @@ -26,8 +25,8 @@ logger = get_module_logger("LPMM知识库-OpenIE导入") def hash_deduplicate( - raw_paragraphs: Dict[str, str], - triple_list_data: Dict[str, List[List[str]]], + raw_paragraphs: dict[str, str], + triple_list_data: dict[str, list[list[str]]], stored_pg_hashes: set, stored_paragraph_hashes: set, ): @@ -126,7 +125,7 @@ def main(): ) # 初始化Embedding库 - embed_manager = embed_manager = EmbeddingManager(llm_client_list[global_config["embedding"]["provider"]]) + embed_manager = EmbeddingManager(llm_client_list[global_config["embedding"]["provider"]]) logger.info("正在从文件加载Embedding库") try: embed_manager.load_from_file() diff --git a/src/api/config_api.py b/src/api/config_api.py index 025888d8..6ecd4e6d 100644 --- a/src/api/config_api.py +++ b/src/api/config_api.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional +from typing import List, Optional import strawberry # from packaging.version import Version, InvalidVersion @@ -128,22 +128,22 @@ class BotConfig: enable_pfc_chatting: bool # 是否启用PFC聊天 # 模型配置 - llm_reasoning: Dict[str, str] # LLM推理 - # llm_reasoning_minor: Dict[str, str] - llm_normal: Dict[str, str] # LLM普通 - llm_topic_judge: Dict[str, str] # LLM话题判断 - llm_summary: Dict[str, str] # LLM话题总结 - llm_emotion_judge: Dict[str, str] # LLM情感判断 - embedding: Dict[str, str] # 嵌入 - vlm: Dict[str, str] # VLM - moderation: Dict[str, str] # 审核 + llm_reasoning: dict[str, str] # LLM推理 + # llm_reasoning_minor: dict[str, str] + llm_normal: dict[str, str] # LLM普通 + llm_topic_judge: dict[str, str] # LLM话题判断 + llm_summary: dict[str, str] # LLM话题总结 + llm_emotion_judge: dict[str, str] # LLM情感判断 + embedding: dict[str, str] # 嵌入 + vlm: dict[str, str] # VLM + moderation: dict[str, str] # 审核 # 实验性 - llm_observation: Dict[str, str] # LLM观察 - llm_sub_heartflow: Dict[str, str] # LLM子心流 - llm_heartflow: Dict[str, str] # LLM心流 + llm_observation: dict[str, str] # LLM观察 + llm_sub_heartflow: dict[str, str] # LLM子心流 + llm_heartflow: dict[str, str] # LLM心流 - api_urls: Dict[str, str] # API URLs + api_urls: dict[str, str] # API URLs @strawberry.type diff --git a/src/common/logger.py b/src/common/logger.py index a0d621d9..a82c6d88 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -1,5 +1,5 @@ from loguru import logger -from typing import Dict, Optional, Union, List, Tuple +from typing import Optional, Union, List, Tuple import sys import os from types import ModuleType @@ -75,8 +75,8 @@ if default_handler_id is not None: LoguruLogger = logger.__class__ # 全局注册表:记录模块与处理器ID的映射 -_handler_registry: Dict[str, List[int]] = {} -_custom_style_handlers: Dict[Tuple[str, str], List[int]] = {} # 记录自定义样式处理器ID +_handler_registry: dict[str, List[int]] = {} +_custom_style_handlers: dict[Tuple[str, str], List[int]] = {} # 记录自定义样式处理器ID # 获取日志存储根地址 current_file_path = Path(__file__).resolve() diff --git a/src/common/message_repository.py b/src/common/message_repository.py index 72643f91..11bd6095 100644 --- a/src/common/message_repository.py +++ b/src/common/message_repository.py @@ -7,11 +7,11 @@ logger = get_module_logger(__name__) def find_messages( - message_filter: Dict[str, Any], + message_filter: dict[str, Any], sort: Optional[List[tuple[str, int]]] = None, limit: int = 0, limit_mode: str = "latest", -) -> List[Dict[str, Any]]: +) -> List[dict[str, Any]]: """ 根据提供的过滤器、排序和限制条件查找消息。 @@ -26,7 +26,7 @@ def find_messages( """ try: query = db.messages.find(message_filter) - results: List[Dict[str, Any]] = [] + results: List[dict[str, Any]] = [] if limit > 0: if limit_mode == "earliest": @@ -56,7 +56,7 @@ def find_messages( return [] -def count_messages(message_filter: Dict[str, Any]) -> int: +def count_messages(message_filter: dict[str, Any]) -> int: """ 根据提供的过滤器计算消息数量。 diff --git a/src/config/config.py b/src/config/config.py index fbf558a3..033d57f5 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -271,8 +271,8 @@ class BotConfig: enable_pfc_chatting: bool = False # 是否启用PFC聊天 # 模型配置 - llm_reasoning: Dict[str, str] = field(default_factory=lambda: {}) - # llm_reasoning_minor: Dict[str, str] = field(default_factory=lambda: {}) + llm_reasoning: dict[str, str] = field(default_factory=lambda: {}) + # llm_reasoning_minor: dict[str, str] = field(default_factory=lambda: {}) llm_normal: Dict[str, str] = field(default_factory=lambda: {}) llm_topic_judge: Dict[str, str] = field(default_factory=lambda: {}) llm_summary: Dict[str, str] = field(default_factory=lambda: {}) diff --git a/src/do_tool/not_used/change_mood.py b/src/do_tool/not_used/change_mood.py index 430561a2..5dee6ac9 100644 --- a/src/do_tool/not_used/change_mood.py +++ b/src/do_tool/not_used/change_mood.py @@ -3,7 +3,7 @@ from src.config.config import global_config from src.common.logger_manager import get_logger from src.plugins.moods.moods import MoodManager -from typing import Dict, Any +from typing import Any logger = get_logger("change_mood_tool") @@ -22,7 +22,7 @@ class ChangeMoodTool(BaseTool): "required": ["text", "response_set"], } - async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]: + async def execute(self, function_args: dict[str, Any], message_txt: str = "") -> dict[str, Any]: """执行心情改变 Args: @@ -30,7 +30,7 @@ class ChangeMoodTool(BaseTool): message_txt: 原始消息文本 Returns: - Dict: 工具执行结果 + dict: 工具执行结果 """ try: response_set = function_args.get("response_set") diff --git a/src/do_tool/not_used/change_relationship.py b/src/do_tool/not_used/change_relationship.py index 4af32fb8..ab91489f 100644 --- a/src/do_tool/not_used/change_relationship.py +++ b/src/do_tool/not_used/change_relationship.py @@ -19,7 +19,7 @@ class RelationshipTool(BaseTool): "required": ["text", "changed_value", "reason"], } - async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> dict: + async def execute(self, function_args: dict[str, Any], message_txt: str = "") -> dict: """执行工具功能 Args: diff --git a/src/do_tool/not_used/get_current_task.py b/src/do_tool/not_used/get_current_task.py index d5660f6a..30184d67 100644 --- a/src/do_tool/not_used/get_current_task.py +++ b/src/do_tool/not_used/get_current_task.py @@ -1,7 +1,7 @@ from src.do_tool.tool_can_use.base_tool import BaseTool from src.plugins.schedule.schedule_generator import bot_schedule from src.common.logger import get_module_logger -from typing import Dict, Any +from typing import Any from datetime import datetime logger = get_module_logger("get_current_task_tool") @@ -21,7 +21,7 @@ class GetCurrentTaskTool(BaseTool): "required": ["start_time", "end_time"], } - async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]: + async def execute(self, function_args: dict[str, Any], message_txt: str = "") -> dict[str, Any]: """执行获取当前任务或指定时间段的日程信息 Args: @@ -29,7 +29,7 @@ class GetCurrentTaskTool(BaseTool): message_txt: 原始消息文本,此工具不使用 Returns: - Dict: 工具执行结果 + dict: 工具执行结果 """ start_time = function_args.get("start_time") end_time = function_args.get("end_time") @@ -55,5 +55,6 @@ class GetCurrentTaskTool(BaseTool): task_info = "\n".join(task_list) else: task_info = f"在 {start_time} 到 {end_time} 之间没有找到日程信息" - + else: + task_info = "请提供有效的开始时间和结束时间" return {"name": "get_current_task", "content": f"日程信息: {task_info}"} diff --git a/src/do_tool/not_used/mid_chat_mem.py b/src/do_tool/not_used/mid_chat_mem.py index 71726a57..0340df13 100644 --- a/src/do_tool/not_used/mid_chat_mem.py +++ b/src/do_tool/not_used/mid_chat_mem.py @@ -1,6 +1,6 @@ from src.do_tool.tool_can_use.base_tool import BaseTool from src.common.logger import get_module_logger -from typing import Dict, Any +from typing import Any logger = get_module_logger("get_mid_memory_tool") @@ -18,7 +18,7 @@ class GetMidMemoryTool(BaseTool): "required": ["id"], } - async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]: + async def execute(self, function_args: dict[str, Any], message_txt: str = "") -> dict[str, Any]: """执行记忆获取 Args: @@ -26,7 +26,7 @@ class GetMidMemoryTool(BaseTool): message_txt: 原始消息文本 Returns: - Dict: 工具执行结果 + dict: 工具执行结果 """ try: id = function_args.get("id") diff --git a/src/do_tool/not_used/send_emoji.py b/src/do_tool/not_used/send_emoji.py index 3c6c8a3f..9bf2dd48 100644 --- a/src/do_tool/not_used/send_emoji.py +++ b/src/do_tool/not_used/send_emoji.py @@ -17,7 +17,7 @@ class SendEmojiTool(BaseTool): "required": ["text"], } - async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]: + async def execute(self, function_args: dict[str, Any], message_txt: str = "") -> dict[str, Any]: text = function_args.get("text", message_txt) return { "name": "send_emoji", diff --git a/src/do_tool/tool_can_use/README.md b/src/do_tool/tool_can_use/README.md index 15c77188..0b746b4e 100644 --- a/src/do_tool/tool_can_use/README.md +++ b/src/do_tool/tool_can_use/README.md @@ -42,7 +42,7 @@ class MyNewTool(BaseTool): message_txt: 原始消息文本 Returns: - Dict: 包含执行结果的字典,必须包含name和content字段 + dict: 包含执行结果的字典,必须包含name和content字段 """ # 实现工具逻辑 result = f"工具执行结果: {function_args.get('param1')}" diff --git a/src/do_tool/tool_can_use/base_tool.py b/src/do_tool/tool_can_use/base_tool.py index 1dd15baf..b01e543e 100644 --- a/src/do_tool/tool_can_use/base_tool.py +++ b/src/do_tool/tool_can_use/base_tool.py @@ -22,11 +22,11 @@ class BaseTool: parameters = None @classmethod - def get_tool_definition(cls) -> Dict[str, Any]: + def get_tool_definition(cls) -> dict[str, Any]: """获取工具定义,用于LLM工具调用 Returns: - Dict: 工具定义字典 + dict: 工具定义字典 """ if not cls.name or not cls.description or not cls.parameters: raise NotImplementedError(f"工具类 {cls.__name__} 必须定义 name, description 和 parameters 属性") @@ -36,14 +36,14 @@ class BaseTool: "function": {"name": cls.name, "description": cls.description, "parameters": cls.parameters}, } - async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]: + async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]: """执行工具函数 Args: function_args: 工具调用参数 Returns: - Dict: 工具执行结果 + dict: 工具执行结果 """ raise NotImplementedError("子类必须实现execute方法") @@ -88,11 +88,11 @@ def discover_tools(): logger.info(f"工具发现完成,共注册 {len(TOOL_REGISTRY)} 个工具") -def get_all_tool_definitions() -> List[Dict[str, Any]]: +def get_all_tool_definitions() -> List[dict[str, Any]]: """获取所有已注册工具的定义 Returns: - List[Dict]: 工具定义列表 + List[dict]: 工具定义列表 """ return [tool_class().get_tool_definition() for tool_class in TOOL_REGISTRY.values()] diff --git a/src/do_tool/tool_can_use/compare_numbers_tool.py b/src/do_tool/tool_can_use/compare_numbers_tool.py index 1fbd812a..ef037de9 100644 --- a/src/do_tool/tool_can_use/compare_numbers_tool.py +++ b/src/do_tool/tool_can_use/compare_numbers_tool.py @@ -1,6 +1,6 @@ from src.do_tool.tool_can_use.base_tool import BaseTool from src.common.logger import get_module_logger -from typing import Dict, Any +from typing import Any logger = get_module_logger("compare_numbers_tool") @@ -19,15 +19,14 @@ class CompareNumbersTool(BaseTool): "required": ["num1", "num2"], } - async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]: + async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]: """执行比较两个数的大小 Args: function_args: 工具参数 - message_txt: 原始消息文本 Returns: - Dict: 工具执行结果 + dict: 工具执行结果 """ try: num1 = function_args.get("num1") diff --git a/src/do_tool/tool_can_use/get_knowledge.py b/src/do_tool/tool_can_use/get_knowledge.py index bd4ce86b..20a92264 100644 --- a/src/do_tool/tool_can_use/get_knowledge.py +++ b/src/do_tool/tool_can_use/get_knowledge.py @@ -2,7 +2,7 @@ from src.do_tool.tool_can_use.base_tool import BaseTool from src.plugins.chat.utils import get_embedding from src.common.database import db from src.common.logger_manager import get_logger -from typing import Dict, Any, Union +from typing import Any, Union logger = get_logger("get_knowledge_tool") @@ -21,15 +21,14 @@ class SearchKnowledgeTool(BaseTool): "required": ["query"], } - async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]: + async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]: """执行知识库搜索 Args: function_args: 工具参数 - message_txt: 原始消息文本 Returns: - Dict: 工具执行结果 + dict: 工具执行结果 """ try: query = function_args.get("query") diff --git a/src/do_tool/tool_can_use/get_memory.py b/src/do_tool/tool_can_use/get_memory.py index b38423ed..c1bcc927 100644 --- a/src/do_tool/tool_can_use/get_memory.py +++ b/src/do_tool/tool_can_use/get_memory.py @@ -25,7 +25,6 @@ class GetMemoryTool(BaseTool): Args: function_args: 工具参数 - message_txt: 原始消息文本 Returns: Dict: 工具执行结果 diff --git a/src/do_tool/tool_can_use/get_time_date.py b/src/do_tool/tool_can_use/get_time_date.py index 6104026e..bd3b1a9c 100644 --- a/src/do_tool/tool_can_use/get_time_date.py +++ b/src/do_tool/tool_can_use/get_time_date.py @@ -22,7 +22,6 @@ class GetCurrentDateTimeTool(BaseTool): Args: function_args: 工具参数(此工具不使用) - message_txt: 原始消息文本(此工具不使用) Returns: Dict: 工具执行结果 diff --git a/src/do_tool/tool_can_use/lpmm_get_knowledge.py b/src/do_tool/tool_can_use/lpmm_get_knowledge.py index 4dba1bc7..8754e603 100644 --- a/src/do_tool/tool_can_use/lpmm_get_knowledge.py +++ b/src/do_tool/tool_can_use/lpmm_get_knowledge.py @@ -29,7 +29,6 @@ class SearchKnowledgeFromLPMMTool(BaseTool): Args: function_args: 工具参数 - message_txt: 原始消息文本 Returns: Dict: 工具执行结果 diff --git a/src/do_tool/tool_use.py b/src/do_tool/tool_use.py index 88289fe0..b2f59cc8 100644 --- a/src/do_tool/tool_use.py +++ b/src/do_tool/tool_use.py @@ -106,7 +106,6 @@ class ToolUser: Args: message_txt: 用户消息文本 - sender_name: 发送者名称 chat_stream: 聊天流对象 observation: 观察对象(可选) diff --git a/src/individuality/scene.py b/src/individuality/scene.py index 76304dbb..4edd5dc2 100644 --- a/src/individuality/scene.py +++ b/src/individuality/scene.py @@ -1,9 +1,9 @@ import json -from typing import Dict import os +from typing import Any -def load_scenes() -> Dict: +def load_scenes() -> dict[str, Any]: """ 从JSON文件加载场景数据 @@ -20,7 +20,7 @@ def load_scenes() -> Dict: PERSONALITY_SCENES = load_scenes() -def get_scene_by_factor(factor: str) -> Dict: +def get_scene_by_factor(factor: str) -> dict | None: """ 根据人格因子获取对应的情景测试 @@ -28,12 +28,12 @@ def get_scene_by_factor(factor: str) -> Dict: factor (str): 人格因子名称 Returns: - Dict: 包含情景描述的字典 + dict: 包含情景描述的字典 """ - return PERSONALITY_SCENES.get(factor, None) + return PERSONALITY_SCENES.get(factor,None) -def get_all_scenes() -> Dict: +def get_all_scenes() -> dict: """ 获取所有情景测试 diff --git a/src/plugins/PFC/chat_observer.py b/src/plugins/PFC/chat_observer.py index 2822e111..e99ed800 100644 --- a/src/plugins/PFC/chat_observer.py +++ b/src/plugins/PFC/chat_observer.py @@ -23,6 +23,7 @@ class ChatObserver: Args: stream_id: 聊天流ID + private_name: 私聊名称 Returns: ChatObserver: 观察器实例 diff --git a/src/plugins/PFC/pfc_manager.py b/src/plugins/PFC/pfc_manager.py index 621686a9..7837606c 100644 --- a/src/plugins/PFC/pfc_manager.py +++ b/src/plugins/PFC/pfc_manager.py @@ -33,6 +33,7 @@ class PFCManager: Args: stream_id: 聊天流ID + private_name: 私聊名称 Returns: Optional[Conversation]: 对话实例,创建失败则返回None diff --git a/src/plugins/PFC/pfc_utils.py b/src/plugins/PFC/pfc_utils.py index 5e35d47b..2f7bd5e0 100644 --- a/src/plugins/PFC/pfc_utils.py +++ b/src/plugins/PFC/pfc_utils.py @@ -18,6 +18,7 @@ def get_items_from_json( Args: content: 包含JSON的文本 + private_name: 私聊名称 *items: 要提取的字段名 default_values: 字段的默认值,格式为 {字段名: 默认值} required_types: 字段的必需类型,格式为 {字段名: 类型} diff --git a/src/plugins/PFC/reply_checker.py b/src/plugins/PFC/reply_checker.py index 18088895..35e9af50 100644 --- a/src/plugins/PFC/reply_checker.py +++ b/src/plugins/PFC/reply_checker.py @@ -29,6 +29,8 @@ class ReplyChecker: Args: reply: 生成的回复 goal: 对话目标 + chat_history: 对话历史记录 + chat_history_text: 对话历史记录文本 retry_count: 当前重试次数 Returns: diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py index 525d30c9..2de88561 100644 --- a/src/plugins/chat/message.py +++ b/src/plugins/chat/message.py @@ -1,6 +1,7 @@ import time +from abc import abstractmethod from dataclasses import dataclass -from typing import Dict, List, Optional, Union +from typing import Optional, Any import urllib3 @@ -58,12 +59,37 @@ class Message(MessageBase): # 回复消息 self.reply = reply + async def _process_message_segments(self, segment: Seg) -> str: + """递归处理消息段,转换为文字描述 + + Args: + segment: 要处理的消息段 + + Returns: + str: 处理后的文本 + """ + if segment.type == "seglist": + # 处理消息段列表 + segments_text = [] + for seg in segment.data: + processed = await self._process_message_segments(seg) + if processed: + segments_text.append(processed) + return " ".join(segments_text) + else: + # 处理单个消息段 + return await self._process_single_segment(segment) + + @abstractmethod + async def _process_single_segment(self, segment): + pass + @dataclass class MessageRecv(Message): """接收消息类,用于处理从MessageCQ序列化的消息""" - def __init__(self, message_dict: Dict): + def __init__(self, message_dict: dict[str, Any]): """从MessageCQ的字典初始化 Args: @@ -90,26 +116,7 @@ class MessageRecv(Message): self.processed_plain_text = await self._process_message_segments(self.message_segment) self.detailed_plain_text = self._generate_detailed_text() - async def _process_message_segments(self, segment: Seg) -> str: - """递归处理消息段,转换为文字描述 - Args: - segment: 要处理的消息段 - - Returns: - str: 处理后的文本 - """ - if segment.type == "seglist": - # 处理消息段列表 - segments_text = [] - for seg in segment.data: - processed = await self._process_message_segments(seg) - if processed: - segments_text.append(processed) - return " ".join(segments_text) - else: - # 处理单个消息段 - return await self._process_single_segment(segment) async def _process_single_segment(self, seg: Seg) -> str: """处理单个消息段 @@ -179,28 +186,7 @@ class MessageProcessBase(Message): self.thinking_time = round(time.time() - self.thinking_start_time, 2) return self.thinking_time - async def _process_message_segments(self, segment: Seg) -> str: - """递归处理消息段,转换为文字描述 - - Args: - segment: 要处理的消息段 - - Returns: - str: 处理后的文本 - """ - if segment.type == "seglist": - # 处理消息段列表 - segments_text = [] - for seg in segment.data: - processed = await self._process_message_segments(seg) - if processed: - segments_text.append(processed) - return " ".join(segments_text) - else: - # 处理单个消息段 - return await self._process_single_segment(segment) - - async def _process_single_segment(self, seg: Seg) -> Union[str, None]: + async def _process_single_segment(self, seg: Seg) -> str | None: """处理单个消息段 Args: @@ -278,7 +264,7 @@ class MessageSending(MessageProcessBase): message_id: str, chat_stream: ChatStream, bot_user_info: UserInfo, - sender_info: UserInfo, # 用来记录发送者信息,用于私聊回复 + sender_info: UserInfo | None, # 用来记录发送者信息,用于私聊回复 message_segment: Seg, reply: Optional["MessageRecv"] = None, is_head: bool = False, @@ -303,7 +289,7 @@ class MessageSending(MessageProcessBase): self.is_emoji = is_emoji self.apply_set_reply_logic = apply_set_reply_logic - def set_reply(self, reply: Optional["MessageRecv"] = None) -> None: + def set_reply(self, reply: Optional["MessageRecv"] = None): """设置回复消息""" if self.message_info.format_info is not None and "reply" in self.message_info.format_info.accept_format: if reply: @@ -317,7 +303,6 @@ class MessageSending(MessageProcessBase): self.message_segment, ], ) - return self async def process(self) -> None: """处理消息内容,生成纯文本和详细文本""" @@ -342,6 +327,7 @@ class MessageSending(MessageProcessBase): reply=thinking.reply, is_head=is_head, is_emoji=is_emoji, + sender_info=None, ) def to_dict(self): @@ -361,7 +347,7 @@ class MessageSet: def __init__(self, chat_stream: ChatStream, message_id: str): self.chat_stream = chat_stream self.message_id = message_id - self.messages: List[MessageSending] = [] + self.messages: list[MessageSending] = [] self.time = round(time.time(), 3) # 保留3位小数 def add_message(self, message: MessageSending) -> None: diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index 30d943d9..ee4865d9 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -1,7 +1,7 @@ # src/plugins/chat/message_sender.py import asyncio import time -from typing import Dict, List, Optional, Union +from typing import Union # from ...common.database import db # 数据库依赖似乎不需要了,注释掉 from ..message.api import global_api @@ -70,7 +70,7 @@ class MessageContainer: def __init__(self, chat_id: str, max_size: int = 100): self.chat_id = chat_id self.max_size = max_size - self.messages: List[Union[MessageThinking, MessageSending]] = [] # 明确类型 + self.messages: list[MessageThinking | MessageSending] = [] # 明确类型 self.last_send_time = 0 self.thinking_wait_timeout = 20 # 思考等待超时时间(秒) - 从旧 sender 合并 @@ -78,7 +78,7 @@ class MessageContainer: """计算当前容器中思考消息的数量""" return sum(1 for msg in self.messages if isinstance(msg, MessageThinking)) - def get_timeout_sending_messages(self) -> List[MessageSending]: + def get_timeout_sending_messages(self) -> list[MessageSending]: """获取所有超时的MessageSending对象(思考时间超过20秒),按thinking_start_time排序 - 从旧 sender 合并""" current_time = time.time() timeout_messages = [] @@ -94,7 +94,7 @@ class MessageContainer: timeout_messages.sort(key=lambda x: x.thinking_start_time) return timeout_messages - def get_earliest_message(self) -> Optional[Union[MessageThinking, MessageSending]]: + def get_earliest_message(self): """获取thinking_start_time最早的消息对象""" if not self.messages: return None @@ -108,7 +108,7 @@ class MessageContainer: earliest_message = msg return earliest_message - def add_message(self, message: Union[MessageThinking, MessageSending, MessageSet]) -> None: + def add_message(self, message: Union[MessageThinking, MessageSending, MessageSet]): """添加消息到队列""" if isinstance(message, MessageSet): for single_message in message.messages: @@ -116,7 +116,7 @@ class MessageContainer: else: self.messages.append(message) - def remove_message(self, message_to_remove: Union[MessageThinking, MessageSending]) -> bool: + def remove_message(self, message_to_remove: Union[MessageThinking, MessageSending]): """移除指定的消息对象,如果消息存在则返回True,否则返回False""" try: _initial_len = len(self.messages) @@ -138,7 +138,7 @@ class MessageContainer: """检查是否有待发送的消息""" return bool(self.messages) - def get_all_messages(self) -> List[Union[MessageSending, MessageThinking]]: + def get_all_messages(self) -> list[MessageThinking | MessageSending]: """获取所有消息""" return list(self.messages) # 返回副本 @@ -148,7 +148,7 @@ class MessageManager: def __init__(self): self._processor_task = None - self.containers: Dict[str, MessageContainer] = {} + self.containers: dict[str, MessageContainer] = {} self.storage = MessageStorage() # 添加 storage 实例 self._running = True # 处理器运行状态 self._container_lock = asyncio.Lock() # 保护 containers 字典的锁 diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index 16581f3a..40b4ab7c 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -2,7 +2,6 @@ import random import time import re from collections import Counter -from typing import Dict, List, Optional import jieba import numpy as np @@ -26,7 +25,7 @@ def is_english_letter(char: str) -> bool: return "a" <= char.lower() <= "z" -def db_message_to_str(message_dict: Dict) -> str: +def db_message_to_str(message_dict: dict) -> str: logger.debug(f"message_dict: {message_dict}") time_str = time.strftime("%m-%d %H:%M:%S", time.localtime(message_dict["time"])) try: @@ -35,7 +34,7 @@ def db_message_to_str(message_dict: Dict) -> str: message_dict.get("user_nickname", ""), message_dict.get("user_cardname", ""), ) - except Exception: + except Exception as e: name = message_dict.get("user_nickname", "") or f"用户{message_dict['user_id']}" content = message_dict.get("processed_plain_text", "") result = f"[{time_str}] {name}: {content}\n" @@ -77,13 +76,13 @@ def is_mentioned_bot_in_message(message: MessageRecv) -> tuple[bool, float]: if not is_mentioned: # 判断是否被回复 if re.match( - f"\[回复 [\s\S]*?\({str(global_config.BOT_QQ)}\):[\s\S]*?\],说:", message.processed_plain_text + f"\[回复 [\s\S]*?\({str(global_config.BOT_QQ)}\):[\s\S]*?],说:" , message.processed_plain_text ): is_mentioned = True else: # 判断内容中是否被提及 message_content = re.sub(r"@[\s\S]*?((\d+))", "", message.processed_plain_text) - message_content = re.sub(r"\[回复 [\s\S]*?\(((\d+)|未知id)\):[\s\S]*?\],说:", "", message_content) + message_content = re.sub(r"\[回复 [\s\S]*?\(((\d+)|未知id)\):[\s\S]*?],说:", "", message_content) for keyword in keywords: if keyword in message_content: is_mentioned = True @@ -223,7 +222,7 @@ def get_recent_group_speaker(chat_stream_id: int, sender, limit: int = 12) -> li return who_chat_in_group -def split_into_sentences_w_remove_punctuation(text: str) -> List[str]: +def split_into_sentences_w_remove_punctuation(text: str) -> list[str]: """将文本分割成句子,并根据概率合并 1. 识别分割点(, , 。 ; 空格),但如果分割点左右都是英文字母则不分割。 2. 将文本分割成 (内容, 分隔符) 的元组。 @@ -370,7 +369,7 @@ def random_remove_punctuation(text: str) -> str: return result -def process_llm_response(text: str) -> List[str]: +def process_llm_response(text: str) -> list[str]: # 先保护颜文字 if global_config.enable_kaomoji_protection: protected_text, kaomoji_mapping = protect_kaomoji(text) @@ -379,7 +378,7 @@ def process_llm_response(text: str) -> List[str]: protected_text = text kaomoji_mapping = {} # 提取被 () 或 [] 包裹且包含中文的内容 - pattern = re.compile(r"[\(\[\(](?=.*[\u4e00-\u9fff]).*?[\)\]\)]") + pattern = re.compile(r"[(\[(](?=.*[一-鿿]).*?[)\])]") # _extracted_contents = pattern.findall(text) _extracted_contents = pattern.findall(protected_text) # 在保护后的文本上查找 # 去除 () 和 [] 及其包裹的内容 @@ -554,7 +553,7 @@ def protect_kaomoji(sentence): r"[^()\[\]()【】]*?" # 非括号字符(惰性匹配) r"[^一-龥a-zA-Z0-9\s]" # 非中文、非英文、非数字、非空格字符(必须包含至少一个) r"[^()\[\]()【】]*?" # 非括号字符(惰性匹配) - r"[\)\])】" # 右括号 + r"[)\])】" # 右括号 r"]" r")" r"|" @@ -704,7 +703,7 @@ def count_messages_between(start_time: float, end_time: float, stream_id: str) - return 0, 0 -def translate_timestamp_to_human_readable(timestamp: float, mode: str = "normal") -> Optional[str]: +def translate_timestamp_to_human_readable(timestamp: float, mode: str = "normal") -> str: """将时间戳转换为人类可读的时间格式 Args: @@ -732,10 +731,9 @@ def translate_timestamp_to_human_readable(timestamp: float, mode: str = "normal" return f"{int(diff / 86400)}天前:\n" else: return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp)) + ":\n" - elif mode == "lite": + else: # mode = "lite" or unknown # 只返回时分秒格式,喵~ return time.strftime("%H:%M:%S", time.localtime(timestamp)) - return None def parse_text_timestamps(text: str, mode: str = "normal") -> str: diff --git a/src/plugins/memory_system/Hippocampus.py b/src/plugins/memory_system/Hippocampus.py index 7a5fc1a8..ff34541e 100644 --- a/src/plugins/memory_system/Hippocampus.py +++ b/src/plugins/memory_system/Hippocampus.py @@ -511,7 +511,7 @@ class Hippocampus: """从文本中提取关键词并获取相关记忆。 Args: - topic (str): 记忆主题 + keywords (list): 输入文本 max_memory_num (int, optional): 返回的记忆条目数量上限。默认为3,表示最多返回3条与输入文本相关度最高的记忆。 max_memory_length (int, optional): 每个主题最多返回的记忆条目数量。默认为2,表示每个主题最多返回2条相似度最高的记忆。 max_depth (int, optional): 记忆检索深度。默认为3。值越大,检索范围越广,可以获取更多间接相关的记忆,但速度会变慢。 @@ -829,7 +829,7 @@ class EntorhinalCortex: return chat_samples @staticmethod - def random_get_msg_snippet(target_timestamp: float, chat_size: int, max_memorized_time_per_msg: int) -> list: + def random_get_msg_snippet(target_timestamp: float, chat_size: int, max_memorized_time_per_msg: int) -> list | None: """从数据库中随机获取指定时间戳附近的消息片段 (使用 chat_message_builder)""" try_count = 0 time_window_seconds = random.randint(300, 1800) # 随机时间窗口,5到30分钟 diff --git a/src/plugins/schedule/schedule_generator.py b/src/plugins/schedule/schedule_generator.py index ee7bdee1..6bd2e587 100644 --- a/src/plugins/schedule/schedule_generator.py +++ b/src/plugins/schedule/schedule_generator.py @@ -1,7 +1,6 @@ import datetime import os import sys -from typing import Dict import asyncio from dateutil import tz @@ -162,7 +161,7 @@ class ScheduleGenerator: async def generate_daily_schedule( self, target_date: datetime.datetime = None, - ) -> Dict[str, str]: + ) -> dict[str, str]: daytime_prompt = self.construct_daytime_prompt(target_date) daytime_response, _ = await self.llm_scheduler_all.generate_response_async(daytime_prompt) return daytime_response From e4959f0386b851f6b40e489672143ed6f053d9cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E7=8C=AB?= Date: Thu, 1 May 2025 07:07:13 +0900 Subject: [PATCH 15/18] ruff fix --- src/common/message_repository.py | 2 +- src/do_tool/not_used/change_relationship.py | 2 +- src/do_tool/not_used/send_emoji.py | 2 +- src/do_tool/tool_can_use/base_tool.py | 2 +- src/heart_flow/background_tasks.py | 10 +++----- src/heart_flow/sub_mind.py | 2 +- src/individuality/scene.py | 2 +- src/plugins/chat/message.py | 2 -- src/plugins/chat/message_sender.py | 3 +-- src/plugins/chat/utils.py | 6 ++--- src/plugins/heartFC_chat/heartFC_sender.py | 1 - .../heartFC_chat/heartflow_prompt_builder.py | 4 +--- .../respon_info_catcher/info_catcher.py | 23 ++++++++++++------- 13 files changed, 29 insertions(+), 32 deletions(-) diff --git a/src/common/message_repository.py b/src/common/message_repository.py index 11bd6095..9f246077 100644 --- a/src/common/message_repository.py +++ b/src/common/message_repository.py @@ -1,7 +1,7 @@ from src.common.database import db from src.common.logger import get_module_logger import traceback -from typing import List, Dict, Any, Optional +from typing import List, Any, Optional logger = get_module_logger(__name__) diff --git a/src/do_tool/not_used/change_relationship.py b/src/do_tool/not_used/change_relationship.py index ab91489f..96f512e5 100644 --- a/src/do_tool/not_used/change_relationship.py +++ b/src/do_tool/not_used/change_relationship.py @@ -1,4 +1,4 @@ -from typing import Dict, Any +from typing import Any from src.common.logger_manager import get_logger from src.do_tool.tool_can_use.base_tool import BaseTool diff --git a/src/do_tool/not_used/send_emoji.py b/src/do_tool/not_used/send_emoji.py index 9bf2dd48..d2d00a92 100644 --- a/src/do_tool/not_used/send_emoji.py +++ b/src/do_tool/not_used/send_emoji.py @@ -1,7 +1,7 @@ from src.do_tool.tool_can_use.base_tool import BaseTool from src.common.logger import get_module_logger -from typing import Dict, Any +from typing import Any logger = get_module_logger("send_emoji_tool") diff --git a/src/do_tool/tool_can_use/base_tool.py b/src/do_tool/tool_can_use/base_tool.py index b01e543e..42570884 100644 --- a/src/do_tool/tool_can_use/base_tool.py +++ b/src/do_tool/tool_can_use/base_tool.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Any, Optional, Type +from typing import List, Any, Optional, Type import inspect import importlib import pkgutil diff --git a/src/heart_flow/background_tasks.py b/src/heart_flow/background_tasks.py index 38360653..1b64c205 100644 --- a/src/heart_flow/background_tasks.py +++ b/src/heart_flow/background_tasks.py @@ -26,7 +26,7 @@ LOG_INTERVAL_SECONDS = 3 async def _run_periodic_loop( - task_name: str, interval: int, task_func: Callable[..., Coroutine[Any, Any, None]], **kwargs + task_name: str, interval: int, task_func: Callable[..., Coroutine[Any, Any, None]], **kwargs ): """周期性任务主循环""" while True: @@ -250,14 +250,10 @@ class BackgroundTaskManager: # --- Specific Task Runners --- # async def _run_state_update_cycle(self, interval: int): - await _run_periodic_loop( - task_name="State Update", interval=interval, task_func=self._perform_state_update_work - ) + await _run_periodic_loop(task_name="State Update", interval=interval, task_func=self._perform_state_update_work) async def _run_absent_into_chat(self, interval: int): - await _run_periodic_loop( - task_name="Into Chat", interval=interval, task_func=self._perform_absent_into_chat - ) + await _run_periodic_loop(task_name="Into Chat", interval=interval, task_func=self._perform_absent_into_chat) async def _run_normal_chat_timeout_check_cycle(self, interval: int): await _run_periodic_loop( diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index 09d7b936..f414e6b2 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -78,7 +78,7 @@ def calculate_replacement_probability(similarity: float) -> float: # p = 3.5 * s - 1.4 probability = 3.5 * similarity - 1.4 return max(0.0, probability) - else: # 0.6 < similarity < 0.9 + else: # 0.6 < similarity < 0.9 # p = s + 0.1 probability = similarity + 0.1 return min(1.0, max(0.0, probability)) diff --git a/src/individuality/scene.py b/src/individuality/scene.py index 4edd5dc2..8d7af97f 100644 --- a/src/individuality/scene.py +++ b/src/individuality/scene.py @@ -30,7 +30,7 @@ def get_scene_by_factor(factor: str) -> dict | None: Returns: dict: 包含情景描述的字典 """ - return PERSONALITY_SCENES.get(factor,None) + return PERSONALITY_SCENES.get(factor, None) def get_all_scenes() -> dict: diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py index 2de88561..e753a999 100644 --- a/src/plugins/chat/message.py +++ b/src/plugins/chat/message.py @@ -116,8 +116,6 @@ class MessageRecv(Message): self.processed_plain_text = await self._process_message_segments(self.message_segment) self.detailed_plain_text = self._generate_detailed_text() - - async def _process_single_segment(self, seg: Seg) -> str: """处理单个消息段 diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index ee4865d9..8d134847 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -4,7 +4,6 @@ import time from typing import Union # from ...common.database import db # 数据库依赖似乎不需要了,注释掉 -from ..message.api import global_api from .message import MessageSending, MessageThinking, MessageSet from ..storage.storage import MessageStorage @@ -27,7 +26,7 @@ async def send_via_ws(message: MessageSending) -> None: async def send_message( - message: MessageSending, + message: MessageSending, ) -> None: """发送消息(核心发送逻辑)""" diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index 40b4ab7c..a7408681 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -34,7 +34,7 @@ def db_message_to_str(message_dict: dict) -> str: message_dict.get("user_nickname", ""), message_dict.get("user_cardname", ""), ) - except Exception as e: + except Exception: name = message_dict.get("user_nickname", "") or f"用户{message_dict['user_id']}" content = message_dict.get("processed_plain_text", "") result = f"[{time_str}] {name}: {content}\n" @@ -76,7 +76,7 @@ def is_mentioned_bot_in_message(message: MessageRecv) -> tuple[bool, float]: if not is_mentioned: # 判断是否被回复 if re.match( - f"\[回复 [\s\S]*?\({str(global_config.BOT_QQ)}\):[\s\S]*?],说:" , message.processed_plain_text + f"\[回复 [\s\S]*?\({str(global_config.BOT_QQ)}\):[\s\S]*?],说:", message.processed_plain_text ): is_mentioned = True else: @@ -731,7 +731,7 @@ def translate_timestamp_to_human_readable(timestamp: float, mode: str = "normal" return f"{int(diff / 86400)}天前:\n" else: return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp)) + ":\n" - else: # mode = "lite" or unknown + else: # mode = "lite" or unknown # 只返回时分秒格式,喵~ return time.strftime("%H:%M:%S", time.localtime(timestamp)) diff --git a/src/plugins/heartFC_chat/heartFC_sender.py b/src/plugins/heartFC_chat/heartFC_sender.py index a4103667..fc96207d 100644 --- a/src/plugins/heartFC_chat/heartFC_sender.py +++ b/src/plugins/heartFC_chat/heartFC_sender.py @@ -1,7 +1,6 @@ # src/plugins/heartFC_chat/heartFC_sender.py import asyncio # 重新导入 asyncio from typing import Dict, Optional # 重新导入类型 -from ..message.api import global_api from ..chat.message import MessageSending, MessageThinking # 只保留 MessageSending 和 MessageThinking from ..storage.storage import MessageStorage from ..chat.utils import truncate_message diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 9b83f265..c6cdf251 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -151,9 +151,7 @@ JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query": Prompt("\n你有以下这些**知识**:\n{prompt_info}\n请你**记住上面的知识**,之后可能会用到。\n", "knowledge_prompt") -async def _build_prompt_focus( - reason, current_mind_info, structured_info, chat_stream, sender_name -) -> tuple[str, str]: +async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_stream, sender_name) -> tuple[str, str]: individuality = Individuality.get_instance() prompt_personality = individuality.get_prompt(x_person=0, level=2) # 日程构建 diff --git a/src/plugins/respon_info_catcher/info_catcher.py b/src/plugins/respon_info_catcher/info_catcher.py index 08be4a76..32add842 100644 --- a/src/plugins/respon_info_catcher/info_catcher.py +++ b/src/plugins/respon_info_catcher/info_catcher.py @@ -185,18 +185,25 @@ class InfoCatcher: try: # 将消息对象转换为可序列化的字典喵~ - thinking_log_data = {"chat_id": self.chat_id, "trigger_text": self.trigger_response_text, - "response_text": self.response_text, "trigger_info": { + thinking_log_data = { + "chat_id": self.chat_id, + "trigger_text": self.trigger_response_text, + "response_text": self.response_text, + "trigger_info": { "time": self.trigger_response_time, "message": self.message_to_dict(self.trigger_response_message), - }, "response_info": { + }, + "response_info": { "time": self.response_time, "message": self.response_messages, - }, "timing_results": self.timing_results, "chat_history": self.message_list_to_dict(self.chat_history), - "chat_history_in_thinking": self.message_list_to_dict(self.chat_history_in_thinking), - "chat_history_after_response": self.message_list_to_dict( - self.chat_history_after_response), "heartflow_data": self.heartflow_data, - "reasoning_data": self.reasoning_data} + }, + "timing_results": self.timing_results, + "chat_history": self.message_list_to_dict(self.chat_history), + "chat_history_in_thinking": self.message_list_to_dict(self.chat_history_in_thinking), + "chat_history_after_response": self.message_list_to_dict(self.chat_history_after_response), + "heartflow_data": self.heartflow_data, + "reasoning_data": self.reasoning_data, + } # 根据不同的响应模式添加相应的数据喵~ # 现在直接都加上去好了喵~ # if self.response_mode == "heart_flow": From 45c64208b40c39e969658746aaac734d9d51a483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E7=8C=AB?= Date: Thu, 1 May 2025 07:24:52 +0900 Subject: [PATCH 16/18] refactor: Clean up unused variables and improve code readability --- bot.py | 2 -- scripts/info_extraction.py | 2 +- src/common/message_repository.py | 1 - src/individuality/individuality.py | 1 - src/plugins/PFC/action_planner.py | 1 - src/plugins/PFC/chat_observer.py | 6 +----- src/plugins/chat/utils.py | 2 +- src/plugins/emoji_system/emoji_manager.py | 2 -- src/plugins/heartFC_chat/heartFC_chat.py | 9 ++++----- src/plugins/memory_system/Hippocampus.py | 2 -- src/plugins/person_info/person_info.py | 1 - src/plugins/willing/mode_dynamic.py | 3 --- src/plugins/willing/mode_llmcheck.py | 6 ++---- 13 files changed, 9 insertions(+), 29 deletions(-) diff --git a/bot.py b/bot.py index 770f5365..9a4a5000 100644 --- a/bot.py +++ b/bot.py @@ -130,9 +130,7 @@ def check_eula(): privacy_file = Path("PRIVACY.md") eula_updated = True - eula_new_hash = None privacy_updated = True - privacy_new_hash = None eula_confirmed = False privacy_confirmed = False diff --git a/scripts/info_extraction.py b/scripts/info_extraction.py index fdb44528..65c4082b 100644 --- a/scripts/info_extraction.py +++ b/scripts/info_extraction.py @@ -76,7 +76,7 @@ def process_single_text(pg_hash, raw_data, llm_client_list): return doc_item, None -def signal_handler(signum, frame): +def signal_handler(_signum, _frame): """处理Ctrl+C信号""" logger.info("\n接收到中断信号,正在优雅地关闭程序...") shutdown_event.set() diff --git a/src/common/message_repository.py b/src/common/message_repository.py index 9f246077..03f192ce 100644 --- a/src/common/message_repository.py +++ b/src/common/message_repository.py @@ -26,7 +26,6 @@ def find_messages( """ try: query = db.messages.find(message_filter) - results: List[dict[str, Any]] = [] if limit > 0: if limit_mode == "earliest": diff --git a/src/individuality/individuality.py b/src/individuality/individuality.py index 86e5b63e..9ffdfdaa 100644 --- a/src/individuality/individuality.py +++ b/src/individuality/individuality.py @@ -113,7 +113,6 @@ class Individuality: p_pronoun = "我" prompt_personality = f"{p_pronoun}{self.personality.personality_core}" else: # x_person == 0 - p_pronoun = "" # 无人称 # 对于无人称,直接描述核心特征 prompt_personality = f"{self.personality.personality_core}" diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 23de9f0d..4770c6ce 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -262,7 +262,6 @@ class ActionPlanner: # --- 知识信息字符串构建结束 --- # 获取聊天历史记录 (chat_history_text) - chat_history_text = "" try: if hasattr(observation_info, "chat_history") and observation_info.chat_history: chat_history_text = observation_info.chat_history_str diff --git a/src/plugins/PFC/chat_observer.py b/src/plugins/PFC/chat_observer.py index e99ed800..3576397d 100644 --- a/src/plugins/PFC/chat_observer.py +++ b/src/plugins/PFC/chat_observer.py @@ -123,11 +123,7 @@ class ChatObserver: self.last_cold_chat_check = current_time # 判断是否冷场 - is_cold = False - if self.last_message_time is None: - is_cold = True - else: - is_cold = (current_time - self.last_message_time) > self.cold_chat_threshold + is_cold = True if self.last_message_time is None else (current_time - self.last_message_time) > self.cold_chat_threshold # 如果冷场状态发生变化,发送通知 if is_cold != self.is_cold_chat_state: diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index a7408681..cdaa2194 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -156,7 +156,7 @@ async def get_recent_group_messages(chat_id: str, limit: int = 12) -> list: return message_objects -def get_recent_group_detailed_plain_text(chat_stream_id: int, limit: int = 12, combine=False): +def get_recent_group_detailed_plain_text(chat_stream_id: str, limit: int = 12, combine=False): recent_messages = list( db.messages.find( {"chat_id": chat_stream_id}, diff --git a/src/plugins/emoji_system/emoji_manager.py b/src/plugins/emoji_system/emoji_manager.py index a75a4f90..f789f906 100644 --- a/src/plugins/emoji_system/emoji_manager.py +++ b/src/plugins/emoji_system/emoji_manager.py @@ -50,8 +50,6 @@ class MaiEmoji: async def initialize_hash_format(self): """从文件创建表情包实例, 计算哈希值和格式""" - image_base64 = None - image_bytes = None try: # 使用 full_path 检查文件是否存在 if not os.path.exists(self.full_path): diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 47d420dd..71b8d81c 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -174,7 +174,7 @@ class HeartFChatting: self, chat_id: str, sub_mind: SubMind, - observations: Observation, + observations: list[Observation], on_consecutive_no_reply_callback: Callable[[], Coroutine[None, None, None]], ): """ @@ -631,19 +631,18 @@ class HeartFChatting: observation = self.observations[0] if self.observations else None try: - dang_qian_deng_dai = 0.0 # 初始化本次等待时间 with Timer("等待新消息", cycle_timers): # 等待新消息、超时或关闭信号,并获取结果 await self._wait_for_new_message(observation, planner_start_db_time, self.log_prefix) # 从计时器获取实际等待时间 - dang_qian_deng_dai = cycle_timers.get("等待新消息", 0.0) + 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 += dang_qian_deng_dai # 累加等待时间 + self._lian_xu_deng_dai_shi_jian += current_waiting # 累加等待时间 logger.debug( f"{self.log_prefix} 连续不回复计数增加: {self._lian_xu_bu_hui_fu_ci_shu}/{CONSECUTIVE_NO_REPLY_THRESHOLD}, " - f"本次等待: {dang_qian_deng_dai:.2f}秒, 累计等待: {self._lian_xu_deng_dai_shi_jian:.2f}秒" + f"本次等待: {current_waiting:.2f}秒, 累计等待: {self._lian_xu_deng_dai_shi_jian:.2f}秒" ) # 检查是否同时达到次数和时间阈值 diff --git a/src/plugins/memory_system/Hippocampus.py b/src/plugins/memory_system/Hippocampus.py index ff34541e..ccc4d4fa 100644 --- a/src/plugins/memory_system/Hippocampus.py +++ b/src/plugins/memory_system/Hippocampus.py @@ -364,7 +364,6 @@ class Hippocampus: logger.debug(f"有效的关键词: {', '.join(valid_keywords)}") # 从每个关键词获取记忆 - all_memories = [] activate_map = {} # 存储每个词的累计激活值 # 对每个关键词进行扩散式检索 @@ -536,7 +535,6 @@ class Hippocampus: logger.debug(f"有效的关键词: {', '.join(valid_keywords)}") # 从每个关键词获取记忆 - all_memories = [] activate_map = {} # 存储每个词的累计激活值 # 对每个关键词进行扩散式检索 diff --git a/src/plugins/person_info/person_info.py b/src/plugins/person_info/person_info.py index 8bafe5eb..a71f95f8 100644 --- a/src/plugins/person_info/person_info.py +++ b/src/plugins/person_info/person_info.py @@ -137,7 +137,6 @@ class PersonInfoManager: @staticmethod def _extract_json_from_text(text: str) -> dict: """从文本中提取JSON数据的高容错方法""" - parsed_json = None try: # 尝试直接解析 parsed_json = json.loads(text) diff --git a/src/plugins/willing/mode_dynamic.py b/src/plugins/willing/mode_dynamic.py index ab1389ea..029da4e0 100644 --- a/src/plugins/willing/mode_dynamic.py +++ b/src/plugins/willing/mode_dynamic.py @@ -50,7 +50,6 @@ class DynamicWillingManager(BaseWillingManager): is_high_mode = self.chat_high_willing_mode.get(chat_id, False) # 获取当前模式的持续时间 - duration = 0 if is_high_mode: duration = self.chat_high_willing_duration.get(chat_id, 180) # 默认3分钟 else: @@ -154,8 +153,6 @@ class DynamicWillingManager(BaseWillingManager): ) # 根据当前模式计算回复概率 - base_probability = 0.0 - if in_conversation_context: # 在对话上下文中,降低基础回复概率 base_probability = 0.5 if is_high_mode else 0.25 diff --git a/src/plugins/willing/mode_llmcheck.py b/src/plugins/willing/mode_llmcheck.py index ec1cde29..697621b1 100644 --- a/src/plugins/willing/mode_llmcheck.py +++ b/src/plugins/willing/mode_llmcheck.py @@ -76,10 +76,8 @@ class LlmcheckWillingManager(MxpWillingManager): current_date = time.strftime("%Y-%m-%d", time.localtime()) current_time = time.strftime("%H:%M:%S", time.localtime()) - chat_talking_prompt = "" - if chat_id: - chat_talking_prompt = get_recent_group_detailed_plain_text(chat_id, limit=length, combine=True) - else: + chat_talking_prompt = get_recent_group_detailed_plain_text(chat_id, limit=length, combine=True) + if not chat_id: return 0 # if is_mentioned_bot: From 0fba84c193d8911a48f5a5eaa9d591948d7b290c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 30 Apr 2025 22:25:33 +0000 Subject: [PATCH 17/18] =?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/chat_observer.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/plugins/PFC/chat_observer.py b/src/plugins/PFC/chat_observer.py index 3576397d..e66824a2 100644 --- a/src/plugins/PFC/chat_observer.py +++ b/src/plugins/PFC/chat_observer.py @@ -123,7 +123,11 @@ class ChatObserver: self.last_cold_chat_check = current_time # 判断是否冷场 - is_cold = True if self.last_message_time is None else (current_time - self.last_message_time) > self.cold_chat_threshold + is_cold = ( + True + if self.last_message_time is None + else (current_time - self.last_message_time) > self.cold_chat_threshold + ) # 如果冷场状态发生变化,发送通知 if is_cold != self.is_cold_chat_state: From 1b30598797a51812c455eb549c339769e27ce0cd Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Thu, 1 May 2025 09:59:36 +0800 Subject: [PATCH 18/18] =?UTF-8?q?=E8=AE=A9=E9=BA=A6=E9=BA=A6=E8=87=B3?= =?UTF-8?q?=E5=B0=91=E8=83=BD=E5=90=AF=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/message_sender.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index 8d134847..fdeec346 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -146,7 +146,6 @@ class MessageManager: """管理所有聊天流的消息容器 (不再是单例)""" def __init__(self): - self._processor_task = None self.containers: dict[str, MessageContainer] = {} self.storage = MessageStorage() # 添加 storage 实例 self._running = True # 处理器运行状态