From cfdaf00559deb369de238c8e73c360c90ad0e36d Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 17 Apr 2025 15:29:20 +0800 Subject: [PATCH] feat: wonderful new --- interest_monitor_gui.py | 244 +++++++++ src/main.py | 10 + src/plugins/chat/bot.py | 11 +- .../chat_module/heartFC_chat/heartFC_chat.py | 497 ++++++++---------- .../heartFC_chat/heartFC_processor.py | 170 ++++++ .../chat_module/heartFC_chat/interest.py | 370 +++++++++++++ .../chat_module/heartFC_chat/messagesender.py | 25 +- src/plugins/memory_system/Hippocampus.py | 8 +- src/plugins/person_info/person_info.py | 1 + 9 files changed, 1032 insertions(+), 304 deletions(-) create mode 100644 interest_monitor_gui.py create mode 100644 src/plugins/chat_module/heartFC_chat/heartFC_processor.py create mode 100644 src/plugins/chat_module/heartFC_chat/interest.py diff --git a/interest_monitor_gui.py b/interest_monitor_gui.py new file mode 100644 index 000000000..e28b6f7ac --- /dev/null +++ b/interest_monitor_gui.py @@ -0,0 +1,244 @@ +import tkinter as tk +from tkinter import ttk +import time +import os +from datetime import datetime +import random +from collections import deque +import json # 引入 json + +# --- 引入 Matplotlib --- +import matplotlib.pyplot as plt +from matplotlib.figure import Figure +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +import matplotlib.dates as mdates # 用于处理日期格式 +import matplotlib # 导入 matplotlib + +# --- 配置 --- +LOG_FILE_PATH = os.path.join("logs", "interest", "interest_history.log") # 指向历史日志文件 +REFRESH_INTERVAL_MS = 200 # 刷新间隔 (毫秒) - 可以适当调长,因为读取文件可能耗时 +WINDOW_TITLE = "Interest Monitor (Live History)" +MAX_HISTORY_POINTS = 1000 # 图表上显示的最大历史点数 (可以增加) +MAX_STREAMS_TO_DISPLAY = 15 # 最多显示多少个聊天流的折线图 (可以增加) + +# *** 添加 Matplotlib 中文字体配置 *** +# 尝试使用 'SimHei' 或 'Microsoft YaHei',如果找不到,matplotlib 会回退到默认字体 +# 确保你的系统上安装了这些字体 +matplotlib.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei'] +matplotlib.rcParams['axes.unicode_minus'] = False # 解决负号'-'显示为方块的问题 + +class InterestMonitorApp: + def __init__(self, root): + self.root = root + self.root.title(WINDOW_TITLE) + self.root.geometry("1800x800") # 调整窗口大小以适应图表 + + # --- 数据存储 --- + # 使用 deque 来存储有限的历史数据点 + # key: stream_id, value: deque([(timestamp, interest_level), ...]) + self.stream_history = {} + self.stream_colors = {} # 为每个 stream 分配颜色 + self.stream_display_names = {} # *** New: Store display names (group_name) *** + + # --- UI 元素 --- + # 状态标签 + self.status_label = tk.Label(root, text="Initializing...", anchor="w", fg="grey") + self.status_label.pack(side=tk.BOTTOM, fill=tk.X, padx=5, pady=2) + + # Matplotlib 图表设置 + self.fig = Figure(figsize=(5, 4), dpi=100) + self.ax = self.fig.add_subplot(111) + # 配置在 update_plot 中进行,避免重复 + + # 创建 Tkinter 画布嵌入 Matplotlib 图表 + self.canvas = FigureCanvasTkAgg(self.fig, master=root) + self.canvas_widget = self.canvas.get_tk_widget() + self.canvas_widget.pack(side=tk.TOP, fill=tk.BOTH, expand=1) + + # --- 初始化和启动刷新 --- + self.update_display() # 首次加载并开始刷新循环 + + def get_random_color(self): + """生成随机颜色用于区分线条""" + return "#{:06x}".format(random.randint(0, 0xFFFFFF)) + + def load_and_update_history(self): + """从 history log 文件加载数据并更新历史记录""" + if not os.path.exists(LOG_FILE_PATH): + self.set_status(f"Error: Log file not found at {LOG_FILE_PATH}", "red") + # 如果文件不存在,不清空现有数据,以便显示最后一次成功读取的状态 + return + + # *** Reset display names each time we reload *** + new_stream_history = {} + new_stream_display_names = {} + read_count = 0 + error_count = 0 + # *** Calculate the timestamp threshold for the last 30 minutes *** + current_time = time.time() + time_threshold = current_time - (15 * 60) # 30 minutes in seconds + + try: + with open(LOG_FILE_PATH, 'r', encoding='utf-8') as f: + for line in f: + read_count += 1 + try: + log_entry = json.loads(line.strip()) + timestamp = log_entry.get("timestamp") + + # *** Add time filtering *** + if timestamp is None or float(timestamp) < time_threshold: + continue # Skip old or invalid entries + + stream_id = log_entry.get("stream_id") + interest_level = log_entry.get("interest_level") + group_name = log_entry.get("group_name", stream_id) # *** Get group_name, fallback to stream_id *** + + # *** Check other required fields AFTER time filtering *** + if stream_id is None or interest_level is None: + error_count += 1 + continue # 跳过无效行 + + # 如果是第一次读到这个 stream_id,则创建 deque + if stream_id not in new_stream_history: + new_stream_history[stream_id] = deque(maxlen=MAX_HISTORY_POINTS) + # 检查是否已有颜色,没有则分配 + if stream_id not in self.stream_colors: + self.stream_colors[stream_id] = self.get_random_color() + + # *** Store the latest display name found for this stream_id *** + new_stream_display_names[stream_id] = group_name + + # 添加数据点 + new_stream_history[stream_id].append((float(timestamp), float(interest_level))) + + except json.JSONDecodeError: + error_count += 1 + # logger.warning(f"Skipping invalid JSON line: {line.strip()}") + continue # 跳过无法解析的行 + except (TypeError, ValueError) as e: + error_count += 1 + # logger.warning(f"Skipping line due to data type error ({e}): {line.strip()}") + continue # 跳过数据类型错误的行 + + # 读取完成后,用新数据替换旧数据 + self.stream_history = new_stream_history + self.stream_display_names = new_stream_display_names # *** Update display names *** + status_msg = f"Data loaded at {datetime.now().strftime('%H:%M:%S')}. Lines read: {read_count}." + if error_count > 0: + status_msg += f" Skipped {error_count} invalid lines." + self.set_status(status_msg, "orange") + else: + self.set_status(status_msg, "green") + + except IOError as e: + self.set_status(f"Error reading file {LOG_FILE_PATH}: {e}", "red") + except Exception as e: + self.set_status(f"An unexpected error occurred during loading: {e}", "red") + + + def update_plot(self): + """更新 Matplotlib 图表""" + self.ax.clear() # 清除旧图 + # *** 设置中文标题和标签 *** + self.ax.set_title("兴趣度随时间变化图") + self.ax.set_xlabel("时间") + self.ax.set_ylabel("兴趣度") + self.ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S')) + self.ax.grid(True) + self.ax.set_ylim(0, 10) # 固定 Y 轴范围 0-10 + + # 只绘制最新的 N 个 stream (按最后记录的兴趣度排序) + # 注意:现在是基于文件读取的快照排序,可能不是实时最新 + active_streams = sorted( + self.stream_history.items(), + key=lambda item: item[1][-1][1] if item[1] else 0, # 按最后兴趣度排序 + reverse=True + )[:MAX_STREAMS_TO_DISPLAY] + + all_times = [] # 用于确定 X 轴范围 + + for stream_id, history in active_streams: + if not history: + continue + + timestamps, interests = zip(*history) + # 将 time.time() 时间戳转换为 matplotlib 可识别的日期格式 + try: + mpl_dates = [datetime.fromtimestamp(ts) for ts in timestamps] + all_times.extend(mpl_dates) # 收集所有时间点 + + # *** Use display name for label *** + display_label = self.stream_display_names.get(stream_id, stream_id) + + self.ax.plot( + mpl_dates, + interests, + label=display_label, # *** Use display_label *** + color=self.stream_colors.get(stream_id, 'grey'), + marker='.', + markersize=3, + linestyle='-', + linewidth=1 + ) + except ValueError as e: + print(f"Skipping plot for {stream_id} due to invalid timestamp: {e}") + continue + + if all_times: + # 根据数据动态调整 X 轴范围,留一点边距 + min_time = min(all_times) + max_time = max(all_times) + # delta = max_time - min_time + # self.ax.set_xlim(min_time - delta * 0.05, max_time + delta * 0.05) + self.ax.set_xlim(min_time, max_time) + + # 自动格式化X轴标签 + self.fig.autofmt_xdate() + else: + # 如果没有数据,设置一个默认的时间范围,例如最近一小时 + now = datetime.now() + one_hour_ago = now - timedelta(hours=1) + self.ax.set_xlim(one_hour_ago, now) + + + # 添加图例 + if active_streams: + # 调整图例位置和大小 + # 字体已通过全局 matplotlib.rcParams 设置 + self.ax.legend(loc='upper left', bbox_to_anchor=(1.02, 1), borderaxespad=0., fontsize='x-small') + # 调整布局,确保图例不被裁剪 + self.fig.tight_layout(rect=[0, 0, 0.85, 1]) # 右侧留出空间给图例 + + + self.canvas.draw() # 重绘画布 + + def update_display(self): + """主更新循环""" + try: + self.load_and_update_history() # 从文件加载数据并更新内部状态 + self.update_plot() # 根据内部状态更新图表 + except Exception as e: + # 提供更详细的错误信息 + import traceback + error_msg = f"Error during update: {e}\n{traceback.format_exc()}" + self.set_status(error_msg, "red") + print(error_msg) # 打印详细错误到控制台 + + # 安排下一次刷新 + self.root.after(REFRESH_INTERVAL_MS, self.update_display) + + def set_status(self, message: str, color: str = "grey"): + """更新状态栏标签""" + # 限制状态栏消息长度 + max_len = 150 + display_message = (message[:max_len] + '...') if len(message) > max_len else message + self.status_label.config(text=display_message, fg=color) + + +if __name__ == "__main__": + # 导入 timedelta 用于默认时间范围 + from datetime import timedelta + root = tk.Tk() + app = InterestMonitorApp(root) + root.mainloop() \ No newline at end of file diff --git a/src/main.py b/src/main.py index 7b571b3aa..898096a9a 100644 --- a/src/main.py +++ b/src/main.py @@ -17,6 +17,7 @@ from .common.logger import get_module_logger from .plugins.remote import heartbeat_thread # noqa: F401 from .individuality.individuality import Individuality from .common.server import global_server +from .plugins.chat_module.heartFC_chat.interest import InterestManager logger = get_module_logger("main") @@ -110,6 +111,15 @@ class MainSystem: asyncio.create_task(heartflow.heartflow_start_working()) logger.success("心流系统启动成功") + # 启动 InterestManager 的后台任务 + interest_manager = InterestManager() # 获取单例 + await interest_manager.start_background_tasks() + logger.success("InterestManager 后台任务启动成功") + + # 启动 HeartFC_Chat 的后台任务(例如兴趣监控) + await chat_bot.heartFC_chat.start() + logger.success("HeartFC_Chat 模块启动成功") + init_time = int(1000 * (time.time() - init_start_time)) logger.success(f"初始化完成,神经元放电{init_time}次") except Exception as e: diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index b7191d846..a4a843cb9 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -8,6 +8,8 @@ from ..chat_module.only_process.only_message_process import MessageProcessor from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig from ..chat_module.think_flow_chat.think_flow_chat import ThinkFlowChat from ..chat_module.reasoning_chat.reasoning_chat import ReasoningChat +from ..chat_module.heartFC_chat.heartFC_chat import HeartFC_Chat +from ..chat_module.heartFC_chat.heartFC_processor import HeartFC_Processor from ..utils.prompt_builder import Prompt, global_prompt_manager import traceback @@ -30,6 +32,8 @@ class ChatBot: self.mood_manager.start_mood_update() # 启动情绪更新 self.think_flow_chat = ThinkFlowChat() self.reasoning_chat = ReasoningChat() + self.heartFC_chat = HeartFC_Chat() + self.heartFC_processor = HeartFC_Processor(self.heartFC_chat) self.only_process_chat = MessageProcessor() # 创建初始化PFC管理器的任务,会在_ensure_started时执行 @@ -117,7 +121,12 @@ class ChatBot: if groupinfo.group_id in global_config.talk_allowed_groups: # logger.debug(f"开始群聊模式{str(message_data)[:50]}...") if global_config.response_mode == "heart_flow": - await self.think_flow_chat.process_message(message_data) + # logger.info(f"启动最新最好的思维流FC模式{str(message_data)[:50]}...") + + await self.heartFC_processor.process_message(message_data) + + + elif global_config.response_mode == "reasoning": # logger.debug(f"开始推理模式{str(message_data)[:50]}...") await self.reasoning_chat.process_message(message_data) diff --git a/src/plugins/chat_module/heartFC_chat/heartFC_chat.py b/src/plugins/chat_module/heartFC_chat/heartFC_chat.py index 44366c615..4020f9ba3 100644 --- a/src/plugins/chat_module/heartFC_chat/heartFC_chat.py +++ b/src/plugins/chat_module/heartFC_chat/heartFC_chat.py @@ -1,27 +1,23 @@ import time from random import random import traceback -from typing import List -from ...memory_system.Hippocampus import HippocampusManager +from typing import List, Optional +import asyncio from ...moods.moods import MoodManager from ....config.config import global_config from ...chat.emoji_manager import emoji_manager from .heartFC__generator import ResponseGenerator from ...chat.message import MessageSending, MessageRecv, MessageThinking, MessageSet from .messagesender import MessageManager -from ...storage.storage import MessageStorage -from ...chat.utils import is_mentioned_bot_in_message, get_recent_group_detailed_plain_text from ...chat.utils_image import image_path_to_base64 -from ...willing.willing_manager import willing_manager from ...message import UserInfo, Seg from src.heart_flow.heartflow import heartflow from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig -from ...chat.chat_stream import chat_manager from ...person_info.relationship_manager import relationship_manager -from ...chat.message_buffer import message_buffer from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager from ...utils.timer_calculater import Timer from src.do_tool.tool_use import ToolUser +from .interest import InterestManager, InterestChatting # 定义日志配置 chat_config = LogConfig( @@ -29,19 +25,103 @@ chat_config = LogConfig( file_format=CHAT_STYLE_CONFIG["file_format"], ) -logger = get_module_logger("think_flow_chat", config=chat_config) +logger = get_module_logger("heartFC_chat", config=chat_config) +# 新增常量 +INTEREST_LEVEL_REPLY_THRESHOLD = 4.0 +INTEREST_MONITOR_INTERVAL_SECONDS = 1 -class ThinkFlowChat: +class HeartFC_Chat: def __init__(self): - self.storage = MessageStorage() self.gpt = ResponseGenerator() self.mood_manager = MoodManager.get_instance() self.mood_manager.start_mood_update() self.tool_user = ToolUser() + self.interest_manager = InterestManager() + self._interest_monitor_task: Optional[asyncio.Task] = None - async def _create_thinking_message(self, message, chat, userinfo, messageinfo): - """创建思考消息""" + async def start(self): + """Starts asynchronous tasks like the interest monitor.""" + logger.info("HeartFC_Chat starting asynchronous tasks...") + await self.interest_manager.start_background_tasks() + self._initialize_monitor_task() + logger.info("HeartFC_Chat asynchronous tasks started.") + + def _initialize_monitor_task(self): + """启动后台兴趣监控任务""" + if self._interest_monitor_task is None or self._interest_monitor_task.done(): + try: + loop = asyncio.get_running_loop() + self._interest_monitor_task = loop.create_task(self._interest_monitor_loop()) + logger.info(f"Interest monitor task created. Interval: {INTEREST_MONITOR_INTERVAL_SECONDS}s, Level Threshold: {INTEREST_LEVEL_REPLY_THRESHOLD}") + except RuntimeError: + logger.error("Failed to create interest monitor task: No running event loop.") + raise + else: + logger.warning("Interest monitor task creation skipped: already running or exists.") + + async def _interest_monitor_loop(self): + """后台任务,定期检查兴趣度变化并触发回复""" + logger.info("Interest monitor loop starting...") + await asyncio.sleep(0.3) + while True: + await asyncio.sleep(INTEREST_MONITOR_INTERVAL_SECONDS) + try: + interest_items_snapshot: List[tuple[str, InterestChatting]] = [] + stream_ids = list(self.interest_manager.interest_dict.keys()) + for stream_id in stream_ids: + chatting_instance = self.interest_manager.get_interest_chatting(stream_id) + if chatting_instance: + interest_items_snapshot.append((stream_id, chatting_instance)) + + for stream_id, chatting_instance in interest_items_snapshot: + triggering_message = chatting_instance.last_triggering_message + current_interest = chatting_instance.get_interest() + + # 添加调试日志,检查触发条件 + # logger.debug(f"[兴趣监控][{stream_id}] 当前兴趣: {current_interest:.2f}, 阈值: {INTEREST_LEVEL_REPLY_THRESHOLD}, 触发消息存在: {triggering_message is not None}") + + if current_interest > INTEREST_LEVEL_REPLY_THRESHOLD and triggering_message is not None: + logger.info(f"[{stream_id}] 检测到高兴趣度 ({current_interest:.2f} > {INTEREST_LEVEL_REPLY_THRESHOLD}). 基于消息 ID: {triggering_message.message_info.message_id} 的上下文触发回复") # 更新日志信息使其更清晰 + + chatting_instance.reset_trigger_info() + logger.debug(f"[{stream_id}] Trigger info reset before starting reply task.") + + asyncio.create_task(self._process_triggered_reply(stream_id, triggering_message)) + + except asyncio.CancelledError: + logger.info("Interest monitor loop cancelled.") + break + except Exception as e: + logger.error(f"Error in interest monitor loop: {e}") + logger.error(traceback.format_exc()) + await asyncio.sleep(5) + + async def _process_triggered_reply(self, stream_id: str, triggering_message: MessageRecv): + """Helper coroutine to handle the processing of a triggered reply based on interest level.""" + try: + logger.info(f"[{stream_id}] Starting level-triggered reply generation for message ID: {triggering_message.message_info.message_id}...") + await self.trigger_reply_generation(triggering_message) + + # 在回复处理后降低兴趣度,降低固定值:新阈值的一半 + decrease_value = INTEREST_LEVEL_REPLY_THRESHOLD / 2 + self.interest_manager.decrease_interest(stream_id, value=decrease_value) + post_trigger_interest = self.interest_manager.get_interest(stream_id) + # 更新日志以反映降低的是基于新阈值的固定值 + logger.info(f"[{stream_id}] Interest decreased by fixed value {decrease_value:.2f} (LevelThreshold/2) after processing level-triggered reply. Current interest: {post_trigger_interest:.2f}") + + except Exception as e: + logger.error(f"Error processing level-triggered reply for stream_id {stream_id}, context message_id {triggering_message.message_info.message_id}: {e}") + logger.error(traceback.format_exc()) + + async def _create_thinking_message(self, message: MessageRecv): + """创建思考消息 (从 message 获取信息)""" + chat = message.chat_stream + if not chat: + logger.error(f"Cannot create thinking message, chat_stream is None for message ID: {message.message_info.message_id}") + return None + userinfo = message.message_info.user_info # 发起思考的用户(即原始消息发送者) + messageinfo = message.message_info # 原始消息信息 bot_user_info = UserInfo( user_id=global_config.BOT_QQ, user_nickname=global_config.BOT_NICKNAME, @@ -53,8 +133,8 @@ class ThinkFlowChat: thinking_message = MessageThinking( message_id=thinking_id, chat_stream=chat, - bot_user_info=bot_user_info, - reply=message, + bot_user_info=bot_user_info, # 思考消息的发出者是 bot + reply=message, # 回复的是原始消息 thinking_start_time=thinking_time_point, ) @@ -62,24 +142,21 @@ class ThinkFlowChat: return thinking_id - async def _send_response_messages(self, message, chat, response_set: List[str], thinking_id) -> MessageSending: - """发送回复消息""" + async def _send_response_messages(self, message: MessageRecv, response_set: List[str], thinking_id) -> MessageSending: + chat = message.chat_stream container = MessageManager().get_container(chat.stream_id) thinking_message = None - for msg in container.messages: if isinstance(msg, MessageThinking) and msg.message_info.message_id == thinking_id: thinking_message = msg container.messages.remove(msg) break - if not thinking_message: logger.warning("未找到对应的思考消息,可能已超时被移除") return None thinking_start_time = thinking_message.thinking_start_time message_set = MessageSet(chat, thinking_id) - mark_head = False first_bot_msg = None for msg in response_set: @@ -90,11 +167,11 @@ class ThinkFlowChat: bot_user_info=UserInfo( user_id=global_config.BOT_QQ, user_nickname=global_config.BOT_NICKNAME, - platform=message.message_info.platform, + platform=message.message_info.platform, # 从传入的 message 获取 platform ), - sender_info=message.message_info.user_info, + sender_info=message.message_info.user_info, # 发送给谁 message_segment=message_segment, - reply=message, + reply=message, # 回复原始消息 is_head=not mark_head, is_emoji=False, thinking_start_time=thinking_start_time, @@ -102,24 +179,22 @@ class ThinkFlowChat: if not mark_head: mark_head = True first_bot_msg = bot_message - - # print(f"thinking_start_time:{bot_message.thinking_start_time}") message_set.add_message(bot_message) MessageManager().add_message(message_set) return first_bot_msg - async def _handle_emoji(self, message, chat, response, send_emoji=""): - """处理表情包""" + async def _handle_emoji(self, message: MessageRecv, response_set, send_emoji=""): + """处理表情包 (从 message 获取信息)""" + chat = message.chat_stream if send_emoji: emoji_raw = await emoji_manager.get_emoji_for_text(send_emoji) else: - emoji_raw = await emoji_manager.get_emoji_for_text(response) + emoji_text_source = "".join(response_set) if response_set else "" + emoji_raw = await emoji_manager.get_emoji_for_text(emoji_text_source) if emoji_raw: emoji_path, description = emoji_raw emoji_cq = image_path_to_base64(emoji_path) - thinking_time_point = round(message.message_info.time, 2) - message_segment = Seg(type="emoji", data=emoji_cq) bot_message = MessageSending( message_id="mt" + str(thinking_time_point), @@ -129,13 +204,12 @@ class ThinkFlowChat: user_nickname=global_config.BOT_NICKNAME, platform=message.message_info.platform, ), - sender_info=message.message_info.user_info, + sender_info=message.message_info.user_info, # 发送给谁 message_segment=message_segment, - reply=message, + reply=message, # 回复原始消息 is_head=False, is_emoji=True, ) - MessageManager().add_message(bot_message) async def _update_relationship(self, message: MessageRecv, response_set): @@ -147,281 +221,144 @@ class ThinkFlowChat: ) self.mood_manager.update_mood_from_emotion(emotion, global_config.mood_intensity_factor) - async def process_message(self, message_data: str) -> None: - """处理消息并生成回复""" - timing_results = {} - response_set = None - - message = MessageRecv(message_data) - groupinfo = message.message_info.group_info + async def trigger_reply_generation(self, message: MessageRecv): + """根据意愿阈值触发的实际回复生成和发送逻辑 (V3 - 简化参数)""" + chat = message.chat_stream userinfo = message.message_info.user_info messageinfo = message.message_info - # 消息加入缓冲池 - await message_buffer.start_caching_messages(message) + timing_results = {} + response_set = None + thinking_id = None + info_catcher = None - # 创建聊天流 - chat = await chat_manager.get_or_create_stream( - platform=messageinfo.platform, - user_info=userinfo, - group_info=groupinfo, - ) - message.update_chat_stream(chat) - - # 创建心流与chat的观察 - heartflow.create_subheartflow(chat.stream_id) - - await message.process() - logger.trace(f"消息处理成功{message.processed_plain_text}") - - # 过滤词/正则表达式过滤 - if self._check_ban_words(message.processed_plain_text, chat, userinfo) or self._check_ban_regex( - message.raw_message, chat, userinfo - ): - return - logger.trace(f"过滤词/正则表达式过滤成功{message.processed_plain_text}") - - await self.storage.store_message(message, chat) - logger.trace(f"存储成功{message.processed_plain_text}") - - # 记忆激活 - with Timer("记忆激活", timing_results): - interested_rate = await HippocampusManager.get_instance().get_activate_from_text( - message.processed_plain_text, fast_retrieval=True - ) - logger.trace(f"记忆激活: {interested_rate}") - - # 查询缓冲器结果,会整合前面跳过的消息,改变processed_plain_text - buffer_result = await message_buffer.query_buffer_result(message) - - # 处理提及 - is_mentioned, reply_probability = is_mentioned_bot_in_message(message) - - # 意愿管理器:设置当前message信息 - willing_manager.setup(message, chat, is_mentioned, interested_rate) - - # 处理缓冲器结果 - if not buffer_result: - await willing_manager.bombing_buffer_message_handle(message.message_info.message_id) - willing_manager.delete(message.message_info.message_id) - F_type = "seglist" - if message.message_segment.type != "seglist": - F_type =message.message_segment.type - else: - if (isinstance(message.message_segment.data, list) - and all(isinstance(x, Seg) for x in message.message_segment.data) - and len(message.message_segment.data) == 1): - F_type = message.message_segment.data[0].type - if F_type == "text": - logger.info(f"触发缓冲,已炸飞消息:{message.processed_plain_text}") - elif F_type == "image": - logger.info("触发缓冲,已炸飞表情包/图片") - elif F_type == "seglist": - logger.info("触发缓冲,已炸飞消息列") - return - - # 获取回复概率 - is_willing = False - if reply_probability != 1: - is_willing = True - reply_probability = await willing_manager.get_reply_probability(message.message_info.message_id) - - if message.message_info.additional_config: - if "maimcore_reply_probability_gain" in message.message_info.additional_config.keys(): - reply_probability += message.message_info.additional_config["maimcore_reply_probability_gain"] - - # 打印消息信息 - 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)) - willing_log = f"[回复意愿:{await willing_manager.get_willing(chat.stream_id):.2f}]" if is_willing else "" - logger.info( - f"[{current_time}][{mes_name}]" - f"{chat.user_info.user_nickname}:" - f"{message.processed_plain_text}{willing_log}[概率:{reply_probability * 100:.1f}%]" - ) - - do_reply = False - if random() < reply_probability: + try: try: - do_reply = True + with Timer("观察", timing_results): + sub_hf = heartflow.get_subheartflow(chat.stream_id) + if not sub_hf: + logger.warning(f"尝试观察时未找到 stream_id {chat.stream_id} 的 subheartflow") + return + await sub_hf.do_observe() + except Exception as e: + logger.error(f"心流观察失败: {e}") + logger.error(traceback.format_exc()) - # 回复前处理 - await willing_manager.before_generate_reply_handle(message.message_info.message_id) + container = MessageManager().get_container(chat.stream_id) + thinking_count = container.count_thinking_messages() + max_thinking_messages = getattr(global_config, 'max_concurrent_thinking_messages', 3) + if thinking_count >= max_thinking_messages: + logger.warning(f"聊天流 {chat.stream_id} 已有 {thinking_count} 条思考消息,取消回复。触发消息: {message.processed_plain_text[:30]}...") + return - # 创建思考消息 - try: - with Timer("创建思考消息", timing_results): - thinking_id = await self._create_thinking_message(message, chat, userinfo, messageinfo) - except Exception as e: - logger.error(f"心流创建思考消息失败: {e}") + try: + with Timer("创建思考消息", timing_results): + thinking_id = await self._create_thinking_message(message) + except Exception as e: + logger.error(f"心流创建思考消息失败: {e}") + return + if not thinking_id: + logger.error("未能成功创建思考消息 ID,无法继续回复流程。") + return - logger.trace(f"创建捕捉器,thinking_id:{thinking_id}") + logger.trace(f"创建捕捉器,thinking_id:{thinking_id}") + info_catcher = info_catcher_manager.get_info_catcher(thinking_id) + info_catcher.catch_decide_to_response(message) - info_catcher = info_catcher_manager.get_info_catcher(thinking_id) - info_catcher.catch_decide_to_response(message) - - # 观察 - try: - with Timer("观察", timing_results): - await heartflow.get_subheartflow(chat.stream_id).do_observe() - except Exception as e: - logger.error(f"心流观察失败: {e}") - logger.error(traceback.format_exc()) - - info_catcher.catch_after_observe(timing_results["观察"]) - - # 思考前使用工具 - update_relationship = "" - get_mid_memory_id = [] - tool_result_info = {} - send_emoji = "" - try: - with Timer("思考前使用工具", timing_results): - tool_result = await self.tool_user.use_tool( - message.processed_plain_text, - message.message_info.user_info.user_nickname, - chat, - heartflow.get_subheartflow(chat.stream_id), - ) - # 如果工具被使用且获得了结果,将收集到的信息合并到思考中 - # collected_info = "" - if tool_result.get("used_tools", False): - if "structured_info" in tool_result: - tool_result_info = tool_result["structured_info"] - # collected_info = "" - get_mid_memory_id = [] - update_relationship = "" - - # 动态解析工具结果 - for tool_name, tool_data in tool_result_info.items(): - # tool_result_info += f"\n{tool_name} 相关信息:\n" - # for item in tool_data: - # tool_result_info += f"- {item['name']}: {item['content']}\n" - - # 特殊判定:mid_chat_mem - if tool_name == "mid_chat_mem": - for mid_memory in tool_data: - get_mid_memory_id.append(mid_memory["content"]) - - # 特殊判定:change_mood - if tool_name == "change_mood": - for mood in tool_data: - self.mood_manager.update_mood_from_emotion( - mood["content"], global_config.mood_intensity_factor - ) - - # 特殊判定:change_relationship - if tool_name == "change_relationship": - update_relationship = tool_data[0]["content"] - - if tool_name == "send_emoji": - send_emoji = tool_data[0]["content"] - - except Exception as e: - logger.error(f"思考前工具调用失败: {e}") - logger.error(traceback.format_exc()) - - # 处理关系更新 - if update_relationship: - stance, emotion = await self.gpt._get_emotion_tags_with_reason( - "你还没有回复", message.processed_plain_text, update_relationship - ) - await relationship_manager.calculate_update_relationship_value( - chat_stream=message.chat_stream, label=emotion, stance=stance + get_mid_memory_id = [] + tool_result_info = {} + send_emoji = "" + try: + with Timer("思考前使用工具", timing_results): + tool_result = await self.tool_user.use_tool( + message.processed_plain_text, + userinfo.user_nickname, + chat, + heartflow.get_subheartflow(chat.stream_id), ) + if tool_result.get("used_tools", False): + if "structured_info" in tool_result: + tool_result_info = tool_result["structured_info"] + get_mid_memory_id = [] + for tool_name, tool_data in tool_result_info.items(): + if tool_name == "mid_chat_mem": + for mid_memory in tool_data: + get_mid_memory_id.append(mid_memory["content"]) + if tool_name == "send_emoji": + send_emoji = tool_data[0]["content"] + except Exception as e: + logger.error(f"思考前工具调用失败: {e}") + logger.error(traceback.format_exc()) - # 思考前脑内状态 - try: - with Timer("思考前脑内状态", timing_results): - current_mind, past_mind = await heartflow.get_subheartflow( - chat.stream_id - ).do_thinking_before_reply( + current_mind, past_mind = "", "" + try: + with Timer("思考前脑内状态", timing_results): + sub_hf = heartflow.get_subheartflow(chat.stream_id) + if sub_hf: + current_mind, past_mind = await sub_hf.do_thinking_before_reply( message_txt=message.processed_plain_text, - sender_info=message.message_info.user_info, + sender_info=userinfo, chat_stream=chat, obs_id=get_mid_memory_id, extra_info=tool_result_info, ) - except Exception as e: - logger.error(f"心流思考前脑内状态失败: {e}") - logger.error(traceback.format_exc()) - # 确保变量被定义,即使在错误情况下 - current_mind = "" - past_mind = "" + else: + logger.warning(f"尝试思考前状态时未找到 stream_id {chat.stream_id} 的 subheartflow") + except Exception as e: + logger.error(f"心流思考前脑内状态失败: {e}") + logger.error(traceback.format_exc()) + if info_catcher: + info_catcher.catch_afer_shf_step(timing_results.get("思考前脑内状态"), past_mind, current_mind) - info_catcher.catch_afer_shf_step(timing_results["思考前脑内状态"], past_mind, current_mind) - - # 生成回复 + try: with Timer("生成回复", timing_results): response_set = await self.gpt.generate_response(message, thinking_id) + except Exception as e: + logger.error(f"GPT 生成回复失败: {e}") + logger.error(traceback.format_exc()) + if info_catcher: info_catcher.done_catch() + return + if info_catcher: + info_catcher.catch_after_generate_response(timing_results.get("生成回复")) + if not response_set: + logger.info("回复生成失败,返回为空") + if info_catcher: info_catcher.done_catch() + return - info_catcher.catch_after_generate_response(timing_results["生成回复"]) - - if not response_set: - logger.info("回复生成失败,返回为空") - return - - # 发送消息 - try: - with Timer("发送消息", timing_results): - first_bot_msg = await self._send_response_messages(message, chat, response_set, thinking_id) - except Exception as e: - logger.error(f"心流发送消息失败: {e}") - - info_catcher.catch_after_response(timing_results["发送消息"], response_set, first_bot_msg) - + first_bot_msg = None + try: + with Timer("发送消息", timing_results): + first_bot_msg = await self._send_response_messages(message, response_set, thinking_id) + except Exception as e: + logger.error(f"心流发送消息失败: {e}") + if info_catcher: + info_catcher.catch_after_response(timing_results.get("发送消息"), response_set, first_bot_msg) info_catcher.done_catch() - # 处理表情包 - try: - with Timer("处理表情包", timing_results): - if send_emoji: - logger.info(f"麦麦决定发送表情包{send_emoji}") - await self._handle_emoji(message, chat, response_set, send_emoji) - - except Exception as e: - logger.error(f"心流处理表情包失败: {e}") - - - # 回复后处理 - await willing_manager.after_generate_reply_handle(message.message_info.message_id) - - + try: + with Timer("处理表情包", timing_results): + if send_emoji: + logger.info(f"麦麦决定发送表情包{send_emoji}") + await self._handle_emoji(message, response_set, send_emoji) except Exception as e: - logger.error(f"心流处理消息失败: {e}") - logger.error(traceback.format_exc()) + logger.error(f"心流处理表情包失败: {e}") - # 输出性能计时结果 - if do_reply: timing_str = " | ".join([f"{step}: {duration:.2f}秒" for step, duration in timing_results.items()]) trigger_msg = message.processed_plain_text response_msg = " ".join(response_set) if response_set else "无回复" - logger.info(f"触发消息: {trigger_msg[:20]}... | 思维消息: {response_msg[:20]}... | 性能计时: {timing_str}") - else: - # 不回复处理 - await willing_manager.not_reply_handle(message.message_info.message_id) + logger.info(f"回复任务完成: 触发消息: {trigger_msg[:20]}... | 思维消息: {response_msg[:20]}... | 性能计时: {timing_str}") - # 意愿管理器:注销当前message信息 - willing_manager.delete(message.message_info.message_id) + if first_bot_msg: + try: + with Timer("更新关系情绪", timing_results): + await self._update_relationship(message, response_set) + except Exception as e: + logger.error(f"更新关系情绪失败: {e}") + logger.error(traceback.format_exc()) - def _check_ban_words(self, text: str, chat, userinfo) -> bool: - """检查消息中是否包含过滤词""" - for word in global_config.ban_words: - if word in text: - logger.info( - f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{text}" - ) - logger.info(f"[过滤词识别]消息中含有{word},filtered") - return True - return False + except Exception as e: + logger.error(f"回复生成任务失败 (trigger_reply_generation V3): {e}") + logger.error(traceback.format_exc()) - def _check_ban_regex(self, text: str, chat, userinfo) -> bool: - """检查消息是否匹配过滤正则表达式""" - for pattern in global_config.ban_msgs_regex: - if pattern.search(text): - logger.info( - f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{text}" - ) - logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered") - return True - return False + finally: + pass diff --git a/src/plugins/chat_module/heartFC_chat/heartFC_processor.py b/src/plugins/chat_module/heartFC_chat/heartFC_processor.py new file mode 100644 index 000000000..1e84361c7 --- /dev/null +++ b/src/plugins/chat_module/heartFC_chat/heartFC_processor.py @@ -0,0 +1,170 @@ +import time +import traceback +import asyncio +from ...memory_system.Hippocampus import HippocampusManager +from ....config.config import global_config +from ...chat.message import MessageRecv +from ...storage.storage import MessageStorage +from ...chat.utils import is_mentioned_bot_in_message +from ...message import UserInfo, Seg +from src.heart_flow.heartflow import heartflow +from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig +from ...chat.chat_stream import chat_manager +from ...chat.message_buffer import message_buffer +from ...utils.timer_calculater import Timer +from .interest import InterestManager +from .heartFC_chat import HeartFC_Chat # 导入 HeartFC_Chat 以调用回复生成 + +# 定义日志配置 +processor_config = LogConfig( + console_format=CHAT_STYLE_CONFIG["console_format"], + file_format=CHAT_STYLE_CONFIG["file_format"], +) +logger = get_module_logger("heartFC_processor", config=processor_config) + +# # 定义兴趣度增加触发回复的阈值 (移至 InterestManager) +# INTEREST_INCREASE_THRESHOLD = 0.5 + +class HeartFC_Processor: + def __init__(self, chat_instance: HeartFC_Chat): + self.storage = MessageStorage() + self.interest_manager = InterestManager() # TODO: 可能需要传递 chat_instance 给 InterestManager 或修改其方法签名 + self.chat_instance = chat_instance # 持有 HeartFC_Chat 实例 + + async def process_message(self, message_data: str) -> None: + """处理接收到的消息,更新状态,并将回复决策委托给 InterestManager""" + timing_results = {} # 初始化 timing_results + message = None + try: + message = MessageRecv(message_data) + groupinfo = message.message_info.group_info + userinfo = message.message_info.user_info + messageinfo = message.message_info + + # 消息加入缓冲池 + await message_buffer.start_caching_messages(message) + + # 创建聊天流 + chat = await chat_manager.get_or_create_stream( + platform=messageinfo.platform, + user_info=userinfo, + group_info=groupinfo, + ) + if not chat: + logger.error(f"无法为消息创建或获取聊天流: user {userinfo.user_id}, group {groupinfo.group_id if groupinfo else 'None'}") + return + + message.update_chat_stream(chat) + + # 创建心流与chat的观察 (在接收消息时创建,以便后续观察和思考) + heartflow.create_subheartflow(chat.stream_id) + + await message.process() + logger.trace(f"消息处理成功: {message.processed_plain_text}") + + # 过滤词/正则表达式过滤 + if self._check_ban_words(message.processed_plain_text, chat, userinfo) or self._check_ban_regex( + message.raw_message, chat, userinfo + ): + return + logger.trace(f"过滤词/正则表达式过滤成功: {message.processed_plain_text}") + + # 查询缓冲器结果 + buffer_result = await message_buffer.query_buffer_result(message) + + # 处理缓冲器结果 (Bombing logic) + if not buffer_result: + F_type = "seglist" + if message.message_segment.type != "seglist": + F_type = message.message_segment.type + else: + if (isinstance(message.message_segment.data, list) + and all(isinstance(x, Seg) for x in message.message_segment.data) + and len(message.message_segment.data) == 1): + F_type = message.message_segment.data[0].type + if F_type == "text": + logger.debug(f"触发缓冲,消息:{message.processed_plain_text}") + elif F_type == "image": + logger.debug("触发缓冲,表情包/图片等待中") + elif F_type == "seglist": + logger.debug("触发缓冲,消息列表等待中") + return # 被缓冲器拦截,不生成回复 + + # ---- 只有通过缓冲的消息才进行存储和后续处理 ---- + + # 存储消息 (使用可能被缓冲器更新过的 message) + try: + await self.storage.store_message(message, chat) + logger.trace(f"存储成功 (通过缓冲后): {message.processed_plain_text}") + except Exception as e: + logger.error(f"存储消息失败: {e}") + logger.error(traceback.format_exc()) + # 存储失败可能仍需考虑是否继续,暂时返回 + return + + # 激活度计算 (使用可能被缓冲器更新过的 message.processed_plain_text) + is_mentioned, _ = is_mentioned_bot_in_message(message) + interested_rate = 0.0 # 默认值 + try: + with Timer("记忆激活", timing_results): + interested_rate = await HippocampusManager.get_instance().get_activate_from_text( + message.processed_plain_text, fast_retrieval=True # 使用更新后的文本 + ) + logger.trace(f"记忆激活率 (通过缓冲后): {interested_rate:.2f}") + except Exception as e: + logger.error(f"计算记忆激活率失败: {e}") + logger.error(traceback.format_exc()) + + if is_mentioned: + interested_rate += 0.8 + + # 更新兴趣度 + try: + self.interest_manager.increase_interest(chat.stream_id, value=interested_rate, message=message) + current_interest = self.interest_manager.get_interest(chat.stream_id) # 获取更新后的值用于日志 + logger.trace(f"使用激活率 {interested_rate:.2f} 更新后 (通过缓冲后),当前兴趣度: {current_interest:.2f}") + + except Exception as e: + logger.error(f"更新兴趣度失败: {e}") # 调整日志消息 + logger.error(traceback.format_exc()) + # ---- 兴趣度计算和更新结束 ---- + + # 打印消息接收和处理信息 + 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)) + logger.info( + f"[{current_time}][{mes_name}]" + f"{chat.user_info.user_nickname}:" + f"{message.processed_plain_text}" + f"兴趣度: {current_interest:.2f}" + ) + + # 回复触发逻辑已移至 HeartFC_Chat 的监控任务 + + except Exception as e: + logger.error(f"消息处理失败 (process_message V3): {e}") + logger.error(traceback.format_exc()) + if message: # 记录失败的消息内容 + logger.error(f"失败消息原始内容: {message.raw_message}") + + def _check_ban_words(self, text: str, chat, userinfo) -> bool: + """检查消息中是否包含过滤词""" + for word in global_config.ban_words: + if word in text: + logger.info( + f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{text}" + ) + logger.info(f"[过滤词识别]消息中含有{word},filtered") + return True + return False + + def _check_ban_regex(self, text: str, chat, userinfo) -> bool: + """检查消息是否匹配过滤正则表达式""" + for pattern in global_config.ban_msgs_regex: + if pattern.search(text): + logger.info( + f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{text}" + ) + logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered") + return True + return False \ No newline at end of file diff --git a/src/plugins/chat_module/heartFC_chat/interest.py b/src/plugins/chat_module/heartFC_chat/interest.py new file mode 100644 index 000000000..0b2a3f290 --- /dev/null +++ b/src/plugins/chat_module/heartFC_chat/interest.py @@ -0,0 +1,370 @@ +import time +import math +import asyncio +import threading +import json # 引入 json +import os # 引入 os +import traceback # <--- 添加导入 +from typing import Optional # <--- 添加导入 +from src.common.logger import get_module_logger, LogConfig, DEFAULT_CONFIG # 引入 DEFAULT_CONFIG +from src.plugins.chat.chat_stream import chat_manager # *** Import ChatManager *** +from ...chat.message import MessageRecv # 导入 MessageRecv + +# 定义日志配置 (使用 loguru 格式) +interest_log_config = LogConfig( + console_format=DEFAULT_CONFIG["console_format"], # 使用默认控制台格式 + file_format=DEFAULT_CONFIG["file_format"] # 使用默认文件格式 +) +logger = get_module_logger("InterestManager", config=interest_log_config) + + +# 定义常量 +DEFAULT_DECAY_RATE_PER_SECOND = 0.95 # 每秒衰减率 (兴趣保留 99%) +# DEFAULT_INCREASE_AMOUNT = 10.0 # 不再需要固定增加值 +MAX_INTEREST = 10.0 # 最大兴趣值 +MIN_INTEREST_THRESHOLD = 0.1 # 低于此值可能被清理 (可选) +CLEANUP_INTERVAL_SECONDS = 3600 # 清理任务运行间隔 (例如:1小时) +INACTIVE_THRESHOLD_SECONDS = 3600 * 24 # 不活跃时间阈值 (例如:1天) +LOG_INTERVAL_SECONDS = 3 # 日志记录间隔 (例如:30秒) +LOG_DIRECTORY = "logs/interest" # 日志目录 +LOG_FILENAME = "interest_log.json" # 快照日志文件名 (保留,以防其他地方用到) +HISTORY_LOG_FILENAME = "interest_history.log" # 新的历史日志文件名 +# 移除阈值,将移至 HeartFC_Chat +# INTEREST_INCREASE_THRESHOLD = 0.5 + +class InterestChatting: + def __init__(self, decay_rate=DEFAULT_DECAY_RATE_PER_SECOND, max_interest=MAX_INTEREST): + self.interest_level: float = 0.0 + self.last_update_time: float = time.time() + self.decay_rate_per_second: float = decay_rate + # self.increase_amount: float = increase_amount # 移除固定的 increase_amount + self.max_interest: float = max_interest + # 新增:用于追踪最后一次显著增加的信息,供外部监控任务使用 + self.last_increase_amount: float = 0.0 + self.last_triggering_message: MessageRecv | None = None + + def _calculate_decay(self, current_time: float): + """计算从上次更新到现在的衰减""" + time_delta = current_time - self.last_update_time + if time_delta > 0: + # 指数衰减: interest = interest * (decay_rate ^ time_delta) + # 添加处理极小兴趣值避免 math domain error + if self.interest_level < 1e-9: + self.interest_level = 0.0 + else: + # 检查 decay_rate_per_second 是否为非正数,避免 math domain error + if self.decay_rate_per_second <= 0: + logger.warning(f"InterestChatting encountered non-positive decay rate: {self.decay_rate_per_second}. Setting interest to 0.") + self.interest_level = 0.0 + # 检查 interest_level 是否为负数,虽然理论上不应发生,但以防万一 + elif self.interest_level < 0: + logger.warning(f"InterestChatting encountered negative interest level: {self.interest_level}. Setting interest to 0.") + self.interest_level = 0.0 + else: + try: + decay_factor = math.pow(self.decay_rate_per_second, time_delta) + self.interest_level *= decay_factor + except ValueError as e: + # 捕获潜在的 math domain error,例如对负数开非整数次方(虽然已加保护) + logger.error(f"Math error during decay calculation: {e}. Rate: {self.decay_rate_per_second}, Delta: {time_delta}, Level: {self.interest_level}. Setting interest to 0.") + self.interest_level = 0.0 + + # 防止低于阈值 (如果需要) + # self.interest_level = max(self.interest_level, MIN_INTEREST_THRESHOLD) + self.last_update_time = current_time + + def increase_interest(self, current_time: float, value: float, message: Optional[MessageRecv]): + """根据传入的值增加兴趣值,并记录增加量和关联消息""" + self._calculate_decay(current_time) # 先计算衰减 + # 记录这次增加的具体数值和消息,供外部判断是否触发 + self.last_increase_amount = value + self.last_triggering_message = message + # 应用增加 + self.interest_level += value + self.interest_level = min(self.interest_level, self.max_interest) # 不超过最大值 + self.last_update_time = current_time # 更新时间戳 + + def decrease_interest(self, current_time: float, value: float): + """降低兴趣值并更新时间 (确保不低于0)""" + # 注意:降低兴趣度是否需要先衰减?取决于具体逻辑,这里假设不衰减直接减 + self.interest_level -= value + self.interest_level = max(self.interest_level, 0.0) # 确保不低于0 + self.last_update_time = current_time # 降低也更新时间戳 + + def reset_trigger_info(self): + """重置触发相关信息,在外部任务处理后调用""" + self.last_increase_amount = 0.0 + self.last_triggering_message = None + + def get_interest(self) -> float: + """获取当前兴趣值 (由后台任务更新)""" + return self.interest_level + + def get_state(self) -> dict: + """获取当前状态字典""" + # 不再需要传入 current_time 来计算,直接获取 + interest = self.get_interest() # 使用修改后的 get_interest + return { + "interest_level": round(interest, 2), + "last_update_time": self.last_update_time, + # 可以选择性地暴露 last_increase_amount 给状态,方便调试 + # "last_increase_amount": round(self.last_increase_amount, 2) + } + + +class InterestManager: + _instance = None + _lock = threading.Lock() + _initialized = False + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + with cls._lock: + # Double-check locking + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self): + if not self._initialized: + with self._lock: + # 确保初始化也只执行一次 + if not self._initialized: + logger.info("Initializing InterestManager singleton...") + # key: stream_id (str), value: InterestChatting instance + self.interest_dict: dict[str, InterestChatting] = {} + # 保留旧的快照文件路径变量,尽管此任务不再写入 + self._snapshot_log_file_path = os.path.join(LOG_DIRECTORY, LOG_FILENAME) + # 定义新的历史日志文件路径 + self._history_log_file_path = os.path.join(LOG_DIRECTORY, HISTORY_LOG_FILENAME) + self._ensure_log_directory() + self._cleanup_task = None + self._logging_task = None # 添加日志任务变量 + self._initialized = True + logger.info("InterestManager initialized.") # 修改日志消息 + self._decay_task = None # 新增:衰减任务变量 + + def _ensure_log_directory(self): + """确保日志目录存在""" + try: + os.makedirs(LOG_DIRECTORY, exist_ok=True) + logger.info(f"Log directory '{LOG_DIRECTORY}' ensured.") + except OSError as e: + logger.error(f"Error creating log directory '{LOG_DIRECTORY}': {e}") + + async def _periodic_cleanup_task(self, interval_seconds: int, threshold: float, max_age_seconds: int): + """后台清理任务的异步函数""" + while True: + await asyncio.sleep(interval_seconds) + logger.info(f"Running periodic cleanup (interval: {interval_seconds}s)...") + self.cleanup_inactive_chats(threshold=threshold, max_age_seconds=max_age_seconds) + + async def _periodic_log_task(self, interval_seconds: int): + """后台日志记录任务的异步函数 (记录历史数据,包含 group_name)""" + while True: + await asyncio.sleep(interval_seconds) + logger.debug(f"Running periodic history logging (interval: {interval_seconds}s)...") + try: + current_timestamp = time.time() + all_states = self.get_all_interest_states() # 获取当前所有状态 + + # 以追加模式打开历史日志文件 + with open(self._history_log_file_path, 'a', encoding='utf-8') as f: + count = 0 + for stream_id, state in all_states.items(): + # *** Get group name from ChatManager *** + group_name = stream_id # Default to stream_id + try: + # Use the imported chat_manager instance + chat_stream = chat_manager.get_stream(stream_id) + if chat_stream and chat_stream.group_info: + group_name = chat_stream.group_info.group_name + elif chat_stream and not chat_stream.group_info: + # Handle private chats - maybe use user nickname? + group_name = f"私聊_{chat_stream.user_info.user_nickname}" if chat_stream.user_info else stream_id + except Exception as e: + logger.warning(f"Could not get group name for stream_id {stream_id}: {e}") + # Fallback to stream_id is already handled by default value + + log_entry = { + "timestamp": round(current_timestamp, 2), + "stream_id": stream_id, + "interest_level": state.get("interest_level", 0.0), # 确保有默认值 + "group_name": group_name # *** Add group_name *** + } + # 将每个条目作为单独的 JSON 行写入 + f.write(json.dumps(log_entry, ensure_ascii=False) + '\n') + count += 1 + logger.debug(f"Successfully appended {count} interest history entries to {self._history_log_file_path}") + + # 注意:不再写入快照文件 interest_log.json + # 如果需要快照文件,可以在这里单独写入 self._snapshot_log_file_path + # 例如: + # with open(self._snapshot_log_file_path, 'w', encoding='utf-8') as snap_f: + # json.dump(all_states, snap_f, indent=4, ensure_ascii=False) + # logger.debug(f"Successfully wrote snapshot to {self._snapshot_log_file_path}") + + except IOError as e: + logger.error(f"Error writing interest history log to {self._history_log_file_path}: {e}") + except Exception as e: + logger.error(f"Unexpected error during periodic history logging: {e}") + + async def _periodic_decay_task(self): + """后台衰减任务的异步函数,每秒更新一次所有实例的衰减""" + while True: + await asyncio.sleep(1) # 每秒运行一次 + current_time = time.time() + # logger.debug("Running periodic decay calculation...") # 调试日志,可能过于频繁 + + # 创建字典项的快照进行迭代,避免在迭代时修改字典的问题 + items_snapshot = list(self.interest_dict.items()) + count = 0 + for stream_id, chatting in items_snapshot: + try: + # 调用 InterestChatting 实例的衰减方法 + chatting._calculate_decay(current_time) + count += 1 + except Exception as e: + logger.error(f"Error calculating decay for stream_id {stream_id}: {e}") + # if count > 0: # 仅在实际处理了项目时记录日志,避免空闲时刷屏 + # logger.debug(f"Applied decay to {count} streams.") + + async def start_background_tasks(self): + """Starts the background cleanup, logging, and decay tasks.""" + if self._cleanup_task is None or self._cleanup_task.done(): + self._cleanup_task = asyncio.create_task( + self._periodic_cleanup_task( + interval_seconds=CLEANUP_INTERVAL_SECONDS, + threshold=MIN_INTEREST_THRESHOLD, + max_age_seconds=INACTIVE_THRESHOLD_SECONDS + ) + ) + logger.info(f"Periodic cleanup task created. Interval: {CLEANUP_INTERVAL_SECONDS}s, Inactive Threshold: {INACTIVE_THRESHOLD_SECONDS}s") + else: + logger.warning("Cleanup task creation skipped: already running or exists.") + + if self._logging_task is None or self._logging_task.done(): + self._logging_task = asyncio.create_task( + self._periodic_log_task(interval_seconds=LOG_INTERVAL_SECONDS) + ) + logger.info(f"Periodic logging task created. Interval: {LOG_INTERVAL_SECONDS}s") + else: + logger.warning("Logging task creation skipped: already running or exists.") + + # 启动新的衰减任务 + if self._decay_task is None or self._decay_task.done(): + self._decay_task = asyncio.create_task( + self._periodic_decay_task() + ) + logger.info("Periodic decay task created. Interval: 1s") + else: + logger.warning("Decay task creation skipped: already running or exists.") + + def get_all_interest_states(self) -> dict[str, dict]: + """获取所有聊天流的当前兴趣状态""" + # 不再需要 current_time, 因为 get_state 现在不接收它 + states = {} + # 创建副本以避免在迭代时修改字典 + items_snapshot = list(self.interest_dict.items()) + for stream_id, chatting in items_snapshot: + try: + # 直接调用 get_state,它会使用内部的 get_interest 获取已更新的值 + states[stream_id] = chatting.get_state() + except Exception as e: + logger.warning(f"Error getting state for stream_id {stream_id}: {e}") + return states + + def get_interest_chatting(self, stream_id: str) -> Optional[InterestChatting]: + """获取指定流的 InterestChatting 实例,如果不存在则返回 None""" + return self.interest_dict.get(stream_id) + + def _get_or_create_interest_chatting(self, stream_id: str) -> InterestChatting: + """获取或创建指定流的 InterestChatting 实例 (线程安全)""" + # 由于字典操作本身在 CPython 中大部分是原子的, + # 且主要写入发生在 __init__ 和 cleanup (由单任务执行), + # 读取和 get_or_create 主要在事件循环线程,简单场景下可能不需要锁。 + # 但为保险起见或跨线程使用考虑,可加锁。 + # with self._lock: + if stream_id not in self.interest_dict: + logger.debug(f"Creating new InterestChatting for stream_id: {stream_id}") + self.interest_dict[stream_id] = InterestChatting() + # 首次创建时兴趣为 0,由第一次消息的 activate rate 决定初始值 + return self.interest_dict[stream_id] + + def get_interest(self, stream_id: str) -> float: + """获取指定聊天流当前的兴趣度 (值由后台任务更新)""" + # current_time = time.time() # 不再需要获取当前时间 + interest_chatting = self._get_or_create_interest_chatting(stream_id) + # 直接调用修改后的 get_interest,不传入时间 + return interest_chatting.get_interest() + + def increase_interest(self, stream_id: str, value: float, message: MessageRecv): + """当收到消息时,增加指定聊天流的兴趣度,并传递关联消息""" + current_time = time.time() + interest_chatting = self._get_or_create_interest_chatting(stream_id) + # 调用修改后的 increase_interest,传入 message + interest_chatting.increase_interest(current_time, value, message) + logger.debug(f"Increased interest for stream_id: {stream_id} by {value:.2f} to {interest_chatting.interest_level:.2f}") # 更新日志 + + def decrease_interest(self, stream_id: str, value: float): + """降低指定聊天流的兴趣度""" + current_time = time.time() + # 尝试获取,如果不存在则不做任何事 + interest_chatting = self.get_interest_chatting(stream_id) + if interest_chatting: + interest_chatting.decrease_interest(current_time, value) + logger.debug(f"Decreased interest for stream_id: {stream_id} by {value:.2f} to {interest_chatting.interest_level:.2f}") + else: + logger.warning(f"Attempted to decrease interest for non-existent stream_id: {stream_id}") + + def cleanup_inactive_chats(self, threshold=MIN_INTEREST_THRESHOLD, max_age_seconds=INACTIVE_THRESHOLD_SECONDS): + """ + 清理长时间不活跃或兴趣度过低的聊天流记录 + threshold: 低于此兴趣度的将被清理 + max_age_seconds: 超过此时间未更新的将被清理 + """ + current_time = time.time() + keys_to_remove = [] + initial_count = len(self.interest_dict) + # with self._lock: # 如果需要锁整个迭代过程 + # 创建副本以避免在迭代时修改字典 + items_snapshot = list(self.interest_dict.items()) + + for stream_id, chatting in items_snapshot: + # 先计算当前兴趣,确保是最新的 + # 加锁保护 chatting 对象状态的读取和可能的修改 + # with self._lock: # 如果 InterestChatting 内部操作不是原子的 + interest = chatting.get_interest() + last_update = chatting.last_update_time + + should_remove = False + reason = "" + if interest < threshold: + should_remove = True + reason = f"interest ({interest:.2f}) < threshold ({threshold})" + # 只有设置了 max_age_seconds 才检查时间 + if max_age_seconds is not None and (current_time - last_update) > max_age_seconds: + should_remove = True + reason = f"inactive time ({current_time - last_update:.0f}s) > max age ({max_age_seconds}s)" + (f", {reason}" if reason else "") # 附加之前的理由 + + if should_remove: + keys_to_remove.append(stream_id) + logger.debug(f"Marking stream_id {stream_id} for removal. Reason: {reason}") + + if keys_to_remove: + logger.info(f"Cleanup identified {len(keys_to_remove)} inactive/low-interest streams.") + # with self._lock: # 确保删除操作的原子性 + for key in keys_to_remove: + # 再次检查 key 是否存在,以防万一在迭代和删除之间状态改变 + if key in self.interest_dict: + del self.interest_dict[key] + logger.debug(f"Removed stream_id: {key}") + final_count = initial_count - len(keys_to_remove) + logger.info(f"Cleanup finished. Removed {len(keys_to_remove)} streams. Current count: {final_count}") + else: + logger.info(f"Cleanup finished. No streams met removal criteria. Current count: {initial_count}") + + +# 不再需要手动创建实例和任务 +# manager = InterestManager() +# asyncio.create_task(periodic_cleanup(manager, 3600)) \ No newline at end of file diff --git a/src/plugins/chat_module/heartFC_chat/messagesender.py b/src/plugins/chat_module/heartFC_chat/messagesender.py index 62ecbfa02..cfffb892c 100644 --- a/src/plugins/chat_module/heartFC_chat/messagesender.py +++ b/src/plugins/chat_module/heartFC_chat/messagesender.py @@ -5,8 +5,7 @@ from typing import Dict, List, Optional, Union from src.common.logger import get_module_logger from ....common.database import db from ...message.api import global_api -from ...message import MessageSending, MessageThinking, MessageSet - +from ...chat.message import MessageSending, MessageThinking, MessageSet from ...storage.storage import MessageStorage from ....config.config import global_config from ...chat.utils import truncate_message, calculate_typing_time, count_messages_between @@ -97,22 +96,10 @@ class MessageContainer: self.max_size = max_size self.messages = [] self.last_send_time = 0 - self.thinking_wait_timeout = 20 # 思考等待超时时间(秒) - def get_timeout_messages(self) -> List[MessageSending]: - """获取所有超时的Message_Sending对象(思考时间超过20秒),按thinking_start_time排序""" - current_time = time.time() - timeout_messages = [] - - for msg in self.messages: - if isinstance(msg, MessageSending): - if current_time - msg.thinking_start_time > self.thinking_wait_timeout: - timeout_messages.append(msg) - - # 按thinking_start_time排序,时间早的在前面 - timeout_messages.sort(key=lambda x: x.thinking_start_time) - - return timeout_messages + def count_thinking_messages(self) -> int: + """计算当前容器中思考消息的数量""" + return sum(1 for msg in self.messages if isinstance(msg, MessageThinking)) def get_earliest_message(self) -> Optional[Union[MessageThinking, MessageSending]]: """获取thinking_start_time最早的消息对象""" @@ -224,10 +211,10 @@ class MessageManager: if ( message_earliest.is_head - and (thinking_messages_count > 4 or thinking_messages_length > 250) + and (thinking_messages_count > 3 or thinking_messages_length > 200) and not message_earliest.is_private_message() # 避免在私聊时插入reply ): - logger.debug(f"设置回复消息{message_earliest.processed_plain_text}") + logger.debug(f"距离原始消息太长,设置回复消息{message_earliest.processed_plain_text}") message_earliest.set_reply() await message_earliest.process() diff --git a/src/plugins/memory_system/Hippocampus.py b/src/plugins/memory_system/Hippocampus.py index 9c8839ef4..8e19f1a87 100644 --- a/src/plugins/memory_system/Hippocampus.py +++ b/src/plugins/memory_system/Hippocampus.py @@ -400,7 +400,7 @@ class Hippocampus: # 过滤掉不存在于记忆图中的关键词 valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G] if not valid_keywords: - logger.info("没有找到有效的关键词节点") + # logger.info("没有找到有效的关键词节点") return [] logger.info(f"有效的关键词: {', '.join(valid_keywords)}") @@ -590,7 +590,7 @@ class Hippocampus: # 过滤掉不存在于记忆图中的关键词 valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G] if not valid_keywords: - logger.info("没有找到有效的关键词节点") + # logger.info("没有找到有效的关键词节点") return 0 logger.info(f"有效的关键词: {', '.join(valid_keywords)}") @@ -1114,7 +1114,7 @@ class Hippocampus: # 过滤掉不存在于记忆图中的关键词 valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G] if not valid_keywords: - logger.info("没有找到有效的关键词节点") + # logger.info("没有找到有效的关键词节点") return [] logger.info(f"有效的关键词: {', '.join(valid_keywords)}") @@ -1304,7 +1304,7 @@ class Hippocampus: # 过滤掉不存在于记忆图中的关键词 valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G] if not valid_keywords: - logger.info("没有找到有效的关键词节点") + # logger.info("没有找到有效的关键词节点") return 0 logger.info(f"有效的关键词: {', '.join(valid_keywords)}") diff --git a/src/plugins/person_info/person_info.py b/src/plugins/person_info/person_info.py index 28117d029..72efb02a4 100644 --- a/src/plugins/person_info/person_info.py +++ b/src/plugins/person_info/person_info.py @@ -371,6 +371,7 @@ class PersonInfoManager: "msg_interval_list", lambda x: isinstance(x, list) and len(x) >= 100 ) for person_id, msg_interval_list_ in msg_interval_lists.items(): + await asyncio.sleep(0.3) try: time_interval = [] for t1, t2 in zip(msg_interval_list_, msg_interval_list_[1:]):