refactor(sleep): 重构睡眠管理系统,移除桥接模式

将 `SleepManager` 从 `schedule_bridge` 中独立出来,使其成为一个自包含的组件。现在由 `HeartFChatting` 和 `ScheduleManager` 直接实例化和使用 `SleepManager`,移除了原有的全局单例桥接器。

主要变更:
- 删除了 `schedule_bridge.py`,其功能被直接整合到 `SleepManager` 和调用方中。
- `SleepManager` 内部逻辑被拆分为更小的模块,如 `TimeChecker`、`NotificationSender` 和 `SleepStateSerializer`,以提高内聚性和可测试性。
- `HeartFChatting` 现在直接创建并管理 `SleepManager` 实例,并通过 `HfcContext` 传递给其他需要的组件(如 `WakeUpManager`)。
- `ScheduleManager` 也创建自己的 `SleepManager` 实例来更新日程,简化了依赖关系。
- 简化了 `WakeUpManager` 中获取睡眠状态和重置状态的逻辑,直接通过上下文访问 `SleepManager`。
This commit is contained in:
minecraft1024a
2025-09-06 12:16:11 +08:00
committed by Windpicker-owo
parent 2e6fc99224
commit a2bebd0f34
10 changed files with 272 additions and 262 deletions

View File

@@ -9,8 +9,7 @@ from src.common.logger import get_logger
from src.config.config import global_config from src.config.config import global_config
from src.person_info.relationship_builder_manager import relationship_builder_manager from src.person_info.relationship_builder_manager import relationship_builder_manager
from src.chat.express.expression_learner import expression_learner_manager from src.chat.express.expression_learner import expression_learner_manager
from src.chat.chat_loop.sleep_manager.schedule_bridge import schedule_sleep_bridge from src.chat.chat_loop.sleep_manager.sleep_manager import SleepManager, SleepState
from src.chat.chat_loop.sleep_manager.sleep_manager import SleepState
from src.plugin_system.apis import message_api from src.plugin_system.apis import message_api
from .hfc_context import HfcContext from .hfc_context import HfcContext
@@ -47,10 +46,12 @@ class HeartFChatting:
self.energy_manager = EnergyManager(self.context) self.energy_manager = EnergyManager(self.context)
self.proactive_thinker = ProactiveThinker(self.context, self.cycle_processor) self.proactive_thinker = ProactiveThinker(self.context, self.cycle_processor)
self.wakeup_manager = WakeUpManager(self.context) self.wakeup_manager = WakeUpManager(self.context)
self.sleep_manager = SleepManager()
# 将唤醒度管理器设置到上下文中 # 将唤醒度管理器设置到上下文中
self.context.wakeup_manager = self.wakeup_manager self.context.wakeup_manager = self.wakeup_manager
self.context.energy_manager = self.energy_manager self.context.energy_manager = self.energy_manager
self.context.sleep_manager = self.sleep_manager
# 将HeartFChatting实例设置到上下文中以便其他组件可以调用其方法 # 将HeartFChatting实例设置到上下文中以便其他组件可以调用其方法
self.context.chat_instance = self self.context.chat_instance = self
@@ -352,8 +353,8 @@ class HeartFChatting:
- NORMAL模式检查进入FOCUS模式的条件并通过normal_mode_handler处理消息 - NORMAL模式检查进入FOCUS模式的条件并通过normal_mode_handler处理消息
""" """
# --- 核心状态更新 --- # --- 核心状态更新 ---
await schedule_sleep_bridge.update_sleep_state(self.wakeup_manager) await self.sleep_manager.update_sleep_state(self.wakeup_manager)
current_sleep_state = schedule_sleep_bridge.get_current_sleep_state() current_sleep_state = self.sleep_manager.get_current_sleep_state()
is_sleeping = current_sleep_state == SleepState.SLEEPING is_sleeping = current_sleep_state == SleepState.SLEEPING
is_in_insomnia = current_sleep_state == SleepState.INSOMNIA is_in_insomnia = current_sleep_state == SleepState.INSOMNIA
@@ -383,7 +384,7 @@ class HeartFChatting:
self._handle_wakeup_messages(recent_messages) self._handle_wakeup_messages(recent_messages)
# 再次获取最新状态,因为 handle_wakeup 可能导致状态变为 WOKEN_UP # 再次获取最新状态,因为 handle_wakeup 可能导致状态变为 WOKEN_UP
current_sleep_state = schedule_sleep_bridge.get_current_sleep_state() current_sleep_state = self.sleep_manager.get_current_sleep_state()
if current_sleep_state == SleepState.SLEEPING: if current_sleep_state == SleepState.SLEEPING:
# 只有在纯粹的 SLEEPING 状态下才跳过消息处理 # 只有在纯粹的 SLEEPING 状态下才跳过消息处理
@@ -429,14 +430,14 @@ class HeartFChatting:
# --- 重新入睡逻辑 --- # --- 重新入睡逻辑 ---
# 如果被吵醒了,并且在一定时间内没有新消息,则尝试重新入睡 # 如果被吵醒了,并且在一定时间内没有新消息,则尝试重新入睡
if schedule_sleep_bridge.get_current_sleep_state() == SleepState.WOKEN_UP and not has_new_messages: if self.sleep_manager.get_current_sleep_state() == SleepState.WOKEN_UP and not has_new_messages:
re_sleep_delay = global_config.sleep_system.re_sleep_delay_minutes * 60 re_sleep_delay = global_config.sleep_system.re_sleep_delay_minutes * 60
# 使用 last_message_time 来判断空闲时间 # 使用 last_message_time 来判断空闲时间
if time.time() - self.context.last_message_time > re_sleep_delay: if time.time() - self.context.last_message_time > re_sleep_delay:
logger.info( logger.info(
f"{self.context.log_prefix} 已被唤醒且超过 {re_sleep_delay / 60} 分钟无新消息,尝试重新入睡。" f"{self.context.log_prefix} 已被唤醒且超过 {re_sleep_delay / 60} 分钟无新消息,尝试重新入睡。"
) )
schedule_sleep_bridge.reset_sleep_state_after_wakeup() self.sleep_manager.reset_sleep_state_after_wakeup()
# 保存HFC上下文状态 # 保存HFC上下文状态
self.context.save_context_state() self.context.save_context_state()

