diff --git a/src/chat/chat_loop/cycle_processor.py b/src/chat/chat_loop/cycle_processor.py index 913111bdc..3bb585697 100644 --- a/src/chat/chat_loop/cycle_processor.py +++ b/src/chat/chat_loop/cycle_processor.py @@ -206,7 +206,7 @@ class CycleProcessor: # 触发规划前事件 result = await event_manager.trigger_event( - EventType.ON_PLAN, plugin_name="SYSTEM", stream_id=self.context.chat_stream + EventType.ON_PLAN, permission_group="SYSTEM", stream_id=self.context.chat_stream ) if not result.all_continue_process(): raise UserWarning(f"插件{result.get_summary().get('stopped_handlers', '')}于规划前中断了内容生成") diff --git a/src/chat/frequency_analyzer/analyzer.py b/src/chat/frequency_analyzer/analyzer.py new file mode 100644 index 000000000..bd6331465 --- /dev/null +++ b/src/chat/frequency_analyzer/analyzer.py @@ -0,0 +1,144 @@ +""" +Chat Frequency Analyzer +======================= + +本模块负责分析用户的聊天时间戳,以识别出他们最活跃的聊天时段(高峰时段)。 + +核心功能: +- 使用滑动窗口算法来检测时间戳集中的区域。 +- 提供接口查询指定用户当前是否处于其聊天高峰时段内。 +- 结果会被缓存以提高性能。 + +可配置参数: +- ANALYSIS_WINDOW_HOURS: 用于分析的时间窗口大小(小时)。 +- MIN_CHATS_FOR_PEAK: 在一个窗口内需要多少次聊天才能被认为是高峰时段。 +- MIN_GAP_BETWEEN_PEAKS_HOURS: 两个独立高峰时段之间的最小间隔(小时)。 +""" +import time as time_module +from datetime import datetime, timedelta, time +from typing import List, Tuple, Optional + +from .tracker import chat_frequency_tracker + +# --- 可配置参数 --- +# 用于分析的时间窗口大小(小时) +ANALYSIS_WINDOW_HOURS = 2 +# 触发高峰时段所需的最小聊天次数 +MIN_CHATS_FOR_PEAK = 4 +# 两个独立高峰时段之间的最小间隔(小时) +MIN_GAP_BETWEEN_PEAKS_HOURS = 1 + + +class ChatFrequencyAnalyzer: + """ + 分析聊天时间戳,以识别用户的高频聊天时段。 + """ + + def __init__(self): + # 缓存分析结果,避免重复计算 + # 格式: { "chat_id": (timestamp_of_analysis, [peak_windows]) } + self._analysis_cache: dict[str, tuple[float, list[tuple[time, time]]]] = {} + self._cache_ttl_seconds = 60 * 30 # 缓存30分钟 + + def _find_peak_windows(self, timestamps: List[float]) -> List[Tuple[datetime, datetime]]: + """ + 使用滑动窗口算法来识别时间戳列表中的高峰时段。 + + Args: + timestamps (List[float]): 按时间排序的聊天时间戳。 + + Returns: + List[Tuple[datetime, datetime]]: 识别出的高峰时段列表,每个元组代表一个时间窗口的开始和结束。 + """ + if len(timestamps) < MIN_CHATS_FOR_PEAK: + return [] + + # 将时间戳转换为 datetime 对象 + datetimes = [datetime.fromtimestamp(ts) for ts in timestamps] + datetimes.sort() + + peak_windows: List[Tuple[datetime, datetime]] = [] + window_start_idx = 0 + + for i in range(len(datetimes)): + # 移动窗口的起始点 + while datetimes[i] - datetimes[window_start_idx] > timedelta(hours=ANALYSIS_WINDOW_HOURS): + window_start_idx += 1 + + # 检查当前窗口是否满足高峰条件 + if i - window_start_idx + 1 >= MIN_CHATS_FOR_PEAK: + current_window_start = datetimes[window_start_idx] + current_window_end = datetimes[i] + + # 合并重叠或相邻的高峰时段 + if peak_windows and current_window_start - peak_windows[-1][1] < timedelta(hours=MIN_GAP_BETWEEN_PEAKS_HOURS): + # 扩展上一个窗口的结束时间 + peak_windows[-1] = (peak_windows[-1][0], current_window_end) + else: + peak_windows.append((current_window_start, current_window_end)) + + return peak_windows + + def get_peak_chat_times(self, chat_id: str) -> List[Tuple[time, time]]: + """ + 获取指定用户的高峰聊天时间段。 + + Args: + chat_id (str): 聊天标识符。 + + Returns: + List[Tuple[time, time]]: 高峰时段的列表,每个元组包含开始和结束时间 (time 对象)。 + """ + # 检查缓存 + cached_timestamp, cached_windows = self._analysis_cache.get(chat_id, (0, [])) + if time_module.time() - cached_timestamp < self._cache_ttl_seconds: + return cached_windows + + timestamps = chat_frequency_tracker.get_timestamps_for_chat(chat_id) + if not timestamps: + return [] + + peak_datetime_windows = self._find_peak_windows(timestamps) + + # 将 datetime 窗口转换为 time 窗口,并进行归一化处理 + peak_time_windows = [] + for start_dt, end_dt in peak_datetime_windows: + # TODO:这里可以添加更复杂的逻辑来处理跨天的平均时间 + # 为简化,我们直接使用窗口的起止时间 + peak_time_windows.append((start_dt.time(), end_dt.time())) + + # 更新缓存 + self._analysis_cache[chat_id] = (time_module.time(), peak_time_windows) + + return peak_time_windows + + def is_in_peak_time(self, chat_id: str, now: Optional[datetime] = None) -> bool: + """ + 检查当前时间是否处于用户的高峰聊天时段内。 + + Args: + chat_id (str): 聊天标识符。 + now (Optional[datetime]): 要检查的时间,默认为当前时间。 + + Returns: + bool: 如果处于高峰时段则返回 True,否则返回 False。 + """ + if now is None: + now = datetime.now() + + now_time = now.time() + peak_times = self.get_peak_chat_times(chat_id) + + for start_time, end_time in peak_times: + if start_time <= end_time: # 同一天 + if start_time <= now_time <= end_time: + return True + else: # 跨天 + if now_time >= start_time or now_time <= end_time: + return True + + return False + + +# 创建一个全局单例 +chat_frequency_analyzer = ChatFrequencyAnalyzer() diff --git a/src/chat/frequency_analyzer/tracker.py b/src/chat/frequency_analyzer/tracker.py new file mode 100644 index 000000000..bee9e4623 --- /dev/null +++ b/src/chat/frequency_analyzer/tracker.py @@ -0,0 +1,77 @@ +import orjson +import time +from typing import Dict, List, Optional +from pathlib import Path + +from src.common.logger import get_logger + +# 数据存储路径 +DATA_DIR = Path("data/frequency_analyzer") +DATA_DIR.mkdir(parents=True, exist_ok=True) +TRACKER_FILE = DATA_DIR / "chat_timestamps.json" + +logger = get_logger("ChatFrequencyTracker") + + +class ChatFrequencyTracker: + """ + 负责跟踪和存储用户聊天启动时间戳。 + """ + + def __init__(self): + self._timestamps: Dict[str, List[float]] = self._load_timestamps() + + def _load_timestamps(self) -> Dict[str, List[float]]: + """从本地文件加载时间戳数据。""" + if not TRACKER_FILE.exists(): + return {} + try: + with open(TRACKER_FILE, "rb") as f: + data = orjson.loads(f.read()) + logger.info(f"成功从 {TRACKER_FILE} 加载了聊天时间戳数据。") + return data + except orjson.JSONDecodeError: + logger.warning(f"无法解析 {TRACKER_FILE},将创建一个新的空数据文件。") + return {} + except Exception as e: + logger.error(f"加载聊天时间戳数据时发生未知错误: {e}") + return {} + + def _save_timestamps(self): + """将当前的时间戳数据保存到本地文件。""" + try: + with open(TRACKER_FILE, "wb") as f: + f.write(orjson.dumps(self._timestamps)) + except Exception as e: + logger.error(f"保存聊天时间戳数据到 {TRACKER_FILE} 时失败: {e}") + + def record_chat_start(self, chat_id: str): + """ + 记录一次聊天会话的开始。 + + Args: + chat_id (str): 唯一的聊天标识符 (例如,用户ID)。 + """ + now = time.time() + if chat_id not in self._timestamps: + self._timestamps[chat_id] = [] + + self._timestamps[chat_id].append(now) + logger.debug(f"为 chat_id '{chat_id}' 记录了新的聊天时间: {now}") + self._save_timestamps() + + def get_timestamps_for_chat(self, chat_id: str) -> Optional[List[float]]: + """ + 获取指定聊天的所有时间戳记录。 + + Args: + chat_id (str): 聊天标识符。 + + Returns: + Optional[List[float]]: 时间戳列表,如果不存在则返回 None。 + """ + return self._timestamps.get(chat_id) + + +# 创建一个全局单例 +chat_frequency_tracker = ChatFrequencyTracker() diff --git a/src/chat/frequency_analyzer/trigger.py b/src/chat/frequency_analyzer/trigger.py new file mode 100644 index 000000000..a6b4d8a3b --- /dev/null +++ b/src/chat/frequency_analyzer/trigger.py @@ -0,0 +1,119 @@ +""" +Frequency-Based Proactive Trigger +================================= + +本模块实现了一个周期性任务,用于根据用户的聊天频率来智能地触发主动思考。 + +核心功能: +- 定期运行,检查所有已知的私聊用户。 +- 调用 ChatFrequencyAnalyzer 判断当前是否处于用户的高峰聊天时段。 +- 如果满足条件(高峰时段、角色清醒、聊天循环空闲),则触发一次主动思考。 +- 包含冷却机制,以避免在同一个高峰时段内重复打扰用户。 + +可配置参数: +- TRIGGER_CHECK_INTERVAL_SECONDS: 触发器检查的周期(秒)。 +- COOLDOWN_HOURS: 在同一个高峰时段内触发一次后的冷却时间(小时)。 +""" +import asyncio +import time +from datetime import datetime +from typing import Dict, Optional + +from src.common.logger import get_logger +from src.chat.chat_loop.proactive.events import ProactiveTriggerEvent +from src.chat.heart_flow.heartflow import heartflow +from src.chat.chat_loop.sleep_manager.sleep_manager import SleepManager +from .analyzer import chat_frequency_analyzer + +logger = get_logger("FrequencyBasedTrigger") + +# --- 可配置参数 --- +# 触发器检查周期(秒) +TRIGGER_CHECK_INTERVAL_SECONDS = 60 * 5 # 5分钟 +# 冷却时间(小时),确保在一个高峰时段只触发一次 +COOLDOWN_HOURS = 3 + + +class FrequencyBasedTrigger: + """ + 一个周期性任务,根据聊天频率分析结果来触发主动思考。 + """ + + def __init__(self, sleep_manager: SleepManager): + self._sleep_manager = sleep_manager + self._task: Optional[asyncio.Task] = None + # 记录上次为用户触发的时间,用于冷却控制 + # 格式: { "chat_id": timestamp } + self._last_triggered: Dict[str, float] = {} + + async def _run_trigger_cycle(self): + """触发器的主要循环逻辑。""" + while True: + try: + await asyncio.sleep(TRIGGER_CHECK_INTERVAL_SECONDS) + logger.debug("开始执行频率触发器检查...") + + # 1. 检查角色是否清醒 + if self._sleep_manager.is_sleeping(): + logger.debug("角色正在睡眠,跳过本次频率触发检查。") + continue + + # 2. 获取所有已知的聊天ID + # 【注意】这里我们假设所有 subheartflow 的 ID 就是 chat_id + all_chat_ids = list(heartflow.subheartflows.keys()) + if not all_chat_ids: + continue + + now = datetime.now() + + for chat_id in all_chat_ids: + # 3. 检查是否处于冷却时间内 + last_triggered_time = self._last_triggered.get(chat_id, 0) + if time.time() - last_triggered_time < COOLDOWN_HOURS * 3600: + continue + + # 4. 检查当前是否是该用户的高峰聊天时间 + if chat_frequency_analyzer.is_in_peak_time(chat_id, now): + + sub_heartflow = await heartflow.get_or_create_subheartflow(chat_id) + if not sub_heartflow: + logger.warning(f"无法为 {chat_id} 获取或创建 sub_heartflow。") + continue + + # 5. 检查用户当前是否已有活跃的思考或回复任务 + cycle_detail = sub_heartflow.heart_fc_instance.context.current_cycle_detail + if cycle_detail and not cycle_detail.end_time: + logger.debug(f"用户 {chat_id} 的聊天循环正忙(仍在周期 {cycle_detail.cycle_id} 中),本次不触发。") + continue + + logger.info(f"检测到用户 {chat_id} 处于聊天高峰期,且聊天循环空闲,准备触发主动思考。") + + # 6. 直接调用 proactive_thinker + event = ProactiveTriggerEvent( + source="frequency_analyzer", + reason=f"User is in a high-frequency chat period." + ) + await sub_heartflow.heart_fc_instance.proactive_thinker.think(event) + + # 7. 更新触发时间,进入冷却 + self._last_triggered[chat_id] = time.time() + + except asyncio.CancelledError: + logger.info("频率触发器任务被取消。") + break + except Exception as e: + logger.error(f"频率触发器循环发生未知错误: {e}", exc_info=True) + # 发生错误后,等待更长时间再重试,避免刷屏 + await asyncio.sleep(TRIGGER_CHECK_INTERVAL_SECONDS * 2) + + def start(self): + """启动触发器任务。""" + if self._task is None or self._task.done(): + self._task = asyncio.create_task(self._run_trigger_cycle()) + logger.info("基于聊天频率的主动思考触发器已启动。") + + def stop(self): + """停止触发器任务。""" + if self._task and not self._task.done(): + self._task.cancel() + logger.info("基于聊天频率的主动思考触发器已停止。") diff --git a/src/chat/message_receive/bot.py b/src/chat/message_receive/bot.py index 260a42170..67c56be2a 100644 --- a/src/chat/message_receive/bot.py +++ b/src/chat/message_receive/bot.py @@ -450,7 +450,7 @@ class ChatBot: logger.info(f"命令处理完成,跳过后续消息处理: {cmd_result}") return - result = await event_manager.trigger_event(EventType.ON_MESSAGE, plugin_name="SYSTEM", message=message) + result = await event_manager.trigger_event(EventType.ON_MESSAGE, permission_group="SYSTEM", message=message) if not result.all_continue_process(): raise UserWarning(f"插件{result.get_summary().get('stopped_handlers', '')}于消息到达时取消了消息处理") diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index b98fcf87e..49871b78a 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -156,6 +156,7 @@ def init_prompt(): Prompt( """ {name_block} +{personality_block} {chat_context_description},{time_block},现在请你根据以下聊天内容,选择一个或多个合适的action。如果没有合适的action,请选择no_action。, {chat_content_block} @@ -423,6 +424,19 @@ class ActionPlanner: bot_nickname = f",也有人叫你{','.join(global_config.bot.alias_names)}" if global_config.bot.alias_names else "" name_block = f"你的名字是{bot_name}{bot_nickname},请注意哪些是你自己的发言。" + # 构建人格信息块(仅在启用时) + personality_block = "" + if global_config.chat.include_personality: + personality_core = global_config.personality.personality_core + personality_side = global_config.personality.personality_side + if personality_core or personality_side: + personality_parts = [] + if personality_core: + personality_parts.append(f"核心人格:{personality_core}") + if personality_side: + personality_parts.append(f"人格侧面:{personality_side}") + personality_block = "你的人格特征是:" + ";".join(personality_parts) + planner_prompt_template = await global_prompt_manager.get_prompt_async("sub_planner_prompt") prompt = planner_prompt_template.format( time_block=time_block, @@ -432,6 +446,7 @@ class ActionPlanner: action_options_text=action_options_block, moderation_prompt=moderation_prompt_block, name_block=name_block, + personality_block=personality_block, ) except Exception as e: logger.error(f"构建小脑提示词时出错: {e}\n{traceback.format_exc()}") diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index 3c71ef1d2..7045b60e6 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -286,7 +286,7 @@ class DefaultReplyer: # 触发 POST_LLM 事件(请求 LLM 之前) if not from_plugin: result = await event_manager.trigger_event( - EventType.POST_LLM, plugin_name="SYSTEM", prompt=prompt, stream_id=stream_id + EventType.POST_LLM, permission_group="SYSTEM", prompt=prompt, stream_id=stream_id ) if not result.all_continue_process(): raise UserWarning(f"插件{result.get_summary().get('stopped_handlers', '')}于请求前中断了内容生成") @@ -310,7 +310,7 @@ class DefaultReplyer: if not from_plugin: result = await event_manager.trigger_event( EventType.AFTER_LLM, - plugin_name="SYSTEM", + permission_group="SYSTEM", prompt=prompt, llm_response=llm_response, stream_id=stream_id, diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 10bc3ae71..5a3b6fd3f 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -93,6 +93,7 @@ class ChatConfig(ValidatedConfigBase): ) delta_sigma: int = Field(default=120, description="采用正态分布随机时间间隔") planner_size: float = Field(default=5.0, ge=1.0, description="小脑(sub-planner)的尺寸,决定每个小脑处理多少个action") + include_personality: bool = Field(default=False, description="是否在小脑决策中包含角色人设信息") def get_current_talk_frequency(self, chat_stream_id: Optional[str] = None) -> float: """ diff --git a/src/main.py b/src/main.py index f641c54e4..2b6fdfd10 100644 --- a/src/main.py +++ b/src/main.py @@ -116,7 +116,7 @@ class MainSystem: # 停止消息重组器 from src.plugin_system.core.event_manager import event_manager from src.plugin_system import EventType - asyncio.run(event_manager.trigger_event(EventType.ON_STOP,plugin_name="SYSTEM")) + asyncio.run(event_manager.trigger_event(EventType.ON_STOP,permission_group="SYSTEM")) from src.utils.message_chunker import reassembler import asyncio @@ -290,7 +290,7 @@ MoFox_Bot(第三方修改版) logger.info("日程表管理器初始化成功。") try: - await event_manager.trigger_event(EventType.ON_START, plugin_name="SYSTEM") + await event_manager.trigger_event(EventType.ON_START, permission_group="SYSTEM") init_time = int(1000 * (time.time() - init_start_time)) logger.info(f"初始化完成,神经元放电{init_time}次") except Exception as e: diff --git a/src/plugin_system/base/plus_command.py b/src/plugin_system/base/plus_command.py index f4e9fb364..ffa02baf1 100644 --- a/src/plugin_system/base/plus_command.py +++ b/src/plugin_system/base/plus_command.py @@ -126,7 +126,7 @@ class PlusCommand(ABC): return True # 检查是否为群聊消息 - is_group = hasattr(self.message, "is_group_message") and self.message.is_group_message + is_group = hasattr(self.message.message_info, "group_info") and self.message.message_info.group_info if self.chat_type_allow == ChatType.GROUP and is_group: return True diff --git a/src/plugin_system/core/event_manager.py b/src/plugin_system/core/event_manager.py index a69fb01c0..f359409af 100644 --- a/src/plugin_system/core/event_manager.py +++ b/src/plugin_system/core/event_manager.py @@ -289,7 +289,7 @@ class EventManager: return {handler.handler_name: handler for handler in event.subscribers} async def trigger_event( - self, event_name: Union[EventType, str], plugin_name: Optional[str] = "", **kwargs + self, event_name: Union[EventType, str], permission_group: Optional[str] = "", **kwargs ) -> Optional[HandlerResultsCollection]: """触发指定事件 @@ -309,11 +309,11 @@ class EventManager: return None # 插件白名单检查 - if event.allowed_triggers and not plugin_name: + if event.allowed_triggers and not permission_group: logger.warning(f"事件 {event_name} 存在触发者白名单,缺少plugin_name无法验证权限,已拒绝触发!") return None - elif event.allowed_triggers and plugin_name not in event.allowed_triggers: - logger.warning(f"插件 {plugin_name} 没有权限触发事件 {event_name},已拒绝触发!") + elif event.allowed_triggers and permission_group not in event.allowed_triggers: + logger.warning(f"插件 {permission_group} 没有权限触发事件 {event_name},已拒绝触发!") return None return await event.activate(params) diff --git a/src/plugins/built_in/at_user_plugin/plugin.py b/src/plugins/built_in/at_user_plugin/plugin.py index 8329480a7..7a80b8ab6 100644 --- a/src/plugins/built_in/at_user_plugin/plugin.py +++ b/src/plugins/built_in/at_user_plugin/plugin.py @@ -85,7 +85,7 @@ class AtAction(BaseAction): reply_to=reply_to, extra_info=extra_info, enable_tool=False, # 艾特回复通常不需要工具调用 - from_plugin=True # 标识来自插件 + from_plugin=False ) if success and llm_response: diff --git a/src/plugins/built_in/napcat_adapter_plugin/CONSTS.py b/src/plugins/built_in/napcat_adapter_plugin/CONSTS.py index 35717d005..a834c0fd6 100644 --- a/src/plugins/built_in/napcat_adapter_plugin/CONSTS.py +++ b/src/plugins/built_in/napcat_adapter_plugin/CONSTS.py @@ -1 +1,254 @@ PLUGIN_NAME = "napcat_adapter" + +QQ_FACE: dict = { + "0": "[表情:惊讶]", + "1": "[表情:撇嘴]", + "2": "[表情:色]", + "3": "[表情:发呆]", + "4": "[表情:得意]", + "5": "[表情:流泪]", + "6": "[表情:害羞]", + "7": "[表情:闭嘴]", + "8": "[表情:睡]", + "9": "[表情:大哭]", + "10": "[表情:尴尬]", + "11": "[表情:发怒]", + "12": "[表情:调皮]", + "13": "[表情:呲牙]", + "14": "[表情:微笑]", + "15": "[表情:难过]", + "16": "[表情:酷]", + "18": "[表情:抓狂]", + "19": "[表情:吐]", + "20": "[表情:偷笑]", + "21": "[表情:可爱]", + "22": "[表情:白眼]", + "23": "[表情:傲慢]", + "24": "[表情:饥饿]", + "25": "[表情:困]", + "26": "[表情:惊恐]", + "27": "[表情:流汗]", + "28": "[表情:憨笑]", + "29": "[表情:悠闲]", + "30": "[表情:奋斗]", + "31": "[表情:咒骂]", + "32": "[表情:疑问]", + "33": "[表情: 嘘]", + "34": "[表情:晕]", + "35": "[表情:折磨]", + "36": "[表情:衰]", + "37": "[表情:骷髅]", + "38": "[表情:敲打]", + "39": "[表情:再见]", + "41": "[表情:发抖]", + "42": "[表情:爱情]", + "43": "[表情:跳跳]", + "46": "[表情:猪头]", + "49": "[表情:拥抱]", + "53": "[表情:蛋糕]", + "56": "[表情:刀]", + "59": "[表情:便便]", + "60": "[表情:咖啡]", + "63": "[表情:玫瑰]", + "64": "[表情:凋谢]", + "66": "[表情:爱心]", + "67": "[表情:心碎]", + "74": "[表情:太阳]", + "75": "[表情:月亮]", + "76": "[表情:赞]", + "77": "[表情:踩]", + "78": "[表情:握手]", + "79": "[表情:胜利]", + "85": "[表情:飞吻]", + "86": "[表情:怄火]", + "89": "[表情:西瓜]", + "96": "[表情:冷汗]", + "97": "[表情:擦汗]", + "98": "[表情:抠鼻]", + "99": "[表情:鼓掌]", + "100": "[表情:糗大了]", + "101": "[表情:坏笑]", + "102": "[表情:左哼哼]", + "103": "[表情:右哼哼]", + "104": "[表情:哈欠]", + "105": "[表情:鄙视]", + "106": "[表情:委屈]", + "107": "[表情:快哭了]", + "108": "[表情:阴险]", + "109": "[表情:左亲亲]", + "110": "[表情:吓]", + "111": "[表情:可怜]", + "112": "[表情:菜刀]", + "114": "[表情:篮球]", + "116": "[表情:示爱]", + "118": "[表情:抱拳]", + "119": "[表情:勾引]", + "120": "[表情:拳头]", + "121": "[表情:差劲]", + "123": "[表情:NO]", + "124": "[表情:OK]", + "125": "[表情:转圈]", + "129": "[表情:挥手]", + "137": "[表情:鞭炮]", + "144": "[表情:喝彩]", + "146": "[表情:爆筋]", + "147": "[表情:棒棒糖]", + "169": "[表情:手枪]", + "171": "[表情:茶]", + "172": "[表情:眨眼睛]", + "173": "[表情:泪奔]", + "174": "[表情:无奈]", + "175": "[表情:卖萌]", + "176": "[表情:小纠结]", + "177": "[表情:喷血]", + "178": "[表情:斜眼笑]", + "179": "[表情:doge]", + "181": "[表情:戳一戳]", + "182": "[表情:笑哭]", + "183": "[表情:我最美]", + "185": "[表情:羊驼]", + "187": "[表情:幽灵]", + "201": "[表情:点赞]", + "212": "[表情:托腮]", + "262": "[表情:脑阔疼]", + "263": "[表情:沧桑]", + "264": "[表情:捂脸]", + "265": "[表情:辣眼睛]", + "266": "[表情:哦哟]", + "267": "[表情:头秃]", + "268": "[表情:问号脸]", + "269": "[表情:暗中观察]", + "270": "[表情:emm]", + "271": "[表情:吃 瓜]", + "272": "[表情:呵呵哒]", + "273": "[表情:我酸了]", + "277": "[表情:汪汪]", + "281": "[表情:无眼笑]", + "282": "[表情:敬礼]", + "283": "[表情:狂笑]", + "284": "[表情:面无表情]", + "285": "[表情:摸鱼]", + "286": "[表情:魔鬼笑]", + "287": "[表情:哦]", + "289": "[表情:睁眼]", + "293": "[表情:摸锦鲤]", + "294": "[表情:期待]", + "295": "[表情:拿到红包]", + "297": "[表情:拜谢]", + "298": "[表情:元宝]", + "299": "[表情:牛啊]", + "300": "[表情:胖三斤]", + "302": "[表情:左拜年]", + "303": "[表情:右拜年]", + "305": "[表情:右亲亲]", + "306": "[表情:牛气冲天]", + "307": "[表情:喵喵]", + "311": "[表情:打call]", + "312": "[表情:变形]", + "314": "[表情:仔细分析]", + "317": "[表情:菜汪]", + "318": "[表情:崇拜]", + "319": "[表情: 比心]", + "320": "[表情:庆祝]", + "323": "[表情:嫌弃]", + "324": "[表情:吃糖]", + "325": "[表情:惊吓]", + "326": "[表情:生气]", + "332": "[表情:举牌牌]", + "333": "[表情:烟花]", + "334": "[表情:虎虎生威]", + "336": "[表情:豹富]", + "337": "[表情:花朵脸]", + "338": "[表情:我想开了]", + "339": "[表情:舔屏]", + "341": "[表情:打招呼]", + "342": "[表情:酸Q]", + "343": "[表情:我方了]", + "344": "[表情:大怨种]", + "345": "[表情:红包多多]", + "346": "[表情:你真棒棒]", + "347": "[表情:大展宏兔]", + "349": "[表情:坚强]", + "350": "[表情:贴贴]", + "351": "[表情:敲敲]", + "352": "[表情:咦]", + "353": "[表情:拜托]", + "354": "[表情:尊嘟假嘟]", + "355": "[表情:耶]", + "356": "[表情:666]", + "357": "[表情:裂开]", + "392": "[表情:龙年 快乐]", + "393": "[表情:新年中龙]", + "394": "[表情:新年大龙]", + "395": "[表情:略略略]", + "396": "[表情:龙年快乐]", + "424": "[表情:按钮]", + "😊": "[表情:嘿嘿]", + "😌": "[表情:羞涩]", + "😚": "[ 表情:亲亲]", + "😓": "[表情:汗]", + "😰": "[表情:紧张]", + "😝": "[表情:吐舌]", + "😁": "[表情:呲牙]", + "😜": "[表情:淘气]", + "☺": "[表情:可爱]", + "😍": "[表情:花痴]", + "😔": "[表情:失落]", + "😄": "[表情:高兴]", + "😏": "[表情:哼哼]", + "😒": "[表情:不屑]", + "😳": "[表情:瞪眼]", + "😘": "[表情:飞吻]", + "😭": "[表情:大哭]", + "😱": "[表情:害怕]", + "😂": "[表情:激动]", + "💪": "[表情:肌肉]", + "👊": "[表情:拳头]", + "👍": "[表情 :厉害]", + "👏": "[表情:鼓掌]", + "👎": "[表情:鄙视]", + "🙏": "[表情:合十]", + "👌": "[表情:好的]", + "👆": "[表情:向上]", + "👀": "[表情:眼睛]", + "🍜": "[表情:拉面]", + "🍧": "[表情:刨冰]", + "🍞": "[表情:面包]", + "🍺": "[表情:啤酒]", + "🍻": "[表情:干杯]", + "☕": "[表情:咖啡]", + "🍎": "[表情:苹果]", + "🍓": "[表情:草莓]", + "🍉": "[表情:西瓜]", + "🚬": "[表情:吸烟]", + "🌹": "[表情:玫瑰]", + "🎉": "[表情:庆祝]", + "💝": "[表情:礼物]", + "💣": "[表情:炸弹]", + "✨": "[表情:闪光]", + "💨": "[表情:吹气]", + "💦": "[表情:水]", + "🔥": "[表情:火]", + "💤": "[表情:睡觉]", + "💩": "[表情:便便]", + "💉": "[表情:打针]", + "📫": "[表情:邮箱]", + "🐎": "[表情:骑马]", + "👧": "[表情:女孩]", + "👦": "[表情:男孩]", + "🐵": "[表情:猴]", + "🐷": "[表情:猪]", + "🐮": "[表情:牛]", + "🐔": "[表情:公鸡]", + "🐸": "[表情:青蛙]", + "👻": "[表情:幽灵]", + "🐛": "[表情:虫]", + "🐶": "[表情:狗]", + "🐳": "[表情:鲸鱼]", + "👢": "[表情:靴子]", + "☀": "[表情:晴天]", + "❔": "[表情:问号]", + "🔫": "[表情:手枪]", + "💓": "[表情:爱 心]", + "🏪": "[表情:便利店]", +} diff --git a/src/plugins/built_in/napcat_adapter_plugin/event_handlers.py b/src/plugins/built_in/napcat_adapter_plugin/event_handlers.py index 1e5fbd531..78d94363e 100644 --- a/src/plugins/built_in/napcat_adapter_plugin/event_handlers.py +++ b/src/plugins/built_in/napcat_adapter_plugin/event_handlers.py @@ -1,3 +1,5 @@ +import orjson + from src.plugin_system import BaseEventHandler from src.plugin_system.base.base_event import HandlerResult diff --git a/src/plugins/built_in/napcat_adapter_plugin/event_types.py b/src/plugins/built_in/napcat_adapter_plugin/event_types.py index af417f37a..08ef35598 100644 --- a/src/plugins/built_in/napcat_adapter_plugin/event_types.py +++ b/src/plugins/built_in/napcat_adapter_plugin/event_types.py @@ -35,6 +35,8 @@ class NapcatEvent: """接收到魔法猜拳消息""" FRIEND_INPUT = "napcat_on_friend_input" """好友正在输入""" + EMOJI_LIEK = "napcat_on_received_emoji_like" + """接收到群聊表情回复""" class ACCOUNT(Enum): """ @@ -682,7 +684,7 @@ class NapcatEvent: GET_MSG = "napcat_get_msg" """获取消息详情 - Args: + Args: message_id (Optional[str|int]): 消息id(必需) raw (Optional[dict]): 原始请求体 diff --git a/src/plugins/built_in/napcat_adapter_plugin/plugin.py b/src/plugins/built_in/napcat_adapter_plugin/plugin.py index 966edd19c..5059a7fb6 100644 --- a/src/plugins/built_in/napcat_adapter_plugin/plugin.py +++ b/src/plugins/built_in/napcat_adapter_plugin/plugin.py @@ -297,8 +297,8 @@ class NapcatAdapterPlugin(BasePlugin): config_schema: dict = { "plugin": { "name": ConfigField(type=str, default="napcat_adapter_plugin", description="插件名称"), - "version": ConfigField(type=str, default="1.0.0", description="插件版本"), - "config_version": ConfigField(type=str, default="1.3.0", description="配置文件版本"), + "version": ConfigField(type=str, default="1.1.0", description="插件版本"), + "config_version": ConfigField(type=str, default="1.3.1", description="配置文件版本"), "enabled": ConfigField(type=bool, default=False, description="是否启用插件"), }, "inner": { @@ -345,6 +345,7 @@ class NapcatAdapterPlugin(BasePlugin): "poke_debounce_seconds": ConfigField(type=int, default=3, description="戳一戳防抖时间(秒),在指定时间内第二次针对机器人的戳一戳将被忽略"), "enable_reply_at": ConfigField(type=bool, default=True, description="是否启用引用回复时艾特用户的功能"), "reply_at_rate": ConfigField(type=float, default=0.5, description="引用回复时艾特用户的几率 (0.0 ~ 1.0)"), + "enable_emoji_like": ConfigField(type=bool, default=True, description="是否启用群聊表情回复功能"), # 视频处理设置 "enable_video_analysis": ConfigField(type=bool, default=True, description="是否启用视频识别功能"), diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/__init__.py b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/__init__.py index 1b25ca14e..48561ffbe 100644 --- a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/__init__.py +++ b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/__init__.py @@ -32,6 +32,7 @@ class NoticeType: # 通知事件 group_recall = "group_recall" # 群聊消息撤回 notify = "notify" group_ban = "group_ban" # 群禁言 + group_msg_emoji_like = "group_msg_emoji_like" # 群聊表情回复 class Notify: poke = "poke" # 戳一戳 diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/message_handler.py b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/message_handler.py index 1c276ce41..0a644345b 100644 --- a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/message_handler.py +++ b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/message_handler.py @@ -385,7 +385,7 @@ class MessageHandler: ret_seg = await self.handle_text_message(sub_message) if ret_seg: await event_manager.trigger_event( - NapcatEvent.ON_RECEIVED.TEXT, plugin_name=PLUGIN_NAME, message_seg=ret_seg + NapcatEvent.ON_RECEIVED.TEXT, permission_group=PLUGIN_NAME, message_seg=ret_seg ) seg_message.append(ret_seg) else: @@ -394,7 +394,7 @@ class MessageHandler: ret_seg = await self.handle_face_message(sub_message) if ret_seg: await event_manager.trigger_event( - NapcatEvent.ON_RECEIVED.FACE, plugin_name=PLUGIN_NAME, message_seg=ret_seg + NapcatEvent.ON_RECEIVED.FACE, permission_group=PLUGIN_NAME, message_seg=ret_seg ) seg_message.append(ret_seg) else: @@ -404,7 +404,7 @@ class MessageHandler: ret_seg = await self.handle_reply_message(sub_message) if ret_seg: await event_manager.trigger_event( - NapcatEvent.ON_RECEIVED.REPLY, plugin_name=PLUGIN_NAME, message_seg=ret_seg + NapcatEvent.ON_RECEIVED.REPLY, permission_group=PLUGIN_NAME, message_seg=ret_seg ) seg_message += ret_seg else: @@ -414,7 +414,7 @@ class MessageHandler: ret_seg = await self.handle_image_message(sub_message) if ret_seg: await event_manager.trigger_event( - NapcatEvent.ON_RECEIVED.IMAGE, plugin_name=PLUGIN_NAME, message_seg=ret_seg + NapcatEvent.ON_RECEIVED.IMAGE, permission_group=PLUGIN_NAME, message_seg=ret_seg ) seg_message.append(ret_seg) logger.debug("图片处理成功,添加到消息段") @@ -425,7 +425,7 @@ class MessageHandler: ret_seg = await self.handle_record_message(sub_message) if ret_seg: await event_manager.trigger_event( - NapcatEvent.ON_RECEIVED.RECORD, plugin_name=PLUGIN_NAME, message_seg=ret_seg + NapcatEvent.ON_RECEIVED.RECORD, permission_group=PLUGIN_NAME, message_seg=ret_seg ) seg_message.clear() seg_message.append(ret_seg) @@ -437,7 +437,7 @@ class MessageHandler: ret_seg = await self.handle_video_message(sub_message) if ret_seg: await event_manager.trigger_event( - NapcatEvent.ON_RECEIVED.VIDEO, plugin_name=PLUGIN_NAME, message_seg=ret_seg + NapcatEvent.ON_RECEIVED.VIDEO, permission_group=PLUGIN_NAME, message_seg=ret_seg ) seg_message.append(ret_seg) else: @@ -451,7 +451,7 @@ class MessageHandler: ) if ret_seg: await event_manager.trigger_event( - NapcatEvent.ON_RECEIVED.AT, plugin_name=PLUGIN_NAME, message_seg=ret_seg + NapcatEvent.ON_RECEIVED.AT, permission_group=PLUGIN_NAME, message_seg=ret_seg ) seg_message.append(ret_seg) else: @@ -460,7 +460,7 @@ class MessageHandler: ret_seg = await self.handle_rps_message(sub_message) if ret_seg: await event_manager.trigger_event( - NapcatEvent.ON_RECEIVED.RPS, plugin_name=PLUGIN_NAME, message_seg=ret_seg + NapcatEvent.ON_RECEIVED.RPS, permission_group=PLUGIN_NAME, message_seg=ret_seg ) seg_message.append(ret_seg) else: @@ -469,7 +469,7 @@ class MessageHandler: ret_seg = await self.handle_dice_message(sub_message) if ret_seg: await event_manager.trigger_event( - NapcatEvent.ON_RECEIVED.DICE, plugin_name=PLUGIN_NAME, message_seg=ret_seg + NapcatEvent.ON_RECEIVED.DICE, permission_group=PLUGIN_NAME, message_seg=ret_seg ) seg_message.append(ret_seg) else: @@ -478,7 +478,7 @@ class MessageHandler: ret_seg = await self.handle_shake_message(sub_message) if ret_seg: await event_manager.trigger_event( - NapcatEvent.ON_RECEIVED.SHAKE, plugin_name=PLUGIN_NAME, message_seg=ret_seg + NapcatEvent.ON_RECEIVED.SHAKE, permission_group=PLUGIN_NAME, message_seg=ret_seg ) seg_message.append(ret_seg) else: @@ -507,7 +507,7 @@ class MessageHandler: ret_seg = await self.handle_json_message(sub_message) if ret_seg: await event_manager.trigger_event( - NapcatEvent.ON_RECEIVED.JSON, plugin_name=PLUGIN_NAME, message_seg=ret_seg + NapcatEvent.ON_RECEIVED.JSON, permission_group=PLUGIN_NAME, message_seg=ret_seg ) seg_message.append(ret_seg) else: diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/notice_handler.py b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/notice_handler.py index e3af0ea83..c373a9a10 100644 --- a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/notice_handler.py +++ b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/notice_handler.py @@ -24,7 +24,7 @@ from ..utils import ( read_ban_list, ) -from ...CONSTS import PLUGIN_NAME +from ...CONSTS import PLUGIN_NAME, QQ_FACE notice_queue: asyncio.Queue[MessageBase] = asyncio.Queue(maxsize=100) unsuccessful_notice_queue: asyncio.Queue[MessageBase] = asyncio.Queue(maxsize=3) @@ -127,9 +127,16 @@ class NoticeHandler: from src.plugin_system.core.event_manager import event_manager from ...event_types import NapcatEvent - await event_manager.trigger_event(NapcatEvent.ON_RECEIVED.FRIEND_INPUT, plugin_name=PLUGIN_NAME) + await event_manager.trigger_event(NapcatEvent.ON_RECEIVED.FRIEND_INPUT, permission_group=PLUGIN_NAME) case _: logger.warning(f"不支持的notify类型: {notice_type}.{sub_type}") + case NoticeType.group_msg_emoji_like: + # 该事件转移到 handle_group_emoji_like_notify函数内触发 + if config_api.get_plugin_config(self.plugin_config, "features.enable_emoji_like", True): + logger.debug("处理群聊表情回复") + handled_message, user_info = await self.handle_group_emoji_like_notify(raw_message,group_id,user_id) + else: + logger.warning("群聊表情回复被禁用,取消群聊表情回复处理") case NoticeType.group_ban: sub_type = raw_message.get("sub_type") match sub_type: @@ -284,6 +291,50 @@ class NoticeHandler: ) return seg_data, user_info + async def handle_group_emoji_like_notify(self, raw_message: dict, group_id: int, user_id: int): + if not group_id: + logger.error("群ID不能为空,无法处理群聊表情回复通知") + return None, None + + user_qq_info: dict = await get_member_info(self.get_server_connection(), group_id, user_id) + if user_qq_info: + user_name = user_qq_info.get("nickname") + user_cardname = user_qq_info.get("card") + else: + user_name = "QQ用户" + user_cardname = "QQ用户" + logger.debug("无法获取表情回复对方的用户昵称") + + from src.plugin_system.core.event_manager import event_manager + from ...event_types import NapcatEvent + + target_message = await event_manager.trigger_event(NapcatEvent.MESSAGE.GET_MSG,message_id=raw_message.get("message_id","")) + target_message_text = target_message.get_message_result().get("data",{}).get("raw_message","") + if not target_message: + logger.error("未找到对应消息") + return None, None + if len(target_message_text) > 15: + target_message_text = target_message_text[:15] + "..." + + user_info: UserInfo = UserInfo( + platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name", "qq"), + user_id=user_id, + user_nickname=user_name, + user_cardname=user_cardname, + ) + + like_emoji_id = raw_message.get("likes")[0].get("emoji_id") + await event_manager.trigger_event( + NapcatEvent.ON_RECEIVED.EMOJI_LIEK, + permission_group=PLUGIN_NAME, + group_id=group_id, + user_id=user_id, + message_id=raw_message.get("message_id",""), + emoji_id=like_emoji_id + ) + seg_data = Seg(type="text",data=f"{user_name}使用Emoji表情{QQ_FACE.get(like_emoji_id,"")}回复了你的消息[{target_message_text}]") + return seg_data, user_info + async def handle_ban_notify(self, raw_message: dict, group_id: int) -> Tuple[Seg, UserInfo] | Tuple[None, None]: if not group_id: logger.error("群ID不能为空,无法处理禁言通知") diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 6f6dd3c5e..55ef3af64 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.7.7" +version = "6.7.9" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -175,6 +175,8 @@ delta_sigma = 120 # 正态分布的标准差,控制时间间隔的随机程度 # --- 大脑/小脑 Planner 配置 --- planner_size = 5.0 # 小脑(sub-planner)的尺寸,决定每个小脑处理多少个action。数值越小,并行度越高,但单个小脑的上下文越少。建议范围:3.0-8.0 +include_personality = false # 是否在小脑决策中包含角色人设信息(personality_core、personality_side) + [relationship] enable_relationship = true # 是否启用关系系统 diff --git a/template/model_config_template.toml b/template/model_config_template.toml index c5f2a2947..611eb9b06 100644 --- a/template/model_config_template.toml +++ b/template/model_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "1.3.1" +version = "1.3.3" # 配置文件版本号迭代规则同bot_config.toml @@ -185,7 +185,7 @@ temperature = 0.7 max_tokens = 800 [model_task_config.schedule_generator]#日程表生成模型 -model_list = ["deepseek-v3"] +model_list = ["siliconflow-deepseek-v3"] temperature = 0.7 max_tokens = 1000 @@ -195,7 +195,7 @@ temperature = 0.1 # 低温度确保检测结果稳定 max_tokens = 200 # 检测结果不需要太长的输出 [model_task_config.monthly_plan_generator] # 月层计划生成模型 -model_list = ["deepseek-v3"] +model_list = ["siliconflow-deepseek-v3"] temperature = 0.7 max_tokens = 1000 diff --git a/test_plugin_config_fix.py b/test_plugin_config_fix.py deleted file mode 100644 index a5e6c77b0..000000000 --- a/test_plugin_config_fix.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python3 -""" -测试脚本用于验证LauchNapcatAdapterHandler的plugin_config修复 -""" - -import sys -import os -from pathlib import Path - -# 添加项目根目录到Python路径 -project_root = Path(__file__).parent -sys.path.insert(0, str(project_root)) - -from src.plugins.built_in.napcat_adapter_plugin.plugin import LauchNapcatAdapterHandler - -def test_plugin_config_fix(): - """测试plugin_config修复""" - print("测试LauchNapcatAdapterHandler的plugin_config修复...") - - # 创建测试配置 - test_config = { - "napcat_server": { - "mode": "reverse", - "host": "localhost", - "port": 8095 - }, - "maibot_server": { - "host": "localhost", - "port": 8000 - } - } - - # 创建处理器实例 - handler = LauchNapcatAdapterHandler() - - # 设置插件配置(模拟事件管理器注册时的行为) - handler.plugin_config = test_config - - print(f"设置的plugin_config: {handler.plugin_config}") - - # 测试配置访问 - if handler.plugin_config is not None and handler.plugin_config == test_config: - print("✅ plugin_config修复成功!") - print(f"✅ 可以正常访问配置: napcat_server.mode = {handler.plugin_config.get('napcat_server', {}).get('mode')}") - return True - else: - print("❌ plugin_config修复失败!") - print(f"❌ 当前plugin_config: {handler.plugin_config}") - return False - -if __name__ == "__main__": - success = test_plugin_config_fix() - sys.exit(0 if success else 1) \ No newline at end of file