From 35157a15213b62b03cf069ce3debec4dcc9a4b71 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Wed, 27 Aug 2025 22:44:26 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=E6=9F=90=E4=B8=AA=E7=8B=90=E7=8B=B8?= =?UTF-8?q?=E5=8F=88=E5=BF=98=E8=AE=B0=E6=94=B9=E7=89=88=E6=9C=AC=E5=8F=B7?= =?UTF-8?q?=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/bot_config_template.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index df9f32f24..168254342 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.5.4" +version = "6.5.5" #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #如果你想要修改配置文件,请递增version的值 From 963087f778ebb7bfd9b6319604f2e38bbcd199aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=85=E8=AF=BA=E7=8B=90?= <212194964+foxcyber907@users.noreply.github.com> Date: Wed, 27 Aug 2025 22:45:07 +0800 Subject: [PATCH 2/7] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20bot=5Fconfig=5Ftemplat?= =?UTF-8?q?e.toml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/bot_config_template.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 168254342..944022c82 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.5.5" +version = "6.5.6" #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #如果你想要修改配置文件,请递增version的值 From 29e18c2db77cd6a79b63ca028f0dab79c5566498 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Wed, 27 Aug 2025 23:13:55 +0800 Subject: [PATCH 3/7] =?UTF-8?q?feat(monthly=5Fplan):=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=9C=88=E5=BA=A6=E8=AE=A1=E5=88=92=E6=95=B0=E9=87=8F=E4=B8=8A?= =?UTF-8?q?=E9=99=90=E5=B9=B6=E8=87=AA=E5=8A=A8=E6=B8=85=E7=90=86=20style:?= =?UTF-8?q?=20=E4=BC=98=E5=8C=96=E6=9C=88=E5=BA=A6=E8=AE=A1=E5=88=92?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E6=97=A5=E5=BF=97=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在数据库模块中新增物理删除月度计划的函数 `delete_plans_by_ids` - 在月度计划管理器中引入 `max_plans_per_month` 配置,用于限制每月计划数量 - 当检测到计划数量超出上限时,自动按创建时间删除最旧的计划以维持数量限制 - 优化了标记计划完成和删除计划时的日志记录,使其输出更详细的计划内容 - 调整了检查现有计划时的日志信息,使其更清晰 --- src/common/database/monthly_plan_db.py | 40 +++++++++++++++++++++++++- src/schedule/monthly_plan_manager.py | 22 ++++++++++++-- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/common/database/monthly_plan_db.py b/src/common/database/monthly_plan_db.py index a65e6f547..01acf2d5a 100644 --- a/src/common/database/monthly_plan_db.py +++ b/src/common/database/monthly_plan_db.py @@ -80,16 +80,54 @@ def mark_plans_completed(plan_ids: List[int]): with get_db_session() as session: 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( MonthlyPlan.id.in_(plan_ids) ).update({"status": "completed"}, synchronize_session=False) session.commit() - logger.info(f"成功将 {len(plan_ids)} 条月度计划标记为已完成。") except Exception as e: logger.error(f"标记月度计划为完成时发生错误: {e}") session.rollback() 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]): """ 将指定ID的计划标记为软删除(兼容旧接口)。 diff --git a/src/schedule/monthly_plan_manager.py b/src/schedule/monthly_plan_manager.py index 2b7899d83..1e25cdf11 100644 --- a/src/schedule/monthly_plan_manager.py +++ b/src/schedule/monthly_plan_manager.py @@ -8,7 +8,9 @@ from src.common.database.monthly_plan_db import ( add_new_plans, get_archived_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.llm_models.utils_model import LLMRequest @@ -67,7 +69,23 @@ class MonthlyPlanManager: logger.info(f" {target_month} 没有任何有效的月度计划,将立即生成。") return await self.generate_monthly_plans(target_month) 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 # 已经有计划,也算成功 async def generate_monthly_plans(self, target_month: Optional[str] = None) -> bool: From d072cde01b594243b0dc7985552d8651c2ab07c0 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Wed, 27 Aug 2025 23:31:46 +0800 Subject: [PATCH 4/7] =?UTF-8?q?fix(config):=20=E4=B8=BA=E5=AE=9E=E9=AA=8C?= =?UTF-8?q?=E6=80=A7=E5=8A=9F=E8=83=BD=E9=85=8D=E7=BD=AE=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 `experimental` 配置项设置 `default_factory`,确保在配置文件中未提供该项时,程序能够正常初始化并使用默认配置,避免因缺少配置而引发的启动错误。 --- src/config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/config.py b/src/config/config.py index 8ffeaa9ab..e403f7ce0 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -373,7 +373,7 @@ class Config(ValidatedConfigBase): chinese_typo: ChineseTypoConfig = Field(..., description="中文错别字配置") response_post_process: ResponsePostProcessConfig = 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消息配置") lpmm_knowledge: LPMMKnowledgeConfig = Field(..., description="LPMM知识配置") tool: ToolConfig = Field(..., description="工具配置") From f34be9728a8a2e25b3b5e7ff1015b6d5f5c14f94 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Thu, 28 Aug 2025 00:45:30 +0800 Subject: [PATCH 5/7] =?UTF-8?q?feat(schedule):=20=E5=BC=95=E5=85=A5?= =?UTF-8?q?=E5=BC=B9=E6=80=A7=E7=9D=A1=E7=9C=A0=E4=B8=8E=E7=9D=A1=E5=89=8D?= =?UTF-8?q?=E9=80=9A=E7=9F=A5=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增了弹性睡眠功能,使AI的入睡行为更加自然。AI现在会根据睡眠压力决定是否延迟入睡,而不是严格按日程表时间立即休眠。 主要更新包括: - **弹性睡眠逻辑**: 在进入理论睡眠时间时,会有一段5-10分钟的准备缓冲期。如果睡眠压力低于阈值,AI会推迟入睡,以增加互动时间。 - **睡前通知**: 在决定入睡后,AI可以自动向指定群组发送晚安消息。 - **配置选项**: 在配置文件中增加了相关选项,允许用户启用/禁用这些功能,并自定义睡眠压力阈值、最大延迟时间、通知群组和提示词。 - **代码重构**: 对 `is_sleeping` 方法进行了重构,将其拆分为理论睡眠时间判断和核心弹性逻辑,提高了代码的可读性和可维护性。 --- src/chat/chat_loop/wakeup_manager.py | 3 +- src/config/official_configs.py | 8 ++ src/schedule/schedule_manager.py | 201 +++++++++++++++++++++------ template/bot_config_template.toml | 17 ++- 4 files changed, 182 insertions(+), 47 deletions(-) diff --git a/src/chat/chat_loop/wakeup_manager.py b/src/chat/chat_loop/wakeup_manager.py index 18344868e..0454d4f7c 100644 --- a/src/chat/chat_loop/wakeup_manager.py +++ b/src/chat/chat_loop/wakeup_manager.py @@ -112,9 +112,8 @@ class WakeUpManager: if not self.enabled: return False - from src.schedule.schedule_manager import schedule_manager - # 只有在休眠状态下才累积唤醒度 + from src.schedule.schedule_manager import schedule_manager if not schedule_manager.is_sleeping(): return False diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 8c7aae355..6a8d47187 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -530,6 +530,14 @@ class ScheduleConfig(ValidatedConfigBase): enable: bool = Field(default=True, description="启用") guidelines: Optional[str] = Field(default=None, description="指导方针") 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="用于生成睡前消息的提示") diff --git a/src/schedule/schedule_manager.py b/src/schedule/schedule_manager.py index 4eeade57f..d1b42fbfb 100644 --- a/src/schedule/schedule_manager.py +++ b/src/schedule/schedule_manager.py @@ -1,5 +1,6 @@ import orjson import asyncio +import random from datetime import datetime, time, timedelta from typing import Optional, List, Dict, Any 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 json_repair import repair_json 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") @@ -128,6 +130,12 @@ class ScheduleManager: self.sleep_log_interval = 35 # 日志记录间隔,单位秒 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): """启动每日零点自动生成新日程的任务""" if not self.daily_task_started: @@ -392,27 +400,97 @@ class ScheduleManager: continue 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: return False if not self.today_schedule: return False - # 从配置获取关键词,如果配置中没有则使用默认列表 - sleep_keywords = ["休眠", "睡觉", "梦乡",] - - now = datetime.now().time() + now = datetime.now() + today = now.date() - # 遍历当天的所有日程 + # --- 每日状态重置 --- + 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: try: activity = event.get("activity", "").strip() @@ -421,47 +499,82 @@ class ScheduleManager: if not activity or not time_range: continue - # 1. 检查活动内容是否包含任一休眠关键词 if any(keyword in activity for keyword in sleep_keywords): - # 2. 如果包含,再检查当前时间是否在该时间段内 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() - is_in_time_range = False if start_time <= end_time: # 同一天 - if start_time <= now < end_time: - is_in_time_range = True + if start_time <= now_time < end_time: + return True, activity else: # 跨天 - if now >= start_time or now < end_time: - is_in_time_range = True - - # 如果时间匹配,则进入最终判断 - 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 - + 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 + + return False, None + + 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: """使用Pydantic验证日程数据格式和完整性""" diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 944022c82..ebec87401 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.5.6" +version = "6.5.7" #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -378,6 +378,21 @@ guidelines = """ """ 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] # 视频分析配置 enable = true # 是否启用视频分析功能 analysis_mode = "batch_frames" # 分析模式:"frame_by_frame"(逐帧分析,非常慢 "建议frames大于8时不要使用这个" ...但是详细)、"batch_frames"(批量分析,快但可能略简单 -其实效果也差不多)或 "auto"(自动选择) From 910b0db5d2bd50d407f51d7c5d0e10f458f28cc7 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Thu, 28 Aug 2025 02:50:02 +0800 Subject: [PATCH 6/7] =?UTF-8?q?feat(core):=20=E5=AE=9E=E7=8E=B0HFC?= =?UTF-8?q?=E5=8F=8A=E7=9D=A1=E7=9C=A0=E7=8A=B6=E6=80=81=E7=9A=84=E6=8C=81?= =?UTF-8?q?=E4=B9=85=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将HFC上下文、唤醒管理器和日程管理器的核心状态保存到本地存储,并在程序启动时加载。这确保了角色的能量、睡眠压力、唤醒度、失眠状态和弹性睡眠进度在重启后能够恢复,增强了长期运行的连续性和稳定性。 主要变更: - **HfcContext**: 增加状态的加载与保存逻辑,持久化能量、睡眠压力和失眠状态。 - **WakeupManager**: 增加状态的加载与保存逻辑,持久化唤醒度及愤怒状态。 - **ScheduleManager**: 增加弹性睡眠状态的加载与保存逻辑,持久化“准备入睡”及延迟睡眠的状态。 - **HeartFChatting**: 在主循环中调用保存HFC上下文状态的方法。 - 调整了失眠和唤醒逻辑,以更好地与持久化状态和弹性睡眠机制协同工作。 --- src/chat/chat_loop/heartFC_chat.py | 3 ++ src/chat/chat_loop/hfc_context.py | 36 ++++++++++++++++++- src/chat/chat_loop/wakeup_manager.py | 44 ++++++++++++++++++++--- src/schedule/schedule_manager.py | 54 +++++++++++++++++++++++++++- 4 files changed, 131 insertions(+), 6 deletions(-) diff --git a/src/chat/chat_loop/heartFC_chat.py b/src/chat/chat_loop/heartFC_chat.py index b6fc3ae07..dbfe541e9 100644 --- a/src/chat/chat_loop/heartFC_chat.py +++ b/src/chat/chat_loop/heartFC_chat.py @@ -265,6 +265,9 @@ class HeartFChatting: # 更新上一帧的睡眠状态 self.context.was_sleeping = is_sleeping + + # 保存HFC上下文状态 + self.context.save_context_state() return has_new_messages diff --git a/src/chat/chat_loop/hfc_context.py b/src/chat/chat_loop/hfc_context.py index bd5c7e25b..8383b2ea1 100644 --- a/src/chat/chat_loop/hfc_context.py +++ b/src/chat/chat_loop/hfc_context.py @@ -1,6 +1,8 @@ from typing import List, Optional, TYPE_CHECKING import time 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.chat.express.expression_learner import ExpressionLearner from src.plugin_system.base.component_types import ChatMode @@ -61,4 +63,36 @@ class HfcContext: # 唤醒度管理器 - 延迟初始化以避免循环导入 self.wakeup_manager: Optional['WakeUpManager'] = None - self.energy_manager: Optional['EnergyManager'] = None \ No newline at end of file + 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, + } + local_storage[self._get_storage_key()] = state + logger = get_logger("hfc_context") + logger.debug(f"{self.log_prefix} 已将HFC上下文状态保存到本地存储: {state}") \ No newline at end of file diff --git a/src/chat/chat_loop/wakeup_manager.py b/src/chat/chat_loop/wakeup_manager.py index 0454d4f7c..9c7c50ade 100644 --- a/src/chat/chat_loop/wakeup_manager.py +++ b/src/chat/chat_loop/wakeup_manager.py @@ -3,6 +3,7 @@ import time from typing import Optional from src.common.logger import get_logger from src.config.config import global_config +from src.manager.local_store_manager import local_storage from .hfc_context import HfcContext logger = get_logger("wakeup") @@ -46,6 +47,33 @@ class WakeUpManager: 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 + + 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): """启动唤醒度管理器""" @@ -89,6 +117,7 @@ class WakeUpManager: from src.mood.mood_manager import mood_manager mood_manager.clear_angry_from_wakeup(self.context.stream_id) logger.info(f"{self.context.log_prefix} 愤怒状态结束,恢复正常") + self._save_wakeup_state() # 唤醒度自然衰减 if self.wakeup_value > 0: @@ -96,6 +125,7 @@ class WakeUpManager: self.wakeup_value = max(0, self.wakeup_value - self.decay_rate) if old_value != self.wakeup_value: 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: """ @@ -112,9 +142,9 @@ class WakeUpManager: if not self.enabled: return False - # 只有在休眠状态下才累积唤醒度 + # 只有在休眠且非失眠状态下才累积唤醒度 from src.schedule.schedule_manager import schedule_manager - if not schedule_manager.is_sleeping(): + if not schedule_manager.is_sleeping() or self.context.is_in_insomnia: return False old_value = self.wakeup_value @@ -142,7 +172,8 @@ class WakeUpManager: if self.wakeup_value >= self.wakeup_threshold: self._trigger_wakeup() return True - + + self._save_wakeup_state() return False def _trigger_wakeup(self): @@ -151,6 +182,8 @@ class WakeUpManager: self.angry_start_time = time.time() self.wakeup_value = 0.0 # 重置唤醒度 + self._save_wakeup_state() + # 通知情绪管理系统进入愤怒状态 from src.mood.mood_manager import mood_manager mood_manager.set_angry_from_wakeup(self.context.stream_id) @@ -204,9 +237,12 @@ class WakeUpManager: return False # 根据睡眠压力决定失眠概率 + from src.schedule.schedule_manager import schedule_manager 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}),触发失眠!") return True else: diff --git a/src/schedule/schedule_manager.py b/src/schedule/schedule_manager.py index d1b42fbfb..ec5b3c63e 100644 --- a/src/schedule/schedule_manager.py +++ b/src/schedule/schedule_manager.py @@ -16,6 +16,7 @@ from src.llm_models.utils_model import LLMRequest from src.common.logger import get_logger from json_repair import repair_json 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 @@ -135,6 +136,10 @@ class ScheduleManager: 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._load_sleep_state() async def start_daily_schedule_generation(self): """启动每日零点自动生成新日程的任务""" @@ -422,11 +427,16 @@ class ScheduleManager: 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: - logger.info("睡眠缓冲期结束,正式进入休眠状态。") + 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() @@ -442,6 +452,8 @@ class ScheduleManager: logger.info("已离开理论休眠时间,取消“准备入睡”状态。") self._is_preparing_sleep = False self._sleep_buffer_end_time = None + self._is_in_voluntary_delay = False + self._save_sleep_state() return False # --- 处理唤醒状态 --- @@ -466,9 +478,11 @@ class ScheduleManager: 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} 分钟内入睡。") @@ -478,6 +492,7 @@ class ScheduleManager: asyncio.create_task(self._send_pre_sleep_notification()) self._is_preparing_sleep = True + self._save_sleep_state() return False # 进入准备阶段,但尚未正式入睡 # --- 经典模式或已在弹性睡眠流程中 --- @@ -576,6 +591,43 @@ class ScheduleManager: 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, + } + 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) + + 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 _validate_schedule_with_pydantic(self, schedule_data) -> bool: """使用Pydantic验证日程数据格式和完整性""" try: From 829ff4cd4fb7e63892b604e886355bee8a645b99 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Thu, 28 Aug 2025 08:48:19 +0800 Subject: [PATCH 7/7] =?UTF-8?q?feat(sleep):=20=E5=AE=9E=E7=8E=B0=E7=9D=A1?= =?UTF-8?q?=E7=9C=A0=E5=94=A4=E9=86=92=E4=B8=8E=E9=87=8D=E6=96=B0=E5=85=A5?= =?UTF-8?q?=E7=9D=A1=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 引入了更完善的睡眠唤醒和重新入睡逻辑,以处理在睡眠期间被消息打扰的情况。 - **唤醒机制**: 当在睡眠时间内收到消息并达到唤醒阈值时,角色会被唤醒并进入愤怒状态。唤醒后,将保持清醒状态处理消息,而不是立即重新入睡。 - **状态持久化**: 新增 `_is_woken_up` 状态到 `schedule_manager`,并将其持久化,以确保在重启后能记住唤醒状态。 - **重新入睡**: 如果角色被吵醒后,在配置的一段时间内(`re_sleep_delay_minutes`)没有收到新消息,系统将自动尝试重新进入睡眠状态,以模拟更自然的行为。 - **上下文同步**: 在唤醒时,`wakeup_manager` 会通知 `schedule_manager` 更新其内部状态,确保系统各模块之间的睡眠状态一致。 --- src/chat/chat_loop/heartFC_chat.py | 21 ++++++++++++++++++--- src/chat/chat_loop/hfc_context.py | 2 ++ src/chat/chat_loop/wakeup_manager.py | 4 ++++ src/schedule/schedule_manager.py | 27 +++++++++++++++++++++++++-- 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/chat/chat_loop/heartFC_chat.py b/src/chat/chat_loop/heartFC_chat.py index dbfe541e9..6ca4dc916 100644 --- a/src/chat/chat_loop/heartFC_chat.py +++ b/src/chat/chat_loop/heartFC_chat.py @@ -242,10 +242,16 @@ class HeartFChatting: # 处理唤醒度逻辑 if is_sleeping: self._handle_wakeup_messages(recent_messages) - # 如果处于失眠状态,则无视睡眠时间,继续处理消息 - # 否则,如果仍然在睡眠(没被吵醒),则跳过本轮处理 - if not self.context.is_in_insomnia and schedule_manager.is_sleeping(self.wakeup_manager): + # 再次检查睡眠状态,因为_handle_wakeup_messages可能会触发唤醒 + current_is_sleeping = schedule_manager.is_sleeping(self.wakeup_manager) + + if not self.context.is_in_insomnia and current_is_sleeping: + # 仍然在睡眠,跳过本轮的消息处理 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: @@ -266,6 +272,15 @@ class HeartFChatting: # 更新上一帧的睡眠状态 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() diff --git a/src/chat/chat_loop/hfc_context.py b/src/chat/chat_loop/hfc_context.py index 8383b2ea1..767ee60bc 100644 --- a/src/chat/chat_loop/hfc_context.py +++ b/src/chat/chat_loop/hfc_context.py @@ -49,6 +49,7 @@ class HfcContext: # 失眠状态 self.is_in_insomnia: bool = False self.insomnia_end_time: float = 0.0 + self.last_wakeup_time: float = 0.0 # 被吵醒的时间 self.last_message_time = time.time() self.last_read_time = time.time() - 10 @@ -92,6 +93,7 @@ class HfcContext: "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") diff --git a/src/chat/chat_loop/wakeup_manager.py b/src/chat/chat_loop/wakeup_manager.py index 9c7c50ade..37fd755aa 100644 --- a/src/chat/chat_loop/wakeup_manager.py +++ b/src/chat/chat_loop/wakeup_manager.py @@ -188,6 +188,10 @@ class WakeUpManager: from src.mood.mood_manager import mood_manager 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}),被吵醒进入愤怒状态!") def get_angry_prompt_addition(self) -> str: diff --git a/src/schedule/schedule_manager.py b/src/schedule/schedule_manager.py index ec5b3c63e..82578046d 100644 --- a/src/schedule/schedule_manager.py +++ b/src/schedule/schedule_manager.py @@ -138,6 +138,7 @@ class ScheduleManager: 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() @@ -453,14 +454,15 @@ class ScheduleManager: 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 wakeup_manager and wakeup_manager.is_in_angry_state(): + 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}' 期间,但已被唤醒。") + logger.info(f"在休眠活动 '{activity}' 期间,但已被唤醒,保持清醒状态。") self.last_sleep_log_time = current_timestamp return False @@ -502,6 +504,16 @@ class ScheduleManager: 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 = ["休眠", "睡觉", "梦乡"] @@ -600,6 +612,7 @@ class ScheduleManager: "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}") @@ -619,6 +632,7 @@ class ScheduleManager: 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: @@ -628,6 +642,15 @@ class ScheduleManager: 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: """使用Pydantic验证日程数据格式和完整性""" try: