This commit is contained in:
雅诺狐
2025-08-28 13:00:36 +08:00
9 changed files with 417 additions and 57 deletions

View File

@@ -242,10 +242,16 @@ class HeartFChatting:
# 处理唤醒度逻辑 # 处理唤醒度逻辑
if is_sleeping: if is_sleeping:
self._handle_wakeup_messages(recent_messages) self._handle_wakeup_messages(recent_messages)
# 如果处于失眠状态,则无视睡眠时间,继续处理消息 # 再次检查睡眠状态因为_handle_wakeup_messages可能会触发唤醒
# 否则,如果仍然在睡眠(没被吵醒),则跳过本轮处理 current_is_sleeping = schedule_manager.is_sleeping(self.wakeup_manager)
if not self.context.is_in_insomnia and schedule_manager.is_sleeping(self.wakeup_manager):
if not self.context.is_in_insomnia and current_is_sleeping:
# 仍然在睡眠,跳过本轮的消息处理
return has_new_messages return has_new_messages
else:
# 从睡眠中被唤醒,需要继续处理本轮消息
logger.info(f"{self.context.log_prefix} 从睡眠中被唤醒,将处理积压的消息。")
self.context.last_wakeup_time = time.time()
# 根据聊天模式处理新消息 # 根据聊天模式处理新消息
if self.context.loop_mode == ChatMode.FOCUS: if self.context.loop_mode == ChatMode.FOCUS:
@@ -266,6 +272,18 @@ class HeartFChatting:
# 更新上一帧的睡眠状态 # 更新上一帧的睡眠状态
self.context.was_sleeping = is_sleeping self.context.was_sleeping = is_sleeping
# --- 重新入睡逻辑 ---
# 如果被吵醒了,并且在一定时间内没有新消息,则尝试重新入睡
if schedule_manager._is_woken_up and not has_new_messages:
re_sleep_delay = global_config.wakeup_system.re_sleep_delay_minutes * 60
# 使用 last_message_time 来判断空闲时间
if time.time() - self.context.last_message_time > re_sleep_delay:
logger.info(f"{self.context.log_prefix} 已被唤醒且超过 {re_sleep_delay / 60} 分钟无新消息,尝试重新入睡。")
schedule_manager.reset_wakeup_state()
# 保存HFC上下文状态
self.context.save_context_state()
return has_new_messages return has_new_messages
def _check_focus_exit(self): def _check_focus_exit(self):

View File

@@ -1,6 +1,8 @@
from typing import List, Optional, TYPE_CHECKING from typing import List, Optional, TYPE_CHECKING
import time import time
from src.chat.message_receive.chat_stream import ChatStream, get_chat_manager from src.chat.message_receive.chat_stream import ChatStream, get_chat_manager
from src.common.logger import get_logger
from src.manager.local_store_manager import local_storage
from src.person_info.relationship_builder_manager import RelationshipBuilder from src.person_info.relationship_builder_manager import RelationshipBuilder
from src.chat.express.expression_learner import ExpressionLearner from src.chat.express.expression_learner import ExpressionLearner
from src.plugin_system.base.component_types import ChatMode from src.plugin_system.base.component_types import ChatMode
@@ -47,6 +49,7 @@ class HfcContext:
# 失眠状态 # 失眠状态
self.is_in_insomnia: bool = False self.is_in_insomnia: bool = False
self.insomnia_end_time: float = 0.0 self.insomnia_end_time: float = 0.0
self.last_wakeup_time: float = 0.0 # 被吵醒的时间
self.last_message_time = time.time() self.last_message_time = time.time()
self.last_read_time = time.time() - 10 self.last_read_time = time.time() - 10
@@ -62,3 +65,36 @@ 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._load_context_state()
def _get_storage_key(self) -> str:
"""获取当前聊天流的本地存储键"""
return f"hfc_context_state_{self.stream_id}"
def _load_context_state(self):
"""从本地存储加载状态"""
state = local_storage[self._get_storage_key()]
if state and isinstance(state, dict):
self.energy_value = state.get("energy_value", 5.0)
self.sleep_pressure = state.get("sleep_pressure", 0.0)
self.is_in_insomnia = state.get("is_in_insomnia", False)
self.insomnia_end_time = state.get("insomnia_end_time", 0.0)
logger = get_logger("hfc_context")
logger.info(f"{self.log_prefix} 成功从本地存储加载HFC上下文状态: {state}")
else:
logger = get_logger("hfc_context")
logger.info(f"{self.log_prefix} 未找到本地HFC上下文状态将使用默认值初始化。")
def save_context_state(self):
"""将当前状态保存到本地存储"""
state = {
"energy_value": self.energy_value,
"sleep_pressure": self.sleep_pressure,
"is_in_insomnia": self.is_in_insomnia,
"insomnia_end_time": self.insomnia_end_time,
"last_wakeup_time": self.last_wakeup_time,
}
local_storage[self._get_storage_key()] = state
logger = get_logger("hfc_context")
logger.debug(f"{self.log_prefix} 已将HFC上下文状态保存到本地存储: {state}")

