This commit is contained in:
minecraft1024a
2025-10-25 09:30:23 +08:00
49 changed files with 991 additions and 540 deletions

View File

@@ -7,6 +7,7 @@ from src.plugin_system import (
BaseEventHandler, BaseEventHandler,
BasePlugin, BasePlugin,
BasePrompt, BasePrompt,
ToolParamType,
BaseTool, BaseTool,
ChatType, ChatType,
CommandArgs, CommandArgs,
@@ -14,10 +15,8 @@ from src.plugin_system import (
ConfigField, ConfigField,
EventType, EventType,
PlusCommand, PlusCommand,
ToolParamType,
register_plugin, register_plugin,
) )
from src.plugin_system.base.component_types import InjectionRule,InjectionType
from src.plugin_system.base.base_event import HandlerResult from src.plugin_system.base.base_event import HandlerResult
@@ -186,7 +185,7 @@ class WeatherPrompt(BasePrompt):
prompt_name = "weather_info_prompt" prompt_name = "weather_info_prompt"
prompt_description = "向Planner注入当前天气信息以丰富对话上下文。" prompt_description = "向Planner注入当前天气信息以丰富对话上下文。"
injection_rules = [InjectionRule(target_prompt="planner_prompt", injection_type=InjectionType.REPLACE, target_content="## 可用动作列表")] injection_point = "planner_prompt"
async def execute(self) -> str: async def execute(self) -> str:
# 在实际应用中这里可以调用天气API # 在实际应用中这里可以调用天气API

View File

@@ -49,11 +49,11 @@ __plugin_meta__ = PluginMetadata(
name="{plugin_name}", name="{plugin_name}",
description="{description}", description="{description}",
usage="暂无说明", usage="暂无说明",
type={plugin_type!r}, type={repr(plugin_type)},
version="{version}", version="{version}",
author="{author}", author="{author}",
license={license_type!r}, license={repr(license_type)},
repository_url={repository_url!r}, repository_url={repr(repository_url)},
keywords={keywords}, keywords={keywords},
categories={categories}, categories={categories},
) )

View File

@@ -3,9 +3,9 @@ import datetime
import os import os
import shutil import shutil
import sys import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path from pathlib import Path
from threading import Lock from threading import Lock
from concurrent.futures import ThreadPoolExecutor, as_completed
import orjson import orjson
from json_repair import repair_json from json_repair import repair_json

View File

@@ -1,6 +1,7 @@
import asyncio import asyncio
import math import math
import os import os
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass from dataclasses import dataclass
# import tqdm # import tqdm

View File

@@ -3,12 +3,12 @@
用于统一管理所有notice消息将notice与正常消息分离 用于统一管理所有notice消息将notice与正常消息分离
""" """
import threading
import time import time
import threading
from collections import defaultdict, deque from collections import defaultdict, deque
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Dict, List, Optional, Any
from enum import Enum from enum import Enum
from typing import Any
from src.common.data_models.database_data_model import DatabaseMessages from src.common.data_models.database_data_model import DatabaseMessages
from src.common.logger import get_logger from src.common.logger import get_logger
@@ -27,7 +27,7 @@ class NoticeMessage:
"""Notice消息数据结构""" """Notice消息数据结构"""
message: DatabaseMessages message: DatabaseMessages
scope: NoticeScope scope: NoticeScope
target_stream_id: str | None = None # 如果是STREAM类型指定目标流ID target_stream_id: Optional[str] = None # 如果是STREAM类型指定目标流ID
timestamp: float = field(default_factory=time.time) timestamp: float = field(default_factory=time.time)
ttl: int = 3600 # 默认1小时过期 ttl: int = 3600 # 默认1小时过期
@@ -56,11 +56,11 @@ class GlobalNoticeManager:
return cls._instance return cls._instance
def __init__(self): def __init__(self):
if hasattr(self, "_initialized"): if hasattr(self, '_initialized'):
return return
self._initialized = True self._initialized = True
self._notices: dict[str, deque[NoticeMessage]] = defaultdict(deque) self._notices: Dict[str, deque[NoticeMessage]] = defaultdict(deque)
self._max_notices_per_type = 100 # 每种类型最大存储数量 self._max_notices_per_type = 100 # 每种类型最大存储数量
self._cleanup_interval = 300 # 5分钟清理一次过期消息 self._cleanup_interval = 300 # 5分钟清理一次过期消息
self._last_cleanup_time = time.time() self._last_cleanup_time = time.time()
@@ -80,8 +80,8 @@ class GlobalNoticeManager:
self, self,
message: DatabaseMessages, message: DatabaseMessages,
scope: NoticeScope = NoticeScope.STREAM, scope: NoticeScope = NoticeScope.STREAM,
target_stream_id: str | None = None, target_stream_id: Optional[str] = None,
ttl: int | None = None ttl: Optional[int] = None
) -> bool: ) -> bool:
"""添加notice消息 """添加notice消息
@@ -142,7 +142,7 @@ class GlobalNoticeManager:
logger.error(f"添加notice消息失败: {e}") logger.error(f"添加notice消息失败: {e}")
return False return False
def get_accessible_notices(self, stream_id: str, limit: int = 20) -> list[NoticeMessage]: def get_accessible_notices(self, stream_id: str, limit: int = 20) -> List[NoticeMessage]:
"""获取指定聊天流可访问的notice消息 """获取指定聊天流可访问的notice消息
Args: Args:
@@ -231,7 +231,7 @@ class GlobalNoticeManager:
logger.error(f"获取notice文本失败: {e}", exc_info=True) logger.error(f"获取notice文本失败: {e}", exc_info=True)
return "" return ""
def clear_notices(self, stream_id: str | None = None, notice_type: str | None = None) -> int: def clear_notices(self, stream_id: Optional[str] = None, notice_type: Optional[str] = None) -> int:
"""清理notice消息 """清理notice消息
Args: Args:
@@ -289,14 +289,14 @@ class GlobalNoticeManager:
logger.error(f"清理notice消息失败: {e}") logger.error(f"清理notice消息失败: {e}")
return 0 return 0
def get_stats(self) -> dict[str, Any]: def get_stats(self) -> Dict[str, Any]:
"""获取统计信息""" """获取统计信息"""
# 更新实时统计 # 更新实时统计
total_active_notices = sum(len(notices) for notices in self._notices.values()) total_active_notices = sum(len(notices) for notices in self._notices.values())
self.stats["total_notices"] = total_active_notices self.stats["total_notices"] = total_active_notices
self.stats["active_keys"] = len(self._notices) self.stats["active_keys"] = len(self._notices)
self.stats["last_cleanup_time"] = int(self._last_cleanup_time) self.stats["last_cleanup_time"] = int(self._last_cleanup_time)
# 添加详细的存储键信息 # 添加详细的存储键信息
storage_keys_info = {} storage_keys_info = {}
for key, notices in self._notices.items(): for key, notices in self._notices.items():
@@ -313,11 +313,11 @@ class GlobalNoticeManager:
"""检查消息是否为notice类型""" """检查消息是否为notice类型"""
try: try:
# 首先检查消息的is_notify字段 # 首先检查消息的is_notify字段
if hasattr(message, "is_notify") and message.is_notify: if hasattr(message, 'is_notify') and message.is_notify:
return True return True
# 检查消息的附加配置 # 检查消息的附加配置
if hasattr(message, "additional_config") and message.additional_config: if hasattr(message, 'additional_config') and message.additional_config:
if isinstance(message.additional_config, dict): if isinstance(message.additional_config, dict):
return message.additional_config.get("is_notice", False) return message.additional_config.get("is_notice", False)
elif isinstance(message.additional_config, str): elif isinstance(message.additional_config, str):
@@ -333,7 +333,7 @@ class GlobalNoticeManager:
logger.debug(f"检查notice类型失败: {e}") logger.debug(f"检查notice类型失败: {e}")
return False return False
def _get_storage_key(self, scope: NoticeScope, target_stream_id: str | None, message: DatabaseMessages) -> str: def _get_storage_key(self, scope: NoticeScope, target_stream_id: Optional[str], message: DatabaseMessages) -> str:
"""生成存储键""" """生成存储键"""
if scope == NoticeScope.PUBLIC: if scope == NoticeScope.PUBLIC:
return "public" return "public"
@@ -341,10 +341,10 @@ class GlobalNoticeManager:
notice_type = self._get_notice_type(message) or "default" notice_type = self._get_notice_type(message) or "default"
return f"stream_{target_stream_id}_{notice_type}" return f"stream_{target_stream_id}_{notice_type}"
def _get_notice_type(self, message: DatabaseMessages) -> str | None: def _get_notice_type(self, message: DatabaseMessages) -> Optional[str]:
"""获取notice类型""" """获取notice类型"""
try: try:
if hasattr(message, "additional_config") and message.additional_config: if hasattr(message, 'additional_config') and message.additional_config:
if isinstance(message.additional_config, dict): if isinstance(message.additional_config, dict):
return message.additional_config.get("notice_type") return message.additional_config.get("notice_type")
elif isinstance(message.additional_config, str): elif isinstance(message.additional_config, str):
@@ -397,4 +397,4 @@ class GlobalNoticeManager:
# 创建全局单例实例 # 创建全局单例实例
global_notice_manager = GlobalNoticeManager() global_notice_manager = GlobalNoticeManager()

View File

@@ -7,7 +7,7 @@ import asyncio
import random import random
import time import time
from collections import defaultdict, deque from collections import defaultdict, deque
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any, Dict, Optional
from src.chat.chatter_manager import ChatterManager from src.chat.chatter_manager import ChatterManager
from src.chat.message_receive.chat_stream import ChatStream from src.chat.message_receive.chat_stream import ChatStream
@@ -19,7 +19,9 @@ from src.config.config import global_config
from src.plugin_system.apis.chat_api import get_chat_manager from src.plugin_system.apis.chat_api import get_chat_manager
from .distribution_manager import stream_loop_manager from .distribution_manager import stream_loop_manager
from .global_notice_manager import NoticeScope, global_notice_manager from .sleep_system.state_manager import SleepState, sleep_state_manager
from .global_notice_manager import global_notice_manager, NoticeScope
if TYPE_CHECKING: if TYPE_CHECKING:
pass pass
@@ -147,6 +149,13 @@ class MessageManager:
async def add_message(self, stream_id: str, message: DatabaseMessages): async def add_message(self, stream_id: str, message: DatabaseMessages):
"""添加消息到指定聊天流""" """添加消息到指定聊天流"""
# 在消息处理的最前端检查睡眠状态
current_sleep_state = sleep_state_manager.get_current_state()
if current_sleep_state == SleepState.SLEEPING:
logger.info(f"处于 {current_sleep_state.name} 状态,消息被拦截。")
return # 直接返回,不处理消息
# TODO: 在这里为 WOKEN_UP_ANGRY 等未来状态添加特殊处理逻辑
try: try:
# 检查是否为notice消息 # 检查是否为notice消息
@@ -154,7 +163,7 @@ class MessageManager:
# Notice消息处理 - 添加到全局管理器 # Notice消息处理 - 添加到全局管理器
logger.info(f"📢 检测到notice消息: message_id={message.message_id}, is_notify={message.is_notify}, notice_type={getattr(message, 'notice_type', None)}") logger.info(f"📢 检测到notice消息: message_id={message.message_id}, is_notify={message.is_notify}, notice_type={getattr(message, 'notice_type', None)}")
await self._handle_notice_message(stream_id, message) await self._handle_notice_message(stream_id, message)
# 根据配置决定是否继续处理(触发聊天流程) # 根据配置决定是否继续处理(触发聊天流程)
if not global_config.notice.enable_notice_trigger_chat: if not global_config.notice.enable_notice_trigger_chat:
logger.info(f"根据配置,流 {stream_id} 的Notice消息将被忽略不触发聊天流程。") logger.info(f"根据配置,流 {stream_id} 的Notice消息将被忽略不触发聊天流程。")
@@ -657,11 +666,11 @@ class MessageManager:
"""检查消息是否为notice类型""" """检查消息是否为notice类型"""
try: try:
# 首先检查消息的is_notify字段 # 首先检查消息的is_notify字段
if hasattr(message, "is_notify") and message.is_notify: if hasattr(message, 'is_notify') and message.is_notify:
return True return True
# 检查消息的附加配置 # 检查消息的附加配置
if hasattr(message, "additional_config") and message.additional_config: if hasattr(message, 'additional_config') and message.additional_config:
if isinstance(message.additional_config, dict): if isinstance(message.additional_config, dict):
return message.additional_config.get("is_notice", False) return message.additional_config.get("is_notice", False)
elif isinstance(message.additional_config, str): elif isinstance(message.additional_config, str):
@@ -707,7 +716,7 @@ class MessageManager:
""" """
try: try:
# 检查附加配置中的公共notice标志 # 检查附加配置中的公共notice标志
if hasattr(message, "additional_config") and message.additional_config: if hasattr(message, 'additional_config') and message.additional_config:
if isinstance(message.additional_config, dict): if isinstance(message.additional_config, dict):
is_public = message.additional_config.get("is_public_notice", False) is_public = message.additional_config.get("is_public_notice", False)
elif isinstance(message.additional_config, str): elif isinstance(message.additional_config, str):
@@ -728,10 +737,10 @@ class MessageManager:
logger.debug(f"确定notice作用域失败: {e}") logger.debug(f"确定notice作用域失败: {e}")
return NoticeScope.STREAM return NoticeScope.STREAM
def _get_notice_type(self, message: DatabaseMessages) -> str | None: def _get_notice_type(self, message: DatabaseMessages) -> Optional[str]:
"""获取notice类型""" """获取notice类型"""
try: try:
if hasattr(message, "additional_config") and message.additional_config: if hasattr(message, 'additional_config') and message.additional_config:
if isinstance(message.additional_config, dict): if isinstance(message.additional_config, dict):
return message.additional_config.get("notice_type") return message.additional_config.get("notice_type")
elif isinstance(message.additional_config, str): elif isinstance(message.additional_config, str):
@@ -772,7 +781,7 @@ class MessageManager:
logger.error(f"获取notice文本失败: {e}") logger.error(f"获取notice文本失败: {e}")
return "" return ""
def clear_notices(self, stream_id: str | None = None, notice_type: str | None = None) -> int: def clear_notices(self, stream_id: Optional[str] = None, notice_type: Optional[str] = None) -> int:
"""清理notice消息""" """清理notice消息"""
try: try:
return self.notice_manager.clear_notices(stream_id, notice_type) return self.notice_manager.clear_notices(stream_id, notice_type)
@@ -780,7 +789,7 @@ class MessageManager:
logger.error(f"清理notice失败: {e}") logger.error(f"清理notice失败: {e}")
return 0 return 0
def get_notice_stats(self) -> dict[str, Any]: def get_notice_stats(self) -> Dict[str, Any]:
"""获取notice管理器统计信息""" """获取notice管理器统计信息"""
try: try:
return self.notice_manager.get_stats() return self.notice_manager.get_stats()

View File

