feat(schedule): 引入弹性睡眠与睡前通知机制

新增了弹性睡眠功能,使AI的入睡行为更加自然。AI现在会根据睡眠压力决定是否延迟入睡,而不是严格按日程表时间立即休眠。

主要更新包括:
- **弹性睡眠逻辑**: 在进入理论睡眠时间时,会有一段5-10分钟的准备缓冲期。如果睡眠压力低于阈值,AI会推迟入睡,以增加互动时间。
- **睡前通知**: 在决定入睡后,AI可以自动向指定群组发送晚安消息。
- **配置选项**: 在配置文件中增加了相关选项,允许用户启用/禁用这些功能,并自定义睡眠压力阈值、最大延迟时间、通知群组和提示词。
- **代码重构**: 对 `is_sleeping` 方法进行了重构,将其拆分为理论睡眠时间判断和核心弹性逻辑,提高了代码的可读性和可维护性。
This commit is contained in:
tt-P607
2025-08-28 00:45:30 +08:00
committed by Windpicker-owo
parent 8bd7e44297
commit 200cad4630
4 changed files with 182 additions and 47 deletions

View File

@@ -112,9 +112,8 @@ class WakeUpManager:
if not self.enabled: if not self.enabled:
return False return False
from src.schedule.schedule_manager import schedule_manager
# 只有在休眠状态下才累积唤醒度 # 只有在休眠状态下才累积唤醒度
from src.schedule.schedule_manager import schedule_manager
if not schedule_manager.is_sleeping(): if not schedule_manager.is_sleeping():
return False return False

View File

@@ -531,6 +531,14 @@ class ScheduleConfig(ValidatedConfigBase):
guidelines: Optional[str] = Field(default=None, description="指导方针") guidelines: Optional[str] = Field(default=None, description="指导方针")
enable_is_sleep: bool = Field(default=True, description="让AI会根据日程表睡觉和苏醒") enable_is_sleep: bool = Field(default=True, description="让AI会根据日程表睡觉和苏醒")
enable_flexible_sleep: bool = Field(default=True, description="是否启用弹性睡眠")
flexible_sleep_pressure_threshold: float = Field(default=40.0, description="触发弹性睡眠的睡眠压力阈值,低于该值可能延迟入睡")
max_sleep_delay_minutes: int = Field(default=60, description="单日最大延迟入睡分钟数")
enable_pre_sleep_notification: bool = Field(default=True, description="是否启用睡前消息")
pre_sleep_notification_groups: List[str] = Field(default_factory=list, description="接收睡前消息的群号列表, 格式: [\"platform:group_id1\", \"platform:group_id2\"]")
pre_sleep_prompt: str = Field(default="我准备睡觉了,请生成一句简短自然的晚安问候。", description="用于生成睡前消息的提示")
class DependencyManagementConfig(ValidatedConfigBase): class DependencyManagementConfig(ValidatedConfigBase):

View File

