From a485aaf4ad78313fd5fa3cd8d4fba0e2f8c62fe6 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 29 Aug 2025 13:56:24 +0800 Subject: [PATCH 1/9] refactor(config): rename wakeup_system to sleep_system for clarity This commit refactors the entire "wakeup system" to be named "sleep system". This change provides a more intuitive and accurate name for the functionality, which manages the AI's sleep cycles, sleep pressure, and related behaviors like insomnia and flexible sleep schedules. The renaming has been applied consistently across all relevant files, including: - Configuration models (`WakeUpSystemConfig` -> `SleepSystemConfig`) - Configuration files (`bot_config_template.toml`) - Core application logic that references these configurations. Additionally, flexible sleep and pre-sleep notification settings have been moved from the `ScheduleConfig` to the new `SleepSystemConfig` to centralize all sleep-related parameters. --- src/chat/chat_loop/cycle_processor.py | 2 +- src/chat/chat_loop/energy_manager.py | 4 +- src/chat/chat_loop/heartFC_chat.py | 6 +-- src/chat/chat_loop/wakeup_manager.py | 28 ++++++------- src/config/config.py | 4 +- src/config/official_configs.py | 23 +++++------ template/bot_config_template.toml | 58 +++++++++++---------------- 7 files changed, 56 insertions(+), 69 deletions(-) diff --git a/src/chat/chat_loop/cycle_processor.py b/src/chat/chat_loop/cycle_processor.py index 4a946bb72..8975a0c83 100644 --- a/src/chat/chat_loop/cycle_processor.py +++ b/src/chat/chat_loop/cycle_processor.py @@ -133,7 +133,7 @@ class CycleProcessor: await stop_typing() # 在一轮动作执行完毕后,增加睡眠压力 - if self.context.energy_manager and global_config.wakeup_system.enable_insomnia_system: + if self.context.energy_manager and global_config.sleep_system.enable_insomnia_system: if action_type not in ["no_reply", "no_action"]: self.context.energy_manager.increase_sleep_pressure() diff --git a/src/chat/chat_loop/energy_manager.py b/src/chat/chat_loop/energy_manager.py index a2c444326..55e5f9556 100644 --- a/src/chat/chat_loop/energy_manager.py +++ b/src/chat/chat_loop/energy_manager.py @@ -98,7 +98,7 @@ class EnergyManager: if is_sleeping: # 睡眠中:减少睡眠压力 - decay_per_10s = global_config.wakeup_system.sleep_pressure_decay_rate / 6 + decay_per_10s = global_config.sleep_system.sleep_pressure_decay_rate / 6 self.context.sleep_pressure -= decay_per_10s self.context.sleep_pressure = max(self.context.sleep_pressure, 0) self._log_sleep_pressure_change("睡眠压力释放") @@ -145,7 +145,7 @@ class EnergyManager: """ 在执行动作后增加睡眠压力 """ - increment = global_config.wakeup_system.sleep_pressure_increment + increment = global_config.sleep_system.sleep_pressure_increment self.context.sleep_pressure += increment self.context.sleep_pressure = min(self.context.sleep_pressure, 100.0) # 设置一个100的上限 self._log_sleep_pressure_change("执行动作,睡眠压力累积") diff --git a/src/chat/chat_loop/heartFC_chat.py b/src/chat/chat_loop/heartFC_chat.py index ccb90da2d..7e386ceac 100644 --- a/src/chat/chat_loop/heartFC_chat.py +++ b/src/chat/chat_loop/heartFC_chat.py @@ -209,12 +209,12 @@ class HeartFChatting: if self.wakeup_manager and self.wakeup_manager.check_for_insomnia(): # 触发失眠 self.context.is_in_insomnia = True - duration = global_config.wakeup_system.insomnia_duration_minutes * 60 + duration = global_config.sleep_system.insomnia_duration_minutes * 60 self.context.insomnia_end_time = time.time() + duration # 判断失眠原因并触发思考 reason = "random" - if self.context.sleep_pressure < global_config.wakeup_system.sleep_pressure_threshold: + if self.context.sleep_pressure < global_config.sleep_system.sleep_pressure_threshold: reason = "low_pressure" await self.proactive_thinker.trigger_insomnia_thinking(reason) @@ -274,7 +274,7 @@ class HeartFChatting: # --- 重新入睡逻辑 --- # 如果被吵醒了,并且在一定时间内没有新消息,则尝试重新入睡 if schedule_manager._is_woken_up and not has_new_messages: - re_sleep_delay = global_config.wakeup_system.re_sleep_delay_minutes * 60 + re_sleep_delay = global_config.sleep_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} 分钟无新消息,尝试重新入睡。") diff --git a/src/chat/chat_loop/wakeup_manager.py b/src/chat/chat_loop/wakeup_manager.py index 37fd755aa..01c95103e 100644 --- a/src/chat/chat_loop/wakeup_manager.py +++ b/src/chat/chat_loop/wakeup_manager.py @@ -31,22 +31,22 @@ class WakeUpManager: self.log_interval = 30 # 从配置文件获取参数 - 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 - self.angry_prompt = wakeup_config.angry_prompt + sleep_config = global_config.sleep_system + self.wakeup_threshold = sleep_config.wakeup_threshold + self.private_message_increment = sleep_config.private_message_increment + self.group_mention_increment = sleep_config.group_mention_increment + self.decay_rate = sleep_config.decay_rate + self.decay_interval = sleep_config.decay_interval + self.angry_duration = sleep_config.angry_duration + self.enabled = sleep_config.enable + self.angry_prompt = sleep_config.angry_prompt # 失眠系统参数 - self.insomnia_enabled = wakeup_config.enable_insomnia_system - self.sleep_pressure_threshold = wakeup_config.sleep_pressure_threshold - 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.insomnia_enabled = sleep_config.enable_insomnia_system + self.sleep_pressure_threshold = sleep_config.sleep_pressure_threshold + self.deep_sleep_threshold = sleep_config.deep_sleep_threshold + self.insomnia_chance_low_pressure = sleep_config.insomnia_chance_low_pressure + self.insomnia_chance_normal_pressure = sleep_config.insomnia_chance_normal_pressure self._load_wakeup_state() diff --git a/src/config/config.py b/src/config/config.py index e403f7ce0..9aa25cc15 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -41,7 +41,7 @@ from src.config.official_configs import ( WebSearchConfig, AntiPromptInjectionConfig, PluginsConfig, - WakeUpSystemConfig, + SleepSystemConfig, MonthlyPlanSystemConfig, CrossContextConfig, PermissionConfig, @@ -390,7 +390,7 @@ class Config(ValidatedConfigBase): dependency_management: DependencyManagementConfig = Field(default_factory=lambda: DependencyManagementConfig(), description="依赖管理配置") web_search: WebSearchConfig = Field(default_factory=lambda: WebSearchConfig(), description="网络搜索配置") plugins: PluginsConfig = Field(default_factory=lambda: PluginsConfig(), description="插件配置") - wakeup_system: WakeUpSystemConfig = Field(default_factory=lambda: WakeUpSystemConfig(), description="唤醒度系统配置") + sleep_system: SleepSystemConfig = Field(default_factory=lambda: SleepSystemConfig(), description="睡眠系统配置") monthly_plan_system: MonthlyPlanSystemConfig = Field(default_factory=lambda: MonthlyPlanSystemConfig(), description="月层计划系统配置") cross_context: CrossContextConfig = Field(default_factory=lambda: CrossContextConfig(), description="跨群聊上下文共享配置") maizone_intercom: MaizoneIntercomConfig = Field(default_factory=lambda: MaizoneIntercomConfig(), description="Maizone互通组配置") diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 28e617474..7e8dd4446 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -530,15 +530,6 @@ 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="用于生成睡前消息的提示") - class DependencyManagementConfig(ValidatedConfigBase): @@ -610,10 +601,10 @@ class PluginsConfig(ValidatedConfigBase): centralized_config: bool = Field(default=True, description="是否启用插件配置集中化管理") -class WakeUpSystemConfig(ValidatedConfigBase): - """唤醒度与失眠系统配置类""" +class SleepSystemConfig(ValidatedConfigBase): + """睡眠系统配置类""" - enable: bool = Field(default=True, description="是否启用唤醒度系统") + 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="群聊艾特增加的唤醒度") @@ -633,6 +624,14 @@ class WakeUpSystemConfig(ValidatedConfigBase): sleep_pressure_increment: float = Field(default=1.5, ge=0.0, description="每次AI执行动作后,增加的睡眠压力值") sleep_pressure_decay_rate: float = Field(default=1.5, ge=0.0, description="睡眠时,每分钟衰减的睡眠压力值") + # --- 弹性睡眠与睡前消息 --- + 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 MonthlyPlanSystemConfig(ValidatedConfigBase): """月度计划系统配置类""" diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index d3b997526..b393db34c 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -89,6 +89,7 @@ learning_strength = 1.0 [[expression.rules]] chat_stream_id = "qq:1919810:group" +group = "group_A" use_expression = true learn_expression = true learning_strength = 1.5 @@ -100,29 +101,18 @@ use_expression = true learn_expression = false learning_strength = 0.5 -[[expression.rules]] -chat_stream_id = "qq:1919810:private" -group = "group_A" -use_expression = true -learn_expression = true -learning_strength = 1.0 - - - [chat] #MoFox-Bot的聊天通用设置 +# 群聊聊天模式设置 +group_chat_mode = "auto" # 群聊聊天模式:auto-自动切换,normal-强制普通模式,focus-强制专注模式 focus_value = 1 -# MoFox-Bot的专注思考能力,越高越容易专注,可能消耗更多token +# MoFox-Bot的专注思考能力,越高越容易专注,可能消耗更多token,仅限自动切换模式下使用哦 # 专注时能更好把握发言时机,能够进行持久的连续对话 -talk_frequency = 1 # MoFox-Bot活跃度,越高,MoFox-Bot回复越频繁 +talk_frequency = 1 # MoFox-Bot活跃度,越高,MoFox-Bot回复越频繁,仅限normal/或者自动切换的normal模式下使用哦 # 强制私聊专注模式 force_focus_private = false # 是否强制私聊进入专注模式,开启后私聊将始终保持专注状态 -# 群聊聊天模式设置 -group_chat_mode = "auto" # 群聊聊天模式:auto-自动切换,normal-强制普通模式,focus-强制专注模式 - - max_context_size = 25 # 上下文长度 thinking_timeout = 40 # MoFox-Bot一次回复最长思考规划时间,超过这个时间的思考会放弃(往往是api反应太慢) replyer_random_probability = 0.5 # 首要replyer模型被选择的概率 @@ -273,7 +263,7 @@ enable_vector_instant_memory = true # 是否启用基于向量的瞬时记忆 memory_ban_words = [ "表情包", "图片", "回复", "聊天记录" ] [voice] -enable_asr = false # 是否启用语音识别,启用后MoFox-Bot可以识别语音消息,启用该功能需要配置语音识别模型[model.voice]s +enable_asr = false # 是否启用语音识别,启用后MoFox-Bot可以识别语音消息,启用该功能需要配置语音识别模型[model.voice] [lpmm_knowledge] # lpmm知识库配置 enable = false # 是否启用lpmm知识库 @@ -376,23 +366,6 @@ 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"(自动选择) @@ -456,8 +429,8 @@ guidelines = """ 请确保计划既有挑战性又不会过于繁重,保持生活的平衡和乐趣。 """ -[wakeup_system] -enable = false #"是否启用唤醒度系统" +[sleep_system] +enable = false #"是否启用睡眠系统" wakeup_threshold = 15.0 #唤醒阈值,达到此值时会被唤醒" private_message_increment = 3.0 #"私聊消息增加的唤醒度" group_mention_increment = 2.0 #"群聊艾特增加的唤醒度" @@ -482,6 +455,21 @@ sleep_pressure_increment = 1.5 sleep_pressure_decay_rate = 1.5 insomnia_duration_minutes = 30 # 单次失眠状态的持续时间(分钟) +# --- 弹性睡眠与睡前消息 --- +# 是否启用弹性睡眠。启用后,AI不会到点立刻入睡,而是会根据睡眠压力增加5-10分钟的缓冲,并可能因为压力不足而推迟睡眠。 +enable_flexible_sleep = false +# 触发弹性睡眠的睡眠压力阈值。当AI的睡眠压力低于此值时,可能会推迟入睡。 +flexible_sleep_pressure_threshold = 40.0 +# 每日最大可推迟入睡的总分钟数。 +max_sleep_delay_minutes = 60 + +# 是否在进入“准备入睡”状态时发送一条消息通知。 +enable_pre_sleep_notification = false +# 接收睡前消息的群组列表。格式为: ["platform:group_id1", "platform:group_id2"],例如 ["qq:12345678"] +pre_sleep_notification_groups = [] +# 用于生成睡前消息的提示。AI会根据这个提示生成一句晚安问候。 +pre_sleep_prompt = "我准备睡觉了,请生成一句简短自然的晚安问候。" + [cross_context] # 跨群聊上下文共享配置 # 这是总开关,用于一键启用或禁用此功能 enable = false From 74863fb9144968a56db387f50461ed972b40a488 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 29 Aug 2025 13:59:34 +0800 Subject: [PATCH 2/9] chore(config): update bot_config_template version and remove unused option - Bumps the config version from 6.5.7 to 6.5.8. - Removes the `prompt_before_install` option which was not implemented. --- template/bot_config_template.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index b393db34c..999310ffd 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.5.7" +version = "6.5.8" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -337,8 +337,6 @@ auto_install_timeout = 300 # 是否使用PyPI镜像源(推荐,可加速下载) use_mirror = true mirror_url = "https://pypi.tuna.tsinghua.edu.cn/simple" # PyPI镜像源URL,如: "https://pypi.tuna.tsinghua.edu.cn/simple" -# 安装前是否提示用户(暂未实现) -prompt_before_install = false # 依赖安装日志级别 install_log_level = "INFO" From ad18af3c99f9274de986134d6d02c817ac9b6f06 Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Fri, 29 Aug 2025 14:16:24 +0800 Subject: [PATCH 3/9] =?UTF-8?q?feat(napcat):=20=E6=B7=BB=E5=8A=A0=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E8=AE=BE=E7=BD=AE=E5=A4=84=E7=90=86=E5=99=A8=E5=92=8C?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E7=B1=BB=E5=9E=8B=E6=B3=A8=E8=A7=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为napcat插件增加SetProfileHandler来处理账号信息设置事件 - 实现参数解析和napcat API调用逻辑 - 添加详细的事件类型文档注解,包含参数说明和返回格式 - 扩展事件枚举类,新增多个群组相关操作事件 - 在插件初始化时自动注册所有事件处理器 - 为HandlerResultsCollection添加获取消息结果的方法 同时添加接口测试处理器用于验证事件系统的正常工作 --- .../napcat_adapter_plugin/event_handlers.py | 90 +++++++- plugins/napcat_adapter_plugin/event_types.py | 218 ++++++++++++++---- plugins/napcat_adapter_plugin/plugin.py | 46 +++- src/plugin_system/base/base_event.py | 13 ++ 4 files changed, 317 insertions(+), 50 deletions(-) diff --git a/plugins/napcat_adapter_plugin/event_handlers.py b/plugins/napcat_adapter_plugin/event_handlers.py index cc5c7c2ec..7922a191e 100644 --- a/plugins/napcat_adapter_plugin/event_handlers.py +++ b/plugins/napcat_adapter_plugin/event_handlers.py @@ -1,6 +1,92 @@ -from typing import List, Tuple +from typing import List, Tuple, Optional -from src.plugin_system import BasePlugin, BaseEventHandler, register_plugin, EventType, ConfigField, BaseAction, ActionActivationType +from src.plugin_system import BaseEventHandler from src.plugin_system.base.base_event import HandlerResult from src.plugin_system.core.event_manager import event_manager +from .src.send_handler import send_handler +from .event_types import * + +from src.common.logger import get_logger +logger = get_logger("napcat_adapter") + + +class SetProfileHandler(BaseEventHandler): + handler_name: str = "napcat_set_qq_profile_handler" + handler_description: str = "设置账号信息" + weight: int = 100 + intercept_message: bool = False + init_subscribe = [NapcatEvent.ACCOUNT.SET_PROFILE] + + async def execute(self,params:dict): + raw = params.get("raw",{}) + nickname = params.get("nickname","") + personal_note = params.get("personal_note","") + sex = params.get("sex","") + + if params.get("raw",""): + nickname = raw.get("nickname","") + personal_note = raw.get("personal_note","") + sex = raw.get("sex","") + + if not nickname: + logger.error("事件 napcat_set_qq_profile 缺少必要参数: nickname ") + return HandlerResult(False,False,{"status":"error"}) + + payload = { + "nickname": nickname, + "personal_note": personal_note, + "sex": sex + } + response = await send_handler.send_message_to_napcat(action="set_qq_profile",params=payload) + if response.get("status","") == "ok": + if response.get("data","").get("result","") == 0: + return HandlerResult(True,True,response) + else: + logger.error(f"事件 napcat_set_qq_profile 请求失败!err={response.get("data","").get("errMsg","")}") + return HandlerResult(False,False,response) + else: + logger.error("事件 napcat_set_qq_profile 请求失败!") + return HandlerResult(False,False,{"status":"error"}) +''' +class SetProfileHandler(BaseEventHandler): + handler_name: str = "napcat_set_qq_profile_handler" + handler_description: str = "设置账号信息" + weight: int = 100 + intercept_message: bool = False + init_subscribe = [NapcatEvent.ACCOUNT.SET_PROFILE] + + async def execute( + self, + nickname: Optional[str] = "", + personal_note: Optional[str] = "", + sex: Optional[list["1","2","3"]] = "", + raw: dict = {} + ): + if raw: + nickname = raw.get("nickname","") + personal_note = raw.get("personal_note","") + sex = raw.get("sex","") + + if not nickname: + logger.error("事件 napcat_set_qq_profile 缺少必要参数: nickname ") + return HandlerResult(False,False,"缺少必要参数: nickname") + + payload = { + "nickname": nickname, + "personal_note": personal_note, + "sex": sex + } + response = await send_handler.send_message_to_napcat(action="set_qq_profile",params=payload) + if response.get("status","") == "ok": + if response.get("data","").get("result","") == 0: + return HandlerResult(True,True,True) + else: + logger.error(f"事件 napcat_set_qq_profile 请求失败!err={response.get("data","").get("errMsg","")}") + return HandlerResult(False,False,False) + else: + logger.error("事件 napcat_set_qq_profile 请求失败!") + return HandlerResult(False,False,False) +''' + + diff --git a/plugins/napcat_adapter_plugin/event_types.py b/plugins/napcat_adapter_plugin/event_types.py index b199cdd37..6bb819a02 100644 --- a/plugins/napcat_adapter_plugin/event_types.py +++ b/plugins/napcat_adapter_plugin/event_types.py @@ -1,69 +1,197 @@ from enum import Enum class NapcatEvent(Enum): - # napcat插件事件枚举类 + """ + napcat插件事件枚举类 + """ class ON_RECEIVED(Enum): """ 该分类下均为消息接受事件,只能由napcat_plugin触发 """ - TEXT = "napcat_on_received_text" # 接收到文本消息 - FACE = "napcat_on_received_face" # 接收到表情消息 - REPLY = "napcat_on_received_reply" # 接收到回复消息 - IMAGE = "napcat_on_received_image" # 接收到图像消息 - RECORD = "napcat_on_received_record" # 接收到语音消息 - VIDEO = "napcat_on_received_video" # 接收到视频消息 - AT = "napcat_on_received_at" # 接收到at消息 - DICE = "napcat_on_received_dice" # 接收到骰子消息 - SHAKE = "napcat_on_received_shake" # 接收到屏幕抖动消息 - JSON = "napcat_on_received_json" # 接收到JSON消息 - RPS = "napcat_on_received_rps" # 接收到魔法猜拳消息 - FRIEND_INPUT = "napcat_on_friend_input" # 好友正在输入 + TEXT = "napcat_on_received_text" + '''接收到文本消息''' + FACE = "napcat_on_received_face" + '''接收到表情消息''' + REPLY = "napcat_on_received_reply" + '''接收到回复消息''' + IMAGE = "napcat_on_received_image" + '''接收到图像消息''' + RECORD = "napcat_on_received_record" + '''接收到语音消息''' + VIDEO = "napcat_on_received_video" + '''接收到视频消息''' + AT = "napcat_on_received_at" + '''接收到at消息''' + DICE = "napcat_on_received_dice" + '''接收到骰子消息''' + SHAKE = "napcat_on_received_shake" + '''接收到屏幕抖动消息''' + JSON = "napcat_on_received_json" + '''接收到JSON消息''' + RPS = "napcat_on_received_rps" + '''接收到魔法猜拳消息''' + FRIEND_INPUT = "napcat_on_friend_input" + '''好友正在输入''' class ACCOUNT(Enum): """ 该分类是对账户相关的操作,只能由外部触发,napcat_plugin负责处理 """ - SET_PROFILE = "napcat_set_qq_profile" # 设置账号信息 - GET_ONLINE_CLIENTS = "napcat_get_online_clients" # 获取当前账号在线客户端列表 - SET_ONLINE_STATUS = "napcat_set_online_status" # 设置在线状态 - GET_FRIENDS_WITH_CATEGORY = "napcat_get_friends_with_category" # 获取好友分组列表 - SET_AVATAR = "napcat_set_qq_avatar" # 设置头像 - SEND_LIKE = "napcat_send_like" # 点赞 - SET_FRIEND_ADD_REQUEST = "napcat_set_friend_add_request" # 处理好友请求 - SET_SELF_LONGNICK = "napcat_set_self_longnick" # 设置个性签名 - GET_LOGIN_INFO = "napcat_get_login_info" # 获取登录号信息 - GET_RECENT_CONTACT = "napcat_get_recent_contact" # 最近消息列表 - GET_STRANGER_INFO = "napcat_get_stranger_info" # 获取(指定)账号信息 - GET_FRIEND_LIST = "napcat_get_friend_list" # 获取好友列表 - GET_PROFILE_LIKE = "napcat_get_profile_like" # 获取点赞列表 - DELETE_FRIEND = "napcat_delete_friend" # 删除好友 - GET_USER_STATUS = "napcat_get_user_status" # 获取用户状态 - GET_STATUS = "napcat_get_status" # 获取状态 - GET_MINI_APP_ARK = "napcat_get_mini_app_ark" # 获取小程序卡片 - SET_DIY_ONLINE_STATUS = "napcat_set_diy_online_status" # 设置自定义在线状态 + SET_PROFILE = "napcat_set_qq_profile" + '''设置账号信息 + + Args: + nickname (Optional[str]): 名称(必须) + personal_note (Optional[str]): 个性签名 + sex (Optional['0'|'1'|'2']): 性别 + raw (Optional[dict]): 原始请求体 + + Returns: + dict: { + "status": "ok", + "retcode": 0, + "data": { + "result": 0, + "errMsg": "string" + }, + "message": "string", + "wording": "string", + "echo": "string" + } + + ''' + GET_ONLINE_CLIENTS = "napcat_get_online_clients" + '''获取当前账号在线客户端列表''' + SET_ONLINE_STATUS = "napcat_set_online_status" + '''设置在线状态''' + GET_FRIENDS_WITH_CATEGORY = "napcat_get_friends_with_category" + '''获取好友分组列表''' + SET_AVATAR = "napcat_set_qq_avatar" + '''设置头像''' + SEND_LIKE = "napcat_send_like" '''点赞''' + SET_FRIEND_ADD_REQUEST = "napcat_set_friend_add_request" + '''处理好友请求''' + SET_SELF_LONGNICK = "napcat_set_self_longnick" + '''设置个性签名''' + GET_LOGIN_INFO = "napcat_get_login_info" + '''获取登录号信息''' + GET_RECENT_CONTACT = "napcat_get_recent_contact" + '''最近消息列表''' + GET_STRANGER_INFO = "napcat_get_stranger_info" + '''获取(指定)账号信息''' + GET_FRIEND_LIST = "napcat_get_friend_list" + '''获取好友列表''' + GET_PROFILE_LIKE = "napcat_get_profile_like" + '''获取点赞列表''' + DELETE_FRIEND = "napcat_delete_friend" + '''删除好友''' + GET_USER_STATUS = "napcat_get_user_status" + '''获取用户状态''' + GET_STATUS = "napcat_get_status" + '''获取状态''' + GET_MINI_APP_ARK = "napcat_get_mini_app_ark" + '''获取小程序卡片''' + SET_DIY_ONLINE_STATUS = "napcat_set_diy_online_status" + '''设置自定义在线状态''' class MESSAGE(Enum): """ 该分类是对信息相关的操作,只能由外部触发,napcat_plugin负责处理 """ - SEND_GROUP_POKE = "napcat_send_group_poke" # 发送群聊戳一戳 - SEND_PRIVATE_MSG = "napcat_send_private_msg" # 发送私聊消息 - SEND_POKE = "napcat_send_friend_poke" # 发送戳一戳 - DELETE_MSG = "napcat_delete_msg" # 撤回消息 - GET_GROUP_MSG_HISTORY = "napcat_get_group_msg_history" # 获取群历史消息 - GET_MSG = "napcat_get_msg" # 获取消息详情 - GET_FORWARD_MSG = "napcat_get_forward_msg" # 获取合并转发消息 - SET_MSG_EMOJI_LIKE = "napcat_set_msg_emoji_like" # 贴表情 - GET_FRIEND_MSG_HISTORY = "napcat_get_friend_msg_history" # 获取好友历史消息 - FETCH_EMOJI_LIKE = "napcat_fetch_emoji_like" # 获取贴表情详情 - SEND_FORWARF_MSG = "napcat_send_forward_msg" # 发送合并转发消息 - GET_RECOED = "napcat_get_record" # 获取语音消息详情 - SEND_GROUP_AI_RECORD = "napcat_send_group_ai_record" # 发送群AI语音 + SEND_GROUP_POKE = "napcat_send_group_poke" + '''发送群聊戳一戳''' + SEND_PRIVATE_MSG = "napcat_send_private_msg" + '''发送私聊消息''' + SEND_POKE = "napcat_send_friend_poke" + '''发送戳一戳''' + DELETE_MSG = "napcat_delete_msg" + '''撤回消息''' + GET_GROUP_MSG_HISTORY = "napcat_get_group_msg_history" + '''获取群历史消息''' + GET_MSG = "napcat_get_msg" + '''获取消息详情''' + GET_FORWARD_MSG = "napcat_get_forward_msg" + '''获取合并转发消息''' + SET_MSG_EMOJI_LIKE = "napcat_set_msg_emoji_like" + '''贴表情''' + GET_FRIEND_MSG_HISTORY = "napcat_get_friend_msg_history" + '''获取好友历史消息''' + FETCH_EMOJI_LIKE = "napcat_fetch_emoji_like" + '''获取贴表情详情''' + SEND_FORWARF_MSG = "napcat_send_forward_msg" + '''发送合并转发消息''' + GET_RECOED = "napcat_get_record" + '''获取语音消息详情''' + SEND_GROUP_AI_RECORD = "napcat_send_group_ai_record" + '''发送群AI语音''' class GROUP(Enum): """ 该分类是对群聊相关的操作,只能由外部触发,napcat_plugin负责处理 """ - + SET_GROUP_SEARCH = "napcat_set_group_search" + '''设置群搜索''' + GET_GROUP_DETAIL_INFO = "napcat_get_group_detail_info" + '''获取群详细信息''' + SET_GROUP_ADD_OPTION = "napcat_set_group_add_option" + '''设置群添加选项''' + SET_GROUP_ROBOT_ADD_OPTION = "napcat_set_group_robot_add_option" + '''设置群机器人添加选项''' + SET_GROUP_KICK_MEMBERS = "napcat_set_group_kick_members" + '''批量踢出群成员''' + SET_GROUP_KICK = "napcat_set_group_kick" + '''群踢人''' + GET_GROUP_SYSTEM_MSG = "napcat_get_group_system_msg" + '''获取群系统消息''' + SET_GROUP_BAN = "napcat_set_group_ban" + '''群禁言''' + GET_ESSENCE_MSG_LIST = "napcat_get_essence_msg_list" + '''获取群精华消息''' + SET_GROUP_WHOLE_BAN = "napcat_set_group_whole_ban" + '''全体禁言''' + SET_GROUP_PORTRAINT = "napcat_set_group_portrait" + '''设置群头像''' + SET_GROUP_ADMIN = "napcat_set_group_admin" + '''设置群管理''' + SET_GROUP_CARD = "napcat_group_card" + '''设置群成员名片''' + SET_ESSENCE_MSG = "napcat_set_essence_msg" + '''设置群精华消息''' + SET_GROUP_NAME = "napcat_set_group_name" + '''设置群名''' + DELETE_ESSENCE_MSG = "napcat_delete_essence_msg" + '''删除群精华消息''' + SET_GROUP_LEAVE = "napcat_set_group_leave" + '''退群''' + SEND_GROUP_NOTICE = "napcat_group_notice" + '''发送群公告''' + SET_GROUP_SPECIAL_TITLE = "napcat_set_group_special_title" + '''设置群头衔''' + GET_GROUP_NOTICE = "napcat_get_group_notice" + '''获取群公告''' + SET_GROUP_ADD_REQUEST = "napcat_set_group_add_request" + '''处理加群请求''' + GET_GROUP_INFO = "napcat_get_group_info" + '''获取群信息''' + GET_GROUP_LIST = "napcat_get_group_list" + '''获取群列表''' + DELETE_GROUP_NOTICE = "napcat_del_group_notice" + '''删除群公告''' + GET_GROUP_MEMBER_INFO = "napcat_get_group_member_info" + '''获取群成员信息''' + GET_GROUP_MEMBER_LIST = "napcat_get_group_member_list" + '''获取群成员列表''' + GET_GROUP_HONOR_INFO = "napcat_get_group_honor_info" + '''获取群荣誉''' + GET_GROUP_INFO_EX = "napcat_get_group_info_ex" + '''获取群信息ex''' + GET_GROUP_AT_ALL_REMAIN = "napcat_get_group_at_all_remain" + '''获取群 @全体成员 剩余次数''' + GET_GROUP_SHUT_LIST = "napcat_get_group_shut_list" + '''获取群禁言列表''' + GET_GROUP_IGNORED_NOTIFIES = "napcat_get_group_ignored_notifies" + '''获取群过滤系统消息''' + SET_GROUP_SIGN = "napcat_set_group_sign" + '''群打卡''' diff --git a/plugins/napcat_adapter_plugin/plugin.py b/plugins/napcat_adapter_plugin/plugin.py index d6ec27853..a68f74cb6 100644 --- a/plugins/napcat_adapter_plugin/plugin.py +++ b/plugins/napcat_adapter_plugin/plugin.py @@ -1,12 +1,13 @@ import sys import asyncio import json +import inspect import websockets as Server -from . import event_types,CONSTS +from . import event_types,CONSTS,event_handlers from typing import List, Tuple -from src.plugin_system import BasePlugin, BaseEventHandler, register_plugin, EventType, ConfigField, BaseAction, ActionActivationType +from src.plugin_system import BasePlugin, BaseEventHandler, register_plugin, EventType, ConfigField from src.plugin_system.base.base_event import HandlerResult from src.plugin_system.core.event_manager import event_manager @@ -32,6 +33,13 @@ from .src.websocket_manager import websocket_manager message_queue = asyncio.Queue() +def get_classes_in_module(module): + classes = [] + for name, member in inspect.getmembers(module): + if inspect.isclass(member): + classes.append(member) + return classes + class LauchNapcatAdapterHandler(BaseEventHandler): """自动启动Adapter""" @@ -98,6 +106,25 @@ class LauchNapcatAdapterHandler(BaseEventHandler): asyncio.create_task(self.message_process()) asyncio.create_task(check_timeout_response()) +class APITestHandler(BaseEventHandler): + handler_name: str = "napcat_api_test_handler" + handler_description: str = "接口测试" + weight: int = 100 + intercept_message: bool = False + init_subscribe = [EventType.ON_MESSAGE] + + async def execute(self,_): + logger.info("5s后开始测试napcat接口...") + await asyncio.sleep(5) + res = await event_manager.trigger_event( + event_types.NapcatEvent.ACCOUNT.SET_PROFILE, + nickname="我叫杰瑞喵、", + personal_note="喵汪~", + sex=2 + ) + logger.info(res.get_message_result()) + return HandlerResult(True,True,"") + @register_plugin class NapcatAdapterPlugin(BasePlugin): plugin_name = CONSTS.PLUGIN_NAME @@ -124,8 +151,21 @@ class NapcatAdapterPlugin(BasePlugin): for e in event_types.NapcatEvent.ON_RECEIVED: event_manager.register_event(e ,allowed_triggers=[self.plugin_name]) - + + for e in event_types.NapcatEvent.ACCOUNT: + event_manager.register_event(e,allowed_subscribers=[f"{e.value}_handler"]) + + for e in event_types.NapcatEvent.GROUP: + event_manager.register_event(e,allowed_subscribers=[f"{e.value}_handler"]) + + for e in event_types.NapcatEvent.MESSAGE: + event_manager.register_event(e,allowed_subscribers=[f"{e.value}_handler"]) + def get_plugin_components(self): components = [] components.append((LauchNapcatAdapterHandler.get_handler_info(), LauchNapcatAdapterHandler)) + components.append((APITestHandler.get_handler_info(), APITestHandler)) + for handler in get_classes_in_module(event_handlers): + if issubclass(handler,BaseEventHandler): + components.append((handler.get_handler_info(), handler)) return components diff --git a/src/plugin_system/base/base_event.py b/src/plugin_system/base/base_event.py index a9ab38911..f884be7c9 100644 --- a/src/plugin_system/base/base_event.py +++ b/src/plugin_system/base/base_event.py @@ -40,6 +40,19 @@ class HandlerResultsCollection: """获取continue_process为False的handler结果""" return [result for result in self.results if not result.continue_process] + def get_message_result(self) -> Any: + """获取handler的message + + 当只有一个handler的结果时,直接返回那个handler结果中的message字段 + 否则用字典的形式{handler_name:message}返回 + """ + if len(self.results) == 0: + return {} + elif len(self.results) == 1: + return self.results[0].message + else: + return {result.handler_name: result.message for result in self.results} + def get_handler_result(self, handler_name: str) -> Optional[HandlerResult]: """获取指定handler的结果""" for result in self.results: From 925604a708d9dfa823a656febff7753a875db74c Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Fri, 29 Aug 2025 16:20:19 +0800 Subject: [PATCH 4/9] =?UTF-8?q?feat(napcat):=20=E6=96=B0=E5=A2=9E18?= =?UTF-8?q?=E4=B8=AA=E8=B4=A6=E5=8F=B7=E7=9B=B8=E5=85=B3=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=99=A8=E4=B8=8E=E5=AE=8C=E5=96=84=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为NapCat适配器添加完整的账号操作功能: - 在线客户端查询、在线状态设置、最近联系人 - 好友列表、分组列表、删除好友、点赞 - 头像设置、个性签名、自定义在线状态 - 小程序卡片生成、用户信息/状态查询 并同步补充事件类型注解 BREAKING CHANGE: BaseEvent构造函数不再接受可变默认实参 --- .../napcat_adapter_plugin/event_handlers.py | 530 ++++++++++++++++-- plugins/napcat_adapter_plugin/event_types.py | 502 ++++++++++++++++- plugins/napcat_adapter_plugin/plugin.py | 1 - src/plugin_system/base/base_event.py | 6 +- 4 files changed, 982 insertions(+), 57 deletions(-) diff --git a/plugins/napcat_adapter_plugin/event_handlers.py b/plugins/napcat_adapter_plugin/event_handlers.py index 7922a191e..b5ba27a52 100644 --- a/plugins/napcat_adapter_plugin/event_handlers.py +++ b/plugins/napcat_adapter_plugin/event_handlers.py @@ -48,45 +48,505 @@ class SetProfileHandler(BaseEventHandler): else: logger.error("事件 napcat_set_qq_profile 请求失败!") return HandlerResult(False,False,{"status":"error"}) -''' -class SetProfileHandler(BaseEventHandler): - handler_name: str = "napcat_set_qq_profile_handler" - handler_description: str = "设置账号信息" + + +class GetOnlineClientsHandler(BaseEventHandler): + handler_name: str = "napcat_get_online_clients_handler" + handler_description: str = "获取当前账号在线客户端列表" weight: int = 100 intercept_message: bool = False - init_subscribe = [NapcatEvent.ACCOUNT.SET_PROFILE] + init_subscribe = [NapcatEvent.ACCOUNT.GET_ONLINE_CLIENTS] - async def execute( - self, - nickname: Optional[str] = "", - personal_note: Optional[str] = "", - sex: Optional[list["1","2","3"]] = "", - raw: dict = {} - ): - if raw: - nickname = raw.get("nickname","") - personal_note = raw.get("personal_note","") - sex = raw.get("sex","") - - if not nickname: - logger.error("事件 napcat_set_qq_profile 缺少必要参数: nickname ") - return HandlerResult(False,False,"缺少必要参数: nickname") + async def execute(self, params: dict): + raw = params.get("raw", {}) + no_cache = params.get("no_cache", False) + + if params.get("raw", ""): + no_cache = raw.get("no_cache", False) payload = { - "nickname": nickname, - "personal_note": personal_note, - "sex": sex - } - response = await send_handler.send_message_to_napcat(action="set_qq_profile",params=payload) - if response.get("status","") == "ok": - if response.get("data","").get("result","") == 0: - return HandlerResult(True,True,True) - else: - logger.error(f"事件 napcat_set_qq_profile 请求失败!err={response.get("data","").get("errMsg","")}") - return HandlerResult(False,False,False) + "no_cache": no_cache + } + response = await send_handler.send_message_to_napcat(action="get_online_clients", params=payload) + if response.get("status", "") == "ok": + return HandlerResult(True, True, response) else: - logger.error("事件 napcat_set_qq_profile 请求失败!") - return HandlerResult(False,False,False) -''' + logger.error("事件 napcat_get_online_clients 请求失败!") + return HandlerResult(False, False, {"status": "error"}) - + +class SetOnlineStatusHandler(BaseEventHandler): + handler_name: str = "napcat_set_online_status_handler" + handler_description: str = "设置在线状态" + weight: int = 100 + intercept_message: bool = False + init_subscribe = [NapcatEvent.ACCOUNT.SET_ONLINE_STATUS] + + async def execute(self, params: dict): + raw = params.get("raw", {}) + status = params.get("status", "") + ext_status = params.get("ext_status", "0") + battery_status = params.get("battery_status", "0") + + if params.get("raw", ""): + status = raw.get("status", "") + ext_status = raw.get("ext_status", "0") + battery_status = raw.get("battery_status", "0") + + if not status: + logger.error("事件 napcat_set_online_status 缺少必要参数: status") + return HandlerResult(False, False, {"status": "error"}) + + payload = { + "status": status, + "ext_status": ext_status, + "battery_status": battery_status + } + response = await send_handler.send_message_to_napcat(action="set_online_status", params=payload) + if response.get("status", "") == "ok": + return HandlerResult(True, True, response) + else: + logger.error("事件 napcat_set_online_status 请求失败!") + return HandlerResult(False, False, {"status": "error"}) + + +class GetFriendsWithCategoryHandler(BaseEventHandler): + handler_name: str = "napcat_get_friends_with_category_handler" + handler_description: str = "获取好友分组列表" + weight: int = 100 + intercept_message: bool = False + init_subscribe = [NapcatEvent.ACCOUNT.GET_FRIENDS_WITH_CATEGORY] + + async def execute(self, params: dict): + payload = {} + response = await send_handler.send_message_to_napcat(action="get_friends_with_category", params=payload) + if response.get("status", "") == "ok": + return HandlerResult(True, True, response) + else: + logger.error("事件 napcat_get_friends_with_category 请求失败!") + return HandlerResult(False, False, {"status": "error"}) + + +class SetAvatarHandler(BaseEventHandler): + handler_name: str = "napcat_set_qq_avatar_handler" + handler_description: str = "设置头像" + weight: int = 100 + intercept_message: bool = False + init_subscribe = [NapcatEvent.ACCOUNT.SET_AVATAR] + + async def execute(self, params: dict): + raw = params.get("raw", {}) + file = params.get("file", "") + + if params.get("raw", ""): + file = raw.get("file", "") + + if not file: + logger.error("事件 napcat_set_qq_avatar 缺少必要参数: file") + return HandlerResult(False, False, {"status": "error"}) + + payload = { + "file": file + } + response = await send_handler.send_message_to_napcat(action="set_qq_avatar", params=payload) + if response.get("status", "") == "ok": + return HandlerResult(True, True, response) + else: + logger.error("事件 napcat_set_qq_avatar 请求失败!") + return HandlerResult(False, False, {"status": "error"}) + + +class SendLikeHandler(BaseEventHandler): + handler_name: str = "napcat_send_like_handler" + handler_description: str = "点赞" + weight: int = 100 + intercept_message: bool = False + init_subscribe = [NapcatEvent.ACCOUNT.SEND_LIKE] + + async def execute(self, params: dict): + raw = params.get("raw", {}) + user_id = params.get("user_id", "") + times = params.get("times", 1) + + if params.get("raw", ""): + user_id = raw.get("user_id", "") + times = raw.get("times", 1) + + if not user_id: + logger.error("事件 napcat_send_like 缺少必要参数: user_id") + return HandlerResult(False, False, {"status": "error"}) + + payload = { + "user_id": str(user_id), + "times": times + } + response = await send_handler.send_message_to_napcat(action="send_like", params=payload) + if response.get("status", "") == "ok": + return HandlerResult(True, True, response) + else: + logger.error("事件 napcat_send_like 请求失败!") + return HandlerResult(False, False, {"status": "error"}) + + +class SetFriendAddRequestHandler(BaseEventHandler): + handler_name: str = "napcat_set_friend_add_request_handler" + handler_description: str = "处理好友请求" + weight: int = 100 + intercept_message: bool = False + init_subscribe = [NapcatEvent.ACCOUNT.SET_FRIEND_ADD_REQUEST] + + async def execute(self, params: dict): + raw = params.get("raw", {}) + flag = params.get("flag", "") + approve = params.get("approve", True) + remark = params.get("remark", "") + + if params.get("raw", ""): + flag = raw.get("flag", "") + approve = raw.get("approve", True) + remark = raw.get("remark", "") + + if not flag or approve is None or remark is None: + logger.error("事件 napcat_set_friend_add_request 缺少必要参数") + return HandlerResult(False, False, {"status": "error"}) + + payload = { + "flag": flag, + "approve": approve, + "remark": remark + } + response = await send_handler.send_message_to_napcat(action="set_friend_add_request", params=payload) + if response.get("status", "") == "ok": + return HandlerResult(True, True, response) + else: + logger.error("事件 napcat_set_friend_add_request 请求失败!") + return HandlerResult(False, False, {"status": "error"}) + + +class SetSelfLongnickHandler(BaseEventHandler): + handler_name: str = "napcat_set_self_longnick_handler" + handler_description: str = "设置个性签名" + weight: int = 100 + intercept_message: bool = False + init_subscribe = [NapcatEvent.ACCOUNT.SET_SELF_LONGNICK] + + async def execute(self, params: dict): + raw = params.get("raw", {}) + longNick = params.get("longNick", "") + + if params.get("raw", ""): + longNick = raw.get("longNick", "") + + if not longNick: + logger.error("事件 napcat_set_self_longnick 缺少必要参数: longNick") + return HandlerResult(False, False, {"status": "error"}) + + payload = { + "longNick": longNick + } + response = await send_handler.send_message_to_napcat(action="set_self_longnick", params=payload) + if response.get("status", "") == "ok": + if response.get("data", {}).get("result", "") == 0: + return HandlerResult(True, True, response) + else: + logger.error(f"事件 napcat_set_self_longnick 请求失败!err={response.get('data', {}).get('errMsg', '')}") + return HandlerResult(False, False, response) + else: + logger.error("事件 napcat_set_self_longnick 请求失败!") + return HandlerResult(False, False, {"status": "error"}) + + +class GetLoginInfoHandler(BaseEventHandler): + handler_name: str = "napcat_get_login_info_handler" + handler_description: str = "获取登录号信息" + weight: int = 100 + intercept_message: bool = False + init_subscribe = [NapcatEvent.ACCOUNT.GET_LOGIN_INFO] + + async def execute(self, params: dict): + payload = {} + response = await send_handler.send_message_to_napcat(action="get_login_info", params=payload) + if response.get("status", "") == "ok": + return HandlerResult(True, True, response) + else: + logger.error("事件 napcat_get_login_info 请求失败!") + return HandlerResult(False, False, {"status": "error"}) + + +class GetRecentContactHandler(BaseEventHandler): + handler_name: str = "napcat_get_recent_contact_handler" + handler_description: str = "最近消息列表" + weight: int = 100 + intercept_message: bool = False + init_subscribe = [NapcatEvent.ACCOUNT.GET_RECENT_CONTACT] + + async def execute(self, params: dict): + raw = params.get("raw", {}) + count = params.get("count", 20) + + if params.get("raw", ""): + count = raw.get("count", 20) + + payload = { + "count": count + } + response = await send_handler.send_message_to_napcat(action="get_recent_contact", params=payload) + if response.get("status", "") == "ok": + return HandlerResult(True, True, response) + else: + logger.error("事件 napcat_get_recent_contact 请求失败!") + return HandlerResult(False, False, {"status": "error"}) + + +class GetStrangerInfoHandler(BaseEventHandler): + handler_name: str = "napcat_get_stranger_info_handler" + handler_description: str = "获取(指定)账号信息" + weight: int = 100 + intercept_message: bool = False + init_subscribe = [NapcatEvent.ACCOUNT.GET_STRANGER_INFO] + + async def execute(self, params: dict): + raw = params.get("raw", {}) + user_id = params.get("user_id", "") + + if params.get("raw", ""): + user_id = raw.get("user_id", "") + + if not user_id: + logger.error("事件 napcat_get_stranger_info 缺少必要参数: user_id") + return HandlerResult(False, False, {"status": "error"}) + + payload = { + "user_id": str(user_id) + } + response = await send_handler.send_message_to_napcat(action="get_stranger_info", params=payload) + if response.get("status", "") == "ok": + return HandlerResult(True, True, response) + else: + logger.error("事件 napcat_get_stranger_info 请求失败!") + return HandlerResult(False, False, {"status": "error"}) + + +class GetFriendListHandler(BaseEventHandler): + handler_name: str = "napcat_get_friend_list_handler" + handler_description: str = "获取好友列表" + weight: int = 100 + intercept_message: bool = False + init_subscribe = [NapcatEvent.ACCOUNT.GET_FRIEND_LIST] + + async def execute(self, params: dict): + raw = params.get("raw", {}) + no_cache = params.get("no_cache", False) + + if params.get("raw", ""): + no_cache = raw.get("no_cache", False) + + payload = { + "no_cache": no_cache + } + response = await send_handler.send_message_to_napcat(action="get_friend_list", params=payload) + if response.get("status", "") == "ok": + return HandlerResult(True, True, response) + else: + logger.error("事件 napcat_get_friend_list 请求失败!") + return HandlerResult(False, False, {"status": "error"}) + + +class GetProfileLikeHandler(BaseEventHandler): + handler_name: str = "napcat_get_profile_like_handler" + handler_description: str = "获取点赞列表" + weight: int = 100 + intercept_message: bool = False + init_subscribe = [NapcatEvent.ACCOUNT.GET_PROFILE_LIKE] + + async def execute(self, params: dict): + raw = params.get("raw", {}) + user_id = params.get("user_id", "") + start = params.get("start", 0) + count = params.get("count", 10) + + if params.get("raw", ""): + user_id = raw.get("user_id", "") + start = raw.get("start", 0) + count = raw.get("count", 10) + + payload = { + "start": start, + "count": count + } + if user_id: + payload["user_id"] = str(user_id) + + response = await send_handler.send_message_to_napcat(action="get_profile_like", params=payload) + if response.get("status", "") == "ok": + return HandlerResult(True, True, response) + else: + logger.error("事件 napcat_get_profile_like 请求失败!") + return HandlerResult(False, False, {"status": "error"}) + + +class DeleteFriendHandler(BaseEventHandler): + handler_name: str = "napcat_delete_friend_handler" + handler_description: str = "删除好友" + weight: int = 100 + intercept_message: bool = False + init_subscribe = [NapcatEvent.ACCOUNT.DELETE_FRIEND] + + async def execute(self, params: dict): + raw = params.get("raw", {}) + user_id = params.get("user_id", "") + temp_block = params.get("temp_block", False) + temp_both_del = params.get("temp_both_del", False) + + if params.get("raw", ""): + user_id = raw.get("user_id", "") + temp_block = raw.get("temp_block", False) + temp_both_del = raw.get("temp_both_del", False) + + if not user_id or temp_block is None or temp_both_del is None: + logger.error("事件 napcat_delete_friend 缺少必要参数") + return HandlerResult(False, False, {"status": "error"}) + + payload = { + "user_id": str(user_id), + "temp_block": temp_block, + "temp_both_del": temp_both_del + } + response = await send_handler.send_message_to_napcat(action="delete_friend", params=payload) + if response.get("status", "") == "ok": + if response.get("data", {}).get("result", "") == 0: + return HandlerResult(True, True, response) + else: + logger.error(f"事件 napcat_delete_friend 请求失败!err={response.get('data', {}).get('errMsg', '')}") + return HandlerResult(False, False, response) + else: + logger.error("事件 napcat_delete_friend 请求失败!") + return HandlerResult(False, False, {"status": "error"}) + + +class GetUserStatusHandler(BaseEventHandler): + handler_name: str = "napcat_get_user_status_handler" + handler_description: str = "获取(指定)用户状态" + weight: int = 100 + intercept_message: bool = False + init_subscribe = [NapcatEvent.ACCOUNT.GET_USER_STATUS] + + async def execute(self, params: dict): + raw = params.get("raw", {}) + user_id = params.get("user_id", "") + + if params.get("raw", ""): + user_id = raw.get("user_id", "") + + if not user_id: + logger.error("事件 napcat_get_user_status 缺少必要参数: user_id") + return HandlerResult(False, False, {"status": "error"}) + + payload = { + "user_id": str(user_id) + } + response = await send_handler.send_message_to_napcat(action="get_user_status", params=payload) + if response.get("status", "") == "ok": + return HandlerResult(True, True, response) + else: + logger.error("事件 napcat_get_user_status 请求失败!") + return HandlerResult(False, False, {"status": "error"}) + + +class GetStatusHandler(BaseEventHandler): + handler_name: str = "napcat_get_status_handler" + handler_description: str = "获取状态" + weight: int = 100 + intercept_message: bool = False + init_subscribe = [NapcatEvent.ACCOUNT.GET_STATUS] + + async def execute(self, params: dict): + payload = {} + response = await send_handler.send_message_to_napcat(action="get_status", params=payload) + if response.get("status", "") == "ok": + return HandlerResult(True, True, response) + else: + logger.error("事件 napcat_get_status 请求失败!") + return HandlerResult(False, False, {"status": "error"}) + + +class GetMiniAppArkHandler(BaseEventHandler): + handler_name: str = "napcat_get_mini_app_ark_handler" + handler_description: str = "获取小程序卡片" + weight: int = 100 + intercept_message: bool = False + init_subscribe = [NapcatEvent.ACCOUNT.GET_MINI_APP_ARK] + + async def execute(self, params: dict): + raw = params.get("raw", {}) + type = params.get("type", "") + title = params.get("title", "") + desc = params.get("desc", "") + picUrl = params.get("picUrl", "") + jumpUrl = params.get("jumpUrl", "") + webUrl = params.get("webUrl", "") + rawArkData = params.get("rawArkData", False) + + if params.get("raw", ""): + type = raw.get("type", "") + title = raw.get("title", "") + desc = raw.get("desc", "") + picUrl = raw.get("picUrl", "") + jumpUrl = raw.get("jumpUrl", "") + webUrl = raw.get("webUrl", "") + rawArkData = raw.get("rawArkData", False) + + if not type or not title or not desc or not picUrl or not jumpUrl: + logger.error("事件 napcat_get_mini_app_ark 缺少必要参数") + return HandlerResult(False, False, {"status": "error"}) + + payload = { + "type": type, + "title": title, + "desc": desc, + "picUrl": picUrl, + "jumpUrl": jumpUrl, + "webUrl": webUrl, + "rawArkData": rawArkData + } + response = await send_handler.send_message_to_napcat(action="get_mini_app_ark", params=payload) + if response.get("status", "") == "ok": + return HandlerResult(True, True, response) + else: + logger.error("事件 napcat_get_mini_app_ark 请求失败!") + return HandlerResult(False, False, {"status": "error"}) + + +class SetDiyOnlineStatusHandler(BaseEventHandler): + handler_name: str = "napcat_set_diy_online_status_handler" + handler_description: str = "设置自定义在线状态" + weight: int = 100 + intercept_message: bool = False + init_subscribe = [NapcatEvent.ACCOUNT.SET_DIY_ONLINE_STATUS] + + async def execute(self, params: dict): + raw = params.get("raw", {}) + face_id = params.get("face_id", "") + face_type = params.get("face_type", "0") + wording = params.get("wording", "") + + if params.get("raw", ""): + face_id = raw.get("face_id", "") + face_type = raw.get("face_type", "0") + wording = raw.get("wording", "") + + if not face_id: + logger.error("事件 napcat_set_diy_online_status 缺少必要参数: face_id") + return HandlerResult(False, False, {"status": "error"}) + + payload = { + "face_id": str(face_id), + "face_type": str(face_type), + "wording": wording + } + response = await send_handler.send_message_to_napcat(action="set_diy_online_status", params=payload) + if response.get("status", "") == "ok": + return HandlerResult(True, True, response) + else: + logger.error("事件 napcat_set_diy_online_status 请求失败!") + return HandlerResult(False, False, {"status": "error"}) diff --git a/plugins/napcat_adapter_plugin/event_types.py b/plugins/napcat_adapter_plugin/event_types.py index 6bb819a02..05a93803d 100644 --- a/plugins/napcat_adapter_plugin/event_types.py +++ b/plugins/napcat_adapter_plugin/event_types.py @@ -43,7 +43,7 @@ class NapcatEvent(Enum): Args: nickname (Optional[str]): 名称(必须) personal_note (Optional[str]): 个性签名 - sex (Optional['0'|'1'|'2']): 性别 + sex ('0'|'1'|'2'): 性别 raw (Optional[dict]): 原始请求体 Returns: @@ -61,38 +61,504 @@ class NapcatEvent(Enum): ''' GET_ONLINE_CLIENTS = "napcat_get_online_clients" - '''获取当前账号在线客户端列表''' + '''获取当前账号在线客户端列表 + + Args: + no_cache (Optional[bool]): 是否不使用缓存 + raw (Optional[dict]): 原始请求体 + + Returns: + dict: { + "status": "ok", + "retcode": 0, + "data": [ + "string" + ], + "message": "string", + "wording": "string", + "echo": "string" + } + ''' SET_ONLINE_STATUS = "napcat_set_online_status" - '''设置在线状态''' + '''设置在线状态 + + Args: + status (Optional[str]): 状态代码(必须) + ext_status (Optional[str]): 额外状态代码,默认为0 + battery_status (Optional[str]): 电池信息,默认为0 + raw (Optional[dict]): 原始请求体 + + Returns: + dict: { + "status": "ok", + "retcode": 0, + "data": null, + "message": "string", + "wording": "string", + "echo": "string" + } + ''' GET_FRIENDS_WITH_CATEGORY = "napcat_get_friends_with_category" - '''获取好友分组列表''' + '''获取好友分组列表 + + Returns: + dict: { + "status": "ok", + "retcode": 0, + "data": [ + { + "categoryId": 0, + "categorySortId": 0, + "categoryName": "string", + "categoryMbCount": 0, + "onlineCount": 0, + "buddyList": [ + { + "birthday_year": 0, + "birthday_month": 0, + "birthday_day": 0, + "user_id": 0, + "age": 0, + "phone_num": "string", + "email": "string", + "category_id": 0, + "nickname": "string", + "remark": "string", + "sex": "string", + "level": 0 + } + ] + } + ], + "message": "string", + "wording": "string", + "echo": "string" + } + ''' SET_AVATAR = "napcat_set_qq_avatar" - '''设置头像''' - SEND_LIKE = "napcat_send_like" '''点赞''' + '''设置头像 + + Args: + file (Optional[str]): 文件路径或base64(必需) + raw (Optional[dict]): 原始请求体 + + Returns: + dict: { + "status": "ok", + "retcode": 0, + "data": null, + "message": "string", + "wording": "string", + "echo": "string" + } + ''' + SEND_LIKE = "napcat_send_like" + '''点赞 + + Args: + user_id (Optional[str|int]): 用户id(必需) + times (Optional[int]): 点赞次数,默认1 + raw (Optional[dict]): 原始请求体 + + Returns: + dict: { + "status": "ok", + "retcode": 0, + "data": null, + "message": "string", + "wording": "string", + "echo": "string" + } + ''' SET_FRIEND_ADD_REQUEST = "napcat_set_friend_add_request" - '''处理好友请求''' + '''处理好友请求 + + Args: + flag (Optional[str]): 请求id(必需) + approve (Optional[bool]): 是否同意(必需) + remark (Optional[str]): 好友备注(必需) + raw (Optional[dict]): 原始请求体 + + Returns: + dict: { + "status": "ok", + "retcode": 0, + "data": null, + "message": "string", + "wording": "string", + "echo": "string" + } + ''' SET_SELF_LONGNICK = "napcat_set_self_longnick" - '''设置个性签名''' + '''设置个性签名 + + Args: + longNick (Optional[str]): 内容(必需) + raw (Optional[dict]): 原始请求体 + + Returns: + dict: { + "status": "ok", + "retcode": 0, + "data": { + "result": 0, + "errMsg": "string" + }, + "message": "string", + "wording": "string", + "echo": "string" + } + ''' GET_LOGIN_INFO = "napcat_get_login_info" - '''获取登录号信息''' + '''获取登录号信息 + + Returns: + dict: { + "status": "ok", + "retcode": 0, + "data": { + "user_id": 0, + "nickname": "string" + }, + "message": "string", + "wording": "string", + "echo": "string" + } + ''' GET_RECENT_CONTACT = "napcat_get_recent_contact" - '''最近消息列表''' + '''最近消息列表 + + Args: + count (Optional[int]): 会话数量 + raw (Optional[dict]): 原始请求体 + + Returns: + dict: { + "status": "ok", + "retcode": 0, + "data": [ + { + "lastestMsg": { + "self_id": 0, + "user_id": 0, + "time": 0, + "real_seq": "string", + "message_type": "string", + "sender": { + "user_id": 0, + "nickname": "string", + "sex": "male", + "age": 0, + "card": "string", + "role": "owner" + }, + "raw_message": "string", + "font": 0, + "sub_type": "string", + "message": [ + { + "type": "text", + "data": { + "text": "string" + } + } + ], + "message_format": "string", + "post_type": "string", + "group_id": 0 + }, + "peerUin": "string", + "remark": "string", + "msgTime": "string", + "chatType": 0, + "msgId": "string", + "sendNickName": "string", + "sendMemberName": "string", + "peerName": "string" + } + ], + "message": "string", + "wording": "string", + "echo": "string" + } + ''' GET_STRANGER_INFO = "napcat_get_stranger_info" - '''获取(指定)账号信息''' + '''获取(指定)账号信息 + + Args: + user_id (Optional[str|int]): 用户id(必需) + raw (Optional[dict]): 原始请求体 + + Returns: + dict: { + "status": "ok", + "retcode": 0, + "data": { + "user_id": 0, + "uid": "string", + "uin": "string", + "nickname": "string", + "age": 0, + "qid": "string", + "qqLevel": 0, + "sex": "string", + "long_nick": "string", + "reg_time": 0, + "is_vip": true, + "is_years_vip": true, + "vip_level": 0, + "remark": "string", + "status": 0, + "login_days": 0 + }, + "message": "string", + "wording": "string", + "echo": "string" + } + ''' GET_FRIEND_LIST = "napcat_get_friend_list" - '''获取好友列表''' + '''获取好友列表 + + Args: + no_cache (Opetional[bool]): 是否不使用缓存 + raw (Optional[dict]): 原始请求体 + + Returns: + dict: { + "status": "ok", + "retcode": 0, + "data": [ + { + "birthday_year": 0, + "birthday_month": 0, + "birthday_day": 0, + "user_id": 0, + "age": 0, + "phone_num": "string", + "email": "string", + "category_id": 0, + "nickname": "string", + "remark": "string", + "sex": "string", + "level": 0 + } + ], + "message": "string", + "wording": "string", + "echo": "string" + } + ''' GET_PROFILE_LIKE = "napcat_get_profile_like" - '''获取点赞列表''' + '''获取点赞列表 + + Args: + user_id (Opetional[str|int]): 用户id,指定用户,不填为获取所有 + start (Opetional[int]): 起始值 + count (Opetional[int]): 返回数量 + raw (Optional[dict]): 原始请求体 + + Returns: + dict: { + "status": "ok", + "retcode": 0, + "data": { + "uid": "string", + "time": 0, + "favoriteInfo": { + "total_count": 0, + "last_time": 0, + "today_count": 0, + "userInfos": [ + { + "uid": "string", + "src": 0, + "latestTime": 0, + "count": 0, + "giftCount": 0, + "customId": 0, + "lastCharged": 0, + "bAvailableCnt": 0, + "bTodayVotedCnt": 0, + "nick": "string", + "gender": 0, + "age": 0, + "isFriend": true, + "isvip": true, + "isSvip": true, + "uin": 0 + } + ] + }, + "voteInfo": { + "total_count": 0, + "new_count": 0, + "new_nearby_count": 0, + "last_visit_time": 0, + "userInfos": [ + { + "uid": "string", + "src": 0, + "latestTime": 0, + "count": 0, + "giftCount": 0, + "customId": 0, + "lastCharged": 0, + "bAvailableCnt": 0, + "bTodayVotedCnt": 0, + "nick": "string", + "gender": 0, + "age": 0, + "isFriend": true, + "isvip": true, + "isSvip": true, + "uin": 0 + } + ] + } + }, + "message": "string", + "wording": "string", + "echo": "string" + } + ''' DELETE_FRIEND = "napcat_delete_friend" - '''删除好友''' + '''删除好友 + + Args: + user_id (Opetional[str|int]): 用户id(必需) + temp_block (Opetional[bool]): 拉黑(必需) + temp_both_del (Opetional[bool]): 双向删除(必需) + raw (Optional[dict]): 原始请求体 + + Returns: + dict: { + "status": "ok", + "retcode": 0, + "data": { + "result": 0, + "errMsg": "string" + }, + "message": "string", + "wording": "string", + "echo": "string" + } + ''' GET_USER_STATUS = "napcat_get_user_status" - '''获取用户状态''' + '''获取(指定)用户状态 + + Args: + user_id (Opetional[str|int]): 用户id(必需) + raw (Optional[dict]): 原始请求体 + + Returns: + dict: { + "status": "ok", + "retcode": 0, + "data": { + "status": 0, + "ext_status": 0 + }, + "message": "string", + "wording": "string", + "echo": "string" + } + ''' GET_STATUS = "napcat_get_status" - '''获取状态''' + '''获取状态 + + Returns: + dict: { + "status": "ok", + "retcode": 0, + "data": { + "online": true, + "good": true, + "stat": {} + }, + "message": "string", + "wording": "string", + "echo": "string" + } + ''' GET_MINI_APP_ARK = "napcat_get_mini_app_ark" - '''获取小程序卡片''' + '''获取小程序卡片 + + Args: + type (Optional[str]): 类型(如bili、weibo,必需) + title (Optional[str]): 标题(必需) + desc (Optional[str]): 描述(必需) + picUrl (Optional[str]): 图片URL(必需) + jumpUrl (Optional[str]): 跳转URL(必需) + webUrl (Optional[str]): 网页URL + rawArkData (Optional[bool]): 是否返回原始ark数据 + raw (Optional[dict]): 原始请求体 + + Returns: + dict: { + "status": "ok", + "retcode": 0, + "data": { + "appName": "string", + "appView": "string", + "ver": "string", + "desc": "string", + "prompt": "string", + "metaData": { + "detail_1": { + "appid": "string", + "appType": 0, + "title": "string", + "desc": "string", + "icon": "string", + "preview": "string", + "url": "string", + "scene": 0, + "host": { + "uin": 0, + "nick": "string" + }, + "shareTemplateId": "string", + "shareTemplateData": {}, + "showLittleTail": "string", + "gamePoints": "string", + "gamePointsUrl": "string", + "shareOrigin": 0 + } + }, + "config": { + "type": "string", + "width": 0, + "height": 0, + "forward": 0, + "autoSize": 0, + "ctime": 0, + "token": "string" + } + }, + "message": "string", + "wording": "string", + "echo": "string" + } + ''' SET_DIY_ONLINE_STATUS = "napcat_set_diy_online_status" - '''设置自定义在线状态''' + '''设置自定义在线状态 + + Args: + face_id (Optional[str|int]): 表情ID(必需) + face_type (Optional[str|int]): 表情类型 + wording (Optional[str]):描述文本 + raw (Optional[dict]): 原始请求体 + + Returns: + dict: { + "status": "ok", + "retcode": 0, + "data": "string", + "message": "string", + "wording": "string", + "echo": "string" + } + ''' class MESSAGE(Enum): """ diff --git a/plugins/napcat_adapter_plugin/plugin.py b/plugins/napcat_adapter_plugin/plugin.py index a68f74cb6..c09debf27 100644 --- a/plugins/napcat_adapter_plugin/plugin.py +++ b/plugins/napcat_adapter_plugin/plugin.py @@ -148,7 +148,6 @@ class NapcatAdapterPlugin(BasePlugin): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - for e in event_types.NapcatEvent.ON_RECEIVED: event_manager.register_event(e ,allowed_triggers=[self.plugin_name]) diff --git a/src/plugin_system/base/base_event.py b/src/plugin_system/base/base_event.py index f884be7c9..1684da74d 100644 --- a/src/plugin_system/base/base_event.py +++ b/src/plugin_system/base/base_event.py @@ -9,7 +9,7 @@ class HandlerResult: 所有事件处理器必须返回此类的实例 """ - def __init__(self, success: bool, continue_process: bool, message: Any = {}, handler_name: str = ""): + def __init__(self, success: bool, continue_process: bool, message: Any = None, handler_name: str = ""): self.success = success self.continue_process = continue_process self.message = message @@ -83,8 +83,8 @@ class BaseEvent: def __init__( self, name: str, - allowed_subscribers: List[str]=[], - allowed_triggers: List[str]=[] + allowed_subscribers: List[str] = None, + allowed_triggers: List[str] = None ): self.name = name self.enabled = True From aa3ecfb63ab9f43d7dc537654797c2069da95e48 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 29 Aug 2025 16:23:08 +0800 Subject: [PATCH 5/9] =?UTF-8?q?feat:=E9=87=8D=E5=86=99=E4=BA=86hello=5Fwor?= =?UTF-8?q?ld?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/hello_world_plugin/_manifest.json | 19 ++++ plugins/hello_world_plugin/plugin.py | 130 ++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 plugins/hello_world_plugin/_manifest.json create mode 100644 plugins/hello_world_plugin/plugin.py diff --git a/plugins/hello_world_plugin/_manifest.json b/plugins/hello_world_plugin/_manifest.json new file mode 100644 index 000000000..29975f408 --- /dev/null +++ b/plugins/hello_world_plugin/_manifest.json @@ -0,0 +1,19 @@ +{ + "manifest_version": 1, + "name": "Hello World 插件", + "version": "1.0.1", + "description": "一个包含四大核心组件和高级配置功能的入门示例插件。", + "author": { + "name": "Kilo Code" + }, + "license": "MIT", + "keywords": [ + "example", + "tutorial", + "hello world" + ], + "categories": [ + "official", + "example" + ] +} \ No newline at end of file diff --git a/plugins/hello_world_plugin/plugin.py b/plugins/hello_world_plugin/plugin.py new file mode 100644 index 000000000..8fdb6c08d --- /dev/null +++ b/plugins/hello_world_plugin/plugin.py @@ -0,0 +1,130 @@ +from typing import List, Tuple, Type, Dict, Any, Optional +import logging +import random + +from src.plugin_system import ( + BasePlugin, + register_plugin, + ComponentInfo, + BaseEventHandler, + EventType, + BaseTool, + ToolParamType, + PlusCommand, + CommandArgs, + ChatType, + BaseAction, + ActionActivationType, + ConfigField, +) +from src.plugin_system.base.base_event import HandlerResult + + +class StartupMessageHandler(BaseEventHandler): + """启动时打印消息的事件处理器。""" + + handler_name = "hello_world_startup_handler" + handler_description = "在机器人启动时打印一条日志。" + init_subscribe = [EventType.ON_START] + + async def execute(self, params: dict) -> HandlerResult: + logging.info("🎉 Hello World 插件已启动,准备就绪!") + return HandlerResult(success=True, continue_process=True) + + +class GetSystemInfoTool(BaseTool): + """一个提供系统信息的示例工具。""" + + name = "get_system_info" + description = "获取当前系统的模拟版本和状态信息。" + available_for_llm = True + parameters = [] + + async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]: + return {"name": self.name, "content": "系统版本: 1.0.1, 状态: 运行正常"} + + +class HelloCommand(PlusCommand): + """一个简单的 /hello 命令,使用配置文件中的问候语。""" + + command_name = "hello" + command_description = "向机器人发送一个简单的问候。" + command_aliases = ["hi", "你好"] + chat_type_allow = ChatType.ALL + + async def execute(self, args: CommandArgs) -> Tuple[bool, Optional[str], bool]: + greeting = str(self.get_config("greeting.message", "Hello, World! 我是一个由 MoFox_Bot 驱动的插件。")) + await self.send_text(greeting) + return True, "成功发送问候", True + + +class RandomEmojiAction(BaseAction): + """一个随机发送表情的动作。""" + + action_name = "random_emoji" + action_description = "随机发送一个表情符号,增加聊天的趣味性。" + activation_type = ActionActivationType.RANDOM + random_activation_probability = 0.1 + action_require = ["当对话气氛轻松时", "可以用来回应简单的情感表达"] + associated_types = ["text"] + + async def execute(self) -> Tuple[bool, str]: + emojis = ["😊", "😂", "👍", "🎉", "🤔", "🤖"] + await self.send_text(random.choice(emojis)) + return True, "成功发送了一个随机表情" + + +@register_plugin +class HelloWorldPlugin(BasePlugin): + """一个包含四大核心组件和高级配置功能的入门示例插件。""" + + plugin_name = "hello_world_plugin" + enable_plugin = True + dependencies = [] + python_dependencies = [] + config_file_name = "config.toml" + enable_plugin = False + + config_schema = { + "meta": { + "config_version": ConfigField( + type=int, + default=1, + description="配置文件版本,请勿手动修改。" + ), + }, + "greeting": { + "message": ConfigField( + type=str, + default="这是来自配置文件的问候!👋", + description="HelloCommand 使用的问候语。" + ), + }, + "components": { + "hello_command_enabled": ConfigField( + type=bool, + default=True, + description="是否启用 /hello 命令。" + ), + "random_emoji_action_enabled": ConfigField( + type=bool, + default=True, + description="是否启用随机表情动作。" + ), + } + } + + def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: + """根据配置文件动态注册插件的功能组件。""" + components: List[Tuple[ComponentInfo, Type]] = [] + + components.append((StartupMessageHandler.get_handler_info(), StartupMessageHandler)) + components.append((GetSystemInfoTool.get_tool_info(), GetSystemInfoTool)) + + if self.get_config("components.hello_command_enabled", True): + components.append((HelloCommand.get_command_info(), HelloCommand)) + + if self.get_config("components.random_emoji_action_enabled", True): + components.append((RandomEmojiAction.get_action_info(), RandomEmojiAction)) + + return components \ No newline at end of file From edc7b36724803815bc7cf33982090bee2ad35396 Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Fri, 29 Aug 2025 16:58:52 +0800 Subject: [PATCH 6/9] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/napcat_adapter_plugin/plugin.py | 92 +++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 7 deletions(-) diff --git a/plugins/napcat_adapter_plugin/plugin.py b/plugins/napcat_adapter_plugin/plugin.py index c09debf27..21096312d 100644 --- a/plugins/napcat_adapter_plugin/plugin.py +++ b/plugins/napcat_adapter_plugin/plugin.py @@ -116,14 +116,92 @@ class APITestHandler(BaseEventHandler): async def execute(self,_): logger.info("5s后开始测试napcat接口...") await asyncio.sleep(5) + ''' + # 测试获取登录信息 + logger.info("测试获取登录信息...") res = await event_manager.trigger_event( - event_types.NapcatEvent.ACCOUNT.SET_PROFILE, - nickname="我叫杰瑞喵、", - personal_note="喵汪~", - sex=2 - ) - logger.info(res.get_message_result()) - return HandlerResult(True,True,"") + event_types.NapcatEvent.ACCOUNT.GET_LOGIN_INFO + ) + logger.info(f"GET_LOGIN_INFO: {res.get_message_result()}") + + # 测试获取状态 + logger.info("测试获取状态...") + res = await event_manager.trigger_event( + event_types.NapcatEvent.ACCOUNT.GET_STATUS + ) + logger.info(f"GET_STATUS: {res.get_message_result()}") + + # 测试获取好友列表 + logger.info("测试获取好友列表...") + res = await event_manager.trigger_event( + event_types.NapcatEvent.ACCOUNT.GET_FRIEND_LIST, + no_cache=False + ) + logger.info(f"GET_FRIEND_LIST: {res.get_message_result()}") + + # 测试获取好友分组列表 + logger.info("测试获取好友分组列表...") + res = await event_manager.trigger_event( + event_types.NapcatEvent.ACCOUNT.GET_FRIENDS_WITH_CATEGORY + ) + logger.info(f"GET_FRIENDS_WITH_CATEGORY: {res.get_message_result()}") + + # 测试获取在线客户端 + logger.info("测试获取在线客户端...") + res = await event_manager.trigger_event( + event_types.NapcatEvent.ACCOUNT.GET_ONLINE_CLIENTS, + no_cache=True + ) + logger.info(f"GET_ONLINE_CLIENTS: {res.get_message_result()}") + + # 测试获取最近联系人 + logger.info("测试获取最近联系人...") + res = await event_manager.trigger_event( + event_types.NapcatEvent.ACCOUNT.GET_RECENT_CONTACT, + count=5 + ) + logger.info(f"GET_RECENT_CONTACT: {res.get_message_result()}") + + # 测试设置个性签名 + logger.info("测试设置个性签名...") + res = await event_manager.trigger_event( + event_types.NapcatEvent.ACCOUNT.SET_SELF_LONGNICK, + longNick="测试个性签名 - 来自APITestHandler" + ) + logger.info(f"SET_SELF_LONGNICK: {res.get_message_result()}") + + # 测试设置在线状态 + logger.info("测试设置在线状态...") + res = await event_manager.trigger_event( + event_types.NapcatEvent.ACCOUNT.SET_ONLINE_STATUS, + status="11", + ext_status="0", + battery_status="0" + ) + logger.info(f"SET_ONLINE_STATUS: {res.get_message_result()}") + + # 测试设置自定义在线状态 + logger.info("测试设置自定义在线状态...") + res = await event_manager.trigger_event( + event_types.NapcatEvent.ACCOUNT.SET_DIY_ONLINE_STATUS, + face_id="358", + face_type="1", + wording="测试中..." + ) + logger.info(f"SET_DIY_ONLINE_STATUS: {res.get_message_result()}") + + # 测试获取点赞列表 + logger.info("测试获取点赞列表...") + res = await event_manager.trigger_event( + event_types.NapcatEvent.ACCOUNT.GET_PROFILE_LIKE, + start=0, + count=5 + ) + logger.info(f"GET_PROFILE_LIKE: {res.get_message_result()}") + + logger.info("所有ACCOUNT接口测试完成!") + ''' + return HandlerResult(True,True,"所有接口测试完成") @register_plugin class NapcatAdapterPlugin(BasePlugin): From 41565eb144b54832656e83d19092ed6a42f0c37b Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 29 Aug 2025 17:40:22 +0800 Subject: [PATCH 7/9] =?UTF-8?q?refactor(schedule):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E7=9D=A1=E7=9C=A0=E7=B3=BB=E7=BB=9F=E4=B8=BA=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E6=9C=BA=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将原有的睡眠、失眠、唤醒等分散的布尔标记逻辑重构为一个统一的睡眠状态机(SleepState),以提高代码的可读性、可维护性和可扩展性。 主要变更: - 引入 `SleepState` 枚举,包含 `AWAKE`, `INSOMNIA`, `PREPARING_SLEEP`, `SLEEPING`, `WOKEN_UP` 状态。 - 在 `ScheduleManager` 中实现 `_update_sleep_state` 作为核心状态机,统一管理所有状态转换。 - 将原有的失眠判断逻辑从 `WakeUpManager` 移至 `ScheduleManager` 的状态机内部,与弹性睡眠决策合并,简化了模块职责。 - `heartFC_chat.py` 中的聊天循环现在直接查询 `ScheduleManager` 的当前状态,而不是处理多个独立的布尔值,使逻辑更清晰。 - 删除了 `WakeUpManager` 中与失眠相关的配置和方法,因为它现在由 `ScheduleManager` 统一管理。 - 删除了配置中已废弃的 `enable_is_sleep` 选项。 --- src/chat/chat_loop/heartFC_chat.py | 45 ++--- src/chat/chat_loop/wakeup_manager.py | 44 +---- src/config/official_configs.py | 1 - src/schedule/schedule_manager.py | 243 +++++++++++++++------------ 4 files changed, 150 insertions(+), 183 deletions(-) diff --git a/src/chat/chat_loop/heartFC_chat.py b/src/chat/chat_loop/heartFC_chat.py index 7e386ceac..05f50388d 100644 --- a/src/chat/chat_loop/heartFC_chat.py +++ b/src/chat/chat_loop/heartFC_chat.py @@ -8,7 +8,7 @@ from src.config.config import global_config from src.person_info.relationship_builder_manager import relationship_builder_manager from src.chat.express.expression_learner import expression_learner_manager from src.plugin_system.base.component_types import ChatMode -from src.schedule.schedule_manager import schedule_manager +from src.schedule.schedule_manager import schedule_manager, SleepState from src.plugin_system.apis import message_api from .hfc_context import HfcContext @@ -196,30 +196,14 @@ class HeartFChatting: - FOCUS模式:直接处理所有消息并检查退出条件 - NORMAL模式:检查进入FOCUS模式的条件,并通过normal_mode_handler处理消息 """ - is_sleeping = schedule_manager.is_sleeping(self.wakeup_manager) - - # --- 失眠状态管理 --- - if self.context.is_in_insomnia and time.time() > self.context.insomnia_end_time: - # 失眠状态结束 - self.context.is_in_insomnia = False - await self.proactive_thinker.trigger_goodnight_thinking() - - if is_sleeping and not self.context.was_sleeping: - # 刚刚进入睡眠状态,进行一次入睡检查 - if self.wakeup_manager and self.wakeup_manager.check_for_insomnia(): - # 触发失眠 - self.context.is_in_insomnia = True - duration = global_config.sleep_system.insomnia_duration_minutes * 60 - self.context.insomnia_end_time = time.time() + duration - - # 判断失眠原因并触发思考 - reason = "random" - if self.context.sleep_pressure < global_config.sleep_system.sleep_pressure_threshold: - reason = "low_pressure" - await self.proactive_thinker.trigger_insomnia_thinking(reason) + # --- 核心状态更新 --- + await schedule_manager._update_sleep_state(self.wakeup_manager) + current_sleep_state = schedule_manager.get_current_sleep_state() + is_sleeping = current_sleep_state == SleepState.SLEEPING + is_in_insomnia = current_sleep_state == SleepState.INSOMNIA # 核心修复:在睡眠模式(包括失眠)下获取消息时,不过滤命令消息,以确保@消息能被接收 - filter_command_flag = not is_sleeping + filter_command_flag = not (is_sleeping or is_in_insomnia) recent_messages = message_api.get_messages_by_time_in_chat( chat_id=self.context.stream_id, @@ -239,16 +223,17 @@ class HeartFChatting: self.context.last_read_time = time.time() # 处理唤醒度逻辑 - if is_sleeping: + if current_sleep_state in [SleepState.SLEEPING, SleepState.PREPARING_SLEEP, SleepState.INSOMNIA]: 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 current_is_sleeping: - # 仍然在睡眠,跳过本轮的消息处理 + # 再次获取最新状态,因为 handle_wakeup 可能导致状态变为 WOKEN_UP + current_sleep_state = schedule_manager.get_current_sleep_state() + + if current_sleep_state == SleepState.SLEEPING: + # 只有在纯粹的 SLEEPING 状态下才跳过消息处理 return has_new_messages - else: - # 从睡眠中被唤醒,需要继续处理本轮消息 + + if current_sleep_state == SleepState.WOKEN_UP: logger.info(f"{self.context.log_prefix} 从睡眠中被唤醒,将处理积压的消息。") self.context.last_wakeup_time = time.time() diff --git a/src/chat/chat_loop/wakeup_manager.py b/src/chat/chat_loop/wakeup_manager.py index 01c95103e..53c111cb1 100644 --- a/src/chat/chat_loop/wakeup_manager.py +++ b/src/chat/chat_loop/wakeup_manager.py @@ -41,13 +41,6 @@ class WakeUpManager: self.enabled = sleep_config.enable self.angry_prompt = sleep_config.angry_prompt - # 失眠系统参数 - self.insomnia_enabled = sleep_config.enable_insomnia_system - self.sleep_pressure_threshold = sleep_config.sleep_pressure_threshold - self.deep_sleep_threshold = sleep_config.deep_sleep_threshold - self.insomnia_chance_low_pressure = sleep_config.insomnia_chance_low_pressure - self.insomnia_chance_normal_pressure = sleep_config.insomnia_chance_normal_pressure - self._load_wakeup_state() def _get_storage_key(self) -> str: @@ -220,39 +213,4 @@ class WakeUpManager: "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 - } - - def check_for_insomnia(self) -> bool: - """ - 在尝试入睡时检查是否会失眠 - - Returns: - bool: 如果失眠则返回 True,否则返回 False - """ - if not self.insomnia_enabled: - return False - - import random - - pressure = self.context.sleep_pressure - - # 压力过高,深度睡眠,极难失眠 - if pressure > self.deep_sleep_threshold: - return False - - # 根据睡眠压力决定失眠概率 - from src.schedule.schedule_manager import schedule_manager - if pressure < self.sleep_pressure_threshold: - # 压力不足型失眠 - 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: - # 压力正常,随机失眠 - if random.random() < self.insomnia_chance_normal_pressure: - logger.info(f"{self.context.log_prefix} 睡眠压力正常 ({pressure:.1f}),触发随机失眠!") - return True - - return False \ No newline at end of file + } \ No newline at end of file diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 7e8dd4446..6be8db364 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -529,7 +529,6 @@ 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会根据日程表睡觉和苏醒") class DependencyManagementConfig(ValidatedConfigBase): diff --git a/src/schedule/schedule_manager.py b/src/schedule/schedule_manager.py index 84b87c657..a1599424e 100644 --- a/src/schedule/schedule_manager.py +++ b/src/schedule/schedule_manager.py @@ -2,7 +2,8 @@ import orjson import asyncio import random from datetime import datetime, time, timedelta -from typing import Optional, List, Dict, Any +from enum import Enum, auto +from typing import Optional, List, Dict, Any, TYPE_CHECKING from lunar_python import Lunar from pydantic import BaseModel, ValidationError, validator @@ -19,6 +20,9 @@ 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 +if TYPE_CHECKING: + from src.chat.chat_loop.wakeup_manager import WakeUpManager + logger = get_logger("schedule_manager") @@ -121,6 +125,14 @@ class ScheduleData(BaseModel): # 检查是否所有分钟都被覆盖 return all(covered) +class SleepState(Enum): + """睡眠状态枚举""" + AWAKE = auto() # 完全清醒 + INSOMNIA = auto() # 失眠(在理论睡眠时间内保持清醒) + PREPARING_SLEEP = auto() # 准备入睡(缓冲期) + SLEEPING = auto() # 正在休眠 + WOKEN_UP = auto() # 被吵醒 + class ScheduleManager: def __init__(self): self.today_schedule: Optional[List[Dict[str, Any]]] = None @@ -131,14 +143,12 @@ class ScheduleManager: self.sleep_log_interval = 35 # 日志记录间隔,单位秒 self.schedule_generation_running = False # 防止重复生成任务 - # 弹性睡眠相关状态 - self._is_preparing_sleep: bool = False + # --- 统一睡眠状态管理 --- + self._current_state: SleepState = SleepState.AWAKE 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() @@ -406,147 +416,162 @@ class ScheduleManager: continue return None - def is_sleeping(self, wakeup_manager: Optional["WakeUpManager"] = None) -> bool: + def get_current_sleep_state(self) -> SleepState: + """获取当前的睡眠状态""" + return self._current_state + + def is_sleeping(self) -> bool: + """检查当前是否处于正式休眠状态""" + return self._current_state == SleepState.SLEEPING + + async def _update_sleep_state(self, wakeup_manager: Optional["WakeUpManager"] = None): """ - 通过关键词匹配、唤醒度、睡眠压力等综合判断是否处于休眠时间。 - 新增弹性睡眠机制,允许在压力低时延迟入睡,并在入睡前发送通知。 + 核心状态机:根据当前情况更新睡眠状态 """ # --- 基础检查 --- - if not global_config.schedule.enable_is_sleep: - return False - if not self.today_schedule: - return False + if not global_config.sleep_system.enable or not self.today_schedule: + if self._current_state != SleepState.AWAKE: + logger.debug("睡眠系统禁用或无日程,强制设为 AWAKE") + self._current_state = SleepState.AWAKE + return now = datetime.now() today = now.date() # --- 每日状态重置 --- if self._last_sleep_check_date != today: - logger.info(f"新的一天 ({today}),重置弹性睡眠状态。") + logger.info(f"新的一天 ({today}),重置睡眠状态为 AWAKE。") self._total_delayed_minutes_today = 0 - self._is_preparing_sleep = False + self._current_state = SleepState.AWAKE 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 # 离开睡眠时间,重置唤醒状态 + # =================================== + # 状态机核心逻辑 + # =================================== + + # 状态:清醒 (AWAKE) + if self._current_state == SleepState.AWAKE: + if is_in_theoretical_sleep: + logger.info(f"进入理论休眠时间 '{activity}',开始进行睡眠决策...") + + # --- 合并后的失眠与弹性睡眠决策逻辑 --- + sleep_pressure = wakeup_manager.context.sleep_pressure if wakeup_manager else 999 + pressure_threshold = global_config.sleep_system.flexible_sleep_pressure_threshold + + # 决策1:因睡眠压力低而延迟入睡(原弹性睡眠) + if sleep_pressure < pressure_threshold and self._total_delayed_minutes_today < global_config.sleep_system.max_sleep_delay_minutes: + delay_minutes = 15 + self._total_delayed_minutes_today += delay_minutes + self._sleep_buffer_end_time = now + timedelta(minutes=delay_minutes) + self._current_state = SleepState.INSOMNIA + logger.info(f"睡眠压力 ({sleep_pressure:.1f}) 低于阈值 ({pressure_threshold}),进入失眠状态,延迟入睡 {delay_minutes} 分钟。") + + # 发送睡前通知 + if global_config.sleep_system.enable_pre_sleep_notification: + asyncio.create_task(self._send_pre_sleep_notification()) + + # 决策2:进入正常的入睡准备流程 + else: + buffer_seconds = random.randint(5 * 60, 10 * 60) + self._sleep_buffer_end_time = now + timedelta(seconds=buffer_seconds) + self._current_state = SleepState.PREPARING_SLEEP + logger.info(f"睡眠压力正常或已达今日最大延迟,进入准备入睡状态,将在 {buffer_seconds / 60:.1f} 分钟内入睡。") + + # 发送睡前通知 + if global_config.sleep_system.enable_pre_sleep_notification: + asyncio.create_task(self._send_pre_sleep_notification()) + 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 + # 状态:失眠 (INSOMNIA) + elif self._current_state == SleepState.INSOMNIA: + if not is_in_theoretical_sleep: + logger.info("已离开理论休眠时间,失眠结束,恢复清醒。") + self._current_state = SleepState.AWAKE + self._save_sleep_state() + # TODO: 添加从失眠到准备入睡的转换逻辑 - # --- 核心:弹性睡眠逻辑 --- - 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} 分钟。") + # 状态:准备入睡 (PREPARING_SLEEP) + elif self._current_state == SleepState.PREPARING_SLEEP: + if not is_in_theoretical_sleep: + logger.info("准备入睡期间离开理论休眠时间,取消入睡,恢复清醒。") + self._current_state = SleepState.AWAKE + self._sleep_buffer_end_time = None + self._save_sleep_state() + elif self._sleep_buffer_end_time and now >= self._sleep_buffer_end_time: + logger.info("睡眠缓冲期结束,正式进入休眠状态。") + self._current_state = SleepState.SLEEPING + self._last_fully_slept_log_time = now.timestamp() + self._save_sleep_state() + + # 状态:休眠中 (SLEEPING) + elif self._current_state == SleepState.SLEEPING: + if not is_in_theoretical_sleep: + logger.info("理论休眠时间结束,自然醒来。") + self._current_state = SleepState.AWAKE + self._save_sleep_state() 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} 分钟内入睡。") + # 记录日志 + 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 - # 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 + # 状态:被吵醒 (WOKEN_UP) + elif self._current_state == SleepState.WOKEN_UP: + if not is_in_theoretical_sleep: + logger.info("理论休眠时间结束,被吵醒的状态自动结束。") + self._current_state = SleepState.AWAKE + self._save_sleep_state() + # TODO: 添加重新入睡的逻辑 def reset_sleep_state_after_wakeup(self): - """被唤醒后重置睡眠状态""" - if self._is_preparing_sleep or self.is_sleeping(): - logger.info("被唤醒,重置所有睡眠准备状态,恢复清醒!") - self._is_preparing_sleep = False + """被唤醒后,将状态切换到 WOKEN_UP""" + if self._current_state in [SleepState.PREPARING_SLEEP, SleepState.SLEEPING, SleepState.INSOMNIA]: + logger.info("被唤醒,进入 WOKEN_UP 状态!") + self._current_state = SleepState.WOKEN_UP 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: - try: - activity = event.get("activity", "").strip() - time_range = event.get("time_range") + if self.today_schedule: + for event in self.today_schedule: + try: + activity = event.get("activity", "").strip() + time_range = event.get("time_range") - if not activity or not time_range: + if not activity or not time_range: + continue + + if any(keyword in activity for keyword in sleep_keywords): + start_str, end_str = time_range.split('-') + start_time = datetime.strptime(start_str.strip(), "%H:%M").time() + end_time = datetime.strptime(end_str.strip(), "%H:%M").time() + + if start_time <= end_time: # 同一天 + if start_time <= now_time < end_time: + return True, activity + else: # 跨天 + if now_time >= start_time or now_time < end_time: + return True, activity + except (ValueError, KeyError, AttributeError) as e: + logger.warning(f"解析日程事件时出错: {event}, 错误: {e}") continue - - if any(keyword in activity for keyword in sleep_keywords): - start_str, end_str = time_range.split('-') - start_time = datetime.strptime(start_str.strip(), "%H:%M").time() - end_time = datetime.strptime(end_str.strip(), "%H:%M").time() - - if start_time <= end_time: # 同一天 - if start_time <= now_time < end_time: - return True, activity - else: # 跨天 - if now_time >= start_time or now_time < end_time: - return True, activity - except (ValueError, KeyError, AttributeError) as e: - logger.warning(f"解析日程事件时出错: {event}, 错误: {e}") - continue - + return False, None async def _send_pre_sleep_notification(self): """异步生成并发送睡前通知""" try: - groups = global_config.schedule.pre_sleep_notification_groups - prompt = global_config.schedule.pre_sleep_prompt + groups = global_config.sleep_system.pre_sleep_notification_groups + prompt = global_config.sleep_system.pre_sleep_prompt if not groups: logger.info("未配置睡前通知的群组,跳过发送。") From 35db4c5d9143296857a047770c625bc5af1f5f2b Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 29 Aug 2025 18:17:16 +0800 Subject: [PATCH 8/9] =?UTF-8?q?refactor(schedule):=20=E5=B0=86=E7=9D=A1?= =?UTF-8?q?=E7=9C=A0=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=B9=B6=E8=BF=81=E7=A7=BB=E5=88=B0SleepMana?= =?UTF-8?q?ger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将原先分散在 `ScheduleManager` 中的睡眠状态机逻辑(包括状态判断、转换、持久化等)抽取并封装到一个新的 `SleepManager` 类中。 这次重构的主要目的如下: - **职责分离**: `ScheduleManager` 的核心职责是管理日程的生成和查询,而睡眠状态的管理是一个独立的、复杂的逻辑单元。将其分离可以使两个类的职责更单一、代码更清晰。 - **可维护性**: 将所有与睡眠相关的状态和逻辑集中到 `SleepManager` 中,使得未来对睡眠功能的修改和扩展更加容易,减少了对 `ScheduleManager` 的影响。 - **代码简化**: `ScheduleManager` 不再需要管理内部的睡眠状态变量(如 `_current_state`, `_sleep_buffer_end_time` 等),而是通过委托 `sleep_manager` 实例来处理,简化了其内部实现。 相应的,`HfcContext` 中冗余的睡眠相关状态(如 `is_in_insomnia`)也被移除,统一由 `SleepManager` 管理。其他模块(如 `HeartFChatting`, `WakeUpManager`)对睡眠状态的调用也已更新为通过 `schedule_manager.sleep_manager` 或其代理方法进行。 --- src/chat/chat_loop/heartFC_chat.py | 7 +- src/chat/chat_loop/hfc_context.py | 10 - src/chat/chat_loop/wakeup_manager.py | 4 +- src/schedule/schedule_manager.py | 429 ++++++--------------------- src/schedule/sleep_manager.py | 334 +++++++++++++++++++++ 5 files changed, 425 insertions(+), 359 deletions(-) create mode 100644 src/schedule/sleep_manager.py diff --git a/src/chat/chat_loop/heartFC_chat.py b/src/chat/chat_loop/heartFC_chat.py index 05f50388d..fabe2a116 100644 --- a/src/chat/chat_loop/heartFC_chat.py +++ b/src/chat/chat_loop/heartFC_chat.py @@ -197,7 +197,7 @@ class HeartFChatting: - NORMAL模式:检查进入FOCUS模式的条件,并通过normal_mode_handler处理消息 """ # --- 核心状态更新 --- - await schedule_manager._update_sleep_state(self.wakeup_manager) + await schedule_manager.update_sleep_state(self.wakeup_manager) current_sleep_state = schedule_manager.get_current_sleep_state() is_sleeping = current_sleep_state == SleepState.SLEEPING is_in_insomnia = current_sleep_state == SleepState.INSOMNIA @@ -235,7 +235,6 @@ class HeartFChatting: if current_sleep_state == SleepState.WOKEN_UP: logger.info(f"{self.context.log_prefix} 从睡眠中被唤醒,将处理积压的消息。") - self.context.last_wakeup_time = time.time() # 根据聊天模式处理新消息 if self.context.loop_mode == ChatMode.FOCUS: @@ -258,12 +257,12 @@ class HeartFChatting: # --- 重新入睡逻辑 --- # 如果被吵醒了,并且在一定时间内没有新消息,则尝试重新入睡 - if schedule_manager._is_woken_up and not has_new_messages: + if schedule_manager.get_current_sleep_state() == SleepState.WOKEN_UP and not has_new_messages: re_sleep_delay = global_config.sleep_system.re_sleep_delay_minutes * 60 # 使用 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() + schedule_manager.reset_sleep_state_after_wakeup() # 保存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 767ee60bc..1920c5417 100644 --- a/src/chat/chat_loop/hfc_context.py +++ b/src/chat/chat_loop/hfc_context.py @@ -46,11 +46,6 @@ class HfcContext: self.sleep_pressure = 0.0 self.was_sleeping = False # 用于检测睡眠状态的切换 - # 失眠状态 - 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 @@ -78,8 +73,6 @@ class HfcContext: 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: @@ -91,9 +84,6 @@ class HfcContext: 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") diff --git a/src/chat/chat_loop/wakeup_manager.py b/src/chat/chat_loop/wakeup_manager.py index 53c111cb1..01a74150c 100644 --- a/src/chat/chat_loop/wakeup_manager.py +++ b/src/chat/chat_loop/wakeup_manager.py @@ -137,7 +137,9 @@ class WakeUpManager: # 只有在休眠且非失眠状态下才累积唤醒度 from src.schedule.schedule_manager import schedule_manager - if not schedule_manager.is_sleeping() or self.context.is_in_insomnia: + from src.schedule.sleep_manager import SleepState + current_sleep_state = schedule_manager.get_current_sleep_state() + if current_sleep_state != SleepState.SLEEPING: return False old_value = self.wakeup_value diff --git a/src/schedule/schedule_manager.py b/src/schedule/schedule_manager.py index a1599424e..e613d1c4b 100644 --- a/src/schedule/schedule_manager.py +++ b/src/schedule/schedule_manager.py @@ -1,8 +1,7 @@ import orjson import asyncio import random -from datetime import datetime, time, timedelta -from enum import Enum, auto +from datetime import datetime, time, timedelta, date from typing import Optional, List, Dict, Any, TYPE_CHECKING from lunar_python import Lunar from pydantic import BaseModel, ValidationError, validator @@ -10,15 +9,14 @@ from pydantic import BaseModel, ValidationError, validator from src.common.database.sqlalchemy_models import Schedule, get_db_session from src.common.database.monthly_plan_db import ( get_smart_plans_for_daily_schedule, - update_plan_usage # 保留兼容性 + update_plan_usage, # 保留兼容性 ) from src.config.config import global_config, model_config 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 +from .sleep_manager import SleepManager, SleepState if TYPE_CHECKING: from src.chat.chat_loop.wakeup_manager import WakeUpManager @@ -35,81 +33,85 @@ DEFAULT_SCHEDULE_GUIDELINES = """ 另外,请保证充足的休眠时间来处理和整合一天的数据。 """ + class ScheduleItem(BaseModel): """单个日程项的Pydantic模型""" + time_range: str activity: str - - @validator('time_range') + + @validator("time_range") def validate_time_range(cls, v): """验证时间范围格式""" - if not v or '-' not in v: + if not v or "-" not in v: raise ValueError("时间范围必须包含'-'分隔符") - + try: - start_str, end_str = v.split('-', 1) + start_str, end_str = v.split("-", 1) start_str = start_str.strip() end_str = end_str.strip() - + # 验证时间格式 datetime.strptime(start_str, "%H:%M") datetime.strptime(end_str, "%H:%M") - + return v except ValueError as e: raise ValueError(f"时间格式无效,应为HH:MM-HH:MM格式: {e}") from e - - @validator('activity') + + @validator("activity") def validate_activity(cls, v): """验证活动描述""" if not v or not v.strip(): raise ValueError("活动描述不能为空") return v.strip() + class ScheduleData(BaseModel): """完整日程数据的Pydantic模型""" + schedule: List[ScheduleItem] - - @validator('schedule') + + @validator("schedule") def validate_schedule_completeness(cls, v): """验证日程是否覆盖24小时""" if not v: raise ValueError("日程不能为空") - + # 收集所有时间段 time_ranges = [] for item in v: try: - start_str, end_str = item.time_range.split('-', 1) + start_str, end_str = item.time_range.split("-", 1) start_time = datetime.strptime(start_str.strip(), "%H:%M").time() end_time = datetime.strptime(end_str.strip(), "%H:%M").time() time_ranges.append((start_time, end_time)) except ValueError: continue - + # 检查是否覆盖24小时 if not cls._check_24_hour_coverage(time_ranges): raise ValueError("日程必须覆盖完整的24小时") - + return v - + @staticmethod def _check_24_hour_coverage(time_ranges: List[tuple]) -> bool: """检查时间段是否覆盖24小时""" if not time_ranges: return False - + # 将时间转换为分钟数进行计算 def time_to_minutes(t: time) -> int: return t.hour * 60 + t.minute - + # 创建覆盖情况数组 (1440分钟 = 24小时) covered = [False] * 1440 - + for start_time, end_time in time_ranges: start_min = time_to_minutes(start_time) end_min = time_to_minutes(end_time) - + if start_min <= end_min: # 同一天内的时间段 for i in range(start_min, end_min): @@ -121,17 +123,10 @@ class ScheduleData(BaseModel): covered[i] = True for i in range(0, end_min): covered[i] = True - + # 检查是否所有分钟都被覆盖 return all(covered) -class SleepState(Enum): - """睡眠状态枚举""" - AWAKE = auto() # 完全清醒 - INSOMNIA = auto() # 失眠(在理论睡眠时间内保持清醒) - PREPARING_SLEEP = auto() # 准备入睡(缓冲期) - SLEEPING = auto() # 正在休眠 - WOKEN_UP = auto() # 被吵醒 class ScheduleManager: def __init__(self): @@ -139,18 +134,8 @@ class ScheduleManager: self.llm = LLMRequest(model_set=model_config.model_task_config.schedule_generator, request_type="schedule") self.max_retries = -1 # 无限重试,直到成功生成标准日程表 self.daily_task_started = False - self.last_sleep_log_time = 0 - self.sleep_log_interval = 35 # 日志记录间隔,单位秒 self.schedule_generation_running = False # 防止重复生成任务 - - # --- 统一睡眠状态管理 --- - self._current_state: SleepState = SleepState.AWAKE - 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._load_sleep_state() + self.sleep_manager = SleepManager(self) async def start_daily_schedule_generation(self): """启动每日零点自动生成新日程的任务""" @@ -175,10 +160,10 @@ class ScheduleManager: schedule_record = session.query(Schedule).filter(Schedule.date == today_str).first() if schedule_record: logger.info(f"从数据库加载今天的日程 ({today_str})。") - + try: schedule_data = orjson.loads(str(schedule_record.schedule_data)) - + # 使用Pydantic验证日程数据 if self._validate_schedule_with_pydantic(schedule_data): self.today_schedule = schedule_data @@ -207,15 +192,15 @@ class ScheduleManager: if self.schedule_generation_running: logger.info("日程生成任务已在运行中,跳过重复启动") return - + # 创建异步任务进行日程生成,不阻塞主程序 asyncio.create_task(self._async_generate_and_save_schedule()) logger.info("已启动异步日程生成任务") - + async def _async_generate_and_save_schedule(self): """异步生成并保存日程的内部方法""" self.schedule_generation_running = True - + try: now = datetime.now() today_str = now.strftime("%Y-%m-%d") @@ -227,7 +212,7 @@ class ScheduleManager: festivals = lunar.getFestivals() other_festivals = lunar.getOtherFestivals() all_festivals = festivals + other_festivals - + festival_block = "" if all_festivals: festival_text = "、".join(all_festivals) @@ -238,33 +223,28 @@ class ScheduleManager: used_plan_ids = [] if global_config.monthly_plan_system and global_config.monthly_plan_system.enable: # 使用新的智能抽取逻辑 - avoid_days = getattr(global_config.monthly_plan_system, 'avoid_repetition_days', 7) + avoid_days = getattr(global_config.monthly_plan_system, "avoid_repetition_days", 7) # 使用新的智能抽取逻辑 - avoid_days = getattr(global_config.monthly_plan_system, 'avoid_repetition_days', 7) + avoid_days = getattr(global_config.monthly_plan_system, "avoid_repetition_days", 7) sampled_plans = get_smart_plans_for_daily_schedule( - current_month_str, - max_count=3, - avoid_days=avoid_days + current_month_str, max_count=3, avoid_days=avoid_days ) # 如果计划耗尽,则触发补充生成 if not sampled_plans: logger.info("可用的月度计划已耗尽或不足,尝试进行补充生成...") from mmc.src.schedule.monthly_plan_manager import monthly_plan_manager + success = await monthly_plan_manager.generate_monthly_plans(current_month_str) if success: logger.info("补充生成完成,重新抽取月度计划...") sampled_plans = get_smart_plans_for_daily_schedule( - current_month_str, - max_count=3, - avoid_days=avoid_days + current_month_str, max_count=3, avoid_days=avoid_days ) else: logger.warning("月度计划补充生成失败。") - + if sampled_plans: - used_plan_ids = [plan.id for plan in sampled_plans] # SQLAlchemy 对象的 id 属性会自动返回实际值 - plan_texts = "\n".join([f"- {plan.plan_text}" for plan in sampled_plans]) monthly_plans_block = f""" **我这个月的一些小目标/计划 (请在今天的日程中适当体现)**: @@ -304,33 +284,33 @@ class ScheduleManager: 请你扮演我,以我的身份和口吻,为我生成一份完整的24小时日程表。 """ - + # 无限重试直到生成成功的标准日程表 attempt = 0 while True: attempt += 1 try: logger.info(f"正在生成日程 (第 {attempt} 次尝试)") - + # 构建当前尝试的prompt,增加压力提示 prompt = base_prompt if attempt > 1: failure_hint = f""" **重要提醒 (第{attempt}次尝试)**: -- 前面{attempt-1}次生成都失败了,请务必严格按照要求生成完整的24小时日程 +- 前面{attempt - 1}次生成都失败了,请务必严格按照要求生成完整的24小时日程 - 确保JSON格式正确,所有时间段连续覆盖24小时 - 时间格式必须为HH:MM-HH:MM,不能有时间间隙或重叠 - 不要输出任何解释文字,只输出纯JSON数组 - 确保输出完整,不要被截断 """ prompt += failure_hint - + response, _ = await self.llm.generate_response_async(prompt) - + # 尝试解析和验证JSON(项目内置的反截断机制会自动处理截断问题) schedule_data = orjson.loads(repair_json(response)) - + # 使用Pydantic验证生成的日程数据 if self._validate_schedule_with_pydantic(schedule_data): # 验证通过,保存到数据库 @@ -339,46 +319,49 @@ class ScheduleManager: existing_schedule = session.query(Schedule).filter(Schedule.date == today_str).first() if existing_schedule: # 更新现有日程 - session.query(Schedule).filter(Schedule.date == today_str).update({ - Schedule.schedule_data: orjson.dumps(schedule_data).decode('utf-8'), - Schedule.updated_at: datetime.now() - }) + session.query(Schedule).filter(Schedule.date == today_str).update( + { + Schedule.schedule_data: orjson.dumps(schedule_data).decode("utf-8"), + Schedule.updated_at: datetime.now(), + } + ) else: # 创建新日程 new_schedule = Schedule( - date=today_str, - schedule_data=orjson.dumps(schedule_data).decode('utf-8') + date=today_str, schedule_data=orjson.dumps(schedule_data).decode("utf-8") ) session.add(new_schedule) session.commit() - + # 美化输出 schedule_str = f"✅ 经过 {attempt} 次尝试,成功生成并保存今天的日程 ({today_str}):\n" for item in schedule_data: - schedule_str += f" - {item.get('time_range', '未知时间')}: {item.get('activity', '未知活动')}\n" + schedule_str += ( + f" - {item.get('time_range', '未知时间')}: {item.get('activity', '未知活动')}\n" + ) logger.info(schedule_str) - + self.today_schedule = schedule_data - + # 成功生成日程后,更新使用过的月度计划的统计信息 if used_plan_ids and global_config.monthly_plan_system: logger.info(f"更新使用过的月度计划 {used_plan_ids} 的统计信息。") update_plan_usage(used_plan_ids, today_str) # type: ignore - + # 成功生成,退出无限循环 break - + else: logger.warning(f"第 {attempt} 次生成的日程验证失败,继续重试...") # 添加短暂延迟,避免过于频繁的请求 await asyncio.sleep(2) - + except Exception as e: logger.error(f"第 {attempt} 次生成日程失败: {e}") logger.info("继续重试...") # 添加短暂延迟,避免过于频繁的请求 await asyncio.sleep(3) - + finally: self.schedule_generation_running = False logger.info("日程生成任务结束") @@ -396,12 +379,12 @@ class ScheduleManager: try: time_range = event.get("time_range") activity = event.get("activity") - + if not time_range or not activity: logger.warning(f"日程事件缺少必要字段: {event}") continue - - start_str, end_str = time_range.split('-') + + 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() @@ -418,262 +401,19 @@ class ScheduleManager: def get_current_sleep_state(self) -> SleepState: """获取当前的睡眠状态""" - return self._current_state + return self.sleep_manager.get_current_sleep_state() def is_sleeping(self) -> bool: """检查当前是否处于正式休眠状态""" - return self._current_state == SleepState.SLEEPING + return self.sleep_manager.is_sleeping() - async def _update_sleep_state(self, wakeup_manager: Optional["WakeUpManager"] = None): - """ - 核心状态机:根据当前情况更新睡眠状态 - """ - # --- 基础检查 --- - if not global_config.sleep_system.enable or not self.today_schedule: - if self._current_state != SleepState.AWAKE: - logger.debug("睡眠系统禁用或无日程,强制设为 AWAKE") - self._current_state = SleepState.AWAKE - return - - now = datetime.now() - today = now.date() - - # --- 每日状态重置 --- - if self._last_sleep_check_date != today: - logger.info(f"新的一天 ({today}),重置睡眠状态为 AWAKE。") - self._total_delayed_minutes_today = 0 - self._current_state = SleepState.AWAKE - self._sleep_buffer_end_time = None - self._last_sleep_check_date = today - self._save_sleep_state() - - # --- 判断当前是否为理论上的睡眠时间 --- - is_in_theoretical_sleep, activity = self._is_in_theoretical_sleep_time(now.time()) - - # =================================== - # 状态机核心逻辑 - # =================================== - - # 状态:清醒 (AWAKE) - if self._current_state == SleepState.AWAKE: - if is_in_theoretical_sleep: - logger.info(f"进入理论休眠时间 '{activity}',开始进行睡眠决策...") - - # --- 合并后的失眠与弹性睡眠决策逻辑 --- - sleep_pressure = wakeup_manager.context.sleep_pressure if wakeup_manager else 999 - pressure_threshold = global_config.sleep_system.flexible_sleep_pressure_threshold - - # 决策1:因睡眠压力低而延迟入睡(原弹性睡眠) - if sleep_pressure < pressure_threshold and self._total_delayed_minutes_today < global_config.sleep_system.max_sleep_delay_minutes: - delay_minutes = 15 - self._total_delayed_minutes_today += delay_minutes - self._sleep_buffer_end_time = now + timedelta(minutes=delay_minutes) - self._current_state = SleepState.INSOMNIA - logger.info(f"睡眠压力 ({sleep_pressure:.1f}) 低于阈值 ({pressure_threshold}),进入失眠状态,延迟入睡 {delay_minutes} 分钟。") - - # 发送睡前通知 - if global_config.sleep_system.enable_pre_sleep_notification: - asyncio.create_task(self._send_pre_sleep_notification()) - - # 决策2:进入正常的入睡准备流程 - else: - buffer_seconds = random.randint(5 * 60, 10 * 60) - self._sleep_buffer_end_time = now + timedelta(seconds=buffer_seconds) - self._current_state = SleepState.PREPARING_SLEEP - logger.info(f"睡眠压力正常或已达今日最大延迟,进入准备入睡状态,将在 {buffer_seconds / 60:.1f} 分钟内入睡。") - - # 发送睡前通知 - if global_config.sleep_system.enable_pre_sleep_notification: - asyncio.create_task(self._send_pre_sleep_notification()) - - self._save_sleep_state() - - # 状态:失眠 (INSOMNIA) - elif self._current_state == SleepState.INSOMNIA: - if not is_in_theoretical_sleep: - logger.info("已离开理论休眠时间,失眠结束,恢复清醒。") - self._current_state = SleepState.AWAKE - self._save_sleep_state() - # TODO: 添加从失眠到准备入睡的转换逻辑 - - # 状态:准备入睡 (PREPARING_SLEEP) - elif self._current_state == SleepState.PREPARING_SLEEP: - if not is_in_theoretical_sleep: - logger.info("准备入睡期间离开理论休眠时间,取消入睡,恢复清醒。") - self._current_state = SleepState.AWAKE - self._sleep_buffer_end_time = None - self._save_sleep_state() - elif self._sleep_buffer_end_time and now >= self._sleep_buffer_end_time: - logger.info("睡眠缓冲期结束,正式进入休眠状态。") - self._current_state = SleepState.SLEEPING - self._last_fully_slept_log_time = now.timestamp() - self._save_sleep_state() - - # 状态:休眠中 (SLEEPING) - elif self._current_state == SleepState.SLEEPING: - if not is_in_theoretical_sleep: - logger.info("理论休眠时间结束,自然醒来。") - self._current_state = SleepState.AWAKE - self._save_sleep_state() - else: - # 记录日志 - 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 - - # 状态:被吵醒 (WOKEN_UP) - elif self._current_state == SleepState.WOKEN_UP: - if not is_in_theoretical_sleep: - logger.info("理论休眠时间结束,被吵醒的状态自动结束。") - self._current_state = SleepState.AWAKE - self._save_sleep_state() - # TODO: 添加重新入睡的逻辑 + async def update_sleep_state(self, wakeup_manager: Optional["WakeUpManager"] = None): + """更新睡眠状态""" + await self.sleep_manager.update_sleep_state(wakeup_manager) def reset_sleep_state_after_wakeup(self): """被唤醒后,将状态切换到 WOKEN_UP""" - if self._current_state in [SleepState.PREPARING_SLEEP, SleepState.SLEEPING, SleepState.INSOMNIA]: - logger.info("被唤醒,进入 WOKEN_UP 状态!") - self._current_state = SleepState.WOKEN_UP - self._sleep_buffer_end_time = None - self._save_sleep_state() - - def _is_in_theoretical_sleep_time(self, now_time: time) -> (bool, Optional[str]): - """检查当前时间是否落在日程表的任何一个睡眠活动中""" - sleep_keywords = ["休眠", "睡觉", "梦乡"] - if self.today_schedule: - for event in self.today_schedule: - try: - activity = event.get("activity", "").strip() - time_range = event.get("time_range") - - if not activity or not time_range: - continue - - if any(keyword in activity for keyword in sleep_keywords): - start_str, end_str = time_range.split('-') - start_time = datetime.strptime(start_str.strip(), "%H:%M").time() - end_time = datetime.strptime(end_str.strip(), "%H:%M").time() - - if start_time <= end_time: # 同一天 - if start_time <= now_time < end_time: - return True, activity - else: # 跨天 - if now_time >= start_time or now_time < end_time: - return True, activity - except (ValueError, KeyError, AttributeError) as e: - logger.warning(f"解析日程事件时出错: {event}, 错误: {e}") - continue - - return False, None - - async def _send_pre_sleep_notification(self): - """异步生成并发送睡前通知""" - try: - groups = global_config.sleep_system.pre_sleep_notification_groups - prompt = global_config.sleep_system.pre_sleep_prompt - - if not groups: - logger.info("未配置睡前通知的群组,跳过发送。") - return - - if not prompt: - logger.warning("睡前通知的prompt为空,跳过发送。") - return - - # 为防止消息风暴,稍微延迟一下 - await asyncio.sleep(random.uniform(5, 15)) - - for group_id_str in groups: - try: - # 格式 "platform:group_id" - parts = group_id_str.split(":") - if len(parts) != 2: - logger.warning(f"无效的群组ID格式: {group_id_str}") - continue - - platform, group_id = parts - - # 使用与 ChatStream.get_stream_id 相同的逻辑生成 stream_id - import hashlib - key = "_".join([platform, group_id]) - stream_id = hashlib.md5(key.encode()).hexdigest() - - logger.info(f"正在为群组 {group_id_str} (Stream ID: {stream_id}) 生成睡前消息...") - - # 调用 generator_api 生成回复 - success, reply_set, _ = await generator_api.generate_reply( - chat_id=stream_id, - extra_info=prompt, - request_type="schedule.pre_sleep_notification" - ) - - if success and reply_set: - # 提取文本内容并发送 - reply_text = "".join([content for msg_type, content in reply_set if msg_type == "text"]) - if reply_text: - logger.info(f"向群组 {group_id_str} 发送睡前消息: {reply_text}") - await send_api.text_to_stream(text=reply_text, stream_id=stream_id) - else: - logger.warning(f"为群组 {group_id_str} 生成的回复内容为空。") - else: - logger.error(f"为群组 {group_id_str} 生成睡前消息失败。") - - await asyncio.sleep(random.uniform(2, 5)) # 避免发送过快 - - except Exception as e: - logger.error(f"向群组 {group_id_str} 发送睡前消息失败: {e}") - - except Exception as e: - logger.error(f"发送睡前通知任务失败: {e}") - - def _save_sleep_state(self): - """将当前弹性睡眠状态保存到本地存储""" - 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() + self.sleep_manager.reset_sleep_state_after_wakeup() def _validate_schedule_with_pydantic(self, schedule_data) -> bool: """使用Pydantic验证日程数据格式和完整性""" @@ -694,22 +434,21 @@ class ScheduleManager: if not isinstance(schedule_data, list): logger.warning("日程数据不是列表格式") return False - + for item in schedule_data: if not isinstance(item, dict): logger.warning(f"日程项不是字典格式: {item}") return False - - if 'time_range' not in item or 'activity' not in item: + + if "time_range" not in item or "activity" not in item: logger.warning(f"日程项缺少必要字段 (time_range 或 activity): {item}") return False - - if not isinstance(item['time_range'], str) or not isinstance(item['activity'], str): + + if not isinstance(item["time_range"], str) or not isinstance(item["activity"], str): logger.warning(f"日程项字段类型不正确: {item}") return False - - return True + return True class DailyScheduleGenerationTask(AsyncTask): @@ -728,15 +467,17 @@ class DailyScheduleGenerationTask(AsyncTask): midnight = datetime.combine(tomorrow, time.min) sleep_seconds = (midnight - now).total_seconds() - logger.info(f"下一次日程生成任务将在 {sleep_seconds:.2f} 秒后运行 (北京时间 {midnight.strftime('%Y-%m-%d %H:%M:%S')})") - + logger.info( + f"下一次日程生成任务将在 {sleep_seconds:.2f} 秒后运行 (北京时间 {midnight.strftime('%Y-%m-%d %H:%M:%S')})" + ) + # 2. 等待直到零点 await asyncio.sleep(sleep_seconds) # 3. 执行异步日程生成 logger.info("到达每日零点,开始异步生成新的一天日程...") await self.schedule_manager.generate_and_save_schedule() - + except asyncio.CancelledError: logger.info("每日日程生成任务被取消。") break @@ -746,4 +487,4 @@ class DailyScheduleGenerationTask(AsyncTask): await asyncio.sleep(300) -schedule_manager = ScheduleManager() \ No newline at end of file +schedule_manager = ScheduleManager() diff --git a/src/schedule/sleep_manager.py b/src/schedule/sleep_manager.py new file mode 100644 index 000000000..66e6a61a5 --- /dev/null +++ b/src/schedule/sleep_manager.py @@ -0,0 +1,334 @@ +import asyncio +import random +from datetime import datetime, timedelta, date, time +from enum import Enum, auto +from typing import Optional, TYPE_CHECKING + +from src.common.logger import get_logger +from src.config.config import global_config +from src.manager.local_store_manager import local_storage +from src.plugin_system.apis import send_api, generator_api + +if TYPE_CHECKING: + from src.chat.chat_loop.wakeup_manager import WakeUpManager + +logger = get_logger("sleep_manager") + + +class SleepState(Enum): + """睡眠状态枚举""" + AWAKE = auto() # 完全清醒 + INSOMNIA = auto() # 失眠(在理论睡眠时间内保持清醒) + PREPARING_SLEEP = auto() # 准备入睡(缓冲期) + SLEEPING = auto() # 正在休眠 + WOKEN_UP = auto() # 被吵醒 + + +class SleepManager: + def __init__(self, schedule_manager): + self.schedule_manager = schedule_manager + self.last_sleep_log_time = 0 + self.sleep_log_interval = 35 # 日志记录间隔,单位秒 + + # --- 统一睡眠状态管理 --- + self._current_state: SleepState = SleepState.AWAKE + self._sleep_buffer_end_time: Optional[datetime] = None + self._total_delayed_minutes_today: int = 0 + self._last_sleep_check_date: Optional[date] = None + self._last_fully_slept_log_time: float = 0 + self._re_sleep_attempt_time: Optional[datetime] = None # 新增:重新入睡的尝试时间 + + self._load_sleep_state() + + def get_current_sleep_state(self) -> SleepState: + """获取当前的睡眠状态""" + return self._current_state + + def is_sleeping(self) -> bool: + """检查当前是否处于正式休眠状态""" + return self._current_state == SleepState.SLEEPING + + async def update_sleep_state(self, wakeup_manager: Optional["WakeUpManager"] = None): + """ + 核心状态机:根据当前情况更新睡眠状态 + """ + # --- 基础检查 --- + if not global_config.sleep_system.enable or not self.schedule_manager.today_schedule: + if self._current_state != SleepState.AWAKE: + logger.debug("睡眠系统禁用或无日程,强制设为 AWAKE") + self._current_state = SleepState.AWAKE + return + + now = datetime.now() + today = now.date() + + # --- 每日状态重置 --- + if self._last_sleep_check_date != today: + logger.info(f"新的一天 ({today}),重置睡眠状态为 AWAKE。") + self._total_delayed_minutes_today = 0 + self._current_state = SleepState.AWAKE + self._sleep_buffer_end_time = None + self._last_sleep_check_date = today + self._save_sleep_state() + + # --- 判断当前是否为理论上的睡眠时间 --- + is_in_theoretical_sleep, activity = self._is_in_theoretical_sleep_time(now.time()) + + # =================================== + # 状态机核心逻辑 + # =================================== + + # 状态:清醒 (AWAKE) + if self._current_state == SleepState.AWAKE: + if is_in_theoretical_sleep: + logger.info(f"进入理论休眠时间 '{activity}',开始进行睡眠决策...") + + # --- 合并后的失眠与弹性睡眠决策逻辑 --- + sleep_pressure = wakeup_manager.context.sleep_pressure if wakeup_manager else 999 + pressure_threshold = global_config.sleep_system.flexible_sleep_pressure_threshold + + # 决策1:因睡眠压力低而延迟入睡(原弹性睡眠) + if sleep_pressure < pressure_threshold and self._total_delayed_minutes_today < global_config.sleep_system.max_sleep_delay_minutes: + delay_minutes = 15 + self._total_delayed_minutes_today += delay_minutes + self._sleep_buffer_end_time = now + timedelta(minutes=delay_minutes) + self._current_state = SleepState.INSOMNIA + logger.info(f"睡眠压力 ({sleep_pressure:.1f}) 低于阈值 ({pressure_threshold}),进入失眠状态,延迟入睡 {delay_minutes} 分钟。") + + # 发送睡前通知 + if global_config.sleep_system.enable_pre_sleep_notification: + asyncio.create_task(self._send_pre_sleep_notification()) + + # 决策2:进入正常的入睡准备流程 + else: + buffer_seconds = random.randint(5 * 60, 10 * 60) + self._sleep_buffer_end_time = now + timedelta(seconds=buffer_seconds) + self._current_state = SleepState.PREPARING_SLEEP + logger.info(f"睡眠压力正常或已达今日最大延迟,进入准备入睡状态,将在 {buffer_seconds / 60:.1f} 分钟内入睡。") + + # 发送睡前通知 + if global_config.sleep_system.enable_pre_sleep_notification: + asyncio.create_task(self._send_pre_sleep_notification()) + + self._save_sleep_state() + + # 状态:失眠 (INSOMNIA) + elif self._current_state == SleepState.INSOMNIA: + if not is_in_theoretical_sleep: + logger.info("已离开理论休眠时间,失眠结束,恢复清醒。") + self._current_state = SleepState.AWAKE + self._save_sleep_state() + elif self._sleep_buffer_end_time and now >= self._sleep_buffer_end_time: + logger.info("失眠状态下的延迟时间已过,重新评估是否入睡...") + sleep_pressure = wakeup_manager.context.sleep_pressure if wakeup_manager else 999 + pressure_threshold = global_config.sleep_system.flexible_sleep_pressure_threshold + + if sleep_pressure >= pressure_threshold or self._total_delayed_minutes_today >= global_config.sleep_system.max_sleep_delay_minutes: + logger.info("睡眠压力足够或已达最大延迟,从失眠状态转换到准备入睡。") + buffer_seconds = random.randint(5 * 60, 10 * 60) + self._sleep_buffer_end_time = now + timedelta(seconds=buffer_seconds) + self._current_state = SleepState.PREPARING_SLEEP + else: + logger.info(f"睡眠压力({sleep_pressure:.1f})仍然较低,再延迟15分钟。") + delay_minutes = 15 + self._total_delayed_minutes_today += delay_minutes + self._sleep_buffer_end_time = now + timedelta(minutes=delay_minutes) + + self._save_sleep_state() + + # 状态:准备入睡 (PREPARING_SLEEP) + elif self._current_state == SleepState.PREPARING_SLEEP: + if not is_in_theoretical_sleep: + logger.info("准备入睡期间离开理论休眠时间,取消入睡,恢复清醒。") + self._current_state = SleepState.AWAKE + self._sleep_buffer_end_time = None + self._save_sleep_state() + elif self._sleep_buffer_end_time and now >= self._sleep_buffer_end_time: + logger.info("睡眠缓冲期结束,正式进入休眠状态。") + self._current_state = SleepState.SLEEPING + self._last_fully_slept_log_time = now.timestamp() + self._save_sleep_state() + + # 状态:休眠中 (SLEEPING) + elif self._current_state == SleepState.SLEEPING: + if not is_in_theoretical_sleep: + logger.info("理论休眠时间结束,自然醒来。") + self._current_state = SleepState.AWAKE + self._save_sleep_state() + else: + # 记录日志 + 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 + + # 状态:被吵醒 (WOKEN_UP) + elif self._current_state == SleepState.WOKEN_UP: + if not is_in_theoretical_sleep: + logger.info("理论休眠时间结束,被吵醒的状态自动结束。") + self._current_state = SleepState.AWAKE + self._re_sleep_attempt_time = None + self._save_sleep_state() + elif self._re_sleep_attempt_time and now >= self._re_sleep_attempt_time: + logger.info("被吵醒后经过一段时间,尝试重新入睡...") + + sleep_pressure = wakeup_manager.context.sleep_pressure if wakeup_manager else 999 + pressure_threshold = global_config.sleep_system.flexible_sleep_pressure_threshold + + if sleep_pressure >= pressure_threshold: + logger.info("睡眠压力足够,从被吵醒状态转换到准备入睡。") + buffer_seconds = random.randint(3 * 60, 8 * 60) # 重新入睡的缓冲期可以短一些 + self._sleep_buffer_end_time = now + timedelta(seconds=buffer_seconds) + self._current_state = SleepState.PREPARING_SLEEP + self._re_sleep_attempt_time = None + else: + delay_minutes = 15 + self._re_sleep_attempt_time = now + timedelta(minutes=delay_minutes) + logger.info(f"睡眠压力({sleep_pressure:.1f})仍然较低,暂时保持清醒,在 {delay_minutes} 分钟后再次尝试。") + + self._save_sleep_state() + + def reset_sleep_state_after_wakeup(self): + """被唤醒后,将状态切换到 WOKEN_UP""" + if self._current_state in [SleepState.PREPARING_SLEEP, SleepState.SLEEPING, SleepState.INSOMNIA]: + logger.info("被唤醒,进入 WOKEN_UP 状态!") + self._current_state = SleepState.WOKEN_UP + self._sleep_buffer_end_time = None + + # 设置一个延迟,之后再尝试重新入睡 + re_sleep_delay_minutes = getattr(global_config.sleep_system, 're_sleep_delay_minutes', 10) + self._re_sleep_attempt_time = datetime.now() + timedelta(minutes=re_sleep_delay_minutes) + logger.info(f"将在 {re_sleep_delay_minutes} 分钟后尝试重新入睡。") + + self._save_sleep_state() + + def _is_in_theoretical_sleep_time(self, now_time: time) -> tuple[bool, Optional[str]]: + """检查当前时间是否落在日程表的任何一个睡眠活动中""" + sleep_keywords = ["休眠", "睡觉", "梦乡"] + if self.schedule_manager.today_schedule: + for event in self.schedule_manager.today_schedule: + try: + activity = event.get("activity", "").strip() + time_range = event.get("time_range") + + if not activity or not time_range: + continue + + if any(keyword in activity for keyword in sleep_keywords): + start_str, end_str = time_range.split('-') + start_time = datetime.strptime(start_str.strip(), "%H:%M").time() + end_time = datetime.strptime(end_str.strip(), "%H:%M").time() + + if start_time <= end_time: # 同一天 + if start_time <= now_time < end_time: + return True, activity + else: # 跨天 + if now_time >= start_time or now_time < end_time: + return True, activity + except (ValueError, KeyError, AttributeError) as e: + logger.warning(f"解析日程事件时出错: {event}, 错误: {e}") + continue + + return False, None + + async def _send_pre_sleep_notification(self): + """异步生成并发送睡前通知""" + try: + groups = global_config.sleep_system.pre_sleep_notification_groups + prompt = global_config.sleep_system.pre_sleep_prompt + + if not groups: + logger.info("未配置睡前通知的群组,跳过发送。") + return + + if not prompt: + logger.warning("睡前通知的prompt为空,跳过发送。") + return + + # 为防止消息风暴,稍微延迟一下 + await asyncio.sleep(random.uniform(5, 15)) + + for group_id_str in groups: + try: + # 格式 "platform:group_id" + parts = group_id_str.split(":") + if len(parts) != 2: + logger.warning(f"无效的群组ID格式: {group_id_str}") + continue + + platform, group_id = parts + + # 使用与 ChatStream.get_stream_id 相同的逻辑生成 stream_id + import hashlib + key = "_".join([platform, group_id]) + stream_id = hashlib.md5(key.encode()).hexdigest() + + logger.info(f"正在为群组 {group_id_str} (Stream ID: {stream_id}) 生成睡前消息...") + + # 调用 generator_api 生成回复 + success, reply_set, _ = await generator_api.generate_reply( + chat_id=stream_id, + extra_info=prompt, + request_type="schedule.pre_sleep_notification" + ) + + if success and reply_set: + # 提取文本内容并发送 + reply_text = "".join([content for msg_type, content in reply_set if msg_type == "text"]) + if reply_text: + logger.info(f"向群组 {group_id_str} 发送睡前消息: {reply_text}") + await send_api.text_to_stream(text=reply_text, stream_id=stream_id) + else: + logger.warning(f"为群组 {group_id_str} 生成的回复内容为空。") + else: + logger.error(f"为群组 {group_id_str} 生成睡前消息失败。") + + await asyncio.sleep(random.uniform(2, 5)) # 避免发送过快 + + except Exception as e: + logger.error(f"向群组 {group_id_str} 发送睡前消息失败: {e}") + + except Exception as e: + logger.error(f"发送睡前通知任务失败: {e}") + + def _save_sleep_state(self): + """将当前睡眠状态保存到本地存储""" + try: + state = { + "current_state": self._current_state.name, + "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, + "re_sleep_attempt_time_ts": self._re_sleep_attempt_time.timestamp() if self._re_sleep_attempt_time else None, + } + local_storage["schedule_sleep_state"] = state + logger.debug(f"已保存睡眠状态: {state}") + except Exception as e: + logger.error(f"保存睡眠状态失败: {e}") + + def _load_sleep_state(self): + """从本地存储加载睡眠状态""" + try: + state = local_storage["schedule_sleep_state"] + if state and isinstance(state, dict): + state_name = state.get("current_state") + if state_name and hasattr(SleepState, state_name): + self._current_state = SleepState[state_name] + + end_time_ts = state.get("sleep_buffer_end_time_ts") + if end_time_ts: + self._sleep_buffer_end_time = datetime.fromtimestamp(end_time_ts) + + re_sleep_ts = state.get("re_sleep_attempt_time_ts") + if re_sleep_ts: + self._re_sleep_attempt_time = datetime.fromtimestamp(re_sleep_ts) + + self._total_delayed_minutes_today = state.get("total_delayed_minutes_today", 0) + + date_str = state.get("last_sleep_check_date_str") + if date_str: + self._last_sleep_check_date = datetime.fromisoformat(date_str).date() + + logger.info(f"成功从本地存储加载睡眠状态: {state}") + except Exception as e: + logger.warning(f"加载睡眠状态失败,将使用默认值: {e}") \ No newline at end of file From 1bad63fcbdd23642d686a2df6a61ae1391bfd6cd Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 29 Aug 2025 18:34:13 +0800 Subject: [PATCH 9/9] ruff ci --- plugins/hello_world_plugin/plugin.py | 1 - plugins/napcat_adapter_plugin/event_handlers.py | 2 -- plugins/napcat_adapter_plugin/plugin.py | 4 ++-- .../src/recv_handler/message_handler.py | 6 +++--- src/chat/utils/rust-video/api_server.py | 5 ++--- src/chat/utils/rust-video/config.py | 1 - src/chat/utils/rust-video/start_server.py | 1 - src/plugin_system/core/plugin_hot_reload.py | 1 - src/plugin_system/core/plugin_manager.py | 1 - .../built_in/maizone_refactored/actions/read_feed_action.py | 4 ++-- .../built_in/maizone_refactored/actions/send_feed_action.py | 4 ++-- src/plugins/built_in/plugin_management/plugin.py | 3 --- src/schedule/schedule_manager.py | 3 +-- 13 files changed, 12 insertions(+), 24 deletions(-) diff --git a/plugins/hello_world_plugin/plugin.py b/plugins/hello_world_plugin/plugin.py index 8fdb6c08d..ea5d64a8e 100644 --- a/plugins/hello_world_plugin/plugin.py +++ b/plugins/hello_world_plugin/plugin.py @@ -9,7 +9,6 @@ from src.plugin_system import ( BaseEventHandler, EventType, BaseTool, - ToolParamType, PlusCommand, CommandArgs, ChatType, diff --git a/plugins/napcat_adapter_plugin/event_handlers.py b/plugins/napcat_adapter_plugin/event_handlers.py index b5ba27a52..ef865e004 100644 --- a/plugins/napcat_adapter_plugin/event_handlers.py +++ b/plugins/napcat_adapter_plugin/event_handlers.py @@ -1,8 +1,6 @@ -from typing import List, Tuple, Optional from src.plugin_system import BaseEventHandler from src.plugin_system.base.base_event import HandlerResult -from src.plugin_system.core.event_manager import event_manager from .src.send_handler import send_handler from .event_types import * diff --git a/plugins/napcat_adapter_plugin/plugin.py b/plugins/napcat_adapter_plugin/plugin.py index 21096312d..ce94ece9e 100644 --- a/plugins/napcat_adapter_plugin/plugin.py +++ b/plugins/napcat_adapter_plugin/plugin.py @@ -5,7 +5,7 @@ import inspect import websockets as Server from . import event_types,CONSTS,event_handlers -from typing import List, Tuple +from typing import List from src.plugin_system import BasePlugin, BaseEventHandler, register_plugin, EventType, ConfigField from src.plugin_system.base.base_event import HandlerResult @@ -27,7 +27,7 @@ from .src.send_handler import send_handler from .src.config import global_config from .src.config.features_config import features_manager from .src.config.migrate_features import auto_migrate_features -from .src.mmc_com_layer import mmc_start_com, mmc_stop_com, router +from .src.mmc_com_layer import mmc_start_com, router from .src.response_pool import put_response, check_timeout_response from .src.websocket_manager import websocket_manager diff --git a/plugins/napcat_adapter_plugin/src/recv_handler/message_handler.py b/plugins/napcat_adapter_plugin/src/recv_handler/message_handler.py index 0e4ba29fe..04631131e 100644 --- a/plugins/napcat_adapter_plugin/src/recv_handler/message_handler.py +++ b/plugins/napcat_adapter_plugin/src/recv_handler/message_handler.py @@ -351,15 +351,15 @@ class MessageHandler: else: logger.warning("reply处理失败") case RealMessageType.image: - logger.debug(f"开始处理图片消息段") + logger.debug("开始处理图片消息段") ret_seg = await self.handle_image_message(sub_message) if ret_seg: await event_manager.trigger_event(NapcatEvent.ON_RECEIVED.IMAGE,plugin_name=PLUGIN_NAME,message_seg=ret_seg) seg_message.append(ret_seg) - logger.debug(f"图片处理成功,添加到消息段") + logger.debug("图片处理成功,添加到消息段") else: logger.warning("image处理失败") - logger.debug(f"图片消息段处理完成") + logger.debug("图片消息段处理完成") case RealMessageType.record: ret_seg = await self.handle_record_message(sub_message) if ret_seg: diff --git a/src/chat/utils/rust-video/api_server.py b/src/chat/utils/rust-video/api_server.py index aeb3fa248..5465c4a7b 100644 --- a/src/chat/utils/rust-video/api_server.py +++ b/src/chat/utils/rust-video/api_server.py @@ -24,13 +24,12 @@ import time import logging from datetime import datetime from pathlib import Path -from typing import Optional, List, Dict, Any +from typing import Optional, Dict, Any import uvicorn -from fastapi import FastAPI, File, UploadFile, Form, HTTPException, BackgroundTasks +from fastapi import FastAPI, File, UploadFile, Form, HTTPException from fastapi.responses import FileResponse, JSONResponse from fastapi.middleware.cors import CORSMiddleware -from pydantic import BaseModel, Field # 导入配置管理 from config import config diff --git a/src/chat/utils/rust-video/config.py b/src/chat/utils/rust-video/config.py index c85b8f9ea..32c6f3fcb 100644 --- a/src/chat/utils/rust-video/config.py +++ b/src/chat/utils/rust-video/config.py @@ -3,7 +3,6 @@ 处理 config.toml 文件的读取和管理 """ -import os from pathlib import Path from typing import Dict, Any diff --git a/src/chat/utils/rust-video/start_server.py b/src/chat/utils/rust-video/start_server.py index b1547d441..3e03a89f5 100644 --- a/src/chat/utils/rust-video/start_server.py +++ b/src/chat/utils/rust-video/start_server.py @@ -5,7 +5,6 @@ 支持开发模式和生产模式启动 """ -import os import sys import subprocess import argparse diff --git a/src/plugin_system/core/plugin_hot_reload.py b/src/plugin_system/core/plugin_hot_reload.py index c35aa71ed..be7d79671 100644 --- a/src/plugin_system/core/plugin_hot_reload.py +++ b/src/plugin_system/core/plugin_hot_reload.py @@ -348,7 +348,6 @@ class PluginHotReloadManager: def _force_clear_plugin_modules(self, plugin_name: str): """强制清理插件相关的模块缓存""" - import sys # 找到所有相关的模块名 modules_to_remove = [] diff --git a/src/plugin_system/core/plugin_manager.py b/src/plugin_system/core/plugin_manager.py index 4b4de3684..b16c80f61 100644 --- a/src/plugin_system/core/plugin_manager.py +++ b/src/plugin_system/core/plugin_manager.py @@ -1,7 +1,6 @@ import asyncio import os import traceback -import sys import importlib from typing import Dict, List, Optional, Tuple, Type, Any diff --git a/src/plugins/built_in/maizone_refactored/actions/read_feed_action.py b/src/plugins/built_in/maizone_refactored/actions/read_feed_action.py index d9089b52a..223f02d95 100644 --- a/src/plugins/built_in/maizone_refactored/actions/read_feed_action.py +++ b/src/plugins/built_in/maizone_refactored/actions/read_feed_action.py @@ -6,9 +6,9 @@ from typing import Tuple from src.common.logger import get_logger from src.plugin_system import BaseAction, ActionActivationType, ChatMode -from src.plugin_system.apis import person_api, generator_api +from src.plugin_system.apis import generator_api from src.plugin_system.apis.permission_api import permission_api -from ..services.manager import get_qzone_service, get_config_getter +from ..services.manager import get_qzone_service logger = get_logger("MaiZone.ReadFeedAction") diff --git a/src/plugins/built_in/maizone_refactored/actions/send_feed_action.py b/src/plugins/built_in/maizone_refactored/actions/send_feed_action.py index 08fa372e1..38553c243 100644 --- a/src/plugins/built_in/maizone_refactored/actions/send_feed_action.py +++ b/src/plugins/built_in/maizone_refactored/actions/send_feed_action.py @@ -6,9 +6,9 @@ from typing import Tuple from src.common.logger import get_logger from src.plugin_system import BaseAction, ActionActivationType, ChatMode -from src.plugin_system.apis import person_api, generator_api +from src.plugin_system.apis import generator_api from src.plugin_system.apis.permission_api import permission_api -from ..services.manager import get_qzone_service, get_config_getter +from ..services.manager import get_qzone_service logger = get_logger("MaiZone.SendFeedAction") diff --git a/src/plugins/built_in/plugin_management/plugin.py b/src/plugins/built_in/plugin_management/plugin.py index d0ffd484d..46933571a 100644 --- a/src/plugins/built_in/plugin_management/plugin.py +++ b/src/plugins/built_in/plugin_management/plugin.py @@ -3,15 +3,12 @@ import asyncio from typing import List, Tuple, Type from src.plugin_system import ( BasePlugin, - BaseCommand, - CommandInfo, ConfigField, register_plugin, plugin_manage_api, component_manage_api, ComponentInfo, ComponentType, - send_api, ) from src.plugin_system.base.plus_command import PlusCommand from src.plugin_system.base.command_args import CommandArgs diff --git a/src/schedule/schedule_manager.py b/src/schedule/schedule_manager.py index e613d1c4b..2834d159a 100644 --- a/src/schedule/schedule_manager.py +++ b/src/schedule/schedule_manager.py @@ -1,7 +1,6 @@ import orjson import asyncio -import random -from datetime import datetime, time, timedelta, date +from datetime import datetime, time, timedelta from typing import Optional, List, Dict, Any, TYPE_CHECKING from lunar_python import Lunar from pydantic import BaseModel, ValidationError, validator