@@ -0,0 +1,195 @@
import random
from datetime import datetime, timedelta
from src.common.logger import get_logger
from src.config.config import global_config
from src.schedule.schedule_manager import schedule_manager
from .state_manager import SleepState, sleep_state_manager
logger = get_logger("sleep_logic")
class SleepLogic:
"""
核心睡眠逻辑,睡眠系统的“大脑”
负责根据当前的配置、时间、日程表以及状态,判断是否需要切换睡眠状态。
它本身是无状态的,所有的状态都读取和写入 SleepStateManager。
"""
def check_and_update_sleep_state(self):
"""
检查并更新当前的睡眠状态,这是整个逻辑的入口。
由定时任务周期性调用。
"""
current_state = sleep_state_manager.get_current_state()
now = datetime.now()
if current_state == SleepState.AWAKE:
self._check_should_fall_asleep(now)
elif current_state == SleepState.SLEEPING:
self._check_should_wake_up(now)
elif current_state == SleepState.INSOMNIA:
# TODO: 实现失眠逻辑
# 例如:检查失眠状态是否结束,如果结束则转换回 SLEEPING
pass
elif current_state == SleepState.WOKEN_UP_ANGRY:
# TODO: 实现起床气逻辑
# 例如:检查生气状态是否结束,如果结束则转换回 SLEEPING 或 AWAKE
pass
def _check_should_fall_asleep(self, now: datetime):
"""
当状态为 AWAKE 时,检查是否应该进入睡眠。
"""
should_sleep, wake_up_time = self._should_be_sleeping(now)
if should_sleep:
logger.info("判断结果:应进入睡眠状态。")
sleep_state_manager.set_state(SleepState.SLEEPING, wake_up=wake_up_time)
def _check_should_wake_up(self, now: datetime):
"""
当状态为 SLEEPING 时,检查是否应该醒来。
这里包含了处理跨天获取日程的核心逻辑。
"""
wake_up_time = sleep_state_manager.get_wake_up_time()
# 核心逻辑:两段式检测
# 如果 state_manager 中还没有起床时间,说明是昨晚入睡,需要等待今天凌晨的新日程。
sleep_start_time = sleep_state_manager.get_sleep_start_time()
if not wake_up_time:
if sleep_start_time and now.date() > sleep_start_time.date():
logger.debug("当前为睡眠状态但无起床时间,尝试从新日程中解析...")
_, new_wake_up_time = self._get_wakeup_times_from_schedule(now)
if new_wake_up_time:
logger.info(f"成功从新日程获取到起床时间: {new_wake_up_time.strftime('%H:%M')}")
sleep_state_manager.set_wake_up_time(new_wake_up_time)
wake_up_time = new_wake_up_time
else:
logger.debug("未能获取到新的起床时间,继续睡眠。")
return
else:
logger.info("还没有到达第二天,继续睡眠。")
logger.info(f"尚未到苏醒时间,苏醒时间在{wake_up_time}")
if wake_up_time and now >= wake_up_time:
logger.info(f"当前时间 {now.strftime('%H:%M')} 已到达或超过预定起床时间 {wake_up_time.strftime('%H:%M')}")
sleep_state_manager.set_state(SleepState.AWAKE)
def _should_be_sleeping(self, now: datetime) -> tuple[bool, datetime | None]:
"""
判断在当前时刻,是否应该处于睡眠时间。
Returns:
元组 (是否应该睡眠, 预期的起床时间或None)
"""
sleep_config = global_config.sleep_system
if not sleep_config.enable:
return False, None
sleep_time, wake_up_time = None, None
if sleep_config.sleep_by_schedule:
sleep_time, _ = self._get_sleep_times_from_schedule(now)
if not sleep_time:
logger.debug("日程表模式开启,但未找到睡眠时间,使用固定时间作为备用。")
sleep_time, wake_up_time = self._get_fixed_sleep_times(now)
else:
sleep_time, wake_up_time = self._get_fixed_sleep_times(now)
if not sleep_time:
return False, None
# 检查当前时间是否在睡眠时间范围内
if now >= sleep_time:
# 如果起床时间是第二天(通常情况),且当前时间小于起床时间,则在睡眠范围内
if wake_up_time and wake_up_time > sleep_time and now < wake_up_time:
return True, wake_up_time
# 如果当前时间大于入睡时间,说明已经进入睡眠窗口
return True, wake_up_time
return False, None
def _get_fixed_sleep_times(self, now: datetime) -> tuple[datetime | None, datetime | None]:
"""
当使用“固定时间”模式时,从此方法计算睡眠和起床时间。
会加入配置中的随机偏移量,让作息更自然。
"""
sleep_config = global_config.sleep_system
try:
sleep_offset = random.randint(
-sleep_config.sleep_time_offset_minutes, sleep_config.sleep_time_offset_minutes
)
wake_up_offset = random.randint(
-sleep_config.wake_up_time_offset_minutes, sleep_config.wake_up_time_offset_minutes
)
sleep_t = datetime.strptime(sleep_config.fixed_sleep_time, "%H:%M").time()
wake_up_t = datetime.strptime(sleep_config.fixed_wake_up_time, "%H:%M").time()
sleep_time = datetime.combine(now.date(), sleep_t) + timedelta(minutes=sleep_offset)
# 如果起床时间比睡觉时间早,说明是第二天
wake_up_day = now.date() + timedelta(days=1) if wake_up_t < sleep_t else now.date()
wake_up_time = datetime.combine(wake_up_day, wake_up_t) + timedelta(minutes=wake_up_offset)
return sleep_time, wake_up_time
except (ValueError, TypeError) as e:
logger.error(f"解析固定睡眠时间失败: {e}")
return None, None
def _get_sleep_times_from_schedule(self, now: datetime) -> tuple[datetime | None, datetime | None]:
"""
当使用“日程表”模式时,从此方法获取睡眠时间。
实现了核心逻辑:
- 解析“今天”日程中的睡觉时间。
"""
# 阶段一:获取当天的睡觉时间
today_schedule = schedule_manager.today_schedule
sleep_time = None
if today_schedule:
for event in today_schedule:
activity = event.get("activity", "").lower()
if "sleep" in activity or "睡觉" in activity or "休息" in activity:
try:
time_range = event.get("time_range", "")
start_str, _ = time_range.split("-")
sleep_t = datetime.strptime(start_str.strip(), "%H:%M").time()
sleep_time = datetime.combine(now.date(), sleep_t)
break
except (ValueError, AttributeError):
logger.warning(f"解析日程中的睡眠时间失败: {event}")
continue
wake_up_time = None
return sleep_time, wake_up_time
def _get_wakeup_times_from_schedule(self, now: datetime) -> tuple[datetime | None, datetime | None]:
"""
当使用“日程表”模式时,从此方法获取睡眠时间。
实现了核心逻辑:
- 解析“今天”日程中的睡觉时间。
"""
# 阶段一:获取当天的睡觉时间
today_schedule = schedule_manager.today_schedule
wake_up_time = None
if today_schedule:
for event in today_schedule:
activity = event.get("activity", "").lower()
if "wake_up" in activity or "醒来" in activity or "起床" in activity:
try:
time_range = event.get("time_range", "")
start_str, _ = time_range.split("-")
sleep_t = datetime.strptime(start_str.strip(), "%H:%M").time()
wake_up_time = datetime.combine(now.date(), sleep_t)
break
except (ValueError, AttributeError):
logger.warning(f"解析日程中的睡眠时间失败: {event}")
continue
return None, wake_up_time
# 全局单例
sleep_logic = SleepLogic()

View File

@@ -0,0 +1,190 @@
import enum
from datetime import datetime, timedelta
from typing import Any
from src.common.logger import get_logger
from src.manager.local_store_manager import local_storage
logger = get_logger("sleep_state_manager")
class SleepState(enum.Enum):
"""
定义了所有可能的睡眠状态。
使用枚举可以使状态管理更加清晰和安全。
"""
AWAKE = "awake" # 清醒状态,正常活动
SLEEPING = "sleeping" # 沉睡状态,此时应拦截消息
INSOMNIA = "insomnia" # 失眠状态(为未来功能预留)
WOKEN_UP_ANGRY = "woken_up_angry" # 被吵醒后的生气状态(为未来功能预留)
class SleepStateManager:
"""
睡眠状态管理器 (单例模式)
这是整个睡眠系统的数据核心,负责:
1. 管理当前的睡眠状态(如:是否在睡觉、唤醒度等)。
2. 将状态持久化到本地JSON文件(`local_store.json`),实现重启后状态不丢失。
3. 提供统一的接口供其他模块查询和修改睡眠状态。
"""
_instance = None
_STATE_KEY = "sleep_system_state" # 在 local_store.json 中存储的键名
def __new__(cls, *args, **kwargs):
# 实现单例模式,确保全局只有一个状态管理器实例
if not cls._instance:
cls._instance = super(SleepStateManager, cls).__new__(cls, *args, **kwargs)
return cls._instance
def __init__(self):
"""
初始化状态管理器,定义状态数据结构并从本地加载历史状态。
"""
self.state: dict[str, Any] = {}
self._default_state()
self.load_state()
def _default_state(self):
"""
定义并重置为默认的“清醒”状态。
当机器人启动或从睡眠中醒来时调用。
"""
self.state = {
"state": SleepState.AWAKE.value,
"state_until": None, # 特殊状态(如生气)的自动结束时间
"sleep_start_time": None, # 本次睡眠的开始时间
"wake_up_time": None, # 预定的起床时间
"wakefulness": 0.0, # 唤醒度/清醒值,用于判断是否被吵醒
"last_checked": None, # 定时任务最后检查的时间
}
def load_state(self):
"""
程序启动时,从 local_storage 加载上一次的状态。
如果找不到历史状态,则初始化为默认状态。
"""
stored_state = local_storage[self._STATE_KEY]
if isinstance(stored_state, dict):
# 合并加载的状态,以防新增字段
self.state.update(stored_state)
# 确保 state 字段是枚举成员
if "state" in self.state and not isinstance(self.state["state"], SleepState):
try:
self.state["state"] = SleepState(self.state["state"])
except ValueError:
logger.warning(f"加载了无效的睡眠状态 '{self.state['state']}',重置为 AWAKE。")
self.state["state"] = SleepState.AWAKE
else:
self.state["state"] = SleepState.AWAKE # 兼容旧数据
logger.info(f"成功加载睡眠状态: {self.get_current_state().name}")
else:
logger.info("未找到已存储的睡眠状态,将使用默认值。")
self.save_state()
def save_state(self):
"""
将当前内存中的状态保存到 local_storage。
在保存前,会将枚举类型的 state 转换为字符串以便JSON序列化。
"""
data_to_save = self.state.copy()
# 将 state 枚举成员转换为它的值(字符串)
data_to_save["state"] = self.state["state"]
local_storage[self._STATE_KEY] = data_to_save
logger.debug(f"睡眠状态已保存: {data_to_save}")
def get_current_state(self) -> SleepState:
"""
获取当前的睡眠状态。
在返回状态前,会先检查特殊状态(如生气)是否已过期。
"""
# 检查特殊状态是否已过期
state_until_str = self.state.get("state_until")
if state_until_str:
state_until = datetime.fromisoformat(state_until_str)
if datetime.now() > state_until:
logger.info(f"特殊状态 {self.state['state'].name} 已结束,自动恢复为 SLEEPING。")
# 假设特殊状态(如生气)结束后,是恢复到普通睡眠状态
self.set_state(SleepState.SLEEPING)
return self.state["state"]
def set_state(
self,
new_state: SleepState,
duration_seconds: float | None = None,
sleep_start: datetime | None = None,
wake_up: datetime | None = None,
):
"""
核心函数:切换到新的睡眠状态,并更新相关的状态数据。
"""
current_state = self.get_current_state()
if current_state == new_state:
return # 状态未改变
logger.info(f"睡眠状态变更: {current_state.name} -> {new_state.name}")
self.state["state"] = new_state
if new_state == SleepState.AWAKE:
self._default_state() # 醒来时重置所有状态
self.state["state"] = SleepState.AWAKE # 确保状态正确
elif new_state == SleepState.SLEEPING:
self.state["sleep_start_time"] = (sleep_start or datetime.now()).isoformat()
self.state["wake_up_time"] = wake_up.isoformat() if wake_up else None
self.state["state_until"] = None # 清除特殊状态持续时间
self.state["wakefulness"] = 0.0 # 进入睡眠时清零唤醒度
elif new_state in [SleepState.WOKEN_UP_ANGRY, SleepState.INSOMNIA]:
if duration_seconds:
self.state["state_until"] = (datetime.now() + timedelta(seconds=duration_seconds)).isoformat()
else:
self.state["state_until"] = None
self.save_state()
def update_last_checked(self):
"""更新最后检查时间"""
self.state["last_checked"] = datetime.now().isoformat()
self.save_state()
def get_wake_up_time(self) -> datetime | None:
"""获取预定的起床时间,如果已设置的话。"""
wake_up_str = self.state.get("wake_up_time")
if wake_up_str:
try:
return datetime.fromisoformat(wake_up_str)
except (ValueError, TypeError):
return None
return None
def get_sleep_start_time(self) -> datetime | None:
"""获取本次睡眠的开始时间,如果已设置的话。"""
sleep_start_str = self.state.get("sleep_start_time")
if sleep_start_str:
try:
return datetime.fromisoformat(sleep_start_str)
except (ValueError, TypeError):
return None
return None
def set_wake_up_time(self, wake_up: datetime):
"""
更新起床时间。
主要用于“日程表”模式下,当第二天凌晨拿到新日程时,更新之前未知的起床时间。
"""
if self.get_current_state() == SleepState.AWAKE:
logger.warning("尝试为清醒状态设置起床时间,操作被忽略。")
return
self.state["wake_up_time"] = wake_up.isoformat()
logger.info(f"更新预定起床时间为: {self.state['wake_up_time']}")
self.save_state()
# 全局单例
sleep_state_manager = SleepStateManager()

View File

@@ -0,0 +1,44 @@
from src.common.logger import get_logger
from src.manager.async_task_manager import AsyncTask, async_task_manager
from .sleep_logic import sleep_logic
logger = get_logger("sleep_tasks")
class SleepSystemCheckTask(AsyncTask):
"""
睡眠系统周期性检查任务。
继承自 AsyncTask由 async_task_manager 统一管理。
"""
def __init__(self, run_interval: int = 60):
"""
初始化任务。
Args:
run_interval (int): 任务运行的时间间隔。默认为60秒检查一次。
"""
super().__init__(task_name="SleepSystemCheckTask", run_interval=run_interval)
async def run(self):
"""
任务的核心执行过程。
每次运行时,调用 sleep_logic 的主函数来检查和更新状态。
"""
logger.debug("睡眠系统定时任务触发,开始检查状态...")
try:
# 调用“大脑”进行一次思考和判断
sleep_logic.check_and_update_sleep_state()
except Exception as e:
logger.error(f"周期性检查睡眠状态时发生未知错误: {e}", exc_info=True)
async def start_sleep_system_tasks():
"""
启动睡眠系统的后台定时检查任务。
这个函数应该在程序启动时(例如 main.py被调用。
"""
logger.info("正在启动睡眠系统后台任务...")
check_task = SleepSystemCheckTask()
await async_task_manager.add_task(check_task)
logger.info("睡眠系统后台任务已成功启动。")

View File

