From 02a54fd3c9e722f1026f602edb42222503e0a1e8 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 17 Oct 2025 19:55:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(chat):=20=E5=BC=95=E5=85=A5=E7=9D=A1?= =?UTF-8?q?=E7=9C=A0=E7=B3=BB=E7=BB=9F=EF=BC=8C=E5=9C=A8=E7=9D=A1=E7=9C=A0?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E4=B8=8B=E6=8B=A6=E6=88=AA=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在消息处理的最前端 (`MessageManager`) 增加了对睡眠状态的检查。当系统处于 `SLEEPING` 状态时,将直接拦截所有新消息,不再进行后续处理。 同时,为了配合睡眠系统的正常运作: - 将睡眠系统的定时任务初始化移至 `main.py`,确保其在应用启动时正确加载。 - 在主动思维插件 (`proactive_thinker`) 的定时任务(冷启动和日常唤醒)中增加了睡眠状态检查,避免在睡眠期间触发主动行为。 --- src/chat/message_manager/message_manager.py | 11 + .../sleep_system/sleep_logic.py | 194 ++++++++++++++++++ .../sleep_system/state_manager.py | 190 +++++++++++++++++ .../message_manager/sleep_system/tasks.py | 43 ++++ src/main.py | 9 + .../proacive_thinker_event.py | 9 + 6 files changed, 456 insertions(+) create mode 100644 src/chat/message_manager/sleep_system/sleep_logic.py create mode 100644 src/chat/message_manager/sleep_system/state_manager.py create mode 100644 src/chat/message_manager/sleep_system/tasks.py diff --git a/src/chat/message_manager/message_manager.py b/src/chat/message_manager/message_manager.py index ba9d1b590..a58c82485 100644 --- a/src/chat/message_manager/message_manager.py +++ b/src/chat/message_manager/message_manager.py @@ -19,6 +19,8 @@ from src.config.config import global_config from src.plugin_system.apis.chat_api import get_chat_manager from .distribution_manager import stream_loop_manager +from .sleep_system.state_manager import SleepState, sleep_state_manager + if TYPE_CHECKING: pass @@ -88,6 +90,7 @@ class MessageManager: logger.error(f"启动自适应流管理器失败: {e}") # 启动睡眠和唤醒管理器 + # 睡眠系统的定时任务启动移至 main.py # 启动流循环管理器并设置chatter_manager await stream_loop_manager.start() @@ -142,6 +145,14 @@ class MessageManager: async def add_message(self, stream_id: str, message: DatabaseMessages): """添加消息到指定聊天流""" + # 在消息处理的最前端检查睡眠状态 + current_sleep_state = sleep_state_manager.get_current_state() + if current_sleep_state == SleepState.SLEEPING: + logger.info(f"处于 {current_sleep_state.name} 状态,消息被拦截。") + return # 直接返回,不处理消息 + + # TODO: 在这里为 WOKEN_UP_ANGRY 等未来状态添加特殊处理逻辑 + try: chat_manager = get_chat_manager() chat_stream = await chat_manager.get_stream(stream_id) diff --git a/src/chat/message_manager/sleep_system/sleep_logic.py b/src/chat/message_manager/sleep_system/sleep_logic.py new file mode 100644 index 000000000..7a77f0cb9 --- /dev/null +++ b/src/chat/message_manager/sleep_system/sleep_logic.py @@ -0,0 +1,194 @@ +from datetime import datetime, time, timedelta +import random +from typing import Optional, Tuple + +from src.common.logger import get_logger +from src.config.config import global_config +from src.schedule.schedule_manager import schedule_manager +from .state_manager import SleepState, sleep_state_manager + +logger = get_logger("sleep_logic") + + +class SleepLogic: + """ + 核心睡眠逻辑,睡眠系统的“大脑” + + 负责根据当前的配置、时间、日程表以及状态,判断是否需要切换睡眠状态。 + 它本身是无状态的,所有的状态都读取和写入 SleepStateManager。 + """ + + def check_and_update_sleep_state(self): + """ + 检查并更新当前的睡眠状态,这是整个逻辑的入口。 + 由定时任务周期性调用。 + """ + current_state = sleep_state_manager.get_current_state() + now = datetime.now() + + if current_state == SleepState.AWAKE: + self._check_should_fall_asleep(now) + elif current_state == SleepState.SLEEPING: + self._check_should_wake_up(now) + elif current_state == SleepState.INSOMNIA: + # TODO: 实现失眠逻辑 + # 例如:检查失眠状态是否结束,如果结束则转换回 SLEEPING + pass + elif current_state == SleepState.WOKEN_UP_ANGRY: + # TODO: 实现起床气逻辑 + # 例如:检查生气状态是否结束,如果结束则转换回 SLEEPING 或 AWAKE + pass + + def _check_should_fall_asleep(self, now: datetime): + """ + 当状态为 AWAKE 时,检查是否应该进入睡眠。 + """ + should_sleep, wake_up_time = self._should_be_sleeping(now) + if should_sleep: + logger.info("判断结果:应进入睡眠状态。") + sleep_state_manager.set_state(SleepState.SLEEPING, wake_up=wake_up_time) + + def _check_should_wake_up(self, now: datetime): + """ + 当状态为 SLEEPING 时,检查是否应该醒来。 + 这里包含了处理跨天获取日程的核心逻辑。 + """ + wake_up_time = sleep_state_manager.get_wake_up_time() + + # 核心逻辑:两段式检测 + # 如果 state_manager 中还没有起床时间,说明是昨晚入睡,需要等待今天凌晨的新日程。 + sleep_start_time = sleep_state_manager.get_sleep_start_time() + if not wake_up_time and sleep_start_time and now.date() > sleep_start_time.date(): + logger.debug("当前为睡眠状态但无起床时间,尝试从新日程中解析...") + _, new_wake_up_time = self._get_wakeup_times_from_schedule(now) + + if new_wake_up_time: + logger.info(f"成功从新日程获取到起床时间: {new_wake_up_time.strftime('%H:%M')}") + sleep_state_manager.set_wake_up_time(new_wake_up_time) + wake_up_time = new_wake_up_time + else: + logger.debug("未能获取到新的起床时间,继续睡眠。") + return + else: + logger.info("还没有到达第二天,继续睡眠。") + + if wake_up_time and now >= wake_up_time: + logger.info(f"当前时间 {now.strftime('%H:%M')} 已到达或超过预定起床时间 {wake_up_time.strftime('%H:%M')}。") + sleep_state_manager.set_state(SleepState.AWAKE) + + def _should_be_sleeping(self, now: datetime) -> Tuple[bool, Optional[datetime]]: + """ + 判断在当前时刻,是否应该处于睡眠时间。 + + Returns: + 元组 (是否应该睡眠, 预期的起床时间或None) + """ + sleep_config = global_config.sleep_system + if not sleep_config.enable: + return False, None + + sleep_time, wake_up_time = None, None + + if sleep_config.sleep_by_schedule: + sleep_time, _ = self._get_sleep_times_from_schedule(now) + if not sleep_time: + logger.debug("日程表模式开启,但未找到睡眠时间,使用固定时间作为备用。") + sleep_time, wake_up_time = self._get_fixed_sleep_times(now) + else: + sleep_time, wake_up_time = self._get_fixed_sleep_times(now) + + if not sleep_time: + return False, None + + # 检查当前时间是否在睡眠时间范围内 + if now >= sleep_time: + # 如果起床时间是第二天(通常情况),且当前时间小于起床时间,则在睡眠范围内 + if wake_up_time and wake_up_time > sleep_time and now < wake_up_time: + return True, wake_up_time + # 如果当前时间大于入睡时间,说明已经进入睡眠窗口 + return True, wake_up_time + + return False, None + + def _get_fixed_sleep_times(self, now: datetime) -> Tuple[Optional[datetime], Optional[datetime]]: + """ + 当使用“固定时间”模式时,从此方法计算睡眠和起床时间。 + 会加入配置中的随机偏移量,让作息更自然。 + """ + sleep_config = global_config.sleep_system + try: + sleep_offset = random.randint( + -sleep_config.sleep_time_offset_minutes, sleep_config.sleep_time_offset_minutes + ) + wake_up_offset = random.randint( + -sleep_config.wake_up_time_offset_minutes, sleep_config.wake_up_time_offset_minutes + ) + + sleep_t = datetime.strptime(sleep_config.fixed_sleep_time, "%H:%M").time() + wake_up_t = datetime.strptime(sleep_config.fixed_wake_up_time, "%H:%M").time() + + sleep_time = datetime.combine(now.date(), sleep_t) + timedelta(minutes=sleep_offset) + + # 如果起床时间比睡觉时间早,说明是第二天 + wake_up_day = now.date() + timedelta(days=1) if wake_up_t < sleep_t else now.date() + wake_up_time = datetime.combine(wake_up_day, wake_up_t) + timedelta(minutes=wake_up_offset) + + return sleep_time, wake_up_time + except (ValueError, TypeError) as e: + logger.error(f"解析固定睡眠时间失败: {e}") + return None, None + + def _get_sleep_times_from_schedule(self, now: datetime) -> Tuple[Optional[datetime], Optional[datetime]]: + """ + 当使用“日程表”模式时,从此方法获取睡眠时间。 + 实现了核心逻辑: + - 解析“今天”日程中的睡觉时间。 + """ + # 阶段一:获取当天的睡觉时间 + today_schedule = schedule_manager.today_schedule + sleep_time = None + if today_schedule: + for event in today_schedule: + activity = event.get("activity", "").lower() + if "sleep" in activity or "睡觉" in activity or "休息" in activity: + try: + time_range = event.get("time_range", "") + start_str, _ = time_range.split("-") + sleep_t = datetime.strptime(start_str.strip(), "%H:%M").time() + sleep_time = datetime.combine(now.date(), sleep_t) + break + except (ValueError, AttributeError): + logger.warning(f"解析日程中的睡眠时间失败: {event}") + continue + wake_up_time = None + + return sleep_time, wake_up_time + + def _get_wakeup_times_from_schedule(self, now: datetime) -> Tuple[Optional[datetime], Optional[datetime]]: + """ + 当使用“日程表”模式时,从此方法获取睡眠时间。 + 实现了核心逻辑: + - 解析“今天”日程中的睡觉时间。 + """ + # 阶段一:获取当天的睡觉时间 + today_schedule = schedule_manager.today_schedule + wake_up_time = None + if today_schedule: + for event in today_schedule: + activity = event.get("activity", "").lower() + if "wake_up" in activity or "醒来" in activity or "起床" in activity: + try: + time_range = event.get("time_range", "") + start_str, _ = time_range.split("-") + sleep_t = datetime.strptime(start_str.strip(), "%H:%M").time() + wake_up_time = datetime.combine(now.date(), sleep_t) + break + except (ValueError, AttributeError): + logger.warning(f"解析日程中的睡眠时间失败: {event}") + continue + + return None, wake_up_time + + +# 全局单例 +sleep_logic = SleepLogic() \ No newline at end of file diff --git a/src/chat/message_manager/sleep_system/state_manager.py b/src/chat/message_manager/sleep_system/state_manager.py new file mode 100644 index 000000000..89262cd82 --- /dev/null +++ b/src/chat/message_manager/sleep_system/state_manager.py @@ -0,0 +1,190 @@ +import enum +from datetime import datetime, timedelta +from typing import Any, Dict, Optional + +from src.common.logger import get_logger +from src.manager.local_store_manager import local_storage + +logger = get_logger("sleep_state_manager") + + +class SleepState(enum.Enum): + """ + 定义了所有可能的睡眠状态。 + 使用枚举可以使状态管理更加清晰和安全。 + """ + + AWAKE = "awake" # 清醒状态,正常活动 + SLEEPING = "sleeping" # 沉睡状态,此时应拦截消息 + INSOMNIA = "insomnia" # 失眠状态(为未来功能预留) + WOKEN_UP_ANGRY = "woken_up_angry" # 被吵醒后的生气状态(为未来功能预留) + + +class SleepStateManager: + """ + 睡眠状态管理器 (单例模式) + + 这是整个睡眠系统的数据核心,负责: + 1. 管理当前的睡眠状态(如:是否在睡觉、唤醒度等)。 + 2. 将状态持久化到本地JSON文件(`local_store.json`),实现重启后状态不丢失。 + 3. 提供统一的接口供其他模块查询和修改睡眠状态。 + """ + + _instance = None + _STATE_KEY = "sleep_system_state" # 在 local_store.json 中存储的键名 + + def __new__(cls, *args, **kwargs): + # 实现单例模式,确保全局只有一个状态管理器实例 + if not cls._instance: + cls._instance = super(SleepStateManager, cls).__new__(cls, *args, **kwargs) + return cls._instance + + def __init__(self): + """ + 初始化状态管理器,定义状态数据结构并从本地加载历史状态。 + """ + self.state: Dict[str, Any] = {} + self._default_state() + self.load_state() + + def _default_state(self): + """ + 定义并重置为默认的“清醒”状态。 + 当机器人启动或从睡眠中醒来时调用。 + """ + self.state = { + "state": SleepState.AWAKE.value, + "state_until": None, # 特殊状态(如生气)的自动结束时间 + "sleep_start_time": None, # 本次睡眠的开始时间 + "wake_up_time": None, # 预定的起床时间 + "wakefulness": 0.0, # 唤醒度/清醒值,用于判断是否被吵醒 + "last_checked": None, # 定时任务最后检查的时间 + } + + def load_state(self): + """ + 程序启动时,从 local_storage 加载上一次的状态。 + 如果找不到历史状态,则初始化为默认状态。 + """ + stored_state = local_storage[self._STATE_KEY] + if isinstance(stored_state, dict): + # 合并加载的状态,以防新增字段 + self.state.update(stored_state) + # 确保 state 字段是枚举成员 + if "state" in self.state and not isinstance(self.state["state"], SleepState): + try: + self.state["state"] = SleepState(self.state["state"]) + except ValueError: + logger.warning(f"加载了无效的睡眠状态 '{self.state['state']}',重置为 AWAKE。") + self.state["state"] = SleepState.AWAKE + else: + self.state["state"] = SleepState.AWAKE # 兼容旧数据 + + logger.info(f"成功加载睡眠状态: {self.get_current_state().name}") + else: + logger.info("未找到已存储的睡眠状态,将使用默认值。") + self.save_state() + + def save_state(self): + """ + 将当前内存中的状态保存到 local_storage。 + 在保存前,会将枚举类型的 state 转换为字符串,以便JSON序列化。 + """ + data_to_save = self.state.copy() + # 将 state 枚举成员转换为它的值(字符串) + data_to_save["state"] = self.state["state"] + local_storage[self._STATE_KEY] = data_to_save + logger.debug(f"睡眠状态已保存: {data_to_save}") + + def get_current_state(self) -> SleepState: + """ + 获取当前的睡眠状态。 + 在返回状态前,会先检查特殊状态(如生气)是否已过期。 + """ + # 检查特殊状态是否已过期 + state_until_str = self.state.get("state_until") + if state_until_str: + state_until = datetime.fromisoformat(state_until_str) + if datetime.now() > state_until: + logger.info(f"特殊状态 {self.state['state'].name} 已结束,自动恢复为 SLEEPING。") + # 假设特殊状态(如生气)结束后,是恢复到普通睡眠状态 + self.set_state(SleepState.SLEEPING) + + return self.state["state"] + + def set_state( + self, + new_state: SleepState, + duration_seconds: Optional[float] = None, + sleep_start: Optional[datetime] = None, + wake_up: Optional[datetime] = None, + ): + """ + 核心函数:切换到新的睡眠状态,并更新相关的状态数据。 + """ + current_state = self.get_current_state() + if current_state == new_state: + return # 状态未改变 + + logger.info(f"睡眠状态变更: {current_state.name} -> {new_state.name}") + self.state["state"] = new_state + + if new_state == SleepState.AWAKE: + self._default_state() # 醒来时重置所有状态 + self.state["state"] = SleepState.AWAKE # 确保状态正确 + + elif new_state == SleepState.SLEEPING: + self.state["sleep_start_time"] = (sleep_start or datetime.now()).isoformat() + self.state["wake_up_time"] = wake_up.isoformat() if wake_up else None + self.state["state_until"] = None # 清除特殊状态持续时间 + self.state["wakefulness"] = 0.0 # 进入睡眠时清零唤醒度 + + elif new_state in [SleepState.WOKEN_UP_ANGRY, SleepState.INSOMNIA]: + if duration_seconds: + self.state["state_until"] = (datetime.now() + timedelta(seconds=duration_seconds)).isoformat() + else: + self.state["state_until"] = None + + + self.save_state() + + def update_last_checked(self): + """更新最后检查时间""" + self.state["last_checked"] = datetime.now().isoformat() + self.save_state() + + def get_wake_up_time(self) -> Optional[datetime]: + """获取预定的起床时间,如果已设置的话。""" + wake_up_str = self.state.get("wake_up_time") + if wake_up_str: + try: + return datetime.fromisoformat(wake_up_str) + except (ValueError, TypeError): + return None + return None + + def get_sleep_start_time(self) -> Optional[datetime]: + """获取本次睡眠的开始时间,如果已设置的话。""" + sleep_start_str = self.state.get("sleep_start_time") + if sleep_start_str: + try: + return datetime.fromisoformat(sleep_start_str) + except (ValueError, TypeError): + return None + return None + + def set_wake_up_time(self, wake_up: datetime): + """ + 更新起床时间。 + 主要用于“日程表”模式下,当第二天凌晨拿到新日程时,更新之前未知的起床时间。 + """ + if self.get_current_state() == SleepState.AWAKE: + logger.warning("尝试为清醒状态设置起床时间,操作被忽略。") + return + self.state["wake_up_time"] = wake_up.isoformat() + logger.info(f"更新预定起床时间为: {self.state['wake_up_time']}") + self.save_state() + + +# 全局单例 +sleep_state_manager = SleepStateManager() \ No newline at end of file diff --git a/src/chat/message_manager/sleep_system/tasks.py b/src/chat/message_manager/sleep_system/tasks.py new file mode 100644 index 000000000..7016c8ef0 --- /dev/null +++ b/src/chat/message_manager/sleep_system/tasks.py @@ -0,0 +1,43 @@ +from src.common.logger import get_logger +from src.manager.async_task_manager import AsyncTask, async_task_manager +from .sleep_logic import sleep_logic + +logger = get_logger("sleep_tasks") + + +class SleepSystemCheckTask(AsyncTask): + """ + 睡眠系统周期性检查任务。 + 继承自 AsyncTask,由 async_task_manager 统一管理。 + """ + + def __init__(self, run_interval: int = 60): + """ + 初始化任务。 + Args: + run_interval (int): 任务运行的时间间隔(秒)。默认为60秒检查一次。 + """ + super().__init__(task_name="SleepSystemCheckTask", run_interval=run_interval) + + async def run(self): + """ + 任务的核心执行过程。 + 每次运行时,调用 sleep_logic 的主函数来检查和更新状态。 + """ + logger.debug("睡眠系统定时任务触发,开始检查状态...") + try: + # 调用“大脑”进行一次思考和判断 + sleep_logic.check_and_update_sleep_state() + except Exception as e: + logger.error(f"周期性检查睡眠状态时发生未知错误: {e}", exc_info=True) + + +async def start_sleep_system_tasks(): + """ + 启动睡眠系统的后台定时检查任务。 + 这个函数应该在程序启动时(例如 main.py)被调用。 + """ + logger.info("正在启动睡眠系统后台任务...") + check_task = SleepSystemCheckTask() + await async_task_manager.add_task(check_task) + logger.info("睡眠系统后台任务已成功启动。") diff --git a/src/main.py b/src/main.py index 46c6ca66d..b566f4efc 100644 --- a/src/main.py +++ b/src/main.py @@ -29,6 +29,7 @@ from src.plugin_system.core.event_manager import event_manager from src.plugin_system.core.plugin_manager import plugin_manager from src.schedule.monthly_plan_manager import monthly_plan_manager from src.schedule.schedule_manager import schedule_manager +from src.chat.message_manager.sleep_system.tasks import start_sleep_system_tasks # 插件系统现在使用统一的插件加载器 install(extra_lines=3) @@ -519,6 +520,14 @@ MoFox_Bot(第三方修改版) except Exception as e: logger.error(f"日程表管理器初始化失败: {e}") + # 初始化睡眠系统 + if global_config.sleep_system.enable: + try: + await start_sleep_system_tasks() + logger.info("睡眠系统初始化成功") + except Exception as e: + logger.error(f"睡眠系统初始化失败: {e}") + def _safe_init(self, component_name: str, init_func) -> callable: """安全初始化组件,捕获异常""" diff --git a/src/plugins/built_in/proactive_thinker/proacive_thinker_event.py b/src/plugins/built_in/proactive_thinker/proacive_thinker_event.py index 2f062c0b0..45c55a0df 100644 --- a/src/plugins/built_in/proactive_thinker/proacive_thinker_event.py +++ b/src/plugins/built_in/proactive_thinker/proacive_thinker_event.py @@ -13,6 +13,7 @@ from src.manager.async_task_manager import AsyncTask, async_task_manager from src.plugin_system import BaseEventHandler, EventType from src.plugin_system.apis import chat_api, message_api, person_api from src.plugin_system.base.base_event import HandlerResult +from src.chat.message_manager.sleep_system.state_manager import SleepState, sleep_state_manager from .proactive_thinker_executor import ProactiveThinkerExecutor @@ -38,6 +39,10 @@ class ColdStartTask(AsyncTask): await asyncio.sleep(30) # 延迟以确保所有服务和聊天流已从数据库加载完毕 try: + current_state = sleep_state_manager.get_current_state() + if current_state == SleepState.SLEEPING: + logger.info("bot正在睡觉,跳过本次任务") + return logger.info("【冷启动】开始扫描白名单,唤醒沉睡的聊天流...") # 【修复】增加对私聊总开关的判断 @@ -147,6 +152,10 @@ class ProactiveThinkingTask(AsyncTask): # 计算下一次检查前的休眠时间 next_interval = self._get_next_interval() try: + current_state = sleep_state_manager.get_current_state() + if current_state == SleepState.SLEEPING: + logger.info("bot正在睡觉,跳过本次任务") + return logger.debug(f"【日常唤醒】下一次检查将在 {next_interval:.2f} 秒后进行。") await asyncio.sleep(next_interval)