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/11] =?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 b5317d58b..12a9c1356 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 cd7393445..20f8f8ad0 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 4a48e977c..291800cf0 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 bfdf2d6ae..5b1e31515 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 69bae0411..7cb847e0a 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/11] =?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 72169bc44..62b2712ca 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/11] 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 12a9c1356..6c95935ea 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 62b2712ca..5f3776782 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 5b1e31515..3dc648ec0 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 7cb847e0a..1e5d8d21f 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/11] =?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 291800cf0..75918e1b7 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 5f3776782..d6da4ce3f 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 3dc648ec0..e4aa28ab8 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/11] =?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 75918e1b7..8d07e6b52 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 e4aa28ab8..b9c50b402 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 1e5d8d21f..66cf6af8c 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 c159a329e..60a6d7cbb 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 02baf94d4..6c7abc7dd 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/11] =?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 20f8f8ad0..29277820e 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 23368ccaf..c924d35ab 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/11] 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 60a6d7cbb..9ed63c2df 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/11] 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 e6cf16d4d..fbf558a37 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/11] =?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 b9c50b402..73d679e4e 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 6c7abc7dd..ea698bf2c 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/11] 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 b9c50b402..73d679e4e 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 6c7abc7dd..ea698bf2c 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/11] =?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 a0f266d66..429e96975 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