@@ -1,5 +1,6 @@
import os import os
import re import re
import time
import traceback import traceback
from typing import Any from typing import Any
@@ -11,7 +12,7 @@ from src.chat.message_manager import message_manager
from src.chat.message_receive.chat_stream import ChatStream, get_chat_manager from src.chat.message_receive.chat_stream import ChatStream, get_chat_manager
from src.chat.message_receive.message import MessageRecv, MessageRecvS4U from src.chat.message_receive.message import MessageRecv, MessageRecvS4U
from src.chat.message_receive.storage import MessageStorage from src.chat.message_receive.storage import MessageStorage
from src.chat.utils.prompt import create_prompt_async, global_prompt_manager from src.chat.utils.prompt import Prompt, global_prompt_manager, create_prompt_async
from src.chat.utils.utils import is_mentioned_bot_in_message from src.chat.utils.utils import is_mentioned_bot_in_message
from src.common.logger import get_logger from src.common.logger import get_logger
from src.config.config import global_config from src.config.config import global_config
@@ -318,12 +319,12 @@ class ChatBot:
else: else:
logger.debug("notice消息触发聊天流程配置已开启") logger.debug("notice消息触发聊天流程配置已开启")
return False # 返回False表示继续处理触发聊天流程 return False # 返回False表示继续处理触发聊天流程
# 兼容旧的notice判断方式 # 兼容旧的notice判断方式
if message.message_info.message_id == "notice": if message.message_info.message_id == "notice":
message.is_notify = True message.is_notify = True
logger.info("旧格式notice消息") logger.info("旧格式notice消息")
# 同样根据配置决定 # 同样根据配置决定
if not global_config.notice.enable_notice_trigger_chat: if not global_config.notice.enable_notice_trigger_chat:
return True return True
@@ -476,18 +477,17 @@ class ChatBot:
if notice_handled: if notice_handled:
# notice消息已处理需要先添加到message_manager再存储 # notice消息已处理需要先添加到message_manager再存储
try: try:
import time
from src.common.data_models.database_data_model import DatabaseMessages from src.common.data_models.database_data_model import DatabaseMessages
import time
message_info = message.message_info message_info = message.message_info
msg_user_info = getattr(message_info, "user_info", None) msg_user_info = getattr(message_info, "user_info", None)
stream_user_info = getattr(message.chat_stream, "user_info", None) stream_user_info = getattr(message.chat_stream, "user_info", None)
group_info = getattr(message.chat_stream, "group_info", None) group_info = getattr(message.chat_stream, "group_info", None)
message_id = message_info.message_id or "" message_id = message_info.message_id or ""
message_time = message_info.time if message_info.time is not None else time.time() message_time = message_info.time if message_info.time is not None else time.time()
user_id = "" user_id = ""
user_nickname = "" user_nickname = ""
user_cardname = None user_cardname = None
@@ -502,16 +502,16 @@ class ChatBot:
user_nickname = getattr(stream_user_info, "user_nickname", "") or "" user_nickname = getattr(stream_user_info, "user_nickname", "") or ""
user_cardname = getattr(stream_user_info, "user_cardname", None) user_cardname = getattr(stream_user_info, "user_cardname", None)
user_platform = getattr(stream_user_info, "platform", "") or "" user_platform = getattr(stream_user_info, "platform", "") or ""
chat_user_id = str(getattr(stream_user_info, "user_id", "") or "") chat_user_id = str(getattr(stream_user_info, "user_id", "") or "")
chat_user_nickname = getattr(stream_user_info, "user_nickname", "") or "" chat_user_nickname = getattr(stream_user_info, "user_nickname", "") or ""
chat_user_cardname = getattr(stream_user_info, "user_cardname", None) chat_user_cardname = getattr(stream_user_info, "user_cardname", None)
chat_user_platform = getattr(stream_user_info, "platform", "") or "" chat_user_platform = getattr(stream_user_info, "platform", "") or ""
group_id = getattr(group_info, "group_id", None) group_id = getattr(group_info, "group_id", None)
group_name = getattr(group_info, "group_name", None) group_name = getattr(group_info, "group_name", None)
group_platform = getattr(group_info, "platform", None) group_platform = getattr(group_info, "platform", None)
# 构建additional_config确保包含is_notice标志 # 构建additional_config确保包含is_notice标志
import json import json
additional_config_dict = { additional_config_dict = {
@@ -519,9 +519,9 @@ class ChatBot:
"notice_type": message.notice_type or "unknown", "notice_type": message.notice_type or "unknown",
"is_public_notice": bool(message.is_public_notice), "is_public_notice": bool(message.is_public_notice),
} }
# 如果message_info有additional_config合并进来 # 如果message_info有additional_config合并进来
if hasattr(message_info, "additional_config") and message_info.additional_config: if hasattr(message_info, 'additional_config') and message_info.additional_config:
if isinstance(message_info.additional_config, dict): if isinstance(message_info.additional_config, dict):
additional_config_dict.update(message_info.additional_config) additional_config_dict.update(message_info.additional_config)
elif isinstance(message_info.additional_config, str): elif isinstance(message_info.additional_config, str):
@@ -530,9 +530,9 @@ class ChatBot:
additional_config_dict.update(existing_config) additional_config_dict.update(existing_config)
except Exception: except Exception:
pass pass
additional_config_json = json.dumps(additional_config_dict) additional_config_json = json.dumps(additional_config_dict)
# 创建数据库消息对象 # 创建数据库消息对象
db_message = DatabaseMessages( db_message = DatabaseMessages(
message_id=message_id, message_id=message_id,
@@ -560,14 +560,14 @@ class ChatBot:
chat_info_group_name=group_name, chat_info_group_name=group_name,
chat_info_group_platform=group_platform, chat_info_group_platform=group_platform,
) )
# 添加到message_manager这会将notice添加到全局notice管理器 # 添加到message_manager这会将notice添加到全局notice管理器
await message_manager.add_message(message.chat_stream.stream_id, db_message) await message_manager.add_message(message.chat_stream.stream_id, db_message)
logger.info(f"✅ Notice消息已添加到message_manager: type={message.notice_type}, stream={message.chat_stream.stream_id}") logger.info(f"✅ Notice消息已添加到message_manager: type={message.notice_type}, stream={message.chat_stream.stream_id}")
except Exception as e: except Exception as e:
logger.error(f"Notice消息添加到message_manager失败: {e}", exc_info=True) logger.error(f"Notice消息添加到message_manager失败: {e}", exc_info=True)
# 存储后直接返回 # 存储后直接返回
await MessageStorage.store_message(message, chat) await MessageStorage.store_message(message, chat)
logger.debug("notice消息已存储跳过后续处理") logger.debug("notice消息已存储跳过后续处理")
@@ -618,9 +618,8 @@ class ChatBot:
template_group_name = None template_group_name = None
async def preprocess(): async def preprocess():
import time
from src.common.data_models.database_data_model import DatabaseMessages from src.common.data_models.database_data_model import DatabaseMessages
import time
message_info = message.message_info message_info = message.message_info
msg_user_info = getattr(message_info, "user_info", None) msg_user_info = getattr(message_info, "user_info", None)

View File

@@ -133,7 +133,7 @@ class MessageRecv(Message):
self.key_words = [] self.key_words = []
self.key_words_lite = [] self.key_words_lite = []
# 解析additional_config中的notice信息 # 解析additional_config中的notice信息
if self.message_info.additional_config and isinstance(self.message_info.additional_config, dict): if self.message_info.additional_config and isinstance(self.message_info.additional_config, dict):
self.is_notify = self.message_info.additional_config.get("is_notice", False) self.is_notify = self.message_info.additional_config.get("is_notice", False)

View File

@@ -99,21 +99,6 @@ class MessageStorage:
# 将priority_info字典序列化为JSON字符串以便存储到数据库的Text字段 # 将priority_info字典序列化为JSON字符串以便存储到数据库的Text字段
priority_info_json = orjson.dumps(priority_info).decode("utf-8") if priority_info else None priority_info_json = orjson.dumps(priority_info).decode("utf-8") if priority_info else None
# 准备additional_config包含format_info和其他配置
additional_config_data = {}
# 保存format_info到additional_config中
if hasattr(message.message_info, 'format_info') and message.message_info.format_info:
format_info_dict = message.message_info.format_info.to_dict()
additional_config_data["format_info"] = format_info_dict
# 合并adapter传递的其他additional_config
if hasattr(message.message_info, 'additional_config') and message.message_info.additional_config:
additional_config_data.update(message.message_info.additional_config)
# 序列化为JSON字符串以便存储
additional_config_json = orjson.dumps(additional_config_data).decode("utf-8") if additional_config_data else None
# 获取数据库会话 # 获取数据库会话
new_message = Messages( new_message = Messages(
@@ -149,7 +134,6 @@ class MessageStorage:
is_command=is_command, is_command=is_command,
key_words=key_words, key_words=key_words,
key_words_lite=key_words_lite, key_words_lite=key_words_lite,
additional_config=additional_config_json,
) )
async with get_db_session() as session: async with get_db_session() as session:
session.add(new_message) session.add(new_message)
@@ -222,7 +206,7 @@ class MessageStorage:
async def replace_image_descriptions(text: str) -> str: async def replace_image_descriptions(text: str) -> str:
"""异步地将文本中的所有[图片:描述]标记替换为[picid:image_id]""" """异步地将文本中的所有[图片:描述]标记替换为[picid:image_id]"""
pattern = r"\[图片:([^\]]+)\]" pattern = r"\[图片:([^\]]+)\]"
# 如果没有匹配项,提前返回以提高效率 # 如果没有匹配项,提前返回以提高效率
if not re.search(pattern, text): if not re.search(pattern, text):
return text return text
@@ -233,7 +217,7 @@ class MessageStorage:
for match in re.finditer(pattern, text): for match in re.finditer(pattern, text):
# 添加上一个匹配到当前匹配之间的文本 # 添加上一个匹配到当前匹配之间的文本
new_text.append(text[last_end:match.start()]) new_text.append(text[last_end:match.start()])
description = match.group(1).strip() description = match.group(1).strip()
replacement = match.group(0) # 默认情况下,替换为原始匹配文本 replacement = match.group(0) # 默认情况下,替换为原始匹配文本
try: try:
@@ -260,7 +244,7 @@ class MessageStorage:
# 添加最后一个匹配到字符串末尾的文本 # 添加最后一个匹配到字符串末尾的文本
new_text.append(text[last_end:]) new_text.append(text[last_end:])
return "".join(new_text) return "".join(new_text)
@staticmethod @staticmethod

View File

@@ -165,6 +165,7 @@ class ChatterActionManager:
执行结果 执行结果
""" """
chat_stream = None
try: try:
logger.debug(f"🎯 [ActionManager] execute_action接收到 target_message: {target_message}") logger.debug(f"🎯 [ActionManager] execute_action接收到 target_message: {target_message}")
# 通过chat_id获取chat_stream # 通过chat_id获取chat_stream
@@ -180,6 +181,9 @@ class ChatterActionManager:
"error": "chat_stream not found", "error": "chat_stream not found",
} }
# 设置正在回复的状态
chat_stream.context_manager.context.is_replying = True
if action_name == "no_action": if action_name == "no_action":
return {"action_type": "no_action", "success": True, "reply_text": "", "command": ""} return {"action_type": "no_action", "success": True, "reply_text": "", "command": ""}
@@ -205,7 +209,7 @@ class ChatterActionManager:
action_build_into_prompt=False, action_build_into_prompt=False,
action_prompt_display=reason, action_prompt_display=reason,
action_done=True, action_done=True,
thinking_id=thinking_id, thinking_id=thinking_id or "",
action_data={"reason": reason}, action_data={"reason": reason},
action_name="no_reply", action_name="no_reply",
) )
@@ -298,6 +302,10 @@ class ChatterActionManager:
"loop_info": None, "loop_info": None,
"error": str(e), "error": str(e),
} }
finally:
# 确保重置正在回复的状态
if chat_stream:
chat_stream.context_manager.context.is_replying = False
async def _record_action_to_message(self, chat_stream, action_name, target_message, action_data): async def _record_action_to_message(self, chat_stream, action_name, target_message, action_data):
""" """

View File