View File

@@ -3,6 +3,7 @@ import time
from typing import Optional from typing import Optional
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 .hfc_context import HfcContext from .hfc_context import HfcContext
logger = get_logger("wakeup") logger = get_logger("wakeup")
@@ -47,6 +48,33 @@ class WakeUpManager:
self.insomnia_chance_low_pressure = wakeup_config.insomnia_chance_low_pressure self.insomnia_chance_low_pressure = wakeup_config.insomnia_chance_low_pressure
self.insomnia_chance_normal_pressure = wakeup_config.insomnia_chance_normal_pressure self.insomnia_chance_normal_pressure = wakeup_config.insomnia_chance_normal_pressure
self._load_wakeup_state()
def _get_storage_key(self) -> str:
"""获取当前聊天流的本地存储键"""
return f"wakeup_manager_state_{self.context.stream_id}"
def _load_wakeup_state(self):
"""从本地存储加载状态"""
state = local_storage[self._get_storage_key()]
if state and isinstance(state, dict):
self.wakeup_value = state.get("wakeup_value", 0.0)
self.is_angry = state.get("is_angry", False)
self.angry_start_time = state.get("angry_start_time", 0.0)
logger.info(f"{self.context.log_prefix} 成功从本地存储加载唤醒状态: {state}")
else:
logger.info(f"{self.context.log_prefix} 未找到本地唤醒状态,将使用默认值初始化。")
def _save_wakeup_state(self):
"""将当前状态保存到本地存储"""
state = {
"wakeup_value": self.wakeup_value,
"is_angry": self.is_angry,
"angry_start_time": self.angry_start_time,
}
local_storage[self._get_storage_key()] = state
logger.debug(f"{self.context.log_prefix} 已将唤醒状态保存到本地存储: {state}")
async def start(self): async def start(self):
"""启动唤醒度管理器""" """启动唤醒度管理器"""
if not self.enabled: if not self.enabled:
@@ -89,6 +117,7 @@ class WakeUpManager:
from src.mood.mood_manager import mood_manager from src.mood.mood_manager import mood_manager
mood_manager.clear_angry_from_wakeup(self.context.stream_id) mood_manager.clear_angry_from_wakeup(self.context.stream_id)
logger.info(f"{self.context.log_prefix} 愤怒状态结束,恢复正常") logger.info(f"{self.context.log_prefix} 愤怒状态结束,恢复正常")
self._save_wakeup_state()
# 唤醒度自然衰减 # 唤醒度自然衰减
if self.wakeup_value > 0: if self.wakeup_value > 0:
@@ -96,6 +125,7 @@ class WakeUpManager:
self.wakeup_value = max(0, self.wakeup_value - self.decay_rate) self.wakeup_value = max(0, self.wakeup_value - self.decay_rate)
if old_value != self.wakeup_value: if old_value != self.wakeup_value:
logger.debug(f"{self.context.log_prefix} 唤醒度衰减: {old_value:.1f} -> {self.wakeup_value:.1f}") logger.debug(f"{self.context.log_prefix} 唤醒度衰减: {old_value:.1f} -> {self.wakeup_value:.1f}")
self._save_wakeup_state()
def add_wakeup_value(self, is_private_chat: bool, is_mentioned: bool = False) -> bool: def add_wakeup_value(self, is_private_chat: bool, is_mentioned: bool = False) -> bool:
""" """
@@ -112,10 +142,9 @@ 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() or self.context.is_in_insomnia:
# 只有在休眠状态下才累积唤醒度
if not schedule_manager.is_sleeping():
return False return False
old_value = self.wakeup_value old_value = self.wakeup_value
@@ -144,6 +173,7 @@ class WakeUpManager:
self._trigger_wakeup() self._trigger_wakeup()
return True return True
self._save_wakeup_state()
return False return False
def _trigger_wakeup(self): def _trigger_wakeup(self):
@@ -152,10 +182,16 @@ class WakeUpManager:
self.angry_start_time = time.time() self.angry_start_time = time.time()
self.wakeup_value = 0.0 # 重置唤醒度 self.wakeup_value = 0.0 # 重置唤醒度
self._save_wakeup_state()
# 通知情绪管理系统进入愤怒状态 # 通知情绪管理系统进入愤怒状态
from src.mood.mood_manager import mood_manager from src.mood.mood_manager import mood_manager
mood_manager.set_angry_from_wakeup(self.context.stream_id) mood_manager.set_angry_from_wakeup(self.context.stream_id)
# 通知日程管理器重置睡眠状态
from src.schedule.schedule_manager import schedule_manager
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}),被吵醒进入愤怒状态!")
def get_angry_prompt_addition(self) -> str: def get_angry_prompt_addition(self) -> str:
@@ -205,9 +241,12 @@ class WakeUpManager:
return False return False
# 根据睡眠压力决定失眠概率 # 根据睡眠压力决定失眠概率
from src.schedule.schedule_manager import schedule_manager
if pressure < self.sleep_pressure_threshold: if pressure < self.sleep_pressure_threshold:
# 压力不足型失眠 # 压力不足型失眠
if random.random() < self.insomnia_chance_low_pressure: if schedule_manager._is_in_voluntary_delay:
logger.debug(f"{self.context.log_prefix} 处于主动延迟睡眠期间,跳过压力不足型失眠判断。")
elif random.random() < self.insomnia_chance_low_pressure:
logger.info(f"{self.context.log_prefix} 睡眠压力不足 ({pressure:.1f}),触发失眠!") logger.info(f"{self.context.log_prefix} 睡眠压力不足 ({pressure:.1f}),触发失眠!")
return True return True
else: else:

View File

@@ -80,16 +80,54 @@ def mark_plans_completed(plan_ids: List[int]):
with get_db_session() as session: with get_db_session() as session:
try: try:
plans_to_mark = session.query(MonthlyPlan).filter(MonthlyPlan.id.in_(plan_ids)).all()
if not plans_to_mark:
logger.info("没有需要标记为完成的月度计划。")
return
plan_details = "\n".join([f" {i+1}. {plan.plan_text}" for i, plan in enumerate(plans_to_mark)])
logger.info(f"以下 {len(plans_to_mark)} 条月度计划将被标记为已完成:\n{plan_details}")
session.query(MonthlyPlan).filter( session.query(MonthlyPlan).filter(
MonthlyPlan.id.in_(plan_ids) MonthlyPlan.id.in_(plan_ids)
).update({"status": "completed"}, synchronize_session=False) ).update({"status": "completed"}, synchronize_session=False)
session.commit() session.commit()
logger.info(f"成功将 {len(plan_ids)} 条月度计划标记为已完成。")
except Exception as e: except Exception as e:
logger.error(f"标记月度计划为完成时发生错误: {e}") logger.error(f"标记月度计划为完成时发生错误: {e}")
session.rollback() session.rollback()
raise raise
def delete_plans_by_ids(plan_ids: List[int]):
"""
根据ID列表从数据库中物理删除月度计划。
:param plan_ids: 需要删除的计划ID列表。
"""
if not plan_ids:
return
with get_db_session() as session:
try:
# 先查询要删除的计划,用于日志记录
plans_to_delete = session.query(MonthlyPlan).filter(MonthlyPlan.id.in_(plan_ids)).all()
if not plans_to_delete:
logger.info("没有找到需要删除的月度计划。")
return
plan_details = "\n".join([f" {i+1}. {plan.plan_text}" for i, plan in enumerate(plans_to_delete)])
logger.info(f"检测到月度计划超额,将删除以下 {len(plans_to_delete)} 条计划:\n{plan_details}")
# 执行删除
session.query(MonthlyPlan).filter(
MonthlyPlan.id.in_(plan_ids)
).delete(synchronize_session=False)
session.commit()
except Exception as e:
logger.error(f"删除月度计划时发生错误: {e}")
session.rollback()
raise
def soft_delete_plans(plan_ids: List[int]): def soft_delete_plans(plan_ids: List[int]):
""" """
将指定ID的计划标记为软删除兼容旧接口 将指定ID的计划标记为软删除兼容旧接口

