diff --git a/src/chat/chat_loop/heartFC_chat.py b/src/chat/chat_loop/heartFC_chat.py index 323599e4b..b9fcf058b 100644 --- a/src/chat/chat_loop/heartFC_chat.py +++ b/src/chat/chat_loop/heartFC_chat.py @@ -19,6 +19,7 @@ from .cycle_processor import CycleProcessor from .response_handler import ResponseHandler from .normal_mode_handler import NormalModeHandler from .cycle_tracker import CycleTracker +from .wakeup_manager import WakeUpManager logger = get_logger("hfc") @@ -44,6 +45,10 @@ class HeartFChatting: self.energy_manager = EnergyManager(self.context) self.proactive_thinker = ProactiveThinker(self.context, self.cycle_processor) self.normal_mode_handler = NormalModeHandler(self.context, self.cycle_processor) + self.wakeup_manager = WakeUpManager(self.context) + + # 将唤醒度管理器设置到上下文中 + self.context.wakeup_manager = self.wakeup_manager self._loop_task: Optional[asyncio.Task] = None @@ -90,6 +95,7 @@ class HeartFChatting: await self.energy_manager.start() await self.proactive_thinker.start() + await self.wakeup_manager.start() self._loop_task = asyncio.create_task(self._main_chat_loop()) self._loop_task.add_done_callback(self._handle_loop_completion) @@ -112,6 +118,7 @@ class HeartFChatting: await self.energy_manager.stop() await self.proactive_thinker.stop() + await self.wakeup_manager.stop() if self._loop_task and not self._loop_task.done(): self._loop_task.cancel() @@ -169,16 +176,15 @@ class HeartFChatting: 单次循环体处理 功能说明: - - 检查是否处于睡眠模式,如果是则跳过处理 + - 检查是否处于睡眠模式,如果是则处理唤醒度逻辑 - 获取最近的新消息(过滤机器人自己的消息和命令) - 更新最后消息时间和读取时间 - 根据当前聊天模式执行不同的处理逻辑 - FOCUS模式:直接处理所有消息并检查退出条件 - NORMAL模式:检查进入FOCUS模式的条件,并通过normal_mode_handler处理消息 """ - if schedule_manager.is_sleeping(): - return - + is_sleeping = schedule_manager.is_sleeping(self.wakeup_manager) + recent_messages = message_api.get_messages_by_time_in_chat( chat_id=self.context.stream_id, start_time=self.context.last_read_time, @@ -192,6 +198,13 @@ class HeartFChatting: if recent_messages: self.context.last_message_time = time.time() self.context.last_read_time = time.time() + + # 处理唤醒度逻辑 + if is_sleeping: + self._handle_wakeup_messages(recent_messages) + # 如果仍在睡眠状态,跳过正常处理 + if schedule_manager.is_sleeping(self.wakeup_manager): + return if self.context.loop_mode == ChatMode.FOCUS: if recent_messages: @@ -262,3 +275,47 @@ class HeartFChatting: if self.context.energy_value >= 30: # 如果能量值达到或超过30 self.context.loop_mode = ChatMode.FOCUS # 进入专注模式 + + def _handle_wakeup_messages(self, messages): + """ + 处理休眠状态下的消息,累积唤醒度 + + Args: + messages: 消息列表 + + 功能说明: + - 区分私聊和群聊消息 + - 检查群聊消息是否艾特了机器人 + - 调用唤醒度管理器累积唤醒度 + - 如果达到阈值则唤醒并进入愤怒状态 + """ + if not self.wakeup_manager: + return + + is_private_chat = self.context.chat_stream.group_info is None if self.context.chat_stream else False + + for message in messages: + is_mentioned = False + + # 检查群聊消息是否艾特了机器人 + if not is_private_chat: + # 检查消息中是否包含艾特信息 + message_content = message.get("processed_plain_text", "") + bot_name = global_config.bot.nickname + alias_names = global_config.bot.alias_names or [] + + # 检查是否被艾特(简单的文本匹配) + if f"@{bot_name}" in message_content: + is_mentioned = True + else: + for alias in alias_names: + if f"@{alias}" in message_content: + is_mentioned = True + break + + # 累积唤醒度 + woke_up = self.wakeup_manager.add_wakeup_value(is_private_chat, is_mentioned) + + if woke_up: + logger.info(f"{self.context.log_prefix} 被消息吵醒,进入愤怒状态!") + break diff --git a/src/chat/chat_loop/hfc_context.py b/src/chat/chat_loop/hfc_context.py index 255824a8d..85cdbbc84 100644 --- a/src/chat/chat_loop/hfc_context.py +++ b/src/chat/chat_loop/hfc_context.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Dict, Any +from typing import List, Optional, Dict, Any, TYPE_CHECKING import time from src.chat.message_receive.chat_stream import ChatStream, get_chat_manager from src.person_info.relationship_builder_manager import RelationshipBuilder @@ -7,6 +7,9 @@ from src.plugin_system.base.component_types import ChatMode from src.chat.planner_actions.action_manager import ActionManager from src.chat.chat_loop.hfc_utils import CycleDetail +if TYPE_CHECKING: + from .wakeup_manager import WakeUpManager + class HfcContext: def __init__(self, chat_id: str): """ @@ -20,6 +23,7 @@ class HfcContext: - 包含聊天流、关系构建器、表达学习器等核心组件 - 管理聊天模式、能量值、时间戳等关键状态 - 提供循环历史记录和当前循环详情的存储 + - 集成唤醒度管理器,处理休眠状态下的唤醒机制 Raises: ValueError: 如果找不到对应的聊天流 @@ -46,4 +50,7 @@ class HfcContext: self.history_loop: List[CycleDetail] = [] self.cycle_counter = 0 - self.current_cycle_detail: Optional[CycleDetail] = None \ No newline at end of file + self.current_cycle_detail: Optional[CycleDetail] = None + + # 唤醒度管理器 - 延迟初始化以避免循环导入 + self.wakeup_manager: Optional['WakeUpManager'] = None \ No newline at end of file diff --git a/src/chat/chat_loop/wakeup_manager.py b/src/chat/chat_loop/wakeup_manager.py new file mode 100644 index 000000000..f3a9ee919 --- /dev/null +++ b/src/chat/chat_loop/wakeup_manager.py @@ -0,0 +1,172 @@ +import asyncio +import time +from typing import Optional +from src.common.logger import get_logger +from src.config.config import global_config +from .hfc_context import HfcContext + +logger = get_logger("wakeup") + +class WakeUpManager: + def __init__(self, context: HfcContext): + """ + 初始化唤醒度管理器 + + Args: + context: HFC聊天上下文对象 + + 功能说明: + - 管理休眠状态下的唤醒度累积 + - 处理唤醒度的自然衰减 + - 控制愤怒状态的持续时间 + """ + self.context = context + self.wakeup_value = 0.0 # 当前唤醒度 + self.is_angry = False # 是否处于愤怒状态 + self.angry_start_time = 0.0 # 愤怒状态开始时间 + self.last_decay_time = time.time() # 上次衰减时间 + self._decay_task: Optional[asyncio.Task] = None + + # 从配置文件获取参数 + wakeup_config = global_config.wakeup_system + self.wakeup_threshold = wakeup_config.wakeup_threshold + self.private_message_increment = wakeup_config.private_message_increment + self.group_mention_increment = wakeup_config.group_mention_increment + self.decay_rate = wakeup_config.decay_rate + self.decay_interval = wakeup_config.decay_interval + self.angry_duration = wakeup_config.angry_duration + self.enabled = wakeup_config.enable + + async def start(self): + """启动唤醒度管理器""" + if not self.enabled: + logger.info(f"{self.context.log_prefix} 唤醒度系统已禁用,跳过启动") + return + + if not self._decay_task: + self._decay_task = asyncio.create_task(self._decay_loop()) + self._decay_task.add_done_callback(self._handle_decay_completion) + logger.info(f"{self.context.log_prefix} 唤醒度管理器已启动") + + async def stop(self): + """停止唤醒度管理器""" + if self._decay_task and not self._decay_task.done(): + self._decay_task.cancel() + await asyncio.sleep(0) + logger.info(f"{self.context.log_prefix} 唤醒度管理器已停止") + + def _handle_decay_completion(self, task: asyncio.Task): + """处理衰减任务完成""" + try: + if exception := task.exception(): + logger.error(f"{self.context.log_prefix} 唤醒度衰减任务异常: {exception}") + else: + logger.info(f"{self.context.log_prefix} 唤醒度衰减任务正常结束") + except asyncio.CancelledError: + logger.info(f"{self.context.log_prefix} 唤醒度衰减任务被取消") + + async def _decay_loop(self): + """唤醒度衰减循环""" + while self.context.running: + await asyncio.sleep(self.decay_interval) + + current_time = time.time() + + # 检查愤怒状态是否过期 + if self.is_angry and current_time - self.angry_start_time >= self.angry_duration: + self.is_angry = False + # 通知情绪管理系统清除愤怒状态 + 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} 愤怒状态结束,恢复正常") + + # 唤醒度自然衰减 + if self.wakeup_value > 0: + old_value = self.wakeup_value + 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}") + + def add_wakeup_value(self, is_private_chat: bool, is_mentioned: bool = False) -> bool: + """ + 增加唤醒度值 + + Args: + is_private_chat: 是否为私聊 + is_mentioned: 是否被艾特(仅群聊有效) + + Returns: + bool: 是否达到唤醒阈值 + """ + # 如果系统未启用,直接返回 + if not self.enabled: + return False + + from src.manager.schedule_manager import schedule_manager + + # 只有在休眠状态下才累积唤醒度 + if not schedule_manager.is_sleeping(): + return False + + old_value = self.wakeup_value + + if is_private_chat: + # 私聊每条消息都增加唤醒度 + self.wakeup_value += self.private_message_increment + logger.debug(f"{self.context.log_prefix} 私聊消息增加唤醒度: +{self.private_message_increment}") + elif is_mentioned: + # 群聊只有被艾特才增加唤醒度 + self.wakeup_value += self.group_mention_increment + logger.debug(f"{self.context.log_prefix} 群聊艾特增加唤醒度: +{self.group_mention_increment}") + else: + # 群聊未被艾特,不增加唤醒度 + return False + + logger.info(f"{self.context.log_prefix} 唤醒度变化: {old_value:.1f} -> {self.wakeup_value:.1f} (阈值: {self.wakeup_threshold})") + + # 检查是否达到唤醒阈值 + if self.wakeup_value >= self.wakeup_threshold: + self._trigger_wakeup() + return True + + return False + + def _trigger_wakeup(self): + """触发唤醒,进入愤怒状态""" + self.is_angry = True + self.angry_start_time = time.time() + self.wakeup_value = 0.0 # 重置唤醒度 + + # 通知情绪管理系统进入愤怒状态 + from src.mood.mood_manager import mood_manager + mood_manager.set_angry_from_wakeup(self.context.stream_id) + + logger.info(f"{self.context.log_prefix} 唤醒度达到阈值({self.wakeup_threshold}),被吵醒进入愤怒状态!") + + def get_angry_prompt_addition(self) -> str: + """获取愤怒状态下的提示词补充""" + if self.is_angry: + return "你被人吵醒了非常生气,说话带着怒气" + return "" + + def is_in_angry_state(self) -> bool: + """检查是否处于愤怒状态""" + if self.is_angry: + current_time = time.time() + if current_time - self.angry_start_time >= self.angry_duration: + self.is_angry = False + # 通知情绪管理系统清除愤怒状态 + 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} 愤怒状态自动过期") + return False + return self.is_angry + + def get_status_info(self) -> dict: + """获取当前状态信息""" + return { + "wakeup_value": self.wakeup_value, + "wakeup_threshold": self.wakeup_threshold, + "is_angry": self.is_angry, + "angry_remaining_time": max(0, self.angry_duration - (time.time() - self.angry_start_time)) if self.is_angry else 0 + } \ No newline at end of file diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index 8cda480e8..ce0d7896b 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -757,6 +757,11 @@ class DefaultReplyer: if global_config.mood.enable_mood: chat_mood = mood_manager.get_mood_by_chat_id(chat_id) mood_prompt = chat_mood.mood_state + + # 检查是否有愤怒状态的补充提示词 + angry_prompt_addition = mood_manager.get_angry_prompt_addition(chat_id) + if angry_prompt_addition: + mood_prompt = f"{mood_prompt}。{angry_prompt_addition}" else: mood_prompt = "" @@ -1000,6 +1005,11 @@ class DefaultReplyer: if global_config.mood.enable_mood: chat_mood = mood_manager.get_mood_by_chat_id(chat_id) mood_prompt = chat_mood.mood_state + + # 检查是否有愤怒状态的补充提示词 + angry_prompt_addition = mood_manager.get_angry_prompt_addition(chat_id) + if angry_prompt_addition: + mood_prompt = f"{mood_prompt}。{angry_prompt_addition}" else: mood_prompt = "" diff --git a/src/config/config.py b/src/config/config.py index c9f6a75f3..0b2a6a81b 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -43,6 +43,7 @@ from src.config.official_configs import ( TavilyConfig, AntiPromptInjectionConfig, PluginsConfig, + WakeUpSystemConfig, MonthlyPlanSystemConfig ) @@ -403,6 +404,7 @@ class Config(ValidatedConfigBase): web_search: WebSearchConfig = Field(default_factory=lambda: WebSearchConfig(), description="网络搜索配置") tavily: TavilyConfig = Field(default_factory=lambda: TavilyConfig(), description="Tavily配置") plugins: PluginsConfig = Field(default_factory=lambda: PluginsConfig(), description="插件配置") + wakeup_system: WakeUpSystemConfig = Field(default_factory=lambda: WakeUpSystemConfig(), description="唤醒度系统配置") monthly_plan_system: MonthlyPlanSystemConfig = Field(default_factory=lambda: MonthlyPlanSystemConfig(), description="月层计划系统配置") diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 83abb7d5c..0c5199df3 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -666,6 +666,18 @@ class PluginsConfig(ValidatedConfigBase): centralized_config: bool = Field(default=True, description="是否启用插件配置集中化管理") +class WakeUpSystemConfig(ValidatedConfigBase): + """唤醒度系统配置类""" + + enable: bool = Field(default=True, description="是否启用唤醒度系统") + wakeup_threshold: float = Field(default=15.0, ge=1.0, 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="群聊艾特增加的唤醒度") + decay_rate: float = Field(default=0.2, ge=0.0, description="每次衰减的唤醒度数值") + decay_interval: float = Field(default=30.0, ge=1.0, description="唤醒度衰减间隔(秒)") + angry_duration: float = Field(default=300.0, ge=10.0, description="愤怒状态持续时间(秒)") + + class MonthlyPlanSystemConfig(ValidatedConfigBase): """月层计划系统配置类""" diff --git a/src/manager/schedule_manager.py b/src/manager/schedule_manager.py index 6021fbcbc..cebc432be 100644 --- a/src/manager/schedule_manager.py +++ b/src/manager/schedule_manager.py @@ -312,8 +312,16 @@ class ScheduleManager: continue return None - def is_sleeping(self) -> bool: - """检查当前是否处于休眠时间(日程表的第一项或最后一项)""" + def is_sleeping(self, wakeup_manager=None) -> bool: + """ + 检查当前是否处于休眠时间(日程表的第一项或最后一项) + + Args: + wakeup_manager: 可选的唤醒度管理器,用于检查是否被唤醒 + + Returns: + bool: 是否处于休眠状态 + """ if not global_config.schedule.enable_is_sleep: return False if not self.today_schedule: @@ -325,6 +333,7 @@ class ScheduleManager: first_item = self.today_schedule[0] last_item = self.today_schedule[-1] + is_in_sleep_time = False for item in [first_item, last_item]: try: time_range = item.get("time_range") @@ -338,16 +347,27 @@ class ScheduleManager: if start_time <= end_time: # 同一天内的时间段 if start_time <= now < end_time: - return True + is_in_sleep_time = True + break else: # 跨天的时间段 if now >= start_time or now < end_time: - return True + is_in_sleep_time = True + break except (ValueError, KeyError, AttributeError) as e: logger.warning(f"解析休眠日程事件失败: {item}, 错误: {e}") continue - return False + # 如果不在休眠时间,直接返回False + if not is_in_sleep_time: + return False + + # 如果在休眠时间,检查是否被唤醒度管理器唤醒 + if wakeup_manager and wakeup_manager.is_in_angry_state(): + logger.debug("虽然在休眠时间,但已被唤醒度管理器唤醒") + return False + + return True def _validate_schedule_with_pydantic(self, schedule_data) -> bool: """使用Pydantic验证日程数据格式和完整性""" diff --git a/src/mood/mood_manager.py b/src/mood/mood_manager.py index b70d99b34..69e71f410 100644 --- a/src/mood/mood_manager.py +++ b/src/mood/mood_manager.py @@ -57,6 +57,7 @@ class ChatMood: self.log_prefix = f"[{self.chat_stream.group_info.group_name if self.chat_stream.group_info else self.chat_stream.user_info.user_nickname}]" self.mood_state: str = "感觉很平静" + self.is_angry_from_wakeup: bool = False # 是否因被吵醒而愤怒 self.regression_count: int = 0 @@ -241,9 +242,33 @@ class MoodManager: if mood.chat_id == chat_id: mood.mood_state = "感觉很平静" mood.regression_count = 0 + mood.is_angry_from_wakeup = False return self.mood_list.append(ChatMood(chat_id)) + def set_angry_from_wakeup(self, chat_id: str): + """设置因被吵醒而愤怒的状态""" + mood = self.get_mood_by_chat_id(chat_id) + mood.is_angry_from_wakeup = True + mood.mood_state = "被人吵醒了非常生气" + mood.last_change_time = time.time() + logger.info(f"{mood.log_prefix} 因被吵醒设置为愤怒状态") + + def clear_angry_from_wakeup(self, chat_id: str): + """清除因被吵醒而愤怒的状态""" + mood = self.get_mood_by_chat_id(chat_id) + if mood.is_angry_from_wakeup: + mood.is_angry_from_wakeup = False + mood.mood_state = "感觉很平静" + logger.info(f"{mood.log_prefix} 清除被吵醒的愤怒状态") + + def get_angry_prompt_addition(self, chat_id: str) -> str: + """获取愤怒状态下的提示词补充""" + mood = self.get_mood_by_chat_id(chat_id) + if mood.is_angry_from_wakeup: + return "你被人吵醒了非常生气,说话带着怒气" + return "" + init_prompt() diff --git a/tests/test_wakeup_system.py b/tests/test_wakeup_system.py new file mode 100644 index 000000000..43a78015d --- /dev/null +++ b/tests/test_wakeup_system.py @@ -0,0 +1,293 @@ +import pytest +import asyncio +import time +from unittest.mock import Mock, patch, MagicMock +import sys +import os + +# 添加项目根目录到Python路径 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from src.chat.chat_loop.wakeup_manager import WakeUpManager +from src.chat.chat_loop.hfc_context import HfcContext +from src.config.official_configs import WakeUpSystemConfig + + +class TestWakeUpManager: + """唤醒度管理器测试类""" + + @pytest.fixture + def mock_context(self): + """创建模拟的HFC上下文""" + context = Mock(spec=HfcContext) + context.stream_id = "test_chat_123" + context.log_prefix = "[TEST]" + context.running = True + return context + + @pytest.fixture + def wakeup_config(self): + """创建测试用的唤醒度配置""" + return WakeUpSystemConfig( + enable=True, + wakeup_threshold=15.0, + private_message_increment=3.0, + group_mention_increment=2.0, + decay_rate=0.2, + decay_interval=30.0, + angry_duration=300.0 # 5分钟 + ) + + @pytest.fixture + def wakeup_manager(self, mock_context, wakeup_config): + """创建唤醒度管理器实例""" + with patch('src.chat.chat_loop.wakeup_manager.global_config') as mock_global_config: + mock_global_config.wakeup_system = wakeup_config + manager = WakeUpManager(mock_context) + return manager + + def test_initialization(self, wakeup_manager, wakeup_config): + """测试初始化""" + assert wakeup_manager.wakeup_value == 0.0 + assert wakeup_manager.is_angry == False + assert wakeup_manager.wakeup_threshold == wakeup_config.wakeup_threshold + assert wakeup_manager.private_message_increment == wakeup_config.private_message_increment + assert wakeup_manager.group_mention_increment == wakeup_config.group_mention_increment + assert wakeup_manager.decay_rate == wakeup_config.decay_rate + assert wakeup_manager.decay_interval == wakeup_config.decay_interval + assert wakeup_manager.angry_duration == wakeup_config.angry_duration + assert wakeup_manager.enabled == wakeup_config.enable + + @patch('src.manager.schedule_manager.schedule_manager') + @patch('src.mood.mood_manager.mood_manager') + def test_private_message_wakeup_accumulation(self, mock_mood_manager, mock_schedule_manager, wakeup_manager): + """测试私聊消息唤醒度累积""" + # 模拟休眠状态 + mock_schedule_manager.is_sleeping.return_value = True + + # 发送5条私聊消息 (5 * 3.0 = 15.0,达到阈值) + for i in range(4): + result = wakeup_manager.add_wakeup_value(is_private_chat=True) + assert result == False # 前4条消息不应该触发唤醒 + assert wakeup_manager.wakeup_value == (i + 1) * 3.0 + + # 第5条消息应该触发唤醒 + result = wakeup_manager.add_wakeup_value(is_private_chat=True) + assert result == True + assert wakeup_manager.is_angry == True + assert wakeup_manager.wakeup_value == 0.0 # 唤醒后重置 + + # 验证情绪管理器被调用 + mock_mood_manager.set_angry_from_wakeup.assert_called_once_with("test_chat_123") + + @patch('src.manager.schedule_manager.schedule_manager') + @patch('src.mood.mood_manager.mood_manager') + def test_group_mention_wakeup_accumulation(self, mock_mood_manager, mock_schedule_manager, wakeup_manager): + """测试群聊艾特消息唤醒度累积""" + # 模拟休眠状态 + mock_schedule_manager.is_sleeping.return_value = True + + # 发送7条群聊艾特消息 (7 * 2.0 = 14.0,未达到阈值) + for i in range(7): + result = wakeup_manager.add_wakeup_value(is_private_chat=False, is_mentioned=True) + assert result == False + assert wakeup_manager.wakeup_value == (i + 1) * 2.0 + + # 第8条消息应该触发唤醒 (8 * 2.0 = 16.0,超过阈值15.0) + result = wakeup_manager.add_wakeup_value(is_private_chat=False, is_mentioned=True) + assert result == True + assert wakeup_manager.is_angry == True + assert wakeup_manager.wakeup_value == 0.0 + + # 验证情绪管理器被调用 + mock_mood_manager.set_angry_from_wakeup.assert_called_once_with("test_chat_123") + + @patch('src.manager.schedule_manager.schedule_manager') + def test_group_message_without_mention(self, mock_schedule_manager, wakeup_manager): + """测试群聊未艾特消息不增加唤醒度""" + # 模拟休眠状态 + mock_schedule_manager.is_sleeping.return_value = True + + # 发送群聊消息但未被艾特 + result = wakeup_manager.add_wakeup_value(is_private_chat=False, is_mentioned=False) + assert result == False + assert wakeup_manager.wakeup_value == 0.0 # 不应该增加 + + @patch('src.manager.schedule_manager.schedule_manager') + def test_no_accumulation_when_not_sleeping(self, mock_schedule_manager, wakeup_manager): + """测试非休眠状态下不累积唤醒度""" + # 模拟非休眠状态 + mock_schedule_manager.is_sleeping.return_value = False + + # 发送私聊消息 + result = wakeup_manager.add_wakeup_value(is_private_chat=True) + assert result == False + assert wakeup_manager.wakeup_value == 0.0 # 不应该增加 + + def test_disabled_system(self, mock_context): + """测试系统禁用时的行为""" + disabled_config = WakeUpSystemConfig(enable=False) + + with patch('src.chat.chat_loop.wakeup_manager.global_config') as mock_global_config: + mock_global_config.wakeup_system = disabled_config + manager = WakeUpManager(mock_context) + + with patch('src.manager.schedule_manager.schedule_manager') as mock_schedule_manager: + mock_schedule_manager.is_sleeping.return_value = True + + # 即使发送消息也不应该累积唤醒度 + result = manager.add_wakeup_value(is_private_chat=True) + assert result == False + assert manager.wakeup_value == 0.0 + + @patch('src.mood.mood_manager.mood_manager') + def test_angry_state_expiration(self, mock_mood_manager, wakeup_manager): + """测试愤怒状态过期""" + # 手动设置愤怒状态 + wakeup_manager.is_angry = True + wakeup_manager.angry_start_time = time.time() - 400 # 400秒前开始愤怒(超过300秒持续时间) + + # 检查愤怒状态应该已过期 + is_angry = wakeup_manager.is_in_angry_state() + assert is_angry == False + assert wakeup_manager.is_angry == False + + # 验证情绪管理器被调用清除愤怒状态 + mock_mood_manager.clear_angry_from_wakeup.assert_called_once_with("test_chat_123") + + def test_angry_prompt_addition(self, wakeup_manager): + """测试愤怒状态提示词""" + # 非愤怒状态 + prompt = wakeup_manager.get_angry_prompt_addition() + assert prompt == "" + + # 愤怒状态 + wakeup_manager.is_angry = True + wakeup_manager.angry_start_time = time.time() + prompt = wakeup_manager.get_angry_prompt_addition() + assert "吵醒" in prompt and "生气" in prompt + + def test_status_info(self, wakeup_manager): + """测试状态信息获取""" + # 设置一些状态 + wakeup_manager.wakeup_value = 10.5 + wakeup_manager.is_angry = True + wakeup_manager.angry_start_time = time.time() + + status = wakeup_manager.get_status_info() + + assert status["wakeup_value"] == 10.5 + assert status["wakeup_threshold"] == 15.0 + assert status["is_angry"] == True + assert status["angry_remaining_time"] > 0 + + @pytest.mark.asyncio + async def test_decay_loop(self, wakeup_manager): + """测试衰减循环""" + # 设置初始唤醒度 + wakeup_manager.wakeup_value = 5.0 + + # 模拟一次衰减 + with patch('asyncio.sleep') as mock_sleep: + # 创建一个会立即停止的衰减循环 + wakeup_manager.context.running = False + + # 手动调用衰减逻辑 + if wakeup_manager.wakeup_value > 0: + old_value = wakeup_manager.wakeup_value + wakeup_manager.wakeup_value = max(0, wakeup_manager.wakeup_value - wakeup_manager.decay_rate) + + assert wakeup_manager.wakeup_value == 4.8 # 5.0 - 0.2 = 4.8 + + @pytest.mark.asyncio + @patch('src.mood.mood_manager.mood_manager') + async def test_angry_state_expiration_in_decay_loop(self, mock_mood_manager, wakeup_manager): + """测试衰减循环中愤怒状态过期""" + # 设置过期的愤怒状态 + wakeup_manager.is_angry = True + wakeup_manager.angry_start_time = time.time() - 400 # 400秒前 + + # 手动调用衰减循环中的愤怒状态检查逻辑 + current_time = time.time() + if wakeup_manager.is_angry and current_time - wakeup_manager.angry_start_time >= wakeup_manager.angry_duration: + wakeup_manager.is_angry = False + mock_mood_manager.clear_angry_from_wakeup(wakeup_manager.context.stream_id) + + assert wakeup_manager.is_angry == False + mock_mood_manager.clear_angry_from_wakeup.assert_called_once_with("test_chat_123") + + @pytest.mark.asyncio + async def test_start_stop_lifecycle(self, wakeup_manager): + """测试启动和停止生命周期""" + # 测试启动 + await wakeup_manager.start() + assert wakeup_manager._decay_task is not None + assert not wakeup_manager._decay_task.done() + + # 测试停止 + await wakeup_manager.stop() + assert wakeup_manager._decay_task.cancelled() + + @pytest.mark.asyncio + async def test_disabled_system_start(self, mock_context): + """测试禁用系统的启动行为""" + disabled_config = WakeUpSystemConfig(enable=False) + + with patch('src.chat.chat_loop.wakeup_manager.global_config') as mock_global_config: + mock_global_config.wakeup_system = disabled_config + manager = WakeUpManager(mock_context) + + await manager.start() + assert manager._decay_task is None # 禁用时不应该创建衰减任务 + + +class TestWakeUpSystemIntegration: + """唤醒度系统集成测试""" + + @patch('src.manager.schedule_manager.schedule_manager') + @patch('src.mood.mood_manager.mood_manager') + def test_mixed_message_types(self, mock_mood_manager, mock_schedule_manager): + """测试混合消息类型的唤醒度累积""" + mock_schedule_manager.is_sleeping.return_value = True + + # 创建配置和管理器 + config = WakeUpSystemConfig( + enable=True, + wakeup_threshold=10.0, # 降低阈值便于测试 + private_message_increment=3.0, + group_mention_increment=2.0, + decay_rate=0.2, + decay_interval=30.0, + angry_duration=300.0 + ) + + context = Mock(spec=HfcContext) + context.stream_id = "test_mixed" + context.log_prefix = "[MIXED]" + context.running = True + + with patch('src.chat.chat_loop.wakeup_manager.global_config') as mock_global_config: + mock_global_config.wakeup_system = config + manager = WakeUpManager(context) + + # 发送2条私聊消息 (2 * 3.0 = 6.0) + manager.add_wakeup_value(is_private_chat=True) + manager.add_wakeup_value(is_private_chat=True) + assert manager.wakeup_value == 6.0 + + # 发送2条群聊艾特消息 (2 * 2.0 = 4.0, 总计10.0) + manager.add_wakeup_value(is_private_chat=False, is_mentioned=True) + assert manager.wakeup_value == 8.0 + + # 最后一条消息触发唤醒 + result = manager.add_wakeup_value(is_private_chat=False, is_mentioned=True) + assert result == True + assert manager.is_angry == True + assert manager.wakeup_value == 0.0 + + mock_mood_manager.set_angry_from_wakeup.assert_called_once_with("test_mixed") + + +if __name__ == "__main__": + # 运行测试 + pytest.main([__file__, "-v"]) \ No newline at end of file