@@ -1,5 +1,6 @@
import orjson import orjson
import asyncio import asyncio
import random
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any
from lunar_python import Lunar from lunar_python import Lunar
@@ -15,6 +16,7 @@ from src.llm_models.utils_model import LLMRequest
from src.common.logger import get_logger from src.common.logger import get_logger
from json_repair import repair_json from json_repair import repair_json
from src.manager.async_task_manager import AsyncTask, async_task_manager from src.manager.async_task_manager import AsyncTask, async_task_manager
from src.plugin_system.apis import send_api, generator_api
logger = get_logger("schedule_manager") logger = get_logger("schedule_manager")
@@ -128,6 +130,12 @@ class ScheduleManager:
self.sleep_log_interval = 35 # 日志记录间隔,单位秒 self.sleep_log_interval = 35 # 日志记录间隔,单位秒
self.schedule_generation_running = False # 防止重复生成任务 self.schedule_generation_running = False # 防止重复生成任务
# 弹性睡眠相关状态
self._is_preparing_sleep: bool = False
self._sleep_buffer_end_time: Optional[datetime] = None
self._total_delayed_minutes_today: int = 0
self._last_sleep_check_date: Optional[datetime.date] = None
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:
@@ -392,27 +400,97 @@ class ScheduleManager:
continue continue
return None return None
def is_sleeping(self, wakeup_manager=None) -> bool: def is_sleeping(self, wakeup_manager: Optional["WakeUpManager"] = None) -> bool:
""" """
通过关键词匹配检查当前是否处于休眠时间。 通过关键词匹配、唤醒度、睡眠压力等综合判断是否处于休眠时间。
新增弹性睡眠机制,允许在压力低时延迟入睡,并在入睡前发送通知。
Args:
wakeup_manager: 可选的唤醒度管理器,用于检查是否被唤醒。
Returns:
bool: 是否处于休眠状态。
""" """
from src.chat.chat_loop.wakeup_manager import WakeUpManager
# --- 基础检查 ---
if not global_config.schedule.enable_is_sleep: if not global_config.schedule.enable_is_sleep:
return False return False
if not self.today_schedule: if not self.today_schedule:
return False return False
# 从配置获取关键词,如果配置中没有则使用默认列表 now = datetime.now()
sleep_keywords = ["休眠", "睡觉", "梦乡",] today = now.date()
now = datetime.now().time() # --- 每日状态重置 ---
if self._last_sleep_check_date != today:
logger.info(f"新的一天 ({today}),重置弹性睡眠状态。")
self._total_delayed_minutes_today = 0
self._is_preparing_sleep = False
self._sleep_buffer_end_time = None
self._last_sleep_check_date = today
# --- 检查是否在“准备入睡”的缓冲期 ---
if self._is_preparing_sleep and self._sleep_buffer_end_time:
if now >= self._sleep_buffer_end_time:
logger.info("睡眠缓冲期结束,正式进入休眠状态。")
return True
else:
remaining_seconds = (self._sleep_buffer_end_time - now).total_seconds()
logger.debug(f"处于入睡缓冲期,剩余 {remaining_seconds:.1f} 秒。")
return False
# --- 判断当前是否为理论上的睡眠时间 ---
is_in_theoretical_sleep, activity = self._is_in_theoretical_sleep_time(now.time())
if not is_in_theoretical_sleep:
# 如果不在理论睡眠时间,确保重置准备状态
if self._is_preparing_sleep:
logger.info("已离开理论休眠时间,取消“准备入睡”状态。")
self._is_preparing_sleep = False
self._sleep_buffer_end_time = None
return False
# --- 处理唤醒状态 ---
if wakeup_manager and wakeup_manager.is_in_angry_state():
current_timestamp = now.timestamp()
if current_timestamp - self.last_sleep_log_time > self.sleep_log_interval:
logger.info(f"在休眠活动 '{activity}' 期间,但已被唤醒。")
self.last_sleep_log_time = current_timestamp
return False
# --- 核心:弹性睡眠逻辑 ---
if global_config.schedule.enable_flexible_sleep and not self._is_preparing_sleep:
# 首次进入理论睡眠时间,触发弹性判断
logger.info(f"进入理论休眠时间 '{activity}',开始弹性睡眠判断...")
# 1. 获取睡眠压力
sleep_pressure = wakeup_manager.context.sleep_pressure if wakeup_manager else 999
pressure_threshold = global_config.schedule.flexible_sleep_pressure_threshold
# 2. 判断是否延迟
if sleep_pressure < pressure_threshold and self._total_delayed_minutes_today < global_config.schedule.max_sleep_delay_minutes:
delay_minutes = 15 # 每次延迟15分钟
self._total_delayed_minutes_today += delay_minutes
self._sleep_buffer_end_time = now + timedelta(minutes=delay_minutes)
logger.info(f"睡眠压力 ({sleep_pressure:.1f}) 低于阈值 ({pressure_threshold}),延迟入睡 {delay_minutes} 分钟。今日已累计延迟 {self._total_delayed_minutes_today} 分钟。")
else:
# 3. 计算5-10分钟的入睡缓冲
buffer_seconds = random.randint(5 * 60, 10 * 60)
self._sleep_buffer_end_time = now + timedelta(seconds=buffer_seconds)
logger.info(f"睡眠压力正常或已达今日最大延迟,将在 {buffer_seconds / 60:.1f} 分钟内入睡。")
# 4. 发送睡前通知
if global_config.schedule.enable_pre_sleep_notification:
asyncio.create_task(self._send_pre_sleep_notification())
self._is_preparing_sleep = True
return False # 进入准备阶段,但尚未正式入睡
# --- 经典模式或已在弹性睡眠流程中 ---
current_timestamp = now.timestamp()
if current_timestamp - self.last_sleep_log_time > self.sleep_log_interval:
logger.info(f"当前处于休眠活动 '{activity}' 中 (经典模式)。")
self.last_sleep_log_time = current_timestamp
return True
def _is_in_theoretical_sleep_time(self, now_time: time) -> (bool, Optional[str]):
"""检查当前时间是否落在日程表的任何一个睡眠活动中"""
sleep_keywords = ["休眠", "睡觉", "梦乡"]
# 遍历当天的所有日程
for event in self.today_schedule: for event in self.today_schedule:
try: try:
activity = event.get("activity", "").strip() activity = event.get("activity", "").strip()
@@ -421,47 +499,82 @@ class ScheduleManager:
if not activity or not time_range: if not activity or not time_range:
continue continue
# 1. 检查活动内容是否包含任一休眠关键词
if any(keyword in activity for keyword in sleep_keywords): if any(keyword in activity for keyword in sleep_keywords):
# 2. 如果包含,再检查当前时间是否在该时间段内
start_str, end_str = time_range.split('-') start_str, end_str = time_range.split('-')
start_time = datetime.strptime(start_str.strip(), "%H:%M").time() start_time = datetime.strptime(start_str.strip(), "%H:%M").time()
end_time = datetime.strptime(end_str.strip(), "%H:%M").time() end_time = datetime.strptime(end_str.strip(), "%H:%M").time()
is_in_time_range = False
if start_time <= end_time: # 同一天 if start_time <= end_time: # 同一天
if start_time <= now < end_time: if start_time <= now_time < end_time:
is_in_time_range = True return True, activity
else: # 跨天 else: # 跨天
if now >= start_time or now < end_time: if now_time >= start_time or now_time < end_time:
is_in_time_range = True return True, activity
# 如果时间匹配,则进入最终判断
if is_in_time_range:
# 检查是否被唤醒
if wakeup_manager and wakeup_manager.is_in_angry_state():
current_timestamp = datetime.now().timestamp()
if current_timestamp - self.last_sleep_log_time > self.sleep_log_interval:
logger.info(f"在休眠活动 '{activity}' 期间,但已被唤醒。")
self.last_sleep_log_time = current_timestamp
else:
logger.debug(f"在休眠活动 '{activity}' 期间,但已被唤醒。")
return False
current_timestamp = datetime.now().timestamp()
if current_timestamp - self.last_sleep_log_time > self.sleep_log_interval:
logger.info(f"当前处于休眠活动 '{activity}' 中。")
self.last_sleep_log_time = current_timestamp
else:
logger.debug(f"当前处于休眠活动 '{activity}' 中。")
return True # 找到匹配的休眠活动直接返回True
except (ValueError, KeyError, AttributeError) as e: except (ValueError, KeyError, AttributeError) as e:
logger.warning(f"解析日程事件时出错: {event}, 错误: {e}") logger.warning(f"解析日程事件时出错: {event}, 错误: {e}")
continue continue
# 遍历完所有日程都未找到匹配的休眠活动 return False, None
return False
async def _send_pre_sleep_notification(self):
"""异步生成并发送睡前通知"""
try:
groups = global_config.schedule.pre_sleep_notification_groups
prompt = global_config.schedule.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 _validate_schedule_with_pydantic(self, schedule_data) -> bool: def _validate_schedule_with_pydantic(self, schedule_data) -> bool:
"""使用Pydantic验证日程数据格式和完整性""" """使用Pydantic验证日程数据格式和完整性"""

