From ecf2df27fad07e86d3cc69a41f87c4af98f2d88d Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Wed, 27 Aug 2025 21:02:21 +0800 Subject: [PATCH] feat(chat): implement sleep pressure and insomnia system This commit introduces a new sleep pressure and insomnia system to simulate more realistic character behavior. Key features include: - **Sleep Pressure**: A new metric that accumulates with each action the bot takes and decreases during scheduled sleep times. - **Insomnia Mechanic**: When a sleep period begins, the system checks the current sleep pressure. Low pressure can lead to a higher chance of "insomnia," causing the bot to stay awake. There is also a small chance for random insomnia. - **Insomnia State**: During insomnia, the bot enters a special state for a configurable duration. It can trigger unique proactive thoughts related to being unable to sleep, and its mood is adjusted accordingly. - **Configuration**: All parameters, such as insomnia probability, duration, and pressure thresholds, are fully configurable. --- src/chat/chat_loop/cycle_processor.py | 5 ++ src/chat/chat_loop/energy_manager.py | 69 ++++++++++++++++++------- src/chat/chat_loop/heartFC_chat.py | 34 ++++++++++-- src/chat/chat_loop/hfc_context.py | 10 +++- src/chat/chat_loop/proactive_thinker.py | 53 +++++++++++++++++++ src/chat/chat_loop/wakeup_manager.py | 41 ++++++++++++++- src/config/official_configs.py | 12 ++++- src/mood/mood_manager.py | 16 ++++++ template/bot_config_template.toml | 19 ++++++- 9 files changed, 231 insertions(+), 28 deletions(-) diff --git a/src/chat/chat_loop/cycle_processor.py b/src/chat/chat_loop/cycle_processor.py index 043282924..781d9dde4 100644 --- a/src/chat/chat_loop/cycle_processor.py +++ b/src/chat/chat_loop/cycle_processor.py @@ -131,6 +131,11 @@ class CycleProcessor: if ENABLE_S4U: await stop_typing() + + # 在一轮动作执行完毕后,增加睡眠压力 + if self.context.energy_manager and global_config.wakeup_system.enable_insomnia_system: + if action_type not in ["no_reply", "no_action"]: + self.context.energy_manager.increase_sleep_pressure() return True diff --git a/src/chat/chat_loop/energy_manager.py b/src/chat/chat_loop/energy_manager.py index 78d0b10c5..a2c444326 100644 --- a/src/chat/chat_loop/energy_manager.py +++ b/src/chat/chat_loop/energy_manager.py @@ -5,6 +5,7 @@ from src.common.logger import get_logger from src.config.config import global_config from src.plugin_system.base.component_types import ChatMode from .hfc_context import HfcContext +from src.schedule.schedule_manager import schedule_manager logger = get_logger("hfc") @@ -77,7 +78,7 @@ class EnergyManager: async def _energy_loop(self): """ - 能量管理的主循环 + 能量与睡眠压力管理的主循环 功能说明: - 每10秒执行一次能量更新 @@ -92,24 +93,35 @@ class EnergyManager: if not self.context.chat_stream: continue - is_group_chat = self.context.chat_stream.group_info is not None - if is_group_chat and global_config.chat.group_chat_mode != "auto": - if global_config.chat.group_chat_mode == "focus": - self.context.loop_mode = ChatMode.FOCUS - self.context.energy_value = 35 - elif global_config.chat.group_chat_mode == "normal": - self.context.loop_mode = ChatMode.NORMAL - self.context.energy_value = 15 - continue + # 判断当前是否为睡眠时间 + is_sleeping = schedule_manager.is_sleeping(self.context.wakeup_manager) - if self.context.loop_mode == ChatMode.NORMAL: - self.context.energy_value -= 0.3 - self.context.energy_value = max(self.context.energy_value, 0.3) - if self.context.loop_mode == ChatMode.FOCUS: - self.context.energy_value -= 0.6 - self.context.energy_value = max(self.context.energy_value, 0.3) - - self._log_energy_change("能量值衰减") + if is_sleeping: + # 睡眠中:减少睡眠压力 + decay_per_10s = global_config.wakeup_system.sleep_pressure_decay_rate / 6 + self.context.sleep_pressure -= decay_per_10s + self.context.sleep_pressure = max(self.context.sleep_pressure, 0) + self._log_sleep_pressure_change("睡眠压力释放") + else: + # 清醒时:处理能量衰减 + is_group_chat = self.context.chat_stream.group_info is not None + if is_group_chat and global_config.chat.group_chat_mode != "auto": + if global_config.chat.group_chat_mode == "focus": + self.context.loop_mode = ChatMode.FOCUS + self.context.energy_value = 35 + elif global_config.chat.group_chat_mode == "normal": + self.context.loop_mode = ChatMode.NORMAL + self.context.energy_value = 15 + continue + + if self.context.loop_mode == ChatMode.NORMAL: + self.context.energy_value -= 0.3 + self.context.energy_value = max(self.context.energy_value, 0.3) + if self.context.loop_mode == ChatMode.FOCUS: + self.context.energy_value -= 0.6 + self.context.energy_value = max(self.context.energy_value, 0.3) + + self._log_energy_change("能量值衰减") def _should_log_energy(self) -> bool: """ @@ -129,6 +141,15 @@ class EnergyManager: return True return False + def increase_sleep_pressure(self): + """ + 在执行动作后增加睡眠压力 + """ + increment = global_config.wakeup_system.sleep_pressure_increment + self.context.sleep_pressure += increment + self.context.sleep_pressure = min(self.context.sleep_pressure, 100.0) # 设置一个100的上限 + self._log_sleep_pressure_change("执行动作,睡眠压力累积") + def _log_energy_change(self, action: str, reason: str = ""): """ 记录能量变化日志 @@ -151,4 +172,14 @@ class EnergyManager: log_message = f"{self.context.log_prefix} {action},当前能量值:{self.context.energy_value:.1f}" if reason: log_message = f"{self.context.log_prefix} {action},{reason},当前能量值:{self.context.energy_value:.1f}" - logger.debug(log_message) \ No newline at end of file + logger.debug(log_message) + + def _log_sleep_pressure_change(self, action: str): + """ + 记录睡眠压力变化日志 + """ + # 使用与能量日志相同的频率控制 + if self._should_log_energy(): + logger.info(f"{self.context.log_prefix} {action},当前睡眠压力:{self.context.sleep_pressure:.1f}") + else: + logger.debug(f"{self.context.log_prefix} {action},当前睡眠压力:{self.context.sleep_pressure:.1f}") \ No newline at end of file diff --git a/src/chat/chat_loop/heartFC_chat.py b/src/chat/chat_loop/heartFC_chat.py index 1b6046ef3..b6fc3ae07 100644 --- a/src/chat/chat_loop/heartFC_chat.py +++ b/src/chat/chat_loop/heartFC_chat.py @@ -10,6 +10,7 @@ from src.chat.express.expression_learner import expression_learner_manager from src.plugin_system.base.component_types import ChatMode from src.schedule.schedule_manager import schedule_manager from src.plugin_system.apis import message_api +from src.mood.mood_manager import mood_manager from .hfc_context import HfcContext from .energy_manager import EnergyManager @@ -48,6 +49,7 @@ class HeartFChatting: # 将唤醒度管理器设置到上下文中 self.context.wakeup_manager = self.wakeup_manager + self.context.energy_manager = self.energy_manager self._loop_task: Optional[asyncio.Task] = None @@ -196,8 +198,28 @@ class HeartFChatting: - NORMAL模式:检查进入FOCUS模式的条件,并通过normal_mode_handler处理消息 """ is_sleeping = schedule_manager.is_sleeping(self.wakeup_manager) - - # 核心修复:在睡眠模式下获取消息时,不过滤命令消息,以确保@消息能被接收 + + # --- 失眠状态管理 --- + if self.context.is_in_insomnia and time.time() > self.context.insomnia_end_time: + # 失眠状态结束 + self.context.is_in_insomnia = False + await self.proactive_thinker.trigger_goodnight_thinking() + + if is_sleeping and not self.context.was_sleeping: + # 刚刚进入睡眠状态,进行一次入睡检查 + if self.wakeup_manager and self.wakeup_manager.check_for_insomnia(): + # 触发失眠 + self.context.is_in_insomnia = True + duration = global_config.wakeup_system.insomnia_duration_minutes * 60 + self.context.insomnia_end_time = time.time() + duration + + # 判断失眠原因并触发思考 + reason = "random" + if self.context.sleep_pressure < global_config.wakeup_system.sleep_pressure_threshold: + reason = "low_pressure" + await self.proactive_thinker.trigger_insomnia_thinking(reason) + + # 核心修复:在睡眠模式(包括失眠)下获取消息时,不过滤命令消息,以确保@消息能被接收 filter_command_flag = not is_sleeping recent_messages = message_api.get_messages_by_time_in_chat( @@ -220,8 +242,9 @@ class HeartFChatting: # 处理唤醒度逻辑 if is_sleeping: self._handle_wakeup_messages(recent_messages) - # 如果仍在睡眠状态,跳过正常处理但仍返回有新消息 - if schedule_manager.is_sleeping(self.wakeup_manager): + # 如果处于失眠状态,则无视睡眠时间,继续处理消息 + # 否则,如果仍然在睡眠(没被吵醒),则跳过本轮处理 + if not self.context.is_in_insomnia and schedule_manager.is_sleeping(self.wakeup_manager): return has_new_messages # 根据聊天模式处理新消息 @@ -239,6 +262,9 @@ class HeartFChatting: self._check_focus_exit() elif self.context.loop_mode == ChatMode.NORMAL: self._check_focus_entry(0) # 传入0表示无新消息 + + # 更新上一帧的睡眠状态 + self.context.was_sleeping = is_sleeping return has_new_messages diff --git a/src/chat/chat_loop/hfc_context.py b/src/chat/chat_loop/hfc_context.py index 6bbbdb7ee..bd5c7e25b 100644 --- a/src/chat/chat_loop/hfc_context.py +++ b/src/chat/chat_loop/hfc_context.py @@ -9,6 +9,7 @@ from src.chat.chat_loop.hfc_utils import CycleDetail if TYPE_CHECKING: from .wakeup_manager import WakeUpManager + from .energy_manager import EnergyManager class HfcContext: def __init__(self, chat_id: str): @@ -40,6 +41,12 @@ class HfcContext: self.loop_mode = ChatMode.NORMAL self.energy_value = 5.0 + self.sleep_pressure = 0.0 + self.was_sleeping = False # 用于检测睡眠状态的切换 + + # 失眠状态 + self.is_in_insomnia: bool = False + self.insomnia_end_time: float = 0.0 self.last_message_time = time.time() self.last_read_time = time.time() - 10 @@ -53,4 +60,5 @@ class HfcContext: self.current_cycle_detail: Optional[CycleDetail] = None # 唤醒度管理器 - 延迟初始化以避免循环导入 - self.wakeup_manager: Optional['WakeUpManager'] = None \ No newline at end of file + self.wakeup_manager: Optional['WakeUpManager'] = None + self.energy_manager: Optional['EnergyManager'] = None \ No newline at end of file diff --git a/src/chat/chat_loop/proactive_thinker.py b/src/chat/chat_loop/proactive_thinker.py index 5bf02d064..69fec753d 100644 --- a/src/chat/chat_loop/proactive_thinker.py +++ b/src/chat/chat_loop/proactive_thinker.py @@ -279,3 +279,56 @@ class ProactiveThinker: except Exception as e: logger.error(f"{self.context.log_prefix} 主动思考执行异常: {e}") logger.error(traceback.format_exc()) + + async def trigger_insomnia_thinking(self, reason: str): + """ + 由外部事件(如失眠)触发的一次性主动思考 + + Args: + reason: 触发的原因 (e.g., "low_pressure", "random") + """ + logger.info(f"{self.context.log_prefix} 因“{reason}”触发失眠,开始深夜思考...") + + # 1. 根据原因修改情绪 + try: + from src.mood.mood_manager import mood_manager + mood_obj = mood_manager.get_mood_by_chat_id(self.context.stream_id) + if reason == "low_pressure": + mood_obj.mood_state = "精力过剩,毫无睡意" + elif reason == "random": + mood_obj.mood_state = "深夜emo,胡思乱想" + mood_obj.last_change_time = time.time() # 更新时间戳以允许后续的情绪回归 + logger.info(f"{self.context.log_prefix} 因失眠,情绪状态被强制更新为: {mood_obj.mood_state}") + except Exception as e: + logger.error(f"{self.context.log_prefix} 设置失眠情绪时出错: {e}") + + # 2. 直接执行主动思考逻辑 + try: + # 传入一个象征性的silence_duration,因为它在这里不重要 + await self._execute_proactive_thinking(silence_duration=1) + except Exception as e: + logger.error(f"{self.context.log_prefix} 失眠思考执行出错: {e}") + logger.error(traceback.format_exc()) + + async def trigger_goodnight_thinking(self): + """ + 在失眠状态结束后,触发一次准备睡觉的主动思考 + """ + logger.info(f"{self.context.log_prefix} 失眠状态结束,准备睡觉,触发告别思考...") + + # 1. 设置一个准备睡觉的特定情绪 + try: + from src.mood.mood_manager import mood_manager + mood_obj = mood_manager.get_mood_by_chat_id(self.context.stream_id) + mood_obj.mood_state = "有点困了,准备睡觉了" + mood_obj.last_change_time = time.time() + logger.info(f"{self.context.log_prefix} 情绪状态更新为: {mood_obj.mood_state}") + except Exception as e: + logger.error(f"{self.context.log_prefix} 设置睡前情绪时出错: {e}") + + # 2. 直接执行主动思考逻辑 + try: + await self._execute_proactive_thinking(silence_duration=1) + except Exception as e: + logger.error(f"{self.context.log_prefix} 睡前告别思考执行出错: {e}") + logger.error(traceback.format_exc()) diff --git a/src/chat/chat_loop/wakeup_manager.py b/src/chat/chat_loop/wakeup_manager.py index 9be420f03..18344868e 100644 --- a/src/chat/chat_loop/wakeup_manager.py +++ b/src/chat/chat_loop/wakeup_manager.py @@ -39,6 +39,13 @@ class WakeUpManager: self.angry_duration = wakeup_config.angry_duration self.enabled = wakeup_config.enable self.angry_prompt = wakeup_config.angry_prompt + + # 失眠系统参数 + self.insomnia_enabled = wakeup_config.enable_insomnia_system + self.sleep_pressure_threshold = wakeup_config.sleep_pressure_threshold + self.deep_sleep_threshold = wakeup_config.deep_sleep_threshold + self.insomnia_chance_low_pressure = wakeup_config.insomnia_chance_low_pressure + self.insomnia_chance_normal_pressure = wakeup_config.insomnia_chance_normal_pressure async def start(self): """启动唤醒度管理器""" @@ -177,4 +184,36 @@ class WakeUpManager: "wakeup_threshold": self.wakeup_threshold, "is_angry": self.is_angry, "angry_remaining_time": max(0, self.angry_duration - (time.time() - self.angry_start_time)) if self.is_angry else 0 - } \ No newline at end of file + } + + def check_for_insomnia(self) -> bool: + """ + 在尝试入睡时检查是否会失眠 + + Returns: + bool: 如果失眠则返回 True,否则返回 False + """ + if not self.insomnia_enabled: + return False + + import random + + pressure = self.context.sleep_pressure + + # 压力过高,深度睡眠,极难失眠 + if pressure > self.deep_sleep_threshold: + return False + + # 根据睡眠压力决定失眠概率 + if pressure < self.sleep_pressure_threshold: + # 压力不足型失眠 + if random.random() < self.insomnia_chance_low_pressure: + logger.info(f"{self.context.log_prefix} 睡眠压力不足 ({pressure:.1f}),触发失眠!") + return True + else: + # 压力正常,随机失眠 + if random.random() < self.insomnia_chance_normal_pressure: + logger.info(f"{self.context.log_prefix} 睡眠压力正常 ({pressure:.1f}),触发随机失眠!") + return True + + return False \ No newline at end of file diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 08518e9c1..dc1e0f157 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -657,7 +657,7 @@ class PluginsConfig(ValidatedConfigBase): class WakeUpSystemConfig(ValidatedConfigBase): - """唤醒度系统配置类""" + """唤醒度与失眠系统配置类""" enable: bool = Field(default=True, description="是否启用唤醒度系统") wakeup_threshold: float = Field(default=15.0, ge=1.0, description="唤醒阈值,达到此值时会被唤醒") @@ -668,6 +668,16 @@ class WakeUpSystemConfig(ValidatedConfigBase): angry_duration: float = Field(default=300.0, ge=10.0, description="愤怒状态持续时间(秒)") angry_prompt: str = Field(default="你被人吵醒了非常生气,说话带着怒气", description="被吵醒后的愤怒提示词") + # --- 失眠机制相关参数 --- + enable_insomnia_system: bool = Field(default=True, description="是否启用失眠系统") + insomnia_duration_minutes: int = Field(default=30, ge=1, description="单次失眠状态的持续时间(分钟)") + sleep_pressure_threshold: float = Field(default=30.0, description="触发“压力不足型失眠”的睡眠压力阈值") + deep_sleep_threshold: float = Field(default=80.0, description="进入“深度睡眠”的睡眠压力阈值") + insomnia_chance_low_pressure: float = Field(default=0.6, ge=0.0, le=1.0, description="压力不足时的失眠基础概率") + insomnia_chance_normal_pressure: float = Field(default=0.1, ge=0.0, le=1.0, description="压力正常时的失眠基础概率") + sleep_pressure_increment: float = Field(default=1.5, ge=0.0, description="每次AI执行动作后,增加的睡眠压力值") + sleep_pressure_decay_rate: float = Field(default=1.5, ge=0.0, description="睡眠时,每分钟衰减的睡眠压力值") + class MonthlyPlanSystemConfig(ValidatedConfigBase): """月度计划系统配置类""" diff --git a/src/mood/mood_manager.py b/src/mood/mood_manager.py index b288953f4..8e533d7c0 100644 --- a/src/mood/mood_manager.py +++ b/src/mood/mood_manager.py @@ -66,6 +66,11 @@ class ChatMood: self.last_change_time: float = 0 async def update_mood_by_message(self, message: MessageRecv, interested_rate: float): + # 如果当前聊天处于失眠状态,则锁定情绪,不允许更新 + if self.chat_id in mood_manager.insomnia_chats: + logger.debug(f"{self.log_prefix} 处于失眠状态,情绪已锁定,跳过更新。") + return + self.regression_count = 0 during_last_time = message.message_info.time - self.last_change_time # type: ignore @@ -216,6 +221,7 @@ class MoodManager: self.mood_list: list[ChatMood] = [] """当前情绪状态""" self.task_started: bool = False + self.insomnia_chats: set[str] = set() # 正在失眠的聊天ID列表 async def start(self): """启动情绪回归后台任务""" @@ -262,6 +268,16 @@ class MoodManager: mood.mood_state = "感觉很平静" logger.info(f"{mood.log_prefix} 清除被吵醒的愤怒状态") + def start_insomnia(self, chat_id: str): + """开始一个聊天的失眠状态,锁定情绪更新""" + logger.info(f"Chat [{chat_id}]进入失眠状态,情绪已锁定。") + self.insomnia_chats.add(chat_id) + + def stop_insomnia(self, chat_id: str): + """停止一个聊天的失眠状态,解锁情绪更新""" + logger.info(f"Chat [{chat_id}]失眠状态结束,情绪已解锁。") + self.insomnia_chats.discard(chat_id) + def get_angry_prompt_addition(self, chat_id: str) -> str: """获取愤怒状态下的提示词补充""" mood = self.get_mood_by_chat_id(chat_id) diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 902f8043e..460983b90 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.5.3" +version = "6.5.4" #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -427,7 +427,7 @@ guidelines = """ """ [wakeup_system] -enable = true #"是否启用唤醒度系统" +enable = false #"是否启用唤醒度系统" wakeup_threshold = 15.0 #唤醒阈值,达到此值时会被唤醒" private_message_increment = 3.0 #"私聊消息增加的唤醒度" group_mention_increment = 2.0 #"群聊艾特增加的唤醒度" @@ -436,6 +436,21 @@ decay_interval = 30.0 #"唤醒度衰减间隔(秒)" angry_duration = 300.0 #"愤怒状态持续时间(秒)" angry_prompt = "你被人吵醒了非常生气,说话带着怒气" # "被吵醒后的愤怒提示词" +# --- 失眠机制相关参数 --- +enable_insomnia_system = true # 是否启用失眠系统 +# 触发“压力不足型失眠”的睡眠压力阈值 +sleep_pressure_threshold = 30.0 +# 进入“深度睡眠”的睡眠压力阈值 +deep_sleep_threshold = 80.0 +# 压力不足时的失眠基础概率 (0.0 to 1.0) +insomnia_chance_low_pressure = 0.6 +# 压力正常时的失眠基础概率 (0.0 to 1.0) +insomnia_chance_normal_pressure = 0.1 +# 每次AI执行动作后,增加的睡眠压力值 +sleep_pressure_increment = 1.5 +# 睡眠时,每分钟衰减的睡眠压力值 +sleep_pressure_decay_rate = 1.5 + [cross_context] # 跨群聊上下文共享配置 # 这是总开关,用于一键启用或禁用此功能 enable = false