View File

@@ -10,6 +10,7 @@ if TYPE_CHECKING:
from .sleep_manager.wakeup_manager import WakeUpManager from .sleep_manager.wakeup_manager import WakeUpManager
from .energy_manager import EnergyManager from .energy_manager import EnergyManager
from .heartFC_chat import HeartFChatting from .heartFC_chat import HeartFChatting
from .sleep_manager.sleep_manager import SleepManager
class HfcContext: class HfcContext:
@@ -61,6 +62,7 @@ class HfcContext:
# 唤醒度管理器 - 延迟初始化以避免循环导入 # 唤醒度管理器 - 延迟初始化以避免循环导入
self.wakeup_manager: Optional["WakeUpManager"] = None self.wakeup_manager: Optional["WakeUpManager"] = None
self.energy_manager: Optional["EnergyManager"] = None self.energy_manager: Optional["EnergyManager"] = None
self.sleep_manager: Optional["SleepManager"] = None
self.focus_energy = 1 self.focus_energy = 1
self.no_reply_consecutive = 0 self.no_reply_consecutive = 0

View File

@@ -0,0 +1,68 @@
import asyncio
import random
import hashlib
from src.common.logger import get_logger
from src.config.config import global_config
from src.plugin_system.apis import send_api, generator_api
logger = get_logger("notification_sender")
class NotificationSender:
@staticmethod
async def send_pre_sleep_notification():
"""异步生成并发送睡前通知"""
try:
groups = global_config.sleep_system.pre_sleep_notification_groups
prompt = global_config.sleep_system.pre_sleep_prompt
if not groups:
logger.info("未配置睡前通知的群组,跳过发送。")
return
if not prompt:
logger.warning("睡前通知的prompt为空跳过发送。")
return
# 为防止消息风暴,稍微延迟一下
await asyncio.sleep(random.uniform(5, 15))
for group_id_str in groups:
try:
# 格式 "platform:group_id"
parts = group_id_str.split(":")
if len(parts) != 2:
logger.warning(f"无效的群组ID格式: {group_id_str}")
continue
platform, group_id = parts
# 使用与 ChatStream.get_stream_id 相同的逻辑生成 stream_id
key = "_".join([platform, group_id])
stream_id = hashlib.md5(key.encode()).hexdigest()
logger.info(f"正在为群组 {group_id_str} (Stream ID: {stream_id}) 生成睡前消息...")
# 调用 generator_api 生成回复
success, reply_set, _ = await generator_api.generate_reply(
chat_id=stream_id, extra_info=prompt, request_type="schedule.pre_sleep_notification"
)
if success and reply_set:
# 提取文本内容并发送
reply_text = "".join([content for msg_type, content in reply_set if msg_type == "text"])
if reply_text:
logger.info(f"向群组 {group_id_str} 发送睡前消息: {reply_text}")
await send_api.text_to_stream(text=reply_text, stream_id=stream_id)
else:
logger.warning(f"为群组 {group_id_str} 生成的回复内容为空。")
else:
logger.error(f"为群组 {group_id_str} 生成睡前消息失败。")
await asyncio.sleep(random.uniform(2, 5)) # 避免发送过快
except Exception as e:
logger.error(f"向群组 {group_id_str} 发送睡前消息失败: {e}")
except Exception as e:
logger.error(f"发送睡前通知任务失败: {e}")

View File

