This commit is contained in:
雅诺狐
2025-08-29 19:15:04 +08:00
22 changed files with 1978 additions and 550 deletions

View File

@@ -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"
]
}

View File

@@ -0,0 +1,129 @@
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,
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

View File

@@ -1,6 +1,550 @@
from typing import List, Tuple
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 GetOnlineClientsHandler(BaseEventHandler):
handler_name: str = "napcat_get_online_clients_handler"
handler_description: str = "获取当前账号在线客户端列表"
weight: int = 100
intercept_message: bool = False
init_subscribe = [NapcatEvent.ACCOUNT.GET_ONLINE_CLIENTS]
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_online_clients", params=payload)
if response.get("status", "") == "ok":
return HandlerResult(True, True, response)
else:
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"})

View File

@@ -1,69 +1,663 @@
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 ('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"
'''获取当前账号在线客户端列表
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"
'''设置头像
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):
"""
该分类是对信息相关的操作只能由外部触发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"
'''群打卡'''

View File

@@ -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 typing import List
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
@@ -26,12 +27,19 @@ 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
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,103 @@ 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)
'''
# 测试获取登录信息
logger.info("测试获取登录信息...")
res = await event_manager.trigger_event(
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):
plugin_name = CONSTS.PLUGIN_NAME
@@ -121,11 +226,23 @@ 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])
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

View File

@@ -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:

View File

@@ -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()

View File

@@ -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("执行动作,睡眠压力累积")

View File

@@ -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.wakeup_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:
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,18 +223,18 @@ 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()
# 根据聊天模式处理新消息
if self.context.loop_mode == ChatMode.FOCUS:
@@ -273,12 +257,12 @@ 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
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()

View File

@@ -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")

View File

@@ -31,22 +31,15 @@ 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
# 失眠系统参数
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
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._load_wakeup_state()
@@ -144,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
@@ -220,39 +215,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
}

View File

@@ -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互通组配置")

View File

@@ -529,16 +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会根据日程表睡觉和苏醒")
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):
@@ -617,10 +607,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="群聊艾特增加的唤醒度")
@@ -640,6 +630,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):
"""月度计划系统配置类"""

View File

@@ -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
@@ -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:
@@ -70,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

View File

@@ -348,7 +348,6 @@ class PluginHotReloadManager:
def _force_clear_plugin_modules(self, plugin_name: str):
"""强制清理插件相关的模块缓存"""
import sys
# 找到所有相关的模块名
modules_to_remove = []

View File

@@ -1,7 +1,6 @@
import asyncio
import os
import traceback
import sys
import importlib
from typing import Dict, List, Optional, Tuple, Type, Any

View File

@@ -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")

View File

@@ -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")

View File

@@ -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

View File

@@ -1,23 +1,24 @@
import orjson
import asyncio
import random
from datetime import datetime, time, timedelta
from typing import Optional, List, Dict, Any
from typing import Optional, List, Dict, Any, TYPE_CHECKING
from lunar_python import Lunar
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
logger = get_logger("schedule_manager")
@@ -31,81 +32,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):
@@ -117,30 +122,19 @@ class ScheduleData(BaseModel):
covered[i] = True
for i in range(0, end_min):
covered[i] = True
# 检查是否所有分钟都被覆盖
return all(covered)
class ScheduleManager:
def __init__(self):
self.today_schedule: Optional[List[Dict[str, Any]]] = None
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._is_preparing_sleep: bool = False
self._sleep_buffer_end_time: Optional[datetime] = None
self._total_delayed_minutes_today: int = 0
self._last_sleep_check_date: Optional[datetime.date] = None
self._last_fully_slept_log_time: float = 0
self._is_in_voluntary_delay: bool = False # 新增:标记是否处于主动延迟睡眠状态
self._is_woken_up: bool = False # 新增:标记是否被吵醒
self._load_sleep_state()
self.sleep_manager = SleepManager(self)
async def start_daily_schedule_generation(self):
"""启动每日零点自动生成新日程的任务"""
@@ -165,10 +159,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
@@ -197,15 +191,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")
@@ -217,7 +211,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)
@@ -228,33 +222,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"""
**我这个月的一些小目标/计划 (请在今天的日程中适当体现)**:
@@ -294,33 +283,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):
# 验证通过,保存到数据库
@@ -329,46 +318,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("日程生成任务结束")
@@ -386,12 +378,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()
@@ -406,249 +398,21 @@ class ScheduleManager:
continue
return None
def is_sleeping(self, wakeup_manager: Optional["WakeUpManager"] = None) -> bool:
"""
通过关键词匹配、唤醒度、睡眠压力等综合判断是否处于休眠时间。
新增弹性睡眠机制,允许在压力低时延迟入睡,并在入睡前发送通知。
"""
# --- 基础检查 ---
if not global_config.schedule.enable_is_sleep:
return False
if not self.today_schedule:
return False
def get_current_sleep_state(self) -> SleepState:
"""获取当前的睡眠状态"""
return self.sleep_manager.get_current_sleep_state()
now = datetime.now()
today = now.date()
def is_sleeping(self) -> bool:
"""检查当前是否处于正式休眠状态"""
return self.sleep_manager.is_sleeping()
# --- 每日状态重置 ---
if self._last_sleep_check_date != today:
logger.info(f"新的一天 ({today}),重置弹性睡眠状态。")
self._total_delayed_minutes_today = 0
self._is_preparing_sleep = False
self._sleep_buffer_end_time = None
self._last_sleep_check_date = today
self._is_in_voluntary_delay = False
self._save_sleep_state()
# --- 检查是否在“准备入睡”的缓冲期 ---
if self._is_preparing_sleep and self._sleep_buffer_end_time:
if now >= self._sleep_buffer_end_time:
current_timestamp = now.timestamp()
if current_timestamp - self._last_fully_slept_log_time > 45:
logger.info("睡眠缓冲期结束,正式进入休眠状态。")
self._last_fully_slept_log_time = current_timestamp
return True
else:
remaining_seconds = (self._sleep_buffer_end_time - now).total_seconds()
logger.debug(f"处于入睡缓冲期,剩余 {remaining_seconds:.1f} 秒。")
return False
# --- 判断当前是否为理论上的睡眠时间 ---
is_in_theoretical_sleep, activity = self._is_in_theoretical_sleep_time(now.time())
if not is_in_theoretical_sleep:
# 如果不在理论睡眠时间,确保重置准备状态
if self._is_preparing_sleep:
logger.info("已离开理论休眠时间,取消“准备入睡”状态。")
self._is_preparing_sleep = False
self._sleep_buffer_end_time = None
self._is_in_voluntary_delay = False
self._is_woken_up = False # 离开睡眠时间,重置唤醒状态
self._save_sleep_state()
return False
# --- 处理唤醒状态 ---
if self._is_woken_up:
current_timestamp = now.timestamp()
if current_timestamp - self.last_sleep_log_time > self.sleep_log_interval:
logger.info(f"在休眠活动 '{activity}' 期间,但已被唤醒,保持清醒状态。")
self.last_sleep_log_time = current_timestamp
return False
# --- 核心:弹性睡眠逻辑 ---
if global_config.schedule.enable_flexible_sleep and not self._is_preparing_sleep:
# 首次进入理论睡眠时间,触发弹性判断
logger.info(f"进入理论休眠时间 '{activity}',开始弹性睡眠判断...")
# 1. 获取睡眠压力
sleep_pressure = wakeup_manager.context.sleep_pressure if wakeup_manager else 999
pressure_threshold = global_config.schedule.flexible_sleep_pressure_threshold
# 2. 判断是否延迟
if sleep_pressure < pressure_threshold and self._total_delayed_minutes_today < global_config.schedule.max_sleep_delay_minutes:
delay_minutes = 15 # 每次延迟15分钟
self._total_delayed_minutes_today += delay_minutes
self._sleep_buffer_end_time = now + timedelta(minutes=delay_minutes)
self._is_in_voluntary_delay = True # 标记进入主动延迟
logger.info(f"睡眠压力 ({sleep_pressure:.1f}) 低于阈值 ({pressure_threshold}),延迟入睡 {delay_minutes} 分钟。今日已累计延迟 {self._total_delayed_minutes_today} 分钟。")
else:
# 3. 计算5-10分钟的入睡缓冲
self._is_in_voluntary_delay = False # 非主动延迟
buffer_seconds = random.randint(5 * 60, 10 * 60)
self._sleep_buffer_end_time = now + timedelta(seconds=buffer_seconds)
logger.info(f"睡眠压力正常或已达今日最大延迟,将在 {buffer_seconds / 60:.1f} 分钟内入睡。")
# 4. 发送睡前通知
if global_config.schedule.enable_pre_sleep_notification:
asyncio.create_task(self._send_pre_sleep_notification())
self._is_preparing_sleep = True
self._save_sleep_state()
return False # 进入准备阶段,但尚未正式入睡
# --- 经典模式或已在弹性睡眠流程中 ---
current_timestamp = now.timestamp()
if current_timestamp - self.last_sleep_log_time > self.sleep_log_interval:
logger.info(f"当前处于休眠活动 '{activity}' 中 (经典模式)。")
self.last_sleep_log_time = current_timestamp
return True
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):
"""被唤醒后重置睡眠状态"""
if self._is_preparing_sleep or self.is_sleeping():
logger.info("被唤醒,重置所有睡眠准备状态,恢复清醒!")
self._is_preparing_sleep = False
self._sleep_buffer_end_time = None
self._is_in_voluntary_delay = False
self._is_woken_up = True # 标记为已被唤醒
self._save_sleep_state()
def _is_in_theoretical_sleep_time(self, now_time: time) -> (bool, Optional[str]):
"""检查当前时间是否落在日程表的任何一个睡眠活动中"""
sleep_keywords = ["休眠", "睡觉", "梦乡"]
for event in self.today_schedule:
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.schedule.pre_sleep_notification_groups
prompt = global_config.schedule.pre_sleep_prompt
if not groups:
logger.info("未配置睡前通知的群组,跳过发送。")
return
if not prompt:
logger.warning("睡前通知的prompt为空跳过发送。")
return
# 为防止消息风暴,稍微延迟一下
await asyncio.sleep(random.uniform(5, 15))
for group_id_str in groups:
try:
# 格式 "platform:group_id"
parts = group_id_str.split(":")
if len(parts) != 2:
logger.warning(f"无效的群组ID格式: {group_id_str}")
continue
platform, group_id = parts
# 使用与 ChatStream.get_stream_id 相同的逻辑生成 stream_id
import hashlib
key = "_".join([platform, group_id])
stream_id = hashlib.md5(key.encode()).hexdigest()
logger.info(f"正在为群组 {group_id_str} (Stream ID: {stream_id}) 生成睡前消息...")
# 调用 generator_api 生成回复
success, reply_set, _ = await generator_api.generate_reply(
chat_id=stream_id,
extra_info=prompt,
request_type="schedule.pre_sleep_notification"
)
if success and reply_set:
# 提取文本内容并发送
reply_text = "".join([content for msg_type, content in reply_set if msg_type == "text"])
if reply_text:
logger.info(f"向群组 {group_id_str} 发送睡前消息: {reply_text}")
await send_api.text_to_stream(text=reply_text, stream_id=stream_id)
else:
logger.warning(f"为群组 {group_id_str} 生成的回复内容为空。")
else:
logger.error(f"为群组 {group_id_str} 生成睡前消息失败。")
await asyncio.sleep(random.uniform(2, 5)) # 避免发送过快
except Exception as e:
logger.error(f"向群组 {group_id_str} 发送睡前消息失败: {e}")
except Exception as e:
logger.error(f"发送睡前通知任务失败: {e}")
def _save_sleep_state(self):
"""将当前弹性睡眠状态保存到本地存储"""
try:
state = {
"is_preparing_sleep": self._is_preparing_sleep,
"sleep_buffer_end_time_ts": self._sleep_buffer_end_time.timestamp() if self._sleep_buffer_end_time else None,
"total_delayed_minutes_today": self._total_delayed_minutes_today,
"last_sleep_check_date_str": self._last_sleep_check_date.isoformat() if self._last_sleep_check_date else None,
"is_in_voluntary_delay": self._is_in_voluntary_delay,
"is_woken_up": self._is_woken_up,
}
local_storage["schedule_sleep_state"] = state
logger.debug(f"已保存睡眠状态: {state}")
except Exception as e:
logger.error(f"保存睡眠状态失败: {e}")
def _load_sleep_state(self):
"""从本地存储加载弹性睡眠状态"""
try:
state = local_storage["schedule_sleep_state"]
if state and isinstance(state, dict):
self._is_preparing_sleep = state.get("is_preparing_sleep", False)
end_time_ts = state.get("sleep_buffer_end_time_ts")
if end_time_ts:
self._sleep_buffer_end_time = datetime.fromtimestamp(end_time_ts)
self._total_delayed_minutes_today = state.get("total_delayed_minutes_today", 0)
self._is_in_voluntary_delay = state.get("is_in_voluntary_delay", False)
self._is_woken_up = state.get("is_woken_up", False)
date_str = state.get("last_sleep_check_date_str")
if date_str:
self._last_sleep_check_date = datetime.fromisoformat(date_str).date()
logger.info(f"成功从本地存储加载睡眠状态: {state}")
except Exception as e:
logger.warning(f"加载睡眠状态失败,将使用默认值: {e}")
def reset_wakeup_state(self):
"""重置被唤醒的状态,允许重新尝试入睡"""
if self._is_woken_up:
logger.info("重置唤醒状态,将重新尝试入睡。")
self._is_woken_up = False
self._is_preparing_sleep = False # 允许重新进入弹性睡眠判断
self._sleep_buffer_end_time = None
self._save_sleep_state()
"""被唤醒后,将状态切换到 WOKEN_UP"""
self.sleep_manager.reset_sleep_state_after_wakeup()
def _validate_schedule_with_pydantic(self, schedule_data) -> bool:
"""使用Pydantic验证日程数据格式和完整性"""
@@ -669,22 +433,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):
@@ -703,15 +466,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
@@ -721,4 +486,4 @@ class DailyScheduleGenerationTask(AsyncTask):
await asyncio.sleep(300)
schedule_manager = ScheduleManager()
schedule_manager = ScheduleManager()

View File

@@ -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}")

View File

@@ -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知识库
@@ -347,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"
@@ -376,23 +364,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"(逐帧分析,非常慢)、"batch_frames"(批量分析,推荐)或 "auto"(自动选择)
@@ -463,8 +434,8 @@ guidelines = """
请确保计划既有挑战性又不会过于繁重,保持生活的平衡和乐趣。
"""
[wakeup_system]
enable = false #"是否启用唤醒度系统"
[sleep_system]
enable = false #"是否启用睡眠系统"
wakeup_threshold = 15.0 #唤醒阈值,达到此值时会被唤醒"
private_message_increment = 3.0 #"私聊消息增加的唤醒度"
group_mention_increment = 2.0 #"群聊艾特增加的唤醒度"
@@ -489,6 +460,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