View File

@@ -373,7 +373,7 @@ class Config(ValidatedConfigBase):
chinese_typo: ChineseTypoConfig = Field(..., description="中文错别字配置") chinese_typo: ChineseTypoConfig = Field(..., description="中文错别字配置")
response_post_process: ResponsePostProcessConfig = Field(..., description="响应后处理配置") response_post_process: ResponsePostProcessConfig = Field(..., description="响应后处理配置")
response_splitter: ResponseSplitterConfig = Field(..., description="响应分割配置") response_splitter: ResponseSplitterConfig = Field(..., description="响应分割配置")
experimental: ExperimentalConfig = Field(..., description="实验性功能配置") experimental: ExperimentalConfig = Field(default_factory=lambda: ExperimentalConfig(), description="实验性功能配置")
maim_message: MaimMessageConfig = Field(..., description="Maim消息配置") maim_message: MaimMessageConfig = Field(..., description="Maim消息配置")
lpmm_knowledge: LPMMKnowledgeConfig = Field(..., description="LPMM知识配置") lpmm_knowledge: LPMMKnowledgeConfig = Field(..., description="LPMM知识配置")
tool: ToolConfig = Field(..., description="工具配置") tool: ToolConfig = Field(..., description="工具配置")

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

@@ -8,7 +8,9 @@ from src.common.database.monthly_plan_db import (
add_new_plans, add_new_plans,
get_archived_plans_for_month, get_archived_plans_for_month,
archive_active_plans_for_month, archive_active_plans_for_month,
has_active_plans has_active_plans,
get_active_plans_for_month,
delete_plans_by_ids
) )
from src.config.config import global_config, model_config from src.config.config import global_config, model_config
from src.llm_models.utils_model import LLMRequest from src.llm_models.utils_model import LLMRequest
@@ -67,7 +69,23 @@ class MonthlyPlanManager:
logger.info(f" {target_month} 没有任何有效的月度计划,将立即生成。") logger.info(f" {target_month} 没有任何有效的月度计划,将立即生成。")
return await self.generate_monthly_plans(target_month) return await self.generate_monthly_plans(target_month)
else: else:
# logger.info(f"{target_month} 已存在有效的月度计划,跳过生成。") logger.info(f"{target_month} 已存在有效的月度计划")
plans = get_active_plans_for_month(target_month)
# 检查是否超出上限
max_plans = global_config.monthly_plan_system.max_plans_per_month
if len(plans) > max_plans:
logger.warning(f"当前月度计划数量 ({len(plans)}) 超出上限 ({max_plans}),将自动删除多余的计划。")
# 按创建时间升序排序(旧的在前),然后删除超出上限的部分(新的)
plans_to_delete = sorted(plans, key=lambda p: p.created_at)[max_plans:]
delete_ids = [p.id for p in plans_to_delete]
delete_plans_by_ids(delete_ids)
# 重新获取计划列表
plans = get_active_plans_for_month(target_month)
if plans:
plan_texts = "\n".join([f" {i+1}. {plan.plan_text}" for i, plan in enumerate(plans)])
logger.info(f"当前月度计划内容:\n{plan_texts}")
return True # 已经有计划,也算成功 return True # 已经有计划,也算成功
async def generate_monthly_plans(self, target_month: Optional[str] = None) -> bool: async def generate_monthly_plans(self, target_month: Optional[str] = None) -> bool:

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,8 @@ 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.manager.local_store_manager import local_storage
from src.plugin_system.apis import send_api, generator_api
logger = get_logger("schedule_manager") logger = get_logger("schedule_manager")
@@ -128,6 +131,17 @@ 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
self._last_fully_slept_log_time: float = 0
self._is_in_voluntary_delay: bool = False # 新增:标记是否处于主动延迟睡眠状态
self._is_woken_up: bool = False # 新增:标记是否被吵醒
self._load_sleep_state()
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 +406,118 @@ 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
self._is_in_voluntary_delay = False
self._save_sleep_state()
# --- 检查是否在“准备入睡”的缓冲期 ---
if self._is_preparing_sleep and self._sleep_buffer_end_time:
if now >= self._sleep_buffer_end_time:
current_timestamp = now.timestamp()
if current_timestamp - self._last_fully_slept_log_time > 45:
logger.info("睡眠缓冲期结束,正式进入休眠状态。")
self._last_fully_slept_log_time = current_timestamp
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
self._is_in_voluntary_delay = False
self._is_woken_up = False # 离开睡眠时间,重置唤醒状态
self._save_sleep_state()
return False
# --- 处理唤醒状态 ---
if self._is_woken_up:
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)
self._is_in_voluntary_delay = True # 标记进入主动延迟
logger.info(f"睡眠压力 ({sleep_pressure:.1f}) 低于阈值 ({pressure_threshold}),延迟入睡 {delay_minutes} 分钟。今日已累计延迟 {self._total_delayed_minutes_today} 分钟。")
else:
# 3. 计算5-10分钟的入睡缓冲
self._is_in_voluntary_delay = False # 非主动延迟
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
self._save_sleep_state()
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 reset_sleep_state_after_wakeup(self):
"""被唤醒后重置睡眠状态"""
if self._is_preparing_sleep or self.is_sleeping():
logger.info("被唤醒,重置所有睡眠准备状态,恢复清醒!")
self._is_preparing_sleep = False
self._sleep_buffer_end_time = None
self._is_in_voluntary_delay = False
self._is_woken_up = True # 标记为已被唤醒
self._save_sleep_state()
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 +526,130 @@ 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 _save_sleep_state(self):
"""将当前弹性睡眠状态保存到本地存储"""
try:
state = {
"is_preparing_sleep": self._is_preparing_sleep,
"sleep_buffer_end_time_ts": self._sleep_buffer_end_time.timestamp() if self._sleep_buffer_end_time else None,
"total_delayed_minutes_today": self._total_delayed_minutes_today,
"last_sleep_check_date_str": self._last_sleep_check_date.isoformat() if self._last_sleep_check_date else None,
"is_in_voluntary_delay": self._is_in_voluntary_delay,
"is_woken_up": self._is_woken_up,
}
local_storage["schedule_sleep_state"] = state
logger.debug(f"已保存睡眠状态: {state}")
except Exception as e:
logger.error(f"保存睡眠状态失败: {e}")
def _load_sleep_state(self):
"""从本地存储加载弹性睡眠状态"""
try:
state = local_storage["schedule_sleep_state"]
if state and isinstance(state, dict):
self._is_preparing_sleep = state.get("is_preparing_sleep", False)
end_time_ts = state.get("sleep_buffer_end_time_ts")
if end_time_ts:
self._sleep_buffer_end_time = datetime.fromtimestamp(end_time_ts)
self._total_delayed_minutes_today = state.get("total_delayed_minutes_today", 0)
self._is_in_voluntary_delay = state.get("is_in_voluntary_delay", False)
self._is_woken_up = state.get("is_woken_up", False)
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}")
def reset_wakeup_state(self):
"""重置被唤醒的状态,允许重新尝试入睡"""
if self._is_woken_up:
logger.info("重置唤醒状态,将重新尝试入睡。")
self._is_woken_up = False
self._is_preparing_sleep = False # 允许重新进入弹性睡眠判断
self._sleep_buffer_end_time = None
self._save_sleep_state()
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.4" version = "6.5.7"
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读----
#如果你想要修改配置文件请递增version的值 #如果你想要修改配置文件请递增version的值
@@ -378,6 +378,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"(自动选择)