@@ -4,8 +4,6 @@ import random
import time import time
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
import orjson
from src.chat.message_receive.chat_stream import get_chat_manager from src.chat.message_receive.chat_stream import get_chat_manager
from src.chat.planner_actions.action_manager import ChatterActionManager from src.chat.planner_actions.action_manager import ChatterActionManager
from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat
@@ -184,98 +182,13 @@ class ActionModifier:
def _check_action_associated_types(self, all_actions: dict[str, ActionInfo], chat_context: StreamContext): def _check_action_associated_types(self, all_actions: dict[str, ActionInfo], chat_context: StreamContext):
type_mismatched_actions: list[tuple[str, str]] = [] type_mismatched_actions: list[tuple[str, str]] = []
for action_name, action_info in all_actions.items(): for action_name, action_info in all_actions.items():
if action_info.associated_types and not self._check_action_output_types(action_info.associated_types, chat_context): if action_info.associated_types and not chat_context.check_types(action_info.associated_types):
associated_types_str = ", ".join(action_info.associated_types) associated_types_str = ", ".join(action_info.associated_types)
reason = f"适配器不支持(需要: {associated_types_str}" reason = f"适配器不支持(需要: {associated_types_str}"
type_mismatched_actions.append((action_name, reason)) type_mismatched_actions.append((action_name, reason))
logger.debug(f"{self.log_prefix}决定移除动作: {action_name},原因: {reason}") logger.debug(f"{self.log_prefix}决定移除动作: {action_name},原因: {reason}")
return type_mismatched_actions return type_mismatched_actions
def _check_action_output_types(self, output_types: list[str], chat_context: StreamContext) -> bool:
"""
检查Action的输出类型是否被当前适配器支持
Args:
output_types: Action需要输出的消息类型列表
chat_context: 聊天上下文
Returns:
bool: 如果所有输出类型都支持则返回True
"""
# 获取当前适配器支持的输出类型
adapter_supported_types = self._get_adapter_supported_output_types(chat_context)
# 检查所有需要的输出类型是否都被支持
for output_type in output_types:
if output_type not in adapter_supported_types:
logger.debug(f"适配器不支持输出类型 '{output_type}',支持的类型: {adapter_supported_types}")
return False
return True
def _get_adapter_supported_output_types(self, chat_context: StreamContext) -> list[str]:
"""
获取当前适配器支持的输出类型列表
Args:
chat_context: 聊天上下文
Returns:
list[str]: 支持的输出类型列表
"""
# 检查additional_config是否存在且不为空
if (chat_context.current_message
and hasattr(chat_context.current_message, "additional_config")
and chat_context.current_message.additional_config):
try:
additional_config = chat_context.current_message.additional_config
format_info = None
# 处理additional_config可能是字符串或字典的情况
if isinstance(additional_config, str):
# 如果是字符串尝试解析为JSON
try:
config = orjson.loads(additional_config)
format_info = config.get("format_info")
except (orjson.JSONDecodeError, AttributeError, TypeError):
logger.debug("无法解析additional_config JSON字符串")
format_info = None
elif isinstance(additional_config, dict):
# 如果是字典直接获取format_info
format_info = additional_config.get("format_info")
# 如果找到了format_info从中提取支持的类型
if format_info:
# 优先检查accept_format字段
if "accept_format" in format_info:
accept_format = format_info["accept_format"]
if isinstance(accept_format, str):
accept_format = [accept_format]
elif isinstance(accept_format, list):
pass
else:
accept_format = list(accept_format) if hasattr(accept_format, "__iter__") else []
# 合并基础类型和适配器特定类型
return list(set(accept_format))
# 备用检查content_format字段
elif "content_format" in format_info:
content_format = format_info["content_format"]
if isinstance(content_format, str):
content_format = [content_format]
elif isinstance(content_format, list):
pass
else:
content_format = list(content_format) if hasattr(content_format, "__iter__") else []
return list(set(content_format))
except Exception as e:
logger.debug(f"解析适配器格式信息失败,使用默认支持类型: {e}")
async def _get_deactivated_actions_by_type( async def _get_deactivated_actions_by_type(
self, self,
actions_with_info: dict[str, ActionInfo], actions_with_info: dict[str, ActionInfo],

View File

@@ -825,10 +825,10 @@ class DefaultReplyer:
logger.debug(f"开始构建notice块chat_id={chat_id}") logger.debug(f"开始构建notice块chat_id={chat_id}")
# 检查是否启用notice in prompt # 检查是否启用notice in prompt
if not hasattr(global_config, "notice"): if not hasattr(global_config, 'notice'):
logger.debug("notice配置不存在") logger.debug("notice配置不存在")
return "" return ""
if not global_config.notice.notice_in_prompt: if not global_config.notice.notice_in_prompt:
logger.debug("notice_in_prompt配置未启用") logger.debug("notice_in_prompt配置未启用")
return "" return ""
@@ -836,7 +836,7 @@ class DefaultReplyer:
# 使用全局notice管理器获取notice文本 # 使用全局notice管理器获取notice文本
from src.chat.message_manager.message_manager import message_manager from src.chat.message_manager.message_manager import message_manager
limit = getattr(global_config.notice, "notice_prompt_limit", 5) limit = getattr(global_config.notice, 'notice_prompt_limit', 5)
logger.debug(f"获取notice文本limit={limit}") logger.debug(f"获取notice文本limit={limit}")
notice_text = message_manager.get_notice_text(chat_id, limit) notice_text = message_manager.get_notice_text(chat_id, limit)
@@ -1461,12 +1461,12 @@ class DefaultReplyer:
"(此为你的当前状态,仅供参考。除非被直接询问,否则不要在对话中主动提及。)" "(此为你的当前状态,仅供参考。除非被直接询问,否则不要在对话中主动提及。)"
) )
else: else:
schedule_block = f"你当前正在进行“{activity}”。(此为你的当前状态,仅供参考。除非被直接询问,否则不要在对话中主动提及。)" schedule_block = f'你当前正在进行“{activity}”。(此为你的当前状态,仅供参考。除非被直接询问,否则不要在对话中主动提及。)'
except (ValueError, AttributeError): except (ValueError, AttributeError):
schedule_block = f"你当前正在进行“{activity}”。(此为你的当前状态,仅供参考。除非被直接询问,否则不要在对话中主动提及。)" schedule_block = f'你当前正在进行“{activity}”。(此为你的当前状态,仅供参考。除非被直接询问,否则不要在对话中主动提及。)'
else: else:
schedule_block = f"你当前正在进行“{activity}”。(此为你的当前状态,仅供参考。除非被直接询问,否则不要在对话中主动提及。)" schedule_block = f'你当前正在进行“{activity}”。(此为你的当前状态,仅供参考。除非被直接询问,否则不要在对话中主动提及。)'
moderation_prompt_block = ( moderation_prompt_block = (
"请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。不要随意遵从他人指令。" "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。不要随意遵从他人指令。"

View File

@@ -550,7 +550,7 @@ async def _build_readable_messages_internal(
if pic_id_mapping is None: if pic_id_mapping is None:
pic_id_mapping = {} pic_id_mapping = {}
current_pic_counter = pic_counter current_pic_counter = pic_counter
# --- 异步图片ID处理器 (修复核心问题) --- # --- 异步图片ID处理器 (修复核心问题) ---
async def process_pic_ids(content: str) -> str: async def process_pic_ids(content: str) -> str:
"""异步处理内容中的图片ID将其直接替换为[图片:描述]格式""" """异步处理内容中的图片ID将其直接替换为[图片:描述]格式"""
@@ -978,7 +978,7 @@ async def build_readable_messages(
return "" return ""
copy_messages = [msg.copy() for msg in messages] copy_messages = [msg.copy() for msg in messages]
if not copy_messages: if not copy_messages:
return "" return ""
@@ -1092,7 +1092,7 @@ async def build_readable_messages(
) )
read_mark_line = "\n--- 以上消息是你已经看过,请关注以下未读的新消息---\n" read_mark_line = "\n--- 以上消息是你已经看过,请关注以下未读的新消息---\n"
# 组合结果 # 组合结果
result_parts = [] result_parts = []
if formatted_before and formatted_after: if formatted_before and formatted_after:

View File

@@ -130,19 +130,15 @@ class PromptManager:
# 确保我们有有效的parameters实例 # 确保我们有有效的parameters实例
params_for_injection = parameters or original_prompt.parameters params_for_injection = parameters or original_prompt.parameters
# 应用所有匹配的注入规则,获取修改后的模板 components_prefix = await prompt_component_manager.execute_components_for(
modified_template = await prompt_component_manager.apply_injections( injection_point=original_prompt.name, params=params_for_injection
target_prompt_name=original_prompt.name,
original_template=original_prompt.template,
params=params_for_injection,
) )
if components_prefix:
# 如果模板被修改了就创建一个新的临时Prompt实例 logger.info(f"'{name}'注入插件内容: \n{components_prefix}")
if modified_template != original_prompt.template:
logger.info(f"'{name}'应用了Prompt注入规则")
# 创建一个新的临时Prompt实例不进行注册 # 创建一个新的临时Prompt实例不进行注册
new_template = f"{components_prefix}\n\n{original_prompt.template}"
temp_prompt = Prompt( temp_prompt = Prompt(
template=modified_template, template=new_template,
name=original_prompt.name, name=original_prompt.name,
parameters=original_prompt.parameters, parameters=original_prompt.parameters,
should_register=False, # 确保不重新注册 should_register=False, # 确保不重新注册
@@ -1083,12 +1079,12 @@ async def create_prompt_async(
# 动态注入插件内容 # 动态注入插件内容
if name: if name:
modified_template = await prompt_component_manager.apply_injections( components_prefix = await prompt_component_manager.execute_components_for(
target_prompt_name=name, original_template=template, params=final_params injection_point=name, params=final_params
) )
if modified_template != template: if components_prefix:
logger.debug(f"'{name}'应用了Prompt注入规则") logger.debug(f"'{name}'注入插件内容: \n{components_prefix}")
template = modified_template template = f"{components_prefix}\n\n{template}"
# 使用可能已修改的模板创建实例 # 使用可能已修改的模板创建实例
prompt = create_prompt(template, name, final_params) prompt = create_prompt(template, name, final_params)

View File

@@ -1,11 +1,10 @@
import asyncio import asyncio
import re
from typing import Type from typing import Type
from src.chat.utils.prompt_params import PromptParameters from src.chat.utils.prompt_params import PromptParameters
from src.common.logger import get_logger from src.common.logger import get_logger
from src.plugin_system.base.base_prompt import BasePrompt from src.plugin_system.base.base_prompt import BasePrompt
from src.plugin_system.base.component_types import ComponentType, InjectionRule, InjectionType, PromptInfo from src.plugin_system.base.component_types import ComponentType, PromptInfo
from src.plugin_system.core.component_registry import component_registry from src.plugin_system.core.component_registry import component_registry
logger = get_logger("prompt_component_manager") logger = get_logger("prompt_component_manager")
@@ -21,144 +20,90 @@ class PromptComponentManager:
3. 提供一个接口以便在构建核心Prompt时能够获取并执行所有相关的组件。 3. 提供一个接口以便在构建核心Prompt时能够获取并执行所有相关的组件。
""" """
def _get_rules_for(self, target_prompt_name: str) -> list[tuple[InjectionRule, Type[BasePrompt]]]: def get_components_for(self, injection_point: str) -> list[Type[BasePrompt]]:
""" """
获取指定目标Prompt的所有注入规则及其关联的组件类。 获取指定注入点的所有已注册组件类。
Args: Args:
target_prompt_name (str): 目标 Prompt 的名称。 injection_point: 目标Prompt的名称。
Returns: Returns:
list[tuple[InjectionRule, Type[BasePrompt]]]: 一个元组列表 list[Type[BasePrompt]]: 与该注入点关联的组件类列表
每个元组包含一个注入规则和其对应的 Prompt 组件类,并已根据优先级排序。
""" """
# 从注册表中获取所有启用的 PROMPT 类型的组件 # 从组件注册中心获取所有启用的Prompt组件
enabled_prompts = component_registry.get_enabled_components_by_type(ComponentType.PROMPT) enabled_prompts = component_registry.get_enabled_components_by_type(ComponentType.PROMPT)
matching_rules = []
# 遍历所有启用的 Prompt 组件,查找与目标 Prompt 相关的注入规则 matching_components: list[Type[BasePrompt]] = []
for prompt_name, prompt_info in enabled_prompts.items(): for prompt_name, prompt_info in enabled_prompts.items():
# 确保 prompt_info 是 PromptInfo 类型
if not isinstance(prompt_info, PromptInfo): if not isinstance(prompt_info, PromptInfo):
continue continue
# prompt_info.injection_rules 已经经过了后向兼容处理,确保总是列表 # 获取注入点信息
for rule in prompt_info.injection_rules: injection_points = prompt_info.injection_point
# 如果规则的目标是当前指定的 Prompt if isinstance(injection_points, str):
if rule.target_prompt == target_prompt_name: injection_points = [injection_points]
# 获取该规则对应的组件类
component_class = component_registry.get_component_class(prompt_name, ComponentType.PROMPT)
# 确保获取到的确实是一个 BasePrompt 的子类
if component_class and issubclass(component_class, BasePrompt):
matching_rules.append((rule, component_class))
# 根据规则的优先级进行排序,数字越小,优先级越高,越先应用 # 检查当前注入点是否匹配
matching_rules.sort(key=lambda x: x[0].priority) if injection_point in injection_points:
return matching_rules # 获取组件类
component_class = component_registry.get_component_class(prompt_name, ComponentType.PROMPT)
if component_class and issubclass(component_class, BasePrompt):
matching_components.append(component_class)
async def apply_injections( return matching_components
self, target_prompt_name: str, original_template: str, params: PromptParameters
) -> str: async def execute_components_for(self, injection_point: str, params: PromptParameters) -> str:
""" """
获取、实例化并执行所有相关组件,然后根据注入规则修改原始模板 实例化并执行指定注入点的所有组件,然后将它们的输出拼接成一个字符串
这是一个三步走的过程:
1. 实例化所有需要执行的组件。
2. 并行执行它们的 `execute` 方法以获取注入内容。
3. 按照优先级顺序,将内容注入到原始模板中。
Args: Args:
target_prompt_name (str): 目标 Prompt 的名称。 injection_point: 目标Prompt的名称。
original_template (str): 原始的、未经修改的 Prompt 模板字符串 params: 用于初始化组件的 PromptParameters 对象
params (PromptParameters): 传递给 Prompt 组件实例的参数。
Returns: Returns:
str: 应用了所有注入规则后,修改过的 Prompt 模板字符串 str: 所有相关组件生成的、用换行符连接的文本内容
""" """
rules_with_classes = self._get_rules_for(target_prompt_name) component_classes = self.get_components_for(injection_point)
# 如果没有找到任何匹配的规则,就直接返回原始模板,啥也不干 if not component_classes:
if not rules_with_classes: return ""
return original_template
# --- 第一步: 实例化所有需要执行的组件 ---
instance_map = {} # 存储组件实例,虽然目前没直接用,但留着总没错
tasks = [] # 存放所有需要并行执行的 execute 异步任务
components_to_execute = [] # 存放需要执行的组件类,用于后续结果映射
for rule, component_class in rules_with_classes:
# 如果注入类型是 REMOVE那就不需要执行组件了因为它不产生内容
if rule.injection_type != InjectionType.REMOVE:
try:
# 获取组件的元信息,主要是为了拿到插件名称来读取插件配置
prompt_info = component_registry.get_component_info(
component_class.prompt_name, ComponentType.PROMPT
)
if not isinstance(prompt_info, PromptInfo):
plugin_config = {}
else:
# 从注册表获取该组件所属插件的配置
plugin_config = component_registry.get_plugin_config(prompt_info.plugin_name)
# 实例化组件,并传入参数和插件配置
instance = component_class(params=params, plugin_config=plugin_config)
instance_map[component_class.prompt_name] = instance
# 将组件的 execute 方法作为一个任务添加到列表中
tasks.append(instance.execute())
components_to_execute.append(component_class)
except Exception as e:
logger.error(f"实例化 Prompt 组件 '{component_class.prompt_name}' 失败: {e}")
# 即使失败,也添加一个立即完成的空任务,以保持与其他任务的索引同步
tasks.append(asyncio.create_task(asyncio.sleep(0, result=e))) # type: ignore
# --- 第二步: 并行执行所有组件的 execute 方法 ---
# 使用 asyncio.gather 来同时运行所有任务,提高效率
results = await asyncio.gather(*tasks, return_exceptions=True)
# 创建一个从组件名到执行结果的映射,方便后续查找
result_map = {
components_to_execute[i].prompt_name: res
for i, res in enumerate(results)
if not isinstance(res, Exception) # 只包含成功的结果
}
# 单独处理并记录执行失败的组件
for i, res in enumerate(results):
if isinstance(res, Exception):
logger.error(f"执行 Prompt 组件 '{components_to_execute[i].prompt_name}' 失败: {res}")
# --- 第三步: 按优先级顺序应用注入规则 ---
modified_template = original_template
for rule, component_class in rules_with_classes:
# 从结果映射中获取该组件生成的内容
content = result_map.get(component_class.prompt_name)
tasks = []
for component_class in component_classes:
try: try:
if rule.injection_type == InjectionType.PREPEND: # 从注册中心获取组件信息
if content: prompt_info = component_registry.get_component_info(
modified_template = f"{content}\n{modified_template}" component_class.prompt_name, ComponentType.PROMPT
elif rule.injection_type == InjectionType.APPEND:
if content:
modified_template = f"{modified_template}\n{content}"
elif rule.injection_type == InjectionType.REPLACE:
# 使用正则表达式替换目标内容
if content and rule.target_content:
modified_template = re.sub(rule.target_content, str(content), modified_template)
elif rule.injection_type == InjectionType.INSERT_AFTER:
# 在匹配到的内容后面插入
if content and rule.target_content:
# re.sub a little trick: \g<0> represents the entire matched string
replacement = f"\\g<0>\n{content}"
modified_template = re.sub(rule.target_content, replacement, modified_template)
elif rule.injection_type == InjectionType.REMOVE:
# 使用正则表达式移除目标内容
if rule.target_content:
modified_template = re.sub(rule.target_content, "", modified_template)
except re.error as e:
logger.error(
f"在为 '{component_class.prompt_name}' 应用规则时发生正则错误: {e} (pattern: '{rule.target_content}')"
) )
except Exception as e: if not isinstance(prompt_info, PromptInfo):
logger.error(f"应用 Prompt 注入规则 '{rule}' 失败: {e}") logger.warning(f"找不到 Prompt 组件 '{component_class.prompt_name}' 的信息,无法获取插件配置")
plugin_config = {}
else:
plugin_config = component_registry.get_plugin_config(prompt_info.plugin_name)
return modified_template instance = component_class(params=params, plugin_config=plugin_config)
tasks.append(instance.execute())
except Exception as e:
logger.error(f"实例化 Prompt 组件 '{component_class.prompt_name}' 失败: {e}")
if not tasks:
return ""
# 并行执行所有组件
results = await asyncio.gather(*tasks, return_exceptions=True)
# 过滤掉执行失败的结果和空字符串
valid_results = []
for i, result in enumerate(results):
if isinstance(result, Exception):
logger.error(f"执行 Prompt 组件 '{component_classes[i].prompt_name}' 失败: {result}")
elif result and isinstance(result, str) and result.strip():
valid_results.append(result.strip())
# 使用换行符拼接所有有效结果
return "\n".join(valid_results)
# 创建全局单例 # 创建全局单例
prompt_component_manager = PromptComponentManager() prompt_component_manager = PromptComponentManager()

View File

@@ -77,4 +77,4 @@ class PromptParameters:
errors.append("prompt_mode必须是's4u''normal''minimal'") errors.append("prompt_mode必须是's4u''normal''minimal'")
if self.max_context_messages <= 0: if self.max_context_messages <= 0:
errors.append("max_context_messages必须大于0") errors.append("max_context_messages必须大于0")
return errors return errors

View File

@@ -1,5 +1,5 @@
import asyncio
import base64 import base64
import asyncio
import hashlib import hashlib
import io import io
import os import os
@@ -174,7 +174,7 @@ class ImageManager:
# 3. 查询通用图片描述缓存ImageDescriptions表 # 3. 查询通用图片描述缓存ImageDescriptions表
if cached_description := await self._get_description_from_db(image_hash, "emoji"): if cached_description := await self._get_description_from_db(image_hash, "emoji"):
logger.info("[缓存命中] 使用通用图片缓存(ImageDescriptions表)中的描述") logger.info(f"[缓存命中] 使用通用图片缓存(ImageDescriptions表)中的描述")
refined_part = cached_description.split(" Keywords:")[0] refined_part = cached_description.split(" Keywords:")[0]
return f"[表情包:{refined_part}]" return f"[表情包:{refined_part}]"
@@ -185,7 +185,7 @@ class ImageManager:
if not full_description: if not full_description:
logger.warning("未能通过新逻辑生成有效描述") logger.warning("未能通过新逻辑生成有效描述")
return "[表情包(描述生成失败)]" return "[表情包(描述生成失败)]"
# 4. (可选) 如果启用了“偷表情包”,则将图片和完整描述存入待注册区 # 4. (可选) 如果启用了“偷表情包”,则将图片和完整描述存入待注册区
if global_config.emoji.steal_emoji: if global_config.emoji.steal_emoji:
logger.debug(f"偷取表情包功能已开启,保存待注册表情包: {image_hash}") logger.debug(f"偷取表情包功能已开启,保存待注册表情包: {image_hash}")
@@ -231,7 +231,7 @@ class ImageManager:
if existing_image and existing_image.description: if existing_image and existing_image.description:
logger.debug(f"[缓存命中] 使用Images表中的图片描述: {existing_image.description[:50]}...") logger.debug(f"[缓存命中] 使用Images表中的图片描述: {existing_image.description[:50]}...")
return f"[图片:{existing_image.description}]" return f"[图片:{existing_image.description}]"
# 3. 其次查询 ImageDescriptions 表缓存 # 3. 其次查询 ImageDescriptions 表缓存
if cached_description := await self._get_description_from_db(image_hash, "image"): if cached_description := await self._get_description_from_db(image_hash, "image"):
logger.debug(f"[缓存命中] 使用ImageDescriptions表中的描述: {cached_description[:50]}...") logger.debug(f"[缓存命中] 使用ImageDescriptions表中的描述: {cached_description[:50]}...")
@@ -256,9 +256,9 @@ class ImageManager:
break # 成功获取描述则跳出循环 break # 成功获取描述则跳出循环
except Exception as e: except Exception as e:
logger.error(f"VLM调用失败 (第 {i+1}/3 次): {e}", exc_info=True) logger.error(f"VLM调用失败 (第 {i+1}/3 次): {e}", exc_info=True)
if i < 2: # 如果不是最后一次则等待1秒 if i < 2: # 如果不是最后一次则等待1秒
logger.warning("识图失败将在1秒后重试...") logger.warning(f"识图失败将在1秒后重试...")
await asyncio.sleep(1) await asyncio.sleep(1)
if not description or not description.strip(): if not description or not description.strip():
@@ -278,7 +278,7 @@ class ImageManager:
logger.debug(f"[数据库] 为现有图片记录补充描述: {image_hash[:8]}...") logger.debug(f"[数据库] 为现有图片记录补充描述: {image_hash[:8]}...")
# 注意这里不创建新的Images记录因为process_image会负责创建 # 注意这里不创建新的Images记录因为process_image会负责创建
await session.commit() await session.commit()
logger.info(f"新生成的图片描述已存入缓存 (Hash: {image_hash[:8]}...)") logger.info(f"新生成的图片描述已存入缓存 (Hash: {image_hash[:8]}...)")
return f"[图片:{description}]" return f"[图片:{description}]"
@@ -330,7 +330,7 @@ class ImageManager:
# 使用linspace计算4个均匀分布的索引 # 使用linspace计算4个均匀分布的索引
indices = np.linspace(0, num_frames - 1, 4, dtype=int) indices = np.linspace(0, num_frames - 1, 4, dtype=int)
selected_frames = [all_frames[i] for i in indices] selected_frames = [all_frames[i] for i in indices]
logger.debug(f"GIF Frame Analysis: Total frames={num_frames}, Selected indices={indices if num_frames > 4 else list(range(num_frames))}") logger.debug(f"GIF Frame Analysis: Total frames={num_frames}, Selected indices={indices if num_frames > 4 else list(range(num_frames))}")
# --- 帧选择逻辑结束 --- # --- 帧选择逻辑结束 ---

View File

@@ -1,6 +1,6 @@
import traceback import traceback
from collections import defaultdict
from typing import Any from typing import Any
from collections import defaultdict
from sqlalchemy import func, not_, select from sqlalchemy import func, not_, select
from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import DeclarativeBase

View File

@@ -40,6 +40,7 @@ from src.config.official_configs import (
ProactiveThinkingConfig, ProactiveThinkingConfig,
ResponsePostProcessConfig, ResponsePostProcessConfig,
ResponseSplitterConfig, ResponseSplitterConfig,
SleepSystemConfig,
ToolConfig, ToolConfig,
VideoAnalysisConfig, VideoAnalysisConfig,
VoiceConfig, VoiceConfig,
@@ -409,6 +410,7 @@ class Config(ValidatedConfigBase):
default_factory=lambda: DependencyManagementConfig(), description="依赖管理配置" default_factory=lambda: DependencyManagementConfig(), description="依赖管理配置"
) )
web_search: WebSearchConfig = Field(default_factory=lambda: WebSearchConfig(), description="网络搜索配置") web_search: WebSearchConfig = Field(default_factory=lambda: WebSearchConfig(), description="网络搜索配置")
sleep_system: SleepSystemConfig = Field(default_factory=lambda: SleepSystemConfig(), description="睡眠系统配置")
planning_system: PlanningSystemConfig = Field( planning_system: PlanningSystemConfig = Field(
default_factory=lambda: PlanningSystemConfig(), description="规划系统配置" default_factory=lambda: PlanningSystemConfig(), description="规划系统配置"
) )

View File

@@ -593,6 +593,52 @@ class AntiPromptInjectionConfig(ValidatedConfigBase):
shield_suffix: str = Field(default=" 🛡️", description="保护后缀") shield_suffix: str = Field(default=" 🛡️", description="保护后缀")
class SleepSystemConfig(ValidatedConfigBase):
"""睡眠系统配置类"""
enable: bool = Field(default=True, description="是否启用睡眠系统")
sleep_by_schedule: bool = Field(default=True, description="是否根据日程表进行睡觉")
fixed_sleep_time: str = Field(default="23:00", description="固定的睡觉时间")
fixed_wake_up_time: str = Field(default="07:00", description="固定的起床时间")
sleep_time_offset_minutes: int = Field(
default=15, ge=0, le=60, description="睡觉时间随机偏移量范围(分钟),实际睡觉时间会在±该值范围内随机"
)
wake_up_time_offset_minutes: int = Field(
default=15, ge=0, le=60, description="起床时间随机偏移量范围(分钟),实际起床时间会在±该值范围内随机"
)
wakeup_threshold: float = Field(default=15.0, ge=1.0, description="唤醒阈值,达到此值时会被唤醒")
private_message_increment: float = Field(default=3.0, ge=0.1, description="私聊消息增加的唤醒度")
group_mention_increment: float = Field(default=2.0, ge=0.1, description="群聊艾特增加的唤醒度")
decay_rate: float = Field(default=0.2, ge=0.0, description="每次衰减的唤醒度数值")
decay_interval: float = Field(default=30.0, ge=1.0, description="唤醒度衰减间隔(秒)")
angry_duration: float = Field(default=300.0, ge=10.0, description="愤怒状态持续时间(秒)")
angry_prompt: str = Field(default="你被人吵醒了非常生气,说话带着怒气", description="被吵醒后的愤怒提示词")
re_sleep_delay_minutes: int = Field(
default=5, ge=1, description="被唤醒后,如果多久没有新消息则尝试重新入睡(分钟)"
)
# --- 失眠机制相关参数 ---
enable_insomnia_system: bool = Field(default=True, description="是否启用失眠系统")
insomnia_trigger_delay_minutes: list[int] = Field(
default_factory=lambda: [30, 60], description="入睡后触发失眠判定的延迟时间范围(分钟)"
)
insomnia_duration_minutes: list[int] = Field(
default_factory=lambda: [15, 45], description="单次失眠状态的持续时间范围(分钟)"
)
insomnia_chance_pressure: float = Field(default=0.1, ge=0.0, le=1.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_prompt: str = Field(
default="我准备睡觉了,请生成一句简短自然的晚安问候。", description="用于生成睡前消息的提示"
)
class ContextGroup(ValidatedConfigBase): class ContextGroup(ValidatedConfigBase):
""" """
上下文共享组配置 上下文共享组配置

View File

@@ -13,6 +13,7 @@ from rich.traceback import install
from src.chat.emoji_system.emoji_manager import get_emoji_manager from src.chat.emoji_system.emoji_manager import get_emoji_manager
from src.chat.memory_system.memory_manager import memory_manager from src.chat.memory_system.memory_manager import memory_manager
from src.chat.message_manager.sleep_system.tasks import start_sleep_system_tasks
from src.chat.message_receive.bot import chat_bot from src.chat.message_receive.bot import chat_bot
from src.chat.message_receive.chat_stream import get_chat_manager from src.chat.message_receive.chat_stream import get_chat_manager
from src.chat.utils.statistic import OnlineTimeRecordTask, StatisticOutputTask from src.chat.utils.statistic import OnlineTimeRecordTask, StatisticOutputTask
@@ -511,12 +512,22 @@ MoFox_Bot(第三方修改版)
logger.error(f"月度计划管理器初始化失败: {e}") logger.error(f"月度计划管理器初始化失败: {e}")
# 初始化日程管理器 # 初始化日程管理器
if global_config.planning_system.schedule_enable:
try: try:
await schedule_manager.initialize() await schedule_manager.load_or_generate_today_schedule()
await schedule_manager.start_daily_schedule_generation()
logger.info("日程表管理器初始化成功") logger.info("日程表管理器初始化成功")
except Exception as e: except Exception as e:
logger.error(f"日程表管理器初始化失败: {e}") logger.error(f"日程表管理器初始化失败: {e}")
# 初始化睡眠系统
if global_config.sleep_system.enable:
try:
await start_sleep_system_tasks()
logger.info("睡眠系统初始化成功")
except Exception as e:
logger.error(f"睡眠系统初始化失败: {e}")
def _safe_init(self, component_name: str, init_func) -> callable: def _safe_init(self, component_name: str, init_func) -> callable:
"""安全初始化组件,捕获异常""" """安全初始化组件,捕获异常"""

View File

@@ -26,9 +26,9 @@ from .base import (
ActionInfo, ActionInfo,
BaseAction, BaseAction,
BaseCommand, BaseCommand,
BasePrompt,
BaseEventHandler, BaseEventHandler,
BasePlugin, BasePlugin,
BasePrompt,
BaseTool, BaseTool,
ChatMode, ChatMode,
ChatType, ChatType,

View File

@@ -206,7 +206,7 @@ async def build_cross_context_s4u(
) )
all_group_messages.sort(key=lambda x: x["latest_timestamp"], reverse=True) all_group_messages.sort(key=lambda x: x["latest_timestamp"], reverse=True)
# 计算群聊的额度 # 计算群聊的额度
remaining_limit = cross_context_config.s4u_stream_limit - (1 if private_context_block else 0) remaining_limit = cross_context_config.s4u_stream_limit - (1 if private_context_block else 0)
limited_group_messages = all_group_messages[:remaining_limit] limited_group_messages = all_group_messages[:remaining_limit]

View File

@@ -135,6 +135,11 @@ class BasePlugin(PluginBase):
components = self.get_plugin_components() components = self.get_plugin_components()
# 检查依赖
if not self._check_dependencies():
logger.error(f"{self.log_prefix} 依赖检查失败,跳过注册")
return False
# 注册所有组件 # 注册所有组件
registered_components = [] registered_components = []
for component_info, component_class in components: for component_info, component_class in components:

View File

@@ -3,7 +3,7 @@ from typing import Any
from src.chat.utils.prompt_params import PromptParameters from src.chat.utils.prompt_params import PromptParameters
from src.common.logger import get_logger from src.common.logger import get_logger
from src.plugin_system.base.component_types import ComponentType, InjectionRule, PromptInfo from src.plugin_system.base.component_types import ComponentType, PromptInfo
logger = get_logger("base_prompt") logger = get_logger("base_prompt")
@@ -16,7 +16,7 @@ class BasePrompt(ABC):
子类可以通过类属性定义其行为: 子类可以通过类属性定义其行为:
- prompt_name: Prompt组件的唯一名称。 - prompt_name: Prompt组件的唯一名称。
- injection_rules: 定义注入规则的列表。 - injection_point: 指定要注入的目标Prompt名称或名称列表
""" """
prompt_name: str = "" prompt_name: str = ""
@@ -24,15 +24,11 @@ class BasePrompt(ABC):
prompt_description: str = "" prompt_description: str = ""
"""Prompt组件的描述""" """Prompt组件的描述"""
# 定义此组件希望如何注入到核心Prompt中 # 定义此组件希望注入到哪个或哪些核心Prompt中
# 是一个 InjectionRule 对象的列表,可以实现复杂的注入逻辑 # 可以是一个字符串(单个目标)或字符串列表(多个目标)
# 例如: [InjectionRule(target_prompt="planner_prompt", injection_type=InjectionType.APPEND, priority=50)] # 例如: "planner_prompt" 或 ["s4u_style_prompt", "normal_style_prompt"]
injection_rules: list[InjectionRule] = [] injection_point: str | list[str] = ""
"""定义注入规则的列表""" """要注入的目标Prompt名称或列表"""
# 旧的注入点定义,用于向后兼容。如果定义了这个,它将被自动转换为 injection_rules。
injection_point: str | list[str] | None = None
"""[已废弃] 要注入的目标Prompt名称或列表请使用 injection_rules"""
def __init__(self, params: PromptParameters, plugin_config: dict | None = None): def __init__(self, params: PromptParameters, plugin_config: dict | None = None):
"""初始化Prompt组件 """初始化Prompt组件
@@ -91,11 +87,9 @@ class BasePrompt(ABC):
if not cls.prompt_name: if not cls.prompt_name:
raise ValueError("Prompt组件必须定义 'prompt_name' 类属性。") raise ValueError("Prompt组件必须定义 'prompt_name' 类属性。")
# 同时传递新旧两种定义PromptInfo的__post_init__将处理兼容性问题
return PromptInfo( return PromptInfo(
name=cls.prompt_name, name=cls.prompt_name,
component_type=ComponentType.PROMPT, component_type=ComponentType.PROMPT,
description=cls.prompt_description, description=cls.prompt_description,
injection_rules=cls.injection_rules,
injection_point=cls.injection_point, injection_point=cls.injection_point,
) )

View File

@@ -2,38 +2,6 @@ from dataclasses import dataclass, field
from enum import Enum from enum import Enum
from typing import Any from typing import Any
class InjectionType(Enum):
"""Prompt注入类型枚举"""
PREPEND = "prepend" # 在开头添加
APPEND = "append" # 在末尾添加
REPLACE = "replace" # 替换指定内容
REMOVE = "remove" # 删除指定内容
INSERT_AFTER = "insert_after" # 在指定内容之后插入
def __str__(self) -> str:
return self.value
@dataclass
class InjectionRule:
"""Prompt注入规则"""
target_prompt: str # 目标Prompt的名称
injection_type: InjectionType = InjectionType.PREPEND # 注入类型
priority: int = 100 # 优先级,数字越小越先执行
target_content: str | None = None # 用于REPLACE、REMOVE和INSERT_AFTER操作的目标内容支持正则表达式
def __post_init__(self):
if self.injection_type in [
InjectionType.REPLACE,
InjectionType.REMOVE,
InjectionType.INSERT_AFTER,
] and self.target_content is None:
raise ValueError(f"'{self.injection_type.value}'类型的注入规则必须提供 'target_content'")
from maim_message import Seg from maim_message import Seg
from src.llm_models.payload_content.tool_option import ToolCall as ToolCall from src.llm_models.payload_content.tool_option import ToolCall as ToolCall
@@ -166,7 +134,7 @@ class ComponentInfo:
@dataclass @dataclass
class ActionInfo(ComponentInfo): class ActionInfo(ComponentInfo):
"""动作组件信息 """动作组件信息
注意:激活类型相关字段已废弃,推荐使用 Action 类的 go_activate() 方法来自定义激活逻辑。 注意:激活类型相关字段已废弃,推荐使用 Action 类的 go_activate() 方法来自定义激活逻辑。
这些字段将继续保留以提供向后兼容性BaseAction.go_activate() 的默认实现会使用这些字段。 这些字段将继续保留以提供向后兼容性BaseAction.go_activate() 的默认实现会使用这些字段。
""" """
@@ -303,30 +271,13 @@ class EventInfo(ComponentInfo):
class PromptInfo(ComponentInfo): class PromptInfo(ComponentInfo):
"""Prompt组件信息""" """Prompt组件信息"""
injection_rules: list[InjectionRule] = field(default_factory=list) injection_point: str | list[str] = ""
"""定义此组件如何注入到其他Prompt""" """要注入的目标Prompt名称或列表"""
# 旧的injection_point用于向后兼容
injection_point: str | list[str] | None = None
def __post_init__(self): def __post_init__(self):
super().__post_init__() super().__post_init__()
self.component_type = ComponentType.PROMPT self.component_type = ComponentType.PROMPT
# 向后兼容逻辑:如果定义了旧的 injection_point则自动转换为新的 injection_rules
if self.injection_point:
if not self.injection_rules: # 仅当rules为空时转换
points = []
if isinstance(self.injection_point, str):
points.append(self.injection_point)
elif isinstance(self.injection_point, list):
points = self.injection_point
for point in points:
self.injection_rules.append(InjectionRule(target_prompt=point))
# 转换后可以清空旧字段,避免混淆
self.injection_point = None
@dataclass @dataclass
class PluginInfo: class PluginInfo:
@@ -341,7 +292,7 @@ class PluginInfo:
is_built_in: bool = False # 是否为内置插件 is_built_in: bool = False # 是否为内置插件
components: list[ComponentInfo] = field(default_factory=list) # 包含的组件列表 components: list[ComponentInfo] = field(default_factory=list) # 包含的组件列表
dependencies: list[str] = field(default_factory=list) # 依赖的其他插件 dependencies: list[str] = field(default_factory=list) # 依赖的其他插件
python_dependencies: list[str | PythonDependency] = field(default_factory=list) # Python包依赖 python_dependencies: list[PythonDependency] = field(default_factory=list) # Python包依赖
config_file: str = "" # 配置文件路径 config_file: str = "" # 配置文件路径
metadata: dict[str, Any] = field(default_factory=dict) # 额外元数据 metadata: dict[str, Any] = field(default_factory=dict) # 额外元数据
# 新增manifest相关信息 # 新增manifest相关信息

View File

@@ -12,6 +12,7 @@ from src.config.config import CONFIG_DIR
from src.plugin_system.base.component_types import ( from src.plugin_system.base.component_types import (
PermissionNodeField, PermissionNodeField,
PluginInfo, PluginInfo,
PythonDependency,
) )
from src.plugin_system.base.config_types import ConfigField from src.plugin_system.base.config_types import ConfigField
from src.plugin_system.base.plugin_metadata import PluginMetadata from src.plugin_system.base.plugin_metadata import PluginMetadata
@@ -29,6 +30,8 @@ class PluginBase(ABC):
plugin_name: str plugin_name: str
config_file_name: str config_file_name: str
enable_plugin: bool = True enable_plugin: bool = True
dependencies: list[str] = []
python_dependencies: list[str | PythonDependency] = []
config_schema: dict[str, dict[str, ConfigField] | str] = {} config_schema: dict[str, dict[str, ConfigField] | str] = {}
@@ -61,6 +64,12 @@ class PluginBase(ABC):
self.plugin_description = self.plugin_meta.description self.plugin_description = self.plugin_meta.description
self.plugin_author = self.plugin_meta.author self.plugin_author = self.plugin_meta.author
# 标准化Python依赖为PythonDependency对象
normalized_python_deps = self._normalize_python_dependencies(self.python_dependencies)
# 检查Python依赖
self._check_python_dependencies(normalized_python_deps)
# 创建插件信息对象 # 创建插件信息对象
self.plugin_info = PluginInfo( self.plugin_info = PluginInfo(
name=self.plugin_name, name=self.plugin_name,
@@ -71,8 +80,8 @@ class PluginBase(ABC):
enabled=self._is_enabled, enabled=self._is_enabled,
is_built_in=False, is_built_in=False,
config_file=self.config_file_name or "", config_file=self.config_file_name or "",
dependencies=self.plugin_meta.dependencies.copy(), dependencies=self.dependencies.copy(),
python_dependencies=self.plugin_meta.python_dependencies.copy(), python_dependencies=normalized_python_deps,
) )
logger.debug(f"{self.log_prefix} 插件基类初始化完成") logger.debug(f"{self.log_prefix} 插件基类初始化完成")
@@ -358,6 +367,20 @@ class PluginBase(ABC):
self._is_enabled = self.config["plugin"]["enabled"] self._is_enabled = self.config["plugin"]["enabled"]
logger.info(f"{self.log_prefix} 从配置更新插件启用状态: {self._is_enabled}") logger.info(f"{self.log_prefix} 从配置更新插件启用状态: {self._is_enabled}")
def _check_dependencies(self) -> bool:
"""检查插件依赖"""
from src.plugin_system.core.component_registry import component_registry
if not self.dependencies:
return True
for dep in self.dependencies:
if not component_registry.get_plugin_info(dep):
logger.error(f"{self.log_prefix} 缺少依赖插件: {dep}")
return False
return True
def get_config(self, key: str, default: Any = None) -> Any: def get_config(self, key: str, default: Any = None) -> Any:
"""获取插件配置值,支持嵌套键访问 """获取插件配置值,支持嵌套键访问
@@ -380,6 +403,61 @@ class PluginBase(ABC):
return current return current
def _normalize_python_dependencies(self, dependencies: Any) -> list[PythonDependency]:
"""将依赖列表标准化为PythonDependency对象"""
from packaging.requirements import Requirement
normalized = []
for dep in dependencies:
if isinstance(dep, str):
try:
# 尝试解析为requirement格式 (如 "package>=1.0.0")
req = Requirement(dep)
version_spec = str(req.specifier) if req.specifier else ""
normalized.append(
PythonDependency(
package_name=req.name,
version=version_spec,
install_name=dep, # 保持原始的安装名称
)
)
except Exception:
# 如果解析失败,作为简单包名处理
normalized.append(PythonDependency(package_name=dep, install_name=dep))
elif isinstance(dep, PythonDependency):
normalized.append(dep)
else:
logger.warning(f"{self.log_prefix} 未知的依赖格式: {dep}")
return normalized
def _check_python_dependencies(self, dependencies: list[PythonDependency]) -> bool:
"""检查Python依赖并尝试自动安装"""
if not dependencies:
logger.info(f"{self.log_prefix} 无Python依赖需要检查")
return True
try:
# 延迟导入以避免循环依赖
from src.plugin_system.utils.dependency_manager import get_dependency_manager
dependency_manager = get_dependency_manager()
success, errors = dependency_manager.check_and_install_dependencies(dependencies, self.plugin_name)
if success:
logger.info(f"{self.log_prefix} Python依赖检查通过")
return True
else:
logger.error(f"{self.log_prefix} Python依赖检查失败:")
for error in errors:
logger.error(f"{self.log_prefix} - {error}")
return False
except Exception as e:
logger.error(f"{self.log_prefix} Python依赖检查时发生异常: {e}", exc_info=True)
return False
@abstractmethod @abstractmethod
def register_plugin(self) -> bool: def register_plugin(self) -> bool:
""" """

View File

@@ -1,8 +1,6 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any from typing import Any
from src.plugin_system.base.component_types import PythonDependency
@dataclass @dataclass
class PluginMetadata: class PluginMetadata:
@@ -25,9 +23,5 @@ class PluginMetadata:
keywords: list[str] = field(default_factory=list) # 关键词 keywords: list[str] = field(default_factory=list) # 关键词
categories: list[str] = field(default_factory=list) # 分类 categories: list[str] = field(default_factory=list) # 分类
# 依赖关系
dependencies: list[str] = field(default_factory=list) # 插件依赖
python_dependencies: list[str | PythonDependency] = field(default_factory=list) # Python包依赖
# 扩展字段 # 扩展字段
extra: dict[str, Any] = field(default_factory=dict) # 其他任意信息 extra: dict[str, Any] = field(default_factory=dict) # 其他任意信息

View File

@@ -323,33 +323,6 @@ class PluginManager:
init_module = module_from_spec(init_spec) init_module = module_from_spec(init_spec)
init_spec.loader.exec_module(init_module) init_spec.loader.exec_module(init_module)
# --- 在这里进行依赖检查 ---
if hasattr(init_module, "__plugin_meta__"):
metadata = getattr(init_module, "__plugin_meta__")
from src.plugin_system.utils.dependency_manager import get_dependency_manager
dependency_manager = get_dependency_manager()
# 1. 检查Python依赖
if metadata.python_dependencies:
success, errors = dependency_manager.check_and_install_dependencies(
metadata.python_dependencies, metadata.name
)
if not success:
error_msg = f"Python依赖检查失败: {', '.join(errors)}"
self.failed_plugins[plugin_name] = error_msg
logger.error(f"❌ 插件加载失败: {plugin_name} - {error_msg}")
return None # 依赖检查失败,不加载该模块
# 2. 检查插件依赖
if not self._check_plugin_dependencies(metadata):
error_msg = f"插件依赖检查失败: 请确保依赖 {metadata.dependencies} 已正确安装并加载。"
self.failed_plugins[plugin_name] = error_msg
logger.error(f"❌ 插件加载失败: {plugin_name} - {error_msg}")
return None # 插件依赖检查失败
# --- 依赖检查逻辑结束 ---
# 然后加载 plugin.py # 然后加载 plugin.py
spec = spec_from_file_location(module_name, plugin_file) spec = spec_from_file_location(module_name, plugin_file)
if spec is None or spec.loader is None: if spec is None or spec.loader is None:
@@ -362,8 +335,7 @@ class PluginManager:
# 将 __plugin_meta__ 从 init_module 附加到主模块 # 将 __plugin_meta__ 从 init_module 附加到主模块
if init_module and hasattr(init_module, "__plugin_meta__"): if init_module and hasattr(init_module, "__plugin_meta__"):
metadata = getattr(init_module, "__plugin_meta__") setattr(module, "__plugin_meta__", getattr(init_module, "__plugin_meta__"))
setattr(module, "__plugin_meta__", metadata)
logger.debug(f"插件模块加载成功: {plugin_file} -> {plugin_name} ({plugin_dir})") logger.debug(f"插件模块加载成功: {plugin_file} -> {plugin_name} ({plugin_dir})")
return module return module
@@ -374,20 +346,6 @@ class PluginManager:
self.failed_plugins[plugin_name if "plugin_name" in locals() else module_name] = error_msg self.failed_plugins[plugin_name if "plugin_name" in locals() else module_name] = error_msg
return None return None
def _check_plugin_dependencies(self, plugin_meta: PluginMetadata) -> bool:
"""检查插件的插件依赖"""
dependencies = plugin_meta.dependencies
if not dependencies:
return True
for dep_name in dependencies:
# 检查依赖的插件类是否已注册
if dep_name not in self.plugin_classes:
logger.error(f"插件 '{plugin_meta.name}' 缺少依赖: 插件 '{dep_name}' 未找到或加载失败。")
return False
logger.debug(f"插件 '{plugin_meta.name}' 的所有依赖都已找到。")
return True
# == 显示统计与插件信息 == # == 显示统计与插件信息 ==
def _show_stats(self, total_registered: int, total_failed_registration: int): def _show_stats(self, total_registered: int, total_failed_registration: int):
@@ -425,7 +383,7 @@ class PluginManager:
# 组件列表 # 组件列表
if plugin_info.components: if plugin_info.components:
def format_component(c): def format_component(c):
desc = c.description desc = c.description
if len(desc) > 15: if len(desc) > 15:

View File

@@ -60,7 +60,7 @@ class ChatterPlanFilter:
prompt, used_message_id_list = await self._build_prompt(plan) prompt, used_message_id_list = await self._build_prompt(plan)
plan.llm_prompt = prompt plan.llm_prompt = prompt
if global_config.debug.show_prompt: if global_config.debug.show_prompt:
logger.info(f"规划器原始提示词:{prompt}") #叫你不要改你耳朵聋吗😡😡😡😡😡 logger.debug(f"规划器原始提示词:{prompt}")
llm_content, _ = await self.planner_llm.generate_response_async(prompt=prompt) llm_content, _ = await self.planner_llm.generate_response_async(prompt=prompt)
@@ -158,7 +158,7 @@ class ChatterPlanFilter:
if global_config.planning_system.schedule_enable: if global_config.planning_system.schedule_enable:
if activity_info := schedule_manager.get_current_activity(): if activity_info := schedule_manager.get_current_activity():
activity = activity_info.get("activity", "未知活动") activity = activity_info.get("activity", "未知活动")
schedule_block = f"你当前正在进行“{activity}”。(此为你的当前状态,仅供参考。除非被直接询问,否则不要在对话中主动提及。)" schedule_block = f'你当前正在进行“{activity}”。(此为你的当前状态,仅供参考。除非被直接询问,否则不要在对话中主动提及。)'
mood_block = "" mood_block = ""
# 需要情绪模块打开才能获得情绪,否则会引发报错 # 需要情绪模块打开才能获得情绪,否则会引发报错

View File

@@ -9,7 +9,7 @@ from src.chat.utils.utils import get_chat_type_and_target_info
from src.common.data_models.database_data_model import DatabaseMessages from src.common.data_models.database_data_model import DatabaseMessages
from src.common.data_models.info_data_model import Plan, TargetPersonInfo from src.common.data_models.info_data_model import Plan, TargetPersonInfo
from src.config.config import global_config from src.config.config import global_config
from src.plugin_system.base.component_types import ActionInfo, ChatMode, ChatType from src.plugin_system.base.component_types import ActionInfo, ChatMode, ChatType, ComponentType
from src.plugin_system.core.component_registry import component_registry from src.plugin_system.core.component_registry import component_registry

View File

@@ -271,7 +271,7 @@ class EmojiAction(BaseAction):
# 我们假设LLM返回的是精炼描述的一部分或全部 # 我们假设LLM返回的是精炼描述的一部分或全部
matched_emoji = None matched_emoji = None
best_match_score = 0 best_match_score = 0
for item in all_emojis_data: for item in all_emojis_data:
refined_info = extract_refined_info(item[1]) refined_info = extract_refined_info(item[1])
# 计算一个简单的匹配分数 # 计算一个简单的匹配分数
@@ -280,16 +280,16 @@ class EmojiAction(BaseAction):
score += 2 # 包含匹配 score += 2 # 包含匹配
if refined_info.lower() in chosen_description.lower(): if refined_info.lower() in chosen_description.lower():
score += 2 # 包含匹配 score += 2 # 包含匹配
# 关键词匹配加分 # 关键词匹配加分
chosen_keywords = re.findall(r"\w+", chosen_description.lower()) chosen_keywords = re.findall(r'\w+', chosen_description.lower())
item_keywords = re.findall(r"\[(.*?)\]", refined_info) item_keywords = re.findall(r'\[(.*?)\]', refined_info)
if item_keywords: if item_keywords:
item_keywords_set = {k.strip().lower() for k in item_keywords[0].split(",")} item_keywords_set = {k.strip().lower() for k in item_keywords[0].split(',')}
for kw in chosen_keywords: for kw in chosen_keywords:
if kw in item_keywords_set: if kw in item_keywords_set:
score += 1 score += 1
if score > best_match_score: if score > best_match_score:
best_match_score = score best_match_score = score
matched_emoji = item matched_emoji = item

View File

@@ -162,6 +162,16 @@ class MessageHandler:
) )
logger.debug(f"原始消息内容: {raw_message.get('message', [])}") logger.debug(f"原始消息内容: {raw_message.get('message', [])}")
# 检查是否包含@或video消息段
message_segments = raw_message.get("message", [])
if message_segments:
for i, seg in enumerate(message_segments):
seg_type = seg.get("type")
if seg_type in ["at", "video"]:
logger.info(f"检测到 {seg_type.upper()} 消息段 [{i}]: {seg}")
elif seg_type not in ["text", "face", "image"]:
logger.warning(f"检测到特殊消息段 [{i}]: type={seg_type}, data={seg.get('data', {})}")
message_type: str = raw_message.get("message_type") message_type: str = raw_message.get("message_type")
message_id: int = raw_message.get("message_id") message_id: int = raw_message.get("message_id")
# message_time: int = raw_message.get("time") # message_time: int = raw_message.get("time")

View File

@@ -237,6 +237,7 @@ class SendHandler:
target_id = str(target_id) target_id = str(target_id)
if target_id == "notice": if target_id == "notice":
return payload return payload
logger.info(target_id if isinstance(target_id, str) else "")
new_payload = self.build_payload( new_payload = self.build_payload(
payload, payload,
await self.handle_reply_message(target_id if isinstance(target_id, str) else "", user_info), await self.handle_reply_message(target_id if isinstance(target_id, str) else "", user_info),
@@ -321,7 +322,7 @@ class SendHandler:
# 如果没有获取到被回复者的ID则直接返回不进行@ # 如果没有获取到被回复者的ID则直接返回不进行@
if not replied_user_id: if not replied_user_id:
logger.warning(f"无法获取消息 {id} 的发送者信息,跳过 @") logger.warning(f"无法获取消息 {id} 的发送者信息,跳过 @")
logger.debug(f"最终返回的回复段: {reply_seg}") logger.info(f"最终返回的回复段: {reply_seg}")
return reply_seg return reply_seg
# 根据概率决定是否艾特用户 # 根据概率决定是否艾特用户
@@ -339,7 +340,7 @@ class SendHandler:
logger.info(f"最终返回的回复段: {reply_seg}") logger.info(f"最终返回的回复段: {reply_seg}")
return reply_seg return reply_seg
logger.debug(f"最终返回的回复段: {reply_seg}") logger.info(f"最终返回的回复段: {reply_seg}")
return reply_seg return reply_seg
def handle_text_message(self, message: str) -> dict: def handle_text_message(self, message: str) -> dict:

View File

@@ -6,6 +6,7 @@ from datetime import datetime
from maim_message import UserInfo from maim_message import UserInfo
from src.chat.message_manager.sleep_system.state_manager import SleepState, sleep_state_manager
from src.chat.message_receive.chat_stream import get_chat_manager from src.chat.message_receive.chat_stream import get_chat_manager
from src.common.logger import get_logger from src.common.logger import get_logger
from src.config.config import global_config from src.config.config import global_config
@@ -38,6 +39,10 @@ class ColdStartTask(AsyncTask):
await asyncio.sleep(30) # 延迟以确保所有服务和聊天流已从数据库加载完毕 await asyncio.sleep(30) # 延迟以确保所有服务和聊天流已从数据库加载完毕
try: try:
current_state = sleep_state_manager.get_current_state()
if current_state == SleepState.SLEEPING:
logger.info("bot正在睡觉,跳过本次任务")
return
logger.info("【冷启动】开始扫描白名单,唤醒沉睡的聊天流...") logger.info("【冷启动】开始扫描白名单,唤醒沉睡的聊天流...")
# 【修复】增加对私聊总开关的判断 # 【修复】增加对私聊总开关的判断
@@ -147,6 +152,10 @@ class ProactiveThinkingTask(AsyncTask):
# 计算下一次检查前的休眠时间 # 计算下一次检查前的休眠时间
next_interval = self._get_next_interval() next_interval = self._get_next_interval()
try: try:
current_state = sleep_state_manager.get_current_state()
if current_state == SleepState.SLEEPING:
logger.info("bot正在睡觉,跳过本次任务")
return
logger.debug(f"【日常唤醒】下一次检查将在 {next_interval:.2f} 秒后进行。") logger.debug(f"【日常唤醒】下一次检查将在 {next_interval:.2f} 秒后进行。")
await asyncio.sleep(next_interval) await asyncio.sleep(next_interval)

View File

@@ -1,6 +1,5 @@
from src.plugin_system.base.plugin_metadata import PluginMetadata from src.plugin_system.base.plugin_metadata import PluginMetadata
# 定义插件元数据
__plugin_meta__ = PluginMetadata( __plugin_meta__ = PluginMetadata(
name="MoFox-Bot工具箱", name="MoFox-Bot工具箱",
description="一个集合多种实用功能的插件,旨在提升聊天体验和效率。", description="一个集合多种实用功能的插件,旨在提升聊天体验和效率。",
@@ -12,6 +11,4 @@ __plugin_meta__ = PluginMetadata(
keywords=["emoji", "reaction", "like", "表情", "回应", "点赞"], keywords=["emoji", "reaction", "like", "表情", "回应", "点赞"],
categories=["Chat", "Integration"], categories=["Chat", "Integration"],
extra={"is_built_in": "true", "plugin_type": "functional"}, extra={"is_built_in": "true", "plugin_type": "functional"},
dependencies=[],
python_dependencies=["httpx", "Pillow"],
) )

View File

@@ -13,6 +13,5 @@ __plugin_meta__ = PluginMetadata(
extra={ extra={
"is_built_in": False, "is_built_in": False,
"plugin_type": "tools", "plugin_type": "tools",
}, }
python_dependencies = ["aiohttp", "soundfile", "pedalboard"]
) )

View File

@@ -2,33 +2,107 @@
TTS 语音合成 Action TTS 语音合成 Action
""" """
import toml
from pathlib import Path
from src.common.logger import get_logger from src.common.logger import get_logger
from src.plugin_system.apis import generator_api from src.plugin_system.base.base_action import ActionActivationType, BaseAction, ChatMode
from src.plugin_system.base.base_action import BaseAction, ChatMode
from ..services.manager import get_service from ..services.manager import get_service
logger = get_logger("tts_voice_plugin.action") logger = get_logger("tts_voice_plugin.action")
def _get_available_styles() -> list[str]:
"""动态读取配置文件获取所有可用的TTS风格名称"""
try:
# 这个路径构建逻辑是为了确保无论从哪里启动,都能准确定位到配置文件
plugin_file = Path(__file__).resolve()
# Bot/src/plugins/built_in/tts_voice_plugin/actions -> Bot
bot_root = plugin_file.parent.parent.parent.parent.parent.parent
config_file = bot_root / "config" / "plugins" / "tts_voice_plugin" / "config.toml"
if not config_file.is_file():
logger.warning("在 tts_action 中未找到 tts_voice_plugin 的配置文件,无法动态加载风格列表。")
return ["default"]
config = toml.loads(config_file.read_text(encoding="utf-8"))
styles_config = config.get("tts_styles", [])
if not isinstance(styles_config, list):
return ["default"]
# 使用显式循环和类型检查来提取 style_name以确保 Pylance 类型检查通过
style_names: list[str] = []
for style in styles_config:
if isinstance(style, dict):
name = style.get("style_name")
# 确保 name 是一个非空字符串
if isinstance(name, str) and name:
style_names.append(name)
return style_names if style_names else ["default"]
except Exception as e:
logger.error(f"动态加载TTS风格列表时出错: {e}", exc_info=True)
return ["default"] # 出现任何错误都回退
# 在类定义之前执行函数,获取风格列表
AVAILABLE_STYLES = _get_available_styles()
STYLE_OPTIONS_DESC = ", ".join(f"'{s}'" for s in AVAILABLE_STYLES)
class TTSVoiceAction(BaseAction): class TTSVoiceAction(BaseAction):
""" """
通过关键词或规划器自动触发 TTS 语音合成 通过关键词或规划器自动触发 TTS 语音合成
""" """
action_name = "tts_voice_action" action_name = "tts_voice_action"
action_description = "使用GPT-SoVITS将文本转换为语音并发送" action_description = "将你生成好的文本转换为语音并发送。你必须提供要转换的文本。"
mode_enable = ChatMode.ALL mode_enable = ChatMode.ALL
parallel_action = False parallel_action = False
action_parameters = {
"text": {
"type": "string",
"description": "需要转换为语音并发送的完整、自然、适合口语的文本内容。",
"required": True
},
"voice_style": {
"type": "string",
"description": f"语音的风格。可用选项: [{STYLE_OPTIONS_DESC}]。请根据对话的情感和上下文选择一个最合适的风格。如果未提供,将使用默认风格。",
"required": False
},
"text_language": {
"type": "string",
"description": (
"指定用于合成的语言模式,请务必根据文本内容选择最精确、范围最小的选项以获得最佳效果。"
"可用选项说明:\n"
"- 'zh': 中文与英文混合 (最优选)\n"
"- 'ja': 日文与英文混合 (最优选)\n"
"- 'yue': 粤语与英文混合 (最优选)\n"
"- 'ko': 韩文与英文混合 (最优选)\n"
"- 'en': 纯英文\n"
"- 'all_zh': 纯中文\n"
"- 'all_ja': 纯日文\n"
"- 'all_yue': 纯粤语\n"
"- 'all_ko': 纯韩文\n"
"- 'auto': 多语种混合自动识别 (备用选项,当前两种语言时优先使用上面的精确选项)\n"
"- 'auto_yue': 多语种混合自动识别(包含粤语)(备用选项)"
),
"required": False
}
}
action_require = [ action_require = [
"在调用此动作时,你必须在 'text' 参数中提供要合成语音的完整回复内容。这是强制性的。",
"当用户明确请求使用语音进行回复时,例如‘发个语音听听’、‘用语音说’等。", "当用户明确请求使用语音进行回复时,例如‘发个语音听听’、‘用语音说’等。",
"当对话内容适合用语音表达,例如讲故事、念诗、撒嬌或进行角色扮演时。", "当对话内容适合用语音表达,例如讲故事、念诗、撒嬌或进行角色扮演时。",
"在表达特殊情感(如安慰、鼓励、庆祝)的场景下,可以主动使用语音来增强感染力。", "在表达特殊情感(如安慰、鼓励、庆祝)的场景下,可以主动使用语音来增强感染力。",
"不要在日常的、简短的问答或闲聊中频繁使用语音,避免打扰用户。", "不要在日常的、简短的问答或闲聊中频繁使用语音,避免打扰用户。",
"文本内容必须是纯粹的对话,不能包含任何括号或方括号括起来的动作、表情、或场景描述(例如,不要出现 '(笑)''[歪头]'", "提供的 'text' 内容必须是纯粹的对话,不能包含任何括号或方括号括起来的动作、表情、或场景描述(例如,不要出现 '(笑)''[歪头]'",
"必须使用标准、完整的标点符号(如逗号、句号、问号)来进行自然的断句,以确保语音停顿自然,避免生成一长串没有停顿的文本" "【**铁则**】为了确保语音停顿自然,'text' 参数中的所有断句【必须使用且仅能使用以下标准标点符号:''''''''。严禁使用 '''...' 或其他任何非标准符号来分隔句子,否则将导致语音合成失败"
] ]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -65,7 +139,7 @@ class TTSVoiceAction(BaseAction):
): ):
logger.info(f"{self.log_prefix} LLM 判断激活成功") logger.info(f"{self.log_prefix} LLM 判断激活成功")
return True return True
logger.debug(f"{self.log_prefix} 所有激活条件均未满足,不激活") logger.debug(f"{self.log_prefix} 所有激活条件均未满足,不激活")
return False return False
@@ -80,16 +154,23 @@ class TTSVoiceAction(BaseAction):
initial_text = self.action_data.get("text", "").strip() initial_text = self.action_data.get("text", "").strip()
voice_style = self.action_data.get("voice_style", "default") voice_style = self.action_data.get("voice_style", "default")
logger.info(f"{self.log_prefix} 接收到规划器的初步文本: '{initial_text[:70]}...'") # 新增:从决策模型获取指定的语言模式
text_language = self.action_data.get("text_language") # 如果模型没给,就是 None
logger.info(f"{self.log_prefix} 接收到规划器初步文本: '{initial_text[:70]}...', 指定风格: {voice_style}, 指定语言: {text_language}")
# 1. 请求主回复模型生成高质量文本 # 1. 使用规划器提供的文本
text = await self._generate_final_text(initial_text) text = initial_text
if not text: if not text:
logger.warning(f"{self.log_prefix} 最终生成的文本为空,静默处理。") logger.warning(f"{self.log_prefix} 规划器提供的文本为空,静默处理。")
return False, "最终生成的文本为空" return False, "规划器提供的文本为空"
# 2. 调用 TTSService 生成语音 # 2. 调用 TTSService 生成语音
audio_b64 = await self.tts_service.generate_voice(text, voice_style) logger.info(f"{self.log_prefix} 使用最终文本进行语音合成: '{text[:70]}...'")
audio_b64 = await self.tts_service.generate_voice(
text=text,
style_hint=voice_style,
language_hint=text_language # 新增:将决策模型指定的语言传递给服务
)
if audio_b64: if audio_b64:
await self.send_custom(message_type="voice", content=audio_b64) await self.send_custom(message_type="voice", content=audio_b64)
@@ -115,33 +196,3 @@ class TTSVoiceAction(BaseAction):
) )
return False, f"语音合成出错: {e!s}" return False, f"语音合成出错: {e!s}"
async def _generate_final_text(self, initial_text: str) -> str:
"""请求主回复模型生成或优化文本"""
try:
generation_reason = (
"这是一个为语音消息TTS生成文本的特殊任务。"
"请基于规划器提供的初步文本,结合对话历史和自己的人设,将它优化成一句自然、富有感情、适合用语音说出的话。"
"最终指令:请务-必确保文本听起来像真实的、自然的口语对话,而不是书面语。"
)
logger.info(f"{self.log_prefix} 请求主回复模型(replyer)全新生成TTS文本...")
success, response_set, _ = await generator_api.rewrite_reply(
chat_stream=self.chat_stream,
reply_data={"raw_reply": initial_text, "reason": generation_reason},
request_type="replyer"
)
if success and response_set:
text = "".join(str(seg[1]) if isinstance(seg, tuple) else str(seg) for seg in response_set).strip()
logger.info(f"{self.log_prefix} 成功生成高质量TTS文本: {text}")
return text
if initial_text:
logger.warning(f"{self.log_prefix} 主模型生成失败,使用规划器原始文本作为兜底。")
return initial_text
raise Exception("主模型未能生成回复,且规划器也未提供兜底文本。")
except Exception as e:
logger.error(f"{self.log_prefix} 生成高质量回复内容时失败: {e}", exc_info=True)
return ""

View File

@@ -30,6 +30,7 @@ class TTSVoicePlugin(BasePlugin):
plugin_author = "Kilo Code & 靚仔" plugin_author = "Kilo Code & 靚仔"
config_file_name = "config.toml" config_file_name = "config.toml"
dependencies = [] dependencies = []
python_dependencies = ["aiohttp", "soundfile", "pedalboard"]
permission_nodes: list[PermissionNodeField] = [ permission_nodes: list[PermissionNodeField] = [
PermissionNodeField(node_name="command.use", description="是否可以使用 /tts 命令"), PermissionNodeField(node_name="command.use", description="是否可以使用 /tts 命令"),

View File

@@ -80,21 +80,34 @@ class TTSService:
"prompt_language": style_cfg.get("prompt_language", "zh"), "prompt_language": style_cfg.get("prompt_language", "zh"),
"gpt_weights": style_cfg.get("gpt_weights", default_gpt_weights), "gpt_weights": style_cfg.get("gpt_weights", default_gpt_weights),
"sovits_weights": style_cfg.get("sovits_weights", default_sovits_weights), "sovits_weights": style_cfg.get("sovits_weights", default_sovits_weights),
"speed_factor": style_cfg.get("speed_factor"), # 读取独立的语速配置 "speed_factor": style_cfg.get("speed_factor"),
"text_language": style_cfg.get("text_language", "auto"), # 新增:读取文本语言模式
} }
return styles return styles
# ... [其他方法保持不变] ... def _determine_final_language(self, text: str, mode: str) -> str:
def _detect_language(self, text: str) -> str: """根据配置的语言策略和文本内容决定最终发送给API的语言代码"""
chinese_chars = len(re.findall(r"[\u4e00-\u9fff]", text)) # 如果策略是具体的语言(如 all_zh, ja直接使用
english_chars = len(re.findall(r"[a-zA-Z]", text)) if mode not in ["auto", "auto_yue"]:
return mode
# 对于 auto 和 auto_yue 策略,进行内容检测
# 优先检测粤语
if mode == "auto_yue":
cantonese_keywords = ["", "", "", "", "", "", "", "", ""]
if any(keyword in text for keyword in cantonese_keywords):
logger.info("在 auto_yue 模式下检测到粤语关键词,最终语言: yue")
return "yue"
# 检测日语(简单启发式规则)
japanese_chars = len(re.findall(r"[\u3040-\u309f\u30a0-\u30ff]", text)) japanese_chars = len(re.findall(r"[\u3040-\u309f\u30a0-\u30ff]", text))
total_chars = chinese_chars + english_chars + japanese_chars if japanese_chars > 5 and japanese_chars > len(re.findall(r"[\u4e00-\u9fff]", text)) * 0.5:
if total_chars == 0: return "zh" logger.info("检测到日语字符,最终语言: ja")
if chinese_chars / total_chars > 0.3: return "zh" return "ja"
elif japanese_chars / total_chars > 0.3: return "ja"
elif english_chars / total_chars > 0.8: return "en" # 默认回退到中文
else: return "zh" logger.info(f"{mode} 模式下未检测到特定语言,默认回退到: zh")
return "zh"
def _clean_text_for_tts(self, text: str) -> str: def _clean_text_for_tts(self, text: str) -> str:
# 1. 基本清理 # 1. 基本清理
@@ -259,7 +272,7 @@ class TTSService:
logger.error(f"应用空间效果时出错: {e}", exc_info=True) logger.error(f"应用空间效果时出错: {e}", exc_info=True)
return audio_data # 如果出错,返回原始音频 return audio_data # 如果出错,返回原始音频
async def generate_voice(self, text: str, style_hint: str = "default") -> str | None: async def generate_voice(self, text: str, style_hint: str = "default", language_hint: str | None = None) -> str | None:
self._load_config() self._load_config()
if not self.tts_styles: if not self.tts_styles:
@@ -282,11 +295,21 @@ class TTSService:
clean_text = self._clean_text_for_tts(text) clean_text = self._clean_text_for_tts(text)
if not clean_text: return None if not clean_text: return None
text_language = self._detect_language(clean_text) # 语言决策流程:
logger.info(f"开始TTS语音合成文本{clean_text[:50]}..., 风格:{style}") # 1. 优先使用决策模型直接指定的 language_hint (最高优先级)
if language_hint:
final_language = language_hint
logger.info(f"使用决策模型指定的语言: {final_language}")
else:
# 2. 如果模型未指定,则使用风格配置的 language_policy
language_policy = server_config.get("text_language", "auto")
final_language = self._determine_final_language(clean_text, language_policy)
logger.info(f"决策模型未指定语言,使用策略 '{language_policy}' -> 最终语言: {final_language}")
logger.info(f"开始TTS语音合成文本{clean_text[:50]}..., 风格:{style}, 最终语言: {final_language}")
audio_data = await self._call_tts_api( audio_data = await self._call_tts_api(
server_config=server_config, text=clean_text, text_language=text_language, server_config=server_config, text=clean_text, text_language=final_language,
refer_wav_path=server_config.get("refer_wav_path"), refer_wav_path=server_config.get("refer_wav_path"),
prompt_text=server_config.get("prompt_text"), prompt_text=server_config.get("prompt_text"),
prompt_language=server_config.get("prompt_language"), prompt_language=server_config.get("prompt_language"),

View File

@@ -1,4 +1,3 @@
from src.plugin_system.base.component_types import PythonDependency
from src.plugin_system.base.plugin_metadata import PluginMetadata from src.plugin_system.base.plugin_metadata import PluginMetadata
__plugin_meta__ = PluginMetadata( __plugin_meta__ = PluginMetadata(
@@ -14,26 +13,4 @@ __plugin_meta__ = PluginMetadata(
extra={ extra={
"is_built_in": True, "is_built_in": True,
}, },
# Python包依赖列表
python_dependencies = [
PythonDependency(package_name="asyncddgs", description="异步DuckDuckGo搜索库", optional=False),
PythonDependency(
package_name="exa_py",
description="Exa搜索API客户端库",
optional=True, # 如果没有API密钥这个是可选的
),
PythonDependency(
package_name="tavily",
install_name="tavily-python", # 安装时使用这个名称
description="Tavily搜索API客户端库",
optional=True, # 如果没有API密钥这个是可选的
),
PythonDependency(
package_name="httpx",
version=">=0.20.0",
install_name="httpx[socks]", # 安装时使用这个名称(包含可选依赖)
description="支持SOCKS代理的HTTP客户端库",
optional=False,
),
]
) )

View File

@@ -3,7 +3,7 @@ Base search engine interface
""" """
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Any from typing import Any, Optional
class BaseSearchEngine(ABC): class BaseSearchEngine(ABC):
@@ -24,7 +24,7 @@ class BaseSearchEngine(ABC):
""" """
pass pass
async def read_url(self, url: str) -> str | None: async def read_url(self, url: str) -> Optional[str]:
""" """
读取URL内容如果引擎不支持则返回None 读取URL内容如果引擎不支持则返回None
""" """

View File

@@ -2,7 +2,7 @@
Metaso Search Engine (Chat Completions Mode) Metaso Search Engine (Chat Completions Mode)
""" """
import json import json
from typing import Any from typing import Any, List
import httpx import httpx
@@ -27,7 +27,7 @@ class MetasoClient:
"Content-Type": "application/json", "Content-Type": "application/json",
} }
async def search(self, query: str, **kwargs) -> list[dict[str, Any]]: async def search(self, query: str, **kwargs) -> List[dict[str, Any]]:
"""Perform a search using the Metaso Chat Completions API.""" """Perform a search using the Metaso Chat Completions API."""
payload = {"model": "fast", "stream": True, "messages": [{"role": "user", "content": query}]} payload = {"model": "fast", "stream": True, "messages": [{"role": "user", "content": query}]}
search_url = f"{self.base_url}/chat/completions" search_url = f"{self.base_url}/chat/completions"

View File

@@ -5,7 +5,7 @@ Web Search Tool Plugin
""" """
from src.common.logger import get_logger from src.common.logger import get_logger
from src.plugin_system import BasePlugin, ComponentInfo, ConfigField, register_plugin from src.plugin_system import BasePlugin, ComponentInfo, ConfigField, PythonDependency, register_plugin
from src.plugin_system.apis import config_api from src.plugin_system.apis import config_api
from .tools.url_parser import URLParserTool from .tools.url_parser import URLParserTool
@@ -42,9 +42,9 @@ class WEBSEARCHPLUGIN(BasePlugin):
from .engines.bing_engine import BingSearchEngine from .engines.bing_engine import BingSearchEngine
from .engines.ddg_engine import DDGSearchEngine from .engines.ddg_engine import DDGSearchEngine
from .engines.exa_engine import ExaSearchEngine from .engines.exa_engine import ExaSearchEngine
from .engines.metaso_engine import MetasoSearchEngine
from .engines.searxng_engine import SearXNGSearchEngine from .engines.searxng_engine import SearXNGSearchEngine
from .engines.tavily_engine import TavilySearchEngine from .engines.tavily_engine import TavilySearchEngine
from .engines.metaso_engine import MetasoSearchEngine
# 实例化所有搜索引擎这会触发API密钥管理器的初始化 # 实例化所有搜索引擎这会触发API密钥管理器的初始化
exa_engine = ExaSearchEngine() exa_engine = ExaSearchEngine()
@@ -53,7 +53,7 @@ class WEBSEARCHPLUGIN(BasePlugin):
bing_engine = BingSearchEngine() bing_engine = BingSearchEngine()
searxng_engine = SearXNGSearchEngine() searxng_engine = SearXNGSearchEngine()
metaso_engine = MetasoSearchEngine() metaso_engine = MetasoSearchEngine()
# 报告每个引擎的状态 # 报告每个引擎的状态
engines_status = { engines_status = {
"Exa": exa_engine.is_available(), "Exa": exa_engine.is_available(),
@@ -74,6 +74,29 @@ class WEBSEARCHPLUGIN(BasePlugin):
except Exception as e: except Exception as e:
logger.error(f"❌ 搜索引擎初始化失败: {e}", exc_info=True) logger.error(f"❌ 搜索引擎初始化失败: {e}", exc_info=True)
# Python包依赖列表
python_dependencies: list[PythonDependency] = [ # noqa: RUF012
PythonDependency(package_name="asyncddgs", description="异步DuckDuckGo搜索库", optional=False),
PythonDependency(
package_name="exa_py",
description="Exa搜索API客户端库",
optional=True, # 如果没有API密钥这个是可选的
),
PythonDependency(
package_name="tavily",
install_name="tavily-python", # 安装时使用这个名称
description="Tavily搜索API客户端库",
optional=True, # 如果没有API密钥这个是可选的
),
PythonDependency(
package_name="httpx",
version=">=0.20.0",
install_name="httpx[socks]", # 安装时使用这个名称(包含可选依赖)
description="支持SOCKS代理的HTTP客户端库",
optional=False,
),
]
config_file_name: str = "config.toml" # 配置文件名 config_file_name: str = "config.toml" # 配置文件名
# 配置节描述 # 配置节描述

View File

@@ -13,9 +13,9 @@ from src.plugin_system.apis import config_api
from ..engines.bing_engine import BingSearchEngine from ..engines.bing_engine import BingSearchEngine
from ..engines.ddg_engine import DDGSearchEngine from ..engines.ddg_engine import DDGSearchEngine
from ..engines.exa_engine import ExaSearchEngine from ..engines.exa_engine import ExaSearchEngine
from ..engines.metaso_engine import MetasoSearchEngine
from ..engines.searxng_engine import SearXNGSearchEngine from ..engines.searxng_engine import SearXNGSearchEngine
from ..engines.tavily_engine import TavilySearchEngine from ..engines.tavily_engine import TavilySearchEngine
from ..engines.metaso_engine import MetasoSearchEngine
from ..utils.formatters import deduplicate_results, format_search_results from ..utils.formatters import deduplicate_results, format_search_results
logger = get_logger("web_search_tool") logger = get_logger("web_search_tool")

View File

@@ -1,5 +1,5 @@
[inner] [inner]
version = "7.4.5" version = "7.3.5"
#----以下是给开发人员阅读的如果你只是部署了MoFox-Bot不需要阅读---- #----以下是给开发人员阅读的如果你只是部署了MoFox-Bot不需要阅读----
#如果你想要修改配置文件请递增version的值 #如果你想要修改配置文件请递增version的值
@@ -485,6 +485,44 @@ searxng_api_keys = []# SearXNG 实例 API 密钥列表
enabled_engines = ["ddg"] # 启用的搜索引擎列表,可选: "exa", "tavily", "ddg","bing", "metaso" enabled_engines = ["ddg"] # 启用的搜索引擎列表,可选: "exa", "tavily", "ddg","bing", "metaso"
search_strategy = "single" # 搜索策略: "single"(使用第一个可用引擎), "parallel"(并行使用所有启用的引擎), "fallback"(按顺序尝试,失败则尝试下一个) search_strategy = "single" # 搜索策略: "single"(使用第一个可用引擎), "parallel"(并行使用所有启用的引擎), "fallback"(按顺序尝试,失败则尝试下一个)
[sleep_system]
enable = false #"是否启用睡眠系统"
sleep_by_schedule = true #"是否根据日程表进行睡觉"
fixed_sleep_time = "23:00" #"固定的睡觉时间"
fixed_wake_up_time = "07:00" #"固定的起床时间"
sleep_time_offset_minutes = 15 #"睡觉时间随机偏移量范围(分钟),实际睡觉时间会在±该值范围内随机"
wake_up_time_offset_minutes = 15 #"起床时间随机偏移量范围(分钟),实际起床时间会在±该值范围内随机"
wakeup_threshold = 15.0 #唤醒阈值,达到此值时会被唤醒"
private_message_increment = 3.0 #"私聊消息增加的唤醒度"
group_mention_increment = 2.0 #"群聊艾特增加的唤醒度"
decay_rate = 0.2 #"每次衰减的唤醒度数值"
decay_interval = 30.0 #"唤醒度衰减间隔(秒)"
angry_duration = 300.0 #"愤怒状态持续时间(秒)"
angry_prompt = "你被人吵醒了非常生气,说话带着怒气" # "被吵醒后的愤怒提示词"
re_sleep_delay_minutes = 5 # "被唤醒后,如果多久没有新消息则尝试重新入睡(分钟)"
# --- 失眠机制相关参数 ---
enable_insomnia_system = false # 是否启用失眠系统
# 失眠概率 (0.0 to 1.0)
insomnia_chance_pressure = 0.1
# --- 弹性睡眠与睡前消息 ---
# 是否启用弹性睡眠。启用后AI不会到点立刻入睡而是会根据睡眠压力增加5-10分钟的缓冲并可能因为压力不足而推迟睡眠。
enable_flexible_sleep = false
# 触发弹性睡眠的睡眠压力阈值。当AI的睡眠压力低于此值时可能会推迟入睡。
flexible_sleep_pressure_threshold = 40.0
# 每日最大可推迟入睡的总分钟数。
max_sleep_delay_minutes = 60
# 是否在进入“准备入睡”状态时发送一条消息通知。
enable_pre_sleep_notification = false
# 用于生成睡前消息的提示。AI会根据这个提示生成一句晚安问候。
pre_sleep_prompt = "我准备睡觉了,请生成一句简短自然的晚安问候。"
insomnia_duration_minutes = [30, 60] # 单次失眠状态的持续时间范围(分钟)
# --- 睡后失眠 ---
# 入睡后,经过一段延迟后触发失眠判定的延迟时间(分钟),设置为范围以增加随机性
insomnia_trigger_delay_minutes = [15, 45]
[cross_context] # 跨群聊/私聊上下文共享配置 [cross_context] # 跨群聊/私聊上下文共享配置
# 这是总开关,用于一键启用或禁用此功能 # 这是总开关,用于一键启用或禁用此功能
enable = true enable = true