View File

@@ -1,5 +1,5 @@
[inner] [inner]
version = "6.5.6" version = "6.5.7"
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读----
#如果你想要修改配置文件请递增version的值 #如果你想要修改配置文件请递增version的值
@@ -379,6 +379,21 @@ guidelines = """
""" """
enable_is_sleep = false enable_is_sleep = false
# --- 弹性睡眠与睡前消息 ---
# 是否启用弹性睡眠。启用后AI不会到点立刻入睡而是会根据睡眠压力增加5-10分钟的缓冲并可能因为压力不足而推迟睡眠。
enable_flexible_sleep = true
# 触发弹性睡眠的睡眠压力阈值。当AI的睡眠压力低于此值时可能会推迟入睡。
flexible_sleep_pressure_threshold = 40.0
# 每日最大可推迟入睡的总分钟数。
max_sleep_delay_minutes = 60
# 是否在进入“准备入睡”状态时发送一条消息通知。
enable_pre_sleep_notification = true
# 接收睡前消息的群组列表。格式为: ["platform:group_id1", "platform:group_id2"],例如 ["qq:12345678"]
pre_sleep_notification_groups = []
# 用于生成睡前消息的提示。AI会根据这个提示生成一句晚安问候。
pre_sleep_prompt = "我准备睡觉了,请生成一句简短自然的晚安问候。"
[video_analysis] # 视频分析配置 [video_analysis] # 视频分析配置
enable = true # 是否启用视频分析功能 enable = true # 是否启用视频分析功能
analysis_mode = "batch_frames" # 分析模式:"frame_by_frame"(逐帧分析,非常慢 "建议frames大于8时不要使用这个" ...但是详细)、"batch_frames"(批量分析,快但可能略简单 -其实效果也差不多)或 "auto"(自动选择) analysis_mode = "batch_frames" # 分析模式:"frame_by_frame"(逐帧分析,非常慢 "建议frames大于8时不要使用这个" ...但是详细)、"batch_frames"(批量分析,快但可能略简单 -其实效果也差不多)或 "auto"(自动选择)