better:让表情包和文字统一发送

This commit is contained in:
SengokuCola
2025-05-13 22:13:00 +08:00
parent 238165d1f9
commit d2f7f093e3
11 changed files with 243 additions and 241 deletions

View File

@@ -1,6 +1,5 @@
import time
import traceback import traceback
from typing import List, Optional, Dict, Any from typing import List, Optional, Dict, Any, Tuple
from src.chat.message_receive.message import MessageRecv, MessageThinking, MessageSending from src.chat.message_receive.message import MessageRecv, MessageThinking, MessageSending
from src.chat.message_receive.message import Seg # Local import needed after move from src.chat.message_receive.message import Seg # Local import needed after move
from src.chat.message_receive.message import UserInfo from src.chat.message_receive.message import UserInfo
@@ -18,6 +17,7 @@ from src.chat.utils.info_catcher import info_catcher_manager
from src.manager.mood_manager import mood_manager from src.manager.mood_manager import mood_manager
from src.chat.heart_flow.utils_chat import get_chat_type_and_target_info from src.chat.heart_flow.utils_chat import get_chat_type_and_target_info
from src.chat.message_receive.chat_stream import ChatStream from src.chat.message_receive.chat_stream import ChatStream
from src.chat.focus_chat.hfc_utils import parse_thinking_id_to_timestamp
logger = get_logger("expressor") logger = get_logger("expressor")
@@ -41,7 +41,7 @@ class DefaultExpressor:
async def initialize(self): async def initialize(self):
self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.chat_id) self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.chat_id)
async def _create_thinking_message(self, anchor_message: Optional[MessageRecv]) -> Optional[str]: async def _create_thinking_message(self, anchor_message: Optional[MessageRecv], thinking_id: str):
"""创建思考消息 (尝试锚定到 anchor_message)""" """创建思考消息 (尝试锚定到 anchor_message)"""
if not anchor_message or not anchor_message.chat_stream: if not anchor_message or not anchor_message.chat_stream:
logger.error(f"{self.log_prefix} 无法创建思考消息,缺少有效的锚点消息或聊天流。") logger.error(f"{self.log_prefix} 无法创建思考消息,缺少有效的锚点消息或聊天流。")
@@ -49,6 +49,7 @@ class DefaultExpressor:
chat = anchor_message.chat_stream chat = anchor_message.chat_stream
messageinfo = anchor_message.message_info messageinfo = anchor_message.message_info
thinking_time_point = parse_thinking_id_to_timestamp(thinking_id)
bot_user_info = UserInfo( bot_user_info = UserInfo(
user_id=global_config.BOT_QQ, user_id=global_config.BOT_QQ,
user_nickname=global_config.BOT_NICKNAME, user_nickname=global_config.BOT_NICKNAME,
@@ -58,9 +59,6 @@ class DefaultExpressor:
# logger.debug(f"创建思考消息chat{chat}") # logger.debug(f"创建思考消息chat{chat}")
# logger.debug(f"创建思考消息bot_user_info{bot_user_info}") # logger.debug(f"创建思考消息bot_user_info{bot_user_info}")
# logger.debug(f"创建思考消息messageinfo{messageinfo}") # logger.debug(f"创建思考消息messageinfo{messageinfo}")
thinking_time_point = round(time.time(), 2)
thinking_id = "mt" + str(thinking_time_point)
thinking_message = MessageThinking( thinking_message = MessageThinking(
message_id=thinking_id, message_id=thinking_id,
chat_stream=chat, chat_stream=chat,
@@ -69,9 +67,8 @@ class DefaultExpressor:
thinking_start_time=thinking_time_point, thinking_start_time=thinking_time_point,
) )
logger.debug(f"创建思考消息thinking_message{thinking_message}") logger.debug(f"创建思考消息thinking_message{thinking_message}")
# Access MessageManager directly (using heart_fc_sender)
await self.heart_fc_sender.register_thinking(thinking_message) await self.heart_fc_sender.register_thinking(thinking_message)
return thinking_id
async def deal_reply( async def deal_reply(
self, self,
@@ -79,11 +76,10 @@ class DefaultExpressor:
action_data: Dict[str, Any], action_data: Dict[str, Any],
reasoning: str, reasoning: str,
anchor_message: MessageRecv, anchor_message: MessageRecv,
thinking_id: str,
) -> tuple[bool, Optional[List[str]]]: ) -> tuple[bool, Optional[List[str]]]:
# 创建思考消息 # 创建思考消息
thinking_id = await self._create_thinking_message(anchor_message) await self._create_thinking_message(anchor_message, thinking_id)
if not thinking_id:
raise Exception("无法创建思考消息")
reply = None # 初始化 reply防止未定义 reply = None # 初始化 reply防止未定义
try: try:
@@ -102,8 +98,14 @@ class DefaultExpressor:
action_data=action_data, action_data=action_data,
) )
with Timer("选择表情", cycle_timers):
emoji_keyword = action_data.get("emojis", [])
emoji_base64 = await self._choose_emoji(emoji_keyword)
if emoji_base64:
reply.append(("emoji", emoji_base64))
if reply: if reply:
with Timer("发送文本消息", cycle_timers): with Timer("发送消息", cycle_timers):
await self._send_response_messages( await self._send_response_messages(
anchor_message=anchor_message, anchor_message=anchor_message,
thinking_id=thinking_id, thinking_id=thinking_id,
@@ -113,12 +115,6 @@ class DefaultExpressor:
else: else:
logger.warning(f"{self.log_prefix} 文本回复生成失败") logger.warning(f"{self.log_prefix} 文本回复生成失败")
# 处理表情部分
emoji_keyword = action_data.get("emojis", [])
if emoji_keyword:
await self._handle_emoji(anchor_message, [], emoji_keyword)
has_sent_something = True
if not has_sent_something: if not has_sent_something:
logger.warning(f"{self.log_prefix} 回复动作未包含任何有效内容") logger.warning(f"{self.log_prefix} 回复动作未包含任何有效内容")
@@ -195,40 +191,46 @@ class DefaultExpressor:
logger.info(f"想要表达:{in_mind_reply}") logger.info(f"想要表达:{in_mind_reply}")
logger.info(f"理由:{reason}") logger.info(f"理由:{reason}")
logger.info(f"生成回复: {content}\n") logger.info(f"生成回复: {content}\n")
info_catcher.catch_after_llm_generated( info_catcher.catch_after_llm_generated(
prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=model_name prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=model_name
) )
except Exception as llm_e: except Exception as llm_e:
# 精简报错信息 # 精简报错信息
logger.error(f"{self.log_prefix}[Replier-{thinking_id}] LLM 生成失败: {llm_e}") logger.error(f"{self.log_prefix}LLM 生成失败: {llm_e}")
return None # LLM 调用失败则无法生成回复 return None # LLM 调用失败则无法生成回复
# 5. 处理 LLM 响应
if not content:
logger.warning(f"{self.log_prefix}[Replier-{thinking_id}] LLM 生成了空内容。")
return None
processed_response = process_llm_response(content) processed_response = process_llm_response(content)
# 5. 处理 LLM 响应
if not content:
logger.warning(f"{self.log_prefix}LLM 生成了空内容。")
return None
if not processed_response: if not processed_response:
logger.warning(f"{self.log_prefix}[Replier-{thinking_id}] 处理后的回复为空。") logger.warning(f"{self.log_prefix}处理后的回复为空。")
return None return None
return processed_response reply_set = []
for str in processed_response:
reply_seg = ("text", str)
reply_set.append(reply_seg)
return reply_set
except Exception as e: except Exception as e:
logger.error(f"{self.log_prefix}[Replier-{thinking_id}] 回复生成意外失败: {e}") logger.error(f"{self.log_prefix}回复生成意外失败: {e}")
traceback.print_exc() traceback.print_exc()
return None return None
# --- 发送器 (Sender) --- # # --- 发送器 (Sender) --- #
async def _send_response_messages( async def _send_response_messages(
self, anchor_message: Optional[MessageRecv], response_set: List[str], thinking_id: str self, anchor_message: Optional[MessageRecv], response_set: List[Tuple[str, str]], thinking_id: str
) -> Optional[MessageSending]: ) -> Optional[MessageSending]:
"""发送回复消息 (尝试锚定到 anchor_message),使用 HeartFCSender""" """发送回复消息 (尝试锚定到 anchor_message),使用 HeartFCSender"""
chat = self.chat_stream chat = self.chat_stream
chat_id = self.chat_id
if chat is None: if chat is None:
logger.error(f"{self.log_prefix} 无法发送回复chat_stream 为空。") logger.error(f"{self.log_prefix} 无法发送回复chat_stream 为空。")
return None return None
@@ -236,98 +238,104 @@ class DefaultExpressor:
logger.error(f"{self.log_prefix} 无法发送回复anchor_message 为空。") logger.error(f"{self.log_prefix} 无法发送回复anchor_message 为空。")
return None return None
chat_id = self.chat_id
stream_name = chat_manager.get_stream_name(chat_id) or chat_id # 获取流名称用于日志 stream_name = chat_manager.get_stream_name(chat_id) or chat_id # 获取流名称用于日志
# 检查思考过程是否仍在进行,并获取开始时间 # 检查思考过程是否仍在进行,并获取开始时间
thinking_start_time = await self.heart_fc_sender.get_thinking_start_time(chat_id, thinking_id) thinking_start_time = await self.heart_fc_sender.get_thinking_start_time(chat_id, thinking_id)
if thinking_start_time is None: if thinking_start_time is None:
logger.warning(f"[{stream_name}] {thinking_id} 思考过程未找到或已结束,无法发送回复。") logger.error(f"[{stream_name}]思考过程未找到或已结束,无法发送回复。")
return None return None
mark_head = False mark_head = False
first_bot_msg: Optional[MessageSending] = None first_bot_msg: Optional[MessageSending] = None
reply_message_ids = [] # 记录实际发送的消息ID reply_message_ids = [] # 记录实际发送的消息ID
bot_user_info = UserInfo(
user_id=global_config.BOT_QQ,
user_nickname=global_config.BOT_NICKNAME,
platform=chat.platform,
)
for i, msg_text in enumerate(response_set): for i, msg_text in enumerate(response_set):
# 为每个消息片段生成唯一ID # 为每个消息片段生成唯一ID
type = msg_text[0]
data = msg_text[1]
part_message_id = f"{thinking_id}_{i}" part_message_id = f"{thinking_id}_{i}"
message_segment = Seg(type="text", data=msg_text) message_segment = Seg(type=type, data=data)
bot_message = MessageSending(
message_id=part_message_id, # 使用片段的唯一ID if type == "emoji":
chat_stream=chat, is_emoji = True
bot_user_info=bot_user_info, else:
sender_info=anchor_message.message_info.user_info, is_emoji = False
reply_to = not mark_head
bot_message = self._build_single_sending_message(
anchor_message=anchor_message,
message_id=part_message_id,
message_segment=message_segment, message_segment=message_segment,
reply=anchor_message, # 回复原始锚点 reply_to=reply_to,
is_head=not mark_head, is_emoji=is_emoji,
is_emoji=False, thinking_id=thinking_id,
thinking_start_time=thinking_start_time, # 传递原始思考开始时间
) )
try: try:
if not mark_head: if not mark_head:
mark_head = True mark_head = True
first_bot_msg = bot_message # 保存第一个成功发送的消息对象 first_bot_msg = bot_message # 保存第一个成功发送的消息对象
await self.heart_fc_sender.type_and_send_message(bot_message, typing=False) typing = False
else: else:
await self.heart_fc_sender.type_and_send_message(bot_message, typing=True) typing = True
await self.heart_fc_sender.send_message(bot_message, has_thinking=True, typing=typing)
reply_message_ids.append(part_message_id) # 记录我们生成的ID reply_message_ids.append(part_message_id) # 记录我们生成的ID
except Exception as e: except Exception as e:
logger.error( logger.error(f"{self.log_prefix}发送回复片段 {i} ({part_message_id}) 时失败: {e}")
f"{self.log_prefix}[Sender-{thinking_id}] 发送回复片段 {i} ({part_message_id}) 时失败: {e}"
)
# 这里可以选择是继续发送下一个片段还是中止 # 这里可以选择是继续发送下一个片段还是中止
# 在尝试发送完所有片段后,完成原始的 thinking_id 状态 # 在尝试发送完所有片段后,完成原始的 thinking_id 状态
try: try:
await self.heart_fc_sender.complete_thinking(chat_id, thinking_id) await self.heart_fc_sender.complete_thinking(chat_id, thinking_id)
except Exception as e: except Exception as e:
logger.error(f"{self.log_prefix}[Sender-{thinking_id}] 完成思考状态 {thinking_id} 时出错: {e}") logger.error(f"{self.log_prefix}完成思考状态 {thinking_id} 时出错: {e}")
return first_bot_msg # 返回第一个成功发送的消息对象 return first_bot_msg # 返回第一个成功发送的消息对象
async def _handle_emoji(self, anchor_message: Optional[MessageRecv], response_set: List[str], send_emoji: str = ""): async def _choose_emoji(self, send_emoji: str):
"""处理表情包 (尝试锚定到 anchor_message),使用 HeartFCSender""" """
if not anchor_message or not anchor_message.chat_stream: 选择表情根据send_emoji文本选择表情返回表情base64
logger.error(f"{self.log_prefix} 无法处理表情包,缺少有效的锚点消息或聊天流。") """
return
chat = anchor_message.chat_stream
emoji_raw = await emoji_manager.get_emoji_for_text(send_emoji) emoji_raw = await emoji_manager.get_emoji_for_text(send_emoji)
if emoji_raw: if emoji_raw:
emoji_path, description = emoji_raw emoji_path, _description = emoji_raw
emoji_base64 = image_path_to_base64(emoji_path)
return emoji_base64
emoji_cq = image_path_to_base64(emoji_path) async def _build_single_sending_message(
thinking_time_point = round(time.time(), 2) # 用于唯一ID self,
message_segment = Seg(type="emoji", data=emoji_cq) anchor_message: MessageRecv,
bot_user_info = UserInfo( message_id: str,
user_id=global_config.BOT_QQ, message_segment: Seg,
user_nickname=global_config.BOT_NICKNAME, reply_to: bool,
platform=anchor_message.message_info.platform, is_emoji: bool,
) thinking_id: str,
bot_message = MessageSending( ) -> MessageSending:
message_id="me" + str(thinking_time_point), # 表情消息的唯一ID """构建单个发送消息"""
chat_stream=chat,
bot_user_info=bot_user_info,
sender_info=anchor_message.message_info.user_info,
message_segment=message_segment,
reply=anchor_message, # 回复原始锚点
is_head=False, # 表情通常不是头部消息
is_emoji=True,
# 不需要 thinking_start_time
)
try: thinking_start_time = await self.heart_fc_sender.get_thinking_start_time(self.chat_id, thinking_id)
await self.heart_fc_sender.send_and_store(bot_message) bot_user_info = UserInfo(
except Exception as e: user_id=global_config.BOT_QQ,
logger.error(f"{self.log_prefix} 发送表情包 {bot_message.message_info.message_id} 时失败: {e}") user_nickname=global_config.BOT_NICKNAME,
platform=self.chat_stream.platform,
)
bot_message = MessageSending(
message_id=message_id, # 使用片段的唯一ID
chat_stream=self.chat_stream,
bot_user_info=bot_user_info,
sender_info=anchor_message.message_info.user_info,
message_segment=message_segment,
reply=anchor_message, # 回复原始锚点
is_head=reply_to,
is_emoji=is_emoji,
thinking_start_time=thinking_start_time, # 传递原始思考开始时间
)
return bot_message

View File

@@ -58,9 +58,9 @@ def init_prompt() -> None:
{chat_str} {chat_str}
请从上面这段群聊中概括除了人名为"麦麦"之外的人的语法和句法特点,只考虑纯文字,不要考虑表情包和图片 请从上面这段群聊中概括除了人名为"麦麦"之外的人的语法和句法特点,只考虑纯文字,不要考虑表情包和图片
不要总结【图片】,【动画表情】,[图片][动画表情],不总结 表情符号 不要总结【图片】,【动画表情】,[图片][动画表情],不总结 表情符号 at @ 回复 和[回复]
不要涉及具体的人名,只考虑语法和句法特点, 不要涉及具体的人名,只考虑语法和句法特点,
语法和句法特点要包括,句子长短(具体字数),如何分局,有何种语病,如何拆分句子。 语法和句法特点要包括,句子长短(具体字数),有何种语病,如何拆分句子。
总结成如下格式的规律,总结的内容要简洁,不浮夸: 总结成如下格式的规律,总结的内容要简洁,不浮夸:
"xxx"时,可以"xxx" "xxx"时,可以"xxx"

View File

@@ -30,7 +30,7 @@ from src.chat.heart_flow.observation.hfcloop_observation import HFCloopObservati
from src.chat.heart_flow.observation.working_observation import WorkingObservation from src.chat.heart_flow.observation.working_observation import WorkingObservation
from src.chat.focus_chat.info_processors.tool_processor import ToolProcessor from src.chat.focus_chat.info_processors.tool_processor import ToolProcessor
from src.chat.focus_chat.expressors.default_expressor import DefaultExpressor from src.chat.focus_chat.expressors.default_expressor import DefaultExpressor
from src.chat.focus_chat.hfc_utils import _create_empty_anchor_message from src.chat.focus_chat.hfc_utils import create_empty_anchor_message, parse_thinking_id_to_timestamp
from src.chat.focus_chat.memory_activator import MemoryActivator from src.chat.focus_chat.memory_activator import MemoryActivator
install(extra_lines=3) install(extra_lines=3)
@@ -203,9 +203,9 @@ class HeartFChatting:
self._cycle_counter = 0 self._cycle_counter = 0
self._cycle_history: Deque[CycleDetail] = deque(maxlen=10) # 保留最近10个循环的信息 self._cycle_history: Deque[CycleDetail] = deque(maxlen=10) # 保留最近10个循环的信息
self._current_cycle: Optional[CycleDetail] = None self._current_cycle: Optional[CycleDetail] = None
self._lian_xu_bu_hui_fu_ci_shu: int = 0 # <--- 新增:连续不回复计数器 self.total_no_reply_count: int = 0 # <--- 新增:连续不回复计数器
self._shutting_down: bool = False # <--- 新增:关闭标志位 self._shutting_down: bool = False # <--- 新增:关闭标志位
self._lian_xu_deng_dai_shi_jian: float = 0.0 # <--- 新增:累计等待时间 self.total_waiting_time: float = 0.0 # <--- 新增:累计等待时间
async def _initialize(self) -> bool: async def _initialize(self) -> bool:
""" """
@@ -325,11 +325,12 @@ class HeartFChatting:
await asyncio.sleep(0.1) # 短暂等待避免空转 await asyncio.sleep(0.1) # 短暂等待避免空转
continue continue
# 记录规划开始时间点 # thinking_id 是思考过程的ID用于标记每一轮思考
planner_start_db_time = time.time() thinking_id = "tid" + str(round(time.time(), 2))
# 主循环:思考->决策->执行 # 主循环:思考->决策->执行
action_taken, thinking_id = await self._think_plan_execute_loop(cycle_timers, planner_start_db_time)
action_taken = await self._think_plan_execute_loop(cycle_timers, thinking_id)
# 更新循环信息 # 更新循环信息
self._current_cycle.set_thinking_id(thinking_id) self._current_cycle.set_thinking_id(thinking_id)
@@ -391,9 +392,8 @@ class HeartFChatting:
if acquired and self._processing_lock.locked(): if acquired and self._processing_lock.locked():
self._processing_lock.release() self._processing_lock.release()
async def _think_plan_execute_loop(self, cycle_timers: dict, planner_start_db_time: float) -> tuple[bool, str]: async def _think_plan_execute_loop(self, cycle_timers: dict, thinking_id: str) -> tuple[bool, str]:
try: try:
await asyncio.sleep(1)
with Timer("观察", cycle_timers): with Timer("观察", cycle_timers):
await self.observations[0].observe() await self.observations[0].observe()
await self.memory_observation.observe() await self.memory_observation.observe()
@@ -515,7 +515,7 @@ class HeartFChatting:
self.hfcloop_observation.add_loop_info(self._current_cycle) self.hfcloop_observation.add_loop_info(self._current_cycle)
return await self._handle_action(action, reasoning, action_data, cycle_timers, planner_start_db_time) return await self._handle_action(action, reasoning, action_data, cycle_timers, thinking_id)
except Exception as e: except Exception as e:
logger.error(f"{self.log_prefix} 并行+串行处理失败: {e}") logger.error(f"{self.log_prefix} 并行+串行处理失败: {e}")
@@ -523,7 +523,12 @@ class HeartFChatting:
return False, "" return False, ""
async def _handle_action( async def _handle_action(
self, action: str, reasoning: str, action_data: dict, cycle_timers: dict, planner_start_db_time: float self,
action: str,
reasoning: str,
action_data: dict,
cycle_timers: dict,
thinking_id: str,
) -> tuple[bool, str]: ) -> tuple[bool, str]:
""" """
处理规划动作 处理规划动作
@@ -550,17 +555,17 @@ class HeartFChatting:
try: try:
if action == "reply": if action == "reply":
return await handler(reasoning, action_data, cycle_timers) return await handler(reasoning, action_data, cycle_timers, thinking_id)
else: # no_reply else: # no_reply
return await handler(reasoning, planner_start_db_time, cycle_timers), "" return await handler(reasoning, cycle_timers, thinking_id)
except Exception as e: except Exception as e:
logger.error(f"{self.log_prefix} 处理{action}时出错: {e}") logger.error(f"{self.log_prefix} 处理{action}时出错: {e}")
# 出错时也重置计数器 # 出错时也重置计数器
self._lian_xu_bu_hui_fu_ci_shu = 0 self.total_no_reply_count = 0
self._lian_xu_deng_dai_shi_jian = 0.0 self.total_waiting_time = 0.0
return False, "" return False, ""
async def _handle_no_reply(self, reasoning: str, planner_start_db_time: float, cycle_timers: dict) -> bool: async def _handle_no_reply(self, reasoning: str, cycle_timers: dict, thinking_id: str) -> bool:
""" """
处理不回复的情况 处理不回复的情况
@@ -584,53 +589,50 @@ class HeartFChatting:
try: try:
with Timer("等待新消息", cycle_timers): with Timer("等待新消息", cycle_timers):
# 等待新消息、超时或关闭信号,并获取结果 # 等待新消息、超时或关闭信号,并获取结果
await self._wait_for_new_message(observation, planner_start_db_time, self.log_prefix) await self._wait_for_new_message(observation, thinking_id, self.log_prefix)
# 从计时器获取实际等待时间 # 从计时器获取实际等待时间
current_waiting = cycle_timers.get("等待新消息", 0.0) current_waiting = cycle_timers.get("等待新消息", 0.0)
if not self._shutting_down: if not self._shutting_down:
self._lian_xu_bu_hui_fu_ci_shu += 1 self.total_no_reply_count += 1
self._lian_xu_deng_dai_shi_jian += current_waiting # 累加等待时间 self.total_waiting_time += current_waiting # 累加等待时间
logger.debug( logger.debug(
f"{self.log_prefix} 连续不回复计数增加: {self._lian_xu_bu_hui_fu_ci_shu}/{CONSECUTIVE_NO_REPLY_THRESHOLD}, " f"{self.log_prefix} 连续不回复计数增加: {self.total_no_reply_count}/{CONSECUTIVE_NO_REPLY_THRESHOLD}, "
f"本次等待: {current_waiting:.2f}秒, 累计等待: {self._lian_xu_deng_dai_shi_jian:.2f}" f"本次等待: {current_waiting:.2f}秒, 累计等待: {self.total_waiting_time:.2f}"
) )
# 检查是否同时达到次数和时间阈值 # 检查是否同时达到次数和时间阈值
time_threshold = 0.66 * WAITING_TIME_THRESHOLD * CONSECUTIVE_NO_REPLY_THRESHOLD time_threshold = 0.66 * WAITING_TIME_THRESHOLD * CONSECUTIVE_NO_REPLY_THRESHOLD
if ( if (
self._lian_xu_bu_hui_fu_ci_shu >= CONSECUTIVE_NO_REPLY_THRESHOLD self.total_no_reply_count >= CONSECUTIVE_NO_REPLY_THRESHOLD
and self._lian_xu_deng_dai_shi_jian >= time_threshold and self.total_waiting_time >= time_threshold
): ):
logger.info( logger.info(
f"{self.log_prefix} 连续不回复达到阈值 ({self._lian_xu_bu_hui_fu_ci_shu}次) " f"{self.log_prefix} 连续不回复达到阈值 ({self.total_no_reply_count}次) "
f"且累计等待时间达到 {self._lian_xu_deng_dai_shi_jian:.2f}秒 (阈值 {time_threshold}秒)" f"且累计等待时间达到 {self.total_waiting_time:.2f}秒 (阈值 {time_threshold}秒)"
f"调用回调请求状态转换" f"调用回调请求状态转换"
) )
# 调用回调。注意:这里不重置计数器和时间,依赖回调函数成功改变状态来隐式重置上下文。 # 调用回调。注意:这里不重置计数器和时间,依赖回调函数成功改变状态来隐式重置上下文。
await self.on_consecutive_no_reply_callback() await self.on_consecutive_no_reply_callback()
elif self._lian_xu_bu_hui_fu_ci_shu >= CONSECUTIVE_NO_REPLY_THRESHOLD: elif self.total_no_reply_count >= CONSECUTIVE_NO_REPLY_THRESHOLD:
# 仅次数达到阈值,但时间未达到 # 仅次数达到阈值,但时间未达到
logger.debug( logger.debug(
f"{self.log_prefix} 连续不回复次数达到阈值 ({self._lian_xu_bu_hui_fu_ci_shu}次) " f"{self.log_prefix} 连续不回复次数达到阈值 ({self.total_no_reply_count}次) "
f"但累计等待时间 {self._lian_xu_deng_dai_shi_jian:.2f}秒 未达到时间阈值 ({time_threshold}秒),暂不调用回调" f"但累计等待时间 {self.total_waiting_time:.2f}秒 未达到时间阈值 ({time_threshold}秒),暂不调用回调"
) )
# else: 次数和时间都未达到阈值,不做处理 # else: 次数和时间都未达到阈值,不做处理
return True return True, thinking_id
except asyncio.CancelledError: except asyncio.CancelledError:
# 如果在等待过程中任务被取消(可能是因为 shutdown
logger.info(f"{self.log_prefix} 处理 'no_reply' 时等待被中断 (CancelledError)") logger.info(f"{self.log_prefix} 处理 'no_reply' 时等待被中断 (CancelledError)")
# 让异常向上传播,由 _hfc_loop 的异常处理逻辑接管
raise raise
except Exception as e: # 捕获调用管理器或其他地方可能发生的错误 except Exception as e: # 捕获调用管理器或其他地方可能发生的错误
logger.error(f"{self.log_prefix} 处理 'no_reply' 时发生错误: {e}") logger.error(f"{self.log_prefix} 处理 'no_reply' 时发生错误: {e}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
# 发生意外错误时,可以选择是否重置计数器,这里选择不重置 return False, thinking_id
return False # 表示动作未成功
async def _wait_for_new_message(self, observation, planner_start_db_time: float, log_prefix: str) -> bool: async def _wait_for_new_message(self, observation: ChattingObservation, thinking_id: str, log_prefix: str) -> bool:
""" """
等待新消息 或 检测到关闭信号 等待新消息 或 检测到关闭信号
@@ -650,8 +652,10 @@ class HeartFChatting:
return False # 表示因为关闭而退出 return False # 表示因为关闭而退出
# ----------------------------------- # -----------------------------------
thinking_id_timestamp = parse_thinking_id_to_timestamp(thinking_id)
# 检查新消息 # 检查新消息
if await observation.has_new_messages_since(planner_start_db_time): if await observation.has_new_messages_since(thinking_id_timestamp):
logger.info(f"{log_prefix} 检测到新消息") logger.info(f"{log_prefix} 检测到新消息")
return True return True
@@ -925,38 +929,42 @@ class HeartFChatting:
"llm_error": llm_error, # 返回错误状态 "llm_error": llm_error, # 返回错误状态
} }
async def _handle_reply(self, reasoning: str, reply_data: dict, cycle_timers: dict) -> tuple[bool, str]: async def _handle_reply(
self, reasoning: str, reply_data: dict, cycle_timers: dict, thinking_id: str
) -> tuple[bool, str]:
""" """
处理统一的回复动作 - 可包含文本和表情,顺序任意 处理统一的回复动作 - 可包含文本和表情,顺序任意
reply_data格式: reply_data格式:
{ {
"text": ["你好啊", "今天天气真不错"], # 文本内容列表(可选) "text": "你好啊" # 文本内容列表(可选)
"emojis": ["微笑", "阳光"] # 表情关键词列表(可选) "target": "锚定消息", # 锚定消息的文本内容
"emojis": "微笑" # 表情关键词列表(可选)
} }
""" """
# 重置连续不回复计数器 # 重置连续不回复计数器
self._lian_xu_bu_hui_fu_ci_shu = 0 self.total_no_reply_count = 0
self._lian_xu_deng_dai_shi_jian = 0.0 self.total_waiting_time = 0.0
# 获取锚定消息 # 从聊天观察获取锚定消息
observations: ChattingObservation = self.observations[0] observations: ChattingObservation = self.observations[0]
anchor_message = observations.serch_message_by_text(reply_data["target"]) anchor_message = observations.serch_message_by_text(reply_data["target"])
# 如果没有找到锚点消息,创建一个占位符 # 如果没有找到锚点消息,创建一个占位符
if not anchor_message: if not anchor_message:
logger.info(f"{self.log_prefix} 未找到锚点消息,创建占位符") logger.info(f"{self.log_prefix} 未找到锚点消息,创建占位符")
anchor_message = await _create_empty_anchor_message( anchor_message = await create_empty_anchor_message(
self.chat_stream.platform, self.chat_stream.group_info, self.chat_stream self.chat_stream.platform, self.chat_stream.group_info, self.chat_stream
) )
if not anchor_message:
logger.error(f"{self.log_prefix} 创建占位符失败,无法继续处理回复")
return False, ""
else: else:
anchor_message.update_chat_stream(self.chat_stream) anchor_message.update_chat_stream(self.chat_stream)
success, reply_set = await self.expressor.deal_reply( success, reply_set = await self.expressor.deal_reply(
cycle_timers=cycle_timers, action_data=reply_data, anchor_message=anchor_message, reasoning=reasoning cycle_timers=cycle_timers,
action_data=reply_data,
anchor_message=anchor_message,
reasoning=reasoning,
thinking_id=thinking_id,
) )
reply_text = "" reply_text = ""

View File

@@ -7,6 +7,7 @@ from src.chat.utils.utils import truncate_message
from src.common.logger_manager import get_logger from src.common.logger_manager import get_logger
from src.chat.utils.utils import calculate_typing_time from src.chat.utils.utils import calculate_typing_time
from rich.traceback import install from rich.traceback import install
import traceback
install(extra_lines=3) install(extra_lines=3)
@@ -16,17 +17,16 @@ logger = get_logger("sender")
async def send_message(message: MessageSending) -> None: async def send_message(message: MessageSending) -> None:
"""合并后的消息发送函数包含WS发送和日志记录""" """合并后的消息发送函数包含WS发送和日志记录"""
message_preview = truncate_message(message.processed_plain_text) message_preview = truncate_message(message.processed_plain_text, max_length=40)
try: try:
# 直接调用API发送消息 # 直接调用API发送消息
await global_api.send_message(message) await global_api.send_message(message)
logger.success(f"发送消息 '{message_preview}' 成功") logger.success(f"已将消息 '{message_preview}' 发往平台'{message.message_info.platform}'")
except Exception as e: except Exception as e:
logger.error(f"发送消息 '{message_preview}' 失败: {str(e)}") logger.error(f"发送消息 '{message_preview}' 发往平台'{message.message_info.platform}' 失败: {str(e)}")
if not message.message_info.platform: traceback.print_exc()
raise ValueError(f"未找到平台:{message.message_info.platform} 的url配置请检查配置文件") from e
raise e # 重新抛出其他异常 raise e # 重新抛出其他异常
@@ -66,21 +66,24 @@ class HeartFCSender:
del self.thinking_messages[chat_id] del self.thinking_messages[chat_id]
logger.debug(f"[{chat_id}] Removed empty thinking message container.") logger.debug(f"[{chat_id}] Removed empty thinking message container.")
def is_thinking(self, chat_id: str, message_id: str) -> bool:
"""检查指定的消息 ID 是否当前正处于思考状态。"""
return chat_id in self.thinking_messages and message_id in self.thinking_messages[chat_id]
async def get_thinking_start_time(self, chat_id: str, message_id: str) -> Optional[float]: async def get_thinking_start_time(self, chat_id: str, message_id: str) -> Optional[float]:
"""获取已注册思考消息的开始时间。""" """获取已注册思考消息的开始时间。"""
async with self._thinking_lock: async with self._thinking_lock:
thinking_message = self.thinking_messages.get(chat_id, {}).get(message_id) thinking_message = self.thinking_messages.get(chat_id, {}).get(message_id)
return thinking_message.thinking_start_time if thinking_message else None return thinking_message.thinking_start_time if thinking_message else None
async def type_and_send_message(self, message: MessageSending, typing=False): async def send_message(self, message: MessageSending, has_thinking=False, typing=False):
""" """
立即处理、发送并存储单个 MessageSending 消息。 处理、发送并存储一条消息。
调用此方法前,应先调用 register_thinking 注册对应的思考消息。
此方法执行后会调用 complete_thinking 清理思考状态。 参数:
message: MessageSending 对象,待发送的消息。
has_thinking: 是否管理思考状态,表情包无思考状态(如需调用 register_thinking/complete_thinking
typing: 是否模拟打字等待(根据 has_thinking 控制等待时长)。
用法:
- has_thinking=True 时,自动处理思考消息的时间和清理。
- typing=True 时,发送前会有打字等待。
""" """
if not message.chat_stream: if not message.chat_stream:
logger.error("消息缺少 chat_stream无法发送") logger.error("消息缺少 chat_stream无法发送")
@@ -93,27 +96,29 @@ class HeartFCSender:
message_id = message.message_info.message_id message_id = message.message_info.message_id
try: try:
_ = message.update_thinking_time() if has_thinking:
_ = message.update_thinking_time()
# --- 条件应用 set_reply 逻辑 --- # --- 条件应用 set_reply 逻辑 ---
if ( if (
message.is_head message.is_head
and not message.is_private_message() and not message.is_private_message()
and message.reply.processed_plain_text != "[System Trigger Context]" and message.reply.processed_plain_text != "[System Trigger Context]"
): ):
logger.debug(f"[{chat_id}] 应用 set_reply 逻辑: {message.processed_plain_text[:20]}...") logger.debug(f"[{chat_id}] 应用 set_reply 逻辑: {message.processed_plain_text[:20]}...")
message.set_reply(message.reply)
# --- 结束条件 set_reply ---
await message.process() await message.process()
if typing: if typing:
typing_time = calculate_typing_time( if has_thinking:
input_string=message.processed_plain_text, typing_time = calculate_typing_time(
thinking_start_time=message.thinking_start_time, input_string=message.processed_plain_text,
is_emoji=message.is_emoji, thinking_start_time=message.thinking_start_time,
) is_emoji=message.is_emoji,
await asyncio.sleep(typing_time) )
await asyncio.sleep(typing_time)
else:
await asyncio.sleep(0.5)
await send_message(message) await send_message(message)
await self.storage.store_message(message, message.chat_stream) await self.storage.store_message(message, message.chat_stream)
@@ -123,30 +128,3 @@ class HeartFCSender:
raise e raise e
finally: finally:
await self.complete_thinking(chat_id, message_id) await self.complete_thinking(chat_id, message_id)
async def send_and_store(self, message: MessageSending):
"""处理、发送并存储单个消息,不涉及思考状态管理。"""
if not message.chat_stream:
logger.error(f"[{message.message_info.platform or 'UnknownPlatform'}] 消息缺少 chat_stream无法发送")
return
if not message.message_info or not message.message_info.message_id:
logger.error(
f"[{message.chat_stream.stream_id if message.chat_stream else 'UnknownStream'}] 消息缺少 message_info 或 message_id无法发送"
)
return
chat_id = message.chat_stream.stream_id
message_id = message.message_info.message_id # 获取消息ID用于日志
try:
await message.process()
await asyncio.sleep(0.5)
await send_message(message) # 使用现有的发送方法
await self.storage.store_message(message, message.chat_stream) # 使用现有的存储方法
except Exception as e:
logger.error(f"[{chat_id}] 处理或存储消息 {message_id} 时出错: {e}")
# 重新抛出异常,让调用者知道失败了
raise e

View File

@@ -9,7 +9,8 @@ from maim_message import Seg
from src.chat.heart_flow.heartflow import heartflow from src.chat.heart_flow.heartflow import heartflow
from src.common.logger_manager import get_logger from src.common.logger_manager import get_logger
from ..message_receive.chat_stream import chat_manager from ..message_receive.chat_stream import chat_manager
from ..message_receive.message_buffer import message_buffer
# from ..message_receive.message_buffer import message_buffer
from ..utils.timer_calculator import Timer from ..utils.timer_calculator import Timer
from src.chat.person_info.relationship_manager import relationship_manager from src.chat.person_info.relationship_manager import relationship_manager
from typing import Optional, Tuple, Dict, Any from typing import Optional, Tuple, Dict, Any
@@ -169,7 +170,7 @@ class HeartFCProcessor:
messageinfo = message.message_info messageinfo = message.message_info
# 2. 消息缓冲与流程序化 # 2. 消息缓冲与流程序化
await message_buffer.start_caching_messages(message) # await message_buffer.start_caching_messages(message)
chat = await chat_manager.get_or_create_stream( chat = await chat_manager.get_or_create_stream(
platform=messageinfo.platform, platform=messageinfo.platform,
@@ -188,16 +189,16 @@ class HeartFCProcessor:
return return
# 4. 缓冲检查 # 4. 缓冲检查
buffer_result = await message_buffer.query_buffer_result(message) # buffer_result = await message_buffer.query_buffer_result(message)
if not buffer_result: # if not buffer_result:
msg_type = _get_message_type(message) # msg_type = _get_message_type(message)
type_messages = { # type_messages = {
"text": f"触发缓冲,消息:{message.processed_plain_text}", # "text": f"触发缓冲,消息:{message.processed_plain_text}",
"image": "触发缓冲,表情包/图片等待中", # "image": "触发缓冲,表情包/图片等待中",
"seglist": "触发缓冲,消息列表等待中", # "seglist": "触发缓冲,消息列表等待中",
} # }
logger.debug(type_messages.get(msg_type, "触发未知类型缓冲")) # logger.debug(type_messages.get(msg_type, "触发未知类型缓冲"))
return # return
# 5. 消息存储 # 5. 消息存储
await self.storage.store_message(message, chat) await self.storage.store_message(message, chat)
@@ -210,7 +211,7 @@ class HeartFCProcessor:
# 7. 日志记录 # 7. 日志记录
mes_name = chat.group_info.group_name if chat.group_info else "私聊" mes_name = chat.group_info.group_name if chat.group_info else "私聊"
current_time = time.strftime("%H%M%S", time.localtime(message.message_info.time)) current_time = time.strftime("%H:%M:%S", time.localtime(message.message_info.time))
logger.info( logger.info(
f"[{current_time}][{mes_name}]" f"[{current_time}][{mes_name}]"
f"{userinfo.user_nickname}:" f"{userinfo.user_nickname}:"

View File

@@ -32,11 +32,11 @@ def init_prompt():
以上是聊天内容,你需要了解聊天记录中的内容 以上是聊天内容,你需要了解聊天记录中的内容
{chat_target} {chat_target}
你的名字是{bot_name}{prompt_personality},在这聊天中,"{target_message}"引起了你的注意,你想表达:{in_mind_reply},原因是:{reason}。你现在要思考怎么回复 你的名字是{bot_name}{prompt_personality},在这聊天中,"{target_message}"引起了你的注意,对这句话,你想表达:{in_mind_reply},原因是:{reason}。你现在要思考怎么回复
你需要使用合适的语法和句法,参考聊天内容,组织一条日常且口语化的回复。 你需要使用合适的语法和句法,参考聊天内容,组织一条日常且口语化的回复。
请你根据情景使用以下句法: 请你根据情景使用以下句法:
{grammar_habbits} {grammar_habbits}
回复尽量简短一些。可以参考贴吧,知乎和微博的回复风格,你可以完全重组回复,保留最基本的表达含义就好,但注意回复要简短。 回复尽量简短一些。可以参考贴吧,知乎和微博的回复风格,你可以完全重组回复,保留最基本的表达含义就好,但注意回复要简短,但重组后保持语意通顺
回复不要浮夸,不要用夸张修辞,平淡一些。不要输出多余内容(包括前后缀冒号和引号括号表情包at或 @等 ),只输出一条回复就好。 回复不要浮夸,不要用夸张修辞,平淡一些。不要输出多余内容(包括前后缀冒号和引号括号表情包at或 @等 ),只输出一条回复就好。
现在,你说: 现在,你说:
""", """,

View File

@@ -1,5 +1,4 @@
import time import time
import traceback
from typing import Optional from typing import Optional
from src.chat.message_receive.message import MessageRecv, BaseMessageInfo from src.chat.message_receive.message import MessageRecv, BaseMessageInfo
from src.chat.message_receive.chat_stream import ChatStream from src.chat.message_receive.chat_stream import ChatStream
@@ -10,7 +9,7 @@ import json
logger = get_logger(__name__) logger = get_logger(__name__)
async def _create_empty_anchor_message( async def create_empty_anchor_message(
platform: str, group_info: dict, chat_stream: ChatStream platform: str, group_info: dict, chat_stream: ChatStream
) -> Optional[MessageRecv]: ) -> Optional[MessageRecv]:
""" """
@@ -18,31 +17,36 @@ async def _create_empty_anchor_message(
如果重构失败或观察为空,则创建一个占位符。 如果重构失败或观察为空,则创建一个占位符。
""" """
try: placeholder_id = f"mid_pf_{int(time.time() * 1000)}"
placeholder_id = f"mid_pf_{int(time.time() * 1000)}" placeholder_user = UserInfo(user_id="system_trigger", user_nickname="System Trigger", platform=platform)
placeholder_user = UserInfo(user_id="system_trigger", user_nickname="System Trigger", platform=platform) placeholder_msg_info = BaseMessageInfo(
placeholder_msg_info = BaseMessageInfo( message_id=placeholder_id,
message_id=placeholder_id, platform=platform,
platform=platform, group_info=group_info,
group_info=group_info, user_info=placeholder_user,
user_info=placeholder_user, time=time.time(),
time=time.time(), )
) placeholder_msg_dict = {
placeholder_msg_dict = { "message_info": placeholder_msg_info.to_dict(),
"message_info": placeholder_msg_info.to_dict(), "processed_plain_text": "[System Trigger Context]",
"processed_plain_text": "[System Trigger Context]", "raw_message": "",
"raw_message": "", "time": placeholder_msg_info.time,
"time": placeholder_msg_info.time, }
} anchor_message = MessageRecv(placeholder_msg_dict)
anchor_message = MessageRecv(placeholder_msg_dict) anchor_message.update_chat_stream(chat_stream)
anchor_message.update_chat_stream(chat_stream)
logger.debug(f"创建占位符锚点消息: ID={anchor_message.message_info.message_id}")
return anchor_message
except Exception as e: return anchor_message
logger.error(f"Error getting/creating anchor message: {e}")
logger.error(traceback.format_exc())
return None def parse_thinking_id_to_timestamp(thinking_id: str) -> float:
"""
将形如 'tid<timestamp>' 的 thinking_id 解析回 float 时间戳
例如: 'tid1718251234.56' -> 1718251234.56
"""
if not thinking_id.startswith("tid"):
raise ValueError("thinking_id 格式不正确")
ts_str = thinking_id[3:]
return float(ts_str)
def get_keywords_from_json(json_str: str) -> list[str]: def get_keywords_from_json(json_str: str) -> list[str]:

View File

@@ -94,13 +94,23 @@ class RelationshipManager:
return False return False
@staticmethod @staticmethod
async def first_knowing_some_one(platform, user_id, user_nickname, user_cardname, user_avatar): async def first_knowing_some_one(
platform: str, user_id: str, user_nickname: str, user_cardname: str, user_avatar: str
):
"""判断是否认识某人""" """判断是否认识某人"""
person_id = person_info_manager.get_person_id(platform, user_id) person_id = person_info_manager.get_person_id(platform, user_id)
await person_info_manager.update_one_field(person_id, "nickname", user_nickname) data = {
# await person_info_manager.update_one_field(person_id, "user_cardname", user_cardname) "platform": platform,
# await person_info_manager.update_one_field(person_id, "user_avatar", user_avatar) "user_id": user_id,
await person_info_manager.qv_person_name(person_id, user_nickname, user_cardname, user_avatar) "nickname": user_nickname,
"konw_time": int(time.time()),
}
await person_info_manager.update_one_field(
person_id=person_id, field_name="nickname", value=user_nickname, data=data
)
await person_info_manager.qv_person_name(
person_id=person_id, user_nickname=user_nickname, user_cardname=user_cardname, user_avatar=user_avatar
)
async def calculate_update_relationship_value(self, user_info: UserInfo, platform: str, label: str, stance: str): async def calculate_update_relationship_value(self, user_info: UserInfo, platform: str, label: str, stance: str):
"""计算并变更关系值 """计算并变更关系值

View File

@@ -622,12 +622,6 @@ class BotConfig:
# config.ban_user_id = set(groups_config.get("ban_user_id", [])) # config.ban_user_id = set(groups_config.get("ban_user_id", []))
config.ban_user_id = set(str(user) for user in groups_config.get("ban_user_id", [])) config.ban_user_id = set(str(user) for user in groups_config.get("ban_user_id", []))
def platforms(parent: dict):
platforms_config = parent["platforms"]
if platforms_config and isinstance(platforms_config, dict):
for k in platforms_config.keys():
config.api_urls[k] = platforms_config[k]
def experimental(parent: dict): def experimental(parent: dict):
experimental_config = parent["experimental"] experimental_config = parent["experimental"]
config.enable_friend_chat = experimental_config.get("enable_friend_chat", config.enable_friend_chat) config.enable_friend_chat = experimental_config.get("enable_friend_chat", config.enable_friend_chat)
@@ -662,7 +656,6 @@ class BotConfig:
"remote": {"func": remote, "support": ">=0.0.10", "necessary": False}, "remote": {"func": remote, "support": ">=0.0.10", "necessary": False},
"keywords_reaction": {"func": keywords_reaction, "support": ">=0.0.2", "necessary": False}, "keywords_reaction": {"func": keywords_reaction, "support": ">=0.0.2", "necessary": False},
"chinese_typo": {"func": chinese_typo, "support": ">=0.0.3", "necessary": False}, "chinese_typo": {"func": chinese_typo, "support": ">=0.0.3", "necessary": False},
"platforms": {"func": platforms, "support": ">=1.0.0"},
"response_splitter": {"func": response_splitter, "support": ">=0.0.11", "necessary": False}, "response_splitter": {"func": response_splitter, "support": ">=0.0.11", "necessary": False},
"experimental": {"func": experimental, "support": ">=0.0.11", "necessary": False}, "experimental": {"func": experimental, "support": ">=0.0.11", "necessary": False},
"chat": {"func": chat, "support": ">=1.6.0", "necessary": False}, "chat": {"func": chat, "support": ">=1.6.0", "necessary": False},

View File

@@ -59,7 +59,7 @@ gender = "男" # 性别
appearance = "用几句话描述外貌特征" # 外貌特征 该选项还在调试中,暂时未生效 appearance = "用几句话描述外貌特征" # 外貌特征 该选项还在调试中,暂时未生效
[platforms] # 必填项目,填写每个平台适配器提供的链接 [platforms] # 必填项目,填写每个平台适配器提供的链接
nonebot-qq="http://127.0.0.1:18002/api/message" qq="http://127.0.0.1:18002/api/message"
[chat] #麦麦的聊天通用设置 [chat] #麦麦的聊天通用设置
allow_focus_mode = false # 是否允许专注聊天状态 allow_focus_mode = false # 是否允许专注聊天状态