@@ -1,54 +0,0 @@
# mmc/src/chat/chat_loop/sleep_manager/schedule_bridge.py
"""
此模块充当 ScheduleManager 和 SleepManager 之间的桥梁,
将睡眠逻辑与日程生成逻辑解耦。
"""
from typing import Optional, TYPE_CHECKING, List, Dict, Any
from .sleep_manager import SleepManager, SleepState
if TYPE_CHECKING:
from src.chat.chat_loop.sleep_manager.wakeup_manager import WakeUpManager
class ScheduleSleepBridge:
def __init__(self):
# 桥接器现在持有 SleepManager 的唯一实例
self.sleep_manager = SleepManager(self)
self.today_schedule: Optional[List[Dict[str, Any]]] = None
def get_today_schedule(self) -> Optional[List[Dict[str, Any]]]:
"""
向 SleepManager 提供当日日程。
"""
return self.today_schedule
def update_today_schedule(self, schedule: Optional[List[Dict[str, Any]]]):
"""
由 ScheduleManager 调用以更新当日日程。
"""
self.today_schedule = schedule
# --- 代理方法,供应用程序的其他部分调用 ---
def get_current_sleep_state(self) -> SleepState:
"""从 SleepManager 获取当前的睡眠状态。"""
return self.sleep_manager.get_current_sleep_state()
def is_sleeping(self) -> bool:
"""检查当前是否处于正式休眠状态。"""
return self.sleep_manager.is_sleeping()
async def update_sleep_state(self, wakeup_manager: Optional["WakeUpManager"] = None):
"""更新睡眠状态机。"""
await self.sleep_manager.update_sleep_state(wakeup_manager)
def reset_sleep_state_after_wakeup(self):
"""被唤醒后,将状态切换到 WOKEN_UP。"""
self.sleep_manager.reset_sleep_state_after_wakeup()
# 创建一个全局可访问的桥接器单例
schedule_sleep_bridge = ScheduleSleepBridge()

View File

