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/chat_loop/cycle_tracker.py b/src/chat/chat_loop/cycle_tracker.py index 9f276383b..1f45c4caf 100644 --- a/src/chat/chat_loop/cycle_tracker.py +++ b/src/chat/chat_loop/cycle_tracker.py @@ -90,20 +90,20 @@ class CycleTracker: timer_strings.append(f"{name}: {formatted_time}") # 获取动作类型,兼容新旧格式 + # 获取动作类型 action_type = "未知动作" - if hasattr(self, "_current_cycle_detail") and self._current_cycle_detail: - loop_plan_info = self._current_cycle_detail.loop_plan_info - if isinstance(loop_plan_info, dict): - action_result = loop_plan_info.get("action_result", {}) - if isinstance(action_result, dict): - # 旧格式:action_result是字典 - action_type = action_result.get("action_type", "未知动作") - elif isinstance(action_result, list) and action_result: - # 新格式:action_result是actions列表 - action_type = action_result[0].get("action_type", "未知动作") - elif isinstance(loop_plan_info, list) and loop_plan_info: - # 直接是actions列表的情况 - action_type = loop_plan_info[0].get("action_type", "未知动作") + if self.context.current_cycle_detail: + loop_plan_info = self.context.current_cycle_detail.loop_plan_info + actions = loop_plan_info.get("action_result") + + if isinstance(actions, list) and actions: + # 从actions列表中提取所有action_type + action_types = [a.get("action_type", "未知") for a in actions] + action_type = ", ".join(action_types) + elif isinstance(actions, dict): + # 兼容旧格式 + action_type = actions.get("action_type", "未知动作") + if self.context.current_cycle_detail.end_time and self.context.current_cycle_detail.start_time: duration = self.context.current_cycle_detail.end_time - self.context.current_cycle_detail.start_time diff --git a/src/chat/chat_loop/proactive/event_scheduler.py b/src/chat/chat_loop/proactive/event_scheduler.py new file mode 100644 index 000000000..767360dd8 --- /dev/null +++ b/src/chat/chat_loop/proactive/event_scheduler.py @@ -0,0 +1,239 @@ +""" +事件驱动的智能调度器 +基于asyncio的精确定时事件调度系统,替代轮询机制 +""" + +import asyncio +import time +import traceback +from datetime import datetime, timedelta +from typing import Dict, Callable, Any, Optional +from dataclasses import dataclass +from src.common.logger import get_logger + +logger = get_logger("event_scheduler") + + +@dataclass +class ScheduledEvent: + """调度事件数据类""" + event_id: str + trigger_time: datetime + callback: Callable + metadata: Dict[str, Any] + task: Optional[asyncio.Task] = None + + +class EventDrivenScheduler: + """事件驱动的调度器""" + + def __init__(self): + self.scheduled_events: Dict[str, ScheduledEvent] = {} + self._shutdown = False + + async def schedule_event( + self, + event_id: str, + trigger_time: datetime, + callback: Callable, + metadata: Dict[str, Any] = None + ) -> bool: + """ + 调度一个事件在指定时间触发 + + Args: + event_id: 事件唯一标识 + trigger_time: 触发时间 + callback: 回调函数 + metadata: 事件元数据 + + Returns: + bool: 调度成功返回True + """ + try: + if metadata is None: + metadata = {} + + # 如果事件已存在,先取消 + if event_id in self.scheduled_events: + await self.cancel_event(event_id) + + # 计算延迟时间 + now = datetime.now() + delay = (trigger_time - now).total_seconds() + + if delay <= 0: + logger.warning(f"事件 {event_id} 的触发时间已过,立即执行") + # 立即执行 + asyncio.create_task(self._execute_callback(event_id, callback, metadata)) + return True + + # 创建调度事件 + scheduled_event = ScheduledEvent( + event_id=event_id, + trigger_time=trigger_time, + callback=callback, + metadata=metadata + ) + + # 创建异步任务 + scheduled_event.task = asyncio.create_task( + self._wait_and_execute(scheduled_event) + ) + + self.scheduled_events[event_id] = scheduled_event + logger.info(f"调度事件 {event_id} 将在 {trigger_time} 触发 (延迟 {delay:.1f} 秒)") + return True + + except Exception as e: + logger.error(f"调度事件失败: {e}") + return False + + async def _wait_and_execute(self, event: ScheduledEvent): + """等待并执行事件""" + try: + now = datetime.now() + delay = (event.trigger_time - now).total_seconds() + + if delay > 0: + await asyncio.sleep(delay) + + # 检查是否被取消 + if self._shutdown or event.event_id not in self.scheduled_events: + return + + # 执行回调 + await self._execute_callback(event.event_id, event.callback, event.metadata) + + except asyncio.CancelledError: + logger.info(f"事件 {event.event_id} 被取消") + except Exception as e: + logger.error(f"执行事件 {event.event_id} 时出错: {e}") + finally: + # 清理已完成的事件 + if event.event_id in self.scheduled_events: + del self.scheduled_events[event.event_id] + + async def _execute_callback(self, event_id: str, callback: Callable, metadata: Dict[str, Any]): + """执行回调函数""" + try: + logger.info(f"执行调度事件: {event_id}") + + # 根据回调函数签名调用 + if asyncio.iscoroutinefunction(callback): + await callback(metadata) + else: + callback(metadata) + + except Exception as e: + logger.error(f"执行回调函数失败: {e}") + logger.error(traceback.format_exc()) + + async def cancel_event(self, event_id: str) -> bool: + """ + 取消一个调度事件 + + Args: + event_id: 事件ID + + Returns: + bool: 取消成功返回True + """ + try: + if event_id in self.scheduled_events: + event = self.scheduled_events[event_id] + if event.task and not event.task.done(): + event.task.cancel() + del self.scheduled_events[event_id] + logger.info(f"取消调度事件: {event_id}") + return True + return False + except Exception as e: + logger.error(f"取消事件失败: {e}") + return False + + async def shutdown(self): + """关闭调度器,取消所有事件""" + self._shutdown = True + for event_id in list(self.scheduled_events.keys()): + await self.cancel_event(event_id) + logger.info("事件调度器已关闭") + + def get_scheduled_events(self) -> Dict[str, ScheduledEvent]: + """获取所有调度事件""" + return self.scheduled_events.copy() + + def get_event_count(self) -> int: + """获取调度事件数量""" + return len(self.scheduled_events) + + +# 全局事件调度器实例 +event_scheduler = EventDrivenScheduler() + + +# 便捷函数 +async def schedule_reminder( + reminder_id: str, + reminder_time: datetime, + chat_id: str, + reminder_content: str, + callback: Callable +): + """ + 调度提醒事件的便捷函数 + + Args: + reminder_id: 提醒唯一标识 + reminder_time: 提醒时间 + chat_id: 聊天ID + reminder_content: 提醒内容 + callback: 回调函数 + """ + metadata = { + "type": "reminder", + "chat_id": chat_id, + "content": reminder_content, + "created_at": datetime.now().isoformat() + } + + return await event_scheduler.schedule_event( + event_id=reminder_id, + trigger_time=reminder_time, + callback=callback, + metadata=metadata + ) + + +async def _execute_reminder_callback(subheartflow_id: str, reminder_text: str): + """执行提醒回调函数""" + try: + # 获取对应的subheartflow实例 + from src.chat.heart_flow.heartflow import heartflow + + subflow = await heartflow.get_or_create_subheartflow(subheartflow_id) + if not subflow: + logger.error(f"无法获取subheartflow实例: {subheartflow_id}") + return + + # 创建主动思考事件,触发完整的思考流程 + from src.chat.chat_loop.proactive.events import ProactiveTriggerEvent + + event = ProactiveTriggerEvent( + source="reminder_system", + reason=f"定时提醒:{reminder_text}", + metadata={ + "reminder_text": reminder_text, + "trigger_time": datetime.now().isoformat() + } + ) + + # 通过subflow的HeartFChatting实例触发主动思考 + await subflow.heart_fc_instance.proactive_thinker.think(event) + + logger.info(f"已触发提醒的主动思考,内容: {reminder_text}") + + except Exception as e: + logger.error(f"执行提醒回调时发生错误: {e}") + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/src/chat/chat_loop/proactive/events.py b/src/chat/chat_loop/proactive/events.py index c273afef1..89a3bc7bb 100644 --- a/src/chat/chat_loop/proactive/events.py +++ b/src/chat/chat_loop/proactive/events.py @@ -11,3 +11,4 @@ class ProactiveTriggerEvent: source: str # 触发源的标识,例如 "silence_monitor", "insomnia_manager" reason: str # 触发的具体原因,例如 "聊天已沉默10分钟", "深夜emo" metadata: Optional[Dict[str, Any]] = field(default_factory=dict) # 可选的元数据,用于传递额外信息 + related_message_id: Optional[str] = None # 关联的消息ID,用于加载上下文 diff --git a/src/chat/chat_loop/proactive/proactive_thinker.py b/src/chat/chat_loop/proactive/proactive_thinker.py index 3522c0dd4..7c736b51a 100644 --- a/src/chat/chat_loop/proactive/proactive_thinker.py +++ b/src/chat/chat_loop/proactive/proactive_thinker.py @@ -1,6 +1,7 @@ import time import traceback import orjson +import re from typing import TYPE_CHECKING, Dict, Any from src.common.logger import get_logger @@ -15,7 +16,8 @@ from src.plugin_system.base.component_types import ComponentType from src.config.config import global_config from src.chat.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat, build_readable_messages_with_id from src.mood.mood_manager import mood_manager -from src.common.database.sqlalchemy_database_api import store_action_info +from src.common.database.sqlalchemy_database_api import store_action_info, db_get +from src.common.database.sqlalchemy_models import Messages if TYPE_CHECKING: from ..cycle_processor import CycleProcessor @@ -118,69 +120,189 @@ class ProactiveThinker: trigger_event (ProactiveTriggerEvent): 触发事件。 """ try: - # 调用规划器的 PROACTIVE 模式,让其决定下一步的行动 - actions, _ = await self.cycle_processor.action_planner.plan(mode=ChatMode.PROACTIVE) + # 如果是提醒事件,跳过规划器,直接构建默认动作 + if trigger_event.source == "reminder_system": + # 1. 获取原始消息上下文 + action_message = {} + if trigger_event.related_message_id: + # 直接将从数据库获取的完整消息记录作为 action_message + action_message = await db_get( + Messages, {"message_id": trigger_event.related_message_id}, single_result=True + ) or {} - # 通常只关心规划出的第一个动作 - action_result = actions[0] if actions else {} + # 2. 智能确定@对象 + reason_text = trigger_event.reason.replace("定时提醒:", "").strip() + user_name_match = re.search(r"艾特一下(\S+)", reason_text) + + if user_name_match: + user_name = user_name_match.group(1) + at_message = reason_text.replace(f"艾特一下{user_name}", "").strip() + elif action_message.get("user_nickname"): + user_name = action_message.get("user_nickname") + at_message = reason_text + else: + user_name = "我" + at_message = reason_text - action_type = action_result.get("action_type") + # 3. 构建动作 + action_result = { + "action_type": "at_user", + "reasoning": "执行定时提醒", + "action_data": { + "user_name": user_name, + "at_message": at_message or "时间到啦!" + }, + "action_message": action_message + } + + # 4. 执行或回退 + try: + success, _, _ = await self.cycle_processor._handle_action( + action=action_result["action_type"], + reasoning=action_result["reasoning"], + action_data=action_result["action_data"], + cycle_timers={}, + thinking_id="", + action_message=action_result["action_message"] + ) + if not success: + raise Exception("at_user action failed") + except Exception: + logger.warning(f"{self.context.log_prefix} at_user动作执行失败,回退到proactive_reply") + fallback_action = { + "action_type": "proactive_reply", + "action_data": {"topic": trigger_event.reason}, + "action_message": action_message + } + await self._generate_proactive_content_and_send(fallback_action, trigger_event) - if action_type == "proactive_reply": - await self._generate_proactive_content_and_send(action_result) - elif action_type != "do_nothing": - logger.warning(f"{self.context.log_prefix} 主动思考返回了未知的动作类型: {action_type}") else: - # 如果规划结果是“什么都不做”,则记录日志 - logger.info(f"{self.context.log_prefix} 主动思考决策: 保持沉默") + # 对于其他来源的主动思考,正常调用规划器 + actions, _ = await self.cycle_processor.action_planner.plan(mode=ChatMode.PROACTIVE) + action_result = actions[0] if actions else {} + action_type = action_result.get("action_type") + if action_type == "proactive_reply": + await self._generate_proactive_content_and_send(action_result, trigger_event) + elif action_type not in ["do_nothing", "no_action"]: + await self.cycle_processor._handle_action( + action=action_result["action_type"], + reasoning=action_result.get("reasoning", ""), + action_data=action_result.get("action_data", {}), + cycle_timers={}, + thinking_id="", + action_message=action_result.get("action_message") + ) + else: + logger.info(f"{self.context.log_prefix} 主动思考决策: 保持沉默") + except Exception as e: logger.error(f"{self.context.log_prefix} 主动思考执行异常: {e}") logger.error(traceback.format_exc()) - async def _generate_proactive_content_and_send(self, action_result: Dict[str, Any]): + async def _get_reminder_context(self, message_id: str) -> str: + """获取提醒消息的上下文""" + try: + # 只获取那一条消息 + message_record = await db_get(Messages, {"message_id": message_id}, single_result=True) + if message_record: + # 使用 build_readable_messages_with_id 来格式化单条消息 + chat_context_block, _ = build_readable_messages_with_id(messages=[message_record]) + return chat_context_block + return "无法加载相关的聊天记录。" + except Exception as e: + logger.error(f"{self.context.log_prefix} 获取提醒上下文失败: {e}") + return "无法加载相关的聊天记录。" + + async def _generate_proactive_content_and_send(self, action_result: Dict[str, Any], trigger_event: ProactiveTriggerEvent): """ 获取实时信息,构建最终的生成提示词,并生成和发送主动回复。 Args: action_result (Dict[str, Any]): 规划器返回的动作结果。 + trigger_event (ProactiveTriggerEvent): 触发事件。 """ try: topic = action_result.get("action_data", {}).get("topic", "随便聊聊") logger.info(f"{self.context.log_prefix} 主动思考确定主题: '{topic}'") - # 1. 获取日程信息 schedule_block = "你今天没有日程安排。" if global_config.planning_system.schedule_enable: if current_activity := schedule_manager.get_current_activity(): schedule_block = f"你当前正在:{current_activity}。" - # 2. 网络搜索 news_block = "暂时没有获取到最新资讯。" - try: - web_search_tool = tool_api.get_tool_instance("web_search") - if web_search_tool: - tool_args = {"query": topic, "max_results": 10} - # 调用工具,并传递参数 - search_result_dict = await web_search_tool.execute(**tool_args) - if search_result_dict and not search_result_dict.get("error"): - news_block = search_result_dict.get("content", "未能提取有效资讯。") + if trigger_event.source != "reminder_system": + try: + web_search_tool = tool_api.get_tool_instance("web_search") + if web_search_tool: + try: + search_result_dict = await web_search_tool.execute(search_query=topic, max_results=10) + except TypeError: + try: + search_result_dict = await web_search_tool.execute(keyword=topic, max_results=10) + except TypeError: + logger.warning(f"{self.context.log_prefix} 网络搜索工具参数不匹配,跳过搜索") + news_block = "跳过网络搜索。" + search_result_dict = None + + if search_result_dict and not search_result_dict.get("error"): + news_block = search_result_dict.get("content", "未能提取有效资讯。") + elif search_result_dict: + logger.warning(f"{self.context.log_prefix} 网络搜索返回错误: {search_result_dict.get('error')}") else: - logger.warning(f"{self.context.log_prefix} 网络搜索返回错误: {search_result_dict.get('error')}") - else: - logger.warning(f"{self.context.log_prefix} 未找到 web_search 工具实例。") - except Exception as e: - logger.error(f"{self.context.log_prefix} 主动思考时网络搜索失败: {e}") + logger.warning(f"{self.context.log_prefix} 未找到 web_search 工具实例。") + except Exception as e: + logger.error(f"{self.context.log_prefix} 主动思考时网络搜索失败: {e}") - # 3. 获取最新的聊天上下文 - message_list = get_raw_msg_before_timestamp_with_chat( - chat_id=self.context.stream_id, - timestamp=time.time(), - limit=int(global_config.chat.max_context_size * 0.3), + if trigger_event.source == "reminder_system" and trigger_event.related_message_id: + chat_context_block = await self._get_reminder_context(trigger_event.related_message_id) + else: + message_list = get_raw_msg_before_timestamp_with_chat( + chat_id=self.context.stream_id, + timestamp=time.time(), + limit=int(global_config.chat.max_context_size * 0.3), + ) + chat_context_block, _ = build_readable_messages_with_id(messages=message_list) + + from src.llm_models.utils_model import LLMRequest + from src.config.config import model_config + + bot_name = global_config.bot.nickname + + confirmation_prompt = f"""# 主动回复二次确认 + +## 基本信息 +你的名字是{bot_name},准备主动发起关于"{topic}"的话题。 + +## 最近的聊天内容 +{chat_context_block} + +## 合理判断标准 +请检查以下条件,如果**大部分条件都合理**就可以回复: + +1. **时间合理性**:当前时间是否在深夜(凌晨2点-6点)这种不适合主动聊天的时段? +2. **内容价值**:这个话题"{topic}"是否有意义,不是完全无关紧要的内容? +3. **重复避免**:你准备说的话题是否与最近2条消息明显重复? +4. **自然性**:在当前上下文中主动提起这个话题是否自然合理? + +## 输出要求 +如果判断应该跳过(比如深夜时段、完全无意义话题、明显重复内容),输出:SKIP_PROACTIVE_REPLY +其他情况都应该输出:PROCEED_TO_REPLY + +请严格按照上述格式输出,不要添加任何解释。""" + + planner_llm = LLMRequest( + model_set=model_config.model_task_config.planner, + request_type="planner" ) - chat_context_block, _ = build_readable_messages_with_id(messages=message_list) - - # 4. 构建最终的生成提示词 + + confirmation_result, _ = await planner_llm.generate_response_async(prompt=confirmation_prompt) + + if not confirmation_result or "SKIP_PROACTIVE_REPLY" in confirmation_result: + logger.info(f"{self.context.log_prefix} 决策模型二次确认决定跳过主动回复") + return + bot_name = global_config.bot.nickname personality = global_config.personality identity_block = ( @@ -200,29 +322,30 @@ class ProactiveThinker: ## 你今天的日程安排 {schedule_block} -## 关于你准备讨论的话题“{topic}”的最新信息 +## 关于你准备讨论的话题"{topic}"的最新信息 {news_block} ## 最近的聊天内容 {chat_context_block} ## 任务 -你之前决定要发起一个关于“{topic}”的对话。现在,请结合以上所有信息,自然地开启这个话题。 +你现在想要主动说些什么。话题是"{topic}",但这只是一个参考方向。 + +根据最近的聊天内容,你可以: +- 如果是想关心朋友,就自然地询问他们的情况 +- 如果想起了之前的话题,就问问后来怎么样了 +- 如果有什么想分享的想法,就自然地开启话题 +- 如果只是想闲聊,就随意地说些什么 ## 要求 -- 你的发言要听起来像是自发的,而不是在念报告。 -- 巧妙地将日程安排或最新信息融入到你的开场白中。 -- 风格要符合你的角色设定。 -- 直接输出你想要说的内容,不要包含其他额外信息。 +- 像真正的朋友一样,自然地表达关心或好奇 +- 不要过于正式,要口语化和亲切 +- 结合你的角色设定,保持温暖的风格 +- 直接输出你想说的话,不要解释为什么要说 -你的回复应该: -1. 可以分享你的看法、提出相关问题,或者开个合适的玩笑。 -2. 目的是让对话更有趣、更深入。 -3. 不要浮夸,不要夸张修辞,不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。 -最终请输出一条简短、完整且口语化的回复。 +请输出一条简短、自然的主动发言。 """ - # 5. 调用生成器API并发送 response_text = await generator_api.generate_response_custom( chat_stream=self.context.chat_stream, prompt=final_prompt, diff --git a/src/chat/chat_loop/proactive/smart_reminder_analyzer.py b/src/chat/chat_loop/proactive/smart_reminder_analyzer.py new file mode 100644 index 000000000..5678270be --- /dev/null +++ b/src/chat/chat_loop/proactive/smart_reminder_analyzer.py @@ -0,0 +1,260 @@ +""" +智能提醒分析器 + +使用LLM分析用户消息,识别提醒请求并提取时间和内容信息 +""" + +import re +import json +from datetime import datetime, timedelta +from typing import Optional + +from src.common.logger import get_logger +from src.llm_models.utils_model import LLMRequest +from src.config.config import model_config + +logger = get_logger("smart_reminder") + + +class ReminderEvent: + """提醒事件数据类""" + def __init__(self, user_id: str, reminder_time: datetime, content: str, confidence: float): + self.user_id = user_id + self.reminder_time = reminder_time + self.content = content + self.confidence = confidence + + def __repr__(self): + return f"ReminderEvent(user_id={self.user_id}, time={self.reminder_time}, content={self.content}, confidence={self.confidence})" + + def to_dict(self): + return { + 'user_id': self.user_id, + 'reminder_time': self.reminder_time.isoformat(), + 'content': self.content, + 'confidence': self.confidence + } + + +class SmartReminderAnalyzer: + """智能提醒分析器""" + + def __init__(self): + self.confidence_threshold = 0.7 + # 使用规划器模型进行分析 + self.analyzer_llm = LLMRequest( + model_set=model_config.model_task_config.utils_small, + request_type="reminder_analyzer" + ) + + async def analyze_message(self, user_id: str, message: str) -> Optional[ReminderEvent]: + """分析消息是否包含提醒请求 + + Args: + user_id: 用户ID + message: 用户消息内容 + + Returns: + ReminderEvent对象,如果没有检测到提醒请求则返回None + """ + if not message or len(message.strip()) == 0: + return None + + logger.debug(f"分析消息中的提醒请求: {message}") + + # 使用LLM分析消息 + analysis_result = await self._analyze_with_llm(message) + + if not analysis_result or analysis_result.get('confidence', 0) < 0.5: # 降低置信度阈值 + return None + + try: + # 解析时间 + reminder_time = self._parse_relative_time(analysis_result['relative_time']) + if not reminder_time: + return None + + # 创建提醒事件 + reminder_event = ReminderEvent( + user_id=user_id, + reminder_time=reminder_time, + content=analysis_result.get('content', '提醒'), + confidence=analysis_result['confidence'] + ) + + logger.info(f"检测到提醒请求: {reminder_event}") + return reminder_event + + except Exception as e: + logger.error(f"创建提醒事件失败: {e}") + return None + + async def _analyze_with_llm(self, message: str) -> Optional[dict]: + """使用LLM分析消息中的提醒请求""" + try: + prompt = f"""分析以下消息是否包含提醒请求。 + +消息: {message} +当前时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} + +请判断用户是否想要设置提醒,如果是,请提取: +1. 是否包含提醒请求 (has_reminder: true/false) +2. 置信度 (confidence: 0.0-1.0) +3. 相对时间表达 (relative_time: 如"3分钟后", "2小时后") +4. 提醒内容 (content: 提醒的具体内容) +5. 分析原因 (reasoning: 判断理由) + +请以JSON格式输出: +{{ + "has_reminder": true/false, + "confidence": 0.0-1.0, + "relative_time": "时间表达", + "content": "提醒内容", + "reasoning": "判断理由" +}}""" + + response, _ = await self.analyzer_llm.generate_response_async(prompt=prompt) + if not response: + return None + + # 解析JSON响应,处理可能的markdown包装 + try: + # 清理响应文本 + cleaned_response = response.strip() + + # 移除markdown代码块包装 + if cleaned_response.startswith('```json'): + cleaned_response = cleaned_response[7:] # 移除 ```json + elif cleaned_response.startswith('```'): + cleaned_response = cleaned_response[3:] # 移除 ``` + + if cleaned_response.endswith('```'): + cleaned_response = cleaned_response[:-3] # 移除结尾的 ``` + + cleaned_response = cleaned_response.strip() + + # 解析JSON + result = json.loads(cleaned_response) + if result.get('has_reminder', False): + logger.info(f"LLM分析结果: {result}") + return result + except json.JSONDecodeError as e: + logger.error(f"LLM响应JSON解析失败: {response}, Error: {e}") + # 尝试使用更宽松的JSON修复 + try: + import re + # 提取JSON部分的正则表达式 + json_match = re.search(r'\{.*\}', cleaned_response, re.DOTALL) + if json_match: + json_str = json_match.group() + result = json.loads(json_str) + if result.get('has_reminder', False): + logger.info(f"备用解析成功: {result}") + return result + except Exception as fallback_error: + logger.error(f"备用JSON解析也失败: {fallback_error}") + + except Exception as e: + logger.error(f"LLM分析失败: {e}") + + return None + + def _parse_relative_time(self, time_expr: str) -> Optional[datetime]: + """解析时间表达式(支持相对时间和绝对时间)""" + try: + now = datetime.now() + + # 1. 匹配相对时间:X分钟后,包括中文数字 + # 先尝试匹配阿拉伯数字 + minutes_match = re.search(r'(\d+)\s*分钟后', time_expr) + if minutes_match: + minutes = int(minutes_match.group(1)) + result = now + timedelta(minutes=minutes) + logger.info(f"相对时间解析结果: timedelta(minutes={minutes}) -> {result}") + return result + + # 匹配中文数字分钟 + chinese_minutes_patterns = [ + (r'一分钟后', 1), (r'二分钟后', 2), (r'两分钟后', 2), (r'三分钟后', 3), (r'四分钟后', 4), (r'五分钟后', 5), + (r'六分钟后', 6), (r'七分钟后', 7), (r'八分钟后', 8), (r'九分钟后', 9), (r'十分钟后', 10), + (r'十一分钟后', 11), (r'十二分钟后', 12), (r'十三分钟后', 13), (r'十四分钟后', 14), (r'十五分钟后', 15), + (r'二十分钟后', 20), (r'三十分钟后', 30), (r'四十分钟后', 40), (r'五十分钟后', 50), (r'六十分钟后', 60) + ] + + for pattern, minutes in chinese_minutes_patterns: + if re.search(pattern, time_expr): + result = now + timedelta(minutes=minutes) + logger.info(f"中文时间解析结果: {pattern} -> {minutes}分钟 -> {result}") + return result + + # 2. 匹配相对时间:X小时后 + hours_match = re.search(r'(\d+)\s*小时后', time_expr) + if hours_match: + hours = int(hours_match.group(1)) + result = now + timedelta(hours=hours) + logger.info(f"相对时间解析结果: timedelta(hours={hours})") + return result + + # 3. 匹配相对时间:X秒后 + seconds_match = re.search(r'(\d+)\s*秒后', time_expr) + if seconds_match: + seconds = int(seconds_match.group(1)) + result = now + timedelta(seconds=seconds) + logger.info(f"相对时间解析结果: timedelta(seconds={seconds})") + return result + + # 4. 匹配明天+具体时间:明天下午2点、明天上午10点 + tomorrow_match = re.search(r'明天.*?(\d{1,2})\s*[点时]', time_expr) + if tomorrow_match: + hour = int(tomorrow_match.group(1)) + # 如果是下午且小于12,加12小时 + if '下午' in time_expr and hour < 12: + hour += 12 + elif '上午' in time_expr and hour == 12: + hour = 0 + + tomorrow = now + timedelta(days=1) + result = tomorrow.replace(hour=hour, minute=0, second=0, microsecond=0) + logger.info(f"绝对时间解析结果: 明天{hour}点") + return result + + # 5. 匹配今天+具体时间:今天下午3点、今天晚上8点 + today_match = re.search(r'今天.*?(\d{1,2})\s*[点时]', time_expr) + if today_match: + hour = int(today_match.group(1)) + # 如果是下午且小于12,加12小时 + if '下午' in time_expr and hour < 12: + hour += 12 + elif '晚上' in time_expr and hour < 12: + hour += 12 + elif '上午' in time_expr and hour == 12: + hour = 0 + + result = now.replace(hour=hour, minute=0, second=0, microsecond=0) + # 如果时间已过,设为明天 + if result <= now: + result += timedelta(days=1) + + logger.info(f"绝对时间解析结果: 今天{hour}点") + return result + + # 6. 匹配纯数字时间:14点、2点 + pure_time_match = re.search(r'(\d{1,2})\s*[点时]', time_expr) + if pure_time_match: + hour = int(pure_time_match.group(1)) + result = now.replace(hour=hour, minute=0, second=0, microsecond=0) + # 如果时间已过,设为明天 + if result <= now: + result += timedelta(days=1) + + logger.info(f"绝对时间解析结果: {hour}点") + return result + + except Exception as e: + logger.error(f"时间解析失败: {time_expr}, Error: {e}") + + return None + + +# 全局智能提醒分析器实例 +smart_reminder_analyzer = SmartReminderAnalyzer() \ No newline at end of file diff --git a/src/chat/heart_flow/heartflow_message_processor.py b/src/chat/heart_flow/heartflow_message_processor.py index 34d02281c..a0d18c44c 100644 --- a/src/chat/heart_flow/heartflow_message_processor.py +++ b/src/chat/heart_flow/heartflow_message_processor.py @@ -2,6 +2,7 @@ import asyncio import re import math import traceback +from datetime import datetime from typing import Tuple, TYPE_CHECKING @@ -16,6 +17,7 @@ from src.chat.utils.chat_message_builder import replace_user_references_sync from src.common.logger import get_logger from src.person_info.relationship_manager import get_relationship_manager from src.mood.mood_manager import mood_manager +from src.chat.message_receive.chat_stream import get_chat_manager if TYPE_CHECKING: from src.chat.heart_flow.sub_heartflow import SubHeartflow @@ -116,10 +118,11 @@ class HeartFCMessageReceiver: 主要流程: 1. 消息解析与初始化 - 2. 消息缓冲处理 - 3. 过滤检查 - 4. 兴趣度计算 - 5. 关系处理 + 2. 智能提醒分析 + 3. 消息缓冲处理 + 4. 过滤检查 + 5. 兴趣度计算 + 6. 关系处理 Args: message_data: 原始消息字符串 @@ -129,7 +132,93 @@ class HeartFCMessageReceiver: userinfo = message.message_info.user_info chat = message.chat_stream - # 2. 兴趣度计算与更新 + # 2. 智能提醒分析 - 检查用户是否请求提醒 + from src.chat.chat_loop.proactive.smart_reminder_analyzer import smart_reminder_analyzer + from src.chat.chat_loop.proactive.event_scheduler import event_scheduler + + try: + reminder_event = await smart_reminder_analyzer.analyze_message( + userinfo.user_id, # type: ignore + message.processed_plain_text + ) + if reminder_event: + logger.info(f"检测到提醒请求: {reminder_event}") + + # 创建提醒回调函数 + async def reminder_callback(metadata): + """提醒执行回调函数 - 触发完整的主动思考流程""" + try: + # 获取对应的subheartflow实例 + from src.chat.heart_flow.heartflow import heartflow + + subflow = await heartflow.get_or_create_subheartflow(chat.stream_id) + if not subflow: + logger.error(f"无法获取subheartflow实例: {chat.stream_id}") + return + + # 创建主动思考事件,触发完整的思考流程 + from src.chat.chat_loop.proactive.events import ProactiveTriggerEvent + + reminder_content = metadata.get('content', '提醒时间到了') + event = ProactiveTriggerEvent( + source="reminder_system", + reason=f"定时提醒:{reminder_content}", + metadata={ + "reminder_text": reminder_content, + "trigger_time": datetime.now().isoformat() + } + ) + + # 通过subflow的HeartFChatting实例触发主动思考 + await subflow.heart_fc_instance.proactive_thinker.think(event) + + logger.info(f"已触发提醒的主动思考,内容: {reminder_content}") + + except Exception as callback_error: + logger.error(f"执行提醒回调失败: {callback_error}") + import traceback + logger.error(traceback.format_exc()) + + # Fallback: 如果主动思考失败,直接发送提醒消息 + try: + reminder_content = metadata.get('content', '提醒时间到了') + await text_to_stream( + text=f"⏰ 提醒:{reminder_content}", + stream_id=chat.stream_id, + typing=False + ) + logger.info(f"Fallback提醒消息已发送: {reminder_content}") + except Exception as fallback_error: + logger.error(f"Fallback提醒也失败了: {fallback_error}") + + # 调度提醒事件 + event_id = f"reminder_{reminder_event.user_id}_{int(reminder_event.reminder_time.timestamp())}" + metadata = { + "type": "reminder", + "user_id": reminder_event.user_id, + "chat_id": chat.stream_id, + "content": reminder_event.content, + "confidence": reminder_event.confidence, + "created_at": datetime.now().isoformat(), + "original_message_id": message.message_info.message_id + } + + success = await event_scheduler.schedule_event( + event_id=event_id, + trigger_time=reminder_event.reminder_time, + callback=reminder_callback, + metadata=metadata + ) + + if success: + logger.info(f"提醒事件调度成功: {event_id}") + else: + logger.error(f"提醒事件调度失败: {event_id}") + + except Exception as e: + logger.error(f"智能提醒分析失败: {e}") + + # 3. 兴趣度计算与更新 interested_rate, is_mentioned, keywords = await _calculate_interest(message) message.interest_value = interested_rate message.is_mentioned = is_mentioned 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 9b071d954..4bf80f2b9 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -75,9 +75,9 @@ def init_prompt(): {action_options_text} -你必须从上面列出的可用action中选择一个,并说明触发action的消息id(不是消息原文)和选择该action的原因。消息id格式:m+数字 +你必须从上面列出的可用action中选择一个或多个,并说明触发action的消息id(不是消息原文)和选择该action的原因。消息id格式:m+数字 -请根据动作示例,以严格的 JSON 格式输出,不要输出markdown格式```json等内容,直接输出且仅包含 JSON 内容: +请根据动作示例,以严格的 JSON 格式输出,返回一个包含所有选定动作的JSON列表。如果只选择一个动作,也请将其包含在列表中。如果没有任何合适的动作,返回一个空列表[]。不要输出markdown格式```json等内容,直接输出且仅包含 JSON 列表内容: """, "planner_prompt", ) @@ -102,29 +102,38 @@ def init_prompt(): {actions_before_now_block} ## 任务 -基于以上所有信息(特别是最近的聊天内容),分析当前情况,决定是否适合主动开启一个**新的、但又与当前氛围相关**的话题。 +你现在要决定是否主动说些什么。就像一个真实的人一样,有时候会突然想起之前聊到的话题,或者对朋友的近况感到好奇,想主动询问或关心一下。 + +请基于聊天内容,用你的判断力来决定是否要主动发言。不要按照固定规则,而是像人类一样自然地思考: +- 是否想起了什么之前提到的事情,想问问后来怎么样了? +- 是否注意到朋友提到了什么值得关心的事情? +- 是否有什么话题突然想到,觉得现在聊聊很合适? +- 或者觉得现在保持沉默更好? ## 可用动作 动作:proactive_reply -动作描述:在当前对话的基础上,主动发起一个新的对话,分享一个有趣的想法、见闻或者对未来的计划。 -- 当你觉得可以说些什么来活跃气氛,并且内容与当前聊天氛围不冲突时 -- 当你有一些新的想法或计划想要分享,并且可以自然地衔接当前话题时 +动作描述:主动发起对话,可以是关心朋友、询问近况、延续之前的话题,或分享想法。 +- 当你突然想起之前的话题,想询问进展时 +- 当你想关心朋友的情况时 +- 当你有什么想法想分享时 +- 当你觉得现在是个合适的聊天时机时 {{ "action": "proactive_reply", - "reason": "决定主动发起对话的具体原因", - "topic": "你想要发起对话的主题或内容(需要简洁)" + "reason": "你决定主动发言的具体原因", + "topic": "你想说的内容主题(简洁描述)" }} 动作:do_nothing -动作描述:保持沉默,不主动发起任何动作或对话。 -- 当你分析了所有信息后,觉得当前不是一个发起互动的好时机时 -- 当最近的聊天内容很连贯,你的插入会打断别人时 +动作描述:保持沉默,不主动发起对话。 +- 当你觉得现在不是合适的时机时 +- 当最近已经说得够多了时 +- 当对话氛围不适合插入时 {{ "action": "do_nothing", - "reason":"决定保持沉默的具体原因" + "reason": "决定保持沉默的原因" }} -你必须从上面列出的可用action中选择一个。 +你必须从上面列出的可用action中选择一个。要像真人一样自然地思考和决策。 请以严格的 JSON 格式输出,且仅包含 JSON 内容: """, "proactive_planner_prompt", @@ -144,36 +153,6 @@ def init_prompt(): "action_prompt", ) - Prompt( - """ -{name_block} - -{chat_context_description},{time_block},现在请你根据以下聊天内容,选择一个或多个合适的action。如果没有合适的action,请选择no_action。, -{chat_content_block} - -**要求** -1.action必须符合使用条件,如果符合条件,就选择 -2.如果聊天内容不适合使用action,即使符合条件,也不要使用 -3.{moderation_prompt} -4.请注意如果相同的内容已经被执行,请不要重复执行 -这是你最近执行过的动作: -{actions_before_now_block} - -**可用的action** - -no_action:不选择任何动作 -{{ - "action": "no_action", - "reason":"不动作的原因" -}} - -{action_options_text} - -请选择,并说明触发action的消息id和选择该action的原因。消息id格式:m+数字 -请根据动作示例,以严格的 JSON 格式输出,且仅包含 JSON 内容: -""", - "sub_planner_prompt", - ) class ActionPlanner: @@ -186,10 +165,6 @@ class ActionPlanner: self.planner_llm = LLMRequest( model_set=model_config.model_task_config.planner, request_type="planner" ) - # --- 小脑 (新增) --- - self.planner_small_llm = LLMRequest( - model_set=model_config.model_task_config.planner_small, request_type="planner_small" - ) self.last_obs_time_mark = 0.0 @@ -309,7 +284,7 @@ class ActionPlanner: action_data = {k: v for k, v in action_json.items() if k not in ["action", "reason"]} target_message = None - if action != "no_action": + if action not in ["no_action", "no_reply"]: if target_message_id := action_json.get("target_message_id"): target_message = self.find_message_by_id(target_message_id, message_id_list) if target_message is None: @@ -319,7 +294,7 @@ class ActionPlanner: logger.warning(f"{self.log_prefix}动作'{action}'缺少target_message_id") available_action_names = [name for name, _ in current_available_actions] - if action not in ["no_action", "reply"] and action not in available_action_names: + if action not in ["no_action", "no_reply", "reply"] and action not in available_action_names: logger.warning( f"{self.log_prefix}LLM 返回了当前不可用或无效的动作: '{action}' (可用: {available_action_names}),将强制使用 'no_action'" ) @@ -361,111 +336,17 @@ class ActionPlanner: # 如果都是 no_action,则返回一个包含第一个 no_action 的列表,以保留 reason return action_list[:1] if action_list else [] - async def sub_plan( - self, - action_list: list, # 使用 planner.py 的 list of tuple - chat_content_block: str, - message_id_list: list, # 使用 planner.py 的 list of dict - is_group_chat: bool = False, - chat_target_info: Optional[dict] = None, - ) -> List[Dict[str, Any]]: - """ - [注释] "小脑"规划器。接收一小组actions,使用轻量级LLM判断其中哪些应该被触发。 - 这是一个独立的、并行的思考单元。返回一个包含action字典的列表。 - """ - try: - actions_before_now = get_actions_by_timestamp_with_chat( - chat_id=self.chat_id, - timestamp_start=time.time() - 1200, - timestamp_end=time.time(), - limit=20, - ) - action_names_in_list = [name for name, _ in action_list] - filtered_actions = [ - record for record in actions_before_now if record.get("action_name") in action_names_in_list - ] - actions_before_now_block = build_readable_actions(actions=filtered_actions) - - chat_context_description = "你现在正在一个群聊中" - if not is_group_chat and chat_target_info: - chat_target_name = chat_target_info.get("person_name") or chat_target_info.get("user_nickname") or "对方" - chat_context_description = f"你正在和 {chat_target_name} 私聊" - - action_options_block = "" - for using_actions_name, using_actions_info in action_list: - param_text = "" - if using_actions_info.action_parameters: - param_text = "\n" + "\n".join( - f' "{p_name}":"{p_desc}"' - for p_name, p_desc in using_actions_info.action_parameters.items() - ) - require_text = "\n".join(f"- {req}" for req in using_actions_info.action_require) - using_action_prompt = await global_prompt_manager.get_prompt_async("action_prompt") - action_options_block += using_action_prompt.format( - action_name=using_actions_name, - action_description=using_actions_info.description, - action_parameters=param_text, - action_require=require_text, - ) - - moderation_prompt_block = "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。" - time_block = f"当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" - bot_name = global_config.bot.nickname - bot_nickname = f",也有人叫你{','.join(global_config.bot.alias_names)}" if global_config.bot.alias_names else "" - name_block = f"你的名字是{bot_name}{bot_nickname},请注意哪些是你自己的发言。" - - planner_prompt_template = await global_prompt_manager.get_prompt_async("sub_planner_prompt") - prompt = planner_prompt_template.format( - time_block=time_block, - chat_context_description=chat_context_description, - chat_content_block=chat_content_block, - actions_before_now_block=actions_before_now_block, - action_options_text=action_options_block, - moderation_prompt=moderation_prompt_block, - name_block=name_block, - ) - except Exception as e: - logger.error(f"构建小脑提示词时出错: {e}\n{traceback.format_exc()}") - return [{"action_type": "no_action", "reasoning": f"构建小脑Prompt时出错: {e}"}] - - action_dicts: List[Dict[str, Any]] = [] - try: - llm_content, (reasoning_content, _, _) = await self.planner_small_llm.generate_response_async(prompt=prompt) - if global_config.debug.show_prompt: - logger.info(f"{self.log_prefix}小脑原始提示词: {prompt}") - logger.info(f"{self.log_prefix}小脑原始响应: {llm_content}") - else: - logger.debug(f"{self.log_prefix}小脑原始响应: {llm_content}") - - if llm_content: - parsed_json = orjson.loads(repair_json(llm_content)) - if isinstance(parsed_json, list): - for item in parsed_json: - if isinstance(item, dict): - action_dicts.extend(self._parse_single_action(item, message_id_list, action_list)) - elif isinstance(parsed_json, dict): - action_dicts.extend(self._parse_single_action(parsed_json, message_id_list, action_list)) - - except Exception as e: - logger.warning(f"{self.log_prefix}解析小脑响应JSON失败: {e}. LLM原始输出: '{llm_content}'") - action_dicts.append({"action_type": "no_action", "reasoning": f"解析小脑响应失败: {e}"}) - - if not action_dicts: - action_dicts.append({"action_type": "no_action", "reasoning": "小脑未返回有效action"}) - - return action_dicts async def plan( self, mode: ChatMode = ChatMode.FOCUS, loop_start_time: float = 0.0, available_actions: Optional[Dict[str, ActionInfo]] = None, + pseudo_message: Optional[str] = None, ) -> Tuple[List[Dict[str, Any]], Optional[Dict[str, Any]]]: """ [注释] "大脑"规划器。 - 1. 启动多个并行的"小脑"(sub_plan)来决定是否执行具体的actions。 - 2. 自己(大脑)则专注于决定是否进行聊天回复(reply)。 - 3. 整合大脑和小脑的决策,返回最终要执行的动作列表。 + 统一决策是否进行聊天回复(reply)以及执行哪些actions。 """ # --- 1. 准备上下文信息 --- message_list_before_now = get_raw_msg_before_timestamp_with_chat( @@ -473,7 +354,6 @@ class ActionPlanner: timestamp=time.time(), limit=int(global_config.chat.max_context_size * 0.6), ) - # 大脑使用较长的上下文 chat_content_block, message_id_list = build_readable_messages_with_id( messages=message_list_before_now, timestamp_mode="normal", @@ -481,77 +361,21 @@ class ActionPlanner: truncate=True, show_actions=True, ) - # 小脑使用较短、较新的上下文 - message_list_before_now_short = message_list_before_now[-int(global_config.chat.max_context_size * 0.3) :] - chat_content_block_short, message_id_list_short = build_readable_messages_with_id( - messages=message_list_before_now_short, - timestamp_mode="normal", - truncate=False, - show_actions=False, - ) + if pseudo_message: + chat_content_block += f"\n[m99] 刚刚, 用户: {pseudo_message}" self.last_obs_time_mark = time.time() is_group_chat, chat_target_info, current_available_actions = self.get_necessary_info() if available_actions is None: available_actions = current_available_actions - # --- 2. 启动小脑并行思考 --- - all_sub_planner_results: List[Dict[str, Any]] = [] + # --- 2. 大脑统一决策 --- + final_actions: List[Dict[str, Any]] = [] try: - sub_planner_actions: Dict[str, ActionInfo] = {} - for action_name, action_info in available_actions.items(): - - if action_info.activation_type in [ActionActivationType.LLM_JUDGE, ActionActivationType.ALWAYS]: - sub_planner_actions[action_name] = action_info - elif action_info.activation_type == ActionActivationType.RANDOM: - if random.random() < action_info.random_activation_probability: - sub_planner_actions[action_name] = action_info - elif action_info.activation_type == ActionActivationType.KEYWORD: - if any(keyword in chat_content_block_short for keyword in action_info.activation_keywords): - sub_planner_actions[action_name] = action_info - - if sub_planner_actions: - sub_planner_actions_num = len(sub_planner_actions) - planner_size_config = global_config.chat.planner_size - sub_planner_size = int(planner_size_config) + ( - 1 if random.random() < planner_size_config - int(planner_size_config) else 0 - ) - sub_planner_num = math.ceil(sub_planner_actions_num / sub_planner_size) - logger.info(f"{self.log_prefix}使用{sub_planner_num}个小脑进行思考 (尺寸: {sub_planner_size})") - - action_items = list(sub_planner_actions.items()) - random.shuffle(action_items) - sub_planner_lists = [action_items[i::sub_planner_num] for i in range(sub_planner_num)] - - sub_plan_tasks = [ - self.sub_plan( - action_list=action_group, - chat_content_block=chat_content_block_short, - message_id_list=message_id_list_short, - is_group_chat=is_group_chat, - chat_target_info=chat_target_info, - ) - for action_group in sub_planner_lists - ] - sub_plan_results = await asyncio.gather(*sub_plan_tasks) - for sub_result in sub_plan_results: - all_sub_planner_results.extend(sub_result) - - sub_actions_str = ", ".join( - a["action_type"] for a in all_sub_planner_results if a["action_type"] != "no_action" - ) or "no_action" - logger.info(f"{self.log_prefix}小脑决策: [{sub_actions_str}]") - - except Exception as e: - logger.error(f"{self.log_prefix}小脑调度过程中出错: {e}\n{traceback.format_exc()}") - - # --- 3. 大脑独立思考是否回复 --- - action, reasoning, action_data, target_message = "no_reply", "大脑初始化默认", {}, None - try: - prompt, _ = await self.build_planner_prompt( + prompt, used_message_id_list = await self.build_planner_prompt( is_group_chat=is_group_chat, chat_target_info=chat_target_info, - current_available_actions={}, + current_available_actions=available_actions, mode=mode, chat_content_block_override=chat_content_block, message_id_list_override=message_id_list, @@ -560,72 +384,54 @@ class ActionPlanner: if llm_content: parsed_json = orjson.loads(repair_json(llm_content)) - parsed_json = parsed_json[-1] if isinstance(parsed_json, list) and parsed_json else parsed_json + + # 确保处理的是列表 if isinstance(parsed_json, dict): - action = parsed_json.get("action", "no_reply") - reasoning = parsed_json.get("reason", "未提供原因") - action_data = {k: v for k, v in parsed_json.items() if k not in ["action", "reason"]} - if action != "no_reply": - if target_id := parsed_json.get("target_message_id"): - target_message = self.find_message_by_id(target_id, message_id_list) - if not target_message: - target_message = self.get_latest_message(message_id_list) - logger.info(f"{self.log_prefix}大脑决策: [{action}]") + parsed_json = [parsed_json] + + if isinstance(parsed_json, list): + for item in parsed_json: + if isinstance(item, dict): + final_actions.extend(self._parse_single_action(item, used_message_id_list, list(available_actions.items()))) + + # 如果是私聊且开启了强制回复,并且没有任何回复性action,则强制添加reply + if not is_group_chat and global_config.chat.force_reply_private: + has_reply_action = any(a.get("action_type") == "reply" for a in final_actions) + if not has_reply_action: + final_actions.append({ + "action_type": "reply", + "reasoning": "私聊强制回复", + "action_data": {}, + "action_message": self.get_latest_message(message_id_list), + "available_actions": available_actions, + }) + logger.info(f"{self.log_prefix}私聊强制回复已触发,添加 'reply' 动作") + + logger.info(f"{self.log_prefix}大脑决策: {[a.get('action_type') for a in final_actions]}") except Exception as e: logger.error(f"{self.log_prefix}大脑处理过程中发生意外错误: {e}\n{traceback.format_exc()}") - action, reasoning = "no_reply", f"大脑处理错误: {e}" - - # --- 4. 整合大脑和小脑的决策 --- - # 如果是私聊且开启了强制回复,则将no_reply强制改为reply - if not is_group_chat and global_config.chat.force_reply_private and action == "no_reply": - action = "reply" - reasoning = "私聊强制回复" - logger.info(f"{self.log_prefix}私聊强制回复已触发,将动作从 'no_reply' 修改为 'reply'") - - is_parallel = True - for info in all_sub_planner_results: - action_type = info.get("action_type") - if action_type and action_type not in ["no_action", "no_reply"]: - action_info = available_actions.get(action_type) - if action_info and not action_info.parallel_action: - is_parallel = False - break - - action_data["loop_start_time"] = loop_start_time - final_actions: List[Dict[str, Any]] = [] - - if is_parallel: - logger.info(f"{self.log_prefix}决策模式: 大脑与小脑并行") - if action not in ["no_action", "no_reply"]: - final_actions.append( - { - "action_type": action, - "reasoning": reasoning, - "action_data": action_data, - "action_message": target_message, - "available_actions": available_actions, - } - ) - final_actions.extend(all_sub_planner_results) - else: - logger.info(f"{self.log_prefix}决策模式: 小脑优先 (检测到非并行action)") - final_actions.extend(all_sub_planner_results) + final_actions.append({"action_type": "no_action", "reasoning": f"大脑处理错误: {e}"}) + # --- 3. 后处理 --- final_actions = self._filter_no_actions(final_actions) if not final_actions: final_actions = [ { "action_type": "no_action", - "reasoning": "所有规划器都选择不执行动作", + "reasoning": "规划器选择不执行动作", "action_data": {}, "action_message": None, "available_actions": available_actions } ] - final_target_message = target_message - if not final_target_message and final_actions: - final_target_message = next((act.get("action_message") for act in final_actions if act.get("action_message")), None) + final_target_message = next((act.get("action_message") for act in final_actions if act.get("action_message")), None) + + # 记录每个动作的原因 + for action_info in final_actions: + action_type = action_info.get("action_type", "N/A") + reasoning = action_info.get("reasoning", "无") + logger.info(f"{self.log_prefix}决策: [{action_type}],原因: {reasoning}") actions_str = ", ".join([a.get('action_type', 'N/A') for a in final_actions]) logger.info(f"{self.log_prefix}最终执行动作 ({len(final_actions)}): [{actions_str}]") 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/api_ada_configs.py b/src/config/api_ada_configs.py index cc25d0646..0b9d333c4 100644 --- a/src/config/api_ada_configs.py +++ b/src/config/api_ada_configs.py @@ -135,7 +135,6 @@ class ModelTaskConfig(ValidatedConfigBase): voice: TaskConfig = Field(..., description="语音识别模型配置") tool_use: TaskConfig = Field(..., description="专注工具使用模型配置") planner: TaskConfig = Field(..., description="规划模型配置") - planner_small: TaskConfig = Field(..., description="小脑(sub-planner)规划模型配置") embedding: TaskConfig = Field(..., description="嵌入模型配置") lpmm_entity_extract: TaskConfig = Field(..., description="LPMM实体提取模型配置") lpmm_rdf_build: TaskConfig = Field(..., description="LPMM RDF构建模型配置") diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 10bc3ae71..97facecc3 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -92,7 +92,6 @@ class ChatConfig(ValidatedConfigBase): default_factory=list, description="启用主动思考的群聊范围,格式:platform:group_id,为空则不限制" ) delta_sigma: int = Field(default=120, description="采用正态分布随机时间间隔") - planner_size: float = Field(default=5.0, ge=1.0, description="小脑(sub-planner)的尺寸,决定每个小脑处理多少个action") def get_current_talk_frequency(self, chat_stream_id: Optional[str] = None) -> float: """ diff --git a/src/main.py b/src/main.py index b0bc08677..9798bab3a 100644 --- a/src/main.py +++ b/src/main.py @@ -117,7 +117,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 @@ -291,7 +291,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 5e24458ef..7a80b8ab6 100644 --- a/src/plugins/built_in/at_user_plugin/plugin.py +++ b/src/plugins/built_in/at_user_plugin/plugin.py @@ -80,19 +80,12 @@ class AtAction(BaseAction): reply_to = f"{user_name}:{at_message}" extra_info = f"你需要艾特用户 {user_name} 并回复他们说: {at_message}" - from src.plugin_system.core.event_manager import event_manager - from src.plugin_system import EventType - # 触发post_llm - result = await event_manager.trigger_event(EventType.POST_LLM,plugin_name="SYSTEM") - if not result.all_continue_process(): - return False, f"被组件{result.get_summary().get("stopped_handlers","")}打断" - # 使用回复器生成回复 success, llm_response, prompt = await replyer.generate_reply_with_context( 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..73fdebd32 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.7.7" +version = "6.8.0" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -173,8 +173,6 @@ delta_sigma = 120 # 正态分布的标准差,控制时间间隔的随机程度 # 实验建议:试试 proactive_thinking_interval=0 + delta_sigma 非常大 的纯随机模式! # 结果保证:生成的间隔永远为正数(负数会取绝对值),最小1秒,最大24小时 -# --- 大脑/小脑 Planner 配置 --- -planner_size = 5.0 # 小脑(sub-planner)的尺寸,决定每个小脑处理多少个action。数值越小,并行度越高,但单个小脑的上下文越少。建议范围:3.0-8.0 [relationship] enable_relationship = true # 是否启用关系系统 diff --git a/template/model_config_template.toml b/template/model_config_template.toml index c5f2a2947..ea200accb 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.4" # 配置文件版本号迭代规则同bot_config.toml @@ -142,10 +142,6 @@ model_list = ["siliconflow-deepseek-v3"] temperature = 0.3 max_tokens = 800 -[model_task_config.planner_small] #决策(小脑):负责决定具体action的模型,建议使用速度快的小模型 -model_list = ["qwen3-30b"] -temperature = 0.5 -max_tokens = 800 [model_task_config.emotion] #负责麦麦的情绪变化 model_list = ["siliconflow-deepseek-v3"] @@ -185,7 +181,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 +191,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