@@ -1,13 +1,13 @@
import asyncio import asyncio
import random import random
from datetime import datetime, timedelta, date, time from datetime import datetime, timedelta, date
from enum import Enum, auto from typing import Optional, TYPE_CHECKING, List, Dict, Any
from typing import Optional, TYPE_CHECKING
from src.common.logger import get_logger from src.common.logger import get_logger
from src.config.config import global_config from src.config.config import global_config
from src.manager.local_store_manager import local_storage from .sleep_state import SleepState, SleepStateSerializer
from src.plugin_system.apis import send_api, generator_api from .time_checker import TimeChecker
from .notification_sender import NotificationSender
if TYPE_CHECKING: if TYPE_CHECKING:
from mmc.src.chat.chat_loop.sleep_manager.wakeup_manager import WakeUpManager from mmc.src.chat.chat_loop.sleep_manager.wakeup_manager import WakeUpManager
@@ -15,21 +15,12 @@ if TYPE_CHECKING:
logger = get_logger("sleep_manager") logger = get_logger("sleep_manager")
class SleepState(Enum):
"""睡眠状态枚举"""
AWAKE = auto() # 完全清醒
INSOMNIA = auto() # 失眠(在理论睡眠时间内保持清醒)
PREPARING_SLEEP = auto() # 准备入睡(缓冲期)
SLEEPING = auto() # 正在休眠
WOKEN_UP = auto() # 被吵醒
class SleepManager: class SleepManager:
def __init__(self, bridge): def __init__(self):
self.bridge = bridge self.time_checker = TimeChecker(self)
self.today_schedule: Optional[List[Dict[str, Any]]] = None
self.last_sleep_log_time = 0 self.last_sleep_log_time = 0
self.sleep_log_interval = 35 # 日志记录间隔,单位秒 self.sleep_log_interval = 35
# --- 统一睡眠状态管理 --- # --- 统一睡眠状态管理 ---
self._current_state: SleepState = SleepState.AWAKE self._current_state: SleepState = SleepState.AWAKE
@@ -37,34 +28,26 @@ class SleepManager:
self._total_delayed_minutes_today: int = 0 self._total_delayed_minutes_today: int = 0
self._last_sleep_check_date: Optional[date] = None self._last_sleep_check_date: Optional[date] = None
self._last_fully_slept_log_time: float = 0 self._last_fully_slept_log_time: float = 0
self._re_sleep_attempt_time: Optional[datetime] = None # 新增:重新入睡的尝试时间 self._re_sleep_attempt_time: Optional[datetime] = None
self._load_sleep_state() self._load_sleep_state()
def get_current_sleep_state(self) -> SleepState: def get_current_sleep_state(self) -> SleepState:
"""获取当前的睡眠状态"""
return self._current_state return self._current_state
def is_sleeping(self) -> bool: def is_sleeping(self) -> bool:
"""检查当前是否处于正式休眠状态"""
return self._current_state == SleepState.SLEEPING return self._current_state == SleepState.SLEEPING
async def update_sleep_state(self, wakeup_manager: Optional["WakeUpManager"] = None): async def update_sleep_state(self, wakeup_manager: Optional["WakeUpManager"] = None):
""" if not global_config.sleep_system.enable:
核心状态机:根据当前情况更新睡眠状态
"""
# --- 基础检查 ---
today_schedule = self.bridge.get_today_schedule()
if not global_config.sleep_system.enable or not today_schedule:
if self._current_state != SleepState.AWAKE: if self._current_state != SleepState.AWAKE:
logger.debug("睡眠系统禁用或无日程,强制设为 AWAKE") logger.debug("睡眠系统禁用,强制设为 AWAKE")
self._current_state = SleepState.AWAKE self._current_state = SleepState.AWAKE
return return
now = datetime.now() now = datetime.now()
today = now.date() today = now.date()
# --- 每日状态重置 ---
if self._last_sleep_check_date != today: if self._last_sleep_check_date != today:
logger.info(f"新的一天 ({today}),重置睡眠状态为 AWAKE。") logger.info(f"新的一天 ({today}),重置睡眠状态为 AWAKE。")
self._total_delayed_minutes_today = 0 self._total_delayed_minutes_today = 0
@@ -73,23 +56,14 @@ class SleepManager:
self._last_sleep_check_date = today self._last_sleep_check_date = today
self._save_sleep_state() self._save_sleep_state()
# --- 判断当前是否为理论上的睡眠时间 --- is_in_theoretical_sleep, activity = self.time_checker.is_in_theoretical_sleep_time(now.time())
is_in_theoretical_sleep, activity = self._is_in_theoretical_sleep_time(now.time())
# ===================================
# 状态机核心逻辑
# ===================================
# 状态:清醒 (AWAKE)
if self._current_state == SleepState.AWAKE: if self._current_state == SleepState.AWAKE:
if is_in_theoretical_sleep: if is_in_theoretical_sleep:
logger.info(f"进入理论休眠时间 '{activity}',开始进行睡眠决策...") logger.info(f"进入理论休眠时间 '{activity}',开始进行睡眠决策...")
# --- 合并后的失眠与弹性睡眠决策逻辑 ---
sleep_pressure = wakeup_manager.context.sleep_pressure if wakeup_manager else 999 sleep_pressure = wakeup_manager.context.sleep_pressure if wakeup_manager else 999
pressure_threshold = global_config.sleep_system.flexible_sleep_pressure_threshold pressure_threshold = global_config.sleep_system.flexible_sleep_pressure_threshold
# 决策1因睡眠压力低而延迟入睡原弹性睡眠
if ( if (
sleep_pressure < pressure_threshold sleep_pressure < pressure_threshold
and self._total_delayed_minutes_today < global_config.sleep_system.max_sleep_delay_minutes and self._total_delayed_minutes_today < global_config.sleep_system.max_sleep_delay_minutes
@@ -101,12 +75,8 @@ class SleepManager:
logger.info( logger.info(
f"睡眠压力 ({sleep_pressure:.1f}) 低于阈值 ({pressure_threshold}),进入失眠状态,延迟入睡 {delay_minutes} 分钟。" f"睡眠压力 ({sleep_pressure:.1f}) 低于阈值 ({pressure_threshold}),进入失眠状态,延迟入睡 {delay_minutes} 分钟。"
) )
# 发送睡前通知
if global_config.sleep_system.enable_pre_sleep_notification: if global_config.sleep_system.enable_pre_sleep_notification:
asyncio.create_task(self._send_pre_sleep_notification()) asyncio.create_task(NotificationSender.send_pre_sleep_notification())
# 决策2进入正常的入睡准备流程
else: else:
buffer_seconds = random.randint(5 * 60, 10 * 60) buffer_seconds = random.randint(5 * 60, 10 * 60)
self._sleep_buffer_end_time = now + timedelta(seconds=buffer_seconds) self._sleep_buffer_end_time = now + timedelta(seconds=buffer_seconds)
@@ -114,14 +84,10 @@ class SleepManager:
logger.info( logger.info(
f"睡眠压力正常或已达今日最大延迟,进入准备入睡状态,将在 {buffer_seconds / 60:.1f} 分钟内入睡。" f"睡眠压力正常或已达今日最大延迟,进入准备入睡状态,将在 {buffer_seconds / 60:.1f} 分钟内入睡。"
) )
# 发送睡前通知
if global_config.sleep_system.enable_pre_sleep_notification: if global_config.sleep_system.enable_pre_sleep_notification:
asyncio.create_task(self._send_pre_sleep_notification()) asyncio.create_task(NotificationSender.send_pre_sleep_notification())
self._save_sleep_state() self._save_sleep_state()
# 状态:失眠 (INSOMNIA)
elif self._current_state == SleepState.INSOMNIA: elif self._current_state == SleepState.INSOMNIA:
if not is_in_theoretical_sleep: if not is_in_theoretical_sleep:
logger.info("已离开理论休眠时间,失眠结束,恢复清醒。") logger.info("已离开理论休眠时间,失眠结束,恢复清醒。")
@@ -145,10 +111,8 @@ class SleepManager:
delay_minutes = 15 delay_minutes = 15
self._total_delayed_minutes_today += delay_minutes self._total_delayed_minutes_today += delay_minutes
self._sleep_buffer_end_time = now + timedelta(minutes=delay_minutes) self._sleep_buffer_end_time = now + timedelta(minutes=delay_minutes)
self._save_sleep_state() self._save_sleep_state()
# 状态:准备入睡 (PREPARING_SLEEP)
elif self._current_state == SleepState.PREPARING_SLEEP: elif self._current_state == SleepState.PREPARING_SLEEP:
if not is_in_theoretical_sleep: if not is_in_theoretical_sleep:
logger.info("准备入睡期间离开理论休眠时间,取消入睡,恢复清醒。") logger.info("准备入睡期间离开理论休眠时间,取消入睡,恢复清醒。")
@@ -161,20 +125,17 @@ class SleepManager:
self._last_fully_slept_log_time = now.timestamp() self._last_fully_slept_log_time = now.timestamp()
self._save_sleep_state() self._save_sleep_state()
# 状态:休眠中 (SLEEPING)
elif self._current_state == SleepState.SLEEPING: elif self._current_state == SleepState.SLEEPING:
if not is_in_theoretical_sleep: if not is_in_theoretical_sleep:
logger.info("理论休眠时间结束,自然醒来。") logger.info("理论休眠时间结束,自然醒来。")
self._current_state = SleepState.AWAKE self._current_state = SleepState.AWAKE
self._save_sleep_state() self._save_sleep_state()
else: else:
# 记录日志
current_timestamp = now.timestamp() current_timestamp = now.timestamp()
if current_timestamp - self.last_sleep_log_time > self.sleep_log_interval: if current_timestamp - self.last_sleep_log_time > self.sleep_log_interval:
logger.info(f"当前处于休眠活动 '{activity}' 中。") logger.info(f"当前处于休眠活动 '{activity}' 中。")
self.last_sleep_log_time = current_timestamp self.last_sleep_log_time = current_timestamp
# 状态:被吵醒 (WOKEN_UP)
elif self._current_state == SleepState.WOKEN_UP: elif self._current_state == SleepState.WOKEN_UP:
if not is_in_theoretical_sleep: if not is_in_theoretical_sleep:
logger.info("理论休眠时间结束,被吵醒的状态自动结束。") logger.info("理论休眠时间结束,被吵醒的状态自动结束。")
@@ -183,13 +144,12 @@ class SleepManager:
self._save_sleep_state() self._save_sleep_state()
elif self._re_sleep_attempt_time and now >= self._re_sleep_attempt_time: elif self._re_sleep_attempt_time and now >= self._re_sleep_attempt_time:
logger.info("被吵醒后经过一段时间,尝试重新入睡...") logger.info("被吵醒后经过一段时间,尝试重新入睡...")
sleep_pressure = wakeup_manager.context.sleep_pressure if wakeup_manager else 999 sleep_pressure = wakeup_manager.context.sleep_pressure if wakeup_manager else 999
pressure_threshold = global_config.sleep_system.flexible_sleep_pressure_threshold pressure_threshold = global_config.sleep_system.flexible_sleep_pressure_threshold
if sleep_pressure >= pressure_threshold: if sleep_pressure >= pressure_threshold:
logger.info("睡眠压力足够,从被吵醒状态转换到准备入睡。") logger.info("睡眠压力足够,从被吵醒状态转换到准备入睡。")
buffer_seconds = random.randint(3 * 60, 8 * 60) # 重新入睡的缓冲期可以短一些 buffer_seconds = random.randint(3 * 60, 8 * 60)
self._sleep_buffer_end_time = now + timedelta(seconds=buffer_seconds) self._sleep_buffer_end_time = now + timedelta(seconds=buffer_seconds)
self._current_state = SleepState.PREPARING_SLEEP self._current_state = SleepState.PREPARING_SLEEP
self._re_sleep_attempt_time = None self._re_sleep_attempt_time = None
@@ -199,156 +159,38 @@ class SleepManager:
logger.info( logger.info(
f"睡眠压力({sleep_pressure:.1f})仍然较低,暂时保持清醒,在 {delay_minutes} 分钟后再次尝试。" f"睡眠压力({sleep_pressure:.1f})仍然较低,暂时保持清醒,在 {delay_minutes} 分钟后再次尝试。"
) )
self._save_sleep_state() self._save_sleep_state()
def reset_sleep_state_after_wakeup(self): def reset_sleep_state_after_wakeup(self):
"""被唤醒后,将状态切换到 WOKEN_UP"""
if self._current_state in [SleepState.PREPARING_SLEEP, SleepState.SLEEPING, SleepState.INSOMNIA]: if self._current_state in [SleepState.PREPARING_SLEEP, SleepState.SLEEPING, SleepState.INSOMNIA]:
logger.info("被唤醒,进入 WOKEN_UP 状态!") logger.info("被唤醒,进入 WOKEN_UP 状态!")
self._current_state = SleepState.WOKEN_UP self._current_state = SleepState.WOKEN_UP
self._sleep_buffer_end_time = None self._sleep_buffer_end_time = None
# 设置一个延迟,之后再尝试重新入睡
re_sleep_delay_minutes = getattr(global_config.sleep_system, "re_sleep_delay_minutes", 10) re_sleep_delay_minutes = getattr(global_config.sleep_system, "re_sleep_delay_minutes", 10)
self._re_sleep_attempt_time = datetime.now() + timedelta(minutes=re_sleep_delay_minutes) self._re_sleep_attempt_time = datetime.now() + timedelta(minutes=re_sleep_delay_minutes)
logger.info(f"将在 {re_sleep_delay_minutes} 分钟后尝试重新入睡。") logger.info(f"将在 {re_sleep_delay_minutes} 分钟后尝试重新入睡。")
self._save_sleep_state() self._save_sleep_state()
def _is_in_theoretical_sleep_time(self, now_time: time) -> tuple[bool, Optional[str]]: def get_today_schedule(self) -> Optional[List[Dict[str, Any]]]:
"""检查当前时间是否落在日程表的任何一个睡眠活动中""" return self.today_schedule
sleep_keywords = ["休眠", "睡觉", "梦乡"]
today_schedule = self.bridge.get_today_schedule()
if today_schedule:
for event in today_schedule:
try:
activity = event.get("activity", "").strip()
time_range = event.get("time_range")
if not activity or not time_range: def update_today_schedule(self, schedule: Optional[List[Dict[str, Any]]]):
continue self.today_schedule = schedule
if any(keyword in activity for keyword in sleep_keywords):
start_str, end_str = time_range.split("-")
start_time = datetime.strptime(start_str.strip(), "%H:%M").time()
end_time = datetime.strptime(end_str.strip(), "%H:%M").time()
if start_time <= end_time: # 同一天
if start_time <= now_time < end_time:
return True, activity
else: # 跨天
if now_time >= start_time or now_time < end_time:
return True, activity
except (ValueError, KeyError, AttributeError) as e:
logger.warning(f"解析日程事件时出错: {event}, 错误: {e}")
continue
return False, None
async def _send_pre_sleep_notification(self):
"""异步生成并发送睡前通知"""
try:
groups = global_config.sleep_system.pre_sleep_notification_groups
prompt = global_config.sleep_system.pre_sleep_prompt
if not groups:
logger.info("未配置睡前通知的群组,跳过发送。")
return
if not prompt:
logger.warning("睡前通知的prompt为空跳过发送。")
return
# 为防止消息风暴,稍微延迟一下
await asyncio.sleep(random.uniform(5, 15))
for group_id_str in groups:
try:
# 格式 "platform:group_id"
parts = group_id_str.split(":")
if len(parts) != 2:
logger.warning(f"无效的群组ID格式: {group_id_str}")
continue
platform, group_id = parts
# 使用与 ChatStream.get_stream_id 相同的逻辑生成 stream_id
import hashlib
key = "_".join([platform, group_id])
stream_id = hashlib.md5(key.encode()).hexdigest()
logger.info(f"正在为群组 {group_id_str} (Stream ID: {stream_id}) 生成睡前消息...")
# 调用 generator_api 生成回复
success, reply_set, _ = await generator_api.generate_reply(
chat_id=stream_id, extra_info=prompt, request_type="schedule.pre_sleep_notification"
)
if success and reply_set:
# 提取文本内容并发送
reply_text = "".join([content for msg_type, content in reply_set if msg_type == "text"])
if reply_text:
logger.info(f"向群组 {group_id_str} 发送睡前消息: {reply_text}")
await send_api.text_to_stream(text=reply_text, stream_id=stream_id)
else:
logger.warning(f"为群组 {group_id_str} 生成的回复内容为空。")
else:
logger.error(f"为群组 {group_id_str} 生成睡前消息失败。")
await asyncio.sleep(random.uniform(2, 5)) # 避免发送过快
except Exception as e:
logger.error(f"向群组 {group_id_str} 发送睡前消息失败: {e}")
except Exception as e:
logger.error(f"发送睡前通知任务失败: {e}")
def _save_sleep_state(self): def _save_sleep_state(self):
"""将当前睡眠状态保存到本地存储""" state_data = {
try: "_current_state": self._current_state,
state = { "_sleep_buffer_end_time": self._sleep_buffer_end_time,
"current_state": self._current_state.name, "_total_delayed_minutes_today": self._total_delayed_minutes_today,
"sleep_buffer_end_time_ts": self._sleep_buffer_end_time.timestamp() "_last_sleep_check_date": self._last_sleep_check_date,
if self._sleep_buffer_end_time "_re_sleep_attempt_time": self._re_sleep_attempt_time,
else None, }
"total_delayed_minutes_today": self._total_delayed_minutes_today, SleepStateSerializer.save(state_data)
"last_sleep_check_date_str": self._last_sleep_check_date.isoformat()
if self._last_sleep_check_date
else None,
"re_sleep_attempt_time_ts": self._re_sleep_attempt_time.timestamp()
if self._re_sleep_attempt_time
else None,
}
local_storage["schedule_sleep_state"] = state
logger.debug(f"已保存睡眠状态: {state}")
except Exception as e:
logger.error(f"保存睡眠状态失败: {e}")
def _load_sleep_state(self): def _load_sleep_state(self):
"""从本地存储加载睡眠状态""" state_data = SleepStateSerializer.load()
try: self._current_state = state_data["_current_state"]
state = local_storage["schedule_sleep_state"] self._sleep_buffer_end_time = state_data["_sleep_buffer_end_time"]
if state and isinstance(state, dict): self._total_delayed_minutes_today = state_data["_total_delayed_minutes_today"]
state_name = state.get("current_state") self._last_sleep_check_date = state_data["_last_sleep_check_date"]
if state_name and hasattr(SleepState, state_name): self._re_sleep_attempt_time = state_data["_re_sleep_attempt_time"]
self._current_state = SleepState[state_name]
end_time_ts = state.get("sleep_buffer_end_time_ts")
if end_time_ts:
self._sleep_buffer_end_time = datetime.fromtimestamp(end_time_ts)
re_sleep_ts = state.get("re_sleep_attempt_time_ts")
if re_sleep_ts:
self._re_sleep_attempt_time = datetime.fromtimestamp(re_sleep_ts)
self._total_delayed_minutes_today = state.get("total_delayed_minutes_today", 0)
date_str = state.get("last_sleep_check_date_str")
if date_str:
self._last_sleep_check_date = datetime.fromisoformat(date_str).date()
logger.info(f"成功从本地存储加载睡眠状态: {state}")
except Exception as e:
logger.warning(f"加载睡眠状态失败,将使用默认值: {e}")

View File

@@ -0,0 +1,77 @@
from enum import Enum, auto
from datetime import datetime
from typing import Optional
from src.common.logger import get_logger
from src.manager.local_store_manager import local_storage
logger = get_logger("sleep_state")
class SleepState(Enum):
"""睡眠状态枚举"""
AWAKE = auto()
INSOMNIA = auto()
PREPARING_SLEEP = auto()
SLEEPING = auto()
WOKEN_UP = auto()
class SleepStateSerializer:
@staticmethod
def save(state_data: dict):
"""将当前睡眠状态保存到本地存储"""
try:
state = {
"current_state": state_data["_current_state"].name,
"sleep_buffer_end_time_ts": state_data["_sleep_buffer_end_time"].timestamp()
if state_data["_sleep_buffer_end_time"]
else None,
"total_delayed_minutes_today": state_data["_total_delayed_minutes_today"],
"last_sleep_check_date_str": state_data["_last_sleep_check_date"].isoformat()
if state_data["_last_sleep_check_date"]
else None,
"re_sleep_attempt_time_ts": state_data["_re_sleep_attempt_time"].timestamp()
if state_data["_re_sleep_attempt_time"]
else None,
}
local_storage["schedule_sleep_state"] = state
logger.debug(f"已保存睡眠状态: {state}")
except Exception as e:
logger.error(f"保存睡眠状态失败: {e}")
@staticmethod
def load() -> dict:
"""从本地存储加载睡眠状态"""
state_data = {
"_current_state": SleepState.AWAKE,
"_sleep_buffer_end_time": None,
"_total_delayed_minutes_today": 0,
"_last_sleep_check_date": None,
"_re_sleep_attempt_time": None,
}
try:
state = local_storage["schedule_sleep_state"]
if state and isinstance(state, dict):
state_name = state.get("current_state")
if state_name and hasattr(SleepState, state_name):
state_data["_current_state"] = SleepState[state_name]
end_time_ts = state.get("sleep_buffer_end_time_ts")
if end_time_ts:
state_data["_sleep_buffer_end_time"] = datetime.fromtimestamp(end_time_ts)
re_sleep_ts = state.get("re_sleep_attempt_time_ts")
if re_sleep_ts:
state_data["_re_sleep_attempt_time"] = datetime.fromtimestamp(re_sleep_ts)
state_data["_total_delayed_minutes_today"] = state.get("total_delayed_minutes_today", 0)
date_str = state.get("last_sleep_check_date_str")
if date_str:
state_data["_last_sleep_check_date"] = datetime.fromisoformat(date_str).date()
logger.info(f"成功从本地存储加载睡眠状态: {state}")
except Exception as e:
logger.warning(f"加载睡眠状态失败,将使用默认值: {e}")
return state_data

View File

@@ -0,0 +1,68 @@
from datetime import datetime, time
from typing import Optional, List, Dict, Any
from src.common.logger import get_logger
from src.config.config import global_config
logger = get_logger("time_checker")
class TimeChecker:
def __init__(self, schedule_source):
self.schedule_source = schedule_source
def is_in_theoretical_sleep_time(self, now_time: time) -> tuple[bool, Optional[str]]:
if global_config.sleep_system.sleep_by_schedule:
if self.schedule_source.get_today_schedule():
return self._is_in_schedule_sleep_time(now_time)
else:
return self._is_in_fixed_sleep_time(now_time)
else:
return self._is_in_fixed_sleep_time(now_time)
def _is_in_schedule_sleep_time(self, now_time: time) -> tuple[bool, Optional[str]]:
"""检查当前时间是否落在日程表的任何一个睡眠活动中"""
sleep_keywords = ["休眠", "睡觉", "梦乡"]
today_schedule = self.schedule_source.get_today_schedule()
if today_schedule:
for event in today_schedule:
try:
activity = event.get("activity", "").strip()
time_range = event.get("time_range")
if not activity or not time_range:
continue
if any(keyword in activity for keyword in sleep_keywords):
start_str, end_str = time_range.split("-")
start_time = datetime.strptime(start_str.strip(), "%H:%M").time()
end_time = datetime.strptime(end_str.strip(), "%H:%M").time()
if start_time <= end_time: # 同一天
if start_time <= now_time < end_time:
return True, activity
else: # 跨天
if now_time >= start_time or now_time < end_time:
return True, activity
except (ValueError, KeyError, AttributeError) as e:
logger.warning(f"解析日程事件时出错: {event}, 错误: {e}")
continue
return False, None
def _is_in_fixed_sleep_time(self, now_time: time) -> tuple[bool, Optional[str]]:
"""检查当前时间是否在固定的睡眠时间内"""
try:
start_time_str = global_config.sleep_system.fixed_sleep_time
end_time_str = global_config.sleep_system.fixed_wake_up_time
start_time = datetime.strptime(start_time_str, "%H:%M").time()
end_time = datetime.strptime(end_time_str, "%H:%M").time()
if start_time <= end_time:
if start_time <= now_time < end_time:
return True, "固定睡眠时间"
else:
if now_time >= start_time or now_time < end_time:
return True, "固定睡眠时间"
except ValueError as e:
logger.error(f"固定的睡眠时间格式不正确,请使用 HH:MM 格式: {e}")
return False, None

View File

@@ -138,10 +138,13 @@ class WakeUpManager:
return False return False
# 只有在休眠且非失眠状态下才累积唤醒度 # 只有在休眠且非失眠状态下才累积唤醒度
from src.schedule.schedule_manager import schedule_manager
from mmc.src.chat.chat_loop.sleep_manager.sleep_manager import SleepState from mmc.src.chat.chat_loop.sleep_manager.sleep_manager import SleepState
current_sleep_state = schedule_manager.get_current_sleep_state() sleep_manager = self.context.sleep_manager
if not sleep_manager:
return False
current_sleep_state = sleep_manager.get_current_sleep_state()
if current_sleep_state != SleepState.SLEEPING: if current_sleep_state != SleepState.SLEEPING:
return False return False
@@ -191,10 +194,9 @@ class WakeUpManager:
mood_manager.set_angry_from_wakeup(self.context.stream_id) mood_manager.set_angry_from_wakeup(self.context.stream_id)
# 通知日程管理器重置睡眠状态 # 通知SleepManager重置睡眠状态
from src.schedule.schedule_manager import schedule_manager if self.context.sleep_manager:
self.context.sleep_manager.reset_sleep_state_after_wakeup()
schedule_manager.reset_sleep_state_after_wakeup()
logger.info(f"{self.context.log_prefix} 唤醒度达到阈值({self.wakeup_threshold}),被吵醒进入愤怒状态!") logger.info(f"{self.context.log_prefix} 唤醒度达到阈值({self.wakeup_threshold}),被吵醒进入愤怒状态!")

View File

@@ -609,6 +609,9 @@ class SleepSystemConfig(ValidatedConfigBase):
"""睡眠系统配置类""" """睡眠系统配置类"""
enable: bool = Field(default=True, description="是否启用睡眠系统") enable: bool = Field(default=True, description="是否启用睡眠系统")
sleep_by_schedule: bool = Field(default=True, description="是否根据日程表进行睡觉")
fixed_sleep_time: str = Field(default="23:00", description="固定的睡觉时间")
fixed_wake_up_time: str = Field(default="07:00", description="固定的起床时间")
wakeup_threshold: float = Field(default=15.0, ge=1.0, description="唤醒阈值,达到此值时会被唤醒") wakeup_threshold: float = Field(default=15.0, ge=1.0, description="唤醒阈值,达到此值时会被唤醒")
private_message_increment: float = Field(default=3.0, ge=0.1, description="私聊消息增加的唤醒度") private_message_increment: float = Field(default=3.0, ge=0.1, description="私聊消息增加的唤醒度")
group_mention_increment: float = Field(default=2.0, ge=0.1, description="群聊艾特增加的唤醒度") group_mention_increment: float = Field(default=2.0, ge=0.1, description="群聊艾特增加的唤醒度")

View File

@@ -7,7 +7,7 @@ from src.common.database.sqlalchemy_models import Schedule, get_db_session
from src.config.config import global_config from src.config.config import global_config
from src.common.logger import get_logger from src.common.logger import get_logger
from src.manager.async_task_manager import AsyncTask, async_task_manager from src.manager.async_task_manager import AsyncTask, async_task_manager
from ..chat.chat_loop.sleep_manager.schedule_bridge import schedule_sleep_bridge from ..chat.chat_loop.sleep_manager.sleep_manager import SleepManager
from .database import update_plan_usage from .database import update_plan_usage
from .llm_generator import ScheduleLLMGenerator from .llm_generator import ScheduleLLMGenerator
from .plan_manager import PlanManager from .plan_manager import PlanManager
@@ -23,6 +23,7 @@ class ScheduleManager:
self.plan_manager = PlanManager() self.plan_manager = PlanManager()
self.daily_task_started = False self.daily_task_started = False
self.schedule_generation_running = False self.schedule_generation_running = False
self.sleep_manager = SleepManager()
async def start_daily_schedule_generation(self): async def start_daily_schedule_generation(self):
if not self.daily_task_started: if not self.daily_task_started:
@@ -44,7 +45,7 @@ class ScheduleManager:
schedule_data = self._load_schedule_from_db(today_str) schedule_data = self._load_schedule_from_db(today_str)
if schedule_data: if schedule_data:
self.today_schedule = schedule_data self.today_schedule = schedule_data
schedule_sleep_bridge.update_today_schedule(self.today_schedule) self.sleep_manager.update_today_schedule(self.today_schedule)
self._log_loaded_schedule(today_str) self._log_loaded_schedule(today_str)
return return
@@ -99,7 +100,7 @@ class ScheduleManager:
if schedule_data: if schedule_data:
self._save_schedule_to_db(today_str, schedule_data) self._save_schedule_to_db(today_str, schedule_data)
self.today_schedule = schedule_data self.today_schedule = schedule_data
schedule_sleep_bridge.update_today_schedule(self.today_schedule) self.sleep_manager.update_today_schedule(self.today_schedule)
self._log_generated_schedule(today_str, schedule_data) self._log_generated_schedule(today_str, schedule_data)
if sampled_plans: if sampled_plans: